Merge "Add a test for setting the reviewed flag with multiple patch sets"
diff --git a/.zuul.yaml b/.zuul.yaml
new file mode 100644
index 0000000..d6dbc34
--- /dev/null
+++ b/.zuul.yaml
@@ -0,0 +1,38 @@
+- job:
+ name: gerrit-base
+ parent: gerrit-setup
+ description: |
+ Base job for all Gerrit-related builds
+
+ This adds required projects needed for all Gerrit-related builds
+ (i.e., builds of Gerrit itself or plugins) on this branch.
+ required-projects:
+ - jgit
+
+- job:
+ name: gerrit-build
+ parent: gerrit-build-base
+ description: |
+ Build Gerrit
+
+ This builds Gerrit with the core plugins.
+ required-projects:
+ # This inherits from gerrit-base, so submodules listed above do
+ # not need to be repeated here.
+ - plugins/codemirror-editor
+ - plugins/commit-message-length-validator
+ - plugins/delete-project
+ - plugins/download-commands
+ - plugins/gitiles
+ - plugins/hooks
+ - plugins/plugin-manager
+ - plugins/replication
+ - plugins/reviewnotes
+ - plugins/singleusergroup
+ - plugins/webhooks
+ - polymer-bridges
+
+- project:
+ check:
+ jobs:
+ - gerrit-build
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 24da6320..80d84be 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -3142,7 +3142,7 @@
[[elasticsearch.numberOfShards]]elasticsearch.numberOfShards::
+
Sets the number of shards to use per index. Refer to the
-link:https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-concepts.html#getting-started-shards-and-replicas[
+link:https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#_static_index_settings[
Elasticsearch documentation,role=external,window=_blank] for details.
+
Defaults to 5 for Elasticsearch versions 5 and 6, and to 1 starting with Elasticsearch 7.
@@ -3150,11 +3150,19 @@
[[elasticsearch.numberOfReplicas]]elasticsearch.numberOfReplicas::
+
Sets the number of replicas to use per index. Refer to the
-link:https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-concepts.html#getting-started-shards-and-replicas[
+link:https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#dynamic-index-settings[
Elasticsearch documentation,role=external,window=_blank] for details.
+
Defaults to 1.
+[[elasticsearch.maxResultWindow]]elasticsearch.maxResultWindow::
++
+Sets the maximum value of `from + size` for searches to use per index. Refer to the
+link:https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#dynamic-index-settings[
+Elasticsearch documentation,role=external,window=_blank] for details.
++
+Defaults to 10000.
+
==== Elasticsearch Security
When security is enabled in Elasticsearch, the username and password must be provided.
@@ -5082,6 +5090,10 @@
The link:#schedule-configuration-interval[interval] for running
account deactivations.
+Note that the task will only be scheduled if the
+link:#autoUpdateAccountActiveStatus[auth.autoUpdateAccountActiveStatus]
+is set to true.
+
link:#schedule-configuration-examples[Schedule examples] can be found
in the link:#schedule-configuration[Schedule Configuration] section.
diff --git a/Documentation/dev-e2e-tests.txt b/Documentation/dev-e2e-tests.txt
index 9a62f01..30da8c5 100644
--- a/Documentation/dev-e2e-tests.txt
+++ b/Documentation/dev-e2e-tests.txt
@@ -1,29 +1,38 @@
:linkattrs:
-= Gerrit Code Review - End to end load tests
+= Gerrit Code Review - End to end tests
-This document provides a description of a Gerrit load test scenario implemented using the
-link:https://gatling.io/[Gatling,role=external,window=_blank] framework.
+This document provides descriptions of Gerrit end-to-end (`e2e`) test scenarios implemented using
+the link:https://gatling.io/[Gatling,role=external,window=_blank] framework.
Similar scenarios have been successfully used to compare performance of different Gerrit versions
-or study the Gerrit response under different load profiles.
+or study the Gerrit response under different load profiles. Although mostly for load, scenarios can
+either be for link:https://gatling.io/load-testing-continuous-integration/[load or functional,role=external,window=_blank]
+(e2e) testing purposes. Functional scenarios may then reuse this framework and Gatling's usability
+features such as its protocols (more below) and
+link:https://en.wikipedia.org/wiki/Domain-specific_language[DSL,role=external,window=_blank].
+
+That cross test-scope reusability applies to both Gerrit core scenarios and non-core ones, such as
+for Gerrit plugins or other potential extensions. End-to-end testing may then include scopes like
+feature integration, deployment, smoke (and load) testing. These load and functional test scopes
+should remain orthogonal to the unit and component (aka Gerrit `IT`-suffixed or `acceptance`) ones.
+The term `acceptance` though may still be coined by organizations to target e2e functional testing.
== What is Gatling?
-Gatling is a load testing tool which provides out of the box support for the HTTP protocol.
+Gatling is mostly a load testing tool which provides out of the box support for the HTTP protocol.
Documentation on how to write an HTTP load test can be found
link:https://gatling.io/docs/current/http/http_protocol/[here,role=external,window=_blank].
-
-However, in the scenario we are proposing, we are leveraging the
-link:https://github.com/GerritForge/gatling-git[Gatling Git extension,role=external,window=_blank]
-to run tests at Git protocol level.
+However, in the scenarios that were initially proposed, the
+link:https://github.com/GerritForge/gatling-git[Gatling Git extension,role=external,window=_blank] was
+leveraged to run tests at the Git protocol level.
Gatling is written in Scala, but the abstraction provided by the Gatling DSL makes the scenarios
implementation easy even without any Scala knowledge. The
link:https://gitenterprise.me/2019/12/20/stress-your-gerrit-with-gatling/[Stress your Gerrit with Gatling,role=external,window=_blank]
blog post has more introductory information.
-Examples of scenarios can be found in the `e2e-tests` directory. The files in that directory
-should be formatted using the mainstream
+Examples of scenarios can be found in the `e2e-tests` directory. The files in that directory should
+be formatted using the mainstream
link:https://plugins.jetbrains.com/plugin/1347-scala[Scala plugin for IntelliJ,role=external,window=_blank].
The latter is not mandatory but preferred for `sbt` and Scala IDE purposes in this project.
@@ -47,6 +56,14 @@
[warn] Credentials file ~/.sbt/sonatype_credentials does not exist
----
+The other warning below can be safely ignored, so far. Running the proposed `sbt evicted` command
+should only list `scala-java8-compat_2.12` as `[warn]`. The other dependency conflicts should show
+as `[info]`. All of the listed conflicts get usually resolved seamlessly or so.
+
+----
+[warn] There may be incompatibilities among your library dependencies; run 'evicted' to see detailed eviction warnings.
+----
+
Every `sbt` command can include an optional log level
link:https://www.scala-sbt.org/1.x/docs/Howto-Logging.html#Change+the+logging+level+globally[argument,role=external,window=_blank].
Below, `[info]` logs are no longer shown:
@@ -86,10 +103,11 @@
=== Input file
The `CloneUsingBothProtocols` scenario is fed with the data coming from the
-`src/test/resources/data/CloneUsingBothProtocols.json` file. Such a file contains the commands and
-repository used during the load test. That file currently looks like below. This scenario serves
-as a simple example with no actual load in it. It can be used to test or validate the local setup.
-More complex scenarios can be further developed, under the `com.google.gerrit.scenarios` package.
+`src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json` file. Such a
+file contains the commands and repository used during the e2e test. That file currently looks like
+below. This scenario serves as a simple example with no actual load in it. It can be used to test
+or validate the local setup. More complex scenarios can be further developed, under the
+`com.google.gerrit.scenarios` package.
----
[
@@ -111,9 +129,19 @@
* `pull`
* `push`
+=== Project and HTTP credentials
+
The example above assumes that the `loadtest-repo` project exists in the Gerrit under test. The
-`HTTP Credentials` or password obtained from test user's `Settings` (in Gerrit) may be required, in
-`src/test/resources/application.conf`, depending on the above commands used.
+`CloneUsingBothProtocols` scenario already includes creating that project and deleting it once done
+with it. That scenario class can be used as an example of how a scenario can compose itself
+alongside other scenarios (here, `CreateProject` and `DeleteProject`).
+
+The `HTTP Credentials` or password obtained from test user's `Settings` (in Gerrit) may be
+required, in `src/test/resources/application.conf`, depending on the above commands used. That
+file's `http` section shows which shell environment variables can be used to set those credentials.
+
+Executing the `CloneUsingBothProtocols` scenario, as is, does require setting the http credentials.
+That is because of the aforementioned create/delete project (http) scenarios composed within it.
== How to run tests
@@ -142,6 +170,27 @@
docker run -it e2e-tests -s com.google.gerrit.scenarios.CloneUsingBothProtocols
----
+=== How to run non-core scenarios
+
+Locally adding non-core scenarios, for example from Gerrit plugins, is as simple as copying such
+files in. Copying is necessary over linking, unless running using Docker (above) is not required.
+Docker does not support links for files it has to copy over through the Dockerfile (here, the
+scenario files). Here is how to proceed for adding such external (e.g., plugin) scenario files in:
+
+----
+pushd e2e-tests/src/test/scala
+cp -r (or, ln -s) scalaPackageStructure .
+popd
+
+pushd e2e-tests/src/test/resources/data
+cp -r (or, ln -s) jsonFilesPackageStructure .
+popd
+----
+
+The destination folders above readily git-ignore every non-core scenario file added under them. If
+running using Docker, `e2e-tests/Dockerfile` may require another `COPY` line for the hereby added
+scenarios. Aforementioned `sbt` or `docker` commands can then be used to run the added tests.
+
GERRIT
------
Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt
index 5974aca..2748413 100644
--- a/Documentation/dev-readme.txt
+++ b/Documentation/dev-readme.txt
@@ -65,8 +65,8 @@
[[e2e]]
=== End-to-end tests
-<<dev-e2e-tests#,This document>> describes how load test scenarios are
-implemented using link:https://gatling.io/[`Gatling`].
+<<dev-e2e-tests#,This document>> describes how `e2e` (load or functional) test
+scenarios are implemented using link:https://gatling.io/[`Gatling`,role=external,window=_blank].
== Local server
diff --git a/Documentation/dev-release-jgit.txt b/Documentation/dev-release-jgit.txt
deleted file mode 100644
index 7fbbb95..0000000
--- a/Documentation/dev-release-jgit.txt
+++ /dev/null
@@ -1,53 +0,0 @@
-:linkattrs:
-= Making a Snapshot Release of JGit
-
-This step is only necessary if we need to create an unofficial JGit
-snapshot release and publish it to the
-link:https://developers.google.com/storage/[Google Cloud Storage,role=external,window=_blank].
-
-[[prepare-environment]]
-== Prepare the Maven Environment
-
-First, make sure you have done the necessary
-link:dev-release-deploy-config.html#deploy-configuration-settings-xml[
-configuration in Maven `settings.xml`].
-
-To apply the necessary settings in JGit's `pom.xml`, follow the instructions
-in link:dev-release-deploy-config.html#deploy-configuration-subprojects[
-Configuration for Subprojects in `pom.xml`], or apply the provided diff by
-executing the following command in the JGit workspace:
-
-----
- git apply /path/to/gerrit/tools/jgit-snapshot-deploy-pom.diff
-----
-
-[[prepare-release]]
-== Prepare the Release
-
-Since JGit has its own release process we do not push any release tags. Instead
-we will use the output of `git describe` as the version of the current JGit
-snapshot.
-
-In the JGit workspace, execute the following command:
-
-----
- ./tools/version.sh --release $(git describe)
-----
-
-[[publish-release]]
-== Publish the Release
-
-To deploy the new snapshot, execute the following command in the JGit
-workspace:
-
-----
- mvn deploy
-----
-
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
-
-SEARCHBOX
----------
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index e238735..6f9367d 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -7031,8 +7031,6 @@
Alphanumerical (plus hyphens or underscores) string to identify what the requirement is and why it
was triggered. Can be seen as a class: requirements sharing the same type were created for a similar
reason, and the data structure will follow one set of rules.
-|`data` |optional|
-Holds custom key-value strings, used in templates to render richer status messages
|===========================
diff --git a/WORKSPACE b/WORKSPACE
index 015675a..6a822d7 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -46,11 +46,13 @@
# otherwise refer to RBE docs.
rbe_autoconfig(name = "rbe_default")
+# TODO(davido): Switch to upstream again, when this PR is merged:
+# https://github.com/bazelbuild/rules_closure/pull/478
http_archive(
name = "io_bazel_rules_closure",
- sha256 = "03c3b16f205085817fd89cfdcb2220a0138647ee7992be9cef291b069dd90301",
- strip_prefix = "rules_closure-196a45f0ede2faec11dcc6c60fbc5e7471f4bd58",
- urls = ["https://github.com/bazelbuild/rules_closure/archive/196a45f0ede2faec11dcc6c60fbc5e7471f4bd58.tar.gz"],
+ sha256 = "b9c2bc6ba377aa497eb7c31681d34404febf9d4e3c9c7d98ce0d78238a0af20f",
+ strip_prefix = "rules_closure-0.31",
+ urls = ["https://github.com/davido/rules_closure/archive/V0.31.tar.gz"],
)
http_archive(
@@ -149,24 +151,24 @@
sha1 = "83cd2cd674a217ade95a4bb83a8a14f351f48bd0",
)
-GUICE_VERS = "4.2.2"
+GUICE_VERS = "4.2.3"
maven_jar(
name = "guice-library",
artifact = "com.google.inject:guice:" + GUICE_VERS,
- sha1 = "6dacbe18e5eaa7f6c9c36db33b42e7985e94ce77",
+ sha1 = "2ea992d6d7bdcac7a43111a95d182a4c42eb5ff7",
)
maven_jar(
name = "guice-assistedinject",
artifact = "com.google.inject.extensions:guice-assistedinject:" + GUICE_VERS,
- sha1 = "c33fb10080d58446f752b4fcfff8a5fabb80a449",
+ sha1 = "acbfddc556ee9496293ed1df250cc378f331d854",
)
maven_jar(
name = "guice-servlet",
artifact = "com.google.inject.extensions:guice-servlet:" + GUICE_VERS,
- sha1 = "0d0054bdd812224078357a9b11409e43d182a046",
+ sha1 = "8d6e7e35eac4fb5e7df19c55b3bc23fa51b10a11",
)
maven_jar(
diff --git a/e2e-tests/Dockerfile b/e2e-tests/Dockerfile
index ceae672..17c001b 100644
--- a/e2e-tests/Dockerfile
+++ b/e2e-tests/Dockerfile
@@ -1,6 +1,6 @@
FROM denvazh/gatling:3.2.1
-ARG gatling_git_version=1.0.9
+ARG gatling_git_version=1.0.10
RUN apk add --no-cache maven
RUN mvn dependency:get \
-DgroupId=com.gerritforge \
diff --git a/e2e-tests/README.md b/e2e-tests/README.md
deleted file mode 100644
index 534fde5..0000000
--- a/e2e-tests/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# How to build the Docker image
-
-```$shell
-docker build . -t e2e-tests
-```
-
-# How to run a test
-
-```$shell
-docker run -it e2e-tests -s com.google.gerrit.scenarios.ReplayRecordsFromFeederScenario
-```
diff --git a/e2e-tests/build.sbt b/e2e-tests/build.sbt
index 685db37..a322970 100644
--- a/e2e-tests/build.sbt
+++ b/e2e-tests/build.sbt
@@ -13,6 +13,7 @@
name := "gerrit",
libraryDependencies ++=
gatling ++
- Seq("io.gatling" % "gatling-core" % "3.1.1") ++
- Seq("io.gatling" % "gatling-app" % "3.1.1")
+ Seq("io.gatling" % "gatling-core" % GatlingVersion) ++
+ Seq("io.gatling" % "gatling-app" % GatlingVersion),
+ scalacOptions += "-language:postfixOps"
) dependsOn gatlingGitExtension
diff --git a/e2e-tests/project/Dependencies.scala b/e2e-tests/project/Dependencies.scala
index 72d2ac2..63328f9 100644
--- a/e2e-tests/project/Dependencies.scala
+++ b/e2e-tests/project/Dependencies.scala
@@ -1,8 +1,10 @@
import sbt._
object Dependencies {
+ val GatlingVersion = "3.2.0"
+
lazy val gatling = Seq(
"io.gatling.highcharts" % "gatling-charts-highcharts",
"io.gatling" % "gatling-test-framework",
- ).map(_ % "3.1.1" % Test)
+ ).map(_ % GatlingVersion % Test)
}
diff --git a/e2e-tests/src/test/resources/data/.gitignore b/e2e-tests/src/test/resources/data/.gitignore
new file mode 100644
index 0000000..7354459
--- /dev/null
+++ b/e2e-tests/src/test/resources/data/.gitignore
@@ -0,0 +1,4 @@
+*
+!*/
+!/com/google/gerrit/scenarios/*
+!/.gitignore
diff --git a/e2e-tests/src/test/resources/data/CloneUsingBothProtocols.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json
similarity index 100%
rename from e2e-tests/src/test/resources/data/CloneUsingBothProtocols.json
rename to e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json
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
new file mode 100644
index 0000000..2e54de5
--- /dev/null
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateProject.json
@@ -0,0 +1,5 @@
+[
+ {
+ "url": "http://localhost:8080/a/projects/loadtest-repo"
+ }
+]
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
new file mode 100644
index 0000000..9312fb4
--- /dev/null
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject.json
@@ -0,0 +1,5 @@
+[
+ {
+ "url": "http://localhost:8080/a/projects/loadtest-repo/delete-project~delete"
+ }
+]
diff --git a/e2e-tests/src/test/resources/data/ReplayRecordsFromFeeder.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.json
similarity index 100%
rename from e2e-tests/src/test/resources/data/ReplayRecordsFromFeeder.json
rename to e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.json
diff --git a/e2e-tests/src/test/scala/.gitignore b/e2e-tests/src/test/scala/.gitignore
new file mode 100644
index 0000000..7354459
--- /dev/null
+++ b/e2e-tests/src/test/scala/.gitignore
@@ -0,0 +1,4 @@
+*
+!*/
+!/com/google/gerrit/scenarios/*
+!/.gitignore
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 c5a7cba..19fbf1b 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
@@ -15,18 +15,32 @@
package com.google.gerrit.scenarios
import io.gatling.core.Predef._
+import io.gatling.core.feeder.FileBasedFeederBuilder
import io.gatling.core.structure.ScenarioBuilder
import scala.concurrent.duration._
class CloneUsingBothProtocols extends GitSimulation {
+ private val data: FileBasedFeederBuilder[Any]#F = jsonFile(resource).queue
private val test: ScenarioBuilder = scenario(name)
.feed(data)
- .exec(request)
+ .exec(gitRequest)
+
+ private val createProject = new CreateProject
+ private val deleteProject = new DeleteProject
setUp(
+ createProject.test.inject(
+ atOnceUsers(1)
+ ),
test.inject(
+ nothingFor(1 second),
constantUsersPerSec(1) during (2 seconds)
- )).protocols(protocol)
+ ),
+ deleteProject.test.inject(
+ nothingFor(3 second),
+ atOnceUsers(1)
+ ),
+ ).protocols(gitProtocol, httpProtocol)
}
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateProject.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateProject.scala
new file mode 100644
index 0000000..58c8994
--- /dev/null
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateProject.scala
@@ -0,0 +1,32 @@
+// 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.
+
+package com.google.gerrit.scenarios
+
+import io.gatling.core.Predef._
+import io.gatling.core.feeder.FileBasedFeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
+
+class CreateProject extends GerritSimulation {
+ private val data: FileBasedFeederBuilder[Any]#F = jsonFile(resource).queue
+
+ val test: ScenarioBuilder = scenario(name)
+ .feed(data)
+ .exec(httpRequest)
+
+ setUp(
+ test.inject(
+ atOnceUsers(1)
+ )).protocols(httpProtocol)
+}
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala
new file mode 100644
index 0000000..4b723cb
--- /dev/null
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala
@@ -0,0 +1,32 @@
+// 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.
+
+package com.google.gerrit.scenarios
+
+import io.gatling.core.Predef._
+import io.gatling.core.feeder.FileBasedFeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
+
+class DeleteProject extends GerritSimulation {
+ private val data: FileBasedFeederBuilder[Any]#F = jsonFile(resource).queue
+
+ val test: ScenarioBuilder = scenario(name)
+ .feed(data)
+ .exec(httpRequest)
+
+ setUp(
+ test.inject(
+ atOnceUsers(1)
+ )).protocols(httpProtocol)
+}
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
new file mode 100644
index 0000000..b628bc7
--- /dev/null
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala
@@ -0,0 +1,34 @@
+// 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.
+
+package com.google.gerrit.scenarios
+
+import com.github.barbasa.gatling.git.GatlingGitConfiguration
+import io.gatling.core.Predef._
+import io.gatling.http.Predef.http
+import io.gatling.http.protocol.HttpProtocolBuilder
+import io.gatling.http.request.builder.HttpRequestBuilder
+
+class GerritSimulation extends Simulation {
+ implicit val conf: GatlingGitConfiguration = GatlingGitConfiguration()
+
+ private val path: String = this.getClass.getPackage.getName.replaceAllLiterally(".", "/")
+ protected val name: String = this.getClass.getSimpleName
+ protected val resource: String = s"data/$path/$name.json"
+
+ protected val httpRequest: HttpRequestBuilder = http(name).post("${url}")
+ protected val httpProtocol: HttpProtocolBuilder = http.basicAuth(
+ conf.httpConfiguration.userName,
+ conf.httpConfiguration.password)
+}
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 4d5130f..e2f13a4 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
@@ -16,23 +16,18 @@
import java.io.{File, IOException}
+import com.github.barbasa.gatling.git.GitRequestSession
import com.github.barbasa.gatling.git.protocol.GitProtocol
import com.github.barbasa.gatling.git.request.builder.GitRequestBuilder
-import com.github.barbasa.gatling.git.{GatlingGitConfiguration, GitRequestSession}
import io.gatling.core.Predef._
-import io.gatling.core.feeder.FileBasedFeederBuilder
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.hooks.CommitMsgHook
-class GitSimulation extends Simulation {
-
- implicit val conf: GatlingGitConfiguration = GatlingGitConfiguration()
+class GitSimulation extends GerritSimulation {
implicit val postMessageHook: Option[String] = Some(s"hooks/${CommitMsgHook.NAME}")
- protected val name: String = this.getClass.getSimpleName
- protected val data: FileBasedFeederBuilder[Any]#F = jsonFile(s"data/$name.json").circular
- protected val request = new GitRequestBuilder(GitRequestSession("${cmd}", "${url}"))
- protected val protocol: GitProtocol = GitProtocol()
+ protected val gitRequest = new GitRequestBuilder(GitRequestSession("${cmd}", "${url}"))
+ protected val gitProtocol: GitProtocol = GitProtocol()
after {
Thread.sleep(5000)
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 82342be..32df1b5 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
@@ -15,16 +15,18 @@
package com.google.gerrit.scenarios
import io.gatling.core.Predef._
+import io.gatling.core.feeder.FileBasedFeederBuilder
import io.gatling.core.structure.ScenarioBuilder
import scala.concurrent.duration._
class ReplayRecordsFromFeeder extends GitSimulation {
+ private val data: FileBasedFeederBuilder[Any]#F = jsonFile(resource).circular
private val test: ScenarioBuilder = scenario(name)
.repeat(10000) {
feed(data)
- .exec(request)
+ .exec(gitRequest)
}
setUp(
@@ -34,6 +36,6 @@
rampUsers(10) during (5 seconds),
constantUsersPerSec(20) during (15 seconds),
constantUsersPerSec(20) during (15 seconds) randomized
- )).protocols(protocol)
+ )).protocols(gitProtocol)
.maxDuration(60 seconds)
}
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 60127b9..7a84501 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -40,6 +40,7 @@
import com.google.common.jimfs.Jimfs;
import com.google.common.primitives.Chars;
import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.Nullable;
@@ -61,6 +62,7 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
@@ -836,6 +838,10 @@
return gApi.changes().id(id).info();
}
+ protected ChangeApi change(Result r) throws RestApiException {
+ return gApi.changes().id(r.getChange().getId().get());
+ }
+
protected Optional<EditInfo> getEdit(String id) throws RestApiException {
return gApi.changes().id(id).edit().get();
}
diff --git a/java/com/google/gerrit/acceptance/ExtensionRegistry.java b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
index 2371bd0..5376d23 100644
--- a/java/com/google/gerrit/acceptance/ExtensionRegistry.java
+++ b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
@@ -15,7 +15,9 @@
package com.google.gerrit.acceptance;
import com.google.gerrit.extensions.api.changes.ActionVisitor;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
import com.google.gerrit.extensions.config.DownloadScheme;
+import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
import com.google.gerrit.extensions.events.AccountIndexedListener;
import com.google.gerrit.extensions.events.ChangeIndexedListener;
import com.google.gerrit.extensions.events.CommentAddedListener;
@@ -47,6 +49,8 @@
import java.util.List;
public class ExtensionRegistry {
+ public static final String PLUGIN_NAME = "myPlugin";
+
private final DynamicSet<AccountIndexedListener> accountIndexedListeners;
private final DynamicSet<ChangeIndexedListener> changeIndexedListeners;
private final DynamicSet<GroupIndexedListener> groupIndexedListeners;
@@ -71,6 +75,8 @@
accountActivationValidationListeners;
private final DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners;
private final DynamicSet<WorkInProgressStateChangedListener> workInProgressStateChangedListeners;
+ private final DynamicMap<CapabilityDefinition> capabilityDefinitions;
+ private final DynamicMap<PluginProjectPermissionDefinition> pluginProjectPermissionDefinitions;
@Inject
ExtensionRegistry(
@@ -96,7 +102,9 @@
DynamicSet<GroupBackend> groupBackends,
DynamicSet<AccountActivationValidationListener> accountActivationValidationListeners,
DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners,
- DynamicSet<WorkInProgressStateChangedListener> workInProgressStateChangedListeners) {
+ DynamicSet<WorkInProgressStateChangedListener> workInProgressStateChangedListeners,
+ DynamicMap<CapabilityDefinition> capabilityDefinitions,
+ DynamicMap<PluginProjectPermissionDefinition> pluginProjectPermissionDefinitions) {
this.accountIndexedListeners = accountIndexedListeners;
this.changeIndexedListeners = changeIndexedListeners;
this.groupIndexedListeners = groupIndexedListeners;
@@ -120,6 +128,8 @@
this.accountActivationValidationListeners = accountActivationValidationListeners;
this.onSubmitValidationListeners = onSubmitValidationListeners;
this.workInProgressStateChangedListeners = workInProgressStateChangedListeners;
+ this.capabilityDefinitions = capabilityDefinitions;
+ this.pluginProjectPermissionDefinitions = pluginProjectPermissionDefinitions;
}
public Registration newRegistration() {
@@ -227,6 +237,15 @@
return add(workInProgressStateChangedListeners, workInProgressStateChangedListener);
}
+ public Registration add(CapabilityDefinition capabilityDefinition, String exportName) {
+ return add(capabilityDefinitions, capabilityDefinition, exportName);
+ }
+
+ public Registration add(
+ PluginProjectPermissionDefinition pluginProjectPermissionDefinition, String exportName) {
+ return add(pluginProjectPermissionDefinitions, pluginProjectPermissionDefinition, exportName);
+ }
+
private <T> Registration add(DynamicSet<T> dynamicSet, T extension) {
return add(dynamicSet, extension, "gerrit");
}
@@ -240,7 +259,7 @@
private <T> Registration add(DynamicMap<T> dynamicMap, T extension, String exportName) {
RegistrationHandle registrationHandle =
((PrivateInternals_DynamicMapImpl<T>) dynamicMap)
- .put("myPlugin", exportName, Providers.of(extension));
+ .put(PLUGIN_NAME, exportName, Providers.of(extension));
registrationHandles.add(registrationHandle);
return this;
}
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
index dede7e0..21bfcd1 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
@@ -280,6 +280,29 @@
private void invalidateProject(TestProjectInvalidation testProjectInvalidation)
throws Exception {
+ if (testProjectInvalidation.makeProjectConfigInvalid()) {
+ Config projectConfig = new Config();
+ projectConfig.fromText(getConfig().toText());
+
+ // Make the project config invalid by adding a permission entry with an invalid permission
+ // name.
+ projectConfig.setString(
+ "access", "refs/*", "Invalid Permission Name", "group Administrators");
+
+ setConfig(projectConfig);
+ try {
+ projectCache.evict(nameKey);
+ } catch (Exception e) {
+ // Evicting the project from the cache, also triggers a reindex of the project.
+ // The reindex step fails if the project config is invalid. That's fine, since it was our
+ // intention to make the project config invalid. Hence we ignore exceptions that are cause
+ // by an invalid project config here.
+ if (!Throwables.getCausalChain(e).stream()
+ .anyMatch(ConfigInvalidException.class::isInstance)) {
+ throw e;
+ }
+ }
+ }
if (!testProjectInvalidation.projectConfigUpdater().isEmpty()) {
Config projectConfig = new Config();
projectConfig.fromText(getConfig().toText());
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectInvalidation.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectInvalidation.java
index f070b56..d4bd912 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectInvalidation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectInvalidation.java
@@ -28,17 +28,32 @@
*/
@AutoValue
public abstract class TestProjectInvalidation {
+ public abstract boolean makeProjectConfigInvalid();
+
public abstract ImmutableList<Consumer<Config>> projectConfigUpdater();
abstract ThrowingConsumer<TestProjectInvalidation> projectInvalidator();
public static Builder builder(ThrowingConsumer<TestProjectInvalidation> projectInvalidator) {
- return new AutoValue_TestProjectInvalidation.Builder().projectInvalidator(projectInvalidator);
+ return new AutoValue_TestProjectInvalidation.Builder()
+ .projectInvalidator(projectInvalidator)
+ .makeProjectConfigInvalid(false);
}
@AutoValue.Builder
public abstract static class Builder {
/**
+ * Updates the project.config file so that it becomes invalid and loading it within Gerrit fails
+ * with {@link org.eclipse.jgit.errors.ConfigInvalidException}.
+ */
+ public Builder makeProjectConfigInvalid() {
+ makeProjectConfigInvalid(true);
+ return this;
+ }
+
+ protected abstract Builder makeProjectConfigInvalid(boolean makeProjectConfigInvalid);
+
+ /**
* Adds a consumer that can update the project config.
*
* <p>This allows tests to set arbitrary values in the project config.
diff --git a/java/com/google/gerrit/common/data/SubmitRequirement.java b/java/com/google/gerrit/common/data/SubmitRequirement.java
index 66e647d..2c341bf 100644
--- a/java/com/google/gerrit/common/data/SubmitRequirement.java
+++ b/java/com/google/gerrit/common/data/SubmitRequirement.java
@@ -18,8 +18,6 @@
import com.google.auto.value.AutoValue;
import com.google.common.base.CharMatcher;
-import com.google.common.collect.ImmutableMap;
-import java.util.Map;
/** Describes a requirement to submit a change. */
@AutoValue
@@ -37,15 +35,6 @@
public abstract Builder setFallbackText(String value);
- public Builder setData(Map<String, String> value) {
- return setData(ImmutableMap.copyOf(value));
- }
-
- public Builder addCustomValue(String key, String value) {
- dataBuilder().put(key, value);
- return this;
- }
-
public SubmitRequirement build() {
SubmitRequirement requirement = autoBuild();
checkState(
@@ -54,10 +43,6 @@
return requirement;
}
- abstract Builder setData(ImmutableMap<String, String> value);
-
- abstract ImmutableMap.Builder<String, String> dataBuilder();
-
abstract SubmitRequirement autoBuild();
}
@@ -65,8 +50,6 @@
public abstract String type();
- public abstract ImmutableMap<String, String> data();
-
public static Builder builder() {
return new AutoValue_SubmitRequirement.Builder();
}
diff --git a/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java b/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
index cbe9bc7..35c33cb 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
@@ -40,10 +40,13 @@
static final String KEY_SERVER = "server";
static final String KEY_NUMBER_OF_SHARDS = "numberOfShards";
static final String KEY_NUMBER_OF_REPLICAS = "numberOfReplicas";
+ static final String KEY_MAX_RESULT_WINDOW = "maxResultWindow";
+
static final String DEFAULT_PORT = "9200";
static final String DEFAULT_USERNAME = "elastic";
static final int DEFAULT_NUMBER_OF_SHARDS = 0;
static final int DEFAULT_NUMBER_OF_REPLICAS = 1;
+ static final int DEFAULT_MAX_RESULT_WINDOW = 10000;
private final Config cfg;
private final List<HttpHost> hosts;
@@ -52,6 +55,7 @@
final String password;
final int numberOfShards;
final int numberOfReplicas;
+ final int maxResultWindow;
final String prefix;
@Inject
@@ -68,6 +72,8 @@
cfg.getInt(SECTION_ELASTICSEARCH, null, KEY_NUMBER_OF_SHARDS, DEFAULT_NUMBER_OF_SHARDS);
this.numberOfReplicas =
cfg.getInt(SECTION_ELASTICSEARCH, null, KEY_NUMBER_OF_REPLICAS, DEFAULT_NUMBER_OF_REPLICAS);
+ this.maxResultWindow =
+ cfg.getInt(SECTION_ELASTICSEARCH, null, KEY_MAX_RESULT_WINDOW, DEFAULT_MAX_RESULT_WINDOW);
this.hosts = new ArrayList<>();
for (String server : cfg.getStringList(SECTION_ELASTICSEARCH, null, KEY_SERVER)) {
try {
diff --git a/java/com/google/gerrit/elasticsearch/ElasticSetting.java b/java/com/google/gerrit/elasticsearch/ElasticSetting.java
index 14e4623..e016efb 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticSetting.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticSetting.java
@@ -35,6 +35,7 @@
properties.analysis = fields.build();
properties.numberOfShards = config.getNumberOfShards(adapter);
properties.numberOfReplicas = config.numberOfReplicas;
+ properties.maxResultWindow = config.maxResultWindow;
return properties;
}
@@ -75,6 +76,7 @@
Map<String, FieldProperties> analysis;
Integer numberOfShards;
Integer numberOfReplicas;
+ Integer maxResultWindow;
}
static class FieldProperties {
diff --git a/java/com/google/gerrit/entities/AttentionStatus.java b/java/com/google/gerrit/entities/AttentionSetUpdate.java
similarity index 77%
rename from java/com/google/gerrit/entities/AttentionStatus.java
rename to java/com/google/gerrit/entities/AttentionSetUpdate.java
index c488ccd..45588722 100644
--- a/java/com/google/gerrit/entities/AttentionStatus.java
+++ b/java/com/google/gerrit/entities/AttentionSetUpdate.java
@@ -21,14 +21,14 @@
/**
* A single update to the attention set. To reconstruct the attention set these instances are parsed
* in reverse chronological order. Since each update contains all required information and
- * invalidates all previous state (hence the name -Status rather than -Update), only the most recent
- * record is relevant for each user.
+ * invalidates all previous state, only the most recent record is relevant for each user.
*
- * <p>See <a href="https://www.gerritcodereview.com/design-docs/attention-set.html">here</a> for
- * details.
+ * <p>See {@link com.google.gerrit.extensions.api.changes.AddToAttentionSetInput} and {@link
+ * com.google.gerrit.extensions.api.changes.RemoveFromAttentionSetInput} for the representation in
+ * the API.
*/
@AutoValue
-public abstract class AttentionStatus {
+public abstract class AttentionSetUpdate {
/** Users can be added to or removed from the attention set. */
public enum Operation {
@@ -56,17 +56,17 @@
* Create an instance from data read from NoteDB. This includes the timestamp taken from the
* commit.
*/
- public static AttentionStatus createFromRead(
+ public static AttentionSetUpdate createFromRead(
Instant timestamp, Account.Id account, Operation operation, String reason) {
- return new AutoValue_AttentionStatus(timestamp, account, operation, reason);
+ return new AutoValue_AttentionSetUpdate(timestamp, account, operation, reason);
}
/**
* Create an instance to be written to NoteDB. This has no timestamp because the timestamp of the
* commit will be used.
*/
- public static AttentionStatus createForWrite(
+ public static AttentionSetUpdate createForWrite(
Account.Id account, Operation operation, String reason) {
- return new AutoValue_AttentionStatus(null, account, operation, reason);
+ return new AutoValue_AttentionSetUpdate(null, account, operation, reason);
}
}
diff --git a/java/com/google/gerrit/extensions/api/changes/AddToAttentionSetInput.java b/java/com/google/gerrit/extensions/api/changes/AddToAttentionSetInput.java
new file mode 100644
index 0000000..39efc64
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/changes/AddToAttentionSetInput.java
@@ -0,0 +1,33 @@
+// 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.
+
+package com.google.gerrit.extensions.api.changes;
+
+/**
+ * Input at API level to add a user to the attention set.
+ *
+ * @see RemoveFromAttentionSetInput
+ * @see com.google.gerrit.extensions.common.AttentionSetEntry
+ */
+public class AddToAttentionSetInput {
+ public String user;
+ public String reason;
+
+ public AddToAttentionSetInput(String user, String reason) {
+ this.user = user;
+ this.reason = reason;
+ }
+
+ public AddToAttentionSetInput() {}
+}
diff --git a/java/com/google/gerrit/extensions/api/changes/AttentionSetApi.java b/java/com/google/gerrit/extensions/api/changes/AttentionSetApi.java
new file mode 100644
index 0000000..5086cd8
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/changes/AttentionSetApi.java
@@ -0,0 +1,35 @@
+// 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.
+
+package com.google.gerrit.extensions.api.changes;
+
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+/** API for managing the attention set of a change. */
+public interface AttentionSetApi {
+
+ void remove(RemoveFromAttentionSetInput input) throws RestApiException;
+
+ /**
+ * A default implementation which allows source compatibility when adding new methods to the
+ * interface.
+ */
+ class NotImplemented implements AttentionSetApi {
+ @Override
+ public void remove(RemoveFromAttentionSetInput input) throws RestApiException {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 96455a6..284d8f6 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -312,6 +312,16 @@
*/
Set<String> getHashtags() throws RestApiException;
+ /**
+ * Manage the attention set.
+ *
+ * @param id The account identifier.
+ */
+ AttentionSetApi attention(String id) throws RestApiException;
+
+ /** Adds a user to the attention set. */
+ AccountInfo addToAttentionSet(AddToAttentionSetInput input) throws RestApiException;
+
/** Set the assignee of a change. */
AccountInfo setAssignee(AssigneeInput input) throws RestApiException;
@@ -581,6 +591,16 @@
}
@Override
+ public AttentionSetApi attention(String id) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public AccountInfo addToAttentionSet(AddToAttentionSetInput input) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public AccountInfo setAssignee(AssigneeInput input) throws RestApiException {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/extensions/api/changes/RemoveFromAttentionSetInput.java b/java/com/google/gerrit/extensions/api/changes/RemoveFromAttentionSetInput.java
new file mode 100644
index 0000000..9212788
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/changes/RemoveFromAttentionSetInput.java
@@ -0,0 +1,33 @@
+// 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.
+
+package com.google.gerrit.extensions.api.changes;
+
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+/**
+ * Input at API level to remove a user from the attention set.
+ *
+ * @see AddToAttentionSetInput
+ * @see com.google.gerrit.extensions.common.AttentionSetEntry
+ */
+public class RemoveFromAttentionSetInput {
+ @DefaultInput public String reason;
+
+ public RemoveFromAttentionSetInput(String reason) {
+ this.reason = reason;
+ }
+
+ public RemoveFromAttentionSetInput() {}
+}
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 6c6389e5..7ae570f 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -16,6 +16,7 @@
import com.google.common.collect.ListMultimap;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.client.ArchiveFormat;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ActionInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -158,6 +159,15 @@
/** Returns votes on the revision. */
ListMultimap<String, ApprovalInfo> votes() throws RestApiException;
+ /**
+ * Retrieves the revision as an archive.
+ *
+ * @param format the format of the archive
+ * @return the archive as {@link BinaryResult}
+ * @throws RestApiException
+ */
+ BinaryResult getArchive(ArchiveFormat format) throws RestApiException;
+
abstract class MergeListRequest {
private boolean addLinks;
private int uninterestingParent = 1;
@@ -392,5 +402,10 @@
public String etag() throws RestApiException {
throw new NotImplementedException();
}
+
+ @Override
+ public BinaryResult getArchive(ArchiveFormat format) throws RestApiException {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/java/com/google/gerrit/extensions/client/ArchiveFormat.java b/java/com/google/gerrit/extensions/client/ArchiveFormat.java
new file mode 100644
index 0000000..4ec59cb
--- /dev/null
+++ b/java/com/google/gerrit/extensions/client/ArchiveFormat.java
@@ -0,0 +1,27 @@
+// 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.
+
+package com.google.gerrit.extensions.client;
+
+/**
+ * The {@link com.google.gerrit.server.restapi.change.GetArchive} REST endpoint allows to download
+ * revisions as archive. This enum defines the supported archive formats.
+ */
+public enum ArchiveFormat {
+ TGZ,
+ TAR,
+ TBZ2,
+ TXZ,
+ ZIP;
+}
diff --git a/java/com/google/gerrit/extensions/common/AttentionSetEntry.java b/java/com/google/gerrit/extensions/common/AttentionSetEntry.java
new file mode 100644
index 0000000..356b38a
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/AttentionSetEntry.java
@@ -0,0 +1,39 @@
+// 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.
+
+package com.google.gerrit.extensions.common;
+
+import java.sql.Timestamp;
+
+/**
+ * Represents a single user included in the attention set. Used in the API. See {@link
+ * com.google.gerrit.entities.AttentionSetUpdate} for the internal representation.
+ *
+ * <p>See <a href="https://www.gerritcodereview.com/design-docs/attention-set.html">here</a> for
+ * background.
+ */
+public class AttentionSetEntry {
+ /** The user included in the attention set. */
+ public AccountInfo accountInfo;
+ /** The timestamp of the last update. */
+ public Timestamp lastUpdate;
+ /** The human readable reason why the user was added. */
+ public String reason;
+
+ public AttentionSetEntry(AccountInfo accountInfo, Timestamp lastUpdate, String reason) {
+ this.accountInfo = accountInfo;
+ this.lastUpdate = lastUpdate;
+ this.reason = reason;
+ }
+}
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index d4a8477..dce6fd1 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -22,13 +22,28 @@
import java.util.List;
import java.util.Map;
+/**
+ * Representation of a change used in the API. Internally {@link
+ * com.google.gerrit.server.query.change.ChangeData} and {@link com.google.gerrit.entities.Change}
+ * are used.
+ *
+ * <p>Many fields are actually nullable.
+ */
public class ChangeInfo {
// ActionJson#copy(List, ChangeInfo) must be adapted if new fields are added that are not
// protected by any ListChangesOption.
+
public String id;
public String project;
public String branch;
public String topic;
+ /**
+ * The <a href="https://www.gerritcodereview.com/design-docs/attention-set.html">attention set</a>
+ * for this change. Keyed by account ID. We don't use {@link
+ * com.google.gerrit.entities.Account.Id} to avoid a circular dependency.
+ */
+ public Map<Integer, AttentionSetEntry> attentionSet;
+
public AccountInfo assignee;
public Collection<String> hashtags;
public String changeId;
diff --git a/java/com/google/gerrit/extensions/common/SubmitRequirementInfo.java b/java/com/google/gerrit/extensions/common/SubmitRequirementInfo.java
index 53f0375..3483de5 100644
--- a/java/com/google/gerrit/extensions/common/SubmitRequirementInfo.java
+++ b/java/com/google/gerrit/extensions/common/SubmitRequirementInfo.java
@@ -15,21 +15,17 @@
package com.google.gerrit.extensions.common;
import com.google.common.base.MoreObjects;
-import java.util.Map;
import java.util.Objects;
public class SubmitRequirementInfo {
public final String status;
public final String fallbackText;
public final String type;
- public final Map<String, String> data;
- public SubmitRequirementInfo(
- String status, String fallbackText, String type, Map<String, String> data) {
+ public SubmitRequirementInfo(String status, String fallbackText, String type) {
this.status = status;
this.fallbackText = fallbackText;
this.type = type;
- this.data = data;
}
@Override
@@ -43,13 +39,12 @@
SubmitRequirementInfo that = (SubmitRequirementInfo) o;
return Objects.equals(status, that.status)
&& Objects.equals(fallbackText, that.fallbackText)
- && Objects.equals(type, that.type)
- && Objects.equals(data, that.data);
+ && Objects.equals(type, that.type);
}
@Override
public int hashCode() {
- return Objects.hash(status, fallbackText, type, data);
+ return Objects.hash(status, fallbackText, type);
}
@Override
@@ -58,7 +53,6 @@
.add("status", status)
.add("fallbackText", fallbackText)
.add("type", type)
- .add("data", data)
.toString();
}
}
diff --git a/java/com/google/gerrit/server/ChangeMessagesUtil.java b/java/com/google/gerrit/server/ChangeMessagesUtil.java
index 5f00b69..dd48b93 100644
--- a/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -49,6 +49,8 @@
public static final String TAG_RESTORE = AUTOGENERATED_TAG_PREFIX + "gerrit:restore";
public static final String TAG_REVERT = AUTOGENERATED_TAG_PREFIX + "gerrit:revert";
public static final String TAG_SET_ASSIGNEE = AUTOGENERATED_TAG_PREFIX + "gerrit:setAssignee";
+ public static final String TAG_UPDATE_ATTENTION_SET =
+ AUTOGENERATED_TAG_PREFIX + "gerrit:updateAttentionSet";
public static final String TAG_SET_DESCRIPTION =
AUTOGENERATED_TAG_PREFIX + "gerrit:setPsDescription";
public static final String TAG_SET_HASHTAGS = AUTOGENERATED_TAG_PREFIX + "gerrit:setHashtag";
diff --git a/java/com/google/gerrit/server/account/AccountDeactivator.java b/java/com/google/gerrit/server/account/AccountDeactivator.java
index 3465459..b12e585 100644
--- a/java/com/google/gerrit/server/account/AccountDeactivator.java
+++ b/java/com/google/gerrit/server/account/AccountDeactivator.java
@@ -57,10 +57,14 @@
@Override
public void start() {
- if (!supportAutomaticAccountActivityUpdate) {
- return;
+ if (schedule.isPresent()) {
+ if (supportAutomaticAccountActivityUpdate) {
+ queue.scheduleAtFixedRate(deactivator, schedule.get());
+ } else {
+ logger.atWarning().log(
+ "Not scheduling AccountDeactivator because auth.autoUpdateAccountActiveStatus is false");
+ }
}
- schedule.ifPresent(s -> queue.scheduleAtFixedRate(deactivator, s));
}
@Override
diff --git a/java/com/google/gerrit/server/api/changes/AttentionSetApiImpl.java b/java/com/google/gerrit/server/api/changes/AttentionSetApiImpl.java
new file mode 100644
index 0000000..8dc44b7
--- /dev/null
+++ b/java/com/google/gerrit/server/api/changes/AttentionSetApiImpl.java
@@ -0,0 +1,51 @@
+// 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.
+
+package com.google.gerrit.server.api.changes;
+
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
+import com.google.gerrit.extensions.api.changes.AttentionSetApi;
+import com.google.gerrit.extensions.api.changes.RemoveFromAttentionSetInput;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.change.AttentionSetEntryResource;
+import com.google.gerrit.server.restapi.change.RemoveFromAttentionSet;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+public class AttentionSetApiImpl implements AttentionSetApi {
+ interface Factory {
+ AttentionSetApiImpl create(AttentionSetEntryResource attentionSetEntryResource);
+ }
+
+ private final RemoveFromAttentionSet removeFromAttentionSet;
+ private final AttentionSetEntryResource attentionSetEntryResource;
+
+ @Inject
+ AttentionSetApiImpl(
+ RemoveFromAttentionSet removeFromAttentionSet,
+ @Assisted AttentionSetEntryResource attentionSetEntryResource) {
+ this.removeFromAttentionSet = removeFromAttentionSet;
+ this.attentionSetEntryResource = attentionSetEntryResource;
+ }
+
+ @Override
+ public void remove(RemoveFromAttentionSetInput input) throws RestApiException {
+ try {
+ removeFromAttentionSet.apply(attentionSetEntryResource, input);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot remove from attention set", e);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 0d640d9..5122f8a 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -23,7 +23,9 @@
import com.google.gerrit.extensions.api.changes.AbandonInput;
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.AddReviewerResult;
+import com.google.gerrit.extensions.api.changes.AddToAttentionSetInput;
import com.google.gerrit.extensions.api.changes.AssigneeInput;
+import com.google.gerrit.extensions.api.changes.AttentionSetApi;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.ChangeEditApi;
import com.google.gerrit.extensions.api.changes.ChangeMessageApi;
@@ -66,6 +68,8 @@
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.WorkInProgressOp;
import com.google.gerrit.server.restapi.change.Abandon;
+import com.google.gerrit.server.restapi.change.AddToAttentionSet;
+import com.google.gerrit.server.restapi.change.AttentionSet;
import com.google.gerrit.server.restapi.change.ChangeIncludedIn;
import com.google.gerrit.server.restapi.change.ChangeMessages;
import com.google.gerrit.server.restapi.change.Check;
@@ -147,6 +151,9 @@
private final Provider<GetChange> getChangeProvider;
private final PostHashtags postHashtags;
private final GetHashtags getHashtags;
+ private final AttentionSet attentionSet;
+ private final AttentionSetApiImpl.Factory attentionSetApi;
+ private final AddToAttentionSet addToAttentionSet;
private final PutAssignee putAssignee;
private final GetAssignee getAssignee;
private final GetPastAssignees getPastAssignees;
@@ -197,6 +204,9 @@
Provider<GetChange> getChangeProvider,
PostHashtags postHashtags,
GetHashtags getHashtags,
+ AttentionSet attentionSet,
+ AttentionSetApiImpl.Factory attentionSetApi,
+ AddToAttentionSet addToAttentionSet,
PutAssignee putAssignee,
GetAssignee getAssignee,
GetPastAssignees getPastAssignees,
@@ -245,6 +255,9 @@
this.getChangeProvider = getChangeProvider;
this.postHashtags = postHashtags;
this.getHashtags = getHashtags;
+ this.attentionSet = attentionSet;
+ this.attentionSetApi = attentionSetApi;
+ this.addToAttentionSet = addToAttentionSet;
this.putAssignee = putAssignee;
this.getAssignee = getAssignee;
this.getPastAssignees = getPastAssignees;
@@ -530,6 +543,24 @@
}
@Override
+ public AccountInfo addToAttentionSet(AddToAttentionSetInput input) throws RestApiException {
+ try {
+ return addToAttentionSet.apply(change, input).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot add to attention set", e);
+ }
+ }
+
+ @Override
+ public AttentionSetApi attention(String id) throws RestApiException {
+ try {
+ return attentionSetApi.create(attentionSet.parse(change, IdString.fromDecoded(id)));
+ } catch (Exception e) {
+ throw asRestApiException("Cannot parse account", e);
+ }
+ }
+
+ @Override
public AccountInfo setAssignee(AssigneeInput input) throws RestApiException {
try {
return putAssignee.apply(change, input).value();
diff --git a/java/com/google/gerrit/server/api/changes/Module.java b/java/com/google/gerrit/server/api/changes/Module.java
index 0edd58a..f54d1fe 100644
--- a/java/com/google/gerrit/server/api/changes/Module.java
+++ b/java/com/google/gerrit/server/api/changes/Module.java
@@ -32,5 +32,6 @@
factory(RevisionReviewerApiImpl.Factory.class);
factory(ChangeEditApiImpl.Factory.class);
factory(ChangeMessageApiImpl.Factory.class);
+ factory(AttentionSetApiImpl.Factory.class);
}
}
diff --git a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 48a8689..b515dfe 100644
--- a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -37,6 +37,7 @@
import com.google.gerrit.extensions.api.changes.RevisionReviewerApi;
import com.google.gerrit.extensions.api.changes.RobotCommentApi;
import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.client.ArchiveFormat;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ActionInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -70,6 +71,7 @@
import com.google.gerrit.server.restapi.change.DraftComments;
import com.google.gerrit.server.restapi.change.Files;
import com.google.gerrit.server.restapi.change.Fixes;
+import com.google.gerrit.server.restapi.change.GetArchive;
import com.google.gerrit.server.restapi.change.GetCommit;
import com.google.gerrit.server.restapi.change.GetDescription;
import com.google.gerrit.server.restapi.change.GetFixPreview;
@@ -96,6 +98,7 @@
import com.google.inject.assistedinject.Assisted;
import java.util.EnumSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.lib.Repository;
@@ -146,6 +149,7 @@
private final GetRelated getRelated;
private final PutDescription putDescription;
private final GetDescription getDescription;
+ private final Provider<GetArchive> getArchiveProvider;
private final ApprovalsUtil approvalsUtil;
private final AccountLoader.Factory accountLoaderFactory;
@@ -190,6 +194,7 @@
GetRelated getRelated,
PutDescription putDescription,
GetDescription getDescription,
+ Provider<GetArchive> getArchiveProvider,
ApprovalsUtil approvalsUtil,
AccountLoader.Factory accountLoaderFactory,
@Assisted RevisionResource r) {
@@ -232,6 +237,7 @@
this.getRelated = getRelated;
this.putDescription = putDescription;
this.getDescription = getDescription;
+ this.getArchiveProvider = getArchiveProvider;
this.approvalsUtil = approvalsUtil;
this.accountLoaderFactory = accountLoaderFactory;
this.revision = r;
@@ -649,4 +655,15 @@
public String etag() throws RestApiException {
return revisionActions.getETag(revision);
}
+
+ @Override
+ public BinaryResult getArchive(ArchiveFormat format) throws RestApiException {
+ GetArchive getArchive = getArchiveProvider.get();
+ getArchive.setFormat(format != null ? format.name().toLowerCase(Locale.US) : null);
+ try {
+ return getArchive.apply(revision).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot get archive", e);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/change/ActionJson.java b/java/com/google/gerrit/server/change/ActionJson.java
index e87cf70..6f28dad 100644
--- a/java/com/google/gerrit/server/change/ActionJson.java
+++ b/java/com/google/gerrit/server/change/ActionJson.java
@@ -89,14 +89,12 @@
return Lists.newArrayList(visitorSet);
}
- public ChangeInfo addChangeActions(ChangeInfo to, ChangeNotes notes) {
+ void addChangeActions(ChangeInfo to, ChangeNotes notes) {
List<ActionVisitor> visitors = visitors();
to.actions = toActionMap(notes, visitors, copy(visitors, to));
- return to;
}
- public RevisionInfo addRevisionActions(
- @Nullable ChangeInfo changeInfo, RevisionInfo to, RevisionResource rsrc) {
+ void addRevisionActions(@Nullable ChangeInfo changeInfo, RevisionInfo to, RevisionResource rsrc) {
List<ActionVisitor> visitors = visitors();
if (!visitors.isEmpty()) {
if (changeInfo != null) {
@@ -106,7 +104,6 @@
}
}
to.actions = toActionMap(rsrc, visitors, changeInfo, copy(visitors, to));
- return to;
}
private ChangeInfo copy(List<ActionVisitor> visitors, ChangeInfo changeInfo) {
@@ -119,6 +116,8 @@
copy.project = changeInfo.project;
copy.branch = changeInfo.branch;
copy.topic = changeInfo.topic;
+ copy.attentionSet =
+ changeInfo.attentionSet == null ? null : ImmutableMap.copyOf(changeInfo.attentionSet);
copy.assignee = changeInfo.assignee;
copy.hashtags = changeInfo.hashtags;
copy.changeId = changeInfo.changeId;
diff --git a/java/com/google/gerrit/server/change/AddToAttentionSetOp.java b/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
new file mode 100644
index 0000000..262fdc2
--- /dev/null
+++ b/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
@@ -0,0 +1,85 @@
+// 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.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AttentionSetUpdate;
+import com.google.gerrit.entities.AttentionSetUpdate.Operation;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.Map;
+import java.util.function.Function;
+
+/** Add a specified user to the attention set. */
+public class AddToAttentionSetOp implements BatchUpdateOp {
+
+ public interface Factory {
+ AddToAttentionSetOp create(Account.Id attentionUserId, String reason);
+ }
+
+ private final ChangeData.Factory changeDataFactory;
+ private final ChangeMessagesUtil cmUtil;
+ private final Account.Id attentionUserId;
+ private final String reason;
+
+ @Inject
+ AddToAttentionSetOp(
+ ChangeData.Factory changeDataFactory,
+ ChangeMessagesUtil cmUtil,
+ @Assisted Account.Id attentionUserId,
+ @Assisted String reason) {
+ this.changeDataFactory = changeDataFactory;
+ this.cmUtil = cmUtil;
+ this.attentionUserId = requireNonNull(attentionUserId, "user");
+ this.reason = requireNonNull(reason, "reason");
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx) throws RestApiException {
+ ChangeData changeData = changeDataFactory.create(ctx.getNotes());
+ Map<Account.Id, AttentionSetUpdate> attentionMap =
+ changeData.attentionSet().stream()
+ .collect(toImmutableMap(AttentionSetUpdate::account, Function.identity()));
+ AttentionSetUpdate existingEntry = attentionMap.get(attentionUserId);
+ if (existingEntry != null && existingEntry.operation() == Operation.ADD) {
+ return false;
+ }
+
+ ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
+ update.setAttentionSetUpdates(
+ ImmutableList.of(
+ AttentionSetUpdate.createForWrite(
+ attentionUserId, AttentionSetUpdate.Operation.ADD, reason)));
+ addMessage(ctx, update);
+ return true;
+ }
+
+ private void addMessage(ChangeContext ctx, ChangeUpdate update) {
+ String message = "Added to attention set: " + attentionUserId;
+ cmUtil.addChangeMessage(
+ update,
+ ChangeMessagesUtil.newMessage(ctx, message, ChangeMessagesUtil.TAG_UPDATE_ATTENTION_SET));
+ }
+}
diff --git a/java/com/google/gerrit/server/change/ArchiveFormat.java b/java/com/google/gerrit/server/change/ArchiveFormatInternal.java
similarity index 95%
rename from java/com/google/gerrit/server/change/ArchiveFormat.java
rename to java/com/google/gerrit/server/change/ArchiveFormatInternal.java
index d895a66..f6e9ff9 100644
--- a/java/com/google/gerrit/server/change/ArchiveFormat.java
+++ b/java/com/google/gerrit/server/change/ArchiveFormatInternal.java
@@ -28,7 +28,7 @@
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectLoader;
-public enum ArchiveFormat {
+public enum ArchiveFormatInternal {
TGZ("application/x-gzip", new TgzFormat()),
TAR("application/x-tar", new TarFormat()),
TBZ2("application/x-bzip2", new Tbz2Format()),
@@ -40,7 +40,7 @@
private final String mimeType;
- ArchiveFormat(String mimeType, ArchiveCommand.Format<?> format) {
+ ArchiveFormatInternal(String mimeType, ArchiveCommand.Format<?> format) {
this.format = format;
this.mimeType = mimeType;
ArchiveCommand.registerFormat(name(), format);
diff --git a/java/com/google/gerrit/server/change/AttentionSetEntryResource.java b/java/com/google/gerrit/server/change/AttentionSetEntryResource.java
new file mode 100644
index 0000000..6c6c765
--- /dev/null
+++ b/java/com/google/gerrit/server/change/AttentionSetEntryResource.java
@@ -0,0 +1,46 @@
+// 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.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+/** REST resource that represents an entry in the attention set of a change. */
+public class AttentionSetEntryResource implements RestResource {
+ public static final TypeLiteral<RestView<AttentionSetEntryResource>> ATTENTION_SET_ENTRY_KIND =
+ new TypeLiteral<RestView<AttentionSetEntryResource>>() {};
+
+ public interface Factory {
+ AttentionSetEntryResource create(ChangeResource change, Account.Id id);
+ }
+
+ private final ChangeResource changeResource;
+ private final Account.Id accountId;
+
+ public AttentionSetEntryResource(ChangeResource changeResource, Account.Id accountId) {
+ this.changeResource = changeResource;
+ this.accountId = accountId;
+ }
+
+ public ChangeResource getChangeResource() {
+ return changeResource;
+ }
+
+ public Account.Id getAccountId() {
+ return accountId;
+ }
+}
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 65ca741..e4148a5 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.change;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.gerrit.extensions.client.ListChangesOption.ALL_COMMITS;
import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
@@ -49,6 +50,7 @@
import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.PatchSet;
@@ -60,6 +62,7 @@
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
+import com.google.gerrit.extensions.common.AttentionSetEntry;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.LabelInfo;
@@ -102,6 +105,7 @@
import com.google.inject.Singleton;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
+import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -340,7 +344,7 @@
}
private static SubmitRequirementInfo requirementToInfo(SubmitRequirement req, Status status) {
- return new SubmitRequirementInfo(status.name(), req.fallbackText(), req.type(), req.data());
+ return new SubmitRequirementInfo(status.name(), req.fallbackText(), req.type());
}
private static void finish(ChangeInfo info) {
@@ -504,6 +508,20 @@
out.project = in.getProject().get();
out.branch = in.getDest().shortName();
out.topic = in.getTopic();
+ if (!cd.attentionSet().isEmpty()) {
+ out.attentionSet =
+ cd.attentionSet().stream()
+ // This filtering should match GetAttentionSet.
+ .filter(a -> a.operation() == AttentionSetUpdate.Operation.ADD)
+ .collect(
+ toImmutableMap(
+ a -> a.account().get(),
+ a ->
+ new AttentionSetEntry(
+ accountLoader.get(a.account()),
+ Timestamp.from(a.timestamp()),
+ a.reason())));
+ }
out.assignee = in.getAssignee() != null ? accountLoader.get(in.getAssignee()) : null;
out.hashtags = cd.hashtags();
out.changeId = in.getKey().get();
diff --git a/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java b/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
new file mode 100644
index 0000000..7118089
--- /dev/null
+++ b/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
@@ -0,0 +1,84 @@
+// 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.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AttentionSetUpdate;
+import com.google.gerrit.entities.AttentionSetUpdate.Operation;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.Map;
+import java.util.function.Function;
+
+/** Remove a specified user from the attention set. */
+public class RemoveFromAttentionSetOp implements BatchUpdateOp {
+
+ public interface Factory {
+ RemoveFromAttentionSetOp create(Account.Id attentionUserId, String reason);
+ }
+
+ private final ChangeData.Factory changeDataFactory;
+ private final ChangeMessagesUtil cmUtil;
+ private final Account.Id attentionUserId;
+ private final String reason;
+
+ @Inject
+ RemoveFromAttentionSetOp(
+ ChangeData.Factory changeDataFactory,
+ ChangeMessagesUtil cmUtil,
+ @Assisted Account.Id attentionUserId,
+ @Assisted String reason) {
+ this.changeDataFactory = changeDataFactory;
+ this.cmUtil = cmUtil;
+ this.attentionUserId = requireNonNull(attentionUserId, "user");
+ this.reason = requireNonNull(reason, "reason");
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx) throws RestApiException {
+ ChangeData changeData = changeDataFactory.create(ctx.getNotes());
+ Map<Account.Id, AttentionSetUpdate> attentionMap =
+ changeData.attentionSet().stream()
+ .collect(toImmutableMap(AttentionSetUpdate::account, Function.identity()));
+ AttentionSetUpdate existingEntry = attentionMap.get(attentionUserId);
+ if (existingEntry == null || existingEntry.operation() == Operation.REMOVE) {
+ return false;
+ }
+
+ ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
+ update.setAttentionSetUpdates(
+ ImmutableList.of(
+ AttentionSetUpdate.createForWrite(attentionUserId, Operation.REMOVE, reason)));
+ addMessage(ctx, update);
+ return true;
+ }
+
+ private void addMessage(ChangeContext ctx, ChangeUpdate update) {
+ String message = "Removed from attention set: " + attentionUserId;
+ cmUtil.addChangeMessage(
+ update,
+ ChangeMessagesUtil.newMessage(ctx, message, ChangeMessagesUtil.TAG_UPDATE_ATTENTION_SET));
+ }
+}
diff --git a/java/com/google/gerrit/server/config/DownloadConfig.java b/java/com/google/gerrit/server/config/DownloadConfig.java
index 6dea07d..58ce098 100644
--- a/java/com/google/gerrit/server/config/DownloadConfig.java
+++ b/java/com/google/gerrit/server/config/DownloadConfig.java
@@ -17,7 +17,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.CoreDownloadSchemes;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DownloadCommand;
-import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.gerrit.server.change.ArchiveFormatInternal;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.lang.reflect.Field;
@@ -37,7 +37,7 @@
public class DownloadConfig {
private final ImmutableSet<String> downloadSchemes;
private final ImmutableSet<DownloadCommand> downloadCommands;
- private final ImmutableSet<ArchiveFormat> archiveFormats;
+ private final ImmutableSet<ArchiveFormatInternal> archiveFormats;
@Inject
DownloadConfig(@GerritServerConfig Config cfg) {
@@ -69,13 +69,13 @@
String v = cfg.getString("download", null, "archive");
if (v == null) {
- archiveFormats = ImmutableSet.copyOf(EnumSet.allOf(ArchiveFormat.class));
+ archiveFormats = ImmutableSet.copyOf(EnumSet.allOf(ArchiveFormatInternal.class));
} else if (v.isEmpty() || "off".equalsIgnoreCase(v)) {
archiveFormats = ImmutableSet.of();
} else {
archiveFormats =
ImmutableSet.copyOf(
- ConfigUtil.getEnumList(cfg, "download", null, "archive", ArchiveFormat.TGZ));
+ ConfigUtil.getEnumList(cfg, "download", null, "archive", ArchiveFormatInternal.TGZ));
}
}
@@ -110,7 +110,7 @@
}
/** Archive formats for downloading. */
- public ImmutableSet<ArchiveFormat> getArchiveFormats() {
+ public ImmutableSet<ArchiveFormatInternal> getArchiveFormats() {
return archiveFormats;
}
}
diff --git a/java/com/google/gerrit/server/data/SubmitRequirementAttribute.java b/java/com/google/gerrit/server/data/SubmitRequirementAttribute.java
index 3203024..2364ec4 100644
--- a/java/com/google/gerrit/server/data/SubmitRequirementAttribute.java
+++ b/java/com/google/gerrit/server/data/SubmitRequirementAttribute.java
@@ -14,14 +14,11 @@
package com.google.gerrit.server.data;
-import java.util.Map;
-
/**
* Represents a {@link com.google.gerrit.common.data.SubmitRequirement} that does not depend on
* Gerrit internal classes, to be serialized
*/
public class SubmitRequirementAttribute {
- public Map<String, String> data;
public String type;
public String fallbackText;
}
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index 9422c18..55dd4fd 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -259,7 +259,6 @@
SubmitRequirementAttribute re = new SubmitRequirementAttribute();
re.fallbackText = req.fallbackText();
re.type = req.type();
- re.data = req.data();
sa.requirements.add(re);
}
}
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 6c4aacc..1e97a44 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -403,6 +403,17 @@
@Override
public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
throws CommitValidationException {
+ // TODO(zieren): Refactor interface to signal the intent of the event instead of hard-coding
+ // it here. Due to interface limitations, this method is called from both receive commits
+ // and from main Gerrit (e.g. when publishing a change edit). This is why we need to gate the
+ // early return on REFS_CHANGES (though pushes to refs/changes are not possible).
+ String refName = receiveEvent.command.getRefName();
+ if (!refName.startsWith("refs/for/") && !refName.startsWith(RefNames.REFS_CHANGES)) {
+ // This is a direct push bypassing review. We don't need to enforce any file-count limits
+ // here.
+ return Collections.emptyList();
+ }
+
PatchListKey patchListKey =
PatchListKey.againstBase(
receiveEvent.commit.getId(), receiveEvent.commit.getParentCount());
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index d3a0065..ffef684 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -33,6 +33,7 @@
import com.google.common.base.Enums;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Iterables;
@@ -745,7 +746,7 @@
static class StoredRequirement {
String fallbackText;
String type;
- Map<String, String> data;
+ @Deprecated Map<String, String> data;
}
SubmitRecord.Status status;
@@ -772,7 +773,12 @@
StoredRequirement sr = new StoredRequirement();
sr.type = requirement.type();
sr.fallbackText = requirement.fallbackText();
- sr.data = requirement.data();
+ // For backwards compatibility, write an empty map to the index.
+ // This is required, because the SubmitRequirement AutoValue can't
+ // handle null in the old code.
+ // TODO(hiesel): Remove once we have rolled out the new code
+ // and waited long enough to not need to roll back.
+ sr.data = ImmutableMap.of();
this.requirements.add(sr);
}
}
@@ -799,7 +805,6 @@
SubmitRequirement.builder()
.setType(req.type)
.setFallbackText(req.fallbackText)
- .setData(req.data)
.build();
rec.requirements.add(sr);
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index 287f3e7..86b6ed7 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -16,7 +16,7 @@
import com.google.auto.value.AutoValue;
import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.AttentionStatus;
+import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.server.config.GerritServerId;
import com.google.gson.Gson;
@@ -171,11 +171,11 @@
private static class AttentionStatusInNoteDb {
final String personIdent;
- final AttentionStatus.Operation operation;
+ final AttentionSetUpdate.Operation operation;
final String reason;
AttentionStatusInNoteDb(
- String personIndent, AttentionStatus.Operation operation, String reason) {
+ String personIndent, AttentionSetUpdate.Operation operation, String reason) {
this.personIdent = personIndent;
this.operation = operation;
this.reason = reason;
@@ -183,7 +183,7 @@
}
/** The returned {@link Optional} holds the parsed entity or is empty if parsing failed. */
- static Optional<AttentionStatus> attentionStatusFromJson(
+ static Optional<AttentionSetUpdate> attentionStatusFromJson(
Instant timestamp, String attentionString) {
AttentionStatusInNoteDb inNoteDb =
gson.fromJson(attentionString, AttentionStatusInNoteDb.class);
@@ -193,18 +193,20 @@
}
Optional<Account.Id> account = NoteDbUtil.parseIdent(personIdent);
return account.map(
- id -> AttentionStatus.createFromRead(timestamp, id, inNoteDb.operation, inNoteDb.reason));
+ id ->
+ AttentionSetUpdate.createFromRead(timestamp, id, inNoteDb.operation, inNoteDb.reason));
}
- String attentionStatusToJson(AttentionStatus attentionStatus) {
+ String attentionSetUpdateToJson(AttentionSetUpdate attentionSetUpdate) {
PersonIdent personIdent =
new PersonIdent(
- getUsername(attentionStatus.account()), getEmailAddress(attentionStatus.account()));
+ getUsername(attentionSetUpdate.account()),
+ getEmailAddress(attentionSetUpdate.account()));
StringBuilder stringBuilder = new StringBuilder();
appendIdentString(stringBuilder, personIdent.getName(), personIdent.getEmailAddress());
return gson.toJson(
new AttentionStatusInNoteDb(
- stringBuilder.toString(), attentionStatus.operation(), attentionStatus.reason()));
+ stringBuilder.toString(), attentionSetUpdate.operation(), attentionSetUpdate.reason()));
}
static void appendIdentString(StringBuilder stringBuilder, String name, String emailAddress) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 2de2195..84bd29b 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -40,7 +40,7 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.AttentionStatus;
+import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
@@ -376,8 +376,9 @@
return state.reviewerUpdates();
}
- public ImmutableList<AttentionStatus> getAttentionUpdates() {
- return state.attentionUpdates();
+ /** Returns the most recent update (i.e. status) per user. */
+ public ImmutableList<AttentionSetUpdate> getAttentionSet() {
+ return state.attentionSet();
}
/**
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index ed6039f..6b6a7ca 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -59,7 +59,7 @@
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.AttentionStatus;
+import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.Comment;
@@ -118,7 +118,7 @@
private final List<Account.Id> allPastReviewers;
private final List<ReviewerStatusUpdate> reviewerUpdates;
/** Holds only the most recent update per user. Older updates are discarded. */
- private final Map<Account.Id, AttentionStatus> latestAttentionStatus;
+ private final Map<Account.Id, AttentionSetUpdate> latestAttentionStatus;
private final List<AssigneeStatusUpdate> assigneeUpdates;
private final List<SubmitRecord> submitRecords;
@@ -367,7 +367,7 @@
}
parseHashtags(commit);
- parseAttentionUpdates(commit);
+ parseAttentionSetUpdates(commit);
parseAssigneeUpdates(ts, commit);
if (submissionId == null) {
@@ -578,11 +578,11 @@
}
}
- private void parseAttentionUpdates(ChangeNotesCommit commit) throws ConfigInvalidException {
+ private void parseAttentionSetUpdates(ChangeNotesCommit commit) throws ConfigInvalidException {
List<String> attentionStrings = commit.getFooterLineValues(FOOTER_ATTENTION);
for (String attentionString : attentionStrings) {
- Optional<AttentionStatus> attentionStatus =
+ Optional<AttentionSetUpdate> attentionStatus =
ChangeNoteUtil.attentionStatusFromJson(
Instant.ofEpochSecond(commit.getCommitTime()), attentionString);
if (!attentionStatus.isPresent()) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 67faa33..9cd4af3 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -35,7 +35,7 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.AttentionStatus;
+import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
@@ -56,7 +56,7 @@
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AssigneeStatusUpdateProto;
-import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AttentionStatusProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AttentionSetUpdateProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ChangeColumnsProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
@@ -119,7 +119,7 @@
ReviewerByEmailSet pendingReviewersByEmail,
List<Account.Id> allPastReviewers,
List<ReviewerStatusUpdate> reviewerUpdates,
- List<AttentionStatus> attentionStatusUpdates,
+ List<AttentionSetUpdate> attentionSetUpdates,
List<AssigneeStatusUpdate> assigneeUpdates,
List<SubmitRecord> submitRecords,
List<ChangeMessage> changeMessages,
@@ -170,7 +170,7 @@
.pendingReviewersByEmail(pendingReviewersByEmail)
.allPastReviewers(allPastReviewers)
.reviewerUpdates(reviewerUpdates)
- .attentionUpdates(attentionStatusUpdates)
+ .attentionSet(attentionSetUpdates)
.assigneeUpdates(assigneeUpdates)
.submitRecords(submitRecords)
.changeMessages(changeMessages)
@@ -305,7 +305,8 @@
abstract ImmutableList<ReviewerStatusUpdate> reviewerUpdates();
- abstract ImmutableList<AttentionStatus> attentionUpdates();
+ /** Returns the most recent update (i.e. current status status) per user. */
+ abstract ImmutableList<AttentionSetUpdate> attentionSet();
abstract ImmutableList<AssigneeStatusUpdate> assigneeUpdates();
@@ -384,7 +385,7 @@
.pendingReviewersByEmail(ReviewerByEmailSet.empty())
.allPastReviewers(ImmutableList.of())
.reviewerUpdates(ImmutableList.of())
- .attentionUpdates(ImmutableList.of())
+ .attentionSet(ImmutableList.of())
.assigneeUpdates(ImmutableList.of())
.submitRecords(ImmutableList.of())
.changeMessages(ImmutableList.of())
@@ -418,7 +419,7 @@
abstract Builder reviewerUpdates(List<ReviewerStatusUpdate> reviewerUpdates);
- abstract Builder attentionUpdates(List<AttentionStatus> attentionUpdates);
+ abstract Builder attentionSet(List<AttentionSetUpdate> attentionSetUpdates);
abstract Builder assigneeUpdates(List<AssigneeStatusUpdate> assigneeUpdates);
@@ -487,7 +488,7 @@
object.allPastReviewers().forEach(a -> b.addPastReviewer(a.get()));
object.reviewerUpdates().forEach(u -> b.addReviewerUpdate(toReviewerStatusUpdateProto(u)));
- object.attentionUpdates().forEach(u -> b.addAttentionStatus(toAttentionStatusProto(u)));
+ object.attentionSet().forEach(u -> b.addAttentionSetUpdate(toAttentionSetUpdateProto(u)));
object.assigneeUpdates().forEach(u -> b.addAssigneeUpdate(toAssigneeStatusUpdateProto(u)));
object
.submitRecords()
@@ -571,12 +572,13 @@
.build();
}
- private static AttentionStatusProto toAttentionStatusProto(AttentionStatus attentionStatus) {
- return AttentionStatusProto.newBuilder()
- .setTimestampMillis(attentionStatus.timestamp().toEpochMilli())
- .setAccount(attentionStatus.account().get())
- .setOperation(attentionStatus.operation().name())
- .setReason(attentionStatus.reason())
+ private static AttentionSetUpdateProto toAttentionSetUpdateProto(
+ AttentionSetUpdate attentionSetUpdate) {
+ return AttentionSetUpdateProto.newBuilder()
+ .setTimestampMillis(attentionSetUpdate.timestamp().toEpochMilli())
+ .setAccount(attentionSetUpdate.account().get())
+ .setOperation(attentionSetUpdate.operation().name())
+ .setReason(attentionSetUpdate.reason())
.build();
}
@@ -620,7 +622,7 @@
.allPastReviewers(
proto.getPastReviewerList().stream().map(Account::id).collect(toImmutableList()))
.reviewerUpdates(toReviewerStatusUpdateList(proto.getReviewerUpdateList()))
- .attentionUpdates(toAttentionUpdateList(proto.getAttentionStatusList()))
+ .attentionSet(toAttentionSetUpdateList(proto.getAttentionSetUpdateList()))
.assigneeUpdates(toAssigneeStatusUpdateList(proto.getAssigneeUpdateList()))
.submitRecords(
proto.getSubmitRecordList().stream()
@@ -719,15 +721,15 @@
return b.build();
}
- private static ImmutableList<AttentionStatus> toAttentionUpdateList(
- List<AttentionStatusProto> protos) {
- ImmutableList.Builder<AttentionStatus> b = ImmutableList.builder();
- for (AttentionStatusProto proto : protos) {
+ private static ImmutableList<AttentionSetUpdate> toAttentionSetUpdateList(
+ List<AttentionSetUpdateProto> protos) {
+ ImmutableList.Builder<AttentionSetUpdate> b = ImmutableList.builder();
+ for (AttentionSetUpdateProto proto : protos) {
b.add(
- AttentionStatus.createFromRead(
+ AttentionSetUpdate.createFromRead(
Instant.ofEpochMilli(proto.getTimestampMillis()),
Account.id(proto.getAccount()),
- AttentionStatus.Operation.valueOf(proto.getOperation()),
+ AttentionSetUpdate.Operation.valueOf(proto.getOperation()),
proto.getReason()));
}
return b.build();
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 4492050..0de090f 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -54,7 +54,7 @@
import com.google.common.collect.TreeBasedTable;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.AttentionStatus;
+import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.Project;
@@ -128,7 +128,7 @@
private String submissionId;
private String topic;
private String commit;
- private List<AttentionStatus> attentionUpdates;
+ private List<AttentionSetUpdate> attentionSetUpdates;
private Optional<Account.Id> assignee;
private Set<String> hashtags;
private String changeMessage;
@@ -369,15 +369,15 @@
* All updates must have a timestamp of null since we use the commit's timestamp. There also must
* not be multiple updates for a single user.
*/
- void setAttentionUpdates(List<AttentionStatus> attentionUpdates) {
+ public void setAttentionSetUpdates(List<AttentionSetUpdate> attentionSetUpdates) {
checkArgument(
- attentionUpdates.stream().noneMatch(x -> x.timestamp() != null),
+ attentionSetUpdates.stream().noneMatch(a -> a.timestamp() != null),
"must not specify timestamp for write");
checkArgument(
- attentionUpdates.stream().map(AttentionStatus::account).distinct().count()
- == attentionUpdates.size(),
+ attentionSetUpdates.stream().map(AttentionSetUpdate::account).distinct().count()
+ == attentionSetUpdates.size(),
"must not specify multiple updates for single user");
- this.attentionUpdates = attentionUpdates;
+ this.attentionSetUpdates = attentionSetUpdates;
}
public void setAssignee(Account.Id assignee) {
@@ -588,9 +588,9 @@
addFooter(msg, FOOTER_COMMIT, commit);
}
- if (attentionUpdates != null) {
- for (AttentionStatus attentionUpdate : attentionUpdates) {
- addFooter(msg, FOOTER_ATTENTION, noteUtil.attentionStatusToJson(attentionUpdate));
+ if (attentionSetUpdates != null) {
+ for (AttentionSetUpdate attentionSetUpdate : attentionSetUpdates) {
+ addFooter(msg, FOOTER_ATTENTION, noteUtil.attentionSetUpdateToJson(attentionSetUpdate));
}
}
@@ -730,7 +730,7 @@
&& status == null
&& submissionId == null
&& submitRecords == null
- && attentionUpdates == null
+ && attentionSetUpdates == null
&& assignee == null
&& hashtags == null
&& topic == null
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 563ebb7..3dce678 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -37,6 +37,7 @@
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.Comment;
@@ -297,6 +298,7 @@
private List<ReviewerStatusUpdate> reviewerUpdates;
private PersonIdent author;
private PersonIdent committer;
+ private ImmutableList<AttentionSetUpdate> attentionSet;
private int parentCount;
private Integer unresolvedCommentCount;
private Integer totalCommentCount;
@@ -598,6 +600,17 @@
return true;
}
+ /** Returns the most recent update (i.e. status) per user. */
+ public ImmutableList<AttentionSetUpdate> attentionSet() {
+ if (attentionSet == null) {
+ if (!lazyLoad) {
+ return ImmutableList.of();
+ }
+ attentionSet = notes().getAttentionSet();
+ }
+ return attentionSet;
+ }
+
/** @return patches for the change, in patch set ID order. */
public Collection<PatchSet> patchSets() {
if (patchSets == null) {
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 43d7fc9..edd5411 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server.query.change;
-import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.entities.Change.CHANGE_ID_PATTERN;
import static com.google.gerrit.server.account.AccountResolver.isSelf;
import static com.google.gerrit.server.query.change.ChangeData.asChanges;
@@ -24,6 +23,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Enums;
import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Ints;
@@ -985,18 +985,23 @@
if (isSelf(who)) {
return isVisible();
}
+ Set<Account.Id> accounts = null;
try {
- return Predicate.or(
- parseAccount(who).stream()
- .map(a -> visibleto(args.userFactory.create(a)))
- .collect(toImmutableList()));
+ accounts = parseAccount(who);
} catch (QueryParseException e) {
if (e instanceof QueryRequiresAuthException) {
throw e;
}
- // Otherwise continue: if it's not an account, maybe it's a group?
+ }
+ if (accounts != null) {
+ if (accounts.size() == 1) {
+ return visibleto(args.userFactory.create(Iterables.getOnlyElement(accounts)));
+ } else if (accounts.size() > 1) {
+ throw error(String.format("\"%s\" resolves to multiple accounts", who));
+ }
}
+ // If its not an account, maybe its a group?
Collection<GroupReference> suggestions = args.groupBackend.suggest(who, null);
if (!suggestions.isEmpty()) {
HashSet<AccountGroup.UUID> ids = new HashSet<>();
diff --git a/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java b/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java
new file mode 100644
index 0000000..5176fe9
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java
@@ -0,0 +1,93 @@
+// 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.
+
+package com.google.gerrit.server.restapi.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.api.changes.AddToAttentionSetInput;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
+import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.change.AddToAttentionSetOp;
+import com.google.gerrit.server.change.AttentionSetEntryResource;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/** Adds a single user to the attention set. */
+@Singleton
+public class AddToAttentionSet
+ implements RestCollectionModifyView<
+ ChangeResource, AttentionSetEntryResource, AddToAttentionSetInput> {
+ private final BatchUpdate.Factory updateFactory;
+ private final AccountResolver accountResolver;
+ private final AddToAttentionSetOp.Factory opFactory;
+ private final AccountLoader.Factory accountLoaderFactory;
+ private final PermissionBackend permissionBackend;
+
+ @Inject
+ AddToAttentionSet(
+ BatchUpdate.Factory updateFactory,
+ AccountResolver accountResolver,
+ AddToAttentionSetOp.Factory opFactory,
+ AccountLoader.Factory accountLoaderFactory,
+ PermissionBackend permissionBackend) {
+ this.updateFactory = updateFactory;
+ this.accountResolver = accountResolver;
+ this.opFactory = opFactory;
+ this.accountLoaderFactory = accountLoaderFactory;
+ this.permissionBackend = permissionBackend;
+ }
+
+ @Override
+ public Response<AccountInfo> apply(ChangeResource changeResource, AddToAttentionSetInput input)
+ throws Exception {
+ input.user = Strings.nullToEmpty(input.user).trim();
+ if (input.user.isEmpty()) {
+ throw new BadRequestException("missing field: user");
+ }
+ input.reason = Strings.nullToEmpty(input.reason).trim();
+ if (input.reason.isEmpty()) {
+ throw new BadRequestException("missing field: reason");
+ }
+
+ Account.Id attentionUserId = accountResolver.resolve(input.user).asUnique().account().id();
+ try {
+ permissionBackend
+ .absentUser(attentionUserId)
+ .change(changeResource.getNotes())
+ .check(ChangePermission.READ);
+ } catch (AuthException e) {
+ throw new AuthException("read not permitted for " + attentionUserId, e);
+ }
+
+ try (BatchUpdate bu =
+ updateFactory.create(
+ changeResource.getChange().getProject(), changeResource.getUser(), TimeUtil.nowTs())) {
+ AddToAttentionSetOp op = opFactory.create(attentionUserId, input.reason);
+ bu.addOp(changeResource.getId(), op);
+ bu.execute();
+ return Response.ok(accountLoaderFactory.create(true).fillOne(attentionUserId));
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/AllowedFormats.java b/java/com/google/gerrit/server/restapi/change/AllowedFormats.java
index 2e313a1..ebec3295 100644
--- a/java/com/google/gerrit/server/restapi/change/AllowedFormats.java
+++ b/java/com/google/gerrit/server/restapi/change/AllowedFormats.java
@@ -18,7 +18,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
-import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.gerrit.server.change.ArchiveFormatInternal;
import com.google.gerrit.server.config.DownloadConfig;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -28,13 +28,13 @@
@Singleton
public class AllowedFormats {
- final ImmutableMap<String, ArchiveFormat> extensions;
- final ImmutableSet<ArchiveFormat> allowed;
+ final ImmutableMap<String, ArchiveFormatInternal> extensions;
+ final ImmutableSet<ArchiveFormatInternal> allowed;
@Inject
AllowedFormats(DownloadConfig cfg) {
- Map<String, ArchiveFormat> exts = new HashMap<>();
- for (ArchiveFormat format : cfg.getArchiveFormats()) {
+ Map<String, ArchiveFormatInternal> exts = new HashMap<>();
+ for (ArchiveFormatInternal format : cfg.getArchiveFormats()) {
for (String ext : format.getSuffixes()) {
exts.put(ext, format);
}
@@ -46,14 +46,14 @@
// valid JAR file, whose code would have access to cookies on the domain.
allowed =
Sets.immutableEnumSet(
- Iterables.filter(cfg.getArchiveFormats(), f -> f != ArchiveFormat.ZIP));
+ Iterables.filter(cfg.getArchiveFormats(), f -> f != ArchiveFormatInternal.ZIP));
}
- public Set<ArchiveFormat> getAllowed() {
+ public Set<ArchiveFormatInternal> getAllowed() {
return allowed;
}
- public ImmutableMap<String, ArchiveFormat> getExtensions() {
+ public ImmutableMap<String, ArchiveFormatInternal> getExtensions() {
return extensions;
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/AttentionSet.java b/java/com/google/gerrit/server/restapi/change/AttentionSet.java
new file mode 100644
index 0000000..45d78dc
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/AttentionSet.java
@@ -0,0 +1,69 @@
+// 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.
+
+package com.google.gerrit.server.restapi.change;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
+import com.google.gerrit.server.change.AttentionSetEntryResource;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class AttentionSet implements ChildCollection<ChangeResource, AttentionSetEntryResource> {
+ private final DynamicMap<RestView<AttentionSetEntryResource>> views;
+ private final AccountResolver accountResolver;
+ private final GetAttentionSet getAttentionSet;
+
+ @Inject
+ AttentionSet(
+ DynamicMap<RestView<AttentionSetEntryResource>> views,
+ GetAttentionSet getAttentionSet,
+ AccountResolver accountResolver) {
+ this.views = views;
+ this.accountResolver = accountResolver;
+ this.getAttentionSet = getAttentionSet;
+ }
+
+ @Override
+ public DynamicMap<RestView<AttentionSetEntryResource>> views() {
+ return views;
+ }
+
+ @Override
+ public RestView<ChangeResource> list() throws ResourceNotFoundException {
+ return getAttentionSet;
+ }
+
+ @Override
+ public AttentionSetEntryResource parse(ChangeResource changeResource, IdString idString)
+ throws ResourceNotFoundException, AuthException, IOException, ConfigInvalidException {
+ try {
+ Account.Id accountId = accountResolver.resolve(idString.get()).asUnique().account().id();
+ return new AttentionSetEntryResource(changeResource, accountId);
+ } catch (UnresolvableAccountException e) {
+ throw new ResourceNotFoundException(idString, e);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/GetArchive.java b/java/com/google/gerrit/server/restapi/change/GetArchive.java
index 4ebcbdd..7ab1432 100644
--- a/java/com/google/gerrit/server/restapi/change/GetArchive.java
+++ b/java/com/google/gerrit/server/restapi/change/GetArchive.java
@@ -17,12 +17,13 @@
import static com.google.gerrit.git.ObjectIds.abbreviateName;
import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.gerrit.server.change.ArchiveFormatInternal;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
@@ -38,9 +39,12 @@
public class GetArchive implements RestReadView<RevisionResource> {
private final GitRepositoryManager repoManager;
private final AllowedFormats allowedFormats;
+ @Nullable private String format;
@Option(name = "--format")
- private String format;
+ public void setFormat(String format) {
+ this.format = format;
+ }
@Inject
GetArchive(GitRepositoryManager repoManager, AllowedFormats allowedFormats) {
@@ -54,17 +58,17 @@
if (Strings.isNullOrEmpty(format)) {
throw new BadRequestException("format is not specified");
}
- final ArchiveFormat f = allowedFormats.extensions.get("." + format);
+ ArchiveFormatInternal f = allowedFormats.extensions.get("." + format);
if (f == null) {
throw new BadRequestException("unknown archive format");
}
- if (f == ArchiveFormat.ZIP) {
+ if (f == ArchiveFormatInternal.ZIP) {
throw new MethodNotAllowedException("zip format is disabled");
}
boolean close = true;
- final Repository repo = repoManager.openRepository(rsrc.getProject());
+ Repository repo = repoManager.openRepository(rsrc.getProject());
try {
- final RevCommit commit;
+ RevCommit commit;
String name;
try (RevWalk rw = new RevWalk(repo)) {
commit = rw.parseCommit(rsrc.getPatchSet().commitId());
@@ -103,7 +107,7 @@
}
}
- private static String name(ArchiveFormat format, RevWalk rw, RevCommit commit)
+ private static String name(ArchiveFormatInternal format, RevWalk rw, RevCommit commit)
throws IOException {
return String.format(
"%s%s", abbreviateName(commit, rw.getObjectReader()), format.getDefaultSuffix());
diff --git a/java/com/google/gerrit/server/restapi/change/GetAttentionSet.java b/java/com/google/gerrit/server/restapi/change/GetAttentionSet.java
new file mode 100644
index 0000000..5d6d03d
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/GetAttentionSet.java
@@ -0,0 +1,59 @@
+// 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.
+
+package com.google.gerrit.server.restapi.change;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.AttentionSetUpdate.Operation;
+import com.google.gerrit.extensions.common.AttentionSetEntry;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.sql.Timestamp;
+import java.util.List;
+
+/** Reads the list of users currently in the attention set. */
+@Singleton
+public class GetAttentionSet implements RestReadView<ChangeResource> {
+
+ private final AccountLoader.Factory accountLoaderFactory;
+
+ @Inject
+ GetAttentionSet(AccountLoader.Factory accountLoaderFactory) {
+ this.accountLoaderFactory = accountLoaderFactory;
+ }
+
+ @Override
+ public Response<List<AttentionSetEntry>> apply(ChangeResource changeResource)
+ throws PermissionBackendException {
+ AccountLoader accountLoader = accountLoaderFactory.create(true);
+ ImmutableList<AttentionSetEntry> response =
+ changeResource.getNotes().getAttentionSet().stream()
+ // This filtering should match ChangeJson.
+ .filter(a -> a.operation() == Operation.ADD)
+ .map(
+ a ->
+ new AttentionSetEntry(
+ accountLoader.get(a.account()), Timestamp.from(a.timestamp()), a.reason()))
+ .collect(toImmutableList());
+ accountLoader.fill();
+ return Response.ok(response);
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/Module.java b/java/com/google/gerrit/server/restapi/change/Module.java
index 453b4df..387d0a8 100644
--- a/java/com/google/gerrit/server/restapi/change/Module.java
+++ b/java/com/google/gerrit/server/restapi/change/Module.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.restapi.change;
+import static com.google.gerrit.server.change.AttentionSetEntryResource.ATTENTION_SET_ENTRY_KIND;
import static com.google.gerrit.server.change.ChangeEditResource.CHANGE_EDIT_KIND;
import static com.google.gerrit.server.change.ChangeMessageResource.CHANGE_MESSAGE_KIND;
import static com.google.gerrit.server.change.ChangeResource.CHANGE_KIND;
@@ -30,6 +31,7 @@
import com.google.gerrit.extensions.restapi.RestApiModule;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.change.AddReviewersOp;
+import com.google.gerrit.server.change.AddToAttentionSetOp;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.DeleteChangeOp;
@@ -38,6 +40,7 @@
import com.google.gerrit.server.change.EmailReviewComments;
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.change.RebaseChangeOp;
+import com.google.gerrit.server.change.RemoveFromAttentionSetOp;
import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.change.SetAssigneeOp;
import com.google.gerrit.server.change.SetCherryPickOp;
@@ -72,6 +75,7 @@
DynamicMap.mapOf(binder(), CHANGE_EDIT_KIND);
DynamicMap.mapOf(binder(), VOTE_KIND);
DynamicMap.mapOf(binder(), CHANGE_MESSAGE_KIND);
+ DynamicMap.mapOf(binder(), ATTENTION_SET_ENTRY_KIND);
postOnCollection(CHANGE_KIND).to(CreateChange.class);
get(CHANGE_KIND).to(GetChange.class);
@@ -79,6 +83,10 @@
get(CHANGE_KIND, "detail").to(GetDetail.class);
get(CHANGE_KIND, "topic").to(GetTopic.class);
get(CHANGE_KIND, "in").to(ChangeIncludedIn.class);
+ child(CHANGE_KIND, "attention").to(AttentionSet.class);
+ delete(ATTENTION_SET_ENTRY_KIND).to(RemoveFromAttentionSet.class);
+ post(ATTENTION_SET_ENTRY_KIND, "delete").to(RemoveFromAttentionSet.class);
+ postOnCollection(ATTENTION_SET_ENTRY_KIND).to(AddToAttentionSet.class);
get(CHANGE_KIND, "assignee").to(GetAssignee.class);
get(CHANGE_KIND, "past_assignees").to(GetPastAssignees.class);
put(CHANGE_KIND, "assignee").to(PutAssignee.class);
@@ -207,5 +215,7 @@
factory(SetPrivateOp.Factory.class);
factory(WorkInProgressOp.Factory.class);
factory(SetTopicOp.Factory.class);
+ factory(AddToAttentionSetOp.Factory.class);
+ factory(RemoveFromAttentionSetOp.Factory.class);
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java b/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
index e6a60d5..ed6c0a5 100644
--- a/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
+++ b/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
@@ -29,7 +29,7 @@
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.gerrit.server.change.ArchiveFormatInternal;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.ioutil.LimitedByteArrayOutputStream;
@@ -87,12 +87,12 @@
if (Strings.isNullOrEmpty(format)) {
throw new BadRequestException("format is not specified");
}
- ArchiveFormat f = allowedFormats.extensions.get("." + format);
+ ArchiveFormatInternal f = allowedFormats.extensions.get("." + format);
if (f == null && format.equals("tgz")) {
// Always allow tgz, even when the allowedFormats doesn't contain it.
// Then we allow at least one format even if the list of allowed
// formats is empty.
- f = ArchiveFormat.TGZ;
+ f = ArchiveFormatInternal.TGZ;
}
if (f == null) {
throw new BadRequestException("unknown archive format");
@@ -109,7 +109,7 @@
return Response.ok(getBundles(rsrc, f));
}
- private BinaryResult getBundles(RevisionResource rsrc, ArchiveFormat f)
+ private BinaryResult getBundles(RevisionResource rsrc, ArchiveFormatInternal f)
throws RestApiException, UpdateException, IOException, ConfigInvalidException,
PermissionBackendException {
IdentifiedUser caller = rsrc.getUser().asIdentifiedUser();
@@ -138,10 +138,11 @@
private static class SubmitPreviewResult extends BinaryResult {
private final MergeOp mergeOp;
- private final ArchiveFormat archiveFormat;
+ private final ArchiveFormatInternal archiveFormat;
private final int maxBundleSize;
- private SubmitPreviewResult(MergeOp mergeOp, ArchiveFormat archiveFormat, int maxBundleSize) {
+ private SubmitPreviewResult(
+ MergeOp mergeOp, ArchiveFormatInternal archiveFormat, int maxBundleSize) {
this.mergeOp = mergeOp;
this.archiveFormat = archiveFormat;
this.maxBundleSize = maxBundleSize;
diff --git a/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java b/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java
new file mode 100644
index 0000000..ccf375a
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java
@@ -0,0 +1,70 @@
+// 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.
+
+package com.google.gerrit.server.restapi.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.api.changes.RemoveFromAttentionSetInput;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.change.AttentionSetEntryResource;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.RemoveFromAttentionSetOp;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** Removes a single user from the attention set. */
+public class RemoveFromAttentionSet
+ implements RestModifyView<AttentionSetEntryResource, RemoveFromAttentionSetInput> {
+ private final BatchUpdate.Factory updateFactory;
+ private final RemoveFromAttentionSetOp.Factory opFactory;
+
+ @Inject
+ RemoveFromAttentionSet(
+ BatchUpdate.Factory updateFactory, RemoveFromAttentionSetOp.Factory opFactory) {
+ this.updateFactory = updateFactory;
+ this.opFactory = opFactory;
+ }
+
+ @Override
+ public Response<Object> apply(
+ AttentionSetEntryResource attentionResource, RemoveFromAttentionSetInput input)
+ throws RestApiException, PermissionBackendException, IOException, ConfigInvalidException,
+ UpdateException {
+ if (input == null) {
+ throw new BadRequestException("input may not be null");
+ }
+ input.reason = Strings.nullToEmpty(input.reason).trim();
+ if (input.reason.isEmpty()) {
+ throw new BadRequestException("missing field: reason");
+ }
+ ChangeResource changeResource = attentionResource.getChangeResource();
+ try (BatchUpdate bu =
+ updateFactory.create(
+ changeResource.getProject(), changeResource.getUser(), TimeUtil.nowTs())) {
+ RemoveFromAttentionSetOp op =
+ opFactory.create(attentionResource.getAccountId(), input.reason);
+ bu.addOp(changeResource.getId(), op);
+ bu.execute();
+ }
+ return Response.none();
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index 4ddc3e8..c83bf42 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -43,7 +43,7 @@
import com.google.gerrit.server.account.AccountVisibilityProvider;
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.avatar.AvatarProvider;
-import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.gerrit.server.change.ArchiveFormatInternal;
import com.google.gerrit.server.change.MergeabilityComputationBehavior;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
@@ -254,7 +254,9 @@
}
});
info.archives =
- archiveFormats.getAllowed().stream().map(ArchiveFormat::getShortName).collect(toList());
+ archiveFormats.getAllowed().stream()
+ .map(ArchiveFormatInternal::getShortName)
+ .collect(toList());
return info;
}
diff --git a/java/com/google/gerrit/server/restapi/project/CreateBranch.java b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
index c15fdeb..b901057 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
@@ -17,7 +17,6 @@
import static com.google.gerrit.entities.RefNames.isConfigRef;
import com.google.common.base.Strings;
-import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.projects.BranchInfo;
@@ -46,7 +45,6 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.RefUpdate;
@@ -58,8 +56,6 @@
@Singleton
public class CreateBranch
implements RestCollectionCreateView<ProjectResource, BranchResource, BranchInput> {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
private final Provider<IdentifiedUser> identifiedUser;
private final PermissionBackend permissionBackend;
private final GitRepositoryManager repoManager;
@@ -114,7 +110,7 @@
+ "\"");
}
- final BranchNameKey name = BranchNameKey.create(rsrc.getNameKey(), ref);
+ BranchNameKey name = BranchNameKey.create(rsrc.getNameKey(), ref);
try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
ObjectId revid = RefUtil.parseBaseRevision(repo, rsrc.getNameKey(), input.revision);
RevWalk rw = RefUtil.verifyConnected(repo, revid);
@@ -122,80 +118,71 @@
if (ref.startsWith(Constants.R_HEADS)) {
// Ensure that what we start the branch from is a commit. If we
- // were given a tag, deference to the commit instead.
+ // were given a tag, dereference to the commit instead.
//
- try {
- object = rw.parseCommit(object);
- } catch (IncorrectObjectTypeException notCommit) {
- throw new BadRequestException("\"" + input.revision + "\" not a commit", notCommit);
- }
+ object = rw.parseCommit(object);
}
createRefControl.checkCreateRef(identifiedUser, repo, name, object);
- try {
- final RefUpdate u = repo.updateRef(ref);
- u.setExpectedOldObjectId(ObjectId.zeroId());
- u.setNewObjectId(object.copy());
- u.setRefLogIdent(identifiedUser.get().newRefLogIdent());
- u.setRefLogMessage("created via REST from " + input.revision, false);
- refCreationValidator.validateRefOperation(rsrc.getName(), identifiedUser.get(), u);
- final RefUpdate.Result result = u.update(rw);
- switch (result) {
- case FAST_FORWARD:
- case NEW:
- case NO_CHANGE:
- referenceUpdated.fire(
- name.project(), u, ReceiveCommand.Type.CREATE, identifiedUser.get().state());
- break;
- case LOCK_FAILURE:
- if (repo.getRefDatabase().exactRef(ref) != null) {
- throw new ResourceConflictException("branch \"" + ref + "\" already exists");
+ RefUpdate u = repo.updateRef(ref);
+ u.setExpectedOldObjectId(ObjectId.zeroId());
+ u.setNewObjectId(object.copy());
+ u.setRefLogIdent(identifiedUser.get().newRefLogIdent());
+ u.setRefLogMessage("created via REST from " + input.revision, false);
+ refCreationValidator.validateRefOperation(rsrc.getName(), identifiedUser.get(), u);
+ RefUpdate.Result result = u.update(rw);
+ switch (result) {
+ case FAST_FORWARD:
+ case NEW:
+ case NO_CHANGE:
+ referenceUpdated.fire(
+ name.project(), u, ReceiveCommand.Type.CREATE, identifiedUser.get().state());
+ break;
+ case LOCK_FAILURE:
+ if (repo.getRefDatabase().exactRef(ref) != null) {
+ throw new ResourceConflictException("branch \"" + ref + "\" already exists");
+ }
+ String refPrefix = RefUtil.getRefPrefix(ref);
+ while (!Constants.R_HEADS.equals(refPrefix)) {
+ if (repo.getRefDatabase().exactRef(refPrefix) != null) {
+ throw new ResourceConflictException(
+ "Cannot create branch \""
+ + ref
+ + "\" since it conflicts with branch \""
+ + refPrefix
+ + "\".");
}
- String refPrefix = RefUtil.getRefPrefix(ref);
- while (!Constants.R_HEADS.equals(refPrefix)) {
- if (repo.getRefDatabase().exactRef(refPrefix) != null) {
- throw new ResourceConflictException(
- "Cannot create branch \""
- + ref
- + "\" since it conflicts with branch \""
- + refPrefix
- + "\".");
- }
- refPrefix = RefUtil.getRefPrefix(refPrefix);
- }
- throw new LockFailureException(String.format("Failed to create %s", ref), u);
- case FORCED:
- case IO_FAILURE:
- case NOT_ATTEMPTED:
- case REJECTED:
- case REJECTED_CURRENT_BRANCH:
- case RENAMED:
- case REJECTED_MISSING_OBJECT:
- case REJECTED_OTHER_REASON:
- default:
- throw new IOException(String.format("Failed to create %s: %s", ref, result.name()));
- }
-
- BranchInfo info = new BranchInfo();
- info.ref = ref;
- info.revision = revid.getName();
-
- if (isConfigRef(name.branch())) {
- // Never allow to delete the meta config branch.
- info.canDelete = null;
- } else {
- info.canDelete =
- permissionBackend.currentUser().ref(name).testOrFalse(RefPermission.DELETE)
- && rsrc.getProjectState().statePermitsWrite()
- ? true
- : null;
- }
- return Response.created(info);
- } catch (IOException err) {
- logger.atSevere().withCause(err).log("Cannot create branch \"%s\"", name);
- throw err;
+ refPrefix = RefUtil.getRefPrefix(refPrefix);
+ }
+ throw new LockFailureException(String.format("Failed to create %s", ref), u);
+ case FORCED:
+ case IO_FAILURE:
+ case NOT_ATTEMPTED:
+ case REJECTED:
+ case REJECTED_CURRENT_BRANCH:
+ case RENAMED:
+ case REJECTED_MISSING_OBJECT:
+ case REJECTED_OTHER_REASON:
+ default:
+ throw new IOException(String.format("Failed to create %s: %s", ref, result.name()));
}
+
+ BranchInfo info = new BranchInfo();
+ info.ref = ref;
+ info.revision = revid.getName();
+
+ if (isConfigRef(name.branch())) {
+ // Never allow to delete the meta config branch.
+ info.canDelete = null;
+ } else {
+ info.canDelete =
+ permissionBackend.currentUser().ref(name).testOrFalse(RefPermission.DELETE)
+ && rsrc.getProjectState().statePermitsWrite()
+ ? true
+ : null;
+ }
+ return Response.created(info);
} catch (RefUtil.InvalidRevisionException e) {
throw new BadRequestException("invalid revision \"" + input.revision + "\"", e);
}
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
index 9b16265..5d5e779 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
@@ -19,6 +19,7 @@
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.entities.Project;
@@ -150,12 +151,18 @@
throw new BadRequestException("invalid section name");
}
RefPattern.validate(name);
+
+ // Check all permissions for soundness
+ for (Permission p : section.getPermissions()) {
+ if (!isPermission(p.getName())) {
+ throw new BadRequestException("Unknown permission: " + p.getName());
+ }
+ }
} else {
// Check all permissions for soundness
for (Permission p : section.getPermissions()) {
if (!isCapability(p.getName())) {
- throw new BadRequestException(
- "Cannot add non-global capability " + p.getName() + " to global capabilities");
+ throw new BadRequestException("Unknown global capability: " + p.getName());
}
}
}
@@ -240,6 +247,23 @@
}
}
+ private boolean isPermission(String name) {
+ if (Permission.isPermission(name)) {
+ if (Permission.isLabel(name) || Permission.isLabelAs(name)) {
+ String labelName = Permission.extractLabel(name);
+ try {
+ LabelType.checkName(labelName);
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+ return true;
+ }
+ Set<String> pluginPermissions =
+ pluginPermissionsUtil.collectPluginProjectPermissions().keySet();
+ return pluginPermissions.contains(name);
+ }
+
private boolean isCapability(String name) {
if (GlobalCapability.isGlobalCapability(name)) {
return true;
diff --git a/java/com/google/gerrit/sshd/commands/UploadArchive.java b/java/com/google/gerrit/sshd/commands/UploadArchive.java
index 8543a1c..67dc5a5 100644
--- a/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -20,7 +20,7 @@
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.gerrit.server.change.ArchiveFormatInternal;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
@@ -174,7 +174,7 @@
// Parse Git arguments
readArguments();
- ArchiveFormat f = allowedFormats.getExtensions().get("." + options.format);
+ ArchiveFormatInternal f = allowedFormats.getExtensions().get("." + options.format);
if (f == null) {
throw new Failure(3, "fatal: upload-archive not permitted for format " + options.format);
}
@@ -222,8 +222,8 @@
}
}
- private Map<String, Object> getFormatOptions(ArchiveFormat f) {
- if (f == ArchiveFormat.ZIP) {
+ private Map<String, Object> getFormatOptions(ArchiveFormatInternal f) {
+ if (f == ArchiveFormatInternal.ZIP) {
int value =
Arrays.asList(
options.level0,
diff --git a/java/com/google/gerrit/util/logging/BUILD b/java/com/google/gerrit/util/logging/BUILD
index b8db49bd..ee598a4 100644
--- a/java/com/google/gerrit/util/logging/BUILD
+++ b/java/com/google/gerrit/util/logging/BUILD
@@ -8,6 +8,7 @@
visibility = ["//visibility:public"],
deps = [
"//lib:gson",
+ "//lib/flogger:api",
"//lib/log:log4j",
],
)
diff --git a/java/com/google/gerrit/util/logging/NamedFluentLogger.java b/java/com/google/gerrit/util/logging/NamedFluentLogger.java
new file mode 100644
index 0000000..04fc18d
--- /dev/null
+++ b/java/com/google/gerrit/util/logging/NamedFluentLogger.java
@@ -0,0 +1,84 @@
+// 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.
+
+package com.google.gerrit.util.logging;
+
+import com.google.common.flogger.AbstractLogger;
+import com.google.common.flogger.LogContext;
+import com.google.common.flogger.LoggingApi;
+import com.google.common.flogger.backend.LoggerBackend;
+import com.google.common.flogger.backend.Platform;
+import com.google.common.flogger.parser.DefaultPrintfMessageParser;
+import com.google.common.flogger.parser.MessageParser;
+import java.util.logging.Level;
+
+/**
+ * FluentLogger.forEnclosingClass() searches for caller class name and passes it as String to
+ * constructor FluentLogger.FluentLogger(LoggerBackend) (which is package protected).
+ *
+ * <p>This allows to create NamedFluentLogger with given name so that dedicated configuration can be
+ * specified by a custom appender in the log4j.properties file. An example of this is the logger
+ * used by the replication queue in the replication plugin, and gerrit's Garbage Collection log.
+ */
+public class NamedFluentLogger extends AbstractLogger<NamedFluentLogger.Api> {
+ /** Copied from FluentLogger */
+ public interface Api extends LoggingApi<Api> {}
+
+ /** Copied from FluentLogger */
+ private static final class NoOp extends LoggingApi.NoOp<Api> implements Api {}
+
+ private static final NoOp NO_OP = new NoOp();
+
+ public static NamedFluentLogger forName(String name) {
+ return new NamedFluentLogger(Platform.getBackend(name));
+ }
+
+ private NamedFluentLogger(LoggerBackend backend) {
+ super(backend);
+ }
+
+ @Override
+ public Api at(Level level) {
+ boolean isLoggable = isLoggable(level);
+ boolean isForced = Platform.shouldForceLogging(getName(), level, isLoggable);
+ return (isLoggable || isForced) ? new Context(level, isForced) : NO_OP;
+ }
+
+ /** Copied from FluentLogger */
+ private final class Context extends LogContext<NamedFluentLogger, Api> implements Api {
+ private Context(Level level, boolean isForced) {
+ super(level, isForced);
+ }
+
+ @Override
+ protected NamedFluentLogger getLogger() {
+ return NamedFluentLogger.this;
+ }
+
+ @Override
+ protected Api api() {
+ return this;
+ }
+
+ @Override
+ protected Api noOp() {
+ return NO_OP;
+ }
+
+ @Override
+ protected MessageParser getMessageParser() {
+ return DefaultPrintfMessageParser.getInstance();
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 9399c3b..50aaa27 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -3210,7 +3210,8 @@
mergeInput.source = "dev";
MergePatchSetInput in = new MergePatchSetInput();
in.merge = mergeInput;
- in.subject = "update change by merge ps2";
+ String subject = "update change by merge ps2";
+ in.subject = subject;
TestWorkInProgressStateChangedListener wipStateChangedListener =
new TestWorkInProgressStateChangedListener();
@@ -3234,6 +3235,16 @@
List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages();
assertThat(messages).hasSize(2);
assertThat(Iterables.getLast(messages).message).isEqualTo("Uploaded patch set 2.");
+
+ assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.message)
+ .contains(subject);
+
+ // No subject: reuse message from previous patchset.
+ in.subject = null;
+ gApi.changes().id(changeId).createMergePatchSet(in);
+ changeInfo = gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION);
+ assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.message)
+ .contains(subject);
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java
index a704f0c..40dd70e 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java
@@ -17,7 +17,6 @@
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.common.data.SubmitRecord;
@@ -39,14 +38,9 @@
public class ChangeSubmitRequirementIT extends AbstractDaemonTest {
private static final SubmitRequirement req =
- SubmitRequirement.builder()
- .setType("custom_rule")
- .setFallbackText("Fallback text")
- .addCustomValue("key", "value")
- .build();
+ SubmitRequirement.builder().setType("custom_rule").setFallbackText("Fallback text").build();
private static final SubmitRequirementInfo reqInfo =
- new SubmitRequirementInfo(
- "NOT_READY", "Fallback text", "custom_rule", ImmutableMap.of("key", "value"));
+ new SubmitRequirementInfo("NOT_READY", "Fallback text", "custom_rule");
@Override
public Module createModule() {
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
index 27dd16a..4163e17 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
@@ -53,14 +53,14 @@
PushOneCommit.Result r = createChange("refs/for/master");
String branch = r.getChange().change().getDest().branch();
- ChangeInfo info = gApi.changes().id(r.getChange().getId().get()).info();
+ ChangeInfo info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
CheckProjectResultInfo checkResult =
gApi.projects().name(project.get()).check(checkProjectInputForAutoCloseableCheck(branch));
assertThat(checkResult.autoCloseableChangesCheckResult.autoCloseableChanges).isEmpty();
- info = gApi.changes().id(r.getChange().getId().get()).info();
+ info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
}
@@ -121,7 +121,7 @@
RevCommit amendedCommit = serverSideTestRepo.amend(r.getCommit()).create();
serverSideTestRepo.branch(branch).update(amendedCommit);
- ChangeInfo info = gApi.changes().id(r.getChange().getId().get()).info();
+ ChangeInfo info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
CheckProjectResultInfo checkResult =
@@ -132,7 +132,7 @@
.collect(toSet()))
.containsExactly(r.getChange().getId().get());
- info = gApi.changes().id(r.getChange().getId().get()).info();
+ info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
}
@@ -144,7 +144,7 @@
RevCommit amendedCommit = serverSideTestRepo.amend(r.getCommit()).create();
serverSideTestRepo.branch(branch).update(amendedCommit);
- ChangeInfo info = gApi.changes().id(r.getChange().getId().get()).info();
+ ChangeInfo info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
CheckProjectInput input = checkProjectInputForAutoCloseableCheck(branch);
@@ -156,7 +156,7 @@
.collect(toSet()))
.containsExactly(r.getChange().getId().get());
- info = gApi.changes().id(r.getChange().getId().get()).info();
+ info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
}
@@ -170,7 +170,7 @@
serverSideTestRepo.commit(amendedCommit);
- ChangeInfo info = gApi.changes().id(r.getChange().getId().get()).info();
+ ChangeInfo info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
CheckProjectInput input = checkProjectInputForAutoCloseableCheck(branch);
@@ -179,7 +179,7 @@
CheckProjectResultInfo checkResult = gApi.projects().name(project.get()).check(input);
assertThat(checkResult.autoCloseableChangesCheckResult.autoCloseableChanges).isEmpty();
- info = gApi.changes().id(r.getChange().getId().get()).info();
+ info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
input.autoCloseableChangesCheck.maxCommits = 2;
@@ -190,7 +190,7 @@
.collect(toSet()))
.containsExactly(r.getChange().getId().get());
- info = gApi.changes().id(r.getChange().getId().get()).info();
+ info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
}
@@ -204,7 +204,7 @@
serverSideTestRepo.commit(amendedCommit);
- ChangeInfo info = gApi.changes().id(r.getChange().getId().get()).info();
+ ChangeInfo info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
CheckProjectInput input = checkProjectInputForAutoCloseableCheck(branch);
@@ -213,7 +213,7 @@
CheckProjectResultInfo checkResult = gApi.projects().name(project.get()).check(input);
assertThat(checkResult.autoCloseableChangesCheckResult.autoCloseableChanges).isEmpty();
- info = gApi.changes().id(r.getChange().getId().get()).info();
+ info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
input.autoCloseableChangesCheck.skipCommits = 1;
@@ -224,7 +224,7 @@
.collect(toSet()))
.containsExactly(r.getChange().getId().get());
- info = gApi.changes().id(r.getChange().getId().get()).info();
+ info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
}
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index cad20b6..8dc76dd 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -894,14 +894,10 @@
@Test
public void getProjectThatHasInvalidProjectConfig() throws Exception {
- // Make the project config invalid by adding permission entry with an invalid permission name.
projectOperations
.project(allProjects)
.forInvalidation()
- .addProjectConfigUpdater(
- cfg ->
- cfg.setString(
- "access", "refs/*", "Invalid Permission Name", "group Administrators"))
+ .makeProjectConfigInvalid()
.invalidate();
// We must test this via the REST API since ExceptionHook is not invoked from the Java API.
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index f001a74..3ec44e8 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -303,13 +303,7 @@
PushOneCommit.Result r = createChange();
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
- assertThrows(
- AuthException.class,
- () ->
- gApi.changes()
- .id(r.getChange().getId().get())
- .current()
- .review(ReviewInput.approve()));
+ assertThrows(AuthException.class, () -> change(r).current().review(ReviewInput.approve()));
assertThat(thrown).hasMessageThat().contains("is restricted");
}
@@ -560,7 +554,7 @@
PushOneCommit.Result r = push.to("refs/for/master%topic=someTopic");
// Verify before the cherry-pick that the change has exactly 1 message.
- ChangeApi changeApi = gApi.changes().id(r.getChange().getId().get());
+ ChangeApi changeApi = change(r);
assertThat(changeApi.get().messages).hasSize(1);
// Cherry-pick the change to the other branch, that should fail with a conflict.
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index f190d59..d1e8cc5 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -437,7 +437,7 @@
r.assertErrorStatus("change " + url + " closed");
// Check change message that was added on auto-close
- ChangeInfo change = gApi.changes().id(r.getChange().getId().get()).get();
+ ChangeInfo change = change(r).get();
assertThat(Iterables.getLast(change.messages).message)
.isEqualTo("Change has been successfully pushed.");
}
@@ -477,7 +477,7 @@
r.assertErrorStatus("change " + url + " closed");
// Check that new commit was added as patch set
- ChangeInfo change = gApi.changes().id(r.getChange().getId().get()).get();
+ ChangeInfo change = change(r).get();
assertThat(change.revisions).hasSize(2);
assertThat(change.currentRevision).isEqualTo(c.name());
}
diff --git a/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java b/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
index 876e342..d7952e4 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
@@ -29,6 +29,7 @@
import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -206,4 +207,22 @@
r4.assertErrorStatus(UPDATE_NONFASTFORWARD.name());
}
}
+
+ @Test
+ @GerritConfig(name = "change.maxFiles", value = "0")
+ public void dontEnforceFileCountForDirectPushes() throws Exception {
+ PushOneCommit push =
+ pushFactory.create(admin.newIdent(), testRepo, "change", "c.txt", "content");
+ PushOneCommit.Result result = push.to("refs/heads/master");
+ result.assertOkStatus();
+ }
+
+ @Test
+ @GerritConfig(name = "change.maxFiles", value = "0")
+ public void enforceFileCountLimitOnPushesForReview() throws Exception {
+ PushOneCommit push =
+ pushFactory.create(admin.newIdent(), testRepo, "change", "c.txt", "content");
+ PushOneCommit.Result result = push.to("refs/for/master");
+ result.assertErrorStatus("Exceeding maximum number of files per change");
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
index 83bc3eb..574e919 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
@@ -63,6 +63,8 @@
RestCall.get("/changes/%s/comments"),
RestCall.get("/changes/%s/robotcomments"),
RestCall.get("/changes/%s/drafts"),
+ RestCall.get("/changes/%s/attention"),
+ RestCall.post("/changes/%s/attention"),
RestCall.get("/changes/%s/assignee"),
RestCall.get("/changes/%s/past_assignees"),
RestCall.put("/changes/%s/assignee"),
@@ -267,6 +269,11 @@
// Delete content of a file in an existing change edit.
RestCall.delete("/changes/%s/edit/%s"));
+ private static final ImmutableList<RestCall> ATTENTION_SET_ENDPOINTS =
+ ImmutableList.of(
+ RestCall.post("/changes/%s/attention/%s/delete"),
+ RestCall.delete("/changes/%s/attention/%s"));
+
private static final String FILENAME = "test.txt";
@Test
@@ -477,6 +484,14 @@
RestApiCallHelper.execute(adminRestSession, CHANGE_EDIT_ENDPOINTS, changeId, FILENAME);
}
+ @Test
+ public void attentionSetEndpoints() throws Exception {
+ String changeId = createChange().getChangeId();
+ gApi.changes().id(changeId).edit().create();
+ RestApiCallHelper.execute(
+ adminRestSession, ATTENTION_SET_ENDPOINTS, changeId, user.id().toString());
+ }
+
private static Comment.Range createRange(
int startLine, int startCharacter, int endLine, int endCharacter) {
Comment.Range range = new Comment.Range();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
index 420ddda..2d47dd8 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
@@ -188,11 +188,11 @@
}
private AccountInfo getAssignee(PushOneCommit.Result r) throws Exception {
- return gApi.changes().id(r.getChange().getId().get()).getAssignee();
+ return change(r).getAssignee();
}
private List<AccountInfo> getPastAssignees(PushOneCommit.Result r) throws Exception {
- return gApi.changes().id(r.getChange().getId().get()).getPastAssignees();
+ return change(r).getPastAssignees();
}
private Iterable<AccountInfo> getReviewers(PushOneCommit.Result r, ReviewerState state)
@@ -203,10 +203,10 @@
private AccountInfo setAssignee(PushOneCommit.Result r, String identifieer) throws Exception {
AssigneeInput input = new AssigneeInput();
input.assignee = identifieer;
- return gApi.changes().id(r.getChange().getId().get()).setAssignee(input);
+ return change(r).setAssignee(input);
}
private AccountInfo deleteAssignee(PushOneCommit.Result r) throws Exception {
- return gApi.changes().id(r.getChange().getId().get()).deleteAssignee();
+ return change(r).deleteAssignee();
}
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
new file mode 100644
index 0000000..caa8832
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -0,0 +1,136 @@
+// 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.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.entities.AttentionSetUpdate;
+import com.google.gerrit.extensions.api.changes.AddToAttentionSetInput;
+import com.google.gerrit.extensions.api.changes.RemoveFromAttentionSetInput;
+import com.google.gerrit.server.util.time.TimeUtil;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
+import java.util.function.LongSupplier;
+import org.junit.Before;
+import org.junit.Test;
+
+@NoHttpd
+@UseClockStep(clockStepUnit = TimeUnit.MINUTES)
+public class AttentionSetIT extends AbstractDaemonTest {
+ /** Simulates a fake clock. Uses second granularity. */
+ private static class FakeClock implements LongSupplier {
+ Instant now = Instant.now();
+
+ @Override
+ public long getAsLong() {
+ return TimeUnit.SECONDS.toMillis(now.getEpochSecond());
+ }
+
+ Instant now() {
+ return Instant.ofEpochSecond(now.getEpochSecond());
+ }
+
+ void advance(Duration duration) {
+ now = now.plus(duration);
+ }
+ }
+
+ private FakeClock fakeClock = new FakeClock();
+
+ @Before
+ public void setUp() {
+ TimeUtil.setCurrentMillisSupplier(fakeClock);
+ }
+
+ @Test
+ public void emptyAttentionSet() throws Exception {
+ PushOneCommit.Result r = createChange();
+ assertThat(r.getChange().attentionSet()).isEmpty();
+ }
+
+ @Test
+ public void addUser() throws Exception {
+ PushOneCommit.Result r = createChange();
+ int accountId =
+ change(r).addToAttentionSet(new AddToAttentionSetInput(user.email(), "first"))._accountId;
+ assertThat(accountId).isEqualTo(user.id().get());
+ AttentionSetUpdate expectedAttentionSetUpdate =
+ AttentionSetUpdate.createFromRead(
+ fakeClock.now(), user.id(), AttentionSetUpdate.Operation.ADD, "first");
+ assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
+
+ // Second add is ignored.
+ accountId =
+ change(r).addToAttentionSet(new AddToAttentionSetInput(user.email(), "second"))._accountId;
+ assertThat(accountId).isEqualTo(user.id().get());
+ assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
+ }
+
+ @Test
+ public void addMultipleUsers() throws Exception {
+ PushOneCommit.Result r = createChange();
+ Instant timestamp1 = fakeClock.now();
+ int accountId1 =
+ change(r).addToAttentionSet(new AddToAttentionSetInput(user.email(), "user"))._accountId;
+ assertThat(accountId1).isEqualTo(user.id().get());
+ fakeClock.advance(Duration.ofSeconds(42));
+ Instant timestamp2 = fakeClock.now();
+ int accountId2 =
+ change(r)
+ .addToAttentionSet(new AddToAttentionSetInput(admin.id().toString(), "admin"))
+ ._accountId;
+ assertThat(accountId2).isEqualTo(admin.id().get());
+
+ AttentionSetUpdate expectedAttentionSetUpdate1 =
+ AttentionSetUpdate.createFromRead(
+ timestamp1, user.id(), AttentionSetUpdate.Operation.ADD, "user");
+ AttentionSetUpdate expectedAttentionSetUpdate2 =
+ AttentionSetUpdate.createFromRead(
+ timestamp2, admin.id(), AttentionSetUpdate.Operation.ADD, "admin");
+ assertThat(r.getChange().attentionSet())
+ .containsExactly(expectedAttentionSetUpdate1, expectedAttentionSetUpdate2);
+ }
+
+ @Test
+ public void removeUser() throws Exception {
+ PushOneCommit.Result r = createChange();
+ change(r).addToAttentionSet(new AddToAttentionSetInput(user.email(), "added"));
+ fakeClock.advance(Duration.ofSeconds(42));
+ change(r).attention(user.id().toString()).remove(new RemoveFromAttentionSetInput("removed"));
+ AttentionSetUpdate expectedAttentionSetUpdate =
+ AttentionSetUpdate.createFromRead(
+ fakeClock.now(), user.id(), AttentionSetUpdate.Operation.REMOVE, "removed");
+ assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
+
+ // Second removal is ignored.
+ fakeClock.advance(Duration.ofSeconds(42));
+ change(r)
+ .attention(user.id().toString())
+ .remove(new RemoveFromAttentionSetInput("removed again"));
+ assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
+ }
+
+ @Test
+ public void removeUnrelatedUser() throws Exception {
+ PushOneCommit.Result r = createChange();
+ change(r).attention(user.id().toString()).remove(new RemoveFromAttentionSetInput("foo"));
+ assertThat(r.getChange().attentionSet()).isEmpty();
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/GetArchiveIT.java b/javatests/com/google/gerrit/acceptance/rest/change/GetArchiveIT.java
new file mode 100644
index 0000000..15e6360
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/change/GetArchiveIT.java
@@ -0,0 +1,154 @@
+// 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.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.client.ArchiveFormat;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.git.ObjectIds;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.HashMap;
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GetArchiveIT extends AbstractDaemonTest {
+ private static final String DIRECTORY_NAME = "foo";
+ private static final String FILE_NAME = DIRECTORY_NAME + "/bar.txt";
+ private static final String FILE_CONTENT = "some content";
+
+ private String changeId;
+ private RevCommit commit;
+
+ @Before
+ public void setUp() throws Exception {
+ PushOneCommit push =
+ pushFactory.create(admin.newIdent(), testRepo, "My Change", FILE_NAME, FILE_CONTENT);
+ PushOneCommit.Result result = push.to("refs/for/master");
+ result.assertOkStatus();
+
+ changeId = result.getChangeId();
+ commit = result.getCommit();
+ }
+
+ @Test
+ public void formatNotSpecified() throws Exception {
+ BadRequestException ex =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.changes().id(changeId).current().getArchive(null));
+ assertThat(ex).hasMessageThat().isEqualTo("format is not specified");
+ }
+
+ @Test
+ public void unknownFormat() throws Exception {
+ // Test this by a REST call, since the Java API doesn't allow to specify an unknown format.
+ RestResponse res =
+ adminRestSession.get(
+ String.format(
+ "/changes/%s/revisions/current/archive?format=%s", changeId, "unknownFormat"));
+ res.assertBadRequest();
+ assertThat(res.getEntityContent()).isEqualTo("unknown archive format");
+ }
+
+ @Test
+ public void zipFormatIsDisabled() throws Exception {
+ MethodNotAllowedException ex =
+ assertThrows(
+ MethodNotAllowedException.class,
+ () -> gApi.changes().id(changeId).current().getArchive(ArchiveFormat.ZIP));
+ assertThat(ex).hasMessageThat().isEqualTo("zip format is disabled");
+ }
+
+ @Test
+ public void getTarArchive() throws Exception {
+ BinaryResult res = gApi.changes().id(changeId).current().getArchive(ArchiveFormat.TAR);
+ assertThat(res.getAttachmentName())
+ .isEqualTo(commit.abbreviate(ObjectIds.ABBREV_STR_LEN).name() + ".tar");
+ assertThat(res.getContentType()).isEqualTo("application/x-tar");
+ assertThat(res.canGzip()).isFalse();
+
+ byte[] archiveBytes = getBinaryContent(res);
+ try (ByteArrayInputStream in = new ByteArrayInputStream(archiveBytes)) {
+ HashMap<String, String> archiveEntries = getTarContent(in);
+ assertThat(archiveEntries)
+ .containsExactly(DIRECTORY_NAME + "/", null, FILE_NAME, FILE_CONTENT);
+ }
+ }
+
+ @Test
+ public void getTgzArchive() throws Exception {
+ BinaryResult res = gApi.changes().id(changeId).current().getArchive(ArchiveFormat.TGZ);
+ assertThat(res.getAttachmentName())
+ .isEqualTo(commit.abbreviate(ObjectIds.ABBREV_STR_LEN).name() + ".tar.gz");
+ assertThat(res.getContentType()).isEqualTo("application/x-gzip");
+ assertThat(res.canGzip()).isFalse();
+
+ byte[] archiveBytes = getBinaryContent(res);
+ try (ByteArrayInputStream in = new ByteArrayInputStream(archiveBytes);
+ GzipCompressorInputStream gzipIn = new GzipCompressorInputStream(in)) {
+ HashMap<String, String> archiveEntries = getTarContent(gzipIn);
+ assertThat(archiveEntries)
+ .containsExactly(DIRECTORY_NAME + "/", null, FILE_NAME, FILE_CONTENT);
+ }
+ }
+
+ private HashMap<String, String> getTarContent(InputStream in) throws Exception {
+ HashMap<String, String> archiveEntries = new HashMap<>();
+ int bufferSize = 100;
+ try (TarArchiveInputStream tarIn = new TarArchiveInputStream(in)) {
+ TarArchiveEntry entry;
+ while ((entry = tarIn.getNextTarEntry()) != null) {
+ if (entry.isDirectory()) {
+ archiveEntries.put(entry.getName(), null);
+ } else {
+ byte data[] = new byte[bufferSize];
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream();
+ BufferedOutputStream bufferedOut = new BufferedOutputStream(out, bufferSize)) {
+ int count;
+ while ((count = tarIn.read(data, 0, bufferSize)) != -1) {
+ bufferedOut.write(data, 0, count);
+ }
+ bufferedOut.flush();
+ archiveEntries.put(entry.getName(), out.toString());
+ }
+ }
+ }
+ }
+ return archiveEntries;
+ }
+
+ private byte[] getBinaryContent(BinaryResult res) throws Exception {
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ res.writeTo(out);
+ return out.toByteArray();
+ } finally {
+ res.close();
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
index c57a035..0099fe6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -226,7 +226,7 @@
HashtagsInput input = new HashtagsInput();
input.add = Sets.newHashSet("tag3", "tag4");
input.remove = Sets.newHashSet("tag1");
- gApi.changes().id(r.getChange().getId().get()).setHashtags(input);
+ change(r).setHashtags(input);
assertThatGet(r).containsExactly("tag2", "tag3", "tag4");
assertMessage(r, "Hashtags added: tag3, tag4\nHashtag removed: tag1");
@@ -235,7 +235,7 @@
input = new HashtagsInput();
input.add = Sets.newHashSet("tag3", "tag4");
input.remove = Sets.newHashSet("tag3");
- gApi.changes().id(r.getChange().getId().get()).setHashtags(input);
+ change(r).setHashtags(input);
assertThatGet(r).containsExactly("tag1", "tag2", "tag4");
assertMessage(r, "Hashtag removed: tag3");
}
@@ -271,19 +271,19 @@
}
private IterableSubject assertThatGet(PushOneCommit.Result r) throws Exception {
- return assertThat(gApi.changes().id(r.getChange().getId().get()).getHashtags());
+ return assertThat(change(r).getHashtags());
}
private void addHashtags(PushOneCommit.Result r, String... toAdd) throws Exception {
HashtagsInput input = new HashtagsInput();
input.add = Sets.newHashSet(toAdd);
- gApi.changes().id(r.getChange().getId().get()).setHashtags(input);
+ change(r).setHashtags(input);
}
private void removeHashtags(PushOneCommit.Result r, String... toRemove) throws Exception {
HashtagsInput input = new HashtagsInput();
input.remove = Sets.newHashSet(toRemove);
- gApi.changes().id(r.getChange().getId().get()).setHashtags(input);
+ change(r).setHashtags(input);
}
private void assertMessage(PushOneCommit.Result r, String expectedMessage) throws Exception {
@@ -299,8 +299,7 @@
}
private ChangeMessageInfo getLastMessage(PushOneCommit.Result r) throws Exception {
- ChangeMessageInfo lastMessage =
- Iterables.getLast(gApi.changes().id(r.getChange().getId().get()).get().messages, null);
+ ChangeMessageInfo lastMessage = Iterables.getLast(change(r).get().messages, null);
assertWithMessage(lastMessage.message).that(lastMessage).isNotNull();
return lastMessage;
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 3e9b1f6..2a8cca0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -44,6 +44,8 @@
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.common.WebLinkInfo;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -143,6 +145,68 @@
}
@Test
+ public void addAccessSectionForPluginPermission() throws Exception {
+ try (Registration registration =
+ extensionRegistry
+ .newRegistration()
+ .add(
+ new PluginProjectPermissionDefinition() {
+ @Override
+ public String getDescription() {
+ return "A Plugin Project Permission";
+ }
+ },
+ "fooPermission")) {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
+
+ PermissionInfo foo = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ foo.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSectionInfo.permissions.put(
+ "plugin-" + ExtensionRegistry.PLUGIN_NAME + "-fooPermission", foo);
+
+ accessInput.add.put(REFS_HEADS, accessSectionInfo);
+ ProjectAccessInfo updatedAccessSectionInfo = pApi().access(accessInput);
+ assertThat(updatedAccessSectionInfo.local).isEqualTo(accessInput.add);
+
+ assertThat(pApi().access().local).isEqualTo(accessInput.add);
+ }
+ }
+
+ @Test
+ public void addAccessSectionWithInvalidPermission() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+
+ PermissionInfo push = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ push.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSectionInfo.permissions.put("Invalid Permission", push);
+
+ accessInput.add.put(REFS_HEADS, accessSectionInfo);
+ BadRequestException ex =
+ assertThrows(BadRequestException.class, () -> pApi().access(accessInput));
+ assertThat(ex).hasMessageThat().isEqualTo("Unknown permission: Invalid Permission");
+ }
+
+ @Test
+ public void addAccessSectionWithInvalidLabelPermission() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+
+ PermissionInfo push = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ push.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSectionInfo.permissions.put("label-Invalid Permission", push);
+
+ accessInput.add.put(REFS_HEADS, accessSectionInfo);
+ BadRequestException ex =
+ assertThrows(BadRequestException.class, () -> pApi().access(accessInput));
+ assertThat(ex).hasMessageThat().isEqualTo("Unknown permission: label-Invalid Permission");
+ }
+
+ @Test
public void createAccessChangeNop() throws Exception {
ProjectAccessInput accessInput = newProjectAccessInput();
assertThrows(BadRequestException.class, () -> pApi().accessChange(accessInput));
@@ -456,6 +520,79 @@
}
@Test
+ public void addPluginGlobalCapability() throws Exception {
+ try (Registration registration =
+ extensionRegistry
+ .newRegistration()
+ .add(
+ new CapabilityDefinition() {
+ @Override
+ public String getDescription() {
+ return "A Plugin Global Capability";
+ }
+ },
+ "fooCapability")) {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
+
+ PermissionInfo foo = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ foo.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSectionInfo.permissions.put(ExtensionRegistry.PLUGIN_NAME + "-fooCapability", foo);
+
+ accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
+
+ ProjectAccessInfo updatedAccessSectionInfo =
+ gApi.projects().name(allProjects.get()).access(accessInput);
+ assertThat(
+ updatedAccessSectionInfo
+ .local
+ .get(AccessSection.GLOBAL_CAPABILITIES)
+ .permissions
+ .keySet())
+ .containsAtLeastElementsIn(accessSectionInfo.permissions.keySet());
+ }
+ }
+
+ @Test
+ public void addPermissionAsGlobalCapability() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
+
+ PermissionInfo push = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ push.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSectionInfo.permissions.put(Permission.PUSH, push);
+
+ accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
+ BadRequestException ex =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).access(accessInput));
+ assertThat(ex).hasMessageThat().isEqualTo("Unknown global capability: " + Permission.PUSH);
+ }
+
+ @Test
+ public void addInvalidGlobalCapability() throws Exception {
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSectionInfo = createDefaultGlobalCapabilitiesAccessSectionInfo();
+
+ PermissionInfo permissionInfo = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ permissionInfo.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSectionInfo.permissions.put("Invalid Global Capability", permissionInfo);
+
+ accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
+ BadRequestException ex =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).access(accessInput));
+ assertThat(ex)
+ .hasMessageThat()
+ .isEqualTo("Unknown global capability: Invalid Global Capability");
+ }
+
+ @Test
public void addGlobalCapabilityForNonRootProject() throws Exception {
ProjectAccessInput accessInput = newProjectAccessInput();
AccessSectionInfo accessSectionInfo = createDefaultGlobalCapabilitiesAccessSectionInfo();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index 85d383e..b01a07b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -23,7 +23,9 @@
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
@@ -38,8 +40,20 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.events.RefReceivedEvent;
+import com.google.gerrit.server.git.validators.RefOperationValidationListener;
+import com.google.gerrit.server.git.validators.ValidationMessage;
+import com.google.gerrit.server.util.MagicBranch;
+import com.google.gerrit.server.validators.ValidationException;
import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.List;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;
@@ -47,6 +61,7 @@
public class CreateBranchIT extends AbstractDaemonTest {
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private ExtensionRegistry extensionRegistry;
private BranchNameKey testBranch;
@@ -82,7 +97,63 @@
@Test
public void branchAlreadyExists_Conflict() throws Exception {
assertCreateSucceeds(testBranch);
- assertCreateFails(testBranch, ResourceConflictException.class);
+ assertCreateFails(
+ testBranch,
+ ResourceConflictException.class,
+ "branch \"" + testBranch.branch() + "\" already exists");
+ }
+
+ @Test
+ public void createBranch_LockFailure() throws Exception {
+ // check that the branch doesn't exist yet
+ assertThrows(ResourceNotFoundException.class, () -> branch(testBranch).get());
+
+ // Register a validation listener that creates the branch to simulate a concurrent request that
+ // creates the same branch.
+ try (ExtensionRegistry.Registration registration =
+ extensionRegistry
+ .newRegistration()
+ .add(
+ new RefOperationValidationListener() {
+ @Override
+ public List<ValidationMessage> onRefOperation(RefReceivedEvent refEvent)
+ throws ValidationException {
+ try (Repository repo = repoManager.openRepository(project)) {
+ RefUpdate u = repo.updateRef(testBranch.branch());
+ u.setExpectedOldObjectId(ObjectId.zeroId());
+ u.setNewObjectId(repo.exactRef("refs/heads/master").getObjectId());
+ RefUpdate.Result result = u.update();
+ if (result != RefUpdate.Result.NEW) {
+ throw new ValidationException(
+ "Concurrent creation of branch failed: " + result);
+ }
+ return ImmutableList.of();
+ } catch (IOException e) {
+ throw new ValidationException("Concurrent creation of branch failed.", e);
+ }
+ }
+ })) {
+ // Creating the branch is expected to fail, since it is created by the validation listener
+ // right before the ref update to create the new branch is done.
+ assertCreateFails(
+ testBranch,
+ ResourceConflictException.class,
+ "branch \"" + testBranch.branch() + "\" already exists");
+ }
+ }
+
+ @Test
+ public void conflictingBranchAlreadyExists_Conflict() throws Exception {
+ assertCreateSucceeds(testBranch);
+ BranchNameKey testBranch2 = BranchNameKey.create(project, testBranch.branch() + "/foo/bar");
+ assertCreateFails(
+ testBranch2,
+ ResourceConflictException.class,
+ "Cannot create branch \""
+ + testBranch2.branch()
+ + "\" since it conflicts with branch \""
+ + testBranch.branch()
+ + "\"");
}
@Test
@@ -119,6 +190,23 @@
}
@Test
+ public void createMetaConfigBranch() throws Exception {
+ // Since the refs/meta/config branch exists by default, we must delete it before we can test
+ // creating it. Since deleting the refs/meta/config branch is not allowed through the API, we
+ // delete it directly in the remote repository.
+ try (TestRepository<Repository> repo =
+ new TestRepository<>(repoManager.openRepository(project))) {
+ repo.delete(RefNames.REFS_CONFIG);
+ }
+
+ // Create refs/meta/config branch.
+ BranchInfo created =
+ branch(BranchNameKey.create(project, RefNames.REFS_CONFIG)).create(new BranchInput()).get();
+ assertThat(created.ref).isEqualTo(RefNames.REFS_CONFIG);
+ assertThat(created.canDelete).isNull();
+ }
+
+ @Test
public void createUserBranch_Conflict() throws Exception {
projectOperations
.project(allUsers)
@@ -258,6 +346,54 @@
"invalid revision \"invalid\trevision\"");
}
+ @Test
+ public void cannotCreateBranchInMagicBranchNamespace() throws Exception {
+ assertCreateFails(
+ BranchNameKey.create(project, MagicBranch.NEW_CHANGE + "foo"),
+ BadRequestException.class,
+ "not allowed to create branches under \"" + MagicBranch.NEW_CHANGE + "\"");
+ }
+
+ @Test
+ public void cannotCreateBranchWithInvalidName() throws Exception {
+ assertCreateFails(
+ BranchNameKey.create(project, RefNames.REFS_HEADS),
+ BadRequestException.class,
+ "invalid branch name \"" + RefNames.REFS_HEADS + "\"");
+ }
+
+ @Test
+ public void createBranchLeadingSlashesAreRemoved() throws Exception {
+ BranchNameKey expectedNameKey = BranchNameKey.create(project, "test");
+
+ // check that the branch doesn't exist yet
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.projects().name(project.get()).branch(expectedNameKey.branch()).get());
+
+ // create the branch, but include leading slashes in the branch name,
+ // when creating the branch ensure that the branch name in the URL matches the branch name in
+ // the input (if there is a mismatch the creation request is rejected)
+ BranchInput branchInput = new BranchInput();
+ branchInput.ref = "////" + expectedNameKey.shortName();
+ gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput);
+
+ // verify that the branch was created without the leading slashes in the name
+ assertThat(gApi.projects().name(project.get()).branch(expectedNameKey.branch()).get().ref)
+ .isEqualTo(expectedNameKey.branch());
+ }
+
+ @Test
+ public void branchNameInInputMustMatchBranchNameInUrl() throws Exception {
+ BranchInput branchInput = new BranchInput();
+ branchInput.ref = "foo";
+ BadRequestException ex =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(project.get()).branch("bar").create(branchInput));
+ assertThat(ex).hasMessageThat().isEqualTo("ref must match URL");
+ }
+
private void blockCreateReference() throws Exception {
projectOperations
.project(project)
@@ -302,9 +438,4 @@
assertThat(thrown).hasMessageThat().contains(errMsg);
}
}
-
- private void assertCreateFails(BranchNameKey branch, Class<? extends RestApiException> errType)
- throws Exception {
- assertCreateFails(branch, errType, null);
- }
}
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index b60cce5..566308d 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -48,6 +48,7 @@
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.notedb.ChangeNoteUtil;
@@ -85,6 +86,7 @@
@Inject private Provider<ChangesCollection> changes;
@Inject private Provider<PostReview> postReview;
@Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private CommentsUtil commentsUtil;
private final Integer[] lines = {0, 1};
@@ -446,6 +448,82 @@
}
@Test
+ public void putDraft_idMismatch() throws Exception {
+ String file = "file";
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ String revId = r.getCommit().getName();
+ DraftInput comment = newDraft(file, Side.REVISION, 0, "foo");
+ CommentInfo commentInfo = addDraft(changeId, revId, comment);
+ DraftInput draftInput = newDraft(file, Side.REVISION, 0, "bar");
+ draftInput.id = "anything_but_" + commentInfo.id;
+ BadRequestException e =
+ assertThrows(
+ BadRequestException.class,
+ () -> updateDraft(changeId, revId, draftInput, commentInfo.id));
+ assertThat(e).hasMessageThat().contains("id must match URL");
+ }
+
+ @Test
+ public void putDraft_negativeLine() throws Exception {
+ String file = "file";
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ String revId = r.getCommit().getName();
+ DraftInput comment = newDraft(file, Side.REVISION, -666, "foo");
+ BadRequestException e =
+ assertThrows(BadRequestException.class, () -> addDraft(changeId, revId, comment));
+ assertThat(e).hasMessageThat().contains("line must be >= 0");
+ }
+
+ @Test
+ public void putDraft_invalidRange() throws Exception {
+ String file = "file";
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ String revId = r.getCommit().getName();
+ DraftInput draftInput = newDraft(file, Side.REVISION, createLineRange(2, 3), "bar");
+ draftInput.line = 666;
+ BadRequestException e =
+ assertThrows(BadRequestException.class, () -> addDraft(changeId, revId, draftInput));
+ assertThat(e)
+ .hasMessageThat()
+ .contains("range endLine must be on the same line as the comment");
+ }
+
+ @Test
+ public void putDraft_updatePath() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ String revId = r.getCommit().getName();
+ DraftInput comment = newDraft("file_foo", Side.REVISION, 0, "foo");
+ CommentInfo commentInfo = addDraft(changeId, revId, comment);
+ assertThat(getDraftComments(changeId, revId).keySet()).containsExactly("file_foo");
+ DraftInput draftInput = newDraft("file_bar", Side.REVISION, 0, "bar");
+ updateDraft(changeId, revId, draftInput, commentInfo.id);
+ assertThat(getDraftComments(changeId, revId).keySet()).containsExactly("file_bar");
+ }
+
+ @Test
+ public void putDraft_updateInReplyToAndTag() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ String revId = r.getCommit().getName();
+ DraftInput draftInput1 = newDraft(FILE_NAME, Side.REVISION, 0, "foo");
+ CommentInfo commentInfo = addDraft(changeId, revId, draftInput1);
+ DraftInput draftInput2 = newDraft(FILE_NAME, Side.REVISION, 0, "bar");
+ String inReplyTo = "in_reply_to";
+ String tag = "täg";
+ draftInput2.inReplyTo = inReplyTo;
+ draftInput2.tag = tag;
+ updateDraft(changeId, revId, draftInput2, commentInfo.id);
+ com.google.gerrit.entities.Comment comment =
+ Iterables.getOnlyElement(commentsUtil.draftByChange(r.getChange().notes()));
+ assertThat(comment.parentUuid).isEqualTo(inReplyTo);
+ assertThat(comment.tag).isEqualTo(tag);
+ }
+
+ @Test
public void listDrafts() throws Exception {
String file = "file";
PushOneCommit.Result r = createChange();
@@ -677,15 +755,15 @@
addDraft(
r1.getChangeId(),
r1.getCommit().getName(),
- newDraft(FILE_NAME, Side.REVISION, createLineRange(1, 4, 10), "Is it that bad?"));
+ newDraft(FILE_NAME, Side.REVISION, createLineRange(4, 10), "Is it that bad?"));
addDraft(
r1.getChangeId(),
r1.getCommit().getName(),
- newDraft(FILE_NAME, Side.PARENT, createLineRange(1, 0, 7), "what happened to this?"));
+ newDraft(FILE_NAME, Side.PARENT, createLineRange(0, 7), "what happened to this?"));
addDraft(
r2.getChangeId(),
r2.getCommit().getName(),
- newDraft(FILE_NAME, Side.REVISION, createLineRange(1, 4, 15), "better now"));
+ newDraft(FILE_NAME, Side.REVISION, createLineRange(4, 15), "better now"));
addDraft(
r2.getChangeId(),
r2.getCommit().getName(),
@@ -905,7 +983,7 @@
@Test
public void deleteCommentCannotBeAppliedByUser() throws Exception {
PushOneCommit.Result result = createChange();
- CommentInput targetComment = addComment(result.getChangeId(), "My password: abc123");
+ CommentInput targetComment = addComment(result.getChangeId());
Map<String, List<CommentInfo>> commentsMap =
getPublishedComments(result.getChangeId(), result.getCommit().name());
@@ -1083,9 +1161,9 @@
.collect(toList());
}
- private CommentInput addComment(String changeId, String message) throws Exception {
+ private CommentInput addComment(String changeId) throws Exception {
ReviewInput input = new ReviewInput();
- CommentInput comment = newComment(FILE_NAME, Side.REVISION, 0, message, false);
+ CommentInput comment = newComment(FILE_NAME, Side.REVISION, 0, "a message", false);
input.comments = ImmutableMap.of(comment.path, Lists.newArrayList(comment));
gApi.changes().id(changeId).current().review(input);
return comment;
@@ -1240,7 +1318,7 @@
private static CommentInput newCommentOnParent(
String path, int parent, int line, String message) {
CommentInput c = new CommentInput();
- return populate(c, path, Side.PARENT, Integer.valueOf(parent), line, message, false);
+ return populate(c, path, Side.PARENT, parent, line, message, false);
}
private DraftInput newDraft(String path, Side side, int line, String message) {
@@ -1255,7 +1333,7 @@
private DraftInput newDraftOnParent(String path, int parent, int line, String message) {
DraftInput d = new DraftInput();
- return populate(d, path, Side.PARENT, Integer.valueOf(parent), line, message, false);
+ return populate(d, path, Side.PARENT, parent, line, message, false);
}
private static <C extends Comment> C populate(
@@ -1284,11 +1362,11 @@
return populate(c, path, side, parent, line, null, message, unresolved);
}
- private static Comment.Range createLineRange(int line, int startChar, int endChar) {
+ private static Comment.Range createLineRange(int startChar, int endChar) {
Comment.Range range = new Comment.Range();
- range.startLine = line;
+ range.startLine = 1;
range.startCharacter = startChar;
- range.endLine = line;
+ range.endLine = 1;
range.endCharacter = endChar;
return range;
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index 96041a1..dd3e364 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -53,7 +53,7 @@
case V6_7:
return "blacktop/elasticsearch:6.7.2";
case V6_8:
- return "blacktop/elasticsearch:6.8.6";
+ return "blacktop/elasticsearch:6.8.7";
case V7_0:
return "blacktop/elasticsearch:7.0.1";
case V7_1:
diff --git a/javatests/com/google/gerrit/server/change/ArchiveFormatInternalTest.java b/javatests/com/google/gerrit/server/change/ArchiveFormatInternalTest.java
new file mode 100644
index 0000000..003225c
--- /dev/null
+++ b/javatests/com/google/gerrit/server/change/ArchiveFormatInternalTest.java
@@ -0,0 +1,35 @@
+// 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.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.extensions.client.ArchiveFormat;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+
+public class ArchiveFormatInternalTest {
+ @Test
+ public void internalAndExternalArchiveFormatEnumsMatch() throws Exception {
+ assertThat(getEnumNames(ArchiveFormatInternal.class))
+ .containsExactlyElementsIn(getEnumNames(ArchiveFormat.class));
+ }
+
+ private static List<String> getEnumNames(Class<? extends Enum<?>> e) {
+ return Arrays.stream(e.getEnumConstants()).map(Enum::name).collect(toList());
+ }
+}
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
index b817b80..47877b6 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
@@ -104,7 +104,6 @@
SubmitRequirement.builder()
.setType("short_type")
.setFallbackText("Fallback text may contain special symbols like < > \\ / ; :")
- .addCustomValue("custom_data", "my value")
.build();
r.requirements = Collections.singletonList(sr);
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index 8bc78b8..0674dc0 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -28,7 +28,7 @@
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.AttentionStatus;
+import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.Comment;
@@ -45,7 +45,7 @@
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AssigneeStatusUpdateProto;
-import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AttentionStatusProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AttentionSetUpdateProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ChangeColumnsProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
@@ -600,31 +600,31 @@
}
@Test
- public void serializeAttentionUpdates() throws Exception {
+ public void serializeAttentionSetUpdates() throws Exception {
assertRoundTrip(
newBuilder()
- .attentionUpdates(
+ .attentionSet(
ImmutableList.of(
- AttentionStatus.createFromRead(
+ AttentionSetUpdate.createFromRead(
Instant.EPOCH.plusSeconds(23),
Account.id(1000),
- AttentionStatus.Operation.ADD,
+ AttentionSetUpdate.Operation.ADD,
"reason 1"),
- AttentionStatus.createFromRead(
+ AttentionSetUpdate.createFromRead(
Instant.EPOCH.plusSeconds(42),
Account.id(2000),
- AttentionStatus.Operation.REMOVE,
+ AttentionSetUpdate.Operation.REMOVE,
"reason 2")))
.build(),
newProtoBuilder()
- .addAttentionStatus(
- AttentionStatusProto.newBuilder()
+ .addAttentionSetUpdate(
+ AttentionSetUpdateProto.newBuilder()
.setTimestampMillis(23_000) // epoch millis
.setAccount(1000)
.setOperation("ADD")
.setReason("reason 1"))
- .addAttentionStatus(
- AttentionStatusProto.newBuilder()
+ .addAttentionSetUpdate(
+ AttentionSetUpdateProto.newBuilder()
.setTimestampMillis(42_000) // epoch millis
.setAccount(2000)
.setOperation("REMOVE")
@@ -789,8 +789,8 @@
"reviewerUpdates",
new TypeLiteral<ImmutableList<ReviewerStatusUpdate>>() {}.getType())
.put(
- "attentionUpdates",
- new TypeLiteral<ImmutableList<AttentionStatus>>() {}.getType())
+ "attentionSet",
+ new TypeLiteral<ImmutableList<AttentionSetUpdate>>() {}.getType())
.put(
"assigneeUpdates",
new TypeLiteral<ImmutableList<AssigneeStatusUpdate>>() {}.getType())
@@ -936,8 +936,7 @@
.hasAutoValueMethods(
ImmutableMap.of(
"fallbackText", String.class,
- "type", String.class,
- "data", new TypeLiteral<ImmutableMap<String, String>>() {}.getType()));
+ "type", String.class));
}
@Test
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 4e068ba..f5a6dc3 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -38,8 +38,8 @@
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.AttentionStatus;
-import com.google.gerrit.entities.AttentionStatus.Operation;
+import com.google.gerrit.entities.AttentionSetUpdate;
+import com.google.gerrit.entities.AttentionSetUpdate.Operation;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
@@ -694,51 +694,51 @@
public void defaultAttentionSetIsEmpty() throws Exception {
Change c = newChange();
ChangeNotes notes = newNotes(c);
- assertThat(notes.getAttentionUpdates()).isEmpty();
+ assertThat(notes.getAttentionSet()).isEmpty();
}
@Test
public void addAttentionStatus() throws Exception {
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
- AttentionStatus attentionStatus =
- AttentionStatus.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test");
- update.setAttentionUpdates(ImmutableList.of(attentionStatus));
+ AttentionSetUpdate attentionSetUpdate =
+ AttentionSetUpdate.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test");
+ update.setAttentionSetUpdates(ImmutableList.of(attentionSetUpdate));
update.commit();
ChangeNotes notes = newNotes(c);
- assertThat(notes.getAttentionUpdates()).containsExactly(addTimestamp(attentionStatus, c));
+ assertThat(notes.getAttentionSet()).containsExactly(addTimestamp(attentionSetUpdate, c));
}
@Test
public void filterLatestAttentionStatus() throws Exception {
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
- AttentionStatus attentionStatus =
- AttentionStatus.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test");
- update.setAttentionUpdates(ImmutableList.of(attentionStatus));
+ AttentionSetUpdate attentionSetUpdate =
+ AttentionSetUpdate.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test");
+ update.setAttentionSetUpdates(ImmutableList.of(attentionSetUpdate));
update.commit();
update = newUpdate(c, changeOwner);
- attentionStatus =
- AttentionStatus.createForWrite(changeOwner.getAccountId(), Operation.REMOVE, "test");
- update.setAttentionUpdates(ImmutableList.of(attentionStatus));
+ attentionSetUpdate =
+ AttentionSetUpdate.createForWrite(changeOwner.getAccountId(), Operation.REMOVE, "test");
+ update.setAttentionSetUpdates(ImmutableList.of(attentionSetUpdate));
update.commit();
ChangeNotes notes = newNotes(c);
- assertThat(notes.getAttentionUpdates()).containsExactly(addTimestamp(attentionStatus, c));
+ assertThat(notes.getAttentionSet()).containsExactly(addTimestamp(attentionSetUpdate, c));
}
@Test
public void addAttentionStatus_rejectTimestamp() throws Exception {
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
- AttentionStatus attentionStatus =
- AttentionStatus.createFromRead(
+ AttentionSetUpdate attentionSetUpdate =
+ AttentionSetUpdate.createFromRead(
Instant.now(), changeOwner.getAccountId(), Operation.ADD, "test");
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
- () -> update.setAttentionUpdates(ImmutableList.of(attentionStatus)));
+ () -> update.setAttentionSetUpdates(ImmutableList.of(attentionSetUpdate)));
assertThat(thrown).hasMessageThat().contains("must not specify timestamp for write");
}
@@ -746,14 +746,16 @@
public void addAttentionStatus_rejectMultiplePerUser() throws Exception {
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
- AttentionStatus attentionStatus0 =
- AttentionStatus.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test 0");
- AttentionStatus attentionStatus1 =
- AttentionStatus.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test 1");
+ AttentionSetUpdate attentionSetUpdate0 =
+ AttentionSetUpdate.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test 0");
+ AttentionSetUpdate attentionSetUpdate1 =
+ AttentionSetUpdate.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test 1");
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
- () -> update.setAttentionUpdates(ImmutableList.of(attentionStatus0, attentionStatus1)));
+ () ->
+ update.setAttentionSetUpdates(
+ ImmutableList.of(attentionSetUpdate0, attentionSetUpdate1)));
assertThat(thrown)
.hasMessageThat()
.contains("must not specify multiple updates for single user");
@@ -763,17 +765,18 @@
public void addAttentionStatusForMultipleUsers() throws Exception {
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
- AttentionStatus attentionStatus0 =
- AttentionStatus.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test");
- AttentionStatus attentionStatus1 =
- AttentionStatus.createForWrite(otherUser.getAccountId(), Operation.ADD, "test");
+ AttentionSetUpdate attentionSetUpdate0 =
+ AttentionSetUpdate.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test");
+ AttentionSetUpdate attentionSetUpdate1 =
+ AttentionSetUpdate.createForWrite(otherUser.getAccountId(), Operation.ADD, "test");
- update.setAttentionUpdates(ImmutableList.of(attentionStatus0, attentionStatus1));
+ update.setAttentionSetUpdates(ImmutableList.of(attentionSetUpdate0, attentionSetUpdate1));
update.commit();
ChangeNotes notes = newNotes(c);
- assertThat(notes.getAttentionUpdates())
- .containsExactly(addTimestamp(attentionStatus0, c), addTimestamp(attentionStatus1, c));
+ assertThat(notes.getAttentionSet())
+ .containsExactly(
+ addTimestamp(attentionSetUpdate0, c), addTimestamp(attentionSetUpdate1, c));
}
@Test
@@ -3230,12 +3233,12 @@
return tr.parseBody(commit);
}
- private AttentionStatus addTimestamp(AttentionStatus attentionStatus, Change c) {
+ private AttentionSetUpdate addTimestamp(AttentionSetUpdate attentionSetUpdate, Change c) {
Timestamp timestamp = newNotes(c).getChange().getLastUpdatedOn();
- return AttentionStatus.createFromRead(
+ return AttentionSetUpdate.createFromRead(
timestamp.toInstant(),
- attentionStatus.account(),
- attentionStatus.operation(),
- attentionStatus.reason());
+ attentionSetUpdate.account(),
+ attentionSetUpdate.operation(),
+ attentionSetUpdate.reason());
}
}
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index fb09c9f..040e2eb 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -1784,16 +1784,43 @@
assertQuery(q + " visibleto:self", change2, change1);
// Second user cannot see first user's private change
- Account.Id user2 = createAccount("anotheruser");
+ Account.Id user2 = createAccount("user2");
assertQuery(q + " visibleto:" + user2.get(), change1);
- assertQuery(q + " visibleto:anotheruser", change1);
+ assertQuery(q + " visibleto:user2", change1);
String g1 = createGroup("group1", "Administrators");
- gApi.groups().id(g1).addMembers("anotheruser");
+ gApi.groups().id(g1).addMembers("user2");
assertQuery(q + " visibleto:" + g1, change1);
requestContext.setContext(newRequestContext(user2));
assertQuery("is:visible", change1);
+
+ Account.Id user3 = createAccount("user3");
+
+ // Explicitly authenticate user2 and user3 so that display name gets set
+ AuthRequest authRequest = AuthRequest.forUser("user2");
+ authRequest.setDisplayName("Another User");
+ authRequest.setEmailAddress("user2@example.com");
+ accountManager.authenticate(authRequest);
+ authRequest = AuthRequest.forUser("user3");
+ authRequest.setDisplayName("Another User");
+ authRequest.setEmailAddress("user3@example.com");
+ accountManager.authenticate(authRequest);
+
+ // Switch to user3
+ requestContext.setContext(newRequestContext(user3));
+ Change change3 = insert(repo, newChange(repo), user3);
+ Change change4 = insert(repo, newChangePrivate(repo), user3);
+
+ // User3 can see both their changes and the first user's change
+ assertQuery(q + " visibleto:" + user3.get(), change4, change3, change1);
+
+ // User2 cannot see user3's private change
+ assertQuery(q + " visibleto:" + user2.get(), change3, change1);
+
+ // Query as user3 by display name matching user2 and user3; bad request
+ assertFailingQuery(
+ q + " visibleto:\"Another User\"", "\"Another User\" resolves to multiple accounts");
}
@Test
diff --git a/plugins/BUILD b/plugins/BUILD
index 8d9682f..5f9c142 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -54,6 +54,7 @@
"//java/com/google/gerrit/server/util/time",
"//java/com/google/gerrit/util/cli",
"//java/com/google/gerrit/util/http",
+ "//java/com/google/gerrit/util/logging",
"//lib/antlr:java-runtime",
"//lib/auto:auto-value-annotations",
"//lib/commons:compress",
diff --git a/plugins/delete-project b/plugins/delete-project
index c06a7e4..e267920 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit c06a7e4d9b63dddbc3575317d0b989ef69c81fe0
+Subproject commit e267920b6f1660b348e2d27b5bb6dd1dee74a36c
diff --git a/plugins/download-commands b/plugins/download-commands
index 5c50f9b..3f5a024 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 5c50f9b17e616fd84d2c561822161fff46bbf902
+Subproject commit 3f5a024fd46f30f4646bfceb285763e44fda15a7
diff --git a/plugins/gitiles b/plugins/gitiles
index 7793e45..860c74e 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit 7793e45da5259cc4aad1add2e4aa20673a95057d
+Subproject commit 860c74e31d41c9ff6b3b39c99b40647fd7fce9d7
diff --git a/plugins/replication b/plugins/replication
index 041f55c..864c077 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 041f55c0ff92e14bea9badacc617a8abb7f37b71
+Subproject commit 864c077e5e13ebbae5e7d0a3abc95fc8ae3fdc8b
diff --git a/plugins/webhooks b/plugins/webhooks
index e503006..c3d83d1 160000
--- a/plugins/webhooks
+++ b/plugins/webhooks
@@ -1 +1 @@
-Subproject commit e50300670040de80c36ec7c4f8d319a8047a2735
+Subproject commit c3d83d14c8a542d241e1e55eb5e1aa44cc0aaa66
diff --git a/polygerrit-ui/Polymer2.md b/polygerrit-ui/Polymer2.md
index 96bf779..e2a9124 100644
--- a/polygerrit-ui/Polymer2.md
+++ b/polygerrit-ui/Polymer2.md
@@ -1,3 +1,7 @@
+Note: Gerrit has moved to polymer 3 as of submitted of https://gerrit-review.googlesource.com/q/topic:%22bower+to+npm+packages+switch%22+(status:open%20OR%20status:merged).
+
+The change is backward compatible, so no code change needed to support all plugins, but we would highly recommend to start moving to latest polymer 3 for all plugins, check out [Polymer3.md](./Polymer3.md) for more insights.
+
## Polymer 2 upgrade
Gerrit is updating to use polymer 2 from polymer 1 by following the [Polymer 2.0 upgrade guide](https://polymer-library.polymer-project.org/2.0/docs/upgrade).
diff --git a/polygerrit-ui/Polymer3.md b/polygerrit-ui/Polymer3.md
new file mode 100644
index 0000000..94750d8
--- /dev/null
+++ b/polygerrit-ui/Polymer3.md
@@ -0,0 +1,20 @@
+## Gerrit in Polymer 3
+
+Gerrit has migrated to polymer 3 as of submitted of submitted of https://gerrit-review.googlesource.com/q/topic:%22bower+to+npm+packages+switch%22+(status:open%20OR%20status:merged).
+
+## Polymer 3 vs Polymer 2
+
+The biggest difference between 2 and 3 is the changing of package management from bower to npm and also replaced the html imports with es6 imports so we no longer need templates in separate `html` files for polymer components.
+
+### How that impact plugins
+
+As of now, we still support all syntax in Polymer 2 and most from Polymer 1 with the [legacy layer](https://polymer-library.polymer-project.org/3.0/docs/devguide/legacy-elements). But we do plan to remove those in the future.
+
+So we recommend all plugin owners to start migrating to Polymer 3 for your plugins. You can refer more about polymer 3 from the related resources section.
+
+To get inspirations, check out our [samples here](https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/app/samples).
+
+### Related resources
+
+- [Polymer 3.0 upgrade guide](https://polymer-library.polymer-project.org/3.0/docs/upgrade)
+-[What's new in Polymer 3.0](https://polymer-library.polymer-project.org/3.0/docs/about_30)
\ No newline at end of file
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index c5dd657..de25d79 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -2,7 +2,13 @@
Follow the
[setup instructions for Gerrit backend developers](https://gerrit-review.googlesource.com/Documentation/dev-readme.html)
-where applicable.
+where applicable, the most important command is:
+
+```
+git clone --recurse-submodules https://gerrit.googlesource.com/gerrit
+```
+
+The --recurse-submodules option is needed on git clone to ensure that the core plugins, which are included as git submodules, are also cloned.
## Installing [Bazel](https://bazel.build/)
@@ -46,17 +52,16 @@
or use [nvm - Node Version Manager](https://github.com/nvm-sh/nvm).
-Various steps below require installing additional npm packages. To start developing, it is enough
-to install only top-level packages with the following command:
+### Additional packages
+
+We have several bazel commands to install packages we may need for FE development.
+
+For first time users to get the local server up, `npm start` should be enough and will take care of all of them for you.
```sh
# Install packages from root-level packages.json
bazel fetch @npm//:node_modules
-```
-All other packages are installed by bazel when needed. If you want to install them manually, run the
-following commands:
-```sh
# Install packages from polygerrit-ui/app/packages.json
bazel fetch @ui_npm//:node_modules
@@ -64,7 +69,7 @@
bazel fetch @ui_dev_npm//:node_modules
# Install packages from tools/node_tools/packages.json
-bazel fetch @ui_dev_npm//:node_modules
+bazel fetch @tools_npm//:node_modules
```
More information for installing and using nodejs rules can be found here https://bazelbuild.github.io/rules_nodejs/install.html
@@ -92,6 +97,8 @@
./polygerrit-ui/run-server.sh --plugins=plugins/my_plugin/static/my_plugin.js,plugins/my_plugin/static/my_plugin.html
```
+If any issues occured, please refer to the Troubleshooting section at the bottom or contact the team!
+
## Running locally against production data
### Local website
@@ -201,7 +208,7 @@
* To run the linter on all of your local changes:
```sh
-git diff --name-only master | xargs node_modules/eslint/bin/eslint.js --ext .html,.js
+git diff --name-only HEAD | xargs node_modules/eslint/bin/eslint.js --ext .html,.js
```
We also use the `polylint` tool to lint use of Polymer. To install polylint,
@@ -274,4 +281,22 @@
If you are willing to join the queue and help the community review changes,
you can create an issue through Monorail and request to join the queue!
-We will review your request and start from there.
\ No newline at end of file
+We will review your request and start from there.
+
+## Troubleshotting & Frequently asked questions
+
+1. Local host is blank page and console shows missing files from `polymer-bridges`
+
+Its likely you missed the `polymer-bridges` submodule when you clone the `gerrit` repo.
+
+To fix that, run:
+```
+// fetch the submodule
+git submodule update --init --recursive
+
+// reset the workspace (please save your local changes before running this command)
+npm run clean
+
+// install all dependencies and start the server
+npm start
+```
\ No newline at end of file
diff --git a/polygerrit-ui/app/.eslintrc.json b/polygerrit-ui/app/.eslintrc.json
index 35f2344..7743420 100644
--- a/polygerrit-ui/app/.eslintrc.json
+++ b/polygerrit-ui/app/.eslintrc.json
@@ -52,7 +52,9 @@
"error",
80,
2,
- {"ignoreComments": true}
+ {"ignoreComments": true,
+ "ignorePattern":"^import .*;$"
+ }
],
"new-cap": ["error", { "capIsNewExceptions": ["Polymer", "LegacyElementMixin", "GestureEventListeners", "LegacyDataMixin"] }],
"no-console": "off",
@@ -139,6 +141,12 @@
"rules": {
"jsdoc/require-file-overview": "off"
}
+ },
+ {
+ "files": ["*_html.js", "gr-icons.js", "*-theme.js", "*-styles.js"],
+ "rules": {
+ "max-len": "off"
+ }
}
],
"plugins": [
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 063f6c1..ed50810 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -1,5 +1,4 @@
load(":rules.bzl", "polygerrit_bundle")
-load("//tools/node_tools/polygerrit_app_preprocessor:index.bzl", "update_links")
package(default_visibility = ["//visibility:public"])
@@ -7,7 +6,6 @@
name = "polygerrit_ui",
srcs = glob(
[
- "**/*.html",
"**/*.js",
],
exclude = [
@@ -15,11 +13,11 @@
"node_modules_licenses/**",
"test/**",
"**/*_test.html",
+ "**/*_test.js",
],
),
outs = ["polygerrit_ui.zip"],
entry_point = "elements/gr-app.html",
- redirects = "redirects.json",
)
filegroup(
@@ -49,27 +47,18 @@
"**/*_test.html",
"test/**",
"samples/**",
+ "**/*_test.js",
],
),
)
-# update_links - temporary action. Later links/references will be updated in repository,
-# so this rule will be removed.
-update_links(
- name = "test-srcs-updated-links",
- srcs = [
- "test/common-test-setup.html",
- "test/index.html",
- ":pg_code",
- ],
- redirects = "redirects.json",
-)
-
# Workaround for https://github.com/bazelbuild/bazel/issues/1305
filegroup(
- name = "test-srcs-updated-links-fg",
+ name = "test-srcs-fg",
srcs = [
- ":test-srcs-updated-links",
+ "test/common-test-setup.js",
+ "test/index.html",
+ ":pg_code",
"@ui_dev_npm//:node_modules",
"@ui_npm//:node_modules",
],
@@ -83,7 +72,7 @@
"$(location @ui_dev_npm//web-component-tester/bin:wct)",
],
data = [
- ":test-srcs-updated-links-fg",
+ ":test-srcs-fg",
"@ui_dev_npm//web-component-tester/bin:wct",
],
# Should not run sandboxed.
@@ -109,21 +98,11 @@
],
)
-# update_links - temporary action. Later links/references will be updated in repository,
-# so this rule will be removed.
-update_links(
- name = "polylint-updated-links",
- srcs = [
- ":pg_code_without_test",
- ],
- redirects = "redirects.json",
-)
-
# Workaround for https://github.com/bazelbuild/bazel/issues/1305
filegroup(
- name = "polylint-updated-links-fg",
+ name = "polylint-fg",
srcs = [
- ":polylint-updated-links",
+ ":pg_code_without_test",
"@ui_npm//:node_modules",
],
)
@@ -138,7 +117,7 @@
],
data = [
"polymer.json",
- ":polylint-updated-links-fg",
+ ":polylint-fg",
"@tools_npm//polymer-cli/bin:polymer",
],
# Should not run sandboxed.
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.js
similarity index 70%
rename from polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html
rename to polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.js
index f560ea8..5e6f7c6 100644
--- a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.js
@@ -1,21 +1,19 @@
-<!--
-@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.
--->
-
-<script>
+/**
+ * @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.
+ */
(function(window) {
'use strict';
@@ -63,4 +61,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
index f2f0e6b..4168801 100644
--- a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
@@ -19,40 +19,37 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>async-foreach-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
-<link rel="import" href="async-foreach-behavior.html">
-
-<script>
- suite('async-foreach-behavior tests', async () => {
- await readyToTest();
- test('loops over each item', () => {
- const fn = sinon.stub().returns(Promise.resolve());
- return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
- .then(() => {
- assert.isTrue(fn.calledThrice);
- assert.equal(fn.getCall(0).args[0], 1);
- assert.equal(fn.getCall(1).args[0], 2);
- assert.equal(fn.getCall(2).args[0], 3);
- });
- });
-
- test('halts on stop condition', () => {
- const stub = sinon.stub();
- const fn = (e, stop) => {
- stub(e);
- stop();
- return Promise.resolve();
- };
- return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
- .then(() => {
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args[0], 1);
- });
- });
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module">
+import '../../test/common-test-setup.js';
+import './async-foreach-behavior.js';
+suite('async-foreach-behavior tests', () => {
+ test('loops over each item', () => {
+ const fn = sinon.stub().returns(Promise.resolve());
+ return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
+ .then(() => {
+ assert.isTrue(fn.calledThrice);
+ assert.equal(fn.getCall(0).args[0], 1);
+ assert.equal(fn.getCall(1).args[0], 2);
+ assert.equal(fn.getCall(2).args[0], 3);
+ });
});
+
+ test('halts on stop condition', () => {
+ const stub = sinon.stub();
+ const fn = (e, stop) => {
+ stub(e);
+ stop();
+ return Promise.resolve();
+ };
+ return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
+ .then(() => {
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args[0], 1);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html
deleted file mode 100644
index 92596e0..0000000
--- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html
+++ /dev/null
@@ -1,49 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<script>
-(function(window) {
- 'use strict';
-
- window.Gerrit = window.Gerrit || {};
-
- /** @polymerBehavior Gerrit.BaseUrlBehavior */
- Gerrit.BaseUrlBehavior = {
- /** @return {string} */
- getBaseUrl() {
- return window.CANONICAL_PATH || '';
- },
- };
-
- // eslint-disable-next-line no-unused-vars
- function defineEmptyMixin() {
- // This is a temporary function.
- // Polymer linter doesn't process correctly the following code:
- // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...}
- // To workaround this issue, the mock mixin is declared in this method.
- // In the following changes, legacy behaviors will be converted to mixins.
-
- /**
- * @polymer
- * @mixinFunction
- */
- Gerrit.BaseUrlMixin = base =>
- class extends base {
- };
- }
-})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.js b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.js
new file mode 100644
index 0000000..9682776
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.js
@@ -0,0 +1,46 @@
+/**
+ * @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.
+ */
+(function(window) {
+ 'use strict';
+
+ window.Gerrit = window.Gerrit || {};
+
+ /** @polymerBehavior Gerrit.BaseUrlBehavior */
+ Gerrit.BaseUrlBehavior = {
+ /** @return {string} */
+ getBaseUrl() {
+ return window.CANONICAL_PATH || '';
+ },
+ };
+
+ // eslint-disable-next-line no-unused-vars
+ function defineEmptyMixin() {
+ // This is a temporary function.
+ // Polymer linter doesn't process correctly the following code:
+ // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...}
+ // To workaround this issue, the mock mixin is declared in this method.
+ // In the following changes, legacy behaviors will be converted to mixins.
+
+ /**
+ * @polymer
+ * @mixinFunction
+ */
+ Gerrit.BaseUrlMixin = base =>
+ class extends base {
+ };
+ }
+})(window);
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
index eb6fd3f..62b497f 100644
--- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
@@ -19,18 +19,16 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>base-url-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
-<script>
- /** @type {string} */
- window.CANONICAL_PATH = '/r';
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module">
+import '../../test/common-test-setup.js';
+import './base-url-behavior.js';
+/** @type {string} */
+window.CANONICAL_PATH = '/r';
</script>
-<link rel="import" href="base-url-behavior.html">
-
<test-fixture id="basic">
<template>
<test-element></test-element>
@@ -45,30 +43,32 @@
</template>
</test-fixture>
-<script>
- suite('base-url-behavior tests', async () => {
- await readyToTest();
- let element;
- // eslint-disable-next-line no-unused-vars
- let overlay;
+<script type="module">
+import '../../test/common-test-setup.js';
+import './base-url-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('base-url-behavior tests', () => {
+ let element;
+ // eslint-disable-next-line no-unused-vars
+ let overlay;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [
- Gerrit.BaseUrlBehavior,
- ],
- });
- });
-
- setup(() => {
- element = fixture('basic');
- overlay = fixture('within-overlay');
- });
-
- test('getBaseUrl', () => {
- assert.deepEqual(element.getBaseUrl(), '/r');
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [
+ Gerrit.BaseUrlBehavior,
+ ],
});
});
+
+ setup(() => {
+ element = fixture('basic');
+ overlay = fixture('within-overlay');
+ });
+
+ test('getBaseUrl', () => {
+ assert.deepEqual(element.getBaseUrl(), '/r');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.html b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.js
similarity index 71%
rename from polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.html
rename to polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.js
index 05a7a58..01bcc87 100644
--- a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.html
+++ b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.js
@@ -1,21 +1,21 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../base-url-behavior/base-url-behavior.js';
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="../base-url-behavior/base-url-behavior.html">
-<script>
(function(window) {
'use strict';
@@ -77,4 +77,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
index e480e30..36eec89 100644
--- a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
@@ -15,17 +15,11 @@
limitations under the License.
-->
<!-- Polymer included for the html import polyfill. -->
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<title>docs-url-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<link rel="import" href="docs-url-behavior.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<test-fixture id="basic">
<template>
@@ -33,71 +27,73 @@
</template>
</test-fixture>
-<script>
- suite('docs-url-behavior tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../test/common-test-setup.js';
+import './docs-url-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('docs-url-behavior tests', () => {
+ let element;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'docs-url-behavior-element',
- behaviors: [Gerrit.DocsUrlBehavior],
- });
- });
-
- setup(() => {
- element = fixture('basic');
- element._clearDocsBaseUrlCache();
- });
-
- test('null config', () => {
- const mockRestApi = {
- probePath: sinon.stub().returns(Promise.resolve(true)),
- };
- return element.getDocsBaseUrl(null, mockRestApi)
- .then(docsBaseUrl => {
- assert.isTrue(
- mockRestApi.probePath.calledWith('/Documentation/index.html'));
- assert.equal(docsBaseUrl, '/Documentation');
- });
- });
-
- test('no doc config', () => {
- const mockRestApi = {
- probePath: sinon.stub().returns(Promise.resolve(true)),
- };
- const config = {gerrit: {}};
- return element.getDocsBaseUrl(config, mockRestApi)
- .then(docsBaseUrl => {
- assert.isTrue(
- mockRestApi.probePath.calledWith('/Documentation/index.html'));
- assert.equal(docsBaseUrl, '/Documentation');
- });
- });
-
- test('has doc config', () => {
- const mockRestApi = {
- probePath: sinon.stub().returns(Promise.resolve(true)),
- };
- const config = {gerrit: {doc_url: 'foobar'}};
- return element.getDocsBaseUrl(config, mockRestApi)
- .then(docsBaseUrl => {
- assert.isFalse(mockRestApi.probePath.called);
- assert.equal(docsBaseUrl, 'foobar');
- });
- });
-
- test('no probe', () => {
- const mockRestApi = {
- probePath: sinon.stub().returns(Promise.resolve(false)),
- };
- return element.getDocsBaseUrl(null, mockRestApi)
- .then(docsBaseUrl => {
- assert.isTrue(
- mockRestApi.probePath.calledWith('/Documentation/index.html'));
- assert.isNotOk(docsBaseUrl);
- });
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'docs-url-behavior-element',
+ behaviors: [Gerrit.DocsUrlBehavior],
});
});
+
+ setup(() => {
+ element = fixture('basic');
+ element._clearDocsBaseUrlCache();
+ });
+
+ test('null config', () => {
+ const mockRestApi = {
+ probePath: sinon.stub().returns(Promise.resolve(true)),
+ };
+ return element.getDocsBaseUrl(null, mockRestApi)
+ .then(docsBaseUrl => {
+ assert.isTrue(
+ mockRestApi.probePath.calledWith('/Documentation/index.html'));
+ assert.equal(docsBaseUrl, '/Documentation');
+ });
+ });
+
+ test('no doc config', () => {
+ const mockRestApi = {
+ probePath: sinon.stub().returns(Promise.resolve(true)),
+ };
+ const config = {gerrit: {}};
+ return element.getDocsBaseUrl(config, mockRestApi)
+ .then(docsBaseUrl => {
+ assert.isTrue(
+ mockRestApi.probePath.calledWith('/Documentation/index.html'));
+ assert.equal(docsBaseUrl, '/Documentation');
+ });
+ });
+
+ test('has doc config', () => {
+ const mockRestApi = {
+ probePath: sinon.stub().returns(Promise.resolve(true)),
+ };
+ const config = {gerrit: {doc_url: 'foobar'}};
+ return element.getDocsBaseUrl(config, mockRestApi)
+ .then(docsBaseUrl => {
+ assert.isFalse(mockRestApi.probePath.called);
+ assert.equal(docsBaseUrl, 'foobar');
+ });
+ });
+
+ test('no probe', () => {
+ const mockRestApi = {
+ probePath: sinon.stub().returns(Promise.resolve(false)),
+ };
+ return element.getDocsBaseUrl(null, mockRestApi)
+ .then(docsBaseUrl => {
+ assert.isTrue(
+ mockRestApi.probePath.calledWith('/Documentation/index.html'));
+ assert.isNotOk(docsBaseUrl);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.html b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js
similarity index 70%
rename from polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.html
rename to polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js
index 1377627..1607ba8 100644
--- a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.html
+++ b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js
@@ -1,20 +1,19 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<script>
+/**
+ * @license
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
(function(window) {
'use strict';
@@ -61,4 +60,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
index 9acc749..0432e20 100644
--- a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
@@ -19,14 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>dom-util-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
-<link rel="import" href="dom-util-behavior.html">
-
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="nested-structure">
<template>
<test-element></test-element>
@@ -40,34 +36,36 @@
</template>
</test-fixture>
-<script>
- suite('dom-util-behavior tests', async () => {
- await readyToTest();
- let element;
- let divs;
+<script type="module">
+import '../../test/common-test-setup.js';
+import './dom-util-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('dom-util-behavior tests', () => {
+ let element;
+ let divs;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [Gerrit.DomUtilBehavior],
- });
- });
-
- setup(() => {
- const testDom = fixture('nested-structure');
- element = testDom[0];
- divs = testDom[1];
- });
-
- test('descendedFromClass', () => {
- // .c is a child of .a and not vice versa.
- assert.isTrue(element.descendedFromClass(divs.querySelector('.c'), 'a'));
- assert.isFalse(element.descendedFromClass(divs.querySelector('.a'), 'c'));
-
- // Stops at stop element.
- assert.isFalse(element.descendedFromClass(divs.querySelector('.c'), 'a',
- divs.querySelector('.b')));
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [Gerrit.DomUtilBehavior],
});
});
+
+ setup(() => {
+ const testDom = fixture('nested-structure');
+ element = testDom[0];
+ divs = testDom[1];
+ });
+
+ test('descendedFromClass', () => {
+ // .c is a child of .a and not vice versa.
+ assert.isTrue(element.descendedFromClass(divs.querySelector('.c'), 'a'));
+ assert.isFalse(element.descendedFromClass(divs.querySelector('.a'), 'c'));
+
+ // Stops at stop element.
+ assert.isFalse(element.descendedFromClass(divs.querySelector('.c'), 'a',
+ divs.querySelector('.b')));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.html b/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.js
similarity index 74%
rename from polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.html
rename to polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.js
index 5b3d420..9e9df1d 100644
--- a/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.html
+++ b/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.js
@@ -1,21 +1,19 @@
-<!--
-@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.
--->
-
-<script>
+/**
+ * @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.
+ */
(function(window) {
'use strict';
@@ -69,4 +67,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.js
similarity index 86%
rename from polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html
rename to polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.js
index 7f01789..18cd356 100644
--- a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.js
@@ -1,21 +1,19 @@
-<!--
-@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.
--->
-
-<script>
+/**
+ * @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.
+ */
(function(window) {
'use strict';
@@ -177,4 +175,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
index f4b3ab0..4f80b2b 100644
--- a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
@@ -19,55 +19,53 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
-<link rel="import" href="gr-access-behavior.html">
-
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
<test-element></test-element>
</template>
</test-fixture>
-<script>
- suite('gr-access-behavior tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../test/common-test-setup.js';
+import './gr-access-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('gr-access-behavior tests', () => {
+ let element;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [Gerrit.AccessBehavior],
- });
- });
-
- setup(() => {
- element = fixture('basic');
- });
-
- test('toSortedArray', () => {
- const rules = {
- 'global:Project-Owners': {
- action: 'ALLOW', force: false,
- },
- '4c97682e6ce6b7247f3381b6f1789356666de7f': {
- action: 'ALLOW', force: false,
- },
- };
- const expectedResult = [
- {id: '4c97682e6ce6b7247f3381b6f1789356666de7f', value: {
- action: 'ALLOW', force: false,
- }},
- {id: 'global:Project-Owners', value: {
- action: 'ALLOW', force: false,
- }},
- ];
- assert.deepEqual(element.toSortedArray(rules), expectedResult);
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [Gerrit.AccessBehavior],
});
});
+
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ test('toSortedArray', () => {
+ const rules = {
+ 'global:Project-Owners': {
+ action: 'ALLOW', force: false,
+ },
+ '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+ action: 'ALLOW', force: false,
+ },
+ };
+ const expectedResult = [
+ {id: '4c97682e6ce6b7247f3381b6f1789356666de7f', value: {
+ action: 'ALLOW', force: false,
+ }},
+ {id: 'global:Project-Owners', value: {
+ action: 'ALLOW', force: false,
+ }},
+ ];
+ assert.deepEqual(element.toSortedArray(rules), expectedResult);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js
similarity index 90%
rename from polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html
rename to polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js
index 49160da..90800a3 100644
--- a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js
@@ -1,20 +1,19 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<script>
+/**
+ * @license
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
(function(window) {
'use strict';
@@ -220,4 +219,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
index 7c179b8..06fe722 100644
--- a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
@@ -19,352 +19,350 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
-<link rel="import" href="gr-admin-nav-behavior.html">
-
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
<test-element></test-element>
</template>
</test-fixture>
-<script>
- suite('gr-admin-nav-behavior tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let capabilityStub;
- let menuLinkStub;
+<script type="module">
+import '../../test/common-test-setup.js';
+import './gr-admin-nav-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('gr-admin-nav-behavior tests', () => {
+ let element;
+ let sandbox;
+ let capabilityStub;
+ let menuLinkStub;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [
- Gerrit.AdminNavBehavior,
- ],
- });
- });
-
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- capabilityStub = sinon.stub();
- menuLinkStub = sinon.stub().returns([]);
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- const testAdminLinks = (account, options, expected, done) => {
- element.getAdminLinks(account,
- capabilityStub,
- menuLinkStub,
- options)
- .then(res => {
- assert.equal(expected.totalLength, res.links.length);
- assert.equal(res.links[0].name, 'Repositories');
- // Repos
- if (expected.groupListShown) {
- assert.equal(res.links[1].name, 'Groups');
- }
-
- if (expected.pluginListShown) {
- assert.equal(res.links[2].name, 'Plugins');
- assert.isNotOk(res.links[2].subsection);
- }
-
- if (expected.projectPageShown) {
- assert.isOk(res.links[0].subsection);
- assert.equal(res.links[0].subsection.children.length, 5);
- } else {
- assert.isNotOk(res.links[0].subsection);
- }
- // Groups
- if (expected.groupPageShown) {
- assert.isOk(res.links[1].subsection);
- assert.equal(res.links[1].subsection.children.length,
- expected.groupSubpageLength);
- } else if ( expected.totalLength > 1) {
- assert.isNotOk(res.links[1].subsection);
- }
-
- if (expected.pluginGeneratedLinks) {
- for (const link of expected.pluginGeneratedLinks) {
- const linkMatch = res.links
- .find(l => (l.url === link.url && l.name === link.text));
- assert.isTrue(!!linkMatch);
-
- // External links should open in new tab.
- if (link.url[0] !== '/') {
- assert.equal(linkMatch.target, '_blank');
- } else {
- assert.isNotOk(linkMatch.target);
- }
- }
- }
-
- // Current section
- if (expected.projectPageShown || expected.groupPageShown) {
- assert.isOk(res.expandedSection);
- assert.isOk(res.expandedSection.children);
- } else {
- assert.isNotOk(res.expandedSection);
- }
- if (expected.projectPageShown) {
- assert.equal(res.expandedSection.name, 'my-repo');
- assert.equal(res.expandedSection.children.length, 5);
- } else if (expected.groupPageShown) {
- assert.equal(res.expandedSection.name, 'my-group');
- assert.equal(res.expandedSection.children.length,
- expected.groupSubpageLength);
- }
- done();
- });
- };
-
- suite('logged out', () => {
- let account;
- let expected;
-
- setup(() => {
- expected = {
- groupListShown: false,
- groupPageShown: false,
- pluginListShown: false,
- };
- });
-
- test('without a specific repo or group', done => {
- let options;
- expected = Object.assign(expected, {
- totalLength: 1,
- projectPageShown: false,
- });
- testAdminLinks(account, options, expected, done);
- });
-
- test('with a repo', done => {
- const options = {repoName: 'my-repo'};
- expected = Object.assign(expected, {
- totalLength: 1,
- projectPageShown: true,
- });
- testAdminLinks(account, options, expected, done);
- });
-
- test('with plugin generated links', done => {
- let options;
- const generatedLinks = [
- {text: 'internal link text', url: '/internal/link/url'},
- {text: 'external link text', url: 'http://external/link/url'},
- ];
- menuLinkStub.returns(generatedLinks);
- expected = Object.assign(expected, {
- totalLength: 3,
- projectPageShown: false,
- pluginGeneratedLinks: generatedLinks,
- });
- testAdminLinks(account, options, expected, done);
- });
- });
-
- suite('no plugin capability logged in', () => {
- const account = {
- name: 'test-user',
- };
- let expected;
-
- setup(() => {
- expected = {
- totalLength: 2,
- pluginListShown: false,
- };
- capabilityStub.returns(Promise.resolve({}));
- });
-
- test('without a specific project or group', done => {
- let options;
- expected = Object.assign(expected, {
- projectPageShown: false,
- groupListShown: true,
- groupPageShown: false,
- });
- testAdminLinks(account, options, expected, done);
- });
-
- test('with a repo', done => {
- const account = {
- name: 'test-user',
- };
- const options = {repoName: 'my-repo'};
- expected = Object.assign(expected, {
- projectPageShown: true,
- groupListShown: true,
- groupPageShown: false,
- });
- testAdminLinks(account, options, expected, done);
- });
- });
-
- suite('view plugin capability logged in', () => {
- const account = {
- name: 'test-user',
- };
- let expected;
-
- setup(() => {
- capabilityStub.returns(Promise.resolve({viewPlugins: true}));
- expected = {
- totalLength: 3,
- groupListShown: true,
- pluginListShown: true,
- };
- });
-
- test('without a specific repo or group', done => {
- let options;
- expected = Object.assign(expected, {
- projectPageShown: false,
- groupPageShown: false,
- });
- testAdminLinks(account, options, expected, done);
- });
-
- test('with a repo', done => {
- const options = {repoName: 'my-repo'};
- expected = Object.assign(expected, {
- projectPageShown: true,
- groupPageShown: false,
- });
- testAdminLinks(account, options, expected, done);
- });
-
- test('admin with internal group', done => {
- const options = {
- groupId: 'a15262',
- groupName: 'my-group',
- groupIsInternal: true,
- isAdmin: true,
- groupOwner: false,
- };
- expected = Object.assign(expected, {
- projectPageShown: false,
- groupPageShown: true,
- groupSubpageLength: 2,
- });
- testAdminLinks(account, options, expected, done);
- });
-
- test('group owner with internal group', done => {
- const options = {
- groupId: 'a15262',
- groupName: 'my-group',
- groupIsInternal: true,
- isAdmin: false,
- groupOwner: true,
- };
- expected = Object.assign(expected, {
- projectPageShown: false,
- groupPageShown: true,
- groupSubpageLength: 2,
- });
- testAdminLinks(account, options, expected, done);
- });
-
- test('non owner or admin with internal group', done => {
- const options = {
- groupId: 'a15262',
- groupName: 'my-group',
- groupIsInternal: true,
- isAdmin: false,
- groupOwner: false,
- };
- expected = Object.assign(expected, {
- projectPageShown: false,
- groupPageShown: true,
- groupSubpageLength: 1,
- });
- testAdminLinks(account, options, expected, done);
- });
-
- test('admin with external group', done => {
- const options = {
- groupId: 'a15262',
- groupName: 'my-group',
- groupIsInternal: false,
- isAdmin: true,
- groupOwner: true,
- };
- expected = Object.assign(expected, {
- projectPageShown: false,
- groupPageShown: true,
- groupSubpageLength: 0,
- });
- testAdminLinks(account, options, expected, done);
- });
- });
-
- suite('view plugin screen with plugin capability', () => {
- const account = {
- name: 'test-user',
- };
- let expected;
-
- setup(() => {
- capabilityStub.returns(Promise.resolve({pluginCapability: true}));
- expected = {};
- });
-
- test('with plugin with capabilities', done => {
- let options;
- const generatedLinks = [
- {text: 'without capability', url: '/without'},
- {text: 'with capability',
- url: '/with',
- capability: 'pluginCapability'},
- ];
- menuLinkStub.returns(generatedLinks);
- expected = Object.assign(expected, {
- totalLength: 4,
- pluginGeneratedLinks: generatedLinks,
- });
- testAdminLinks(account, options, expected, done);
- });
- });
-
- suite('view plugin screen without plugin capability', () => {
- const account = {
- name: 'test-user',
- };
- let expected;
-
- setup(() => {
- capabilityStub.returns(Promise.resolve({}));
- expected = {};
- });
-
- test('with plugin with capabilities', done => {
- let options;
- const generatedLinks = [
- {text: 'without capability', url: '/without'},
- {text: 'with capability',
- url: '/with',
- capability: 'pluginCapability'},
- ];
- menuLinkStub.returns(generatedLinks);
- expected = Object.assign(expected, {
- totalLength: 3,
- pluginGeneratedLinks: [generatedLinks[0]],
- });
- testAdminLinks(account, options, expected, done);
- });
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [
+ Gerrit.AdminNavBehavior,
+ ],
});
});
+
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ capabilityStub = sinon.stub();
+ menuLinkStub = sinon.stub().returns([]);
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ const testAdminLinks = (account, options, expected, done) => {
+ element.getAdminLinks(account,
+ capabilityStub,
+ menuLinkStub,
+ options)
+ .then(res => {
+ assert.equal(expected.totalLength, res.links.length);
+ assert.equal(res.links[0].name, 'Repositories');
+ // Repos
+ if (expected.groupListShown) {
+ assert.equal(res.links[1].name, 'Groups');
+ }
+
+ if (expected.pluginListShown) {
+ assert.equal(res.links[2].name, 'Plugins');
+ assert.isNotOk(res.links[2].subsection);
+ }
+
+ if (expected.projectPageShown) {
+ assert.isOk(res.links[0].subsection);
+ assert.equal(res.links[0].subsection.children.length, 5);
+ } else {
+ assert.isNotOk(res.links[0].subsection);
+ }
+ // Groups
+ if (expected.groupPageShown) {
+ assert.isOk(res.links[1].subsection);
+ assert.equal(res.links[1].subsection.children.length,
+ expected.groupSubpageLength);
+ } else if ( expected.totalLength > 1) {
+ assert.isNotOk(res.links[1].subsection);
+ }
+
+ if (expected.pluginGeneratedLinks) {
+ for (const link of expected.pluginGeneratedLinks) {
+ const linkMatch = res.links
+ .find(l => (l.url === link.url && l.name === link.text));
+ assert.isTrue(!!linkMatch);
+
+ // External links should open in new tab.
+ if (link.url[0] !== '/') {
+ assert.equal(linkMatch.target, '_blank');
+ } else {
+ assert.isNotOk(linkMatch.target);
+ }
+ }
+ }
+
+ // Current section
+ if (expected.projectPageShown || expected.groupPageShown) {
+ assert.isOk(res.expandedSection);
+ assert.isOk(res.expandedSection.children);
+ } else {
+ assert.isNotOk(res.expandedSection);
+ }
+ if (expected.projectPageShown) {
+ assert.equal(res.expandedSection.name, 'my-repo');
+ assert.equal(res.expandedSection.children.length, 5);
+ } else if (expected.groupPageShown) {
+ assert.equal(res.expandedSection.name, 'my-group');
+ assert.equal(res.expandedSection.children.length,
+ expected.groupSubpageLength);
+ }
+ done();
+ });
+ };
+
+ suite('logged out', () => {
+ let account;
+ let expected;
+
+ setup(() => {
+ expected = {
+ groupListShown: false,
+ groupPageShown: false,
+ pluginListShown: false,
+ };
+ });
+
+ test('without a specific repo or group', done => {
+ let options;
+ expected = Object.assign(expected, {
+ totalLength: 1,
+ projectPageShown: false,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('with a repo', done => {
+ const options = {repoName: 'my-repo'};
+ expected = Object.assign(expected, {
+ totalLength: 1,
+ projectPageShown: true,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('with plugin generated links', done => {
+ let options;
+ const generatedLinks = [
+ {text: 'internal link text', url: '/internal/link/url'},
+ {text: 'external link text', url: 'http://external/link/url'},
+ ];
+ menuLinkStub.returns(generatedLinks);
+ expected = Object.assign(expected, {
+ totalLength: 3,
+ projectPageShown: false,
+ pluginGeneratedLinks: generatedLinks,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+ });
+
+ suite('no plugin capability logged in', () => {
+ const account = {
+ name: 'test-user',
+ };
+ let expected;
+
+ setup(() => {
+ expected = {
+ totalLength: 2,
+ pluginListShown: false,
+ };
+ capabilityStub.returns(Promise.resolve({}));
+ });
+
+ test('without a specific project or group', done => {
+ let options;
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupListShown: true,
+ groupPageShown: false,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('with a repo', done => {
+ const account = {
+ name: 'test-user',
+ };
+ const options = {repoName: 'my-repo'};
+ expected = Object.assign(expected, {
+ projectPageShown: true,
+ groupListShown: true,
+ groupPageShown: false,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+ });
+
+ suite('view plugin capability logged in', () => {
+ const account = {
+ name: 'test-user',
+ };
+ let expected;
+
+ setup(() => {
+ capabilityStub.returns(Promise.resolve({viewPlugins: true}));
+ expected = {
+ totalLength: 3,
+ groupListShown: true,
+ pluginListShown: true,
+ };
+ });
+
+ test('without a specific repo or group', done => {
+ let options;
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupPageShown: false,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('with a repo', done => {
+ const options = {repoName: 'my-repo'};
+ expected = Object.assign(expected, {
+ projectPageShown: true,
+ groupPageShown: false,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('admin with internal group', done => {
+ const options = {
+ groupId: 'a15262',
+ groupName: 'my-group',
+ groupIsInternal: true,
+ isAdmin: true,
+ groupOwner: false,
+ };
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupPageShown: true,
+ groupSubpageLength: 2,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('group owner with internal group', done => {
+ const options = {
+ groupId: 'a15262',
+ groupName: 'my-group',
+ groupIsInternal: true,
+ isAdmin: false,
+ groupOwner: true,
+ };
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupPageShown: true,
+ groupSubpageLength: 2,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('non owner or admin with internal group', done => {
+ const options = {
+ groupId: 'a15262',
+ groupName: 'my-group',
+ groupIsInternal: true,
+ isAdmin: false,
+ groupOwner: false,
+ };
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupPageShown: true,
+ groupSubpageLength: 1,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('admin with external group', done => {
+ const options = {
+ groupId: 'a15262',
+ groupName: 'my-group',
+ groupIsInternal: false,
+ isAdmin: true,
+ groupOwner: true,
+ };
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupPageShown: true,
+ groupSubpageLength: 0,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+ });
+
+ suite('view plugin screen with plugin capability', () => {
+ const account = {
+ name: 'test-user',
+ };
+ let expected;
+
+ setup(() => {
+ capabilityStub.returns(Promise.resolve({pluginCapability: true}));
+ expected = {};
+ });
+
+ test('with plugin with capabilities', done => {
+ let options;
+ const generatedLinks = [
+ {text: 'without capability', url: '/without'},
+ {text: 'with capability',
+ url: '/with',
+ capability: 'pluginCapability'},
+ ];
+ menuLinkStub.returns(generatedLinks);
+ expected = Object.assign(expected, {
+ totalLength: 4,
+ pluginGeneratedLinks: generatedLinks,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+ });
+
+ suite('view plugin screen without plugin capability', () => {
+ const account = {
+ name: 'test-user',
+ };
+ let expected;
+
+ setup(() => {
+ capabilityStub.returns(Promise.resolve({}));
+ expected = {};
+ });
+
+ test('with plugin with capabilities', done => {
+ let options;
+ const generatedLinks = [
+ {text: 'without capability', url: '/without'},
+ {text: 'with capability',
+ url: '/with',
+ capability: 'pluginCapability'},
+ ];
+ menuLinkStub.returns(generatedLinks);
+ expected = Object.assign(expected, {
+ totalLength: 3,
+ pluginGeneratedLinks: [generatedLinks[0]],
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.js
similarity index 79%
rename from polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html
rename to polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.js
index d03316a..d12b279 100644
--- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.js
@@ -1,20 +1,19 @@
-<!--
-@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.
--->
-<script>
+/**
+ * @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.
+ */
(function(window) {
'use strict';
@@ -105,4 +104,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
index a8d4041..2889371 100644
--- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
@@ -19,14 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
-<link rel="import" href="gr-change-table-behavior.html">
-
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
<test-element></test-element>
@@ -41,87 +37,89 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-table-behavior tests', async () => {
- await readyToTest();
- let element;
- // eslint-disable-next-line no-unused-vars
- let overlay;
+<script type="module">
+import '../../test/common-test-setup.js';
+import './gr-change-table-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('gr-change-table-behavior tests', () => {
+ let element;
+ // eslint-disable-next-line no-unused-vars
+ let overlay;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [Gerrit.ChangeTableBehavior],
- });
- });
-
- setup(() => {
- element = fixture('basic');
- overlay = fixture('within-overlay');
- });
-
- test('getComplementColumns', () => {
- let columns = [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Repo',
- 'Branch',
- 'Updated',
- 'Size',
- ];
- assert.deepEqual(element.getComplementColumns(columns), []);
-
- columns = [
- 'Subject',
- 'Status',
- 'Assignee',
- 'Repo',
- 'Branch',
- 'Size',
- ];
- assert.deepEqual(element.getComplementColumns(columns),
- ['Owner', 'Updated']);
- });
-
- test('isColumnHidden', () => {
- const columnToCheck = 'Repo';
- let columnsToDisplay = [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Repo',
- 'Branch',
- 'Updated',
- 'Size',
- ];
- assert.isFalse(element.isColumnHidden(columnToCheck, columnsToDisplay));
-
- columnsToDisplay = [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Branch',
- 'Updated',
- 'Size',
- ];
- assert.isTrue(element.isColumnHidden(columnToCheck, columnsToDisplay));
- });
-
- test('getVisibleColumns maps Project to Repo', () => {
- const columns = [
- 'Subject',
- 'Status',
- 'Owner',
- ];
- assert.deepEqual(element.getVisibleColumns(columns), columns.slice(0));
- assert.deepEqual(
- element.getVisibleColumns(columns.concat(['Project'])),
- columns.slice(0).concat(['Repo']));
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [Gerrit.ChangeTableBehavior],
});
});
+
+ setup(() => {
+ element = fixture('basic');
+ overlay = fixture('within-overlay');
+ });
+
+ test('getComplementColumns', () => {
+ let columns = [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Repo',
+ 'Branch',
+ 'Updated',
+ 'Size',
+ ];
+ assert.deepEqual(element.getComplementColumns(columns), []);
+
+ columns = [
+ 'Subject',
+ 'Status',
+ 'Assignee',
+ 'Repo',
+ 'Branch',
+ 'Size',
+ ];
+ assert.deepEqual(element.getComplementColumns(columns),
+ ['Owner', 'Updated']);
+ });
+
+ test('isColumnHidden', () => {
+ const columnToCheck = 'Repo';
+ let columnsToDisplay = [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Repo',
+ 'Branch',
+ 'Updated',
+ 'Size',
+ ];
+ assert.isFalse(element.isColumnHidden(columnToCheck, columnsToDisplay));
+
+ columnsToDisplay = [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Branch',
+ 'Updated',
+ 'Size',
+ ];
+ assert.isTrue(element.isColumnHidden(columnToCheck, columnsToDisplay));
+ });
+
+ test('getVisibleColumns maps Project to Repo', () => {
+ const columns = [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ ];
+ assert.deepEqual(element.getVisibleColumns(columns), columns.slice(0));
+ assert.deepEqual(
+ element.getVisibleColumns(columns.concat(['Project'])),
+ columns.slice(0).concat(['Repo']));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.html b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.html
deleted file mode 100644
index e5ded0e..0000000
--- a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.html
+++ /dev/null
@@ -1,60 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<script src="../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
-
-<script>
-(function(window) {
- 'use strict';
-
- window.Gerrit = window.Gerrit || {};
-
- /** @polymerBehavior Gerrit.DisplayNameBehavior */
- Gerrit.DisplayNameBehavior = {
- // TODO(dmfilippov) replace DisplayNameBehavior with GrDisplayNameUtils
-
- /**
- * enableEmail when true enables to fallback to using email if
- * the account name is not avilable.
- */
- getUserName(config, account, enableEmail) {
- return GrDisplayNameUtils.getUserName(config, account, enableEmail);
- },
-
- getGroupDisplayName(group) {
- return GrDisplayNameUtils.getGroupDisplayName(group);
- },
- };
-
- // eslint-disable-next-line no-unused-vars
- function defineEmptyMixin() {
- // This is a temporary function.
- // Polymer linter doesn't process correctly the following code:
- // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...}
- // To workaround this issue, the mock mixin is declared in this method.
- // In the following changes, legacy behaviors will be converted to mixins.
-
- /**
- * @polymer
- * @mixinFunction
- */
- Gerrit.DisplayNameMixin = base =>
- class extends base {
- };
- }
-})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.js b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.js
new file mode 100644
index 0000000..2e7f5d4
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.js
@@ -0,0 +1,53 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../scripts/gr-display-name-utils/gr-display-name-utils.js';
+
+(function(window) {
+ 'use strict';
+
+ window.Gerrit = window.Gerrit || {};
+
+ /** @polymerBehavior Gerrit.DisplayNameBehavior */
+ Gerrit.DisplayNameBehavior = {
+ // TODO(dmfilippov) replace DisplayNameBehavior with GrDisplayNameUtils
+
+ getUserName(config, account) {
+ return GrDisplayNameUtils.getUserName(config, account);
+ },
+
+ getGroupDisplayName(group) {
+ return GrDisplayNameUtils.getGroupDisplayName(group);
+ },
+ };
+
+ // eslint-disable-next-line no-unused-vars
+ function defineEmptyMixin() {
+ // This is a temporary function.
+ // Polymer linter doesn't process correctly the following code:
+ // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...}
+ // To workaround this issue, the mock mixin is declared in this method.
+ // In the following changes, legacy behaviors will be converted to mixins.
+
+ /**
+ * @polymer
+ * @mixinFunction
+ */
+ Gerrit.DisplayNameMixin = base =>
+ class extends base {
+ };
+ }
+})(window);
diff --git a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
index 931f6fa..e4fc3f2 100644
--- a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
@@ -19,83 +19,81 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-display-name-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
-<link rel="import" href="gr-display-name-behavior.html">
-
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
<test-element-anon></test-element-anon>
</template>
</test-fixture>
-<script>
- suite('gr-display-name-behavior tests', async () => {
- await readyToTest();
- let element;
- // eslint-disable-next-line no-unused-vars
- const config = {
- user: {
- anonymous_coward_name: 'Anonymous Coward',
- },
- };
+<script type="module">
+import '../../test/common-test-setup.js';
+import './gr-display-name-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('gr-display-name-behavior tests', () => {
+ let element;
+ // eslint-disable-next-line no-unused-vars
+ const config = {
+ user: {
+ anonymous_coward_name: 'Anonymous Coward',
+ },
+ };
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element-anon',
- behaviors: [
- Gerrit.DisplayNameBehavior,
- ],
- });
- });
-
- setup(() => {
- element = fixture('basic');
- });
-
- test('getUserName name only', () => {
- const account = {
- name: 'test-name',
- };
- assert.deepEqual(element.getUserName(config, account, true), 'test-name');
- });
-
- test('getUserName username only', () => {
- const account = {
- username: 'test-user',
- };
- assert.deepEqual(element.getUserName(config, account, true), 'test-user');
- });
-
- test('getUserName email only', () => {
- const account = {
- email: 'test-user@test-url.com',
- };
- assert.deepEqual(element.getUserName(config, account, true),
- 'test-user@test-url.com');
- });
-
- test('getUserName returns not Anonymous Coward as the anon name', () => {
- assert.deepEqual(element.getUserName(config, null, true), 'Anonymous');
- });
-
- test('getUserName for the config returning the anon name', () => {
- const config = {
- user: {
- anonymous_coward_name: 'Test Anon',
- },
- };
- assert.deepEqual(element.getUserName(config, null, true), 'Test Anon');
- });
-
- test('getGroupDisplayName', () => {
- assert.equal(element.getGroupDisplayName({name: 'Some user name'}),
- 'Some user name (group)');
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element-anon',
+ behaviors: [
+ Gerrit.DisplayNameBehavior,
+ ],
});
});
+
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ test('getUserName name only', () => {
+ const account = {
+ name: 'test-name',
+ };
+ assert.equal(element.getUserName(config, account), 'test-name');
+ });
+
+ test('getUserName username only', () => {
+ const account = {
+ username: 'test-user',
+ };
+ assert.equal(element.getUserName(config, account), 'test-user');
+ });
+
+ test('getUserName email only', () => {
+ const account = {
+ email: 'test-user@test-url.com',
+ };
+ assert.equal(element.getUserName(config, account),
+ 'test-user@test-url.com');
+ });
+
+ test('getUserName returns not Anonymous Coward as the anon name', () => {
+ assert.equal(element.getUserName(config, null), 'Anonymous');
+ });
+
+ test('getUserName for the config returning the anon name', () => {
+ const config = {
+ user: {
+ anonymous_coward_name: 'Test Anon',
+ },
+ };
+ assert.equal(element.getUserName(config, null), 'Test Anon');
+ });
+
+ test('getGroupDisplayName', () => {
+ assert.equal(element.getGroupDisplayName({name: 'Some user name'}),
+ 'Some user name (group)');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.html b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js
similarity index 65%
rename from polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.html
rename to polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js
index 06912d5..6e64a4d 100644
--- a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js
@@ -1,22 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../base-url-behavior/base-url-behavior.js';
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="../base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<script>
+import '../gr-url-encoding-behavior/gr-url-encoding-behavior.js';
(function(window) {
'use strict';
@@ -80,5 +80,3 @@
};
}
})(window);
-
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
index c2cb073..c918d5e 100644
--- a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
@@ -19,76 +19,74 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
-<link rel="import" href="gr-list-view-behavior.html">
-
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
<test-element></test-element>
</template>
</test-fixture>
-<script>
- suite('gr-list-view-behavior tests', async () => {
- await readyToTest();
- let element;
- // eslint-disable-next-line no-unused-vars
- let overlay;
+<script type="module">
+import '../../test/common-test-setup.js';
+import './gr-list-view-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('gr-list-view-behavior tests', () => {
+ let element;
+ // eslint-disable-next-line no-unused-vars
+ let overlay;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [Gerrit.ListViewBehavior],
- });
- });
-
- setup(() => {
- element = fixture('basic');
- });
-
- test('computeLoadingClass', () => {
- assert.equal(element.computeLoadingClass(true), 'loading');
- assert.equal(element.computeLoadingClass(false), '');
- });
-
- test('computeShownItems', () => {
- const myArr = new Array(26);
- assert.equal(element.computeShownItems(myArr).length, 25);
- });
-
- test('getUrl', () => {
- assert.equal(element.getUrl('/path/to/something/', 'item'),
- '/path/to/something/item');
- assert.equal(element.getUrl('/path/to/something/', 'item%test'),
- '/path/to/something/item%2525test');
- });
-
- test('getFilterValue', () => {
- let params;
- assert.equal(element.getFilterValue(params), '');
-
- params = {filter: null};
- assert.equal(element.getFilterValue(params), '');
-
- params = {filter: 'test'};
- assert.equal(element.getFilterValue(params), 'test');
- });
-
- test('getOffsetValue', () => {
- let params;
- assert.equal(element.getOffsetValue(params), 0);
-
- params = {offset: null};
- assert.equal(element.getOffsetValue(params), 0);
-
- params = {offset: 1};
- assert.equal(element.getOffsetValue(params), 1);
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [Gerrit.ListViewBehavior],
});
});
+
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ test('computeLoadingClass', () => {
+ assert.equal(element.computeLoadingClass(true), 'loading');
+ assert.equal(element.computeLoadingClass(false), '');
+ });
+
+ test('computeShownItems', () => {
+ const myArr = new Array(26);
+ assert.equal(element.computeShownItems(myArr).length, 25);
+ });
+
+ test('getUrl', () => {
+ assert.equal(element.getUrl('/path/to/something/', 'item'),
+ '/path/to/something/item');
+ assert.equal(element.getUrl('/path/to/something/', 'item%test'),
+ '/path/to/something/item%2525test');
+ });
+
+ test('getFilterValue', () => {
+ let params;
+ assert.equal(element.getFilterValue(params), '');
+
+ params = {filter: null};
+ assert.equal(element.getFilterValue(params), '');
+
+ params = {filter: 'test'};
+ assert.equal(element.getFilterValue(params), 'test');
+ });
+
+ test('getOffsetValue', () => {
+ let params;
+ assert.equal(element.getOffsetValue(params), 0);
+
+ params = {offset: null};
+ assert.equal(element.getOffsetValue(params), 0);
+
+ params = {offset: 1};
+ assert.equal(element.getOffsetValue(params), 1);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js
similarity index 93%
rename from polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
rename to polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js
index 61e6b0a..b36375e 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js
@@ -1,20 +1,19 @@
-<!--
-@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.
--->
-<script>
+/**
+ * @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.
+ */
(function(window) {
'use strict';
@@ -298,4 +297,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
index d9adb8b..e0bbf17 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
@@ -15,313 +15,310 @@
limitations under the License.
-->
<!-- Polymer included for the html import polyfill. -->
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<title>gr-patch-set-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<link rel="import" href="gr-patch-set-behavior.html">
-
-<script>
- suite('gr-patch-set-behavior tests', async () => {
- await readyToTest();
- test('getRevisionByPatchNum', () => {
- const get = Gerrit.PatchSetBehavior.getRevisionByPatchNum;
- const revisions = [
- {_number: 0},
- {_number: 1},
- {_number: 2},
- ];
- assert.deepEqual(get(revisions, '1'), revisions[1]);
- assert.deepEqual(get(revisions, 2), revisions[2]);
- assert.equal(get(revisions, '3'), undefined);
- });
-
- test('fetchChangeUpdates on latest', done => {
- const knownChange = {
- revisions: {
- sha1: {description: 'patch 1', _number: 1},
- sha2: {description: 'patch 2', _number: 2},
- },
- status: 'NEW',
- messages: [],
- };
- const mockRestApi = {
- getChangeDetail() {
- return Promise.resolve(knownChange);
- },
- };
- Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
- .then(result => {
- assert.isTrue(result.isLatest);
- assert.isNotOk(result.newStatus);
- assert.isFalse(result.newMessages);
- done();
- });
- });
-
- test('fetchChangeUpdates not on latest', done => {
- const knownChange = {
- revisions: {
- sha1: {description: 'patch 1', _number: 1},
- sha2: {description: 'patch 2', _number: 2},
- },
- status: 'NEW',
- messages: [],
- };
- const actualChange = {
- revisions: {
- sha1: {description: 'patch 1', _number: 1},
- sha2: {description: 'patch 2', _number: 2},
- sha3: {description: 'patch 3', _number: 3},
- },
- status: 'NEW',
- messages: [],
- };
- const mockRestApi = {
- getChangeDetail() {
- return Promise.resolve(actualChange);
- },
- };
- Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
- .then(result => {
- assert.isFalse(result.isLatest);
- assert.isNotOk(result.newStatus);
- assert.isFalse(result.newMessages);
- done();
- });
- });
-
- test('fetchChangeUpdates new status', done => {
- const knownChange = {
- revisions: {
- sha1: {description: 'patch 1', _number: 1},
- sha2: {description: 'patch 2', _number: 2},
- },
- status: 'NEW',
- messages: [],
- };
- const actualChange = {
- revisions: {
- sha1: {description: 'patch 1', _number: 1},
- sha2: {description: 'patch 2', _number: 2},
- },
- status: 'MERGED',
- messages: [],
- };
- const mockRestApi = {
- getChangeDetail() {
- return Promise.resolve(actualChange);
- },
- };
- Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
- .then(result => {
- assert.isTrue(result.isLatest);
- assert.equal(result.newStatus, 'MERGED');
- assert.isFalse(result.newMessages);
- done();
- });
- });
-
- test('fetchChangeUpdates new messages', done => {
- const knownChange = {
- revisions: {
- sha1: {description: 'patch 1', _number: 1},
- sha2: {description: 'patch 2', _number: 2},
- },
- status: 'NEW',
- messages: [],
- };
- const actualChange = {
- revisions: {
- sha1: {description: 'patch 1', _number: 1},
- sha2: {description: 'patch 2', _number: 2},
- },
- status: 'NEW',
- messages: [{message: 'blah blah'}],
- };
- const mockRestApi = {
- getChangeDetail() {
- return Promise.resolve(actualChange);
- },
- };
- Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
- .then(result => {
- assert.isTrue(result.isLatest);
- assert.isNotOk(result.newStatus);
- assert.isTrue(result.newMessages);
- done();
- });
- });
-
- test('_computeWipForPatchSets', () => {
- // Compute patch sets for a given timeline on a change. The initial WIP
- // property of the change can be true or false. The map of tags by
- // revision is keyed by patch set number. Each value is a list of change
- // message tags in the order that they occurred in the timeline. These
- // indicate actions that modify the WIP property of the change and/or
- // create new patch sets.
- //
- // Returns the actual results with an assertWip method that can be used
- // to compare against an expected value for a particular patch set.
- const compute = (initialWip, tagsByRevision) => {
- const change = {
- messages: [],
- work_in_progress: initialWip,
- };
- const revs = Object.keys(tagsByRevision).sort((a, b) => a - b);
- for (const rev of revs) {
- for (const tag of tagsByRevision[rev]) {
- change.messages.push({
- tag,
- _revision_number: rev,
- });
- }
- }
- let patchNums = revs.map(rev => { return {num: rev}; });
- patchNums = Gerrit.PatchSetBehavior._computeWipForPatchSets(
- change, patchNums);
- const actualWipsByRevision = {};
- for (const patchNum of patchNums) {
- actualWipsByRevision[patchNum.num] = patchNum.wip;
- }
- const verifier = {
- assertWip(revision, expectedWip) {
- const patchNum = patchNums.find(patchNum => patchNum.num == revision);
- if (!patchNum) {
- assert.fail('revision ' + revision + ' not found');
- }
- assert.equal(patchNum.wip, expectedWip,
- 'wip state for ' + revision + ' is ' +
- patchNum.wip + '; expected ' + expectedWip);
- return verifier;
- },
- };
- return verifier;
- };
-
- compute(false, {1: ['upload']}).assertWip(1, false);
- compute(true, {1: ['upload']}).assertWip(1, true);
-
- const setWip = 'autogenerated:gerrit:setWorkInProgress';
- const uploadInWip = 'autogenerated:gerrit:newWipPatchSet';
- const clearWip = 'autogenerated:gerrit:setReadyForReview';
-
- compute(false, {
- 1: ['upload', setWip],
- 2: ['upload'],
- 3: ['upload', clearWip],
- 4: ['upload', setWip],
- }).assertWip(1, false) // Change was created with PS1 ready for review
- .assertWip(2, true) // PS2 was uploaded during WIP
- .assertWip(3, false) // PS3 was marked ready for review after upload
- .assertWip(4, false); // PS4 was uploaded ready for review
-
- compute(false, {
- 1: [uploadInWip, null, 'addReviewer'],
- 2: ['upload'],
- 3: ['upload', clearWip, setWip],
- 4: ['upload'],
- 5: ['upload', clearWip],
- 6: [uploadInWip],
- }).assertWip(1, true) // Change was created in WIP
- .assertWip(2, true) // PS2 was uploaded during WIP
- .assertWip(3, false) // PS3 was marked ready for review
- .assertWip(4, true) // PS4 was uploaded during WIP
- .assertWip(5, false) // PS5 was marked ready for review
- .assertWip(6, true); // PS6 was uploaded with WIP option
- });
-
- test('patchNumEquals', () => {
- const equals = Gerrit.PatchSetBehavior.patchNumEquals;
- assert.isFalse(equals('edit', 'PARENT'));
- assert.isFalse(equals('edit', NaN));
- assert.isFalse(equals(1, '2'));
-
- assert.isTrue(equals(1, '1'));
- assert.isTrue(equals(1, 1));
- assert.isTrue(equals('edit', 'edit'));
- assert.isTrue(equals('PARENT', 'PARENT'));
- });
-
- test('isMergeParent', () => {
- const isParent = Gerrit.PatchSetBehavior.isMergeParent;
- assert.isFalse(isParent(1));
- assert.isFalse(isParent(4321));
- assert.isFalse(isParent('52'));
- assert.isFalse(isParent('edit'));
- assert.isFalse(isParent('PARENT'));
- assert.isFalse(isParent(0));
-
- assert.isTrue(isParent(-23));
- assert.isTrue(isParent(-1));
- assert.isTrue(isParent('-42'));
- });
-
- test('findEditParentRevision', () => {
- const findParent = Gerrit.PatchSetBehavior.findEditParentRevision;
- let revisions = [
- {_number: 0},
- {_number: 1},
- {_number: 2},
- ];
- assert.strictEqual(findParent(revisions), null);
-
- revisions = [...revisions, {_number: 'edit', basePatchNum: 3}];
- assert.strictEqual(findParent(revisions), null);
-
- revisions = [...revisions, {_number: 3}];
- assert.deepEqual(findParent(revisions), {_number: 3});
- });
-
- test('findEditParentPatchNum', () => {
- const findNum = Gerrit.PatchSetBehavior.findEditParentPatchNum;
- let revisions = [
- {_number: 0},
- {_number: 1},
- {_number: 2},
- ];
- assert.equal(findNum(revisions), -1);
-
- revisions =
- [...revisions, {_number: 'edit', basePatchNum: 3}, {_number: 3}];
- assert.deepEqual(findNum(revisions), 3);
- });
-
- test('sortRevisions', () => {
- const sort = Gerrit.PatchSetBehavior.sortRevisions;
- const revisions = [
- {_number: 0},
- {_number: 2},
- {_number: 1},
- ];
- const sorted = [
- {_number: 2},
- {_number: 1},
- {_number: 0},
- ];
-
- assert.deepEqual(sort(revisions), sorted);
-
- // Edit patchset should follow directly after its basePatchNum.
- revisions.push({_number: 'edit', basePatchNum: 2});
- sorted.unshift({_number: 'edit', basePatchNum: 2});
- assert.deepEqual(sort(revisions), sorted);
-
- revisions[0].basePatchNum = 0;
- const edit = sorted.shift();
- edit.basePatchNum = 0;
- // Edit patchset should be at index 2.
- sorted.splice(2, 0, edit);
- assert.deepEqual(sort(revisions), sorted);
- });
-
- test('getParentIndex', () => {
- assert.equal(Gerrit.PatchSetBehavior.getParentIndex('-13'), 13);
- assert.equal(Gerrit.PatchSetBehavior.getParentIndex(-4), 4);
- });
+<script type="module">
+import '../../test/common-test-setup.js';
+import './gr-patch-set-behavior.js';
+suite('gr-patch-set-behavior tests', () => {
+ test('getRevisionByPatchNum', () => {
+ const get = Gerrit.PatchSetBehavior.getRevisionByPatchNum;
+ const revisions = [
+ {_number: 0},
+ {_number: 1},
+ {_number: 2},
+ ];
+ assert.deepEqual(get(revisions, '1'), revisions[1]);
+ assert.deepEqual(get(revisions, 2), revisions[2]);
+ assert.equal(get(revisions, '3'), undefined);
});
+
+ test('fetchChangeUpdates on latest', done => {
+ const knownChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'NEW',
+ messages: [],
+ };
+ const mockRestApi = {
+ getChangeDetail() {
+ return Promise.resolve(knownChange);
+ },
+ };
+ Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+ .then(result => {
+ assert.isTrue(result.isLatest);
+ assert.isNotOk(result.newStatus);
+ assert.isFalse(result.newMessages);
+ done();
+ });
+ });
+
+ test('fetchChangeUpdates not on latest', done => {
+ const knownChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'NEW',
+ messages: [],
+ };
+ const actualChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ sha3: {description: 'patch 3', _number: 3},
+ },
+ status: 'NEW',
+ messages: [],
+ };
+ const mockRestApi = {
+ getChangeDetail() {
+ return Promise.resolve(actualChange);
+ },
+ };
+ Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+ .then(result => {
+ assert.isFalse(result.isLatest);
+ assert.isNotOk(result.newStatus);
+ assert.isFalse(result.newMessages);
+ done();
+ });
+ });
+
+ test('fetchChangeUpdates new status', done => {
+ const knownChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'NEW',
+ messages: [],
+ };
+ const actualChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'MERGED',
+ messages: [],
+ };
+ const mockRestApi = {
+ getChangeDetail() {
+ return Promise.resolve(actualChange);
+ },
+ };
+ Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+ .then(result => {
+ assert.isTrue(result.isLatest);
+ assert.equal(result.newStatus, 'MERGED');
+ assert.isFalse(result.newMessages);
+ done();
+ });
+ });
+
+ test('fetchChangeUpdates new messages', done => {
+ const knownChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'NEW',
+ messages: [],
+ };
+ const actualChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'NEW',
+ messages: [{message: 'blah blah'}],
+ };
+ const mockRestApi = {
+ getChangeDetail() {
+ return Promise.resolve(actualChange);
+ },
+ };
+ Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+ .then(result => {
+ assert.isTrue(result.isLatest);
+ assert.isNotOk(result.newStatus);
+ assert.isTrue(result.newMessages);
+ done();
+ });
+ });
+
+ test('_computeWipForPatchSets', () => {
+ // Compute patch sets for a given timeline on a change. The initial WIP
+ // property of the change can be true or false. The map of tags by
+ // revision is keyed by patch set number. Each value is a list of change
+ // message tags in the order that they occurred in the timeline. These
+ // indicate actions that modify the WIP property of the change and/or
+ // create new patch sets.
+ //
+ // Returns the actual results with an assertWip method that can be used
+ // to compare against an expected value for a particular patch set.
+ const compute = (initialWip, tagsByRevision) => {
+ const change = {
+ messages: [],
+ work_in_progress: initialWip,
+ };
+ const revs = Object.keys(tagsByRevision).sort((a, b) => a - b);
+ for (const rev of revs) {
+ for (const tag of tagsByRevision[rev]) {
+ change.messages.push({
+ tag,
+ _revision_number: rev,
+ });
+ }
+ }
+ let patchNums = revs.map(rev => { return {num: rev}; });
+ patchNums = Gerrit.PatchSetBehavior._computeWipForPatchSets(
+ change, patchNums);
+ const actualWipsByRevision = {};
+ for (const patchNum of patchNums) {
+ actualWipsByRevision[patchNum.num] = patchNum.wip;
+ }
+ const verifier = {
+ assertWip(revision, expectedWip) {
+ const patchNum = patchNums.find(patchNum => patchNum.num == revision);
+ if (!patchNum) {
+ assert.fail('revision ' + revision + ' not found');
+ }
+ assert.equal(patchNum.wip, expectedWip,
+ 'wip state for ' + revision + ' is ' +
+ patchNum.wip + '; expected ' + expectedWip);
+ return verifier;
+ },
+ };
+ return verifier;
+ };
+
+ compute(false, {1: ['upload']}).assertWip(1, false);
+ compute(true, {1: ['upload']}).assertWip(1, true);
+
+ const setWip = 'autogenerated:gerrit:setWorkInProgress';
+ const uploadInWip = 'autogenerated:gerrit:newWipPatchSet';
+ const clearWip = 'autogenerated:gerrit:setReadyForReview';
+
+ compute(false, {
+ 1: ['upload', setWip],
+ 2: ['upload'],
+ 3: ['upload', clearWip],
+ 4: ['upload', setWip],
+ }).assertWip(1, false) // Change was created with PS1 ready for review
+ .assertWip(2, true) // PS2 was uploaded during WIP
+ .assertWip(3, false) // PS3 was marked ready for review after upload
+ .assertWip(4, false); // PS4 was uploaded ready for review
+
+ compute(false, {
+ 1: [uploadInWip, null, 'addReviewer'],
+ 2: ['upload'],
+ 3: ['upload', clearWip, setWip],
+ 4: ['upload'],
+ 5: ['upload', clearWip],
+ 6: [uploadInWip],
+ }).assertWip(1, true) // Change was created in WIP
+ .assertWip(2, true) // PS2 was uploaded during WIP
+ .assertWip(3, false) // PS3 was marked ready for review
+ .assertWip(4, true) // PS4 was uploaded during WIP
+ .assertWip(5, false) // PS5 was marked ready for review
+ .assertWip(6, true); // PS6 was uploaded with WIP option
+ });
+
+ test('patchNumEquals', () => {
+ const equals = Gerrit.PatchSetBehavior.patchNumEquals;
+ assert.isFalse(equals('edit', 'PARENT'));
+ assert.isFalse(equals('edit', NaN));
+ assert.isFalse(equals(1, '2'));
+
+ assert.isTrue(equals(1, '1'));
+ assert.isTrue(equals(1, 1));
+ assert.isTrue(equals('edit', 'edit'));
+ assert.isTrue(equals('PARENT', 'PARENT'));
+ });
+
+ test('isMergeParent', () => {
+ const isParent = Gerrit.PatchSetBehavior.isMergeParent;
+ assert.isFalse(isParent(1));
+ assert.isFalse(isParent(4321));
+ assert.isFalse(isParent('52'));
+ assert.isFalse(isParent('edit'));
+ assert.isFalse(isParent('PARENT'));
+ assert.isFalse(isParent(0));
+
+ assert.isTrue(isParent(-23));
+ assert.isTrue(isParent(-1));
+ assert.isTrue(isParent('-42'));
+ });
+
+ test('findEditParentRevision', () => {
+ const findParent = Gerrit.PatchSetBehavior.findEditParentRevision;
+ let revisions = [
+ {_number: 0},
+ {_number: 1},
+ {_number: 2},
+ ];
+ assert.strictEqual(findParent(revisions), null);
+
+ revisions = [...revisions, {_number: 'edit', basePatchNum: 3}];
+ assert.strictEqual(findParent(revisions), null);
+
+ revisions = [...revisions, {_number: 3}];
+ assert.deepEqual(findParent(revisions), {_number: 3});
+ });
+
+ test('findEditParentPatchNum', () => {
+ const findNum = Gerrit.PatchSetBehavior.findEditParentPatchNum;
+ let revisions = [
+ {_number: 0},
+ {_number: 1},
+ {_number: 2},
+ ];
+ assert.equal(findNum(revisions), -1);
+
+ revisions =
+ [...revisions, {_number: 'edit', basePatchNum: 3}, {_number: 3}];
+ assert.deepEqual(findNum(revisions), 3);
+ });
+
+ test('sortRevisions', () => {
+ const sort = Gerrit.PatchSetBehavior.sortRevisions;
+ const revisions = [
+ {_number: 0},
+ {_number: 2},
+ {_number: 1},
+ ];
+ const sorted = [
+ {_number: 2},
+ {_number: 1},
+ {_number: 0},
+ ];
+
+ assert.deepEqual(sort(revisions), sorted);
+
+ // Edit patchset should follow directly after its basePatchNum.
+ revisions.push({_number: 'edit', basePatchNum: 2});
+ sorted.unshift({_number: 'edit', basePatchNum: 2});
+ assert.deepEqual(sort(revisions), sorted);
+
+ revisions[0].basePatchNum = 0;
+ const edit = sorted.shift();
+ edit.basePatchNum = 0;
+ // Edit patchset should be at index 2.
+ sorted.splice(2, 0, edit);
+ assert.deepEqual(sort(revisions), sorted);
+ });
+
+ test('getParentIndex', () => {
+ assert.equal(Gerrit.PatchSetBehavior.getParentIndex('-13'), 13);
+ assert.equal(Gerrit.PatchSetBehavior.getParentIndex(-4), 4);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.js
similarity index 84%
rename from polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
rename to polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.js
index 67e4ca6..9ec7c8e 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.js
@@ -1,21 +1,21 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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 '../../scripts/util.js';
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<script src="../../scripts/util.js"></script>
-<script>
(function(window) {
'use strict';
@@ -137,4 +137,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
index 740a8c8..f644469 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
@@ -15,89 +15,86 @@
limitations under the License.
-->
<!-- Polymer included for the html import polyfill. -->
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<title>gr-path-list-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<link rel="import" href="gr-path-list-behavior.html">
-
-<script>
- suite('gr-path-list-behavior tests', async () => {
- await readyToTest();
- test('special sort', () => {
- const sort = Gerrit.PathListBehavior.specialFilePathCompare;
- const testFiles = [
- '/a.h',
- '/MERGE_LIST',
- '/a.cpp',
- '/COMMIT_MSG',
- '/asdasd',
- '/mrPeanutbutter.py',
- ];
- assert.deepEqual(
- testFiles.sort(sort),
- [
- '/COMMIT_MSG',
- '/MERGE_LIST',
- '/a.h',
- '/a.cpp',
- '/asdasd',
- '/mrPeanutbutter.py',
- ]);
- });
-
- test('file display name', () => {
- const name = Gerrit.PathListBehavior.computeDisplayPath;
- assert.equal(name('/foo/bar/baz'), '/foo/bar/baz');
- assert.equal(name('/foobarbaz'), '/foobarbaz');
- assert.equal(name('/COMMIT_MSG'), 'Commit message');
- assert.equal(name('/MERGE_LIST'), 'Merge list');
- });
-
- test('isMagicPath', () => {
- const isMagic = Gerrit.PathListBehavior.isMagicPath;
- assert.isFalse(isMagic(undefined));
- assert.isFalse(isMagic('/foo.cc'));
- assert.isTrue(isMagic('/COMMIT_MSG'));
- assert.isTrue(isMagic('/MERGE_LIST'));
- });
-
- test('truncatePath with long path should add ellipsis', () => {
- const truncatePath = Gerrit.PathListBehavior.truncatePath;
- let path = 'level1/level2/level3/level4/file.js';
- let shortenedPath = truncatePath(path);
- // The expected path is truncated with an ellipsis.
- const expectedPath = '\u2026/file.js';
- assert.equal(shortenedPath, expectedPath);
-
- path = 'level2/file.js';
- shortenedPath = truncatePath(path);
- assert.equal(shortenedPath, expectedPath);
- });
-
- test('truncatePath with opt_threshold', () => {
- const truncatePath = Gerrit.PathListBehavior.truncatePath;
- let path = 'level1/level2/level3/level4/file.js';
- let shortenedPath = truncatePath(path, 2);
- // The expected path is truncated with an ellipsis.
- const expectedPath = '\u2026/level4/file.js';
- assert.equal(shortenedPath, expectedPath);
-
- path = 'level2/file.js';
- shortenedPath = truncatePath(path, 2);
- assert.equal(shortenedPath, path);
- });
-
- test('truncatePath with short path should not add ellipsis', () => {
- const truncatePath = Gerrit.PathListBehavior.truncatePath;
- const path = 'file.js';
- const expectedPath = 'file.js';
- const shortenedPath = truncatePath(path);
- assert.equal(shortenedPath, expectedPath);
- });
+<script type="module">
+import '../../test/common-test-setup.js';
+import './gr-path-list-behavior.js';
+suite('gr-path-list-behavior tests', () => {
+ test('special sort', () => {
+ const sort = Gerrit.PathListBehavior.specialFilePathCompare;
+ const testFiles = [
+ '/a.h',
+ '/MERGE_LIST',
+ '/a.cpp',
+ '/COMMIT_MSG',
+ '/asdasd',
+ '/mrPeanutbutter.py',
+ ];
+ assert.deepEqual(
+ testFiles.sort(sort),
+ [
+ '/COMMIT_MSG',
+ '/MERGE_LIST',
+ '/a.h',
+ '/a.cpp',
+ '/asdasd',
+ '/mrPeanutbutter.py',
+ ]);
});
+
+ test('file display name', () => {
+ const name = Gerrit.PathListBehavior.computeDisplayPath;
+ assert.equal(name('/foo/bar/baz'), '/foo/bar/baz');
+ assert.equal(name('/foobarbaz'), '/foobarbaz');
+ assert.equal(name('/COMMIT_MSG'), 'Commit message');
+ assert.equal(name('/MERGE_LIST'), 'Merge list');
+ });
+
+ test('isMagicPath', () => {
+ const isMagic = Gerrit.PathListBehavior.isMagicPath;
+ assert.isFalse(isMagic(undefined));
+ assert.isFalse(isMagic('/foo.cc'));
+ assert.isTrue(isMagic('/COMMIT_MSG'));
+ assert.isTrue(isMagic('/MERGE_LIST'));
+ });
+
+ test('truncatePath with long path should add ellipsis', () => {
+ const truncatePath = Gerrit.PathListBehavior.truncatePath;
+ let path = 'level1/level2/level3/level4/file.js';
+ let shortenedPath = truncatePath(path);
+ // The expected path is truncated with an ellipsis.
+ const expectedPath = '\u2026/file.js';
+ assert.equal(shortenedPath, expectedPath);
+
+ path = 'level2/file.js';
+ shortenedPath = truncatePath(path);
+ assert.equal(shortenedPath, expectedPath);
+ });
+
+ test('truncatePath with opt_threshold', () => {
+ const truncatePath = Gerrit.PathListBehavior.truncatePath;
+ let path = 'level1/level2/level3/level4/file.js';
+ let shortenedPath = truncatePath(path, 2);
+ // The expected path is truncated with an ellipsis.
+ const expectedPath = '\u2026/level4/file.js';
+ assert.equal(shortenedPath, expectedPath);
+
+ path = 'level2/file.js';
+ shortenedPath = truncatePath(path, 2);
+ assert.equal(shortenedPath, path);
+ });
+
+ test('truncatePath with short path should not add ellipsis', () => {
+ const truncatePath = Gerrit.PathListBehavior.truncatePath;
+ const path = 'file.js';
+ const expectedPath = 'file.js';
+ const shortenedPath = truncatePath(path);
+ assert.equal(shortenedPath, expectedPath);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html b/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.js
similarity index 62%
rename from polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html
rename to polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.js
index 69ebf23..3758b79 100644
--- a/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.js
@@ -1,20 +1,19 @@
-<!--
-@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.
--->
-<script>
+/**
+ * @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.
+ */
(function(window) {
'use strict';
@@ -52,4 +51,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
deleted file mode 100644
index 0e2e99f..0000000
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
+++ /dev/null
@@ -1,21 +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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../elements/shared/gr-tooltip/gr-tooltip.html">
-<script src="../../scripts/rootElement.js"></script>
-
-<script src="gr-tooltip-behavior.js"></script>
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
index b65c63b..481642b 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
@@ -14,6 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../scripts/bundled-polymer.js';
+
+import '../../elements/shared/gr-tooltip/gr-tooltip.js';
+import '../../scripts/rootElement.js';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
(function(window) {
'use strict';
@@ -135,7 +141,7 @@
_positionTooltip(tooltip) {
// This flush is needed for tooltips to be positioned correctly in Firefox
// and Safari.
- Polymer.dom.flush();
+ flush();
const rect = this.getBoundingClientRect();
const boxRect = tooltip.getBoundingClientRect();
const parentRect = tooltip.parentElement.getBoundingClientRect();
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
index e0218ad..efc8e9b 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
@@ -18,15 +18,10 @@
<title>tooltip-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
-<link rel="import" href="gr-tooltip-behavior.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -34,124 +29,126 @@
</template>
</test-fixture>
-<script>
- suite('gr-tooltip-behavior tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../test/common-test-setup.js';
+import './gr-tooltip-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('gr-tooltip-behavior tests', () => {
+ let element;
+ let sandbox;
- function makeTooltip(tooltipRect, parentRect) {
- return {
- getBoundingClientRect() { return tooltipRect; },
- updateStyles: sinon.stub(),
- style: {left: 0, top: 0},
- parentElement: {
- getBoundingClientRect() { return parentRect; },
- },
- };
- }
+ function makeTooltip(tooltipRect, parentRect) {
+ return {
+ getBoundingClientRect() { return tooltipRect; },
+ updateStyles: sinon.stub(),
+ style: {left: 0, top: 0},
+ parentElement: {
+ getBoundingClientRect() { return parentRect; },
+ },
+ };
+ }
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'tooltip-behavior-element',
- behaviors: [Gerrit.TooltipBehavior],
- });
- });
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('normal position', () => {
- sandbox.stub(element, 'getBoundingClientRect', () => {
- return {top: 100, left: 100, width: 200};
- });
- const tooltip = makeTooltip(
- {height: 30, width: 50},
- {top: 0, left: 0, width: 1000});
-
- element._positionTooltip(tooltip);
- assert.isFalse(tooltip.updateStyles.called);
- assert.equal(tooltip.style.left, '175px');
- assert.equal(tooltip.style.top, '100px');
- });
-
- test('left side position', () => {
- sandbox.stub(element, 'getBoundingClientRect', () => {
- return {top: 100, left: 10, width: 50};
- });
- const tooltip = makeTooltip(
- {height: 30, width: 120},
- {top: 0, left: 0, width: 1000});
-
- element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isBelow(parseFloat(offset.replace(/px$/, '')), 0);
- assert.equal(tooltip.style.left, '0px');
- assert.equal(tooltip.style.top, '100px');
- });
-
- test('right side position', () => {
- sandbox.stub(element, 'getBoundingClientRect', () => {
- return {top: 100, left: 950, width: 50};
- });
- const tooltip = makeTooltip(
- {height: 30, width: 120},
- {top: 0, left: 0, width: 1000});
-
- element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
- assert.equal(tooltip.style.left, '915px');
- assert.equal(tooltip.style.top, '100px');
- });
-
- test('position to bottom', () => {
- sandbox.stub(element, 'getBoundingClientRect', () => {
- return {top: 100, left: 950, width: 50, height: 50};
- });
- const tooltip = makeTooltip(
- {height: 30, width: 120},
- {top: 0, left: 0, width: 1000});
-
- element.positionBelow = true;
- element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
- assert.equal(tooltip.style.left, '915px');
- assert.equal(tooltip.style.top, '157.2px');
- });
-
- test('hides tooltip when detached', () => {
- sandbox.stub(element, '_handleHideTooltip');
- element.remove();
- flushAsynchronousOperations();
- assert.isTrue(element._handleHideTooltip.called);
- });
-
- test('sets up listeners when has-tooltip is changed', () => {
- const addListenerStub = sandbox.stub(element, 'addEventListener');
- element.hasTooltip = true;
- assert.isTrue(addListenerStub.called);
- });
-
- test('clean up listeners when has-tooltip changed to false', () => {
- const removeListenerStub = sandbox.stub(element, 'removeEventListener');
- element.hasTooltip = true;
- element.hasTooltip = false;
- assert.isTrue(removeListenerStub.called);
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'tooltip-behavior-element',
+ behaviors: [Gerrit.TooltipBehavior],
});
});
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('normal position', () => {
+ sandbox.stub(element, 'getBoundingClientRect', () => {
+ return {top: 100, left: 100, width: 200};
+ });
+ const tooltip = makeTooltip(
+ {height: 30, width: 50},
+ {top: 0, left: 0, width: 1000});
+
+ element._positionTooltip(tooltip);
+ assert.isFalse(tooltip.updateStyles.called);
+ assert.equal(tooltip.style.left, '175px');
+ assert.equal(tooltip.style.top, '100px');
+ });
+
+ test('left side position', () => {
+ sandbox.stub(element, 'getBoundingClientRect', () => {
+ return {top: 100, left: 10, width: 50};
+ });
+ const tooltip = makeTooltip(
+ {height: 30, width: 120},
+ {top: 0, left: 0, width: 1000});
+
+ element._positionTooltip(tooltip);
+ assert.isTrue(tooltip.updateStyles.called);
+ const offset = tooltip.updateStyles
+ .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
+ assert.isBelow(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.equal(tooltip.style.left, '0px');
+ assert.equal(tooltip.style.top, '100px');
+ });
+
+ test('right side position', () => {
+ sandbox.stub(element, 'getBoundingClientRect', () => {
+ return {top: 100, left: 950, width: 50};
+ });
+ const tooltip = makeTooltip(
+ {height: 30, width: 120},
+ {top: 0, left: 0, width: 1000});
+
+ element._positionTooltip(tooltip);
+ assert.isTrue(tooltip.updateStyles.called);
+ const offset = tooltip.updateStyles
+ .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
+ assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.equal(tooltip.style.left, '915px');
+ assert.equal(tooltip.style.top, '100px');
+ });
+
+ test('position to bottom', () => {
+ sandbox.stub(element, 'getBoundingClientRect', () => {
+ return {top: 100, left: 950, width: 50, height: 50};
+ });
+ const tooltip = makeTooltip(
+ {height: 30, width: 120},
+ {top: 0, left: 0, width: 1000});
+
+ element.positionBelow = true;
+ element._positionTooltip(tooltip);
+ assert.isTrue(tooltip.updateStyles.called);
+ const offset = tooltip.updateStyles
+ .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
+ assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.equal(tooltip.style.left, '915px');
+ assert.equal(tooltip.style.top, '157.2px');
+ });
+
+ test('hides tooltip when detached', () => {
+ sandbox.stub(element, '_handleHideTooltip');
+ element.remove();
+ flushAsynchronousOperations();
+ assert.isTrue(element._handleHideTooltip.called);
+ });
+
+ test('sets up listeners when has-tooltip is changed', () => {
+ const addListenerStub = sandbox.stub(element, 'addEventListener');
+ element.hasTooltip = true;
+ assert.isTrue(addListenerStub.called);
+ });
+
+ test('clean up listeners when has-tooltip changed to false', () => {
+ const removeListenerStub = sandbox.stub(element, 'removeEventListener');
+ element.hasTooltip = true;
+ element.hasTooltip = false;
+ assert.isTrue(removeListenerStub.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js
similarity index 73%
rename from polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html
rename to polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js
index 0396c4f..8b139da 100644
--- a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js
@@ -1,20 +1,19 @@
-<!--
-@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.
--->
-<script>
+/**
+ * @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.
+ */
(function(window) {
'use strict';
@@ -73,4 +72,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
index b7a1d92..6968bbf 100644
--- a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
@@ -18,15 +18,10 @@
<title>gr-url-encoding-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
-<link rel="import" href="gr-url-encoding-behavior.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -34,62 +29,64 @@
</template>
</test-fixture>
-<script>
- suite('gr-url-encoding-behavior tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../test/common-test-setup.js';
+import './gr-url-encoding-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('gr-url-encoding-behavior tests', () => {
+ let element;
+ let sandbox;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [Gerrit.URLEncodingBehavior],
- });
- });
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- suite('encodeURL', () => {
- test('double encodes', () => {
- assert.equal(element.encodeURL('abc?123'), 'abc%253F123');
- assert.equal(element.encodeURL('def/ghi'), 'def%252Fghi');
- assert.equal(element.encodeURL('jkl'), 'jkl');
- assert.equal(element.encodeURL(''), '');
- });
-
- test('does not convert colons', () => {
- assert.equal(element.encodeURL('mno:pqr'), 'mno:pqr');
- });
-
- test('converts spaces to +', () => {
- assert.equal(element.encodeURL('words with spaces'), 'words+with+spaces');
- });
-
- test('does not convert slashes when configured', () => {
- assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
- });
-
- test('does not convert slashes when configured', () => {
- assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
- });
- });
-
- suite('singleDecodeUrl', () => {
- test('single decodes', () => {
- assert.equal(element.singleDecodeURL('abc%3Fdef'), 'abc?def');
- });
-
- test('converts + to space', () => {
- assert.equal(element.singleDecodeURL('ghi+jkl'), 'ghi jkl');
- });
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [Gerrit.URLEncodingBehavior],
});
});
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('encodeURL', () => {
+ test('double encodes', () => {
+ assert.equal(element.encodeURL('abc?123'), 'abc%253F123');
+ assert.equal(element.encodeURL('def/ghi'), 'def%252Fghi');
+ assert.equal(element.encodeURL('jkl'), 'jkl');
+ assert.equal(element.encodeURL(''), '');
+ });
+
+ test('does not convert colons', () => {
+ assert.equal(element.encodeURL('mno:pqr'), 'mno:pqr');
+ });
+
+ test('converts spaces to +', () => {
+ assert.equal(element.encodeURL('words with spaces'), 'words+with+spaces');
+ });
+
+ test('does not convert slashes when configured', () => {
+ assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
+ });
+
+ test('does not convert slashes when configured', () => {
+ assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
+ });
+ });
+
+ suite('singleDecodeUrl', () => {
+ test('single decodes', () => {
+ assert.equal(element.singleDecodeURL('abc%3Fdef'), 'abc?def');
+ });
+
+ test('converts + to space', () => {
+ assert.equal(element.singleDecodeURL('ghi+jkl'), 'ghi jkl');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
similarity index 94%
rename from polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
rename to polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
index a22c3ba..360a85b 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
@@ -1,21 +1,20 @@
-<!--
-@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.
--->
-
-<!--
+/**
+ * @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.
+ */
+/*
How to Add a Keyboard Shortcut
==============================
@@ -95,12 +94,17 @@
NOTE: doc-only shortcuts will not be customizable in the same way that other
shortcuts are.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
-<script src="../../types/polymer-behaviors.js"></script>
+*/
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+import '../../scripts/bundled-polymer.js';
-<script>
+import {IronA11yKeysBehavior} from '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
+import '../../types/polymer-behaviors.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
(function(window) {
'use strict';
@@ -307,7 +311,7 @@
/** @return {!(Event|PolymerDomApi|PolymerEventApi)} */
const getKeyboardEvent = function(e) {
- e = Polymer.dom(e.detail ? e.detail.keyboardEvent : e);
+ e = dom(e.detail ? e.detail.keyboardEvent : e);
// When e is a keyboardEvent, e.event is not null.
if (e.event) { e = e.event; }
return e;
@@ -482,7 +486,7 @@
/** @polymerBehavior Gerrit.KeyboardShortcutBehavior*/
Gerrit.KeyboardShortcutBehavior = [
- Polymer.IronA11yKeysBehavior,
+ IronA11yKeysBehavior,
{
// Exports for convenience. Note: Closure compiler crashes when
// object-shorthand syntax is used here.
@@ -518,7 +522,7 @@
shouldSuppressKeyboardShortcut(e) {
e = getKeyboardEvent(e);
- const tagName = Polymer.dom(e).rootTarget.tagName;
+ const tagName = dom(e).rootTarget.tagName;
if (tagName === 'INPUT' || tagName === 'TEXTAREA' ||
(e.keyCode === 13 && tagName === 'A')) {
// Suppress shortcuts if the key is 'enter' and target is an anchor.
@@ -542,7 +546,7 @@
},
getRootTarget(e) {
- return Polymer.dom(getKeyboardEvent(e)).rootTarget;
+ return dom(getKeyboardEvent(e)).rootTarget;
},
bindShortcut(shortcut, ...bindings) {
@@ -671,4 +675,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
index e7407f7..ac979b2 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
@@ -19,14 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
-<link rel="import" href="keyboard-shortcut-behavior.html">
-
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
<test-element></test-element>
@@ -41,403 +37,405 @@
</template>
</test-fixture>
-<script>
- suite('keyboard-shortcut-behavior tests', async () => {
- await readyToTest();
- const kb = window.Gerrit.KeyboardShortcutBinder;
+<script type="module">
+import '../../test/common-test-setup.js';
+import './keyboard-shortcut-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('keyboard-shortcut-behavior tests', () => {
+ const kb = window.Gerrit.KeyboardShortcutBinder;
- let element;
- let overlay;
- let sandbox;
+ let element;
+ let overlay;
+ let sandbox;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [Gerrit.KeyboardShortcutBehavior],
- keyBindings: {
- k: '_handleKey',
- enter: '_handleKey',
- },
- _handleKey() {},
- });
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [Gerrit.KeyboardShortcutBehavior],
+ keyBindings: {
+ k: '_handleKey',
+ enter: '_handleKey',
+ },
+ _handleKey() {},
+ });
+ });
+
+ setup(() => {
+ element = fixture('basic');
+ overlay = fixture('within-overlay');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('ShortcutManager', () => {
+ test('bindings management', () => {
+ const mgr = new kb.ShortcutManager();
+ const {NEXT_FILE} = kb.Shortcut;
+
+ assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
+ mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
+ assert.deepEqual(
+ mgr.getBindingsForShortcut(NEXT_FILE),
+ [']', '}', 'right']);
});
- setup(() => {
- element = fixture('basic');
- overlay = fixture('within-overlay');
- sandbox = sinon.sandbox.create();
- });
+ suite('binding descriptions', () => {
+ function mapToObject(m) {
+ const o = {};
+ m.forEach((v, k) => o[k] = v);
+ return o;
+ }
- teardown(() => {
- sandbox.restore();
- });
-
- suite('ShortcutManager', () => {
- test('bindings management', () => {
+ test('single combo description', () => {
const mgr = new kb.ShortcutManager();
- const {NEXT_FILE} = kb.Shortcut;
-
- assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
- mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
+ assert.deepEqual(mgr.describeBinding('a'), ['a']);
+ assert.deepEqual(mgr.describeBinding('a:keyup'), ['a']);
+ assert.deepEqual(mgr.describeBinding('ctrl+a'), ['Ctrl', 'a']);
assert.deepEqual(
- mgr.getBindingsForShortcut(NEXT_FILE),
- [']', '}', 'right']);
+ mgr.describeBinding('ctrl+shift+up:keyup'),
+ ['Ctrl', 'Shift', '↑']);
});
- suite('binding descriptions', () => {
- function mapToObject(m) {
- const o = {};
- m.forEach((v, k) => o[k] = v);
- return o;
- }
+ test('combo set description', () => {
+ const {GO_KEY, DOC_ONLY, ShortcutManager} = kb;
+ const {GO_TO_OPENED_CHANGES, NEXT_FILE, PREV_FILE} = kb.Shortcut;
- test('single combo description', () => {
- const mgr = new kb.ShortcutManager();
- assert.deepEqual(mgr.describeBinding('a'), ['a']);
- assert.deepEqual(mgr.describeBinding('a:keyup'), ['a']);
- assert.deepEqual(mgr.describeBinding('ctrl+a'), ['Ctrl', 'a']);
- assert.deepEqual(
- mgr.describeBinding('ctrl+shift+up:keyup'),
- ['Ctrl', 'Shift', '↑']);
+ const mgr = new ShortcutManager();
+ assert.isNull(mgr.describeBindings(NEXT_FILE));
+
+ mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
+ assert.deepEqual(
+ mgr.describeBindings(GO_TO_OPENED_CHANGES),
+ [['g', 'o']]);
+
+ mgr.bindShortcut(NEXT_FILE, DOC_ONLY, ']', 'ctrl+shift+right:keyup');
+ assert.deepEqual(
+ mgr.describeBindings(NEXT_FILE),
+ [[']'], ['Ctrl', 'Shift', '→']]);
+
+ mgr.bindShortcut(PREV_FILE, '[');
+ assert.deepEqual(mgr.describeBindings(PREV_FILE), [['[']]);
+ });
+
+ test('combo set description width', () => {
+ const mgr = new kb.ShortcutManager();
+ assert.strictEqual(mgr.comboSetDisplayWidth([['u']]), 1);
+ assert.strictEqual(mgr.comboSetDisplayWidth([['g', 'o']]), 2);
+ assert.strictEqual(mgr.comboSetDisplayWidth([['Shift', 'r']]), 6);
+ assert.strictEqual(mgr.comboSetDisplayWidth([['x'], ['y']]), 4);
+ assert.strictEqual(
+ mgr.comboSetDisplayWidth([['x'], ['y'], ['Shift', 'z']]),
+ 12);
+ });
+
+ test('distribute shortcut help', () => {
+ const mgr = new kb.ShortcutManager();
+ assert.deepEqual(mgr.distributeBindingDesc([['o']]), [[['o']]]);
+ assert.deepEqual(
+ mgr.distributeBindingDesc([['g', 'o']]),
+ [[['g', 'o']]]);
+ assert.deepEqual(
+ mgr.distributeBindingDesc([['ctrl', 'shift', 'meta', 'enter']]),
+ [[['ctrl', 'shift', 'meta', 'enter']]]);
+ assert.deepEqual(
+ mgr.distributeBindingDesc([
+ ['ctrl', 'shift', 'meta', 'enter'],
+ ['o'],
+ ]),
+ [
+ [['ctrl', 'shift', 'meta', 'enter']],
+ [['o']],
+ ]);
+ assert.deepEqual(
+ mgr.distributeBindingDesc([
+ ['ctrl', 'enter'],
+ ['meta', 'enter'],
+ ['ctrl', 's'],
+ ['meta', 's'],
+ ]),
+ [
+ [['ctrl', 'enter'], ['meta', 'enter']],
+ [['ctrl', 's'], ['meta', 's']],
+ ]);
+ });
+
+ test('active shortcuts by section', () => {
+ const {NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH} =
+ kb.Shortcut;
+ const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
+
+ const mgr = new kb.ShortcutManager();
+ mgr.bindShortcut(NEXT_FILE, ']');
+ mgr.bindShortcut(NEXT_LINE, 'j');
+ mgr.bindShortcut(GO_TO_OPENED_CHANGES, 'g+o');
+ mgr.bindShortcut(SEARCH, '/');
+
+ assert.deepEqual(
+ mapToObject(mgr.activeShortcutsBySection()),
+ {});
+
+ mgr.attachHost({
+ keyboardShortcuts() {
+ return {
+ [NEXT_FILE]: null,
+ };
+ },
});
+ assert.deepEqual(
+ mapToObject(mgr.activeShortcutsBySection()),
+ {
+ [NAVIGATION]: [
+ {shortcut: NEXT_FILE, text: 'Go to next file'},
+ ],
+ });
- test('combo set description', () => {
- const {GO_KEY, DOC_ONLY, ShortcutManager} = kb;
- const {GO_TO_OPENED_CHANGES, NEXT_FILE, PREV_FILE} = kb.Shortcut;
-
- const mgr = new ShortcutManager();
- assert.isNull(mgr.describeBindings(NEXT_FILE));
-
- mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
- assert.deepEqual(
- mgr.describeBindings(GO_TO_OPENED_CHANGES),
- [['g', 'o']]);
-
- mgr.bindShortcut(NEXT_FILE, DOC_ONLY, ']', 'ctrl+shift+right:keyup');
- assert.deepEqual(
- mgr.describeBindings(NEXT_FILE),
- [[']'], ['Ctrl', 'Shift', '→']]);
-
- mgr.bindShortcut(PREV_FILE, '[');
- assert.deepEqual(mgr.describeBindings(PREV_FILE), [['[']]);
+ mgr.attachHost({
+ keyboardShortcuts() {
+ return {
+ [NEXT_LINE]: null,
+ };
+ },
});
+ assert.deepEqual(
+ mapToObject(mgr.activeShortcutsBySection()),
+ {
+ [DIFFS]: [
+ {shortcut: NEXT_LINE, text: 'Go to next line'},
+ ],
+ [NAVIGATION]: [
+ {shortcut: NEXT_FILE, text: 'Go to next file'},
+ ],
+ });
- test('combo set description width', () => {
- const mgr = new kb.ShortcutManager();
- assert.strictEqual(mgr.comboSetDisplayWidth([['u']]), 1);
- assert.strictEqual(mgr.comboSetDisplayWidth([['g', 'o']]), 2);
- assert.strictEqual(mgr.comboSetDisplayWidth([['Shift', 'r']]), 6);
- assert.strictEqual(mgr.comboSetDisplayWidth([['x'], ['y']]), 4);
- assert.strictEqual(
- mgr.comboSetDisplayWidth([['x'], ['y'], ['Shift', 'z']]),
- 12);
+ mgr.attachHost({
+ keyboardShortcuts() {
+ return {
+ [SEARCH]: null,
+ [GO_TO_OPENED_CHANGES]: null,
+ };
+ },
});
+ assert.deepEqual(
+ mapToObject(mgr.activeShortcutsBySection()),
+ {
+ [DIFFS]: [
+ {shortcut: NEXT_LINE, text: 'Go to next line'},
+ ],
+ [EVERYWHERE]: [
+ {shortcut: SEARCH, text: 'Search'},
+ {
+ shortcut: GO_TO_OPENED_CHANGES,
+ text: 'Go to Opened Changes',
+ },
+ ],
+ [NAVIGATION]: [
+ {shortcut: NEXT_FILE, text: 'Go to next file'},
+ ],
+ });
+ });
- test('distribute shortcut help', () => {
- const mgr = new kb.ShortcutManager();
- assert.deepEqual(mgr.distributeBindingDesc([['o']]), [[['o']]]);
- assert.deepEqual(
- mgr.distributeBindingDesc([['g', 'o']]),
- [[['g', 'o']]]);
- assert.deepEqual(
- mgr.distributeBindingDesc([['ctrl', 'shift', 'meta', 'enter']]),
- [[['ctrl', 'shift', 'meta', 'enter']]]);
- assert.deepEqual(
- mgr.distributeBindingDesc([
- ['ctrl', 'shift', 'meta', 'enter'],
- ['o'],
- ]),
- [
- [['ctrl', 'shift', 'meta', 'enter']],
- [['o']],
- ]);
- assert.deepEqual(
- mgr.distributeBindingDesc([
- ['ctrl', 'enter'],
- ['meta', 'enter'],
- ['ctrl', 's'],
- ['meta', 's'],
- ]),
- [
- [['ctrl', 'enter'], ['meta', 'enter']],
- [['ctrl', 's'], ['meta', 's']],
- ]);
+ test('directory view', () => {
+ const {
+ NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH,
+ SAVE_COMMENT,
+ } = kb.Shortcut;
+ const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
+ const {GO_KEY, ShortcutManager} = kb;
+
+ const mgr = new ShortcutManager();
+ mgr.bindShortcut(NEXT_FILE, ']');
+ mgr.bindShortcut(NEXT_LINE, 'j');
+ mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
+ mgr.bindShortcut(SEARCH, '/');
+ mgr.bindShortcut(
+ SAVE_COMMENT, 'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
+
+ assert.deepEqual(mapToObject(mgr.directoryView()), {});
+
+ mgr.attachHost({
+ keyboardShortcuts() {
+ return {
+ [GO_TO_OPENED_CHANGES]: null,
+ [NEXT_FILE]: null,
+ [NEXT_LINE]: null,
+ [SAVE_COMMENT]: null,
+ [SEARCH]: null,
+ };
+ },
});
-
- test('active shortcuts by section', () => {
- const {NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH} =
- kb.Shortcut;
- const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
-
- const mgr = new kb.ShortcutManager();
- mgr.bindShortcut(NEXT_FILE, ']');
- mgr.bindShortcut(NEXT_LINE, 'j');
- mgr.bindShortcut(GO_TO_OPENED_CHANGES, 'g+o');
- mgr.bindShortcut(SEARCH, '/');
-
- assert.deepEqual(
- mapToObject(mgr.activeShortcutsBySection()),
- {});
-
- mgr.attachHost({
- keyboardShortcuts() {
- return {
- [NEXT_FILE]: null,
- };
- },
- });
- assert.deepEqual(
- mapToObject(mgr.activeShortcutsBySection()),
- {
- [NAVIGATION]: [
- {shortcut: NEXT_FILE, text: 'Go to next file'},
- ],
- });
-
- mgr.attachHost({
- keyboardShortcuts() {
- return {
- [NEXT_LINE]: null,
- };
- },
- });
- assert.deepEqual(
- mapToObject(mgr.activeShortcutsBySection()),
- {
- [DIFFS]: [
- {shortcut: NEXT_LINE, text: 'Go to next line'},
- ],
- [NAVIGATION]: [
- {shortcut: NEXT_FILE, text: 'Go to next file'},
- ],
- });
-
- mgr.attachHost({
- keyboardShortcuts() {
- return {
- [SEARCH]: null,
- [GO_TO_OPENED_CHANGES]: null,
- };
- },
- });
- assert.deepEqual(
- mapToObject(mgr.activeShortcutsBySection()),
- {
- [DIFFS]: [
- {shortcut: NEXT_LINE, text: 'Go to next line'},
- ],
- [EVERYWHERE]: [
- {shortcut: SEARCH, text: 'Search'},
- {
- shortcut: GO_TO_OPENED_CHANGES,
- text: 'Go to Opened Changes',
- },
- ],
- [NAVIGATION]: [
- {shortcut: NEXT_FILE, text: 'Go to next file'},
- ],
- });
- });
-
- test('directory view', () => {
- const {
- NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH,
- SAVE_COMMENT,
- } = kb.Shortcut;
- const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
- const {GO_KEY, ShortcutManager} = kb;
-
- const mgr = new ShortcutManager();
- mgr.bindShortcut(NEXT_FILE, ']');
- mgr.bindShortcut(NEXT_LINE, 'j');
- mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
- mgr.bindShortcut(SEARCH, '/');
- mgr.bindShortcut(
- SAVE_COMMENT, 'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
-
- assert.deepEqual(mapToObject(mgr.directoryView()), {});
-
- mgr.attachHost({
- keyboardShortcuts() {
- return {
- [GO_TO_OPENED_CHANGES]: null,
- [NEXT_FILE]: null,
- [NEXT_LINE]: null,
- [SAVE_COMMENT]: null,
- [SEARCH]: null,
- };
- },
- });
- assert.deepEqual(
- mapToObject(mgr.directoryView()),
- {
- [DIFFS]: [
- {binding: [['j']], text: 'Go to next line'},
- {
- binding: [['Ctrl', 'Enter'], ['Meta', 'Enter']],
- text: 'Save comment',
- },
- {
- binding: [['Ctrl', 's'], ['Meta', 's']],
- text: 'Save comment',
- },
- ],
- [EVERYWHERE]: [
- {binding: [['/']], text: 'Search'},
- {binding: [['g', 'o']], text: 'Go to Opened Changes'},
- ],
- [NAVIGATION]: [
- {binding: [[']']], text: 'Go to next file'},
- ],
- });
- });
- });
- });
-
- test('doesn’t block kb shortcuts for non-whitelisted els', done => {
- const divEl = document.createElement('div');
- element.appendChild(divEl);
- element._handleKey = e => {
- assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
- done();
- };
- MockInteractions.keyDownOn(divEl, 75, null, 'k');
- });
-
- test('blocks kb shortcuts for input els', done => {
- const inputEl = document.createElement('input');
- element.appendChild(inputEl);
- element._handleKey = e => {
- assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
- done();
- };
- MockInteractions.keyDownOn(inputEl, 75, null, 'k');
- });
-
- test('blocks kb shortcuts for textarea els', done => {
- const textareaEl = document.createElement('textarea');
- element.appendChild(textareaEl);
- element._handleKey = e => {
- assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
- done();
- };
- MockInteractions.keyDownOn(textareaEl, 75, null, 'k');
- });
-
- test('blocks kb shortcuts for anything in a gr-overlay', done => {
- const divEl = document.createElement('div');
- const element = overlay.querySelector('test-element');
- element.appendChild(divEl);
- element._handleKey = e => {
- assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
- done();
- };
- MockInteractions.keyDownOn(divEl, 75, null, 'k');
- });
-
- test('blocks enter shortcut on an anchor', done => {
- const anchorEl = document.createElement('a');
- const element = overlay.querySelector('test-element');
- element.appendChild(anchorEl);
- element._handleKey = e => {
- assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
- done();
- };
- MockInteractions.keyDownOn(anchorEl, 13, null, 'enter');
- });
-
- test('modifierPressed returns accurate values', () => {
- const spy = sandbox.spy(element, 'modifierPressed');
- element._handleKey = e => {
- element.modifierPressed(e);
- };
- MockInteractions.keyDownOn(element, 75, 'shift', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'meta', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'alt', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- });
-
- test('isModifierPressed returns accurate value', () => {
- const spy = sandbox.spy(element, 'isModifierPressed');
- element._handleKey = e => {
- element.isModifierPressed(e, 'shiftKey');
- };
- MockInteractions.keyDownOn(element, 75, 'shift', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'meta', 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'alt', 'k');
- assert.isFalse(spy.lastCall.returnValue);
- });
-
- suite('GO_KEY timing', () => {
- let handlerStub;
-
- setup(() => {
- element._shortcut_go_table.set('a', '_handleA');
- handlerStub = element._handleA = sinon.stub();
- sandbox.stub(Date, 'now').returns(10000);
- });
-
- test('success', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._shortcut_go_key_last_pressed = 9000;
- element._handleGoAction(e);
- assert.isTrue(handlerStub.calledOnce);
- assert.strictEqual(handlerStub.lastCall.args[0], e);
- });
-
- test('go key not pressed', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._shortcut_go_key_last_pressed = null;
- element._handleGoAction(e);
- assert.isFalse(handlerStub.called);
- });
-
- test('go key pressed too long ago', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._shortcut_go_key_last_pressed = 3000;
- element._handleGoAction(e);
- assert.isFalse(handlerStub.called);
- });
-
- test('should suppress', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(true);
- element._shortcut_go_key_last_pressed = 9000;
- element._handleGoAction(e);
- assert.isFalse(handlerStub.called);
- });
-
- test('unrecognized key', () => {
- const e = {detail: {key: 'f'}, preventDefault: () => {}};
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._shortcut_go_key_last_pressed = 9000;
- element._handleGoAction(e);
- assert.isFalse(handlerStub.called);
+ assert.deepEqual(
+ mapToObject(mgr.directoryView()),
+ {
+ [DIFFS]: [
+ {binding: [['j']], text: 'Go to next line'},
+ {
+ binding: [['Ctrl', 'Enter'], ['Meta', 'Enter']],
+ text: 'Save comment',
+ },
+ {
+ binding: [['Ctrl', 's'], ['Meta', 's']],
+ text: 'Save comment',
+ },
+ ],
+ [EVERYWHERE]: [
+ {binding: [['/']], text: 'Search'},
+ {binding: [['g', 'o']], text: 'Go to Opened Changes'},
+ ],
+ [NAVIGATION]: [
+ {binding: [[']']], text: 'Go to next file'},
+ ],
+ });
});
});
});
+
+ test('doesn’t block kb shortcuts for non-whitelisted els', done => {
+ const divEl = document.createElement('div');
+ element.appendChild(divEl);
+ element._handleKey = e => {
+ assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
+ done();
+ };
+ MockInteractions.keyDownOn(divEl, 75, null, 'k');
+ });
+
+ test('blocks kb shortcuts for input els', done => {
+ const inputEl = document.createElement('input');
+ element.appendChild(inputEl);
+ element._handleKey = e => {
+ assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
+ done();
+ };
+ MockInteractions.keyDownOn(inputEl, 75, null, 'k');
+ });
+
+ test('blocks kb shortcuts for textarea els', done => {
+ const textareaEl = document.createElement('textarea');
+ element.appendChild(textareaEl);
+ element._handleKey = e => {
+ assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
+ done();
+ };
+ MockInteractions.keyDownOn(textareaEl, 75, null, 'k');
+ });
+
+ test('blocks kb shortcuts for anything in a gr-overlay', done => {
+ const divEl = document.createElement('div');
+ const element = overlay.querySelector('test-element');
+ element.appendChild(divEl);
+ element._handleKey = e => {
+ assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
+ done();
+ };
+ MockInteractions.keyDownOn(divEl, 75, null, 'k');
+ });
+
+ test('blocks enter shortcut on an anchor', done => {
+ const anchorEl = document.createElement('a');
+ const element = overlay.querySelector('test-element');
+ element.appendChild(anchorEl);
+ element._handleKey = e => {
+ assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
+ done();
+ };
+ MockInteractions.keyDownOn(anchorEl, 13, null, 'enter');
+ });
+
+ test('modifierPressed returns accurate values', () => {
+ const spy = sandbox.spy(element, 'modifierPressed');
+ element._handleKey = e => {
+ element.modifierPressed(e);
+ };
+ MockInteractions.keyDownOn(element, 75, 'shift', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'meta', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'alt', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ });
+
+ test('isModifierPressed returns accurate value', () => {
+ const spy = sandbox.spy(element, 'isModifierPressed');
+ element._handleKey = e => {
+ element.isModifierPressed(e, 'shiftKey');
+ };
+ MockInteractions.keyDownOn(element, 75, 'shift', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'meta', 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'alt', 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ });
+
+ suite('GO_KEY timing', () => {
+ let handlerStub;
+
+ setup(() => {
+ element._shortcut_go_table.set('a', '_handleA');
+ handlerStub = element._handleA = sinon.stub();
+ sandbox.stub(Date, 'now').returns(10000);
+ });
+
+ test('success', () => {
+ const e = {detail: {key: 'a'}, preventDefault: () => {}};
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ element._shortcut_go_key_last_pressed = 9000;
+ element._handleGoAction(e);
+ assert.isTrue(handlerStub.calledOnce);
+ assert.strictEqual(handlerStub.lastCall.args[0], e);
+ });
+
+ test('go key not pressed', () => {
+ const e = {detail: {key: 'a'}, preventDefault: () => {}};
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ element._shortcut_go_key_last_pressed = null;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+
+ test('go key pressed too long ago', () => {
+ const e = {detail: {key: 'a'}, preventDefault: () => {}};
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ element._shortcut_go_key_last_pressed = 3000;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+
+ test('should suppress', () => {
+ const e = {detail: {key: 'a'}, preventDefault: () => {}};
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(true);
+ element._shortcut_go_key_last_pressed = 9000;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+
+ test('unrecognized key', () => {
+ const e = {detail: {key: 'f'}, preventDefault: () => {}};
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ element._shortcut_go_key_last_pressed = 9000;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js
similarity index 86%
rename from polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
rename to polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js
index 709cc8a..d52ffbf 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js
@@ -1,22 +1,22 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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 '../../scripts/bundled-polymer.js';
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../base-url-behavior/base-url-behavior.html">
-<script>
+import '../base-url-behavior/base-url-behavior.js';
(function(window) {
'use strict';
@@ -198,4 +198,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
index 4bcd928..aa24ffc 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
@@ -19,20 +19,18 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
-<script>
- /** @type {string} */
- window.CANONICAL_PATH = '/r';
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module">
+import '../../test/common-test-setup.js';
+import '../base-url-behavior/base-url-behavior.js';
+import './rest-client-behavior.js';
+/** @type {string} */
+window.CANONICAL_PATH = '/r';
</script>
-<link rel="import" href="../base-url-behavior/base-url-behavior.html">
-<link rel="import" href="rest-client-behavior.html">
-
<test-fixture id="basic">
<template>
<test-element></test-element>
@@ -47,191 +45,194 @@
</template>
</test-fixture>
-<script>
- suite('rest-client-behavior tests', async () => {
- await readyToTest();
- let element;
- // eslint-disable-next-line no-unused-vars
- let overlay;
+<script type="module">
+import '../../test/common-test-setup.js';
+import '../base-url-behavior/base-url-behavior.js';
+import './rest-client-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('rest-client-behavior tests', () => {
+ let element;
+ // eslint-disable-next-line no-unused-vars
+ let overlay;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [
- Gerrit.BaseUrlBehavior,
- Gerrit.RESTClientBehavior,
- ],
- });
- });
-
- setup(() => {
- element = fixture('basic');
- overlay = fixture('within-overlay');
- });
-
- test('changeBaseURL', () => {
- assert.deepEqual(
- element.changeBaseURL('test/project', '1', '2'),
- '/r/changes/test%2Fproject~1/revisions/2'
- );
- });
-
- test('changePath', () => {
- assert.deepEqual(element.changePath('1'), '/r/c/1');
- });
-
- test('Open status', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- mergeable: true,
- };
- let statuses = element.changeStatuses(change);
- const statusString = element.changeStatusString(change);
- assert.deepEqual(statuses, []);
- assert.equal(statusString, '');
-
- change.submittable = false;
- statuses = element.changeStatuses(change,
- {includeDerived: true});
- assert.deepEqual(statuses, ['Active']);
-
- // With no missing labels but no submitEnabled option.
- change.submittable = true;
- statuses = element.changeStatuses(change,
- {includeDerived: true});
- assert.deepEqual(statuses, ['Active']);
-
- // Without missing labels and enabled submit
- statuses = element.changeStatuses(change,
- {includeDerived: true, submitEnabled: true});
- assert.deepEqual(statuses, ['Ready to submit']);
-
- change.mergeable = false;
- change.submittable = true;
- statuses = element.changeStatuses(change,
- {includeDerived: true});
- assert.deepEqual(statuses, ['Merge Conflict']);
-
- delete change.mergeable;
- change.submittable = true;
- statuses = element.changeStatuses(change,
- {includeDerived: true, mergeable: true, submitEnabled: true});
- assert.deepEqual(statuses, ['Ready to submit']);
-
- change.submittable = true;
- statuses = element.changeStatuses(change,
- {includeDerived: true, mergeable: false});
- assert.deepEqual(statuses, ['Merge Conflict']);
- });
-
- test('Merge conflict', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- mergeable: false,
- };
- const statuses = element.changeStatuses(change);
- const statusString = element.changeStatusString(change);
- assert.deepEqual(statuses, ['Merge Conflict']);
- assert.equal(statusString, 'Merge Conflict');
- });
-
- test('mergeable prop undefined', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- };
- const statuses = element.changeStatuses(change);
- const statusString = element.changeStatusString(change);
- assert.deepEqual(statuses, []);
- assert.equal(statusString, '');
- });
-
- test('Merged status', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'MERGED',
- labels: {},
- };
- const statuses = element.changeStatuses(change);
- const statusString = element.changeStatusString(change);
- assert.deepEqual(statuses, ['Merged']);
- assert.equal(statusString, 'Merged');
- });
-
- test('Abandoned status', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'ABANDONED',
- labels: {},
- };
- const statuses = element.changeStatuses(change);
- const statusString = element.changeStatusString(change);
- assert.deepEqual(statuses, ['Abandoned']);
- assert.equal(statusString, 'Abandoned');
- });
-
- test('Open status with private and wip', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'NEW',
- is_private: true,
- work_in_progress: true,
- labels: {},
- mergeable: true,
- };
- const statuses = element.changeStatuses(change);
- const statusString = element.changeStatusString(change);
- assert.deepEqual(statuses, ['WIP', 'Private']);
- assert.equal(statusString, 'WIP, Private');
- });
-
- test('Merge conflict with private and wip', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'NEW',
- is_private: true,
- work_in_progress: true,
- labels: {},
- mergeable: false,
- };
- const statuses = element.changeStatuses(change);
- const statusString = element.changeStatusString(change);
- assert.deepEqual(statuses, ['Merge Conflict', 'WIP', 'Private']);
- assert.equal(statusString, 'Merge Conflict, WIP, Private');
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.RESTClientBehavior,
+ ],
});
});
+
+ setup(() => {
+ element = fixture('basic');
+ overlay = fixture('within-overlay');
+ });
+
+ test('changeBaseURL', () => {
+ assert.deepEqual(
+ element.changeBaseURL('test/project', '1', '2'),
+ '/r/changes/test%2Fproject~1/revisions/2'
+ );
+ });
+
+ test('changePath', () => {
+ assert.deepEqual(element.changePath('1'), '/r/c/1');
+ });
+
+ test('Open status', () => {
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ mergeable: true,
+ };
+ let statuses = element.changeStatuses(change);
+ const statusString = element.changeStatusString(change);
+ assert.deepEqual(statuses, []);
+ assert.equal(statusString, '');
+
+ change.submittable = false;
+ statuses = element.changeStatuses(change,
+ {includeDerived: true});
+ assert.deepEqual(statuses, ['Active']);
+
+ // With no missing labels but no submitEnabled option.
+ change.submittable = true;
+ statuses = element.changeStatuses(change,
+ {includeDerived: true});
+ assert.deepEqual(statuses, ['Active']);
+
+ // Without missing labels and enabled submit
+ statuses = element.changeStatuses(change,
+ {includeDerived: true, submitEnabled: true});
+ assert.deepEqual(statuses, ['Ready to submit']);
+
+ change.mergeable = false;
+ change.submittable = true;
+ statuses = element.changeStatuses(change,
+ {includeDerived: true});
+ assert.deepEqual(statuses, ['Merge Conflict']);
+
+ delete change.mergeable;
+ change.submittable = true;
+ statuses = element.changeStatuses(change,
+ {includeDerived: true, mergeable: true, submitEnabled: true});
+ assert.deepEqual(statuses, ['Ready to submit']);
+
+ change.submittable = true;
+ statuses = element.changeStatuses(change,
+ {includeDerived: true, mergeable: false});
+ assert.deepEqual(statuses, ['Merge Conflict']);
+ });
+
+ test('Merge conflict', () => {
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ mergeable: false,
+ };
+ const statuses = element.changeStatuses(change);
+ const statusString = element.changeStatusString(change);
+ assert.deepEqual(statuses, ['Merge Conflict']);
+ assert.equal(statusString, 'Merge Conflict');
+ });
+
+ test('mergeable prop undefined', () => {
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ };
+ const statuses = element.changeStatuses(change);
+ const statusString = element.changeStatusString(change);
+ assert.deepEqual(statuses, []);
+ assert.equal(statusString, '');
+ });
+
+ test('Merged status', () => {
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: 'MERGED',
+ labels: {},
+ };
+ const statuses = element.changeStatuses(change);
+ const statusString = element.changeStatusString(change);
+ assert.deepEqual(statuses, ['Merged']);
+ assert.equal(statusString, 'Merged');
+ });
+
+ test('Abandoned status', () => {
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: 'ABANDONED',
+ labels: {},
+ };
+ const statuses = element.changeStatuses(change);
+ const statusString = element.changeStatusString(change);
+ assert.deepEqual(statuses, ['Abandoned']);
+ assert.equal(statusString, 'Abandoned');
+ });
+
+ test('Open status with private and wip', () => {
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ is_private: true,
+ work_in_progress: true,
+ labels: {},
+ mergeable: true,
+ };
+ const statuses = element.changeStatuses(change);
+ const statusString = element.changeStatusString(change);
+ assert.deepEqual(statuses, ['WIP', 'Private']);
+ assert.equal(statusString, 'WIP, Private');
+ });
+
+ test('Merge conflict with private and wip', () => {
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ is_private: true,
+ work_in_progress: true,
+ labels: {},
+ mergeable: false,
+ };
+ const statuses = element.changeStatuses(change);
+ const statusString = element.changeStatusString(change);
+ assert.deepEqual(statuses, ['Merge Conflict', 'WIP', 'Private']);
+ assert.equal(statusString, 'Merge Conflict, WIP, Private');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.html b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.js
similarity index 72%
rename from polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.html
rename to polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.js
index 8f08f0c..23f2290 100644
--- a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.html
+++ b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.js
@@ -1,20 +1,19 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<script>
+/**
+ * @license
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
(function(window) {
'use strict';
@@ -74,4 +73,3 @@
throw new Error(`Refused to bind value as ${type}: ${value}`);
};
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
index e123c96..791dc4d 100644
--- a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
@@ -18,15 +18,10 @@
<title>safe-types-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
-<link rel="import" href="safe-types-behavior.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -34,92 +29,94 @@
</template>
</test-fixture>
-<script>
- suite('gr-tooltip-behavior tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../test/common-test-setup.js';
+import './safe-types-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('gr-tooltip-behavior tests', () => {
+ let element;
+ let sandbox;
- suiteSetup(() => {
- Polymer({
- is: 'safe-types-element',
- behaviors: [Gerrit.SafeTypes],
- });
- });
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('SafeUrl accepts valid urls', () => {
- function accepts(url) {
- const safeUrl = new element.SafeUrl(url);
- assert.isOk(safeUrl);
- assert.equal(url, safeUrl.asString());
- }
- accepts('http://www.google.com/');
- accepts('https://www.google.com/');
- accepts('HtTpS://www.google.com/');
- accepts('//www.google.com/');
- accepts('/c/1234/file/path.html@45');
- accepts('#hash-url');
- accepts('mailto:name@example.com');
- });
-
- test('SafeUrl rejects invalid urls', () => {
- function rejects(url) {
- assert.throws(() => { new element.SafeUrl(url); });
- }
- rejects('javascript://alert("evil");');
- rejects('ftp:example.com');
- rejects('data:text/html,scary business');
- });
-
- suite('safeTypesBridge', () => {
- function acceptsString(value, type) {
- assert.equal(Gerrit.SafeTypes.safeTypesBridge(value, type),
- value);
- }
-
- function rejects(value, type) {
- assert.throws(() => { Gerrit.SafeTypes.safeTypesBridge(value, type); });
- }
-
- test('accepts valid URL strings', () => {
- acceptsString('/foo/bar', 'URL');
- acceptsString('#baz', 'URL');
- });
-
- test('rejects invalid URL strings', () => {
- rejects('javascript://void();', 'URL');
- });
-
- test('accepts SafeUrl values', () => {
- const url = '/abc/123';
- const safeUrl = new element.SafeUrl(url);
- assert.equal(Gerrit.SafeTypes.safeTypesBridge(safeUrl, 'URL'), url);
- });
-
- test('rejects non-string or non-SafeUrl types', () => {
- rejects(3.1415926, 'URL');
- });
-
- test('accepts any binding to STRING or CONSTANT', () => {
- acceptsString('foo/bar/baz', 'STRING');
- acceptsString('lorem ipsum dolor', 'CONSTANT');
- });
-
- test('rejects all other types', () => {
- rejects('foo', 'JAVASCRIPT');
- rejects('foo', 'HTML');
- rejects('foo', 'RESOURCE_URL');
- rejects('foo', 'STYLE');
- });
+ suiteSetup(() => {
+ Polymer({
+ is: 'safe-types-element',
+ behaviors: [Gerrit.SafeTypes],
});
});
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('SafeUrl accepts valid urls', () => {
+ function accepts(url) {
+ const safeUrl = new element.SafeUrl(url);
+ assert.isOk(safeUrl);
+ assert.equal(url, safeUrl.asString());
+ }
+ accepts('http://www.google.com/');
+ accepts('https://www.google.com/');
+ accepts('HtTpS://www.google.com/');
+ accepts('//www.google.com/');
+ accepts('/c/1234/file/path.html@45');
+ accepts('#hash-url');
+ accepts('mailto:name@example.com');
+ });
+
+ test('SafeUrl rejects invalid urls', () => {
+ function rejects(url) {
+ assert.throws(() => { new element.SafeUrl(url); });
+ }
+ rejects('javascript://alert("evil");');
+ rejects('ftp:example.com');
+ rejects('data:text/html,scary business');
+ });
+
+ suite('safeTypesBridge', () => {
+ function acceptsString(value, type) {
+ assert.equal(Gerrit.SafeTypes.safeTypesBridge(value, type),
+ value);
+ }
+
+ function rejects(value, type) {
+ assert.throws(() => { Gerrit.SafeTypes.safeTypesBridge(value, type); });
+ }
+
+ test('accepts valid URL strings', () => {
+ acceptsString('/foo/bar', 'URL');
+ acceptsString('#baz', 'URL');
+ });
+
+ test('rejects invalid URL strings', () => {
+ rejects('javascript://void();', 'URL');
+ });
+
+ test('accepts SafeUrl values', () => {
+ const url = '/abc/123';
+ const safeUrl = new element.SafeUrl(url);
+ assert.equal(Gerrit.SafeTypes.safeTypesBridge(safeUrl, 'URL'), url);
+ });
+
+ test('rejects non-string or non-SafeUrl types', () => {
+ rejects(3.1415926, 'URL');
+ });
+
+ test('accepts any binding to STRING or CONSTANT', () => {
+ acceptsString('foo/bar/baz', 'STRING');
+ acceptsString('lorem ipsum dolor', 'CONSTANT');
+ });
+
+ test('rejects all other types', () => {
+ rejects('foo', 'JAVASCRIPT');
+ rejects('foo', 'HTML');
+ rejects('foo', 'RESOURCE_URL');
+ rejects('foo', 'STYLE');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
deleted file mode 100644
index a52cb1a..0000000
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
+++ /dev/null
@@ -1,168 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-permission/gr-permission.html">
-
-<script src="../../../scripts/util.js"></script>
-
-<dom-module id="gr-access-section">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- margin-bottom: var(--spacing-l);
- }
- fieldset {
- border: 1px solid var(--border-color);
- }
- .name {
- align-items: center;
- display: flex;
- }
- .header,
- #deletedContainer {
- align-items: center;
- background: var(--table-header-background-color);
- border-bottom: 1px dotted var(--border-color);
- display: flex;
- justify-content: space-between;
- min-height: 3em;
- padding: 0 var(--spacing-m);
- }
- #deletedContainer {
- border-bottom: 0;
- }
- .sectionContent {
- padding: var(--spacing-m);
- }
- #editBtn,
- .editing #editBtn.global,
- #deletedContainer,
- .deleted #mainContainer,
- #addPermission,
- #deleteBtn,
- .editingRef .name,
- .editRefInput {
- display: none;
- }
- .editing #editBtn,
- .editingRef .editRefInput {
- display: flex;
- }
- .deleted #deletedContainer {
- display: flex;
- }
- .editing #addPermission,
- #mainContainer,
- .editing #deleteBtn {
- display: block;
- }
- .editing #deleteBtn,
- #undoRemoveBtn {
- padding-right: var(--spacing-m);
- }
- </style>
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <fieldset id="section"
- class$="gr-form-styles [[_computeSectionClass(editing, canUpload, ownerOf, _editingRef, _deleted)]]">
- <div id="mainContainer">
- <div class="header">
- <div class="name">
- <h3>[[_computeSectionName(section.id)]]</h3>
- <gr-button
- id="editBtn"
- link
- class$="[[_computeEditBtnClass(section.id)]]"
- on-click="editReference">
- <iron-icon id="icon" icon="gr-icons:create"></iron-icon>
- </gr-button>
- </div>
- <iron-input
- class="editRefInput"
- bind-value="{{section.id}}"
- type="text"
- on-input="_handleValueChange">
- <input
- class="editRefInput"
- bind-value="{{section.id}}"
- is="iron-input"
- type="text"
- on-input="_handleValueChange">
- </iron-input>
- <gr-button
- link
- id="deleteBtn"
- on-click="_handleRemoveReference">Remove</gr-button>
- </div><!-- end header -->
- <div class="sectionContent">
- <template
- is="dom-repeat"
- items="{{_permissions}}"
- as="permission">
- <gr-permission
- name="[[_computePermissionName(section.id, permission, permissionValues, capabilities)]]"
- permission="{{permission}}"
- labels="[[labels]]"
- section="[[section.id]]"
- editing="[[editing]]"
- groups="[[groups]]"
- on-added-permission-removed="_handleAddedPermissionRemoved">
- </gr-permission>
- </template>
- <div id="addPermission">
- Add permission:
- <select id="permissionSelect">
- <!-- called with a third parameter so that permissions update
- after a new section is added. -->
- <template
- is="dom-repeat"
- items="[[_computePermissions(section.id, capabilities, labels, section.value.permissions.*)]]">
- <option value="[[item.value.id]]">[[item.value.name]]</option>
- </template>
- </select>
- <gr-button
- link
- id="addBtn"
- on-click="_handleAddPermission">Add</gr-button>
- </div>
- <!-- end addPermission -->
- </div><!-- end sectionContent -->
- </div><!-- end mainContainer -->
- <div id="deletedContainer">
- <span>[[_computeSectionName(section.id)]] was deleted</span>
- <gr-button
- link
- id="undoRemoveBtn"
- on-click="_handleUndoRemove">Undo</gr-button>
- </div><!-- end deletedContainer -->
- </fieldset>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-access-section.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
index a421043..cfb28bb 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
@@ -14,291 +14,308 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-permission/gr-permission.js';
+import '../../../scripts/util.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {htmlTemplate} from './gr-access-section_html.js';
+
+/**
+ * Fired when the section has been modified or removed.
+ *
+ * @event access-modified
+ */
+
+/**
+ * Fired when a section that was previously added was removed.
+ *
+ * @event added-section-removed
+ */
+
+const GLOBAL_NAME = 'GLOBAL_CAPABILITIES';
+
+// The name that gets automatically input when a new reference is added.
+const NEW_NAME = 'refs/heads/*';
+const REFS_NAME = 'refs/';
+const ON_BEHALF_OF = '(On Behalf Of)';
+const LABEL = 'Label';
+
+/**
+ * @appliesMixin Gerrit.AccessMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrAccessSection extends mixinBehaviors( [
+ Gerrit.AccessBehavior,
/**
- * Fired when the section has been modified or removed.
- *
- * @event access-modified
+ * Unused in this element, but called by other elements in tests
+ * e.g gr-repo-access_test.
*/
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /**
- * Fired when a section that was previously added was removed.
- *
- * @event added-section-removed
- */
+ static get is() { return 'gr-access-section'; }
- const GLOBAL_NAME = 'GLOBAL_CAPABILITIES';
+ static get properties() {
+ return {
+ capabilities: Object,
+ /** @type {?} */
+ section: {
+ type: Object,
+ notify: true,
+ observer: '_updateSection',
+ },
+ groups: Object,
+ labels: Object,
+ editing: {
+ type: Boolean,
+ value: false,
+ observer: '_handleEditingChanged',
+ },
+ canUpload: Boolean,
+ ownerOf: Array,
+ _originalId: String,
+ _editingRef: {
+ type: Boolean,
+ value: false,
+ },
+ _deleted: {
+ type: Boolean,
+ value: false,
+ },
+ _permissions: Array,
+ };
+ }
- // The name that gets automatically input when a new reference is added.
- const NEW_NAME = 'refs/heads/*';
- const REFS_NAME = 'refs/';
- const ON_BEHALF_OF = '(On Behalf Of)';
- const LABEL = 'Label';
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('access-saved',
+ () => this._handleAccessSaved());
+ }
- /**
- * @appliesMixin Gerrit.AccessMixin
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
- */
- class GrAccessSection extends Polymer.mixinBehaviors( [
- Gerrit.AccessBehavior,
- /**
- * Unused in this element, but called by other elements in tests
- * e.g gr-repo-access_test.
- */
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-access-section'; }
+ _updateSection(section) {
+ this._permissions = this.toSortedArray(section.value.permissions);
+ this._originalId = section.id;
+ }
- static get properties() {
- return {
- capabilities: Object,
- /** @type {?} */
- section: {
- type: Object,
- notify: true,
- observer: '_updateSection',
- },
- groups: Object,
- labels: Object,
- editing: {
- type: Boolean,
- value: false,
- observer: '_handleEditingChanged',
- },
- canUpload: Boolean,
- ownerOf: Array,
- _originalId: String,
- _editingRef: {
- type: Boolean,
- value: false,
- },
- _deleted: {
- type: Boolean,
- value: false,
- },
- _permissions: Array,
- };
+ _handleAccessSaved() {
+ // Set a new 'original' value to keep track of after the value has been
+ // saved.
+ this._updateSection(this.section);
+ }
+
+ _handleValueChange() {
+ if (!this.section.value.added) {
+ this.section.value.modified = this.section.id !== this._originalId;
+ // Allows overall access page to know a change has been made.
+ // For a new section, this is not fired because new permissions and
+ // rules have to be added in order to save, modifying the ref is not
+ // enough.
+ this.dispatchEvent(new CustomEvent(
+ 'access-modified', {bubbles: true, composed: true}));
}
+ this.section.value.updatedId = this.section.id;
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('access-saved',
- () => this._handleAccessSaved());
- }
-
- _updateSection(section) {
- this._permissions = this.toSortedArray(section.value.permissions);
- this._originalId = section.id;
- }
-
- _handleAccessSaved() {
- // Set a new 'original' value to keep track of after the value has been
- // saved.
- this._updateSection(this.section);
- }
-
- _handleValueChange() {
- if (!this.section.value.added) {
- this.section.value.modified = this.section.id !== this._originalId;
- // Allows overall access page to know a change has been made.
- // For a new section, this is not fired because new permissions and
- // rules have to be added in order to save, modifying the ref is not
- // enough.
- this.dispatchEvent(new CustomEvent(
- 'access-modified', {bubbles: true, composed: true}));
- }
- this.section.value.updatedId = this.section.id;
- }
-
- _handleEditingChanged(editing, editingOld) {
- // Ignore when editing gets set initially.
- if (!editingOld) { return; }
- // Restore original values if no longer editing.
- if (!editing) {
- this._editingRef = false;
- this._deleted = false;
- delete this.section.value.deleted;
- // Restore section ref.
- this.set(['section', 'id'], this._originalId);
- // Remove any unsaved but added permissions.
- this._permissions = this._permissions.filter(p => !p.value.added);
- for (const key of Object.keys(this.section.value.permissions)) {
- if (this.section.value.permissions[key].added) {
- delete this.section.value.permissions[key];
- }
- }
- }
- }
-
- _computePermissions(name, capabilities, labels) {
- let allPermissions;
- if (!this.section || !this.section.value) {
- return [];
- }
- if (name === GLOBAL_NAME) {
- allPermissions = this.toSortedArray(capabilities);
- } else {
- const labelOptions = this._computeLabelOptions(labels);
- allPermissions = labelOptions.concat(
- this.toSortedArray(this.permissionValues));
- }
- return allPermissions
- .filter(permission => !this.section.value.permissions[permission.id]);
- }
-
- _computeHideEditClass(section) {
- return section.id === 'GLOBAL_CAPABILITIES' ? 'hide' : '';
- }
-
- _handleAddedPermissionRemoved(e) {
- const index = e.model.index;
- this._permissions = this._permissions.slice(0, index).concat(
- this._permissions.slice(index + 1, this._permissions.length));
- }
-
- _computeLabelOptions(labels) {
- const labelOptions = [];
- if (!labels) { return []; }
- for (const labelName of Object.keys(labels)) {
- labelOptions.push({
- id: 'label-' + labelName,
- value: {
- name: `${LABEL} ${labelName}`,
- id: 'label-' + labelName,
- },
- });
- labelOptions.push({
- id: 'labelAs-' + labelName,
- value: {
- name: `${LABEL} ${labelName} ${ON_BEHALF_OF}`,
- id: 'labelAs-' + labelName,
- },
- });
- }
- return labelOptions;
- }
-
- _computePermissionName(name, permission, permissionValues, capabilities) {
- if (name === GLOBAL_NAME) {
- return capabilities[permission.id].name;
- } else if (permissionValues[permission.id]) {
- return permissionValues[permission.id].name;
- } else if (permission.value.label) {
- let behalfOf = '';
- if (permission.id.startsWith('labelAs-')) {
- behalfOf = ON_BEHALF_OF;
- }
- return `${LABEL} ${permission.value.label}${behalfOf}`;
- }
- }
-
- _computeSectionName(name) {
- // When a new section is created, it doesn't yet have a ref. Set into
- // edit mode so that the user can input one.
- if (!name) {
- this._editingRef = true;
- // Needed for the title value. This is the same default as GWT.
- name = NEW_NAME;
- // Needed for the input field value.
- this.set('section.id', name);
- }
- if (name === GLOBAL_NAME) {
- return 'Global Capabilities';
- } else if (name.startsWith(REFS_NAME)) {
- return `Reference: ${name}`;
- }
- return name;
- }
-
- _handleRemoveReference() {
- if (this.section.value.added) {
- this.dispatchEvent(new CustomEvent(
- 'added-section-removed', {bubbles: true, composed: true}));
- }
- this._deleted = true;
- this.section.value.deleted = true;
- this.dispatchEvent(
- new CustomEvent('access-modified', {bubbles: true, composed: true}));
- }
-
- _handleUndoRemove() {
+ _handleEditingChanged(editing, editingOld) {
+ // Ignore when editing gets set initially.
+ if (!editingOld) { return; }
+ // Restore original values if no longer editing.
+ if (!editing) {
+ this._editingRef = false;
this._deleted = false;
delete this.section.value.deleted;
- }
-
- editRefInput() {
- return Polymer.dom(this.root).querySelector(Polymer.Element ?
- 'iron-input.editRefInput' :
- 'input[is=iron-input].editRefInput');
- }
-
- editReference() {
- this._editingRef = true;
- this.editRefInput().focus();
- }
-
- _isEditEnabled(canUpload, ownerOf, sectionId) {
- return canUpload || (ownerOf && ownerOf.indexOf(sectionId) >= 0);
- }
-
- _computeSectionClass(editing, canUpload, ownerOf, editingRef, deleted) {
- const classList = [];
- if (editing
- && this._isEditEnabled(canUpload, ownerOf, this.section.id)) {
- classList.push('editing');
+ // Restore section ref.
+ this.set(['section', 'id'], this._originalId);
+ // Remove any unsaved but added permissions.
+ this._permissions = this._permissions.filter(p => !p.value.added);
+ for (const key of Object.keys(this.section.value.permissions)) {
+ if (this.section.value.permissions[key].added) {
+ delete this.section.value.permissions[key];
+ }
}
- if (editingRef) {
- classList.push('editingRef');
- }
- if (deleted) {
- classList.push('deleted');
- }
- return classList.join(' ');
- }
-
- _computeEditBtnClass(name) {
- return name === GLOBAL_NAME ? 'global' : '';
- }
-
- _handleAddPermission() {
- const value = this.$.permissionSelect.value;
- const permission = {
- id: value,
- value: {rules: {}, added: true},
- };
-
- // This is needed to update the 'label' property of the
- // 'label-<label-name>' permission.
- //
- // The value from the add permission dropdown will either be
- // label-<label-name> or labelAs-<labelName>.
- // But, the format of the API response is as such:
- // "permissions": {
- // "label-Code-Review": {
- // "label": "Code-Review",
- // "rules": {...}
- // }
- // }
- // }
- // When we add a new item, we have to push the new permission in the same
- // format as the ones that have been returned by the API.
- if (value.startsWith('label')) {
- permission.value.label =
- value.replace('label-', '').replace('labelAs-', '');
- }
- // Add to the end of the array (used in dom-repeat) and also to the
- // section object that is two way bound with its parent element.
- this.push('_permissions', permission);
- this.set(['section.value.permissions', permission.id],
- permission.value);
}
}
- customElements.define(GrAccessSection.is, GrAccessSection);
-})();
+ _computePermissions(name, capabilities, labels) {
+ let allPermissions;
+ if (!this.section || !this.section.value) {
+ return [];
+ }
+ if (name === GLOBAL_NAME) {
+ allPermissions = this.toSortedArray(capabilities);
+ } else {
+ const labelOptions = this._computeLabelOptions(labels);
+ allPermissions = labelOptions.concat(
+ this.toSortedArray(this.permissionValues));
+ }
+ return allPermissions
+ .filter(permission => !this.section.value.permissions[permission.id]);
+ }
+
+ _computeHideEditClass(section) {
+ return section.id === 'GLOBAL_CAPABILITIES' ? 'hide' : '';
+ }
+
+ _handleAddedPermissionRemoved(e) {
+ const index = e.model.index;
+ this._permissions = this._permissions.slice(0, index).concat(
+ this._permissions.slice(index + 1, this._permissions.length));
+ }
+
+ _computeLabelOptions(labels) {
+ const labelOptions = [];
+ if (!labels) { return []; }
+ for (const labelName of Object.keys(labels)) {
+ labelOptions.push({
+ id: 'label-' + labelName,
+ value: {
+ name: `${LABEL} ${labelName}`,
+ id: 'label-' + labelName,
+ },
+ });
+ labelOptions.push({
+ id: 'labelAs-' + labelName,
+ value: {
+ name: `${LABEL} ${labelName} ${ON_BEHALF_OF}`,
+ id: 'labelAs-' + labelName,
+ },
+ });
+ }
+ return labelOptions;
+ }
+
+ _computePermissionName(name, permission, permissionValues, capabilities) {
+ if (name === GLOBAL_NAME) {
+ return capabilities[permission.id].name;
+ } else if (permissionValues[permission.id]) {
+ return permissionValues[permission.id].name;
+ } else if (permission.value.label) {
+ let behalfOf = '';
+ if (permission.id.startsWith('labelAs-')) {
+ behalfOf = ON_BEHALF_OF;
+ }
+ return `${LABEL} ${permission.value.label}${behalfOf}`;
+ }
+ }
+
+ _computeSectionName(name) {
+ // When a new section is created, it doesn't yet have a ref. Set into
+ // edit mode so that the user can input one.
+ if (!name) {
+ this._editingRef = true;
+ // Needed for the title value. This is the same default as GWT.
+ name = NEW_NAME;
+ // Needed for the input field value.
+ this.set('section.id', name);
+ }
+ if (name === GLOBAL_NAME) {
+ return 'Global Capabilities';
+ } else if (name.startsWith(REFS_NAME)) {
+ return `Reference: ${name}`;
+ }
+ return name;
+ }
+
+ _handleRemoveReference() {
+ if (this.section.value.added) {
+ this.dispatchEvent(new CustomEvent(
+ 'added-section-removed', {bubbles: true, composed: true}));
+ }
+ this._deleted = true;
+ this.section.value.deleted = true;
+ this.dispatchEvent(
+ new CustomEvent('access-modified', {bubbles: true, composed: true}));
+ }
+
+ _handleUndoRemove() {
+ this._deleted = false;
+ delete this.section.value.deleted;
+ }
+
+ editRefInput() {
+ return dom(this.root).querySelector(PolymerElement ?
+ 'iron-input.editRefInput' :
+ 'input[is=iron-input].editRefInput');
+ }
+
+ editReference() {
+ this._editingRef = true;
+ this.editRefInput().focus();
+ }
+
+ _isEditEnabled(canUpload, ownerOf, sectionId) {
+ return canUpload || (ownerOf && ownerOf.indexOf(sectionId) >= 0);
+ }
+
+ _computeSectionClass(editing, canUpload, ownerOf, editingRef, deleted) {
+ const classList = [];
+ if (editing
+ && this._isEditEnabled(canUpload, ownerOf, this.section.id)) {
+ classList.push('editing');
+ }
+ if (editingRef) {
+ classList.push('editingRef');
+ }
+ if (deleted) {
+ classList.push('deleted');
+ }
+ return classList.join(' ');
+ }
+
+ _computeEditBtnClass(name) {
+ return name === GLOBAL_NAME ? 'global' : '';
+ }
+
+ _handleAddPermission() {
+ const value = this.$.permissionSelect.value;
+ const permission = {
+ id: value,
+ value: {rules: {}, added: true},
+ };
+
+ // This is needed to update the 'label' property of the
+ // 'label-<label-name>' permission.
+ //
+ // The value from the add permission dropdown will either be
+ // label-<label-name> or labelAs-<labelName>.
+ // But, the format of the API response is as such:
+ // "permissions": {
+ // "label-Code-Review": {
+ // "label": "Code-Review",
+ // "rules": {...}
+ // }
+ // }
+ // }
+ // When we add a new item, we have to push the new permission in the same
+ // format as the ones that have been returned by the API.
+ if (value.startsWith('label')) {
+ permission.value.label =
+ value.replace('label-', '').replace('labelAs-', '');
+ }
+ // Add to the end of the array (used in dom-repeat) and also to the
+ // section object that is two way bound with its parent element.
+ this.push('_permissions', permission);
+ this.set(['section.value.permissions', permission.id],
+ permission.value);
+ }
+}
+
+customElements.define(GrAccessSection.is, GrAccessSection);
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js
new file mode 100644
index 0000000..5f35f55
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js
@@ -0,0 +1,117 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ margin-bottom: var(--spacing-l);
+ }
+ fieldset {
+ border: 1px solid var(--border-color);
+ }
+ .name {
+ align-items: center;
+ display: flex;
+ }
+ .header,
+ #deletedContainer {
+ align-items: center;
+ background: var(--table-header-background-color);
+ border-bottom: 1px dotted var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ min-height: 3em;
+ padding: 0 var(--spacing-m);
+ }
+ #deletedContainer {
+ border-bottom: 0;
+ }
+ .sectionContent {
+ padding: var(--spacing-m);
+ }
+ #editBtn,
+ .editing #editBtn.global,
+ #deletedContainer,
+ .deleted #mainContainer,
+ #addPermission,
+ #deleteBtn,
+ .editingRef .name,
+ .editRefInput {
+ display: none;
+ }
+ .editing #editBtn,
+ .editingRef .editRefInput {
+ display: flex;
+ }
+ .deleted #deletedContainer {
+ display: flex;
+ }
+ .editing #addPermission,
+ #mainContainer,
+ .editing #deleteBtn {
+ display: block;
+ }
+ .editing #deleteBtn,
+ #undoRemoveBtn {
+ padding-right: var(--spacing-m);
+ }
+ </style>
+ <style include="gr-form-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <fieldset id="section" class\$="gr-form-styles [[_computeSectionClass(editing, canUpload, ownerOf, _editingRef, _deleted)]]">
+ <div id="mainContainer">
+ <div class="header">
+ <div class="name">
+ <h3>[[_computeSectionName(section.id)]]</h3>
+ <gr-button id="editBtn" link="" class\$="[[_computeEditBtnClass(section.id)]]" on-click="editReference">
+ <iron-icon id="icon" icon="gr-icons:create"></iron-icon>
+ </gr-button>
+ </div>
+ <iron-input class="editRefInput" bind-value="{{section.id}}" type="text" on-input="_handleValueChange">
+ <input class="editRefInput" bind-value="{{section.id}}" is="iron-input" type="text" on-input="_handleValueChange">
+ </iron-input>
+ <gr-button link="" id="deleteBtn" on-click="_handleRemoveReference">Remove</gr-button>
+ </div><!-- end header -->
+ <div class="sectionContent">
+ <template is="dom-repeat" items="{{_permissions}}" as="permission">
+ <gr-permission name="[[_computePermissionName(section.id, permission, permissionValues, capabilities)]]" permission="{{permission}}" labels="[[labels]]" section="[[section.id]]" editing="[[editing]]" groups="[[groups]]" on-added-permission-removed="_handleAddedPermissionRemoved">
+ </gr-permission>
+ </template>
+ <div id="addPermission">
+ Add permission:
+ <select id="permissionSelect">
+ <!-- called with a third parameter so that permissions update
+ after a new section is added. -->
+ <template is="dom-repeat" items="[[_computePermissions(section.id, capabilities, labels, section.value.permissions.*)]]">
+ <option value="[[item.value.id]]">[[item.value.name]]</option>
+ </template>
+ </select>
+ <gr-button link="" id="addBtn" on-click="_handleAddPermission">Add</gr-button>
+ </div>
+ <!-- end addPermission -->
+ </div><!-- end sectionContent -->
+ </div><!-- end mainContainer -->
+ <div id="deletedContainer">
+ <span>[[_computeSectionName(section.id)]] was deleted</span>
+ <gr-button link="" id="undoRemoveBtn" on-click="_handleUndoRemove">Undo</gr-button>
+ </div><!-- end deletedContainer -->
+ </fieldset>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
index 2a3044e..459949e 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
@@ -19,16 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-access-section</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-access-section.html">
-
-<script>void(0);</script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,28 +31,309 @@
</template>
</test-fixture>
-<script>
- suite('gr-access-section tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-access-section.js';
+suite('gr-access-section tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('unit tests', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
+ element.section = {
+ id: 'refs/*',
+ value: {
+ permissions: {
+ read: {
+ rules: {},
+ },
+ },
+ },
+ };
+ element.capabilities = {
+ accessDatabase: {
+ id: 'accessDatabase',
+ name: 'Access Database',
+ },
+ administrateServer: {
+ id: 'administrateServer',
+ name: 'Administrate Server',
+ },
+ batchChangesLimit: {
+ id: 'batchChangesLimit',
+ name: 'Batch Changes Limit',
+ },
+ createAccount: {
+ id: 'createAccount',
+ name: 'Create Account',
+ },
+ };
+ element.labels = {
+ 'Code-Review': {
+ values: {
+ ' 0': 'No score',
+ '-1': 'I would prefer this is not merged as is',
+ '-2': 'This shall not be merged',
+ '+1': 'Looks good to me, but someone else must approve',
+ '+2': 'Looks good to me, approved',
+ },
+ default_value: 0,
+ },
+ };
+ element._updateSection(element.section);
+ flushAsynchronousOperations();
});
- teardown(() => {
- sandbox.restore();
+ test('_updateSection', () => {
+ // _updateSection was called in setup, so just make assertions.
+ const expectedPermissions = [
+ {
+ id: 'read',
+ value: {
+ rules: {},
+ },
+ },
+ ];
+ assert.deepEqual(element._permissions, expectedPermissions);
+ assert.equal(element._originalId, element.section.id);
});
- suite('unit tests', () => {
+ test('_computeLabelOptions', () => {
+ const expectedLabelOptions = [
+ {
+ id: 'label-Code-Review',
+ value: {
+ name: 'Label Code-Review',
+ id: 'label-Code-Review',
+ },
+ },
+ {
+ id: 'labelAs-Code-Review',
+ value: {
+ name: 'Label Code-Review (On Behalf Of)',
+ id: 'labelAs-Code-Review',
+ },
+ },
+ ];
+
+ assert.deepEqual(element._computeLabelOptions(element.labels),
+ expectedLabelOptions);
+ });
+
+ test('_handleAccessSaved', () => {
+ assert.equal(element._originalId, 'refs/*');
+ element.section.id = 'refs/for/bar';
+ element._handleAccessSaved();
+ assert.equal(element._originalId, 'refs/for/bar');
+ });
+
+ test('_computePermissions', () => {
+ sandbox.stub(element, 'toSortedArray').returns(
+ [{
+ id: 'push',
+ value: {
+ rules: {},
+ },
+ },
+ {
+ id: 'read',
+ value: {
+ rules: {},
+ },
+ },
+ ]);
+
+ const expectedPermissions = [{
+ id: 'push',
+ value: {
+ rules: {},
+ },
+ },
+ ];
+ const labelOptions = [
+ {
+ id: 'label-Code-Review',
+ value: {
+ name: 'Label Code-Review',
+ id: 'label-Code-Review',
+ },
+ },
+ {
+ id: 'labelAs-Code-Review',
+ value: {
+ name: 'Label Code-Review (On Behalf Of)',
+ id: 'labelAs-Code-Review',
+ },
+ },
+ ];
+
+ // For global capabilities, just return the sorted array filtered by
+ // existing permissions.
+ let name = 'GLOBAL_CAPABILITIES';
+ assert.deepEqual(element._computePermissions(name, element.capabilities,
+ element.labels), expectedPermissions);
+
+ // Uses the capabilities array to come up with possible values.
+ assert.isTrue(element.toSortedArray.lastCall.
+ calledWithExactly(element.capabilities));
+
+ // For everything else, include possible label values before filtering.
+ name = 'refs/for/*';
+ assert.deepEqual(element._computePermissions(name, element.capabilities,
+ element.labels), labelOptions.concat(expectedPermissions));
+
+ // Uses permissionValues (defined in gr-access-behavior) to come up with
+ // possible values.
+ assert.isTrue(element.toSortedArray.lastCall.
+ calledWithExactly(element.permissionValues));
+ });
+
+ test('_computePermissionName', () => {
+ let name = 'GLOBAL_CAPABILITIES';
+ let permission = {
+ id: 'administrateServer',
+ value: {},
+ };
+ assert.equal(element._computePermissionName(name, permission,
+ element.permissionValues, element.capabilities),
+ element.capabilities[permission.id].name);
+
+ name = 'refs/for/*';
+ permission = {
+ id: 'abandon',
+ value: {},
+ };
+
+ assert.equal(element._computePermissionName(
+ name, permission, element.permissionValues, element.capabilities),
+ element.permissionValues[permission.id].name);
+
+ name = 'refs/for/*';
+ permission = {
+ id: 'label-Code-Review',
+ value: {
+ label: 'Code-Review',
+ },
+ };
+
+ assert.equal(element._computePermissionName(name, permission,
+ element.permissionValues, element.capabilities),
+ 'Label Code-Review');
+
+ permission = {
+ id: 'labelAs-Code-Review',
+ value: {
+ label: 'Code-Review',
+ },
+ };
+
+ assert.equal(element._computePermissionName(name, permission,
+ element.permissionValues, element.capabilities),
+ 'Label Code-Review(On Behalf Of)');
+ });
+
+ test('_computeSectionName', () => {
+ let name;
+ // When computing the section name for an undefined name, it means a
+ // new section is being added. In this case, it should defualt to
+ // 'refs/heads/*'.
+ element._editingRef = false;
+ assert.equal(element._computeSectionName(name),
+ 'Reference: refs/heads/*');
+ assert.isTrue(element._editingRef);
+ assert.equal(element.section.id, 'refs/heads/*');
+
+ // Reset editing to false.
+ element._editingRef = false;
+ name = 'GLOBAL_CAPABILITIES';
+ assert.equal(element._computeSectionName(name), 'Global Capabilities');
+ assert.isFalse(element._editingRef);
+
+ name = 'refs/for/*';
+ assert.equal(element._computeSectionName(name),
+ 'Reference: refs/for/*');
+ assert.isFalse(element._editingRef);
+ });
+
+ test('editReference', () => {
+ element.editReference();
+ assert.isTrue(element._editingRef);
+ });
+
+ test('_computeSectionClass', () => {
+ let editingRef = false;
+ let canUpload = false;
+ let ownerOf = [];
+ let editing = false;
+ let deleted = false;
+ assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
+ editingRef, deleted), '');
+
+ editing = true;
+ assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
+ editingRef, deleted), '');
+
+ ownerOf = ['refs/*'];
+ assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
+ editingRef, deleted), 'editing');
+
+ ownerOf = [];
+ canUpload = true;
+ assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
+ editingRef, deleted), 'editing');
+
+ editingRef = true;
+ assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
+ editingRef, deleted), 'editing editingRef');
+
+ deleted = true;
+ assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
+ editingRef, deleted), 'editing editingRef deleted');
+
+ editingRef = false;
+ assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
+ editingRef, deleted), 'editing deleted');
+ });
+
+ test('_computeEditBtnClass', () => {
+ let name = 'GLOBAL_CAPABILITIES';
+ assert.equal(element._computeEditBtnClass(name), 'global');
+ name = 'refs/for/*';
+ assert.equal(element._computeEditBtnClass(name), '');
+ });
+ });
+
+ suite('interactive tests', () => {
+ setup(() => {
+ element.labels = {
+ 'Code-Review': {
+ values: {
+ ' 0': 'No score',
+ '-1': 'I would prefer this is not merged as is',
+ '-2': 'This shall not be merged',
+ '+1': 'Looks good to me, but someone else must approve',
+ '+2': 'Looks good to me, approved',
+ },
+ default_value: 0,
+ },
+ };
+ });
+ suite('Global section', () => {
setup(() => {
element.section = {
- id: 'refs/*',
+ id: 'GLOBAL_CAPABILITIES',
value: {
permissions: {
- read: {
+ accessDatabase: {
rules: {},
},
},
@@ -81,476 +357,196 @@
name: 'Create Account',
},
};
- element.labels = {
- 'Code-Review': {
- values: {
- ' 0': 'No score',
- '-1': 'I would prefer this is not merged as is',
- '-2': 'This shall not be merged',
- '+1': 'Looks good to me, but someone else must approve',
- '+2': 'Looks good to me, approved',
- },
- default_value: 0,
- },
- };
element._updateSection(element.section);
flushAsynchronousOperations();
});
- test('_updateSection', () => {
- // _updateSection was called in setup, so just make assertions.
- const expectedPermissions = [
- {
- id: 'read',
- value: {
- rules: {},
- },
- },
- ];
- assert.deepEqual(element._permissions, expectedPermissions);
- assert.equal(element._originalId, element.section.id);
- });
-
- test('_computeLabelOptions', () => {
- const expectedLabelOptions = [
- {
- id: 'label-Code-Review',
- value: {
- name: 'Label Code-Review',
- id: 'label-Code-Review',
- },
- },
- {
- id: 'labelAs-Code-Review',
- value: {
- name: 'Label Code-Review (On Behalf Of)',
- id: 'labelAs-Code-Review',
- },
- },
- ];
-
- assert.deepEqual(element._computeLabelOptions(element.labels),
- expectedLabelOptions);
- });
-
- test('_handleAccessSaved', () => {
- assert.equal(element._originalId, 'refs/*');
- element.section.id = 'refs/for/bar';
- element._handleAccessSaved();
- assert.equal(element._originalId, 'refs/for/bar');
- });
-
- test('_computePermissions', () => {
- sandbox.stub(element, 'toSortedArray').returns(
- [{
- id: 'push',
- value: {
- rules: {},
- },
- },
- {
- id: 'read',
- value: {
- rules: {},
- },
- },
- ]);
-
- const expectedPermissions = [{
- id: 'push',
- value: {
- rules: {},
- },
- },
- ];
- const labelOptions = [
- {
- id: 'label-Code-Review',
- value: {
- name: 'Label Code-Review',
- id: 'label-Code-Review',
- },
- },
- {
- id: 'labelAs-Code-Review',
- value: {
- name: 'Label Code-Review (On Behalf Of)',
- id: 'labelAs-Code-Review',
- },
- },
- ];
-
- // For global capabilities, just return the sorted array filtered by
- // existing permissions.
- let name = 'GLOBAL_CAPABILITIES';
- assert.deepEqual(element._computePermissions(name, element.capabilities,
- element.labels), expectedPermissions);
-
- // Uses the capabilities array to come up with possible values.
- assert.isTrue(element.toSortedArray.lastCall.
- calledWithExactly(element.capabilities));
-
- // For everything else, include possible label values before filtering.
- name = 'refs/for/*';
- assert.deepEqual(element._computePermissions(name, element.capabilities,
- element.labels), labelOptions.concat(expectedPermissions));
-
- // Uses permissionValues (defined in gr-access-behavior) to come up with
- // possible values.
- assert.isTrue(element.toSortedArray.lastCall.
- calledWithExactly(element.permissionValues));
- });
-
- test('_computePermissionName', () => {
- let name = 'GLOBAL_CAPABILITIES';
- let permission = {
- id: 'administrateServer',
- value: {},
- };
- assert.equal(element._computePermissionName(name, permission,
- element.permissionValues, element.capabilities),
- element.capabilities[permission.id].name);
-
- name = 'refs/for/*';
- permission = {
- id: 'abandon',
- value: {},
- };
-
- assert.equal(element._computePermissionName(
- name, permission, element.permissionValues, element.capabilities),
- element.permissionValues[permission.id].name);
-
- name = 'refs/for/*';
- permission = {
- id: 'label-Code-Review',
- value: {
- label: 'Code-Review',
- },
- };
-
- assert.equal(element._computePermissionName(name, permission,
- element.permissionValues, element.capabilities),
- 'Label Code-Review');
-
- permission = {
- id: 'labelAs-Code-Review',
- value: {
- label: 'Code-Review',
- },
- };
-
- assert.equal(element._computePermissionName(name, permission,
- element.permissionValues, element.capabilities),
- 'Label Code-Review(On Behalf Of)');
- });
-
- test('_computeSectionName', () => {
- let name;
- // When computing the section name for an undefined name, it means a
- // new section is being added. In this case, it should defualt to
- // 'refs/heads/*'.
- element._editingRef = false;
- assert.equal(element._computeSectionName(name),
- 'Reference: refs/heads/*');
- assert.isTrue(element._editingRef);
- assert.equal(element.section.id, 'refs/heads/*');
-
- // Reset editing to false.
- element._editingRef = false;
- name = 'GLOBAL_CAPABILITIES';
- assert.equal(element._computeSectionName(name), 'Global Capabilities');
- assert.isFalse(element._editingRef);
-
- name = 'refs/for/*';
- assert.equal(element._computeSectionName(name),
- 'Reference: refs/for/*');
- assert.isFalse(element._editingRef);
- });
-
- test('editReference', () => {
- element.editReference();
- assert.isTrue(element._editingRef);
- });
-
- test('_computeSectionClass', () => {
- let editingRef = false;
- let canUpload = false;
- let ownerOf = [];
- let editing = false;
- let deleted = false;
- assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
- editingRef, deleted), '');
-
- editing = true;
- assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
- editingRef, deleted), '');
-
- ownerOf = ['refs/*'];
- assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
- editingRef, deleted), 'editing');
-
- ownerOf = [];
- canUpload = true;
- assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
- editingRef, deleted), 'editing');
-
- editingRef = true;
- assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
- editingRef, deleted), 'editing editingRef');
-
- deleted = true;
- assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
- editingRef, deleted), 'editing editingRef deleted');
-
- editingRef = false;
- assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
- editingRef, deleted), 'editing deleted');
- });
-
- test('_computeEditBtnClass', () => {
- let name = 'GLOBAL_CAPABILITIES';
- assert.equal(element._computeEditBtnClass(name), 'global');
- name = 'refs/for/*';
- assert.equal(element._computeEditBtnClass(name), '');
+ test('classes are assigned correctly', () => {
+ assert.isFalse(element.$.section.classList.contains('editing'));
+ assert.isFalse(element.$.section.classList.contains('deleted'));
+ assert.isTrue(element.$.editBtn.classList.contains('global'));
+ element.editing = true;
+ element.canUpload = true;
+ element.ownerOf = [];
+ assert.equal(getComputedStyle(element.$.editBtn).display, 'none');
});
});
- suite('interactive tests', () => {
+ suite('Non-global section', () => {
setup(() => {
- element.labels = {
- 'Code-Review': {
- values: {
- ' 0': 'No score',
- '-1': 'I would prefer this is not merged as is',
- '-2': 'This shall not be merged',
- '+1': 'Looks good to me, but someone else must approve',
- '+2': 'Looks good to me, approved',
+ element.section = {
+ id: 'refs/*',
+ value: {
+ permissions: {
+ read: {
+ rules: {},
+ },
},
- default_value: 0,
},
};
- });
- suite('Global section', () => {
- setup(() => {
- element.section = {
- id: 'GLOBAL_CAPABILITIES',
- value: {
- permissions: {
- accessDatabase: {
- rules: {},
- },
- },
- },
- };
- element.capabilities = {
- accessDatabase: {
- id: 'accessDatabase',
- name: 'Access Database',
- },
- administrateServer: {
- id: 'administrateServer',
- name: 'Administrate Server',
- },
- batchChangesLimit: {
- id: 'batchChangesLimit',
- name: 'Batch Changes Limit',
- },
- createAccount: {
- id: 'createAccount',
- name: 'Create Account',
- },
- };
- element._updateSection(element.section);
- flushAsynchronousOperations();
- });
-
- test('classes are assigned correctly', () => {
- assert.isFalse(element.$.section.classList.contains('editing'));
- assert.isFalse(element.$.section.classList.contains('deleted'));
- assert.isTrue(element.$.editBtn.classList.contains('global'));
- element.editing = true;
- element.canUpload = true;
- element.ownerOf = [];
- assert.equal(getComputedStyle(element.$.editBtn).display, 'none');
- });
+ element.capabilities = {};
+ element._updateSection(element.section);
+ flushAsynchronousOperations();
});
- suite('Non-global section', () => {
- setup(() => {
- element.section = {
- id: 'refs/*',
- value: {
- permissions: {
- read: {
- rules: {},
- },
- },
- },
- };
- element.capabilities = {};
- element._updateSection(element.section);
- flushAsynchronousOperations();
- });
+ test('classes are assigned correctly', () => {
+ assert.isFalse(element.$.section.classList.contains('editing'));
+ assert.isFalse(element.$.section.classList.contains('deleted'));
+ assert.isFalse(element.$.editBtn.classList.contains('global'));
+ element.editing = true;
+ element.canUpload = true;
+ element.ownerOf = [];
+ flushAsynchronousOperations();
+ assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none');
+ });
- test('classes are assigned correctly', () => {
- assert.isFalse(element.$.section.classList.contains('editing'));
- assert.isFalse(element.$.section.classList.contains('deleted'));
- assert.isFalse(element.$.editBtn.classList.contains('global'));
- element.editing = true;
- element.canUpload = true;
- element.ownerOf = [];
- flushAsynchronousOperations();
- assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none');
- });
+ test('add permission', () => {
+ element.editing = true;
+ element.$.permissionSelect.value = 'label-Code-Review';
+ assert.equal(element._permissions.length, 1);
+ assert.equal(Object.keys(element.section.value.permissions).length,
+ 1);
+ MockInteractions.tap(element.$.addBtn);
+ flushAsynchronousOperations();
- test('add permission', () => {
- element.editing = true;
- element.$.permissionSelect.value = 'label-Code-Review';
- assert.equal(element._permissions.length, 1);
- assert.equal(Object.keys(element.section.value.permissions).length,
- 1);
- MockInteractions.tap(element.$.addBtn);
- flushAsynchronousOperations();
+ // The permission is added to both the permissions array and also
+ // the section's permission object.
+ assert.equal(element._permissions.length, 2);
+ let permission = {
+ id: 'label-Code-Review',
+ value: {
+ added: true,
+ label: 'Code-Review',
+ rules: {},
+ },
+ };
+ assert.equal(element._permissions.length, 2);
+ assert.deepEqual(element._permissions[1], permission);
+ assert.equal(Object.keys(element.section.value.permissions).length,
+ 2);
+ assert.deepEqual(
+ element.section.value.permissions['label-Code-Review'],
+ permission.value);
- // The permission is added to both the permissions array and also
- // the section's permission object.
- assert.equal(element._permissions.length, 2);
- let permission = {
- id: 'label-Code-Review',
- value: {
- added: true,
- label: 'Code-Review',
- rules: {},
- },
- };
- assert.equal(element._permissions.length, 2);
- assert.deepEqual(element._permissions[1], permission);
- assert.equal(Object.keys(element.section.value.permissions).length,
- 2);
- assert.deepEqual(
- element.section.value.permissions['label-Code-Review'],
- permission.value);
+ element.$.permissionSelect.value = 'abandon';
+ MockInteractions.tap(element.$.addBtn);
+ flushAsynchronousOperations();
- element.$.permissionSelect.value = 'abandon';
- MockInteractions.tap(element.$.addBtn);
- flushAsynchronousOperations();
+ permission = {
+ id: 'abandon',
+ value: {
+ added: true,
+ rules: {},
+ },
+ };
- permission = {
- id: 'abandon',
- value: {
- added: true,
- rules: {},
- },
- };
+ assert.equal(element._permissions.length, 3);
+ assert.deepEqual(element._permissions[2], permission);
+ assert.equal(Object.keys(element.section.value.permissions).length,
+ 3);
+ assert.deepEqual(element.section.value.permissions['abandon'],
+ permission.value);
- assert.equal(element._permissions.length, 3);
- assert.deepEqual(element._permissions[2], permission);
- assert.equal(Object.keys(element.section.value.permissions).length,
- 3);
- assert.deepEqual(element.section.value.permissions['abandon'],
- permission.value);
+ // Unsaved changes are discarded when editing is cancelled.
+ element.editing = false;
+ assert.equal(element._permissions.length, 1);
+ assert.equal(Object.keys(element.section.value.permissions).length,
+ 1);
+ });
- // Unsaved changes are discarded when editing is cancelled.
+ test('edit section reference', done => {
+ element.canUpload = true;
+ element.ownerOf = [];
+ element.section = {id: 'refs/for/bar', value: {permissions: {}}};
+ assert.isFalse(element.$.section.classList.contains('editing'));
+ element.editing = true;
+ assert.isTrue(element.$.section.classList.contains('editing'));
+ assert.isFalse(element._editingRef);
+ MockInteractions.tap(element.$.editBtn);
+ element.editRefInput().bindValue='new/ref';
+ setTimeout(() => {
+ assert.equal(element.section.id, 'new/ref');
+ assert.isTrue(element._editingRef);
+ assert.isTrue(element.$.section.classList.contains('editingRef'));
element.editing = false;
- assert.equal(element._permissions.length, 1);
- assert.equal(Object.keys(element.section.value.permissions).length,
- 1);
- });
-
- test('edit section reference', done => {
- element.canUpload = true;
- element.ownerOf = [];
- element.section = {id: 'refs/for/bar', value: {permissions: {}}};
- assert.isFalse(element.$.section.classList.contains('editing'));
- element.editing = true;
- assert.isTrue(element.$.section.classList.contains('editing'));
assert.isFalse(element._editingRef);
- MockInteractions.tap(element.$.editBtn);
- element.editRefInput().bindValue='new/ref';
- setTimeout(() => {
- assert.equal(element.section.id, 'new/ref');
- assert.isTrue(element._editingRef);
- assert.isTrue(element.$.section.classList.contains('editingRef'));
- element.editing = false;
- assert.isFalse(element._editingRef);
- assert.equal(element.section.id, 'refs/for/bar');
- done();
- });
+ assert.equal(element.section.id, 'refs/for/bar');
+ done();
});
+ });
- test('_handleValueChange', () => {
- // For an exising section.
- const modifiedHandler = sandbox.stub();
- element.section = {id: 'refs/for/bar', value: {permissions: {}}};
- assert.notOk(element.section.value.updatedId);
- element.section.id = 'refs/for/baz';
- element.addEventListener('access-modified', modifiedHandler);
- assert.isNotOk(element.section.value.modified);
- element._handleValueChange();
- assert.equal(element.section.value.updatedId, 'refs/for/baz');
- assert.isTrue(element.section.value.modified);
- assert.equal(modifiedHandler.callCount, 1);
- element.section.id = 'refs/for/bar';
- element._handleValueChange();
- assert.isFalse(element.section.value.modified);
- assert.equal(modifiedHandler.callCount, 2);
+ test('_handleValueChange', () => {
+ // For an exising section.
+ const modifiedHandler = sandbox.stub();
+ element.section = {id: 'refs/for/bar', value: {permissions: {}}};
+ assert.notOk(element.section.value.updatedId);
+ element.section.id = 'refs/for/baz';
+ element.addEventListener('access-modified', modifiedHandler);
+ assert.isNotOk(element.section.value.modified);
+ element._handleValueChange();
+ assert.equal(element.section.value.updatedId, 'refs/for/baz');
+ assert.isTrue(element.section.value.modified);
+ assert.equal(modifiedHandler.callCount, 1);
+ element.section.id = 'refs/for/bar';
+ element._handleValueChange();
+ assert.isFalse(element.section.value.modified);
+ assert.equal(modifiedHandler.callCount, 2);
- // For a new section.
- element.section.value.added = true;
- element._handleValueChange();
- assert.isFalse(element.section.value.modified);
- assert.equal(modifiedHandler.callCount, 2);
- element.section.id = 'refs/for/bar';
- element._handleValueChange();
- assert.isFalse(element.section.value.modified);
- assert.equal(modifiedHandler.callCount, 2);
- });
+ // For a new section.
+ element.section.value.added = true;
+ element._handleValueChange();
+ assert.isFalse(element.section.value.modified);
+ assert.equal(modifiedHandler.callCount, 2);
+ element.section.id = 'refs/for/bar';
+ element._handleValueChange();
+ assert.isFalse(element.section.value.modified);
+ assert.equal(modifiedHandler.callCount, 2);
+ });
- test('remove section', () => {
- element.editing = true;
- element.canUpload = true;
- element.ownerOf = [];
- assert.isFalse(element._deleted);
- assert.isNotOk(element.section.value.deleted);
- MockInteractions.tap(element.$.deleteBtn);
- flushAsynchronousOperations();
- assert.isTrue(element._deleted);
- assert.isTrue(element.section.value.deleted);
- assert.isTrue(element.$.section.classList.contains('deleted'));
- assert.isTrue(element.section.value.deleted);
+ test('remove section', () => {
+ element.editing = true;
+ element.canUpload = true;
+ element.ownerOf = [];
+ assert.isFalse(element._deleted);
+ assert.isNotOk(element.section.value.deleted);
+ MockInteractions.tap(element.$.deleteBtn);
+ flushAsynchronousOperations();
+ assert.isTrue(element._deleted);
+ assert.isTrue(element.section.value.deleted);
+ assert.isTrue(element.$.section.classList.contains('deleted'));
+ assert.isTrue(element.section.value.deleted);
- MockInteractions.tap(element.$.undoRemoveBtn);
- flushAsynchronousOperations();
- assert.isFalse(element._deleted);
- assert.isNotOk(element.section.value.deleted);
+ MockInteractions.tap(element.$.undoRemoveBtn);
+ flushAsynchronousOperations();
+ assert.isFalse(element._deleted);
+ assert.isNotOk(element.section.value.deleted);
- MockInteractions.tap(element.$.deleteBtn);
- assert.isTrue(element._deleted);
- assert.isTrue(element.section.value.deleted);
- element.editing = false;
- assert.isFalse(element._deleted);
- assert.isNotOk(element.section.value.deleted);
- });
+ MockInteractions.tap(element.$.deleteBtn);
+ assert.isTrue(element._deleted);
+ assert.isTrue(element.section.value.deleted);
+ element.editing = false;
+ assert.isFalse(element._deleted);
+ assert.isNotOk(element.section.value.deleted);
+ });
- test('removing an added permission', () => {
- element.editing = true;
- assert.equal(element._permissions.length, 1);
- element.shadowRoot
- .querySelector('gr-permission').fire('added-permission-removed');
- flushAsynchronousOperations();
- assert.equal(element._permissions.length, 0);
- });
+ test('removing an added permission', () => {
+ element.editing = true;
+ assert.equal(element._permissions.length, 1);
+ element.shadowRoot
+ .querySelector('gr-permission').fire('added-permission-removed');
+ flushAsynchronousOperations();
+ assert.equal(element._permissions.length, 0);
+ });
- test('remove an added section', () => {
- const removeStub = sandbox.stub();
- element.addEventListener('added-section-removed', removeStub);
- element.editing = true;
- element.section.value.added = true;
- MockInteractions.tap(element.$.deleteBtn);
- assert.isTrue(removeStub.called);
- });
+ test('remove an added section', () => {
+ const removeStub = sandbox.stub();
+ element.addEventListener('added-section-removed', removeStub);
+ element.editing = true;
+ element.section.value.added = true;
+ MockInteractions.tap(element.$.deleteBtn);
+ assert.isTrue(removeStub.called);
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
deleted file mode 100644
index 5207717..0000000
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
+++ /dev/null
@@ -1,93 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../styles/gr-table-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-create-group-dialog/gr-create-group-dialog.html">
-
-<dom-module id="gr-admin-group-list">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-table-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <gr-list-view
- create-new="[[_createNewCapability]]"
- filter="[[_filter]]"
- items="[[_groups]]"
- items-per-page="[[_groupsPerPage]]"
- loading="[[_loading]]"
- offset="[[_offset]]"
- on-create-clicked="_handleCreateClicked"
- path="[[_path]]">
- <table id="list" class="genericList">
- <tr class="headerRow">
- <th class="name topHeader">Group Name</th>
- <th class="description topHeader">Group Description</th>
- <th class="visibleToAll topHeader">Visible To All</th>
- </tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
- <td>Loading...</td>
- </tr>
- <tbody class$="[[computeLoadingClass(_loading)]]">
- <template is="dom-repeat" items="[[_shownGroups]]">
- <tr class="table">
- <td class="name">
- <a href$="[[_computeGroupUrl(item.group_id)]]">[[item.name]]</a>
- </td>
- <td class="description">[[item.description]]</td>
- <td class="visibleToAll">[[_visibleToAll(item)]]</td>
- </tr>
- </template>
- </tbody>
- </table>
- </gr-list-view>
- <gr-overlay id="createOverlay" with-backdrop>
- <gr-dialog
- id="createDialog"
- class="confirmDialog"
- disabled="[[!_hasNewGroupName]]"
- confirm-label="Create"
- confirm-on-enter
- on-confirm="_handleCreateGroup"
- on-cancel="_handleCloseCreate">
- <div class="header" slot="header">
- Create Group
- </div>
- <div class="main" slot="main">
- <gr-create-group-dialog
- has-new-group-name="{{_hasNewGroupName}}"
- params="[[params]]"
- id="createNewModal"></gr-create-group-dialog>
- </div>
- </gr-dialog>
- </gr-overlay>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-admin-group-list.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
index 96008b7..bdf64de 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
@@ -14,155 +14,171 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
+import '../../../styles/gr-table-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-list-view/gr-list-view.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-create-group-dialog/gr-create-group-dialog.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-admin-group-list_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.ListViewMixin
+ * @extends Polymer.Element
+ */
+class GrAdminGroupList extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.ListViewBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-admin-group-list'; }
+
+ static get properties() {
+ return {
+ /**
+ * URL params passed from the router.
+ */
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
+
+ /**
+ * Offset of currently visible query results.
+ */
+ _offset: Number,
+ _path: {
+ type: String,
+ readOnly: true,
+ value: '/admin/groups',
+ },
+ _hasNewGroupName: Boolean,
+ _createNewCapability: {
+ type: Boolean,
+ value: false,
+ },
+ _groups: Array,
+
+ /**
+ * Because we request one more than the groupsPerPage, _shownGroups
+ * may be one less than _groups.
+ * */
+ _shownGroups: {
+ type: Array,
+ computed: 'computeShownItems(_groups)',
+ },
+
+ _groupsPerPage: {
+ type: Number,
+ value: 25,
+ },
+
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _filter: String,
+ };
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._getCreateGroupCapability();
+ this.fire('title-change', {title: 'Groups'});
+ this._maybeOpenCreateOverlay(this.params);
+ }
+
+ _paramsChanged(params) {
+ this._loading = true;
+ this._filter = this.getFilterValue(params);
+ this._offset = this.getOffsetValue(params);
+
+ return this._getGroups(this._filter, this._groupsPerPage,
+ this._offset);
+ }
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.ListViewMixin
- * @extends Polymer.Element
+ * Opens the create overlay if the route has a hash 'create'
+ *
+ * @param {!Object} params
*/
- class GrAdminGroupList extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.ListViewBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-admin-group-list'; }
-
- static get properties() {
- return {
- /**
- * URL params passed from the router.
- */
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
-
- /**
- * Offset of currently visible query results.
- */
- _offset: Number,
- _path: {
- type: String,
- readOnly: true,
- value: '/admin/groups',
- },
- _hasNewGroupName: Boolean,
- _createNewCapability: {
- type: Boolean,
- value: false,
- },
- _groups: Array,
-
- /**
- * Because we request one more than the groupsPerPage, _shownGroups
- * may be one less than _groups.
- * */
- _shownGroups: {
- type: Array,
- computed: 'computeShownItems(_groups)',
- },
-
- _groupsPerPage: {
- type: Number,
- value: 25,
- },
-
- _loading: {
- type: Boolean,
- value: true,
- },
- _filter: String,
- };
- }
-
- /** @override */
- attached() {
- super.attached();
- this._getCreateGroupCapability();
- this.fire('title-change', {title: 'Groups'});
- this._maybeOpenCreateOverlay(this.params);
- }
-
- _paramsChanged(params) {
- this._loading = true;
- this._filter = this.getFilterValue(params);
- this._offset = this.getOffsetValue(params);
-
- return this._getGroups(this._filter, this._groupsPerPage,
- this._offset);
- }
-
- /**
- * Opens the create overlay if the route has a hash 'create'
- *
- * @param {!Object} params
- */
- _maybeOpenCreateOverlay(params) {
- if (params && params.openCreateModal) {
- this.$.createOverlay.open();
- }
- }
-
- _computeGroupUrl(id) {
- return Gerrit.Nav.getUrlForGroup(id);
- }
-
- _getCreateGroupCapability() {
- return this.$.restAPI.getAccount().then(account => {
- if (!account) { return; }
- return this.$.restAPI.getAccountCapabilities(['createGroup'])
- .then(capabilities => {
- if (capabilities.createGroup) {
- this._createNewCapability = true;
- }
- });
- });
- }
-
- _getGroups(filter, groupsPerPage, offset) {
- this._groups = [];
- return this.$.restAPI.getGroups(filter, groupsPerPage, offset)
- .then(groups => {
- if (!groups) {
- return;
- }
- this._groups = Object.keys(groups)
- .map(key => {
- const group = groups[key];
- group.name = key;
- return group;
- });
- this._loading = false;
- });
- }
-
- _refreshGroupsList() {
- this.$.restAPI.invalidateGroupsCache();
- return this._getGroups(this._filter, this._groupsPerPage,
- this._offset);
- }
-
- _handleCreateGroup() {
- this.$.createNewModal.handleCreateGroup().then(() => {
- this._refreshGroupsList();
- });
- }
-
- _handleCloseCreate() {
- this.$.createOverlay.close();
- }
-
- _handleCreateClicked() {
+ _maybeOpenCreateOverlay(params) {
+ if (params && params.openCreateModal) {
this.$.createOverlay.open();
}
-
- _visibleToAll(item) {
- return item.options.visible_to_all === true ? 'Y' : 'N';
- }
}
- customElements.define(GrAdminGroupList.is, GrAdminGroupList);
-})();
+ _computeGroupUrl(id) {
+ return Gerrit.Nav.getUrlForGroup(id);
+ }
+
+ _getCreateGroupCapability() {
+ return this.$.restAPI.getAccount().then(account => {
+ if (!account) { return; }
+ return this.$.restAPI.getAccountCapabilities(['createGroup'])
+ .then(capabilities => {
+ if (capabilities.createGroup) {
+ this._createNewCapability = true;
+ }
+ });
+ });
+ }
+
+ _getGroups(filter, groupsPerPage, offset) {
+ this._groups = [];
+ return this.$.restAPI.getGroups(filter, groupsPerPage, offset)
+ .then(groups => {
+ if (!groups) {
+ return;
+ }
+ this._groups = Object.keys(groups)
+ .map(key => {
+ const group = groups[key];
+ group.name = key;
+ return group;
+ });
+ this._loading = false;
+ });
+ }
+
+ _refreshGroupsList() {
+ this.$.restAPI.invalidateGroupsCache();
+ return this._getGroups(this._filter, this._groupsPerPage,
+ this._offset);
+ }
+
+ _handleCreateGroup() {
+ this.$.createNewModal.handleCreateGroup().then(() => {
+ this._refreshGroupsList();
+ });
+ }
+
+ _handleCloseCreate() {
+ this.$.createOverlay.close();
+ }
+
+ _handleCreateClicked() {
+ this.$.createOverlay.open();
+ }
+
+ _visibleToAll(item) {
+ return item.options.visible_to_all === true ? 'Y' : 'N';
+ }
+}
+
+customElements.define(GrAdminGroupList.is, GrAdminGroupList);
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.js
new file mode 100644
index 0000000..ffc10d7
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.js
@@ -0,0 +1,60 @@
+/**
+ * @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.js';
+
+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-table-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <gr-list-view create-new="[[_createNewCapability]]" filter="[[_filter]]" items="[[_groups]]" items-per-page="[[_groupsPerPage]]" loading="[[_loading]]" offset="[[_offset]]" on-create-clicked="_handleCreateClicked" path="[[_path]]">
+ <table id="list" class="genericList">
+ <tbody><tr class="headerRow">
+ <th class="name topHeader">Group Name</th>
+ <th class="description topHeader">Group Description</th>
+ <th class="visibleToAll topHeader">Visible To All</th>
+ </tr>
+ <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]">
+ <td>Loading...</td>
+ </tr>
+ </tbody><tbody class\$="[[computeLoadingClass(_loading)]]">
+ <template is="dom-repeat" items="[[_shownGroups]]">
+ <tr class="table">
+ <td class="name">
+ <a href\$="[[_computeGroupUrl(item.group_id)]]">[[item.name]]</a>
+ </td>
+ <td class="description">[[item.description]]</td>
+ <td class="visibleToAll">[[_visibleToAll(item)]]</td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ </gr-list-view>
+ <gr-overlay id="createOverlay" with-backdrop="">
+ <gr-dialog id="createDialog" class="confirmDialog" disabled="[[!_hasNewGroupName]]" confirm-label="Create" confirm-on-enter="" on-confirm="_handleCreateGroup" on-cancel="_handleCloseCreate">
+ <div class="header" slot="header">
+ Create Group
+ </div>
+ <div class="main" slot="main">
+ <gr-create-group-dialog has-new-group-name="{{_hasNewGroupName}}" params="[[params]]" id="createNewModal"></gr-create-group-dialog>
+ </div>
+ </gr-dialog>
+ </gr-overlay>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
index c0558d2..7a32737 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
@@ -19,18 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-admin-group-list</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-
-<link rel="import" href="gr-admin-group-list.html">
-
-<script>void(0);</script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -38,153 +31,154 @@
</template>
</test-fixture>
-<script>
- let counter = 0;
- const groupGenerator = () => {
- return {
- name: `test${++counter}`,
- id: '59b92f35489e62c80d1ab1bf0c2d17843038df8b',
- url: '#/admin/groups/uuid-59b92f35489e62c80d1ab1bf0c2d17843038df8b',
- options: {
- visible_to_all: false,
- },
- description: 'Gerrit Site Administrators',
- group_id: 1,
- owner: 'Administrators',
- owner_id: '7ca042f4d5847936fcb90ca91057673157fd06fc',
- };
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-admin-group-list.js';
+let counter = 0;
+const groupGenerator = () => {
+ return {
+ name: `test${++counter}`,
+ id: '59b92f35489e62c80d1ab1bf0c2d17843038df8b',
+ url: '#/admin/groups/uuid-59b92f35489e62c80d1ab1bf0c2d17843038df8b',
+ options: {
+ visible_to_all: false,
+ },
+ description: 'Gerrit Site Administrators',
+ group_id: 1,
+ owner: 'Administrators',
+ owner_id: '7ca042f4d5847936fcb90ca91057673157fd06fc',
};
+};
- suite('gr-admin-group-list tests', async () => {
- await readyToTest();
- let element;
- let groups;
- let sandbox;
- let value;
+suite('gr-admin-group-list tests', () => {
+ let element;
+ let groups;
+ let sandbox;
+ let value;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('list with groups', () => {
+ setup(done => {
+ groups = _.times(26, groupGenerator);
+
+ stub('gr-rest-api-interface', {
+ getGroups(num, offset) {
+ return Promise.resolve(groups);
+ },
+ });
+
+ element._paramsChanged(value).then(() => { flush(done); });
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('list with groups', () => {
- setup(done => {
- groups = _.times(26, groupGenerator);
-
- stub('gr-rest-api-interface', {
- getGroups(num, offset) {
- return Promise.resolve(groups);
- },
- });
-
- element._paramsChanged(value).then(() => { flush(done); });
- });
-
- test('test for test group in the list', done => {
- flush(() => {
- assert.equal(element._groups[1].name, '1');
- assert.equal(element._groups[1].options.visible_to_all, false);
- done();
- });
- });
-
- test('_shownGroups', () => {
- assert.equal(element._shownGroups.length, 25);
- });
-
- test('_maybeOpenCreateOverlay', () => {
- const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
- element._maybeOpenCreateOverlay();
- assert.isFalse(overlayOpen.called);
- const params = {};
- element._maybeOpenCreateOverlay(params);
- assert.isFalse(overlayOpen.called);
- params.openCreateModal = true;
- element._maybeOpenCreateOverlay(params);
- assert.isTrue(overlayOpen.called);
+ test('test for test group in the list', done => {
+ flush(() => {
+ assert.equal(element._groups[1].name, '1');
+ assert.equal(element._groups[1].options.visible_to_all, false);
+ done();
});
});
- suite('test with less then 25 groups', () => {
- setup(done => {
- groups = _.times(25, groupGenerator);
-
- stub('gr-rest-api-interface', {
- getGroups(num, offset) {
- return Promise.resolve(groups);
- },
- });
-
- element._paramsChanged(value).then(() => { flush(done); });
- });
-
- test('_shownGroups', () => {
- assert.equal(element._shownGroups.length, 25);
- });
+ test('_shownGroups', () => {
+ assert.equal(element._shownGroups.length, 25);
});
- suite('filter', () => {
- test('_paramsChanged', done => {
- sandbox.stub(
- element.$.restAPI,
- 'getGroups',
- () => Promise.resolve(groups));
- const value = {
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(value).then(() => {
- assert.isTrue(element.$.restAPI.getGroups.lastCall
- .calledWithExactly('test', 25, 25));
- done();
- });
+ test('_maybeOpenCreateOverlay', () => {
+ const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
+ element._maybeOpenCreateOverlay();
+ assert.isFalse(overlayOpen.called);
+ const params = {};
+ element._maybeOpenCreateOverlay(params);
+ assert.isFalse(overlayOpen.called);
+ params.openCreateModal = true;
+ element._maybeOpenCreateOverlay(params);
+ assert.isTrue(overlayOpen.called);
+ });
+ });
+
+ suite('test with less then 25 groups', () => {
+ setup(done => {
+ groups = _.times(25, groupGenerator);
+
+ stub('gr-rest-api-interface', {
+ getGroups(num, offset) {
+ return Promise.resolve(groups);
+ },
});
+
+ element._paramsChanged(value).then(() => { flush(done); });
});
- suite('loading', () => {
- test('correct contents are displayed', () => {
- assert.isTrue(element._loading);
- assert.equal(element.computeLoadingClass(element._loading), 'loading');
- assert.equal(getComputedStyle(element.$.loading).display, 'block');
-
- element._loading = false;
- element._groups = _.times(25, groupGenerator);
-
- flushAsynchronousOperations();
- assert.equal(element.computeLoadingClass(element._loading), '');
- assert.equal(getComputedStyle(element.$.loading).display, 'none');
- });
+ test('_shownGroups', () => {
+ assert.equal(element._shownGroups.length, 25);
});
+ });
- suite('create new', () => {
- test('_handleCreateClicked called when create-click fired', () => {
- sandbox.stub(element, '_handleCreateClicked');
- element.shadowRoot
- .querySelector('gr-list-view').fire('create-clicked');
- assert.isTrue(element._handleCreateClicked.called);
- });
-
- test('_handleCreateClicked opens modal', () => {
- const openStub = sandbox.stub(element.$.createOverlay, 'open');
- element._handleCreateClicked();
- assert.isTrue(openStub.called);
- });
-
- test('_handleCreateGroup called when confirm fired', () => {
- sandbox.stub(element, '_handleCreateGroup');
- element.$.createDialog.fire('confirm');
- assert.isTrue(element._handleCreateGroup.called);
- });
-
- test('_handleCloseCreate called when cancel fired', () => {
- sandbox.stub(element, '_handleCloseCreate');
- element.$.createDialog.fire('cancel');
- assert.isTrue(element._handleCloseCreate.called);
+ suite('filter', () => {
+ test('_paramsChanged', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'getGroups',
+ () => Promise.resolve(groups));
+ const value = {
+ filter: 'test',
+ offset: 25,
+ };
+ element._paramsChanged(value).then(() => {
+ assert.isTrue(element.$.restAPI.getGroups.lastCall
+ .calledWithExactly('test', 25, 25));
+ done();
});
});
});
+
+ suite('loading', () => {
+ test('correct contents are displayed', () => {
+ assert.isTrue(element._loading);
+ assert.equal(element.computeLoadingClass(element._loading), 'loading');
+ assert.equal(getComputedStyle(element.$.loading).display, 'block');
+
+ element._loading = false;
+ element._groups = _.times(25, groupGenerator);
+
+ flushAsynchronousOperations();
+ assert.equal(element.computeLoadingClass(element._loading), '');
+ assert.equal(getComputedStyle(element.$.loading).display, 'none');
+ });
+ });
+
+ suite('create new', () => {
+ test('_handleCreateClicked called when create-click fired', () => {
+ sandbox.stub(element, '_handleCreateClicked');
+ element.shadowRoot
+ .querySelector('gr-list-view').fire('create-clicked');
+ assert.isTrue(element._handleCreateClicked.called);
+ });
+
+ test('_handleCreateClicked opens modal', () => {
+ const openStub = sandbox.stub(element.$.createOverlay, 'open');
+ element._handleCreateClicked();
+ assert.isTrue(openStub.called);
+ });
+
+ test('_handleCreateGroup called when confirm fired', () => {
+ sandbox.stub(element, '_handleCreateGroup');
+ element.$.createDialog.fire('confirm');
+ assert.isTrue(element._handleCreateGroup.called);
+ });
+
+ test('_handleCloseCreate called when cancel fired', () => {
+ sandbox.stub(element, '_handleCloseCreate');
+ element.$.createDialog.fire('cancel');
+ assert.isTrue(element._handleCloseCreate.called);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
deleted file mode 100644
index aae11d3..0000000
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
+++ /dev/null
@@ -1,200 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../styles/gr-menu-page-styles.html">
-<link rel="import" href="../../../styles/gr-page-nav-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-dropdown-list/gr-dropdown-list.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-<link rel="import" href="../../shared/gr-page-nav/gr-page-nav.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-admin-group-list/gr-admin-group-list.html">
-<link rel="import" href="../gr-group/gr-group.html">
-<link rel="import" href="../gr-group-audit-log/gr-group-audit-log.html">
-<link rel="import" href="../gr-group-members/gr-group-members.html">
-<link rel="import" href="../gr-plugin-list/gr-plugin-list.html">
-<link rel="import" href="../gr-repo/gr-repo.html">
-<link rel="import" href="../gr-repo-access/gr-repo-access.html">
-<link rel="import" href="../gr-repo-commands/gr-repo-commands.html">
-<link rel="import" href="../gr-repo-dashboards/gr-repo-dashboards.html">
-<link rel="import" href="../gr-repo-detail-list/gr-repo-detail-list.html">
-<link rel="import" href="../gr-repo-list/gr-repo-list.html">
-
-<dom-module id="gr-admin-view">
- <template>
- <style include="shared-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>
- <style include="gr-page-nav-styles">
- gr-dropdown-list {
- --trigger-style: {
- text-transform: none;
- }
- }
- .breadcrumbText {
- /* Same as dropdown trigger so chevron spacing is consistent. */
- padding: 5px 4px;
- }
- iron-icon {
- margin: 0 var(--spacing-xs);
- }
- .breadcrumb {
- align-items: center;
- display: flex;
- }
- .mainHeader {
- align-items: baseline;
- border-bottom: 1px solid var(--border-color);
- display: flex;
- }
- .selectText {
- display: none;
- }
- .selectText.show {
- display: inline-block;
- }
- main.breadcrumbs:not(.table) {
- margin-top: var(--spacing-l);
- }
- </style>
- <gr-page-nav class="navStyles">
- <ul class="sectionContent">
- <template id="adminNav" is="dom-repeat" items="[[_filteredLinks]]">
- <li class$="sectionTitle [[_computeSelectedClass(item.view, params)]]">
- <a class="title" href="[[_computeLinkURL(item)]]"
- rel="noopener">[[item.name]]</a>
- </li>
- <template is="dom-repeat" items="[[item.children]]" as="child">
- <li class$="[[_computeSelectedClass(child.view, params)]]">
- <a href$="[[_computeLinkURL(child)]]"
- rel="noopener">[[child.name]]</a>
- </li>
- </template>
- <template is="dom-if" if="[[item.subsection]]">
- <!--If a section has a subsection, render that.-->
- <li class$="[[_computeSelectedClass(item.subsection.view, params)]]">
- <a class="title" href$="[[_computeLinkURL(item.subsection)]]"
- rel="noopener">
- [[item.subsection.name]]</a>
- </li>
- <!--Loop through the links in the sub-section.-->
- <template is="dom-repeat"
- items="[[item.subsection.children]]" as="child">
- <li class$="subsectionItem [[_computeSelectedClass(child.view, params, child.detailType)]]">
- <a href$="[[_computeLinkURL(child)]]">[[child.name]]</a>
- </li>
- </template>
- </template>
- </template>
- </ul>
- </gr-page-nav>
- <template is="dom-if" if="[[_subsectionLinks.length]]">
- <section class="mainHeader">
- <span class="breadcrumb">
- <span class="breadcrumbText">[[_breadcrumbParentName]]</span>
- <iron-icon icon="gr-icons:chevron-right"></iron-icon>
- </span>
- <gr-dropdown-list
- lowercase
- id="pageSelect"
- value="[[_computeSelectValue(params)]]"
- items="[[_subsectionLinks]]"
- on-value-change="_handleSubsectionChange">
- </gr-dropdown-list>
- </section>
- </template>
- <template is="dom-if" if="[[_showRepoList]]" restamp="true">
- <main class="table">
- <gr-repo-list class="table" params="[[params]]"></gr-repo-list>
- </main>
- </template>
- <template is="dom-if" if="[[_showGroupList]]" restamp="true">
- <main class="table">
- <gr-admin-group-list class="table" params="[[params]]">
- </gr-admin-group-list>
- </main>
- </template>
- <template is="dom-if" if="[[_showPluginList]]" restamp="true">
- <main class="table">
- <gr-plugin-list class="table" params="[[params]]"></gr-plugin-list>
- </main>
- </template>
- <template is="dom-if" if="[[_showRepoMain]]" restamp="true">
- <main class="breadcrumbs">
- <gr-repo repo="[[params.repo]]"></gr-repo>
- </main>
- </template>
- <template is="dom-if" if="[[_showGroup]]" restamp="true">
- <main class="breadcrumbs">
- <gr-group
- group-id="[[params.groupId]]"
- on-name-changed="_updateGroupName"></gr-group>
- </main>
- </template>
- <template is="dom-if" if="[[_showGroupMembers]]" restamp="true">
- <main class="breadcrumbs">
- <gr-group-members
- group-id="[[params.groupId]]"></gr-group-members>
- </main>
- </template>
- <template is="dom-if" if="[[_showRepoDetailList]]" restamp="true">
- <main class="table breadcrumbs">
- <gr-repo-detail-list
- params="[[params]]"
- class="table"></gr-repo-detail-list>
- </main>
- </template>
- <template is="dom-if" if="[[_showGroupAuditLog]]" restamp="true">
- <main class="table breadcrumbs">
- <gr-group-audit-log
- group-id="[[params.groupId]]"
- class="table"></gr-group-audit-log>
- </main>
- </template>
- <template is="dom-if" if="[[_showRepoCommands]]" restamp="true">
- <main class="breadcrumbs">
- <gr-repo-commands
- repo="[[params.repo]]"></gr-repo-commands>
- </main>
- </template>
- <template is="dom-if" if="[[_showRepoAccess]]" restamp="true">
- <main class="breadcrumbs">
- <gr-repo-access
- path="[[path]]"
- repo="[[params.repo]]"></gr-repo-access>
- </main>
- </template>
- <template is="dom-if" if="[[_showRepoDashboards]]" restamp="true">
- <main class="table breadcrumbs">
- <gr-repo-dashboards repo="[[params.repo]]"></gr-repo-dashboards>
- </main>
- </template>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
- </template>
- <script src="gr-admin-view.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index e300c90..d03df39 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -14,281 +14,310 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../styles/gr-menu-page-styles.js';
+import '../../../styles/gr-page-nav-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-dropdown-list/gr-dropdown-list.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import '../../shared/gr-page-nav/gr-page-nav.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-admin-group-list/gr-admin-group-list.js';
+import '../gr-group/gr-group.js';
+import '../gr-group-audit-log/gr-group-audit-log.js';
+import '../gr-group-members/gr-group-members.js';
+import '../gr-plugin-list/gr-plugin-list.js';
+import '../gr-repo/gr-repo.js';
+import '../gr-repo-access/gr-repo-access.js';
+import '../gr-repo-commands/gr-repo-commands.js';
+import '../gr-repo-dashboards/gr-repo-dashboards.js';
+import '../gr-repo-detail-list/gr-repo-detail-list.js';
+import '../gr-repo-list/gr-repo-list.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-admin-view_html.js';
- /**
- * @appliesMixin Gerrit.AdminNavMixin
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrAdminView extends Polymer.mixinBehaviors( [
- Gerrit.AdminNavBehavior,
- Gerrit.BaseUrlBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-admin-view'; }
+const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
- static get properties() {
- return {
- /** @type {?} */
- params: Object,
- path: String,
- adminView: String,
+/**
+ * @appliesMixin Gerrit.AdminNavMixin
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrAdminView extends mixinBehaviors( [
+ Gerrit.AdminNavBehavior,
+ Gerrit.BaseUrlBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _breadcrumbParentName: String,
- _repoName: String,
- _groupId: {
- type: Number,
- observer: '_computeGroupName',
- },
- _groupIsInternal: Boolean,
- _groupName: String,
- _groupOwner: {
- type: Boolean,
- value: false,
- },
- _subsectionLinks: Array,
- _filteredLinks: Array,
- _showDownload: {
- type: Boolean,
- value: false,
- },
- _isAdmin: {
- type: Boolean,
- value: false,
- },
- _showGroup: Boolean,
- _showGroupAuditLog: Boolean,
- _showGroupList: Boolean,
- _showGroupMembers: Boolean,
- _showRepoAccess: Boolean,
- _showRepoCommands: Boolean,
- _showRepoDashboards: Boolean,
- _showRepoDetailList: Boolean,
- _showRepoMain: Boolean,
- _showRepoList: Boolean,
- _showPluginList: Boolean,
- };
- }
+ static get is() { return 'gr-admin-view'; }
- static get observers() {
- return [
- '_paramsChanged(params)',
- ];
- }
+ static get properties() {
+ return {
+ /** @type {?} */
+ params: Object,
+ path: String,
+ adminView: String,
- /** @override */
- attached() {
- super.attached();
- this.reload();
- }
-
- reload() {
- const promises = [
- this.$.restAPI.getAccount(),
- Gerrit.awaitPluginsLoaded(),
- ];
- return Promise.all(promises).then(result => {
- this._account = result[0];
- let options;
- if (this._repoName) {
- options = {repoName: this._repoName};
- } else if (this._groupId) {
- options = {
- groupId: this._groupId,
- groupName: this._groupName,
- groupIsInternal: this._groupIsInternal,
- isAdmin: this._isAdmin,
- groupOwner: this._groupOwner,
- };
- }
-
- return this.getAdminLinks(this._account,
- this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI),
- this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI),
- options)
- .then(res => {
- this._filteredLinks = res.links;
- this._breadcrumbParentName = res.expandedSection ?
- res.expandedSection.name : '';
-
- if (!res.expandedSection) {
- this._subsectionLinks = [];
- return;
- }
- this._subsectionLinks = [res.expandedSection]
- .concat(res.expandedSection.children).map(section => {
- return {
- text: !section.detailType ? 'Home' : section.name,
- value: section.view + (section.detailType || ''),
- view: section.view,
- url: section.url,
- detailType: section.detailType,
- parent: this._groupId || this._repoName || '',
- };
- });
- });
- });
- }
-
- _computeSelectValue(params) {
- if (!params || !params.view) { return; }
- return params.view + (params.detail || '');
- }
-
- _selectedIsCurrentPage(selected) {
- return (selected.parent === (this._repoName || this._groupId) &&
- selected.view === this.params.view &&
- selected.detailType === this.params.detail);
- }
-
- _handleSubsectionChange(e) {
- const selected = this._subsectionLinks
- .find(section => section.value === e.detail.value);
-
- // This is when it gets set initially.
- if (this._selectedIsCurrentPage(selected)) {
- return;
- }
- Gerrit.Nav.navigateToRelativeUrl(selected.url);
- }
-
- _paramsChanged(params) {
- const isGroupView = params.view === Gerrit.Nav.View.GROUP;
- const isRepoView = params.view === Gerrit.Nav.View.REPO;
- const isAdminView = params.view === Gerrit.Nav.View.ADMIN;
-
- this.set('_showGroup', isGroupView && !params.detail);
- this.set('_showGroupAuditLog', isGroupView &&
- params.detail === Gerrit.Nav.GroupDetailView.LOG);
- this.set('_showGroupMembers', isGroupView &&
- params.detail === Gerrit.Nav.GroupDetailView.MEMBERS);
-
- this.set('_showGroupList', isAdminView &&
- params.adminView === 'gr-admin-group-list');
-
- this.set('_showRepoAccess', isRepoView &&
- params.detail === Gerrit.Nav.RepoDetailView.ACCESS);
- this.set('_showRepoCommands', isRepoView &&
- params.detail === Gerrit.Nav.RepoDetailView.COMMANDS);
- this.set('_showRepoDetailList', isRepoView &&
- (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES ||
- params.detail === Gerrit.Nav.RepoDetailView.TAGS));
- this.set('_showRepoDashboards', isRepoView &&
- params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS);
- this.set('_showRepoMain', isRepoView && !params.detail);
-
- this.set('_showRepoList', isAdminView &&
- params.adminView === 'gr-repo-list');
-
- this.set('_showPluginList', isAdminView &&
- params.adminView === 'gr-plugin-list');
-
- let needsReload = false;
- if (params.repo !== this._repoName) {
- this._repoName = params.repo || '';
- // Reloads the admin menu.
- needsReload = true;
- }
- if (params.groupId !== this._groupId) {
- this._groupId = params.groupId || '';
- // Reloads the admin menu.
- needsReload = true;
- }
- if (this._breadcrumbParentName && !params.groupId && !params.repo) {
- needsReload = true;
- }
- if (!needsReload) { return; }
- this.reload();
- }
-
- // TODO (beckysiegel): Update these functions after router abstraction is
- // updated. They are currently copied from gr-dropdown (and should be
- // updated there as well once complete).
- _computeURLHelper(host, path) {
- return '//' + host + this.getBaseUrl() + path;
- }
-
- _computeRelativeURL(path) {
- const host = window.location.host;
- return this._computeURLHelper(host, path);
- }
-
- _computeLinkURL(link) {
- if (!link || typeof link.url === 'undefined') { return ''; }
- if (link.target || !link.noBaseUrl) {
- return link.url;
- }
- return this._computeRelativeURL(link.url);
- }
-
- /**
- * @param {string} itemView
- * @param {Object} params
- * @param {string=} opt_detailType
- */
- _computeSelectedClass(itemView, params, opt_detailType) {
- if (!params) return '';
- // Group params are structured differently from admin params. Compute
- // selected differently for groups.
- // TODO(wyatta): Simplify this when all routes work like group params.
- if (params.view === Gerrit.Nav.View.GROUP &&
- itemView === Gerrit.Nav.View.GROUP) {
- if (!params.detail && !opt_detailType) { return 'selected'; }
- if (params.detail === opt_detailType) { return 'selected'; }
- return '';
- }
-
- if (params.view === Gerrit.Nav.View.REPO &&
- itemView === Gerrit.Nav.View.REPO) {
- if (!params.detail && !opt_detailType) { return 'selected'; }
- if (params.detail === opt_detailType) { return 'selected'; }
- return '';
- }
-
- if (params.detailType && params.detailType !== opt_detailType) {
- return '';
- }
- return itemView === params.adminView ? 'selected' : '';
- }
-
- _computeGroupName(groupId) {
- if (!groupId) { return ''; }
-
- const promises = [];
- this.$.restAPI.getGroupConfig(groupId).then(group => {
- if (!group || !group.name) { return; }
-
- this._groupName = group.name;
- this._groupIsInternal = !!group.id.match(INTERNAL_GROUP_REGEX);
- this.reload();
-
- promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
- this._isAdmin = isAdmin;
- }));
-
- promises.push(this.$.restAPI.getIsGroupOwner(group.name).then(
- isOwner => {
- this._groupOwner = isOwner;
- }));
-
- return Promise.all(promises).then(() => {
- this.reload();
- });
- });
- }
-
- _updateGroupName(e) {
- this._groupName = e.detail.name;
- this.reload();
- }
+ _breadcrumbParentName: String,
+ _repoName: String,
+ _groupId: {
+ type: Number,
+ observer: '_computeGroupName',
+ },
+ _groupIsInternal: Boolean,
+ _groupName: String,
+ _groupOwner: {
+ type: Boolean,
+ value: false,
+ },
+ _subsectionLinks: Array,
+ _filteredLinks: Array,
+ _showDownload: {
+ type: Boolean,
+ value: false,
+ },
+ _isAdmin: {
+ type: Boolean,
+ value: false,
+ },
+ _showGroup: Boolean,
+ _showGroupAuditLog: Boolean,
+ _showGroupList: Boolean,
+ _showGroupMembers: Boolean,
+ _showRepoAccess: Boolean,
+ _showRepoCommands: Boolean,
+ _showRepoDashboards: Boolean,
+ _showRepoDetailList: Boolean,
+ _showRepoMain: Boolean,
+ _showRepoList: Boolean,
+ _showPluginList: Boolean,
+ };
}
- customElements.define(GrAdminView.is, GrAdminView);
-})();
+ static get observers() {
+ return [
+ '_paramsChanged(params)',
+ ];
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this.reload();
+ }
+
+ reload() {
+ const promises = [
+ this.$.restAPI.getAccount(),
+ Gerrit.awaitPluginsLoaded(),
+ ];
+ return Promise.all(promises).then(result => {
+ this._account = result[0];
+ let options;
+ if (this._repoName) {
+ options = {repoName: this._repoName};
+ } else if (this._groupId) {
+ options = {
+ groupId: this._groupId,
+ groupName: this._groupName,
+ groupIsInternal: this._groupIsInternal,
+ isAdmin: this._isAdmin,
+ groupOwner: this._groupOwner,
+ };
+ }
+
+ return this.getAdminLinks(this._account,
+ this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI),
+ this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI),
+ options)
+ .then(res => {
+ this._filteredLinks = res.links;
+ this._breadcrumbParentName = res.expandedSection ?
+ res.expandedSection.name : '';
+
+ if (!res.expandedSection) {
+ this._subsectionLinks = [];
+ return;
+ }
+ this._subsectionLinks = [res.expandedSection]
+ .concat(res.expandedSection.children).map(section => {
+ return {
+ text: !section.detailType ? 'Home' : section.name,
+ value: section.view + (section.detailType || ''),
+ view: section.view,
+ url: section.url,
+ detailType: section.detailType,
+ parent: this._groupId || this._repoName || '',
+ };
+ });
+ });
+ });
+ }
+
+ _computeSelectValue(params) {
+ if (!params || !params.view) { return; }
+ return params.view + (params.detail || '');
+ }
+
+ _selectedIsCurrentPage(selected) {
+ return (selected.parent === (this._repoName || this._groupId) &&
+ selected.view === this.params.view &&
+ selected.detailType === this.params.detail);
+ }
+
+ _handleSubsectionChange(e) {
+ const selected = this._subsectionLinks
+ .find(section => section.value === e.detail.value);
+
+ // This is when it gets set initially.
+ if (this._selectedIsCurrentPage(selected)) {
+ return;
+ }
+ Gerrit.Nav.navigateToRelativeUrl(selected.url);
+ }
+
+ _paramsChanged(params) {
+ const isGroupView = params.view === Gerrit.Nav.View.GROUP;
+ const isRepoView = params.view === Gerrit.Nav.View.REPO;
+ const isAdminView = params.view === Gerrit.Nav.View.ADMIN;
+
+ this.set('_showGroup', isGroupView && !params.detail);
+ this.set('_showGroupAuditLog', isGroupView &&
+ params.detail === Gerrit.Nav.GroupDetailView.LOG);
+ this.set('_showGroupMembers', isGroupView &&
+ params.detail === Gerrit.Nav.GroupDetailView.MEMBERS);
+
+ this.set('_showGroupList', isAdminView &&
+ params.adminView === 'gr-admin-group-list');
+
+ this.set('_showRepoAccess', isRepoView &&
+ params.detail === Gerrit.Nav.RepoDetailView.ACCESS);
+ this.set('_showRepoCommands', isRepoView &&
+ params.detail === Gerrit.Nav.RepoDetailView.COMMANDS);
+ this.set('_showRepoDetailList', isRepoView &&
+ (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES ||
+ params.detail === Gerrit.Nav.RepoDetailView.TAGS));
+ this.set('_showRepoDashboards', isRepoView &&
+ params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS);
+ this.set('_showRepoMain', isRepoView && !params.detail);
+
+ this.set('_showRepoList', isAdminView &&
+ params.adminView === 'gr-repo-list');
+
+ this.set('_showPluginList', isAdminView &&
+ params.adminView === 'gr-plugin-list');
+
+ let needsReload = false;
+ if (params.repo !== this._repoName) {
+ this._repoName = params.repo || '';
+ // Reloads the admin menu.
+ needsReload = true;
+ }
+ if (params.groupId !== this._groupId) {
+ this._groupId = params.groupId || '';
+ // Reloads the admin menu.
+ needsReload = true;
+ }
+ if (this._breadcrumbParentName && !params.groupId && !params.repo) {
+ needsReload = true;
+ }
+ if (!needsReload) { return; }
+ this.reload();
+ }
+
+ // TODO (beckysiegel): Update these functions after router abstraction is
+ // updated. They are currently copied from gr-dropdown (and should be
+ // updated there as well once complete).
+ _computeURLHelper(host, path) {
+ return '//' + host + this.getBaseUrl() + path;
+ }
+
+ _computeRelativeURL(path) {
+ const host = window.location.host;
+ return this._computeURLHelper(host, path);
+ }
+
+ _computeLinkURL(link) {
+ if (!link || typeof link.url === 'undefined') { return ''; }
+ if (link.target || !link.noBaseUrl) {
+ return link.url;
+ }
+ return this._computeRelativeURL(link.url);
+ }
+
+ /**
+ * @param {string} itemView
+ * @param {Object} params
+ * @param {string=} opt_detailType
+ */
+ _computeSelectedClass(itemView, params, opt_detailType) {
+ if (!params) return '';
+ // Group params are structured differently from admin params. Compute
+ // selected differently for groups.
+ // TODO(wyatta): Simplify this when all routes work like group params.
+ if (params.view === Gerrit.Nav.View.GROUP &&
+ itemView === Gerrit.Nav.View.GROUP) {
+ if (!params.detail && !opt_detailType) { return 'selected'; }
+ if (params.detail === opt_detailType) { return 'selected'; }
+ return '';
+ }
+
+ if (params.view === Gerrit.Nav.View.REPO &&
+ itemView === Gerrit.Nav.View.REPO) {
+ if (!params.detail && !opt_detailType) { return 'selected'; }
+ if (params.detail === opt_detailType) { return 'selected'; }
+ return '';
+ }
+
+ if (params.detailType && params.detailType !== opt_detailType) {
+ return '';
+ }
+ return itemView === params.adminView ? 'selected' : '';
+ }
+
+ _computeGroupName(groupId) {
+ if (!groupId) { return ''; }
+
+ const promises = [];
+ this.$.restAPI.getGroupConfig(groupId).then(group => {
+ if (!group || !group.name) { return; }
+
+ this._groupName = group.name;
+ this._groupIsInternal = !!group.id.match(INTERNAL_GROUP_REGEX);
+ this.reload();
+
+ promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
+ this._isAdmin = isAdmin;
+ }));
+
+ promises.push(this.$.restAPI.getIsGroupOwner(group.name).then(
+ isOwner => {
+ this._groupOwner = isOwner;
+ }));
+
+ return Promise.all(promises).then(() => {
+ this.reload();
+ });
+ });
+ }
+
+ _updateGroupName(e) {
+ this._groupName = e.detail.name;
+ this.reload();
+ }
+}
+
+customElements.define(GrAdminView.is, GrAdminView);
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.js
new file mode 100644
index 0000000..0bc9431
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.js
@@ -0,0 +1,153 @@
+/**
+ * @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.js';
+
+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-menu-page-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="gr-page-nav-styles">
+ gr-dropdown-list {
+ --trigger-style: {
+ text-transform: none;
+ }
+ }
+ .breadcrumbText {
+ /* Same as dropdown trigger so chevron spacing is consistent. */
+ padding: 5px 4px;
+ }
+ iron-icon {
+ margin: 0 var(--spacing-xs);
+ }
+ .breadcrumb {
+ align-items: center;
+ display: flex;
+ }
+ .mainHeader {
+ align-items: baseline;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ }
+ .selectText {
+ display: none;
+ }
+ .selectText.show {
+ display: inline-block;
+ }
+ main.breadcrumbs:not(.table) {
+ margin-top: var(--spacing-l);
+ }
+ </style>
+ <gr-page-nav class="navStyles">
+ <ul class="sectionContent">
+ <template id="adminNav" is="dom-repeat" items="[[_filteredLinks]]">
+ <li class\$="sectionTitle [[_computeSelectedClass(item.view, params)]]">
+ <a class="title" href="[[_computeLinkURL(item)]]" rel="noopener">[[item.name]]</a>
+ </li>
+ <template is="dom-repeat" items="[[item.children]]" as="child">
+ <li class\$="[[_computeSelectedClass(child.view, params)]]">
+ <a href\$="[[_computeLinkURL(child)]]" rel="noopener">[[child.name]]</a>
+ </li>
+ </template>
+ <template is="dom-if" if="[[item.subsection]]">
+ <!--If a section has a subsection, render that.-->
+ <li class\$="[[_computeSelectedClass(item.subsection.view, params)]]">
+ <a class="title" href\$="[[_computeLinkURL(item.subsection)]]" rel="noopener">
+ [[item.subsection.name]]</a>
+ </li>
+ <!--Loop through the links in the sub-section.-->
+ <template is="dom-repeat" items="[[item.subsection.children]]" as="child">
+ <li class\$="subsectionItem [[_computeSelectedClass(child.view, params, child.detailType)]]">
+ <a href\$="[[_computeLinkURL(child)]]">[[child.name]]</a>
+ </li>
+ </template>
+ </template>
+ </template>
+ </ul>
+ </gr-page-nav>
+ <template is="dom-if" if="[[_subsectionLinks.length]]">
+ <section class="mainHeader">
+ <span class="breadcrumb">
+ <span class="breadcrumbText">[[_breadcrumbParentName]]</span>
+ <iron-icon icon="gr-icons:chevron-right"></iron-icon>
+ </span>
+ <gr-dropdown-list lowercase="" id="pageSelect" value="[[_computeSelectValue(params)]]" items="[[_subsectionLinks]]" on-value-change="_handleSubsectionChange">
+ </gr-dropdown-list>
+ </section>
+ </template>
+ <template is="dom-if" if="[[_showRepoList]]" restamp="true">
+ <main class="table">
+ <gr-repo-list class="table" params="[[params]]"></gr-repo-list>
+ </main>
+ </template>
+ <template is="dom-if" if="[[_showGroupList]]" restamp="true">
+ <main class="table">
+ <gr-admin-group-list class="table" params="[[params]]">
+ </gr-admin-group-list>
+ </main>
+ </template>
+ <template is="dom-if" if="[[_showPluginList]]" restamp="true">
+ <main class="table">
+ <gr-plugin-list class="table" params="[[params]]"></gr-plugin-list>
+ </main>
+ </template>
+ <template is="dom-if" if="[[_showRepoMain]]" restamp="true">
+ <main class="breadcrumbs">
+ <gr-repo repo="[[params.repo]]"></gr-repo>
+ </main>
+ </template>
+ <template is="dom-if" if="[[_showGroup]]" restamp="true">
+ <main class="breadcrumbs">
+ <gr-group group-id="[[params.groupId]]" on-name-changed="_updateGroupName"></gr-group>
+ </main>
+ </template>
+ <template is="dom-if" if="[[_showGroupMembers]]" restamp="true">
+ <main class="breadcrumbs">
+ <gr-group-members group-id="[[params.groupId]]"></gr-group-members>
+ </main>
+ </template>
+ <template is="dom-if" if="[[_showRepoDetailList]]" restamp="true">
+ <main class="table breadcrumbs">
+ <gr-repo-detail-list params="[[params]]" class="table"></gr-repo-detail-list>
+ </main>
+ </template>
+ <template is="dom-if" if="[[_showGroupAuditLog]]" restamp="true">
+ <main class="table breadcrumbs">
+ <gr-group-audit-log group-id="[[params.groupId]]" class="table"></gr-group-audit-log>
+ </main>
+ </template>
+ <template is="dom-if" if="[[_showRepoCommands]]" restamp="true">
+ <main class="breadcrumbs">
+ <gr-repo-commands repo="[[params.repo]]"></gr-repo-commands>
+ </main>
+ </template>
+ <template is="dom-if" if="[[_showRepoAccess]]" restamp="true">
+ <main class="breadcrumbs">
+ <gr-repo-access path="[[path]]" repo="[[params.repo]]"></gr-repo-access>
+ </main>
+ </template>
+ <template is="dom-if" if="[[_showRepoDashboards]]" restamp="true">
+ <main class="table breadcrumbs">
+ <gr-repo-dashboards repo="[[params.repo]]"></gr-repo-dashboards>
+ </main>
+ </template>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
index 416099d..7f989e7 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-admin-view</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-admin-view.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,645 +30,647 @@
</template>
</test-fixture>
-<script>
- suite('gr-admin-view tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-admin-view.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-admin-view tests', () => {
+ let element;
+ let sandbox;
- setup(done => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- stub('gr-rest-api-interface', {
- getProjectConfig() {
- return Promise.resolve({});
- },
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ stub('gr-rest-api-interface', {
+ getProjectConfig() {
+ return Promise.resolve({});
+ },
+ });
+ const pluginsLoaded = Promise.resolve();
+ sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(pluginsLoaded);
+ pluginsLoaded.then(() => flush(done));
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_computeURLHelper', () => {
+ const path = '/test';
+ const host = 'http://www.testsite.com';
+ const computedPath = element._computeURLHelper(host, path);
+ assert.equal(computedPath, '//http://www.testsite.com/test');
+ });
+
+ test('link URLs', () => {
+ assert.equal(
+ element._computeLinkURL({url: '/test', noBaseUrl: true}),
+ '//' + window.location.host + '/test');
+
+ sandbox.stub(element, 'getBaseUrl').returns('/foo');
+ assert.equal(
+ element._computeLinkURL({url: '/test', noBaseUrl: true}),
+ '//' + window.location.host + '/foo/test');
+ assert.equal(element._computeLinkURL({url: '/test'}), '/test');
+ assert.equal(
+ element._computeLinkURL({url: '/test', target: '_blank'}),
+ '/test');
+ });
+
+ test('current page gets selected and is displayed', () => {
+ element._filteredLinks = [{
+ name: 'Repositories',
+ url: '/admin/repos',
+ view: 'gr-repo-list',
+ }];
+
+ element.params = {
+ view: 'admin',
+ adminView: 'gr-repo-list',
+ };
+
+ flushAsynchronousOperations();
+ assert.equal(dom(element.root).querySelectorAll(
+ '.selected').length, 1);
+ assert.ok(element.shadowRoot
+ .querySelector('gr-repo-list'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-admin-create-repo'));
+ });
+
+ test('_filteredLinks admin', done => {
+ sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
+ name: 'test-user',
+ }));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccountCapabilities',
+ () => Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ })
+ );
+ element.reload().then(() => {
+ assert.equal(element._filteredLinks.length, 3);
+
+ // Repos
+ assert.isNotOk(element._filteredLinks[0].subsection);
+
+ // Groups
+ assert.isNotOk(element._filteredLinks[0].subsection);
+
+ // Plugins
+ assert.isNotOk(element._filteredLinks[0].subsection);
+ done();
+ });
+ });
+
+ test('_filteredLinks non admin authenticated', done => {
+ sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
+ name: 'test-user',
+ }));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccountCapabilities',
+ () => Promise.resolve({})
+ );
+ element.reload().then(() => {
+ assert.equal(element._filteredLinks.length, 2);
+
+ // Repos
+ assert.isNotOk(element._filteredLinks[0].subsection);
+
+ // Groups
+ assert.isNotOk(element._filteredLinks[0].subsection);
+ done();
+ });
+ });
+
+ test('_filteredLinks non admin unathenticated', done => {
+ element.reload().then(() => {
+ assert.equal(element._filteredLinks.length, 1);
+
+ // Repos
+ assert.isNotOk(element._filteredLinks[0].subsection);
+ done();
+ });
+ });
+
+ test('_filteredLinks from plugin', () => {
+ sandbox.stub(element.$.jsAPI, 'getAdminMenuLinks').returns([
+ {text: 'internal link text', url: '/internal/link/url'},
+ {text: 'external link text', url: 'http://external/link/url'},
+ ]);
+ return element.reload().then(() => {
+ assert.equal(element._filteredLinks.length, 3);
+ assert.deepEqual(element._filteredLinks[1], {
+ capability: null,
+ url: '/internal/link/url',
+ name: 'internal link text',
+ noBaseUrl: true,
+ view: null,
+ viewableToAll: true,
+ target: null,
});
- const pluginsLoaded = Promise.resolve();
- sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(pluginsLoaded);
- pluginsLoaded.then(() => flush(done));
+ assert.deepEqual(element._filteredLinks[2], {
+ capability: null,
+ url: 'http://external/link/url',
+ name: 'external link text',
+ noBaseUrl: false,
+ view: null,
+ viewableToAll: true,
+ target: '_blank',
+ });
});
+ });
- teardown(() => {
- sandbox.restore();
- });
-
- test('_computeURLHelper', () => {
- const path = '/test';
- const host = 'http://www.testsite.com';
- const computedPath = element._computeURLHelper(host, path);
- assert.equal(computedPath, '//http://www.testsite.com/test');
- });
-
- test('link URLs', () => {
+ test('Repo shows up in nav', done => {
+ element._repoName = 'Test Repo';
+ sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
+ name: 'test-user',
+ }));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccountCapabilities',
+ () => Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ }));
+ element.reload().then(() => {
+ flushAsynchronousOperations();
+ assert.equal(dom(element.root)
+ .querySelectorAll('.sectionTitle').length, 3);
+ assert.equal(element.shadowRoot
+ .querySelector('.breadcrumbText').innerText, 'Test Repo');
assert.equal(
- element._computeLinkURL({url: '/test', noBaseUrl: true}),
- '//' + window.location.host + '/test');
-
- sandbox.stub(element, 'getBaseUrl').returns('/foo');
- assert.equal(
- element._computeLinkURL({url: '/test', noBaseUrl: true}),
- '//' + window.location.host + '/foo/test');
- assert.equal(element._computeLinkURL({url: '/test'}), '/test');
- assert.equal(
- element._computeLinkURL({url: '/test', target: '_blank'}),
- '/test');
+ element.shadowRoot.querySelector('#pageSelect').items.length,
+ 6
+ );
+ done();
});
+ });
- test('current page gets selected and is displayed', () => {
- element._filteredLinks = [{
+ test('Group shows up in nav', done => {
+ element._groupId = 'a15262';
+ element._groupName = 'my-group';
+ element._groupIsInternal = true;
+ element._isAdmin = true;
+ element._groupOwner = false;
+ sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
+ name: 'test-user',
+ }));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccountCapabilities',
+ () => Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ }));
+ element.reload().then(() => {
+ flushAsynchronousOperations();
+ assert.equal(element._filteredLinks.length, 3);
+
+ // Repos
+ assert.isNotOk(element._filteredLinks[0].subsection);
+
+ // Groups
+ assert.equal(element._filteredLinks[1].subsection.children.length, 2);
+ assert.equal(element._filteredLinks[1].subsection.name, 'my-group');
+
+ // Plugins
+ assert.isNotOk(element._filteredLinks[2].subsection);
+ done();
+ });
+ });
+
+ test('Nav is reloaded when repo changes', () => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccountCapabilities',
+ () => Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ }));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccount',
+ () => Promise.resolve({_id: 1}));
+ sandbox.stub(element, 'reload');
+ element.params = {repo: 'Test Repo', adminView: 'gr-repo'};
+ assert.equal(element.reload.callCount, 1);
+ element.params = {repo: 'Test Repo 2',
+ adminView: 'gr-repo'};
+ assert.equal(element.reload.callCount, 2);
+ });
+
+ test('Nav is reloaded when group changes', () => {
+ sandbox.stub(element, '_computeGroupName');
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccountCapabilities',
+ () => Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ }));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccount',
+ () => Promise.resolve({_id: 1}));
+ sandbox.stub(element, 'reload');
+ element.params = {groupId: '1', adminView: 'gr-group'};
+ assert.equal(element.reload.callCount, 1);
+ });
+
+ test('Nav is reloaded when group name changes', done => {
+ const newName = 'newName';
+ sandbox.stub(element, '_computeGroupName');
+ sandbox.stub(element, 'reload', () => {
+ assert.equal(element._groupName, newName);
+ assert.isTrue(element.reload.called);
+ done();
+ });
+ element.params = {group: 1, view: Gerrit.Nav.View.GROUP};
+ element._groupName = 'oldName';
+ flushAsynchronousOperations();
+ element.shadowRoot
+ .querySelector('gr-group').fire('name-changed', {name: newName});
+ });
+
+ test('dropdown displays if there is a subsection', () => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('.mainHeader'));
+ element._subsectionLinks = [
+ {
+ text: 'Home',
+ value: 'repo',
+ view: 'repo',
+ url: '',
+ parent: 'my-repo',
+ detailType: undefined,
+ },
+ ];
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('.mainHeader'));
+ element._subsectionLinks = undefined;
+ flushAsynchronousOperations();
+ assert.equal(
+ getComputedStyle(element.shadowRoot
+ .querySelector('.mainHeader')).display,
+ 'none');
+ });
+
+ test('Dropdown only triggers navigation on explicit select', done => {
+ element._repoName = 'my-repo';
+ element.params = {
+ repo: 'my-repo',
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.ACCESS,
+ };
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccountCapabilities',
+ () => Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ }));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccount',
+ () => Promise.resolve({_id: 1}));
+ flushAsynchronousOperations();
+ const expectedFilteredLinks = [
+ {
name: 'Repositories',
+ noBaseUrl: true,
url: '/admin/repos',
view: 'gr-repo-list',
- }];
-
- element.params = {
- view: 'admin',
- adminView: 'gr-repo-list',
- };
-
- flushAsynchronousOperations();
- assert.equal(Polymer.dom(element.root).querySelectorAll(
- '.selected').length, 1);
- assert.ok(element.shadowRoot
- .querySelector('gr-repo-list'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-admin-create-repo'));
- });
-
- test('_filteredLinks admin', done => {
- sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
- name: 'test-user',
- }));
- sandbox.stub(
- element.$.restAPI,
- 'getAccountCapabilities',
- () => Promise.resolve({
- createGroup: true,
- createProject: true,
- viewPlugins: true,
- })
- );
- element.reload().then(() => {
- assert.equal(element._filteredLinks.length, 3);
-
- // Repos
- assert.isNotOk(element._filteredLinks[0].subsection);
-
- // Groups
- assert.isNotOk(element._filteredLinks[0].subsection);
-
- // Plugins
- assert.isNotOk(element._filteredLinks[0].subsection);
- done();
- });
- });
-
- test('_filteredLinks non admin authenticated', done => {
- sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
- name: 'test-user',
- }));
- sandbox.stub(
- element.$.restAPI,
- 'getAccountCapabilities',
- () => Promise.resolve({})
- );
- element.reload().then(() => {
- assert.equal(element._filteredLinks.length, 2);
-
- // Repos
- assert.isNotOk(element._filteredLinks[0].subsection);
-
- // Groups
- assert.isNotOk(element._filteredLinks[0].subsection);
- done();
- });
- });
-
- test('_filteredLinks non admin unathenticated', done => {
- element.reload().then(() => {
- assert.equal(element._filteredLinks.length, 1);
-
- // Repos
- assert.isNotOk(element._filteredLinks[0].subsection);
- done();
- });
- });
-
- test('_filteredLinks from plugin', () => {
- sandbox.stub(element.$.jsAPI, 'getAdminMenuLinks').returns([
- {text: 'internal link text', url: '/internal/link/url'},
- {text: 'external link text', url: 'http://external/link/url'},
- ]);
- return element.reload().then(() => {
- assert.equal(element._filteredLinks.length, 3);
- assert.deepEqual(element._filteredLinks[1], {
- capability: null,
- url: '/internal/link/url',
- name: 'internal link text',
- noBaseUrl: true,
- view: null,
- viewableToAll: true,
- target: null,
- });
- assert.deepEqual(element._filteredLinks[2], {
- capability: null,
- url: 'http://external/link/url',
- name: 'external link text',
- noBaseUrl: false,
- view: null,
- viewableToAll: true,
- target: '_blank',
- });
- });
- });
-
- test('Repo shows up in nav', done => {
- element._repoName = 'Test Repo';
- sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
- name: 'test-user',
- }));
- sandbox.stub(
- element.$.restAPI,
- 'getAccountCapabilities',
- () => Promise.resolve({
- createGroup: true,
- createProject: true,
- viewPlugins: true,
- }));
- element.reload().then(() => {
- flushAsynchronousOperations();
- assert.equal(Polymer.dom(element.root)
- .querySelectorAll('.sectionTitle').length, 3);
- assert.equal(element.shadowRoot
- .querySelector('.breadcrumbText').innerText, 'Test Repo');
- assert.equal(
- element.shadowRoot.querySelector('#pageSelect').items.length,
- 6
- );
- done();
- });
- });
-
- test('Group shows up in nav', done => {
- element._groupId = 'a15262';
- element._groupName = 'my-group';
- element._groupIsInternal = true;
- element._isAdmin = true;
- element._groupOwner = false;
- sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
- name: 'test-user',
- }));
- sandbox.stub(
- element.$.restAPI,
- 'getAccountCapabilities',
- () => Promise.resolve({
- createGroup: true,
- createProject: true,
- viewPlugins: true,
- }));
- element.reload().then(() => {
- flushAsynchronousOperations();
- assert.equal(element._filteredLinks.length, 3);
-
- // Repos
- assert.isNotOk(element._filteredLinks[0].subsection);
-
- // Groups
- assert.equal(element._filteredLinks[1].subsection.children.length, 2);
- assert.equal(element._filteredLinks[1].subsection.name, 'my-group');
-
- // Plugins
- assert.isNotOk(element._filteredLinks[2].subsection);
- done();
- });
- });
-
- test('Nav is reloaded when repo changes', () => {
- sandbox.stub(
- element.$.restAPI,
- 'getAccountCapabilities',
- () => Promise.resolve({
- createGroup: true,
- createProject: true,
- viewPlugins: true,
- }));
- sandbox.stub(
- element.$.restAPI,
- 'getAccount',
- () => Promise.resolve({_id: 1}));
- sandbox.stub(element, 'reload');
- element.params = {repo: 'Test Repo', adminView: 'gr-repo'};
- assert.equal(element.reload.callCount, 1);
- element.params = {repo: 'Test Repo 2',
- adminView: 'gr-repo'};
- assert.equal(element.reload.callCount, 2);
- });
-
- test('Nav is reloaded when group changes', () => {
- sandbox.stub(element, '_computeGroupName');
- sandbox.stub(
- element.$.restAPI,
- 'getAccountCapabilities',
- () => Promise.resolve({
- createGroup: true,
- createProject: true,
- viewPlugins: true,
- }));
- sandbox.stub(
- element.$.restAPI,
- 'getAccount',
- () => Promise.resolve({_id: 1}));
- sandbox.stub(element, 'reload');
- element.params = {groupId: '1', adminView: 'gr-group'};
- assert.equal(element.reload.callCount, 1);
- });
-
- test('Nav is reloaded when group name changes', done => {
- const newName = 'newName';
- sandbox.stub(element, '_computeGroupName');
- sandbox.stub(element, 'reload', () => {
- assert.equal(element._groupName, newName);
- assert.isTrue(element.reload.called);
- done();
- });
- element.params = {group: 1, view: Gerrit.Nav.View.GROUP};
- element._groupName = 'oldName';
- flushAsynchronousOperations();
- element.shadowRoot
- .querySelector('gr-group').fire('name-changed', {name: newName});
- });
-
- test('dropdown displays if there is a subsection', () => {
- assert.isNotOk(element.shadowRoot
- .querySelector('.mainHeader'));
- element._subsectionLinks = [
- {
- text: 'Home',
- value: 'repo',
+ viewableToAll: true,
+ subsection: {
+ name: 'my-repo',
view: 'repo',
url: '',
- parent: 'my-repo',
- detailType: undefined,
+ children: [
+ {
+ name: 'Access',
+ view: 'repo',
+ detailType: 'access',
+ url: '',
+ },
+ {
+ name: 'Commands',
+ view: 'repo',
+ detailType: 'commands',
+ url: '',
+ },
+ {
+ name: 'Branches',
+ view: 'repo',
+ detailType: 'branches',
+ url: '',
+ },
+ {
+ name: 'Tags',
+ view: 'repo',
+ detailType: 'tags',
+ url: '',
+ },
+ {
+ name: 'Dashboards',
+ view: 'repo',
+ detailType: 'dashboards',
+ url: '',
+ },
+ ],
},
- ];
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('.mainHeader'));
- element._subsectionLinks = undefined;
- flushAsynchronousOperations();
- assert.equal(
- getComputedStyle(element.shadowRoot
- .querySelector('.mainHeader')).display,
- 'none');
- });
-
- test('Dropdown only triggers navigation on explicit select', done => {
- element._repoName = 'my-repo';
- element.params = {
- repo: 'my-repo',
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.ACCESS,
- };
- sandbox.stub(
- element.$.restAPI,
- 'getAccountCapabilities',
- () => Promise.resolve({
- createGroup: true,
- createProject: true,
- viewPlugins: true,
- }));
- sandbox.stub(
- element.$.restAPI,
- 'getAccount',
- () => Promise.resolve({_id: 1}));
- flushAsynchronousOperations();
- const expectedFilteredLinks = [
- {
- name: 'Repositories',
- noBaseUrl: true,
- url: '/admin/repos',
- view: 'gr-repo-list',
- viewableToAll: true,
- subsection: {
- name: 'my-repo',
- view: 'repo',
- url: '',
- children: [
- {
- name: 'Access',
- view: 'repo',
- detailType: 'access',
- url: '',
- },
- {
- name: 'Commands',
- view: 'repo',
- detailType: 'commands',
- url: '',
- },
- {
- name: 'Branches',
- view: 'repo',
- detailType: 'branches',
- url: '',
- },
- {
- name: 'Tags',
- view: 'repo',
- detailType: 'tags',
- url: '',
- },
- {
- name: 'Dashboards',
- view: 'repo',
- detailType: 'dashboards',
- url: '',
- },
- ],
- },
- },
- {
- name: 'Groups',
- section: 'Groups',
- noBaseUrl: true,
- url: '/admin/groups',
- view: 'gr-admin-group-list',
- },
- {
- name: 'Plugins',
- capability: 'viewPlugins',
- section: 'Plugins',
- noBaseUrl: true,
- url: '/admin/plugins',
- view: 'gr-plugin-list',
- },
- ];
- const expectedSubsectionLinks = [
- {
- text: 'Home',
- value: 'repo',
- view: 'repo',
- url: '',
- parent: 'my-repo',
- detailType: undefined,
- },
- {
- text: 'Access',
- value: 'repoaccess',
- view: 'repo',
- url: '',
- detailType: 'access',
- parent: 'my-repo',
- },
- {
- text: 'Commands',
- value: 'repocommands',
- view: 'repo',
- url: '',
- detailType: 'commands',
- parent: 'my-repo',
- },
- {
- text: 'Branches',
- value: 'repobranches',
- view: 'repo',
- url: '',
- detailType: 'branches',
- parent: 'my-repo',
- },
- {
- text: 'Tags',
- value: 'repotags',
- view: 'repo',
- url: '',
- detailType: 'tags',
- parent: 'my-repo',
- },
- {
- text: 'Dashboards',
- value: 'repodashboards',
- view: 'repo',
- url: '',
- detailType: 'dashboards',
- parent: 'my-repo',
- },
- ];
- sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
- sandbox.spy(element, '_selectedIsCurrentPage');
- sandbox.spy(element, '_handleSubsectionChange');
- element.reload().then(() => {
- assert.deepEqual(element._filteredLinks, expectedFilteredLinks);
- assert.deepEqual(element._subsectionLinks, expectedSubsectionLinks);
- assert.equal(
- element.shadowRoot.querySelector('#pageSelect').value,
- 'repoaccess'
- );
- assert.isTrue(element._selectedIsCurrentPage.calledOnce);
- // Doesn't trigger navigation from the page select menu.
- assert.isFalse(Gerrit.Nav.navigateToRelativeUrl.called);
-
- // When explicitly changed, navigation is called
- element.shadowRoot.querySelector('#pageSelect').value = 'repo';
- assert.isTrue(element._selectedIsCurrentPage.calledTwice);
- assert.isTrue(Gerrit.Nav.navigateToRelativeUrl.calledOnce);
- done();
- });
- });
-
- test('_selectedIsCurrentPage', () => {
- element._repoName = 'my-repo';
- element.params = {view: 'repo', repo: 'my-repo'};
- const selected = {
+ },
+ {
+ name: 'Groups',
+ section: 'Groups',
+ noBaseUrl: true,
+ url: '/admin/groups',
+ view: 'gr-admin-group-list',
+ },
+ {
+ name: 'Plugins',
+ capability: 'viewPlugins',
+ section: 'Plugins',
+ noBaseUrl: true,
+ url: '/admin/plugins',
+ view: 'gr-plugin-list',
+ },
+ ];
+ const expectedSubsectionLinks = [
+ {
+ text: 'Home',
+ value: 'repo',
view: 'repo',
- detailType: undefined,
+ url: '',
parent: 'my-repo',
- };
- assert.isTrue(element._selectedIsCurrentPage(selected));
- selected.parent = 'my-second-repo';
- assert.isFalse(element._selectedIsCurrentPage(selected));
- selected.detailType = 'detailType';
- assert.isFalse(element._selectedIsCurrentPage(selected));
+ detailType: undefined,
+ },
+ {
+ text: 'Access',
+ value: 'repoaccess',
+ view: 'repo',
+ url: '',
+ detailType: 'access',
+ parent: 'my-repo',
+ },
+ {
+ text: 'Commands',
+ value: 'repocommands',
+ view: 'repo',
+ url: '',
+ detailType: 'commands',
+ parent: 'my-repo',
+ },
+ {
+ text: 'Branches',
+ value: 'repobranches',
+ view: 'repo',
+ url: '',
+ detailType: 'branches',
+ parent: 'my-repo',
+ },
+ {
+ text: 'Tags',
+ value: 'repotags',
+ view: 'repo',
+ url: '',
+ detailType: 'tags',
+ parent: 'my-repo',
+ },
+ {
+ text: 'Dashboards',
+ value: 'repodashboards',
+ view: 'repo',
+ url: '',
+ detailType: 'dashboards',
+ parent: 'my-repo',
+ },
+ ];
+ sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
+ sandbox.spy(element, '_selectedIsCurrentPage');
+ sandbox.spy(element, '_handleSubsectionChange');
+ element.reload().then(() => {
+ assert.deepEqual(element._filteredLinks, expectedFilteredLinks);
+ assert.deepEqual(element._subsectionLinks, expectedSubsectionLinks);
+ assert.equal(
+ element.shadowRoot.querySelector('#pageSelect').value,
+ 'repoaccess'
+ );
+ assert.isTrue(element._selectedIsCurrentPage.calledOnce);
+ // Doesn't trigger navigation from the page select menu.
+ assert.isFalse(Gerrit.Nav.navigateToRelativeUrl.called);
+
+ // When explicitly changed, navigation is called
+ element.shadowRoot.querySelector('#pageSelect').value = 'repo';
+ assert.isTrue(element._selectedIsCurrentPage.calledTwice);
+ assert.isTrue(Gerrit.Nav.navigateToRelativeUrl.calledOnce);
+ done();
+ });
+ });
+
+ test('_selectedIsCurrentPage', () => {
+ element._repoName = 'my-repo';
+ element.params = {view: 'repo', repo: 'my-repo'};
+ const selected = {
+ view: 'repo',
+ detailType: undefined,
+ parent: 'my-repo',
+ };
+ assert.isTrue(element._selectedIsCurrentPage(selected));
+ selected.parent = 'my-second-repo';
+ assert.isFalse(element._selectedIsCurrentPage(selected));
+ selected.detailType = 'detailType';
+ assert.isFalse(element._selectedIsCurrentPage(selected));
+ });
+
+ suite('_computeSelectedClass', () => {
+ setup(() => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccountCapabilities',
+ () => Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ }));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccount',
+ () => Promise.resolve({_id: 1}));
+
+ return element.reload();
});
- suite('_computeSelectedClass', () => {
+ suite('repos', () => {
setup(() => {
- sandbox.stub(
- element.$.restAPI,
- 'getAccountCapabilities',
- () => Promise.resolve({
- createGroup: true,
- createProject: true,
- viewPlugins: true,
- }));
- sandbox.stub(
- element.$.restAPI,
- 'getAccount',
- () => Promise.resolve({_id: 1}));
+ stub('gr-repo-access', {
+ _repoChanged: () => {},
+ });
+ });
+ test('repo list', () => {
+ element.params = {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-repo-list',
+ openCreateModal: false,
+ };
+ flushAsynchronousOperations();
+ const selected = element.shadowRoot
+ .querySelector('gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent.trim(), 'Repositories');
+ });
+
+ test('repo', () => {
+ element.params = {
+ view: Gerrit.Nav.View.REPO,
+ repoName: 'foo',
+ };
+ element._repoName = 'foo';
+ return element.reload().then(() => {
+ flushAsynchronousOperations();
+ const selected = element.shadowRoot
+ .querySelector('gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent.trim(), 'foo');
+ });
+ });
+
+ test('repo access', () => {
+ element.params = {
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.ACCESS,
+ repoName: 'foo',
+ };
+ element._repoName = 'foo';
+ return element.reload().then(() => {
+ flushAsynchronousOperations();
+ const selected = element.shadowRoot
+ .querySelector('gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent.trim(), 'Access');
+ });
+ });
+
+ test('repo dashboards', () => {
+ element.params = {
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
+ repoName: 'foo',
+ };
+ element._repoName = 'foo';
+ return element.reload().then(() => {
+ flushAsynchronousOperations();
+ const selected = element.shadowRoot
+ .querySelector('gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent.trim(), 'Dashboards');
+ });
+ });
+ });
+
+ suite('groups', () => {
+ setup(() => {
+ stub('gr-group', {
+ _loadGroup: () => Promise.resolve({}),
+ });
+ stub('gr-group-members', {
+ _loadGroupDetails: () => {},
+ });
+
+ sandbox.stub(element.$.restAPI, 'getGroupConfig')
+ .returns(Promise.resolve({
+ name: 'foo',
+ id: 'c0f83e941ce90caea30e6ad88f0d4ea0e841a7a9',
+ }));
+ sandbox.stub(element.$.restAPI, 'getIsGroupOwner')
+ .returns(Promise.resolve(true));
return element.reload();
});
- suite('repos', () => {
- setup(() => {
- stub('gr-repo-access', {
- _repoChanged: () => {},
- });
- });
+ test('group list', () => {
+ element.params = {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-admin-group-list',
+ openCreateModal: false,
+ };
+ flushAsynchronousOperations();
+ const selected = element.shadowRoot
+ .querySelector('gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent.trim(), 'Groups');
+ });
- test('repo list', () => {
- element.params = {
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-repo-list',
- openCreateModal: false,
- };
+ test('internal group', () => {
+ element.params = {
+ view: Gerrit.Nav.View.GROUP,
+ groupId: 1234,
+ };
+ element._groupName = 'foo';
+ return element.reload().then(() => {
flushAsynchronousOperations();
+ const subsectionItems = dom(element.root)
+ .querySelectorAll('.subsectionItem');
+ assert.equal(subsectionItems.length, 2);
+ assert.isTrue(element._groupIsInternal);
const selected = element.shadowRoot
.querySelector('gr-page-nav .selected');
assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'Repositories');
- });
-
- test('repo', () => {
- element.params = {
- view: Gerrit.Nav.View.REPO,
- repoName: 'foo',
- };
- element._repoName = 'foo';
- return element.reload().then(() => {
- flushAsynchronousOperations();
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'foo');
- });
- });
-
- test('repo access', () => {
- element.params = {
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.ACCESS,
- repoName: 'foo',
- };
- element._repoName = 'foo';
- return element.reload().then(() => {
- flushAsynchronousOperations();
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'Access');
- });
- });
-
- test('repo dashboards', () => {
- element.params = {
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
- repoName: 'foo',
- };
- element._repoName = 'foo';
- return element.reload().then(() => {
- flushAsynchronousOperations();
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'Dashboards');
- });
+ assert.equal(selected.textContent.trim(), 'foo');
});
});
- suite('groups', () => {
- setup(() => {
- stub('gr-group', {
- _loadGroup: () => Promise.resolve({}),
- });
- stub('gr-group-members', {
- _loadGroupDetails: () => {},
- });
-
- sandbox.stub(element.$.restAPI, 'getGroupConfig')
- .returns(Promise.resolve({
- name: 'foo',
- id: 'c0f83e941ce90caea30e6ad88f0d4ea0e841a7a9',
- }));
- sandbox.stub(element.$.restAPI, 'getIsGroupOwner')
- .returns(Promise.resolve(true));
- return element.reload();
+ test('external group', () => {
+ element.$.restAPI.getGroupConfig.restore();
+ sandbox.stub(element.$.restAPI, 'getGroupConfig')
+ .returns(Promise.resolve({
+ name: 'foo',
+ id: 'external-id',
+ }));
+ element.params = {
+ view: Gerrit.Nav.View.GROUP,
+ groupId: 1234,
+ };
+ element._groupName = 'foo';
+ return element.reload().then(() => {
+ flushAsynchronousOperations();
+ const subsectionItems = dom(element.root)
+ .querySelectorAll('.subsectionItem');
+ assert.equal(subsectionItems.length, 0);
+ assert.isFalse(element._groupIsInternal);
+ const selected = element.shadowRoot
+ .querySelector('gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent.trim(), 'foo');
});
+ });
- test('group list', () => {
- element.params = {
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-group-list',
- openCreateModal: false,
- };
+ test('group members', () => {
+ element.params = {
+ view: Gerrit.Nav.View.GROUP,
+ detail: Gerrit.Nav.GroupDetailView.MEMBERS,
+ groupId: 1234,
+ };
+ element._groupName = 'foo';
+ return element.reload().then(() => {
flushAsynchronousOperations();
const selected = element.shadowRoot
.querySelector('gr-page-nav .selected');
assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'Groups');
- });
-
- test('internal group', () => {
- element.params = {
- view: Gerrit.Nav.View.GROUP,
- groupId: 1234,
- };
- element._groupName = 'foo';
- return element.reload().then(() => {
- flushAsynchronousOperations();
- const subsectionItems = Polymer.dom(element.root)
- .querySelectorAll('.subsectionItem');
- assert.equal(subsectionItems.length, 2);
- assert.isTrue(element._groupIsInternal);
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'foo');
- });
- });
-
- test('external group', () => {
- element.$.restAPI.getGroupConfig.restore();
- sandbox.stub(element.$.restAPI, 'getGroupConfig')
- .returns(Promise.resolve({
- name: 'foo',
- id: 'external-id',
- }));
- element.params = {
- view: Gerrit.Nav.View.GROUP,
- groupId: 1234,
- };
- element._groupName = 'foo';
- return element.reload().then(() => {
- flushAsynchronousOperations();
- const subsectionItems = Polymer.dom(element.root)
- .querySelectorAll('.subsectionItem');
- assert.equal(subsectionItems.length, 0);
- assert.isFalse(element._groupIsInternal);
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'foo');
- });
- });
-
- test('group members', () => {
- element.params = {
- view: Gerrit.Nav.View.GROUP,
- detail: Gerrit.Nav.GroupDetailView.MEMBERS,
- groupId: 1234,
- };
- element._groupName = 'foo';
- return element.reload().then(() => {
- flushAsynchronousOperations();
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'Members');
- });
+ assert.equal(selected.textContent.trim(), 'Members');
});
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html
deleted file mode 100644
index 9d8ee18..0000000
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html
+++ /dev/null
@@ -1,48 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-confirm-delete-item-dialog">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- width: 30em;
- }
- </style>
- <gr-dialog
- confirm-label="Delete [[_computeItemName(itemType)]]"
- confirm-on-enter
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap">
- <div class="header" slot="header">[[_computeItemName(itemType)]] Deletion</div>
- <div class="main" slot="main">
- <label for="branchInput">
- Do you really want to delete the following [[_computeItemName(itemType)]]?
- </label>
- <div>
- [[item]]
- </div>
- </div>
- </gr-dialog>
- </template>
- <script src="gr-confirm-delete-item-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
index 3fde410..5ebf00e 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
@@ -14,67 +14,76 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const DETAIL_TYPES = {
- BRANCHES: 'branches',
- ID: 'id',
- TAGS: 'tags',
- };
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../../styles/shared-styles.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-confirm-delete-item-dialog_html.js';
+
+const DETAIL_TYPES = {
+ BRANCHES: 'branches',
+ ID: 'id',
+ TAGS: 'tags',
+};
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrConfirmDeleteItemDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-delete-item-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
*/
- class GrConfirmDeleteItemDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-confirm-delete-item-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
- item: String,
- itemType: String,
- };
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('confirm', null, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', null, {bubbles: false});
- }
-
- _computeItemName(detailType) {
- if (detailType === DETAIL_TYPES.BRANCHES) {
- return 'Branch';
- } else if (detailType === DETAIL_TYPES.TAGS) {
- return 'Tag';
- } else if (detailType === DETAIL_TYPES.ID) {
- return 'ID';
- }
- }
+ static get properties() {
+ return {
+ item: String,
+ itemType: String,
+ };
}
- customElements.define(GrConfirmDeleteItemDialog.is,
- GrConfirmDeleteItemDialog);
-})();
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('confirm', null, {bubbles: false});
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', null, {bubbles: false});
+ }
+
+ _computeItemName(detailType) {
+ if (detailType === DETAIL_TYPES.BRANCHES) {
+ return 'Branch';
+ } else if (detailType === DETAIL_TYPES.TAGS) {
+ return 'Tag';
+ } else if (detailType === DETAIL_TYPES.ID) {
+ return 'ID';
+ }
+ }
+}
+
+customElements.define(GrConfirmDeleteItemDialog.is,
+ GrConfirmDeleteItemDialog);
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.js
new file mode 100644
index 0000000..12dc29c
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.js
@@ -0,0 +1,37 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ width: 30em;
+ }
+ </style>
+ <gr-dialog confirm-label="Delete [[_computeItemName(itemType)]]" confirm-on-enter="" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap">
+ <div class="header" slot="header">[[_computeItemName(itemType)]] Deletion</div>
+ <div class="main" slot="main">
+ <label for="branchInput">
+ Do you really want to delete the following [[_computeItemName(itemType)]]?
+ </label>
+ <div>
+ [[item]]
+ </div>
+ </div>
+ </gr-dialog>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html
index b937e76..daf37bf 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-confirm-delete-item-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-confirm-delete-item-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,53 +30,54 @@
</template>
</test-fixture>
-<script>
- suite('gr-confirm-delete-item-dialog tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-confirm-delete-item-dialog.js';
+suite('gr-confirm-delete-item-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('_handleConfirmTap', () => {
- const confirmHandler = sandbox.stub();
- element.addEventListener('confirm', confirmHandler);
- sandbox.spy(element, '_handleConfirmTap');
- element.shadowRoot
- .querySelector('gr-dialog').fire('confirm');
- assert.isTrue(confirmHandler.called);
- assert.isTrue(confirmHandler.calledOnce);
- assert.isTrue(element._handleConfirmTap.called);
- assert.isTrue(element._handleConfirmTap.calledOnce);
- });
-
- test('_handleCancelTap', () => {
- const cancelHandler = sandbox.stub();
- element.addEventListener('cancel', cancelHandler);
- sandbox.spy(element, '_handleCancelTap');
- element.shadowRoot
- .querySelector('gr-dialog').fire('cancel');
- assert.isTrue(cancelHandler.called);
- assert.isTrue(cancelHandler.calledOnce);
- assert.isTrue(element._handleCancelTap.called);
- assert.isTrue(element._handleCancelTap.calledOnce);
- });
-
- test('_computeItemName function for branches', () => {
- assert.deepEqual(element._computeItemName('branches'), 'Branch');
- assert.notEqual(element._computeItemName('branches'), 'Tag');
- });
-
- test('_computeItemName function for tags', () => {
- assert.deepEqual(element._computeItemName('tags'), 'Tag');
- assert.notEqual(element._computeItemName('tags'), 'Branch');
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_handleConfirmTap', () => {
+ const confirmHandler = sandbox.stub();
+ element.addEventListener('confirm', confirmHandler);
+ sandbox.spy(element, '_handleConfirmTap');
+ element.shadowRoot
+ .querySelector('gr-dialog').fire('confirm');
+ assert.isTrue(confirmHandler.called);
+ assert.isTrue(confirmHandler.calledOnce);
+ assert.isTrue(element._handleConfirmTap.called);
+ assert.isTrue(element._handleConfirmTap.calledOnce);
+ });
+
+ test('_handleCancelTap', () => {
+ const cancelHandler = sandbox.stub();
+ element.addEventListener('cancel', cancelHandler);
+ sandbox.spy(element, '_handleCancelTap');
+ element.shadowRoot
+ .querySelector('gr-dialog').fire('cancel');
+ assert.isTrue(cancelHandler.called);
+ assert.isTrue(cancelHandler.calledOnce);
+ assert.isTrue(element._handleCancelTap.called);
+ assert.isTrue(element._handleCancelTap.calledOnce);
+ });
+
+ test('_computeItemName function for branches', () => {
+ assert.deepEqual(element._computeItemName('branches'), 'Branch');
+ assert.notEqual(element._computeItemName('branches'), 'Tag');
+ });
+
+ test('_computeItemName function for tags', () => {
+ assert.deepEqual(element._computeItemName('tags'), 'Tag');
+ assert.notEqual(element._computeItemName('tags'), 'Branch');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
deleted file mode 100644
index 1d6e706..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
+++ /dev/null
@@ -1,128 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-
-<dom-module id="gr-create-change-dialog">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- input:not([type="checkbox"]),
- gr-autocomplete,
- iron-autogrow-textarea {
- width: 100%;
- }
- .value {
- width: 32em;
- }
- .hide {
- display: none;
- }
- @media only screen and (max-width: 40em) {
- .value {
- width: 29em;
- }
- }
- </style>
- <div class="gr-form-styles">
- <section class$="[[_computeBranchClass(baseChange)]]">
- <span class="title">Select branch for new change</span>
- <span class="value">
- <gr-autocomplete
- id="branchInput"
- text="{{branch}}"
- query="[[_query]]"
- placeholder="Destination branch">
- </gr-autocomplete>
- </span>
- </section>
- <section class$="[[_computeBranchClass(baseChange)]]">
- <span class="title">Provide base commit sha1 for change</span>
- <span class="value">
- <iron-input
- maxlength="40"
- placeholder="(optional)"
- bind-value="{{baseCommit}}">
- <input
- is="iron-input"
- id="baseCommitInput"
- maxlength="40"
- placeholder="(optional)"
- bind-value="{{baseCommit}}">
- </iron-input>
- </span>
- </section>
- <section>
- <span class="title">Enter topic for new change</span>
- <span class="value">
- <iron-input
- maxlength="1024"
- placeholder="(optional)"
- bind-value="{{topic}}">
- <input
- is="iron-input"
- id="tagNameInput"
- maxlength="1024"
- placeholder="(optional)"
- bind-value="{{topic}}">
- </iron-input>
- </span>
- </section>
- <section id="description">
- <span class="title">Description</span>
- <span class="value">
- <iron-autogrow-textarea
- id="messageInput"
- class="message"
- autocomplete="on"
- rows="4"
- max-rows="15"
- bind-value="{{subject}}"
- placeholder="Insert the description of the change.">
- </iron-autogrow-textarea>
- </span>
- </section>
- <section class$="[[_computePrivateSectionClass(_privateChangesEnabled)]]">
- <label
- class="title"
- for="privateChangeCheckBox">Private change</label>
- <span class="value">
- <input
- type="checkbox"
- id="privateChangeCheckBox"
- checked$="[[_formatBooleanString(privateByDefault)]]">
- </span>
- </section>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-create-change-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
index 3b85304..94de228 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
@@ -14,148 +14,166 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
- const SUGGESTIONS_LIMIT = 15;
- const REF_PREFIX = 'refs/heads/';
+import '@polymer/iron-input/iron-input.js';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-create-change-dialog_html.js';
+const SUGGESTIONS_LIMIT = 15;
+const REF_PREFIX = 'refs/heads/';
+
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrCreateChangeDialog extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
/**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
+ * Unused in this element, but called by other elements in tests
+ * e.g gr-repo-commands_test.
*/
- class GrCreateChangeDialog extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- /**
- * Unused in this element, but called by other elements in tests
- * e.g gr-repo-commands_test.
- */
- Gerrit.FireBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-create-change-dialog'; }
+ Gerrit.FireBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- static get properties() {
- return {
- repoName: String,
- branch: String,
- /** @type {?} */
- _repoConfig: Object,
- subject: String,
- topic: String,
- _query: {
- type: Function,
- value() {
- return this._getRepoBranchesSuggestions.bind(this);
- },
+ static get is() { return 'gr-create-change-dialog'; }
+
+ static get properties() {
+ return {
+ repoName: String,
+ branch: String,
+ /** @type {?} */
+ _repoConfig: Object,
+ subject: String,
+ topic: String,
+ _query: {
+ type: Function,
+ value() {
+ return this._getRepoBranchesSuggestions.bind(this);
},
- baseChange: String,
- baseCommit: String,
- privateByDefault: String,
- canCreate: {
- type: Boolean,
- notify: true,
- value: false,
- },
- _privateChangesEnabled: Boolean,
- };
+ },
+ baseChange: String,
+ baseCommit: String,
+ privateByDefault: String,
+ canCreate: {
+ type: Boolean,
+ notify: true,
+ value: false,
+ },
+ _privateChangesEnabled: Boolean,
+ };
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ if (!this.repoName) { return Promise.resolve(); }
+
+ const promises = [];
+
+ promises.push(this.$.restAPI.getProjectConfig(this.repoName)
+ .then(config => {
+ this.privateByDefault = config.private_by_default;
+ }));
+
+ promises.push(this.$.restAPI.getConfig().then(config => {
+ if (!config) { return; }
+
+ this._privateConfig = config && config.change &&
+ config.change.disable_private_changes;
+ }));
+
+ return Promise.all(promises);
+ }
+
+ static get observers() {
+ return [
+ '_allowCreate(branch, subject)',
+ ];
+ }
+
+ _computeBranchClass(baseChange) {
+ return baseChange ? 'hide' : '';
+ }
+
+ _allowCreate(branch, subject) {
+ this.canCreate = !!branch && !!subject;
+ }
+
+ handleCreateChange() {
+ const isPrivate = this.$.privateChangeCheckBox.checked;
+ const isWip = true;
+ return this.$.restAPI.createChange(this.repoName, this.branch,
+ this.subject, this.topic, isPrivate, isWip, this.baseChange,
+ this.baseCommit || null)
+ .then(changeCreated => {
+ if (!changeCreated) { return; }
+ Gerrit.Nav.navigateToChange(changeCreated);
+ });
+ }
+
+ _getRepoBranchesSuggestions(input) {
+ if (input.startsWith(REF_PREFIX)) {
+ input = input.substring(REF_PREFIX.length);
}
-
- /** @override */
- attached() {
- super.attached();
- if (!this.repoName) { return Promise.resolve(); }
-
- const promises = [];
-
- promises.push(this.$.restAPI.getProjectConfig(this.repoName)
- .then(config => {
- this.privateByDefault = config.private_by_default;
- }));
-
- promises.push(this.$.restAPI.getConfig().then(config => {
- if (!config) { return; }
-
- this._privateConfig = config && config.change &&
- config.change.disable_private_changes;
- }));
-
- return Promise.all(promises);
- }
-
- static get observers() {
- return [
- '_allowCreate(branch, subject)',
- ];
- }
-
- _computeBranchClass(baseChange) {
- return baseChange ? 'hide' : '';
- }
-
- _allowCreate(branch, subject) {
- this.canCreate = !!branch && !!subject;
- }
-
- handleCreateChange() {
- const isPrivate = this.$.privateChangeCheckBox.checked;
- const isWip = true;
- return this.$.restAPI.createChange(this.repoName, this.branch,
- this.subject, this.topic, isPrivate, isWip, this.baseChange,
- this.baseCommit || null)
- .then(changeCreated => {
- if (!changeCreated) { return; }
- Gerrit.Nav.navigateToChange(changeCreated);
- });
- }
-
- _getRepoBranchesSuggestions(input) {
- if (input.startsWith(REF_PREFIX)) {
- input = input.substring(REF_PREFIX.length);
- }
- return this.$.restAPI.getRepoBranches(
- input, this.repoName, SUGGESTIONS_LIMIT).then(response => {
- const branches = [];
- let branch;
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- if (response[key].ref.startsWith('refs/heads/')) {
- branch = response[key].ref.substring('refs/heads/'.length);
- } else {
- branch = response[key].ref;
- }
- branches.push({
- name: branch,
- });
- }
- return branches;
- });
- }
-
- _formatBooleanString(config) {
- if (config && config.configured_value === 'TRUE') {
- return true;
- } else if (config && config.configured_value === 'FALSE') {
- return false;
- } else if (config && config.configured_value === 'INHERIT') {
- if (config && config.inherited_value) {
- return true;
+ return this.$.restAPI.getRepoBranches(
+ input, this.repoName, SUGGESTIONS_LIMIT).then(response => {
+ const branches = [];
+ let branch;
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ if (response[key].ref.startsWith('refs/heads/')) {
+ branch = response[key].ref.substring('refs/heads/'.length);
} else {
- return false;
+ branch = response[key].ref;
}
+ branches.push({
+ name: branch,
+ });
+ }
+ return branches;
+ });
+ }
+
+ _formatBooleanString(config) {
+ if (config && config.configured_value === 'TRUE') {
+ return true;
+ } else if (config && config.configured_value === 'FALSE') {
+ return false;
+ } else if (config && config.configured_value === 'INHERIT') {
+ if (config && config.inherited_value) {
+ return true;
} else {
return false;
}
- }
-
- _computePrivateSectionClass(config) {
- return config ? 'hide' : '';
+ } else {
+ return false;
}
}
- customElements.define(GrCreateChangeDialog.is, GrCreateChangeDialog);
-})();
+ _computePrivateSectionClass(config) {
+ return config ? 'hide' : '';
+ }
+}
+
+customElements.define(GrCreateChangeDialog.is, GrCreateChangeDialog);
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.js
new file mode 100644
index 0000000..8f11af3
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.js
@@ -0,0 +1,80 @@
+/**
+ * @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.js';
+
+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">
+ input:not([type="checkbox"]),
+ gr-autocomplete,
+ iron-autogrow-textarea {
+ width: 100%;
+ }
+ .value {
+ width: 32em;
+ }
+ .hide {
+ display: none;
+ }
+ @media only screen and (max-width: 40em) {
+ .value {
+ width: 29em;
+ }
+ }
+ </style>
+ <div class="gr-form-styles">
+ <section class\$="[[_computeBranchClass(baseChange)]]">
+ <span class="title">Select branch for new change</span>
+ <span class="value">
+ <gr-autocomplete id="branchInput" text="{{branch}}" query="[[_query]]" placeholder="Destination branch">
+ </gr-autocomplete>
+ </span>
+ </section>
+ <section class\$="[[_computeBranchClass(baseChange)]]">
+ <span class="title">Provide base commit sha1 for change</span>
+ <span class="value">
+ <iron-input maxlength="40" placeholder="(optional)" bind-value="{{baseCommit}}">
+ <input is="iron-input" id="baseCommitInput" maxlength="40" placeholder="(optional)" bind-value="{{baseCommit}}">
+ </iron-input>
+ </span>
+ </section>
+ <section>
+ <span class="title">Enter topic for new change</span>
+ <span class="value">
+ <iron-input maxlength="1024" placeholder="(optional)" bind-value="{{topic}}">
+ <input is="iron-input" id="tagNameInput" maxlength="1024" placeholder="(optional)" bind-value="{{topic}}">
+ </iron-input>
+ </span>
+ </section>
+ <section id="description">
+ <span class="title">Description</span>
+ <span class="value">
+ <iron-autogrow-textarea id="messageInput" class="message" autocomplete="on" rows="4" max-rows="15" bind-value="{{subject}}" placeholder="Insert the description of the change.">
+ </iron-autogrow-textarea>
+ </span>
+ </section>
+ <section class\$="[[_computePrivateSectionClass(_privateChangesEnabled)]]">
+ <label class="title" for="privateChangeCheckBox">Private change</label>
+ <span class="value">
+ <input type="checkbox" id="privateChangeCheckBox" checked\$="[[_formatBooleanString(privateByDefault)]]">
+ </span>
+ </section>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
index aad2428f..4ce1855 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-create-change-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-create-change-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,136 +30,137 @@
</template>
</test-fixture>
-<script>
- suite('gr-create-change-dialog tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-create-change-dialog.js';
+suite('gr-create-change-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- getRepoBranches(input) {
- if (input.startsWith('test')) {
- return Promise.resolve([
- {
- ref: 'refs/heads/test-branch',
- revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
- can_delete: true,
- },
- ]);
- } else {
- return Promise.resolve({});
- }
- },
- });
- element = fixture('basic');
- element.repoName = 'test-repo',
- element._repoConfig = {
- private_by_default: {
- configured_value: 'FALSE',
- inherited_value: false,
- },
- };
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
+ getRepoBranches(input) {
+ if (input.startsWith('test')) {
+ return Promise.resolve([
+ {
+ ref: 'refs/heads/test-branch',
+ revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
+ can_delete: true,
+ },
+ ]);
+ } else {
+ return Promise.resolve({});
+ }
+ },
});
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('new change created with default', done => {
- const configInputObj = {
- branch: 'test-branch',
- subject: 'first change created with polygerrit ui',
- topic: 'test-topic',
- is_private: false,
- work_in_progress: true,
- };
-
- const saveStub = sandbox.stub(element.$.restAPI,
- 'createChange', () => Promise.resolve({}));
-
- element.branch = 'test-branch';
- element.topic = 'test-topic';
- element.subject = 'first change created with polygerrit ui';
- assert.isFalse(element.$.privateChangeCheckBox.checked);
-
- element.$.branchInput.bindValue = configInputObj.branch;
- element.$.tagNameInput.bindValue = configInputObj.topic;
- element.$.messageInput.bindValue = configInputObj.subject;
-
- element.handleCreateChange().then(() => {
- // Private change
- assert.isFalse(saveStub.lastCall.args[4]);
- // WIP Change
- assert.isTrue(saveStub.lastCall.args[5]);
- assert.isTrue(saveStub.called);
- done();
- });
- });
-
- test('new change created with private', done => {
- element.privateByDefault = {
- configured_value: 'TRUE',
+ element = fixture('basic');
+ element.repoName = 'test-repo',
+ element._repoConfig = {
+ private_by_default: {
+ configured_value: 'FALSE',
inherited_value: false,
- };
- sandbox.stub(element, '_formatBooleanString', () => Promise.resolve(true));
- flushAsynchronousOperations();
+ },
+ };
+ });
- const configInputObj = {
- branch: 'test-branch',
- subject: 'first change created with polygerrit ui',
- topic: 'test-topic',
- is_private: true,
- work_in_progress: true,
- };
+ teardown(() => {
+ sandbox.restore();
+ });
- const saveStub = sandbox.stub(element.$.restAPI,
- 'createChange', () => Promise.resolve({}));
+ test('new change created with default', done => {
+ const configInputObj = {
+ branch: 'test-branch',
+ subject: 'first change created with polygerrit ui',
+ topic: 'test-topic',
+ is_private: false,
+ work_in_progress: true,
+ };
- element.branch = 'test-branch';
- element.topic = 'test-topic';
- element.subject = 'first change created with polygerrit ui';
- assert.isTrue(element.$.privateChangeCheckBox.checked);
+ const saveStub = sandbox.stub(element.$.restAPI,
+ 'createChange', () => Promise.resolve({}));
- element.$.branchInput.bindValue = configInputObj.branch;
- element.$.tagNameInput.bindValue = configInputObj.topic;
- element.$.messageInput.bindValue = configInputObj.subject;
+ element.branch = 'test-branch';
+ element.topic = 'test-topic';
+ element.subject = 'first change created with polygerrit ui';
+ assert.isFalse(element.$.privateChangeCheckBox.checked);
- element.handleCreateChange().then(() => {
- // Private change
- assert.isTrue(saveStub.lastCall.args[4]);
- // WIP Change
- assert.isTrue(saveStub.lastCall.args[5]);
- assert.isTrue(saveStub.called);
- done();
- });
- });
+ element.$.branchInput.bindValue = configInputObj.branch;
+ element.$.tagNameInput.bindValue = configInputObj.topic;
+ element.$.messageInput.bindValue = configInputObj.subject;
- test('_getRepoBranchesSuggestions empty', done => {
- element._getRepoBranchesSuggestions('nonexistent').then(branches => {
- assert.equal(branches.length, 0);
- done();
- });
- });
-
- test('_getRepoBranchesSuggestions non-empty', done => {
- element._getRepoBranchesSuggestions('test-branch').then(branches => {
- assert.equal(branches.length, 1);
- assert.equal(branches[0].name, 'test-branch');
- done();
- });
- });
-
- test('_computeBranchClass', () => {
- assert.equal(element._computeBranchClass(true), 'hide');
- assert.equal(element._computeBranchClass(false), '');
- });
-
- test('_computePrivateSectionClass', () => {
- assert.equal(element._computePrivateSectionClass(true), 'hide');
- assert.equal(element._computePrivateSectionClass(false), '');
+ element.handleCreateChange().then(() => {
+ // Private change
+ assert.isFalse(saveStub.lastCall.args[4]);
+ // WIP Change
+ assert.isTrue(saveStub.lastCall.args[5]);
+ assert.isTrue(saveStub.called);
+ done();
});
});
+
+ test('new change created with private', done => {
+ element.privateByDefault = {
+ configured_value: 'TRUE',
+ inherited_value: false,
+ };
+ sandbox.stub(element, '_formatBooleanString', () => Promise.resolve(true));
+ flushAsynchronousOperations();
+
+ const configInputObj = {
+ branch: 'test-branch',
+ subject: 'first change created with polygerrit ui',
+ topic: 'test-topic',
+ is_private: true,
+ work_in_progress: true,
+ };
+
+ const saveStub = sandbox.stub(element.$.restAPI,
+ 'createChange', () => Promise.resolve({}));
+
+ element.branch = 'test-branch';
+ element.topic = 'test-topic';
+ element.subject = 'first change created with polygerrit ui';
+ assert.isTrue(element.$.privateChangeCheckBox.checked);
+
+ element.$.branchInput.bindValue = configInputObj.branch;
+ element.$.tagNameInput.bindValue = configInputObj.topic;
+ element.$.messageInput.bindValue = configInputObj.subject;
+
+ element.handleCreateChange().then(() => {
+ // Private change
+ assert.isTrue(saveStub.lastCall.args[4]);
+ // WIP Change
+ assert.isTrue(saveStub.lastCall.args[5]);
+ assert.isTrue(saveStub.called);
+ done();
+ });
+ });
+
+ test('_getRepoBranchesSuggestions empty', done => {
+ element._getRepoBranchesSuggestions('nonexistent').then(branches => {
+ assert.equal(branches.length, 0);
+ done();
+ });
+ });
+
+ test('_getRepoBranchesSuggestions non-empty', done => {
+ element._getRepoBranchesSuggestions('test-branch').then(branches => {
+ assert.equal(branches.length, 1);
+ assert.equal(branches[0].name, 'test-branch');
+ done();
+ });
+ });
+
+ test('_computeBranchClass', () => {
+ assert.equal(element._computeBranchClass(true), 'hide');
+ assert.equal(element._computeBranchClass(false), '');
+ });
+
+ test('_computePrivateSectionClass', () => {
+ assert.equal(element._computePrivateSectionClass(true), 'hide');
+ assert.equal(element._computePrivateSectionClass(false), '');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.html
deleted file mode 100644
index d0a1fca..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.html
+++ /dev/null
@@ -1,56 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-create-group-dialog">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- :host {
- display: inline-block;
- }
- input {
- width: 20em;
- }
- </style>
- <div class="gr-form-styles">
- <div id="form">
- <section>
- <span class="title">Group name</span>
- <iron-input
- bind-value="{{_name}}">
- <input
- is="iron-input"
- bind-value="{{_name}}">
- </iron-input>
- </section>
- </div>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-create-group-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
index 8a4edab..0860fdb 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
@@ -14,65 +14,77 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrCreateGroupDialog extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-create-group-dialog'; }
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-create-group-dialog_html.js';
- static get properties() {
- return {
- params: Object,
- hasNewGroupName: {
- type: Boolean,
- notify: true,
- value: false,
- },
- _name: Object,
- _groupCreated: {
- type: Boolean,
- value: false,
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrCreateGroupDialog extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- static get observers() {
- return [
- '_updateGroupName(_name)',
- ];
- }
+ static get is() { return 'gr-create-group-dialog'; }
- _computeGroupUrl(groupId) {
- return this.getBaseUrl() + '/admin/groups/' +
- this.encodeURL(groupId, true);
- }
-
- _updateGroupName(name) {
- this.hasNewGroupName = !!name;
- }
-
- handleCreateGroup() {
- return this.$.restAPI.createGroup({name: this._name})
- .then(groupRegistered => {
- if (groupRegistered.status !== 201) { return; }
- this._groupCreated = true;
- return this.$.restAPI.getGroupConfig(this._name)
- .then(group => {
- page.show(this._computeGroupUrl(group.group_id));
- });
- });
- }
+ static get properties() {
+ return {
+ params: Object,
+ hasNewGroupName: {
+ type: Boolean,
+ notify: true,
+ value: false,
+ },
+ _name: Object,
+ _groupCreated: {
+ type: Boolean,
+ value: false,
+ },
+ };
}
- customElements.define(GrCreateGroupDialog.is, GrCreateGroupDialog);
-})();
+ static get observers() {
+ return [
+ '_updateGroupName(_name)',
+ ];
+ }
+
+ _computeGroupUrl(groupId) {
+ return this.getBaseUrl() + '/admin/groups/' +
+ this.encodeURL(groupId, true);
+ }
+
+ _updateGroupName(name) {
+ this.hasNewGroupName = !!name;
+ }
+
+ handleCreateGroup() {
+ return this.$.restAPI.createGroup({name: this._name})
+ .then(groupRegistered => {
+ if (groupRegistered.status !== 201) { return; }
+ this._groupCreated = true;
+ return this.$.restAPI.getGroupConfig(this._name)
+ .then(group => {
+ page.show(this._computeGroupUrl(group.group_id));
+ });
+ });
+ }
+}
+
+customElements.define(GrCreateGroupDialog.is, GrCreateGroupDialog);
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.js
new file mode 100644
index 0000000..2cdde81
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.js
@@ -0,0 +1,42 @@
+/**
+ * @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.js';
+
+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">
+ :host {
+ display: inline-block;
+ }
+ input {
+ width: 20em;
+ }
+ </style>
+ <div class="gr-form-styles">
+ <div id="form">
+ <section>
+ <span class="title">Group name</span>
+ <iron-input bind-value="{{_name}}">
+ <input is="iron-input" bind-value="{{_name}}">
+ </iron-input>
+ </section>
+ </div>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html
index d630556..14644a4 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html
@@ -19,16 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-create-group-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-create-group-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,66 +31,67 @@
</template>
</test-fixture>
-<script>
- suite('gr-create-group-dialog tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- const GROUP_NAME = 'test-group';
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-create-group-dialog.js';
+suite('gr-create-group-dialog tests', () => {
+ let element;
+ let sandbox;
+ const GROUP_NAME = 'test-group';
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- });
- element = fixture('basic');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
});
+ element = fixture('basic');
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('name is updated correctly', done => {
- assert.isFalse(element.hasNewGroupName);
+ test('name is updated correctly', done => {
+ assert.isFalse(element.hasNewGroupName);
- const inputEl = element.root.querySelector('iron-input');
- inputEl.bindValue = GROUP_NAME;
+ const inputEl = element.root.querySelector('iron-input');
+ inputEl.bindValue = GROUP_NAME;
- setTimeout(() => {
- assert.isTrue(element.hasNewGroupName);
- assert.deepEqual(element._name, GROUP_NAME);
- done();
- });
- });
-
- test('test for redirecting to group on successful creation', done => {
- sandbox.stub(element.$.restAPI, 'createGroup')
- .returns(Promise.resolve({status: 201}));
-
- sandbox.stub(element.$.restAPI, 'getGroupConfig')
- .returns(Promise.resolve({group_id: 551}));
-
- const showStub = sandbox.stub(page, 'show');
- element.handleCreateGroup()
- .then(() => {
- assert.isTrue(showStub.calledWith('/admin/groups/551'));
- done();
- });
- });
-
- test('test for unsuccessful group creation', done => {
- sandbox.stub(element.$.restAPI, 'createGroup')
- .returns(Promise.resolve({status: 409}));
-
- sandbox.stub(element.$.restAPI, 'getGroupConfig')
- .returns(Promise.resolve({group_id: 551}));
-
- const showStub = sandbox.stub(page, 'show');
- element.handleCreateGroup()
- .then(() => {
- assert.isFalse(showStub.called);
- done();
- });
+ setTimeout(() => {
+ assert.isTrue(element.hasNewGroupName);
+ assert.deepEqual(element._name, GROUP_NAME);
+ done();
});
});
+
+ test('test for redirecting to group on successful creation', done => {
+ sandbox.stub(element.$.restAPI, 'createGroup')
+ .returns(Promise.resolve({status: 201}));
+
+ sandbox.stub(element.$.restAPI, 'getGroupConfig')
+ .returns(Promise.resolve({group_id: 551}));
+
+ const showStub = sandbox.stub(page, 'show');
+ element.handleCreateGroup()
+ .then(() => {
+ assert.isTrue(showStub.calledWith('/admin/groups/551'));
+ done();
+ });
+ });
+
+ test('test for unsuccessful group creation', done => {
+ sandbox.stub(element.$.restAPI, 'createGroup')
+ .returns(Promise.resolve({status: 409}));
+
+ sandbox.stub(element.$.restAPI, 'getGroupConfig')
+ .returns(Promise.resolve({group_id: 551}));
+
+ const showStub = sandbox.stub(page, 'show');
+ element.handleCreateGroup()
+ .then(() => {
+ assert.isFalse(showStub.called);
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html
deleted file mode 100644
index d1980a5..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html
+++ /dev/null
@@ -1,89 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-
-<dom-module id="gr-create-pointer-dialog">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- :host {
- display: inline-block;
- }
- input {
- width: 20em;
- }
- /* Add css selector with #id to increase priority
- (otherwise ".gr-form-styles section" rule wins) */
- .hideItem,
- #itemAnnotationSection.hideItem {
- display: none;
- }
- </style>
- <div class="gr-form-styles">
- <div id="form">
- <section id="itemNameSection">
- <span class="title">[[detailType]] name</span>
- <iron-input
- placeholder="[[detailType]] Name"
- bind-value="{{_itemName}}">
- <input
- is="iron-input"
- placeholder="[[detailType]] Name"
- bind-value="{{_itemName}}">
- </iron-input>
- </section>
- <section id="itemRevisionSection">
- <span class="title">Initial Revision</span>
- <iron-input
- placeholder="Revision (Branch or SHA-1)"
- bind-value="{{_itemRevision}}">
- <input
- is="iron-input"
- placeholder="Revision (Branch or SHA-1)"
- bind-value="{{_itemRevision}}">
- </iron-input>
- </section>
- <section id="itemAnnotationSection"
- class$="[[_computeHideItemClass(itemDetail)]]">
- <span class="title">Annotation</span>
- <iron-input
- placeholder="Annotation (Optional)"
- bind-value="{{_itemAnnotation}}">
- <input
- is="iron-input"
- placeholder="Annotation (Optional)"
- bind-value="{{_itemAnnotation}}">
- </iron-input>
- </section>
- </div>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-create-pointer-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
index 2d6b4aa..40ddb66 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
@@ -14,89 +14,103 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const DETAIL_TYPES = {
- branches: 'branches',
- tags: 'tags',
- };
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-create-pointer-dialog_html.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrCreatePointerDialog extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-create-pointer-dialog'; }
+const DETAIL_TYPES = {
+ branches: 'branches',
+ tags: 'tags',
+};
- static get properties() {
- return {
- detailType: String,
- repoName: String,
- hasNewItemName: {
- type: Boolean,
- notify: true,
- value: false,
- },
- itemDetail: String,
- _itemName: String,
- _itemRevision: String,
- _itemAnnotation: String,
- };
- }
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrCreatePointerDialog extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- static get observers() {
- return [
- '_updateItemName(_itemName)',
- ];
- }
+ static get is() { return 'gr-create-pointer-dialog'; }
- _updateItemName(name) {
- this.hasNewItemName = !!name;
- }
+ static get properties() {
+ return {
+ detailType: String,
+ repoName: String,
+ hasNewItemName: {
+ type: Boolean,
+ notify: true,
+ value: false,
+ },
+ itemDetail: String,
+ _itemName: String,
+ _itemRevision: String,
+ _itemAnnotation: String,
+ };
+ }
- _computeItemUrl(project) {
- if (this.itemDetail === DETAIL_TYPES.branches) {
- return this.getBaseUrl() + '/admin/repos/' +
- this.encodeURL(this.repoName, true) + ',branches';
- } else if (this.itemDetail === DETAIL_TYPES.tags) {
- return this.getBaseUrl() + '/admin/repos/' +
- this.encodeURL(this.repoName, true) + ',tags';
- }
- }
+ static get observers() {
+ return [
+ '_updateItemName(_itemName)',
+ ];
+ }
- handleCreateItem() {
- const USE_HEAD = this._itemRevision ? this._itemRevision : 'HEAD';
- if (this.itemDetail === DETAIL_TYPES.branches) {
- return this.$.restAPI.createRepoBranch(this.repoName,
- this._itemName, {revision: USE_HEAD})
- .then(itemRegistered => {
- if (itemRegistered.status === 201) {
- page.show(this._computeItemUrl(this.itemDetail));
- }
- });
- } else if (this.itemDetail === DETAIL_TYPES.tags) {
- return this.$.restAPI.createRepoTag(this.repoName,
- this._itemName,
- {revision: USE_HEAD, message: this._itemAnnotation || null})
- .then(itemRegistered => {
- if (itemRegistered.status === 201) {
- page.show(this._computeItemUrl(this.itemDetail));
- }
- });
- }
- }
+ _updateItemName(name) {
+ this.hasNewItemName = !!name;
+ }
- _computeHideItemClass(type) {
- return type === DETAIL_TYPES.branches ? 'hideItem' : '';
+ _computeItemUrl(project) {
+ if (this.itemDetail === DETAIL_TYPES.branches) {
+ return this.getBaseUrl() + '/admin/repos/' +
+ this.encodeURL(this.repoName, true) + ',branches';
+ } else if (this.itemDetail === DETAIL_TYPES.tags) {
+ return this.getBaseUrl() + '/admin/repos/' +
+ this.encodeURL(this.repoName, true) + ',tags';
}
}
- customElements.define(GrCreatePointerDialog.is, GrCreatePointerDialog);
-})();
+ handleCreateItem() {
+ const USE_HEAD = this._itemRevision ? this._itemRevision : 'HEAD';
+ if (this.itemDetail === DETAIL_TYPES.branches) {
+ return this.$.restAPI.createRepoBranch(this.repoName,
+ this._itemName, {revision: USE_HEAD})
+ .then(itemRegistered => {
+ if (itemRegistered.status === 201) {
+ page.show(this._computeItemUrl(this.itemDetail));
+ }
+ });
+ } else if (this.itemDetail === DETAIL_TYPES.tags) {
+ return this.$.restAPI.createRepoTag(this.repoName,
+ this._itemName,
+ {revision: USE_HEAD, message: this._itemAnnotation || null})
+ .then(itemRegistered => {
+ if (itemRegistered.status === 201) {
+ page.show(this._computeItemUrl(this.itemDetail));
+ }
+ });
+ }
+ }
+
+ _computeHideItemClass(type) {
+ return type === DETAIL_TYPES.branches ? 'hideItem' : '';
+ }
+}
+
+customElements.define(GrCreatePointerDialog.is, GrCreatePointerDialog);
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.js
new file mode 100644
index 0000000..3a6df2f
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.js
@@ -0,0 +1,60 @@
+/**
+ * @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.js';
+
+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">
+ :host {
+ display: inline-block;
+ }
+ input {
+ width: 20em;
+ }
+ /* Add css selector with #id to increase priority
+ (otherwise ".gr-form-styles section" rule wins) */
+ .hideItem,
+ #itemAnnotationSection.hideItem {
+ display: none;
+ }
+ </style>
+ <div class="gr-form-styles">
+ <div id="form">
+ <section id="itemNameSection">
+ <span class="title">[[detailType]] name</span>
+ <iron-input placeholder="[[detailType]] Name" bind-value="{{_itemName}}">
+ <input is="iron-input" placeholder="[[detailType]] Name" bind-value="{{_itemName}}">
+ </iron-input>
+ </section>
+ <section id="itemRevisionSection">
+ <span class="title">Initial Revision</span>
+ <iron-input placeholder="Revision (Branch or SHA-1)" bind-value="{{_itemRevision}}">
+ <input is="iron-input" placeholder="Revision (Branch or SHA-1)" bind-value="{{_itemRevision}}">
+ </iron-input>
+ </section>
+ <section id="itemAnnotationSection" class\$="[[_computeHideItemClass(itemDetail)]]">
+ <span class="title">Annotation</span>
+ <iron-input placeholder="Annotation (Optional)" bind-value="{{_itemAnnotation}}">
+ <input is="iron-input" placeholder="Annotation (Optional)" bind-value="{{_itemAnnotation}}">
+ </iron-input>
+ </section>
+ </div>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html
index db33587..394b1d8 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-create-pointer-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-create-pointer-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,103 +30,105 @@
</template>
</test-fixture>
-<script>
- suite('gr-create-pointer-dialog tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-create-pointer-dialog.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-create-pointer-dialog tests', () => {
+ let element;
+ let sandbox;
- const ironInput = function(element) {
- return Polymer.dom(element).querySelector('iron-input');
- };
+ const ironInput = function(element) {
+ return dom(element).querySelector('iron-input');
+ };
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- });
- element = fixture('basic');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
});
+ element = fixture('basic');
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('branch created', done => {
- sandbox.stub(
- element.$.restAPI,
- 'createRepoBranch',
- () => Promise.resolve({}));
+ test('branch created', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'createRepoBranch',
+ () => Promise.resolve({}));
- assert.isFalse(element.hasNewItemName);
+ assert.isFalse(element.hasNewItemName);
- element._itemName = 'test-branch';
- element.itemDetail = 'branches';
+ element._itemName = 'test-branch';
+ element.itemDetail = 'branches';
- ironInput(element.$.itemNameSection).bindValue = 'test-branch2';
- ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
+ ironInput(element.$.itemNameSection).bindValue = 'test-branch2';
+ ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
- setTimeout(() => {
- assert.isTrue(element.hasNewItemName);
- assert.equal(element._itemName, 'test-branch2');
- assert.equal(element._itemRevision, 'HEAD');
- done();
- });
- });
-
- test('tag created', done => {
- sandbox.stub(
- element.$.restAPI,
- 'createRepoTag',
- () => Promise.resolve({}));
-
- assert.isFalse(element.hasNewItemName);
-
- element._itemName = 'test-tag';
- element.itemDetail = 'tags';
-
- ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
- ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
-
- setTimeout(() => {
- assert.isTrue(element.hasNewItemName);
- assert.equal(element._itemName, 'test-tag2');
- assert.equal(element._itemRevision, 'HEAD');
- done();
- });
- });
-
- test('tag created with annotations', done => {
- sandbox.stub(
- element.$.restAPI,
- 'createRepoTag',
- () => Promise.resolve({}));
-
- assert.isFalse(element.hasNewItemName);
-
- element._itemName = 'test-tag';
- element._itemAnnotation = 'test-message';
- element.itemDetail = 'tags';
-
- ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
- ironInput(element.$.itemAnnotationSection).bindValue = 'test-message2';
- ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
-
- setTimeout(() => {
- assert.isTrue(element.hasNewItemName);
- assert.equal(element._itemName, 'test-tag2');
- assert.equal(element._itemAnnotation, 'test-message2');
- assert.equal(element._itemRevision, 'HEAD');
- done();
- });
- });
-
- test('_computeHideItemClass returns hideItem if type is branches', () => {
- assert.equal(element._computeHideItemClass('branches'), 'hideItem');
- });
-
- test('_computeHideItemClass returns strings if not branches', () => {
- assert.equal(element._computeHideItemClass('tags'), '');
+ setTimeout(() => {
+ assert.isTrue(element.hasNewItemName);
+ assert.equal(element._itemName, 'test-branch2');
+ assert.equal(element._itemRevision, 'HEAD');
+ done();
});
});
+
+ test('tag created', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'createRepoTag',
+ () => Promise.resolve({}));
+
+ assert.isFalse(element.hasNewItemName);
+
+ element._itemName = 'test-tag';
+ element.itemDetail = 'tags';
+
+ ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
+ ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
+
+ setTimeout(() => {
+ assert.isTrue(element.hasNewItemName);
+ assert.equal(element._itemName, 'test-tag2');
+ assert.equal(element._itemRevision, 'HEAD');
+ done();
+ });
+ });
+
+ test('tag created with annotations', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'createRepoTag',
+ () => Promise.resolve({}));
+
+ assert.isFalse(element.hasNewItemName);
+
+ element._itemName = 'test-tag';
+ element._itemAnnotation = 'test-message';
+ element.itemDetail = 'tags';
+
+ ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
+ ironInput(element.$.itemAnnotationSection).bindValue = 'test-message2';
+ ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
+
+ setTimeout(() => {
+ assert.isTrue(element.hasNewItemName);
+ assert.equal(element._itemName, 'test-tag2');
+ assert.equal(element._itemAnnotation, 'test-message2');
+ assert.equal(element._itemRevision, 'HEAD');
+ done();
+ });
+ });
+
+ test('_computeHideItemClass returns hideItem if type is branches', () => {
+ assert.equal(element._computeHideItemClass('branches'), 'hideItem');
+ });
+
+ test('_computeHideItemClass returns strings if not branches', () => {
+ assert.equal(element._computeHideItemClass('tags'), '');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html
deleted file mode 100644
index b78090c..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html
+++ /dev/null
@@ -1,112 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-
-<dom-module id="gr-create-repo-dialog">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- :host {
- display: inline-block;
- }
- input {
- width: 20em;
- }
- gr-autocomplete {
- width: 20em;
- }
- </style>
-
- <div class="gr-form-styles">
- <div id="form">
- <section>
- <span class="title">Repository name</span>
- <iron-input autocomplete="on"
- bind-value="{{_repoConfig.name}}">
- <input is="iron-input"
- id="repoNameInput"
- autocomplete="on"
- bind-value="{{_repoConfig.name}}">
- </iron-input>
- </section>
- <section>
- <span class="title">Rights inherit from</span>
- <span class="value">
- <gr-autocomplete
- id="rightsInheritFromInput"
- text="{{_repoConfig.parent}}"
- query="[[_query]]"
- placeholder="Optional, defaults to 'All-Projects'">
- </gr-autocomplete>
- </span>
- </section>
- <section>
- <span class="title">Owner</span>
- <span class="value">
- <gr-autocomplete
- id="ownerInput"
- text="{{_repoOwner}}"
- value="{{_repoOwnerId}}"
- query="[[_queryGroups]]">
- </gr-autocomplete>
- </span>
- </section>
- <section>
- <span class="title">Create initial empty commit</span>
- <span class="value">
- <gr-select
- id="initialCommit"
- bind-value="{{_repoConfig.create_empty_commit}}">
- <select>
- <option value="false">False</option>
- <option value="true">True</option>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Only serve as parent for other repositories</span>
- <span class="value">
- <gr-select
- id="parentRepo"
- bind-value="{{_repoConfig.permissions_only}}">
- <select>
- <option value="false">False</option>
- <option value="true">True</option>
- </select>
- </gr-select>
- </span>
- </section>
- </div>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-create-repo-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
index 290f025..7a77874 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
@@ -14,130 +14,145 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrCreateRepoDialog extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-create-repo-dialog'; }
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-create-repo-dialog_html.js';
- static get properties() {
- return {
- params: Object,
- hasNewRepoName: {
- type: Boolean,
- notify: true,
- value: false,
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrCreateRepoDialog extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-create-repo-dialog'; }
+
+ static get properties() {
+ return {
+ params: Object,
+ hasNewRepoName: {
+ type: Boolean,
+ notify: true,
+ value: false,
+ },
+
+ /** @type {?} */
+ _repoConfig: {
+ type: Object,
+ value: () => {
+ // Set default values for dropdowns.
+ return {
+ create_empty_commit: true,
+ permissions_only: false,
+ };
},
+ },
+ _repoCreated: {
+ type: Boolean,
+ value: false,
+ },
+ _repoOwner: String,
+ _repoOwnerId: {
+ type: String,
+ observer: '_repoOwnerIdUpdate',
+ },
- /** @type {?} */
- _repoConfig: {
- type: Object,
- value: () => {
- // Set default values for dropdowns.
- return {
- create_empty_commit: true,
- permissions_only: false,
- };
- },
+ _query: {
+ type: Function,
+ value() {
+ return this._getRepoSuggestions.bind(this);
},
- _repoCreated: {
- type: Boolean,
- value: false,
+ },
+ _queryGroups: {
+ type: Function,
+ value() {
+ return this._getGroupSuggestions.bind(this);
},
- _repoOwner: String,
- _repoOwnerId: {
- type: String,
- observer: '_repoOwnerIdUpdate',
- },
+ },
+ };
+ }
- _query: {
- type: Function,
- value() {
- return this._getRepoSuggestions.bind(this);
- },
- },
- _queryGroups: {
- type: Function,
- value() {
- return this._getGroupSuggestions.bind(this);
- },
- },
- };
- }
+ static get observers() {
+ return [
+ '_updateRepoName(_repoConfig.name)',
+ ];
+ }
- static get observers() {
- return [
- '_updateRepoName(_repoConfig.name)',
- ];
- }
+ _computeRepoUrl(repoName) {
+ return this.getBaseUrl() + '/admin/repos/' +
+ this.encodeURL(repoName, true);
+ }
- _computeRepoUrl(repoName) {
- return this.getBaseUrl() + '/admin/repos/' +
- this.encodeURL(repoName, true);
- }
+ _updateRepoName(name) {
+ this.hasNewRepoName = !!name;
+ }
- _updateRepoName(name) {
- this.hasNewRepoName = !!name;
- }
-
- _repoOwnerIdUpdate(id) {
- if (id) {
- this.set('_repoConfig.owners', [id]);
- } else {
- this.set('_repoConfig.owners', undefined);
- }
- }
-
- handleCreateRepo() {
- return this.$.restAPI.createRepo(this._repoConfig)
- .then(repoRegistered => {
- if (repoRegistered.status === 201) {
- this._repoCreated = true;
- page.show(this._computeRepoUrl(this._repoConfig.name));
- }
- });
- }
-
- _getRepoSuggestions(input) {
- return this.$.restAPI.getSuggestedProjects(input)
- .then(response => {
- const repos = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- repos.push({
- name: key,
- value: response[key],
- });
- }
- return repos;
- });
- }
-
- _getGroupSuggestions(input) {
- return this.$.restAPI.getSuggestedGroups(input)
- .then(response => {
- const groups = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- groups.push({
- name: key,
- value: decodeURIComponent(response[key].id),
- });
- }
- return groups;
- });
+ _repoOwnerIdUpdate(id) {
+ if (id) {
+ this.set('_repoConfig.owners', [id]);
+ } else {
+ this.set('_repoConfig.owners', undefined);
}
}
- customElements.define(GrCreateRepoDialog.is, GrCreateRepoDialog);
-})();
+ handleCreateRepo() {
+ return this.$.restAPI.createRepo(this._repoConfig)
+ .then(repoRegistered => {
+ if (repoRegistered.status === 201) {
+ this._repoCreated = true;
+ page.show(this._computeRepoUrl(this._repoConfig.name));
+ }
+ });
+ }
+
+ _getRepoSuggestions(input) {
+ return this.$.restAPI.getSuggestedProjects(input)
+ .then(response => {
+ const repos = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ repos.push({
+ name: key,
+ value: response[key],
+ });
+ }
+ return repos;
+ });
+ }
+
+ _getGroupSuggestions(input) {
+ return this.$.restAPI.getSuggestedGroups(input)
+ .then(response => {
+ const groups = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ groups.push({
+ name: key,
+ value: decodeURIComponent(response[key].id),
+ });
+ }
+ return groups;
+ });
+ }
+}
+
+customElements.define(GrCreateRepoDialog.is, GrCreateRepoDialog);
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.js
new file mode 100644
index 0000000..65666ea
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.js
@@ -0,0 +1,82 @@
+/**
+ * @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.js';
+
+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">
+ :host {
+ display: inline-block;
+ }
+ input {
+ width: 20em;
+ }
+ gr-autocomplete {
+ width: 20em;
+ }
+ </style>
+
+ <div class="gr-form-styles">
+ <div id="form">
+ <section>
+ <span class="title">Repository name</span>
+ <iron-input autocomplete="on" bind-value="{{_repoConfig.name}}">
+ <input is="iron-input" id="repoNameInput" autocomplete="on" bind-value="{{_repoConfig.name}}">
+ </iron-input>
+ </section>
+ <section>
+ <span class="title">Rights inherit from</span>
+ <span class="value">
+ <gr-autocomplete id="rightsInheritFromInput" text="{{_repoConfig.parent}}" query="[[_query]]" placeholder="Optional, defaults to 'All-Projects'">
+ </gr-autocomplete>
+ </span>
+ </section>
+ <section>
+ <span class="title">Owner</span>
+ <span class="value">
+ <gr-autocomplete id="ownerInput" text="{{_repoOwner}}" value="{{_repoOwnerId}}" query="[[_queryGroups]]">
+ </gr-autocomplete>
+ </span>
+ </section>
+ <section>
+ <span class="title">Create initial empty commit</span>
+ <span class="value">
+ <gr-select id="initialCommit" bind-value="{{_repoConfig.create_empty_commit}}">
+ <select>
+ <option value="false">False</option>
+ <option value="true">True</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title">Only serve as parent for other repositories</span>
+ <span class="value">
+ <gr-select id="parentRepo" bind-value="{{_repoConfig.permissions_only}}">
+ <select>
+ <option value="false">False</option>
+ <option value="true">True</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ </div>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html
index 578c074..a14a9c9 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-create-repo-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-create-repo-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,74 +30,75 @@
</template>
</test-fixture>
-<script>
- suite('gr-create-repo-dialog tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-create-repo-dialog.js';
+suite('gr-create-repo-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- });
- element = fixture('basic');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
});
+ element = fixture('basic');
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('default values are populated', () => {
- assert.isTrue(element.$.initialCommit.bindValue);
- assert.isFalse(element.$.parentRepo.bindValue);
- });
+ test('default values are populated', () => {
+ assert.isTrue(element.$.initialCommit.bindValue);
+ assert.isFalse(element.$.parentRepo.bindValue);
+ });
- test('repo created', done => {
- const configInputObj = {
- name: 'test-repo',
- create_empty_commit: true,
- parent: 'All-Project',
- permissions_only: false,
- owners: ['testId'],
- };
+ test('repo created', done => {
+ const configInputObj = {
+ name: 'test-repo',
+ create_empty_commit: true,
+ parent: 'All-Project',
+ permissions_only: false,
+ owners: ['testId'],
+ };
- const saveStub = sandbox.stub(element.$.restAPI,
- 'createRepo', () => Promise.resolve({}));
+ const saveStub = sandbox.stub(element.$.restAPI,
+ 'createRepo', () => Promise.resolve({}));
- assert.isFalse(element.hasNewRepoName);
+ assert.isFalse(element.hasNewRepoName);
- element._repoConfig = {
- name: 'test-repo',
- create_empty_commit: true,
- parent: 'All-Project',
- permissions_only: false,
- };
+ element._repoConfig = {
+ name: 'test-repo',
+ create_empty_commit: true,
+ parent: 'All-Project',
+ permissions_only: false,
+ };
- element._repoOwner = 'test';
- element._repoOwnerId = 'testId';
+ element._repoOwner = 'test';
+ element._repoOwnerId = 'testId';
- element.$.repoNameInput.bindValue = configInputObj.name;
- element.$.rightsInheritFromInput.bindValue = configInputObj.parent;
- element.$.ownerInput.text = configInputObj.owners[0];
- element.$.initialCommit.bindValue =
- configInputObj.create_empty_commit;
- element.$.parentRepo.bindValue =
- configInputObj.permissions_only;
+ element.$.repoNameInput.bindValue = configInputObj.name;
+ element.$.rightsInheritFromInput.bindValue = configInputObj.parent;
+ element.$.ownerInput.text = configInputObj.owners[0];
+ element.$.initialCommit.bindValue =
+ configInputObj.create_empty_commit;
+ element.$.parentRepo.bindValue =
+ configInputObj.permissions_only;
- assert.isTrue(element.hasNewRepoName);
+ assert.isTrue(element.hasNewRepoName);
- assert.deepEqual(element._repoConfig, configInputObj);
+ assert.deepEqual(element._repoConfig, configInputObj);
- element.handleCreateRepo().then(() => {
- assert.isTrue(saveStub.lastCall.calledWithExactly(configInputObj));
- done();
- });
- });
-
- test('testing observer of _repoOwner', () => {
- element._repoOwnerId = 'test-5';
- assert.deepEqual(element._repoConfig.owners, ['test-5']);
+ element.handleCreateRepo().then(() => {
+ assert.isTrue(saveStub.lastCall.calledWithExactly(configInputObj));
+ done();
});
});
+
+ test('testing observer of _repoOwner', () => {
+ element._repoOwnerId = 'test-5';
+ assert.deepEqual(element._repoConfig.owners, ['test-5']);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html
deleted file mode 100644
index 4ed751d..0000000
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html
+++ /dev/null
@@ -1,82 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/gr-table-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
-
-<dom-module id="gr-group-audit-log">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-table-styles">
- /* GenericList style centers the last column, but we don't want that here. */
- .genericList tr th:last-of-type,
- .genericList tr td:last-of-type {
- text-align: left;
- }
- </style>
- <table id="list" class="genericList">
- <tr class="headerRow">
- <th class="date topHeader">Date</th>
- <th class="type topHeader">Type</th>
- <th class="member topHeader">Member</th>
- <th class="by-user topHeader">By User</th>
- </tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
- <td>Loading...</td>
- </tr>
- <tbody class$="[[computeLoadingClass(_loading)]]">
- <template is="dom-repeat" items="[[_auditLog]]">
- <tr class="table">
- <td class="date">
- <gr-date-formatter
- has-tooltip
- date-str="[[item.date]]">
- </gr-date-formatter>
- </td>
- <td class="type">[[itemType(item.type)]]</td>
- <td class="member">
- <template is="dom-if" if="[[_isGroupEvent(item.type)]]">
- <a href$="[[_computeGroupUrl(item.member)]]">
- [[_getNameForGroup(item.member)]]
- </a>
- </template>
- <template is="dom-if" if="[[!_isGroupEvent(item.type)]]">
- <gr-account-link account="[[item.member]]"></gr-account-link>
- [[_getIdForUser(item.member)]]
- </template>
- </td>
- <td class="by-user">
- <gr-account-link account="[[item.user]]"></gr-account-link>
- [[_getIdForUser(item.user)]]
- </td>
- </tr>
- </template>
- </tbody>
- </table>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-group-audit-log.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
index 11517d6..81c9cde 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
@@ -14,113 +14,127 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
- const GROUP_EVENTS = ['ADD_GROUP', 'REMOVE_GROUP'];
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/gr-table-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-account-link/gr-account-link.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-group-audit-log_html.js';
- /**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.ListViewMixin
- * @extends Polymer.Element
- */
- class GrGroupAuditLog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.ListViewBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-group-audit-log'; }
+const GROUP_EVENTS = ['ADD_GROUP', 'REMOVE_GROUP'];
- static get properties() {
- return {
- groupId: String,
- _auditLog: Array,
- _loading: {
- type: Boolean,
- value: true,
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.ListViewMixin
+ * @extends Polymer.Element
+ */
+class GrGroupAuditLog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.ListViewBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** @override */
- attached() {
- super.attached();
- this.fire('title-change', {title: 'Audit Log'});
- }
+ static get is() { return 'gr-group-audit-log'; }
- /** @override */
- ready() {
- super.ready();
- this._getAuditLogs();
- }
-
- _getAuditLogs() {
- if (!this.groupId) { return ''; }
-
- const errFn = response => {
- this.fire('page-error', {response});
- };
-
- return this.$.restAPI.getGroupAuditLog(this.groupId, errFn)
- .then(auditLog => {
- if (!auditLog) {
- this._auditLog = [];
- return;
- }
- this._auditLog = auditLog;
- this._loading = false;
- });
- }
-
- _status(item) {
- return item.disabled ? 'Disabled' : 'Enabled';
- }
-
- itemType(type) {
- let item;
- switch (type) {
- case 'ADD_GROUP':
- case 'ADD_USER':
- item = 'Added';
- break;
- case 'REMOVE_GROUP':
- case 'REMOVE_USER':
- item = 'Removed';
- break;
- default:
- item = '';
- }
- return item;
- }
-
- _isGroupEvent(type) {
- return GROUP_EVENTS.indexOf(type) !== -1;
- }
-
- _computeGroupUrl(group) {
- if (group && group.url && group.id) {
- return Gerrit.Nav.getUrlForGroup(group.id);
- }
-
- return '';
- }
-
- _getIdForUser(account) {
- return account._account_id ? ' (' + account._account_id + ')' : '';
- }
-
- _getNameForGroup(group) {
- if (group && group.name) {
- return group.name;
- } else if (group && group.id) {
- // The URL encoded id of the member
- return decodeURIComponent(group.id);
- }
-
- return '';
- }
+ static get properties() {
+ return {
+ groupId: String,
+ _auditLog: Array,
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ };
}
- customElements.define(GrGroupAuditLog.is, GrGroupAuditLog);
-})();
+ /** @override */
+ attached() {
+ super.attached();
+ this.fire('title-change', {title: 'Audit Log'});
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ this._getAuditLogs();
+ }
+
+ _getAuditLogs() {
+ if (!this.groupId) { return ''; }
+
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
+
+ return this.$.restAPI.getGroupAuditLog(this.groupId, errFn)
+ .then(auditLog => {
+ if (!auditLog) {
+ this._auditLog = [];
+ return;
+ }
+ this._auditLog = auditLog;
+ this._loading = false;
+ });
+ }
+
+ _status(item) {
+ return item.disabled ? 'Disabled' : 'Enabled';
+ }
+
+ itemType(type) {
+ let item;
+ switch (type) {
+ case 'ADD_GROUP':
+ case 'ADD_USER':
+ item = 'Added';
+ break;
+ case 'REMOVE_GROUP':
+ case 'REMOVE_USER':
+ item = 'Removed';
+ break;
+ default:
+ item = '';
+ }
+ return item;
+ }
+
+ _isGroupEvent(type) {
+ return GROUP_EVENTS.indexOf(type) !== -1;
+ }
+
+ _computeGroupUrl(group) {
+ if (group && group.url && group.id) {
+ return Gerrit.Nav.getUrlForGroup(group.id);
+ }
+
+ return '';
+ }
+
+ _getIdForUser(account) {
+ return account._account_id ? ' (' + account._account_id + ')' : '';
+ }
+
+ _getNameForGroup(group) {
+ if (group && group.name) {
+ return group.name;
+ } else if (group && group.id) {
+ // The URL encoded id of the member
+ return decodeURIComponent(group.id);
+ }
+
+ return '';
+ }
+}
+
+customElements.define(GrGroupAuditLog.is, GrGroupAuditLog);
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.js b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.js
new file mode 100644
index 0000000..0958e7c
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.js
@@ -0,0 +1,68 @@
+/**
+ * @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.js';
+
+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-table-styles">
+ /* GenericList style centers the last column, but we don't want that here. */
+ .genericList tr th:last-of-type,
+ .genericList tr td:last-of-type {
+ text-align: left;
+ }
+ </style>
+ <table id="list" class="genericList">
+ <tbody><tr class="headerRow">
+ <th class="date topHeader">Date</th>
+ <th class="type topHeader">Type</th>
+ <th class="member topHeader">Member</th>
+ <th class="by-user topHeader">By User</th>
+ </tr>
+ <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]">
+ <td>Loading...</td>
+ </tr>
+ </tbody><tbody class\$="[[computeLoadingClass(_loading)]]">
+ <template is="dom-repeat" items="[[_auditLog]]">
+ <tr class="table">
+ <td class="date">
+ <gr-date-formatter has-tooltip="" date-str="[[item.date]]">
+ </gr-date-formatter>
+ </td>
+ <td class="type">[[itemType(item.type)]]</td>
+ <td class="member">
+ <template is="dom-if" if="[[_isGroupEvent(item.type)]]">
+ <a href\$="[[_computeGroupUrl(item.member)]]">
+ [[_getNameForGroup(item.member)]]
+ </a>
+ </template>
+ <template is="dom-if" if="[[!_isGroupEvent(item.type)]]">
+ <gr-account-link account="[[item.member]]"></gr-account-link>
+ [[_getIdForUser(item.member)]]
+ </template>
+ </td>
+ <td class="by-user">
+ <gr-account-link account="[[item.user]]"></gr-account-link>
+ [[_getIdForUser(item.user)]]
+ </td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html
index 3a75611..c7a41bf 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-group-audit-log</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-group-audit-log.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,85 +30,86 @@
</template>
</test-fixture>
-<script>
- suite('gr-group-audit-log tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-group-audit-log.js';
+suite('gr-group-audit-log tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('members', () => {
+ test('test _getNameForGroup', () => {
+ let group = {
+ member: {
+ name: 'test-name',
+ },
+ };
+ assert.equal(element._getNameForGroup(group.member), 'test-name');
+
+ group = {
+ member: {
+ id: 'test-id',
+ },
+ };
+ assert.equal(element._getNameForGroup(group.member), 'test-id');
});
- teardown(() => {
- sandbox.restore();
- });
+ test('test _isGroupEvent', () => {
+ assert.isTrue(element._isGroupEvent('ADD_GROUP'));
+ assert.isTrue(element._isGroupEvent('REMOVE_GROUP'));
- suite('members', () => {
- test('test _getNameForGroup', () => {
- let group = {
- member: {
- name: 'test-name',
- },
- };
- assert.equal(element._getNameForGroup(group.member), 'test-name');
-
- group = {
- member: {
- id: 'test-id',
- },
- };
- assert.equal(element._getNameForGroup(group.member), 'test-id');
- });
-
- test('test _isGroupEvent', () => {
- assert.isTrue(element._isGroupEvent('ADD_GROUP'));
- assert.isTrue(element._isGroupEvent('REMOVE_GROUP'));
-
- assert.isFalse(element._isGroupEvent('ADD_USER'));
- assert.isFalse(element._isGroupEvent('REMOVE_USER'));
- });
- });
-
- suite('users', () => {
- test('test _getIdForUser', () => {
- const account = {
- user: {
- username: 'test-user',
- _account_id: 12,
- },
- };
- assert.equal(element._getIdForUser(account.user), ' (12)');
- });
-
- test('test _account_id not present', () => {
- const account = {
- user: {
- username: 'test-user',
- },
- };
- assert.equal(element._getIdForUser(account.user), '');
- });
- });
-
- suite('404', () => {
- test('fires page-error', done => {
- element.groupId = 1;
-
- const response = {status: 404};
- sandbox.stub(
- element.$.restAPI, 'getGroupAuditLog', (group, errFn) => {
- errFn(response);
- });
-
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- element._getAuditLogs();
- });
+ assert.isFalse(element._isGroupEvent('ADD_USER'));
+ assert.isFalse(element._isGroupEvent('REMOVE_USER'));
});
});
+
+ suite('users', () => {
+ test('test _getIdForUser', () => {
+ const account = {
+ user: {
+ username: 'test-user',
+ _account_id: 12,
+ },
+ };
+ assert.equal(element._getIdForUser(account.user), ' (12)');
+ });
+
+ test('test _account_id not present', () => {
+ const account = {
+ user: {
+ username: 'test-user',
+ },
+ };
+ assert.equal(element._getIdForUser(account.user), '');
+ });
+ });
+
+ suite('404', () => {
+ test('fires page-error', done => {
+ element.groupId = 1;
+
+ const response = {status: 404};
+ sandbox.stub(
+ element.$.restAPI, 'getGroupAuditLog', (group, errFn) => {
+ errFn(response);
+ });
+
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
+ });
+
+ element._getAuditLogs();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
deleted file mode 100644
index cf24793..0000000
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
+++ /dev/null
@@ -1,190 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/gr-subpage-styles.html">
-<link rel="import" href="../../../styles/gr-table-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html">
-
-<dom-module id="gr-group-members">
- <template>
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-table-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-subpage-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- .input {
- width: 15em;
- }
- gr-autocomplete {
- width: 20em;
- }
- a {
- color: var(--primary-text-color);
- text-decoration: none;
- }
- a:hover {
- text-decoration: underline;
- }
- th {
- border-bottom: 1px solid var(--border-color);
- font-weight: var(--font-weight-bold);
- text-align: left;
- }
- .canModify #groupMemberSearchInput,
- .canModify #saveGroupMember,
- .canModify .deleteHeader,
- .canModify .deleteColumn,
- .canModify #includedGroupSearchInput,
- .canModify #saveIncludedGroups,
- .canModify .deleteIncludedHeader,
- .canModify #saveIncludedGroups {
- display: none;
- }
- </style>
- <main class$="gr-form-styles [[_computeHideItemClass(_groupOwner, _isAdmin)]]">
- <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
- Loading...
- </div>
- <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <h1 id="Title">[[_groupName]]</h1>
- <div id="form">
- <h3 id="members">Members</h3>
- <fieldset>
- <span class="value">
- <gr-autocomplete
- id="groupMemberSearchInput"
- text="{{_groupMemberSearchName}}"
- value="{{_groupMemberSearchId}}"
- query="[[_queryMembers]]"
- placeholder="Name Or Email">
- </gr-autocomplete>
- </span>
- <gr-button
- id="saveGroupMember"
- on-click="_handleSavingGroupMember"
- disabled="[[!_groupMemberSearchId]]">
- Add
- </gr-button>
- <table id="groupMembers">
- <tr class="headerRow">
- <th class="nameHeader">Name</th>
- <th class="emailAddressHeader">Email Address</th>
- <th class="deleteHeader">Delete Member</th>
- </tr>
- <tbody>
- <template is="dom-repeat" items="[[_groupMembers]]">
- <tr>
- <td class="nameColumn">
- <gr-account-link account="[[item]]"></gr-account-link>
- </td>
- <td>[[item.email]]</td>
- <td class="deleteColumn">
- <gr-button
- class="deleteMembersButton"
- on-click="_handleDeleteMember">
- Delete
- </gr-button>
- </td>
- </tr>
- </template>
- </tbody>
- </table>
- </fieldset>
- <h3 id="includedGroups">Included Groups</h3>
- <fieldset>
- <span class="value">
- <gr-autocomplete
- id="includedGroupSearchInput"
- text="{{_includedGroupSearchName}}"
- value="{{_includedGroupSearchId}}"
- query="[[_queryIncludedGroup]]"
- placeholder="Group Name">
- </gr-autocomplete>
- </span>
- <gr-button
- id="saveIncludedGroups"
- on-click="_handleSavingIncludedGroups"
- disabled="[[!_includedGroupSearchId]]">
- Add
- </gr-button>
- <table id="includedGroups">
- <tr class="headerRow">
- <th class="groupNameHeader">Group Name</th>
- <th class="descriptionHeader">Description</th>
- <th class="deleteIncludedHeader">
- Delete Group
- </th>
- </tr>
- <tbody>
- <template is="dom-repeat" items="[[_includedGroups]]">
- <tr>
- <td class="nameColumn">
- <template is="dom-if" if="[[item.url]]">
- <a href$="[[_computeGroupUrl(item.url)]]"
- rel="noopener">
- [[item.name]]
- </a>
- </template>
- <template is="dom-if" if="[[!item.url]]">
- [[item.name]]
- </template>
- </td>
- <td>[[item.description]]</td>
- <td class="deleteColumn">
- <gr-button
- class="deleteIncludedGroupButton"
- on-click="_handleDeleteIncludedGroup">
- Delete
- </gr-button>
- </td>
- </tr>
- </template>
- </tbody>
- </table>
- </fieldset>
- </div>
- </div>
- </main>
- <gr-overlay id="overlay" with-backdrop>
- <gr-confirm-delete-item-dialog
- class="confirmDialog"
- on-confirm="_handleDeleteConfirm"
- on-cancel="_handleConfirmDialogCancel"
- item="[[_itemName]]"
- item-type="[[_itemType]]"></gr-confirm-delete-item-dialog>
- </gr-overlay>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-group-members.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
index 8c29f73..fc9e4a4 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
@@ -14,280 +14,300 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- const SUGGESTIONS_LIMIT = 15;
- const SAVING_ERROR_TEXT = 'Group may not exist, or you may not have '+
- 'permission to add it';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../scripts/bundled-polymer.js';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/gr-subpage-styles.js';
+import '../../../styles/gr-table-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-account-link/gr-account-link.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-group-members_html.js';
- const URL_REGEX = '^(?:[a-z]+:)?//';
+const SUGGESTIONS_LIMIT = 15;
+const SAVING_ERROR_TEXT = 'Group may not exist, or you may not have '+
+ 'permission to add it';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrGroupMembers extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.FireBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-group-members'; }
+const URL_REGEX = '^(?:[a-z]+:)?//';
- static get properties() {
- return {
- groupId: Number,
- _groupMemberSearchId: String,
- _groupMemberSearchName: String,
- _includedGroupSearchId: String,
- _includedGroupSearchName: String,
- _loading: {
- type: Boolean,
- value: true,
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrGroupMembers extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.FireBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-group-members'; }
+
+ static get properties() {
+ return {
+ groupId: Number,
+ _groupMemberSearchId: String,
+ _groupMemberSearchName: String,
+ _includedGroupSearchId: String,
+ _includedGroupSearchName: String,
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _groupName: String,
+ _groupMembers: Object,
+ _includedGroups: Object,
+ _itemName: String,
+ _itemType: String,
+ _queryMembers: {
+ type: Function,
+ value() {
+ return this._getAccountSuggestions.bind(this);
},
- _groupName: String,
- _groupMembers: Object,
- _includedGroups: Object,
- _itemName: String,
- _itemType: String,
- _queryMembers: {
- type: Function,
- value() {
- return this._getAccountSuggestions.bind(this);
- },
+ },
+ _queryIncludedGroup: {
+ type: Function,
+ value() {
+ return this._getGroupSuggestions.bind(this);
},
- _queryIncludedGroup: {
- type: Function,
- value() {
- return this._getGroupSuggestions.bind(this);
- },
- },
- _groupOwner: {
- type: Boolean,
- value: false,
- },
- _isAdmin: {
- type: Boolean,
- value: false,
- },
- };
- }
+ },
+ _groupOwner: {
+ type: Boolean,
+ value: false,
+ },
+ _isAdmin: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
- /** @override */
- attached() {
- super.attached();
- this._loadGroupDetails();
+ /** @override */
+ attached() {
+ super.attached();
+ this._loadGroupDetails();
- this.fire('title-change', {title: 'Members'});
- }
+ this.fire('title-change', {title: 'Members'});
+ }
- _loadGroupDetails() {
- if (!this.groupId) { return; }
+ _loadGroupDetails() {
+ if (!this.groupId) { return; }
- const promises = [];
+ const promises = [];
- const errFn = response => {
- this.fire('page-error', {response});
- };
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
- return this.$.restAPI.getGroupConfig(this.groupId, errFn)
- .then(config => {
- if (!config || !config.name) { return Promise.resolve(); }
+ return this.$.restAPI.getGroupConfig(this.groupId, errFn)
+ .then(config => {
+ if (!config || !config.name) { return Promise.resolve(); }
- this._groupName = config.name;
+ this._groupName = config.name;
- promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
- this._isAdmin = isAdmin ? true : false;
- }));
+ promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
+ this._isAdmin = isAdmin ? true : false;
+ }));
- promises.push(this.$.restAPI.getIsGroupOwner(config.name)
- .then(isOwner => {
- this._groupOwner = isOwner ? true : false;
- }));
-
- promises.push(this.$.restAPI.getGroupMembers(config.name).then(
- members => {
- this._groupMembers = members;
- }));
-
- promises.push(this.$.restAPI.getIncludedGroup(config.name)
- .then(includedGroup => {
- this._includedGroups = includedGroup;
- }));
-
- return Promise.all(promises).then(() => {
- this._loading = false;
- });
- });
- }
-
- _computeLoadingClass(loading) {
- return loading ? 'loading' : '';
- }
-
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _computeGroupUrl(url) {
- if (!url) { return; }
-
- const r = new RegExp(URL_REGEX, 'i');
- if (r.test(url)) {
- return url;
- }
-
- // For GWT compatibility
- if (url.startsWith('#')) {
- return this.getBaseUrl() + url.slice(1);
- }
- return this.getBaseUrl() + url;
- }
-
- _handleSavingGroupMember() {
- return this.$.restAPI.saveGroupMembers(this._groupName,
- this._groupMemberSearchId).then(config => {
- if (!config) {
- return;
- }
- this.$.restAPI.getGroupMembers(this._groupName).then(members => {
- this._groupMembers = members;
- });
- this._groupMemberSearchName = '';
- this._groupMemberSearchId = '';
- });
- }
-
- _handleDeleteConfirm() {
- this.$.overlay.close();
- if (this._itemType === 'member') {
- return this.$.restAPI.deleteGroupMembers(this._groupName,
- this._itemId)
- .then(itemDeleted => {
- if (itemDeleted.status === 204) {
- this.$.restAPI.getGroupMembers(this._groupName)
- .then(members => {
- this._groupMembers = members;
- });
- }
- });
- } else if (this._itemType === 'includedGroup') {
- return this.$.restAPI.deleteIncludedGroup(this._groupName,
- this._itemId)
- .then(itemDeleted => {
- if (itemDeleted.status === 204 || itemDeleted.status === 205) {
- this.$.restAPI.getIncludedGroup(this._groupName)
- .then(includedGroup => {
- this._includedGroups = includedGroup;
- });
- }
- });
- }
- }
-
- _handleConfirmDialogCancel() {
- this.$.overlay.close();
- }
-
- _handleDeleteMember(e) {
- const id = e.model.get('item._account_id');
- const name = e.model.get('item.name');
- const username = e.model.get('item.username');
- const email = e.model.get('item.email');
- const item = username || name || email || id;
- if (!item) {
- return '';
- }
- this._itemName = item;
- this._itemId = id;
- this._itemType = 'member';
- this.$.overlay.open();
- }
-
- _handleSavingIncludedGroups() {
- return this.$.restAPI.saveIncludedGroup(this._groupName,
- this._includedGroupSearchId.replace(/\+/g, ' '), err => {
- if (err.status === 404) {
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {message: SAVING_ERROR_TEXT},
- bubbles: true,
- composed: true,
+ promises.push(this.$.restAPI.getIsGroupOwner(config.name)
+ .then(isOwner => {
+ this._groupOwner = isOwner ? true : false;
}));
- return err;
- }
- throw Error(err.statusText);
- })
- .then(config => {
- if (!config) {
- return;
- }
- this.$.restAPI.getIncludedGroup(this._groupName)
- .then(includedGroup => {
- this._includedGroups = includedGroup;
- });
- this._includedGroupSearchName = '';
- this._includedGroupSearchId = '';
+
+ promises.push(this.$.restAPI.getGroupMembers(config.name).then(
+ members => {
+ this._groupMembers = members;
+ }));
+
+ promises.push(this.$.restAPI.getIncludedGroup(config.name)
+ .then(includedGroup => {
+ this._includedGroups = includedGroup;
+ }));
+
+ return Promise.all(promises).then(() => {
+ this._loading = false;
});
+ });
+ }
+
+ _computeLoadingClass(loading) {
+ return loading ? 'loading' : '';
+ }
+
+ _isLoading() {
+ return this._loading || this._loading === undefined;
+ }
+
+ _computeGroupUrl(url) {
+ if (!url) { return; }
+
+ const r = new RegExp(URL_REGEX, 'i');
+ if (r.test(url)) {
+ return url;
}
- _handleDeleteIncludedGroup(e) {
- const id = decodeURIComponent(e.model.get('item.id')).replace(/\+/g, ' ');
- const name = e.model.get('item.name');
- const item = name || id;
- if (!item) { return ''; }
- this._itemName = item;
- this._itemId = id;
- this._itemType = 'includedGroup';
- this.$.overlay.open();
+ // For GWT compatibility
+ if (url.startsWith('#')) {
+ return this.getBaseUrl() + url.slice(1);
}
+ return this.getBaseUrl() + url;
+ }
- _getAccountSuggestions(input) {
- if (input.length === 0) { return Promise.resolve([]); }
- return this.$.restAPI.getSuggestedAccounts(
- input, SUGGESTIONS_LIMIT).then(accounts => {
- const accountSuggestions = [];
- let nameAndEmail;
- if (!accounts) { return []; }
- for (const key in accounts) {
- if (!accounts.hasOwnProperty(key)) { continue; }
- if (accounts[key].email !== undefined) {
- nameAndEmail = accounts[key].name +
- ' <' + accounts[key].email + '>';
- } else {
- nameAndEmail = accounts[key].name;
- }
- accountSuggestions.push({
- name: nameAndEmail,
- value: accounts[key]._account_id,
- });
- }
- return accountSuggestions;
+ _handleSavingGroupMember() {
+ return this.$.restAPI.saveGroupMembers(this._groupName,
+ this._groupMemberSearchId).then(config => {
+ if (!config) {
+ return;
+ }
+ this.$.restAPI.getGroupMembers(this._groupName).then(members => {
+ this._groupMembers = members;
});
- }
+ this._groupMemberSearchName = '';
+ this._groupMemberSearchId = '';
+ });
+ }
- _getGroupSuggestions(input) {
- return this.$.restAPI.getSuggestedGroups(input)
- .then(response => {
- const groups = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- groups.push({
- name: key,
- value: decodeURIComponent(response[key].id),
- });
+ _handleDeleteConfirm() {
+ this.$.overlay.close();
+ if (this._itemType === 'member') {
+ return this.$.restAPI.deleteGroupMembers(this._groupName,
+ this._itemId)
+ .then(itemDeleted => {
+ if (itemDeleted.status === 204) {
+ this.$.restAPI.getGroupMembers(this._groupName)
+ .then(members => {
+ this._groupMembers = members;
+ });
}
- return groups;
});
- }
-
- _computeHideItemClass(owner, admin) {
- return admin || owner ? '' : 'canModify';
+ } else if (this._itemType === 'includedGroup') {
+ return this.$.restAPI.deleteIncludedGroup(this._groupName,
+ this._itemId)
+ .then(itemDeleted => {
+ if (itemDeleted.status === 204 || itemDeleted.status === 205) {
+ this.$.restAPI.getIncludedGroup(this._groupName)
+ .then(includedGroup => {
+ this._includedGroups = includedGroup;
+ });
+ }
+ });
}
}
- customElements.define(GrGroupMembers.is, GrGroupMembers);
-})();
+ _handleConfirmDialogCancel() {
+ this.$.overlay.close();
+ }
+
+ _handleDeleteMember(e) {
+ const id = e.model.get('item._account_id');
+ const name = e.model.get('item.name');
+ const username = e.model.get('item.username');
+ const email = e.model.get('item.email');
+ const item = username || name || email || id;
+ if (!item) {
+ return '';
+ }
+ this._itemName = item;
+ this._itemId = id;
+ this._itemType = 'member';
+ this.$.overlay.open();
+ }
+
+ _handleSavingIncludedGroups() {
+ return this.$.restAPI.saveIncludedGroup(this._groupName,
+ this._includedGroupSearchId.replace(/\+/g, ' '), err => {
+ if (err.status === 404) {
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {message: SAVING_ERROR_TEXT},
+ bubbles: true,
+ composed: true,
+ }));
+ return err;
+ }
+ throw Error(err.statusText);
+ })
+ .then(config => {
+ if (!config) {
+ return;
+ }
+ this.$.restAPI.getIncludedGroup(this._groupName)
+ .then(includedGroup => {
+ this._includedGroups = includedGroup;
+ });
+ this._includedGroupSearchName = '';
+ this._includedGroupSearchId = '';
+ });
+ }
+
+ _handleDeleteIncludedGroup(e) {
+ const id = decodeURIComponent(e.model.get('item.id')).replace(/\+/g, ' ');
+ const name = e.model.get('item.name');
+ const item = name || id;
+ if (!item) { return ''; }
+ this._itemName = item;
+ this._itemId = id;
+ this._itemType = 'includedGroup';
+ this.$.overlay.open();
+ }
+
+ _getAccountSuggestions(input) {
+ if (input.length === 0) { return Promise.resolve([]); }
+ return this.$.restAPI.getSuggestedAccounts(
+ input, SUGGESTIONS_LIMIT).then(accounts => {
+ const accountSuggestions = [];
+ let nameAndEmail;
+ if (!accounts) { return []; }
+ for (const key in accounts) {
+ if (!accounts.hasOwnProperty(key)) { continue; }
+ if (accounts[key].email !== undefined) {
+ nameAndEmail = accounts[key].name +
+ ' <' + accounts[key].email + '>';
+ } else {
+ nameAndEmail = accounts[key].name;
+ }
+ accountSuggestions.push({
+ name: nameAndEmail,
+ value: accounts[key]._account_id,
+ });
+ }
+ return accountSuggestions;
+ });
+ }
+
+ _getGroupSuggestions(input) {
+ return this.$.restAPI.getSuggestedGroups(input)
+ .then(response => {
+ const groups = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ groups.push({
+ name: key,
+ value: decodeURIComponent(response[key].id),
+ });
+ }
+ return groups;
+ });
+ }
+
+ _computeHideItemClass(owner, admin) {
+ return admin || owner ? '' : 'canModify';
+ }
+}
+
+customElements.define(GrGroupMembers.is, GrGroupMembers);
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js
new file mode 100644
index 0000000..79a88fd
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js
@@ -0,0 +1,146 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="gr-form-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="gr-table-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="gr-subpage-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="shared-styles">
+ .input {
+ width: 15em;
+ }
+ gr-autocomplete {
+ width: 20em;
+ }
+ a {
+ color: var(--primary-text-color);
+ text-decoration: none;
+ }
+ a:hover {
+ text-decoration: underline;
+ }
+ th {
+ border-bottom: 1px solid var(--border-color);
+ font-weight: var(--font-weight-bold);
+ text-align: left;
+ }
+ .canModify #groupMemberSearchInput,
+ .canModify #saveGroupMember,
+ .canModify .deleteHeader,
+ .canModify .deleteColumn,
+ .canModify #includedGroupSearchInput,
+ .canModify #saveIncludedGroups,
+ .canModify .deleteIncludedHeader,
+ .canModify #saveIncludedGroups {
+ display: none;
+ }
+ </style>
+ <main class\$="gr-form-styles [[_computeHideItemClass(_groupOwner, _isAdmin)]]">
+ <div id="loading" class\$="[[_computeLoadingClass(_loading)]]">
+ Loading...
+ </div>
+ <div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]">
+ <h1 id="Title">[[_groupName]]</h1>
+ <div id="form">
+ <h3 id="members">Members</h3>
+ <fieldset>
+ <span class="value">
+ <gr-autocomplete id="groupMemberSearchInput" text="{{_groupMemberSearchName}}" value="{{_groupMemberSearchId}}" query="[[_queryMembers]]" placeholder="Name Or Email">
+ </gr-autocomplete>
+ </span>
+ <gr-button id="saveGroupMember" on-click="_handleSavingGroupMember" disabled="[[!_groupMemberSearchId]]">
+ Add
+ </gr-button>
+ <table id="groupMembers">
+ <tbody><tr class="headerRow">
+ <th class="nameHeader">Name</th>
+ <th class="emailAddressHeader">Email Address</th>
+ <th class="deleteHeader">Delete Member</th>
+ </tr>
+ </tbody><tbody>
+ <template is="dom-repeat" items="[[_groupMembers]]">
+ <tr>
+ <td class="nameColumn">
+ <gr-account-link account="[[item]]"></gr-account-link>
+ </td>
+ <td>[[item.email]]</td>
+ <td class="deleteColumn">
+ <gr-button class="deleteMembersButton" on-click="_handleDeleteMember">
+ Delete
+ </gr-button>
+ </td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ </fieldset>
+ <h3 id="includedGroups">Included Groups</h3>
+ <fieldset>
+ <span class="value">
+ <gr-autocomplete id="includedGroupSearchInput" text="{{_includedGroupSearchName}}" value="{{_includedGroupSearchId}}" query="[[_queryIncludedGroup]]" placeholder="Group Name">
+ </gr-autocomplete>
+ </span>
+ <gr-button id="saveIncludedGroups" on-click="_handleSavingIncludedGroups" disabled="[[!_includedGroupSearchId]]">
+ Add
+ </gr-button>
+ <table id="includedGroups">
+ <tbody><tr class="headerRow">
+ <th class="groupNameHeader">Group Name</th>
+ <th class="descriptionHeader">Description</th>
+ <th class="deleteIncludedHeader">
+ Delete Group
+ </th>
+ </tr>
+ </tbody><tbody>
+ <template is="dom-repeat" items="[[_includedGroups]]">
+ <tr>
+ <td class="nameColumn">
+ <template is="dom-if" if="[[item.url]]">
+ <a href\$="[[_computeGroupUrl(item.url)]]" rel="noopener">
+ [[item.name]]
+ </a>
+ </template>
+ <template is="dom-if" if="[[!item.url]]">
+ [[item.name]]
+ </template>
+ </td>
+ <td>[[item.description]]</td>
+ <td class="deleteColumn">
+ <gr-button class="deleteIncludedGroupButton" on-click="_handleDeleteIncludedGroup">
+ Delete
+ </gr-button>
+ </td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ </fieldset>
+ </div>
+ </div>
+ </main>
+ <gr-overlay id="overlay" with-backdrop="">
+ <gr-confirm-delete-item-dialog class="confirmDialog" on-confirm="_handleDeleteConfirm" on-cancel="_handleConfirmDialogCancel" item="[[_itemName]]" item-type="[[_itemType]]"></gr-confirm-delete-item-dialog>
+ </gr-overlay>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
index ec9a80c..e7c4afd 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-group-members</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-group-members.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,342 +30,344 @@
</template>
</test-fixture>
-<script>
- suite('gr-group-members tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let groups;
- let groupMembers;
- let includedGroups;
- let groupStub;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-group-members.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-group-members tests', () => {
+ let element;
+ let sandbox;
+ let groups;
+ let groupMembers;
+ let includedGroups;
+ let groupStub;
- setup(() => {
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ sandbox = sinon.sandbox.create();
- groups = {
- name: 'Administrators',
- owner: 'Administrators',
- group_id: 1,
- };
+ groups = {
+ name: 'Administrators',
+ owner: 'Administrators',
+ group_id: 1,
+ };
- groupMembers = [
- {
- _account_id: 1000097,
- name: 'Jane Roe',
- email: 'jane.roe@example.com',
- username: 'jane',
- },
- {
- _account_id: 1000096,
- name: 'Test User',
- email: 'john.doe@example.com',
- },
- {
- _account_id: 1000095,
- name: 'Gerrit',
- },
- {
- _account_id: 1000098,
- },
- ];
-
- includedGroups = [{
- url: 'https://group/url',
- options: {},
- id: 'testId',
- name: 'testName',
+ groupMembers = [
+ {
+ _account_id: 1000097,
+ name: 'Jane Roe',
+ email: 'jane.roe@example.com',
+ username: 'jane',
},
{
- url: '/group/url',
- options: {},
- id: 'testId2',
- name: 'testName2',
+ _account_id: 1000096,
+ name: 'Test User',
+ email: 'john.doe@example.com',
},
{
- url: '#/group/url',
- options: {},
- id: 'testId3',
- name: 'testName3',
+ _account_id: 1000095,
+ name: 'Gerrit',
},
- ];
+ {
+ _account_id: 1000098,
+ },
+ ];
- stub('gr-rest-api-interface', {
- getSuggestedAccounts(input) {
- if (input.startsWith('test')) {
- return Promise.resolve([
- {
- _account_id: 1000096,
- name: 'test-account',
- email: 'test.account@example.com',
- username: 'test123',
- },
- {
- _account_id: 1001439,
- name: 'test-admin',
- email: 'test.admin@example.com',
- username: 'test_admin',
- },
- {
- _account_id: 1001439,
- name: 'test-git',
- username: 'test_git',
- },
- ]);
- } else {
- return Promise.resolve({});
- }
- },
- getSuggestedGroups(input) {
- if (input.startsWith('test')) {
- return Promise.resolve({
- 'test-admin': {
- id: '1ce023d3fb4e4260776fb92cd08b52bbd21ce70a',
- },
- 'test/Administrator (admin)': {
- id: 'test%3Aadmin',
- },
- });
- } else {
- return Promise.resolve({});
- }
- },
- getLoggedIn() { return Promise.resolve(true); },
- getConfig() {
- return Promise.resolve();
- },
- getGroupMembers() {
- return Promise.resolve(groupMembers);
- },
- getIsGroupOwner() {
- return Promise.resolve(true);
- },
- getIncludedGroup() {
- return Promise.resolve(includedGroups);
- },
- getAccountCapabilities() {
- return Promise.resolve();
- },
- });
- element = fixture('basic');
- sandbox.stub(element, 'getBaseUrl').returns('https://test/site');
- element.groupId = 1;
- groupStub = sandbox.stub(
- element.$.restAPI,
- 'getGroupConfig',
- () => Promise.resolve(groups));
- return element._loadGroupDetails();
+ includedGroups = [{
+ url: 'https://group/url',
+ options: {},
+ id: 'testId',
+ name: 'testName',
+ },
+ {
+ url: '/group/url',
+ options: {},
+ id: 'testId2',
+ name: 'testName2',
+ },
+ {
+ url: '#/group/url',
+ options: {},
+ id: 'testId3',
+ name: 'testName3',
+ },
+ ];
+
+ stub('gr-rest-api-interface', {
+ getSuggestedAccounts(input) {
+ if (input.startsWith('test')) {
+ return Promise.resolve([
+ {
+ _account_id: 1000096,
+ name: 'test-account',
+ email: 'test.account@example.com',
+ username: 'test123',
+ },
+ {
+ _account_id: 1001439,
+ name: 'test-admin',
+ email: 'test.admin@example.com',
+ username: 'test_admin',
+ },
+ {
+ _account_id: 1001439,
+ name: 'test-git',
+ username: 'test_git',
+ },
+ ]);
+ } else {
+ return Promise.resolve({});
+ }
+ },
+ getSuggestedGroups(input) {
+ if (input.startsWith('test')) {
+ return Promise.resolve({
+ 'test-admin': {
+ id: '1ce023d3fb4e4260776fb92cd08b52bbd21ce70a',
+ },
+ 'test/Administrator (admin)': {
+ id: 'test%3Aadmin',
+ },
+ });
+ } else {
+ return Promise.resolve({});
+ }
+ },
+ getLoggedIn() { return Promise.resolve(true); },
+ getConfig() {
+ return Promise.resolve();
+ },
+ getGroupMembers() {
+ return Promise.resolve(groupMembers);
+ },
+ getIsGroupOwner() {
+ return Promise.resolve(true);
+ },
+ getIncludedGroup() {
+ return Promise.resolve(includedGroups);
+ },
+ getAccountCapabilities() {
+ return Promise.resolve();
+ },
});
+ element = fixture('basic');
+ sandbox.stub(element, 'getBaseUrl').returns('https://test/site');
+ element.groupId = 1;
+ groupStub = sandbox.stub(
+ element.$.restAPI,
+ 'getGroupConfig',
+ () => Promise.resolve(groups));
+ return element._loadGroupDetails();
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('_includedGroups', () => {
- assert.equal(element._includedGroups.length, 3);
- assert.equal(Polymer.dom(element.root)
- .querySelectorAll('.nameColumn a')[0].href, includedGroups[0].url);
- assert.equal(Polymer.dom(element.root)
- .querySelectorAll('.nameColumn a')[1].href,
- 'https://test/site/group/url');
- assert.equal(Polymer.dom(element.root)
- .querySelectorAll('.nameColumn a')[2].href,
- 'https://test/site/group/url');
- });
+ test('_includedGroups', () => {
+ assert.equal(element._includedGroups.length, 3);
+ assert.equal(dom(element.root)
+ .querySelectorAll('.nameColumn a')[0].href, includedGroups[0].url);
+ assert.equal(dom(element.root)
+ .querySelectorAll('.nameColumn a')[1].href,
+ 'https://test/site/group/url');
+ assert.equal(dom(element.root)
+ .querySelectorAll('.nameColumn a')[2].href,
+ 'https://test/site/group/url');
+ });
- test('save members correctly', () => {
- element._groupOwner = true;
+ test('save members correctly', () => {
+ element._groupOwner = true;
- const memberName = 'test-admin';
+ const memberName = 'test-admin';
- const saveStub = sandbox.stub(element.$.restAPI, 'saveGroupMembers',
- () => Promise.resolve({}));
+ const saveStub = sandbox.stub(element.$.restAPI, 'saveGroupMembers',
+ () => Promise.resolve({}));
- const button = element.$.saveGroupMember;
+ const button = element.$.saveGroupMember;
+ assert.isTrue(button.hasAttribute('disabled'));
+
+ element.$.groupMemberSearchInput.text = memberName;
+ element.$.groupMemberSearchInput.value = 1234;
+
+ assert.isFalse(button.hasAttribute('disabled'));
+
+ return element._handleSavingGroupMember().then(() => {
assert.isTrue(button.hasAttribute('disabled'));
-
- element.$.groupMemberSearchInput.text = memberName;
- element.$.groupMemberSearchInput.value = 1234;
-
- assert.isFalse(button.hasAttribute('disabled'));
-
- return element._handleSavingGroupMember().then(() => {
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- assert.isTrue(saveStub.lastCall.calledWithExactly('Administrators',
- 1234));
- });
- });
-
- test('save included groups correctly', () => {
- element._groupOwner = true;
-
- const includedGroupName = 'testName';
-
- const saveIncludedGroupStub = sandbox.stub(
- element.$.restAPI, 'saveIncludedGroup', () => Promise.resolve({}));
-
- const button = element.$.saveIncludedGroups;
-
- assert.isTrue(button.hasAttribute('disabled'));
-
- element.$.includedGroupSearchInput.text = includedGroupName;
- element.$.includedGroupSearchInput.value = 'testId';
-
- assert.isFalse(button.hasAttribute('disabled'));
-
- return element._handleSavingIncludedGroups().then(() => {
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- assert.equal(saveIncludedGroupStub.lastCall.args[0], 'Administrators');
- assert.equal(saveIncludedGroupStub.lastCall.args[1], 'testId');
- });
- });
-
- test('add included group 404 shows helpful error text', () => {
- element._groupOwner = true;
-
- const memberName = 'bad-name';
- const alertStub = sandbox.stub();
- element.addEventListener('show-alert', alertStub);
- const error = new Error('error');
- error.status = 404;
- sandbox.stub(element.$.restAPI, 'saveGroupMembers',
- () => Promise.reject(error));
-
- element.$.groupMemberSearchInput.text = memberName;
- element.$.groupMemberSearchInput.value = 1234;
-
- return element._handleSavingIncludedGroups().then(() => {
- assert.isTrue(alertStub.called);
- });
- });
-
- test('_getAccountSuggestions empty', done => {
- element
- ._getAccountSuggestions('nonexistent').then(accounts => {
- assert.equal(accounts.length, 0);
- done();
- });
- });
-
- test('_getAccountSuggestions non-empty', done => {
- element
- ._getAccountSuggestions('test-').then(accounts => {
- assert.equal(accounts.length, 3);
- assert.equal(accounts[0].name,
- 'test-account <test.account@example.com>');
- assert.equal(accounts[1].name, 'test-admin <test.admin@example.com>');
- assert.equal(accounts[2].name, 'test-git');
- done();
- });
- });
-
- test('_getGroupSuggestions empty', done => {
- element
- ._getGroupSuggestions('nonexistent').then(groups => {
- assert.equal(groups.length, 0);
- done();
- });
- });
-
- test('_getGroupSuggestions non-empty', done => {
- element
- ._getGroupSuggestions('test').then(groups => {
- assert.equal(groups.length, 2);
- assert.equal(groups[0].name, 'test-admin');
- assert.equal(groups[1].name, 'test/Administrator (admin)');
- done();
- });
- });
-
- test('_computeHideItemClass returns string for admin', () => {
- const admin = true;
- const owner = false;
- assert.equal(element._computeHideItemClass(owner, admin), '');
- });
-
- test('_computeHideItemClass returns hideItem for admin and owner', () => {
- const admin = false;
- const owner = false;
- assert.equal(element._computeHideItemClass(owner, admin), 'canModify');
- });
-
- test('_computeHideItemClass returns string for owner', () => {
- const admin = false;
- const owner = true;
- assert.equal(element._computeHideItemClass(owner, admin), '');
- });
-
- test('delete member', () => {
- const deletelBtns = Polymer.dom(element.root)
- .querySelectorAll('.deleteMembersButton');
- MockInteractions.tap(deletelBtns[0]);
- assert.equal(element._itemId, '1000097');
- assert.equal(element._itemName, 'jane');
- MockInteractions.tap(deletelBtns[1]);
- assert.equal(element._itemId, '1000096');
- assert.equal(element._itemName, 'Test User');
- MockInteractions.tap(deletelBtns[2]);
- assert.equal(element._itemId, '1000095');
- assert.equal(element._itemName, 'Gerrit');
- MockInteractions.tap(deletelBtns[3]);
- assert.equal(element._itemId, '1000098');
- assert.equal(element._itemName, '1000098');
- });
-
- test('delete included groups', () => {
- const deletelBtns = Polymer.dom(element.root)
- .querySelectorAll('.deleteIncludedGroupButton');
- MockInteractions.tap(deletelBtns[0]);
- assert.equal(element._itemId, 'testId');
- assert.equal(element._itemName, 'testName');
- MockInteractions.tap(deletelBtns[1]);
- assert.equal(element._itemId, 'testId2');
- assert.equal(element._itemName, 'testName2');
- MockInteractions.tap(deletelBtns[2]);
- assert.equal(element._itemId, 'testId3');
- assert.equal(element._itemName, 'testName3');
- });
-
- test('_computeLoadingClass', () => {
- assert.equal(element._computeLoadingClass(true), 'loading');
-
- assert.equal(element._computeLoadingClass(false), '');
- });
-
- test('_computeGroupUrl', () => {
- assert.isUndefined(element._computeGroupUrl(undefined));
-
- assert.isUndefined(element._computeGroupUrl(false));
-
- let url = '#/admin/groups/uuid-529b3c2605bb1029c8146f9de4a91c776fe64498';
- assert.equal(element._computeGroupUrl(url),
- 'https://test/site/admin/groups/' +
- 'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498');
-
- url = 'https://gerrit.local/admin/groups/' +
- 'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498';
- assert.equal(element._computeGroupUrl(url), url);
- });
-
- test('fires page-error', done => {
- groupStub.restore();
-
- element.groupId = 1;
-
- const response = {status: 404};
- sandbox.stub(
- element.$.restAPI, 'getGroupConfig', (group, errFn) => {
- errFn(response);
- });
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- element._loadGroupDetails();
+ assert.isFalse(element.$.Title.classList.contains('edited'));
+ assert.isTrue(saveStub.lastCall.calledWithExactly('Administrators',
+ 1234));
});
});
+
+ test('save included groups correctly', () => {
+ element._groupOwner = true;
+
+ const includedGroupName = 'testName';
+
+ const saveIncludedGroupStub = sandbox.stub(
+ element.$.restAPI, 'saveIncludedGroup', () => Promise.resolve({}));
+
+ const button = element.$.saveIncludedGroups;
+
+ assert.isTrue(button.hasAttribute('disabled'));
+
+ element.$.includedGroupSearchInput.text = includedGroupName;
+ element.$.includedGroupSearchInput.value = 'testId';
+
+ assert.isFalse(button.hasAttribute('disabled'));
+
+ return element._handleSavingIncludedGroups().then(() => {
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(element.$.Title.classList.contains('edited'));
+ assert.equal(saveIncludedGroupStub.lastCall.args[0], 'Administrators');
+ assert.equal(saveIncludedGroupStub.lastCall.args[1], 'testId');
+ });
+ });
+
+ test('add included group 404 shows helpful error text', () => {
+ element._groupOwner = true;
+
+ const memberName = 'bad-name';
+ const alertStub = sandbox.stub();
+ element.addEventListener('show-alert', alertStub);
+ const error = new Error('error');
+ error.status = 404;
+ sandbox.stub(element.$.restAPI, 'saveGroupMembers',
+ () => Promise.reject(error));
+
+ element.$.groupMemberSearchInput.text = memberName;
+ element.$.groupMemberSearchInput.value = 1234;
+
+ return element._handleSavingIncludedGroups().then(() => {
+ assert.isTrue(alertStub.called);
+ });
+ });
+
+ test('_getAccountSuggestions empty', done => {
+ element
+ ._getAccountSuggestions('nonexistent').then(accounts => {
+ assert.equal(accounts.length, 0);
+ done();
+ });
+ });
+
+ test('_getAccountSuggestions non-empty', done => {
+ element
+ ._getAccountSuggestions('test-').then(accounts => {
+ assert.equal(accounts.length, 3);
+ assert.equal(accounts[0].name,
+ 'test-account <test.account@example.com>');
+ assert.equal(accounts[1].name, 'test-admin <test.admin@example.com>');
+ assert.equal(accounts[2].name, 'test-git');
+ done();
+ });
+ });
+
+ test('_getGroupSuggestions empty', done => {
+ element
+ ._getGroupSuggestions('nonexistent').then(groups => {
+ assert.equal(groups.length, 0);
+ done();
+ });
+ });
+
+ test('_getGroupSuggestions non-empty', done => {
+ element
+ ._getGroupSuggestions('test').then(groups => {
+ assert.equal(groups.length, 2);
+ assert.equal(groups[0].name, 'test-admin');
+ assert.equal(groups[1].name, 'test/Administrator (admin)');
+ done();
+ });
+ });
+
+ test('_computeHideItemClass returns string for admin', () => {
+ const admin = true;
+ const owner = false;
+ assert.equal(element._computeHideItemClass(owner, admin), '');
+ });
+
+ test('_computeHideItemClass returns hideItem for admin and owner', () => {
+ const admin = false;
+ const owner = false;
+ assert.equal(element._computeHideItemClass(owner, admin), 'canModify');
+ });
+
+ test('_computeHideItemClass returns string for owner', () => {
+ const admin = false;
+ const owner = true;
+ assert.equal(element._computeHideItemClass(owner, admin), '');
+ });
+
+ test('delete member', () => {
+ const deletelBtns = dom(element.root)
+ .querySelectorAll('.deleteMembersButton');
+ MockInteractions.tap(deletelBtns[0]);
+ assert.equal(element._itemId, '1000097');
+ assert.equal(element._itemName, 'jane');
+ MockInteractions.tap(deletelBtns[1]);
+ assert.equal(element._itemId, '1000096');
+ assert.equal(element._itemName, 'Test User');
+ MockInteractions.tap(deletelBtns[2]);
+ assert.equal(element._itemId, '1000095');
+ assert.equal(element._itemName, 'Gerrit');
+ MockInteractions.tap(deletelBtns[3]);
+ assert.equal(element._itemId, '1000098');
+ assert.equal(element._itemName, '1000098');
+ });
+
+ test('delete included groups', () => {
+ const deletelBtns = dom(element.root)
+ .querySelectorAll('.deleteIncludedGroupButton');
+ MockInteractions.tap(deletelBtns[0]);
+ assert.equal(element._itemId, 'testId');
+ assert.equal(element._itemName, 'testName');
+ MockInteractions.tap(deletelBtns[1]);
+ assert.equal(element._itemId, 'testId2');
+ assert.equal(element._itemName, 'testName2');
+ MockInteractions.tap(deletelBtns[2]);
+ assert.equal(element._itemId, 'testId3');
+ assert.equal(element._itemName, 'testName3');
+ });
+
+ test('_computeLoadingClass', () => {
+ assert.equal(element._computeLoadingClass(true), 'loading');
+
+ assert.equal(element._computeLoadingClass(false), '');
+ });
+
+ test('_computeGroupUrl', () => {
+ assert.isUndefined(element._computeGroupUrl(undefined));
+
+ assert.isUndefined(element._computeGroupUrl(false));
+
+ let url = '#/admin/groups/uuid-529b3c2605bb1029c8146f9de4a91c776fe64498';
+ assert.equal(element._computeGroupUrl(url),
+ 'https://test/site/admin/groups/' +
+ 'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498');
+
+ url = 'https://gerrit.local/admin/groups/' +
+ 'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498';
+ assert.equal(element._computeGroupUrl(url), url);
+ });
+
+ test('fires page-error', done => {
+ groupStub.restore();
+
+ element.groupId = 1;
+
+ const response = {status: 404};
+ sandbox.stub(
+ element.$.restAPI, 'getGroupConfig', (group, errFn) => {
+ errFn(response);
+ });
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
+ });
+
+ element._loadGroupDetails();
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
deleted file mode 100644
index faabe84..0000000
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
+++ /dev/null
@@ -1,152 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/gr-subpage-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-
-<dom-module id="gr-group">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-subpage-styles">
- h3.edited:after {
- color: var(--deemphasized-text-color);
- content: ' *';
- }
- .inputUpdateBtn {
- margin-top: var(--spacing-s);
- }
- </style>
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <main class="gr-form-styles read-only">
- <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
- Loading...
- </div>
- <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <h1 id="Title">[[_groupName]]</h1>
- <h2 id="configurations">General</h2>
- <div id="form">
- <fieldset>
- <h3 id="groupUUID">Group UUID</h3>
- <fieldset>
- <gr-copy-clipboard
- text="[[groupId]]"></gr-copy-clipboard>
- </fieldset>
- <h3 id="groupName" class$="[[_computeHeaderClass(_rename)]]">
- Group Name
- </h3>
- <fieldset>
- <span class="value">
- <gr-autocomplete
- id="groupNameInput"
- text="{{_groupConfig.name}}"
- disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></gr-autocomplete>
- </span>
- <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
- <gr-button
- id="inputUpdateNameBtn"
- on-click="_handleSaveName"
- disabled="[[!_rename]]">
- Rename Group</gr-button>
- </span>
- </fieldset>
- <h3 class$="[[_computeHeaderClass(_owner)]]">
- Owners
- </h3>
- <fieldset>
- <span class="value">
- <gr-autocomplete
- id="groupOwnerInput"
- text="{{_groupConfig.owner}}"
- value="{{_groupConfigOwner}}"
- query="[[_query]]"
- disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
- </gr-autocomplete>
- </span>
- <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
- <gr-button
- on-click="_handleSaveOwner"
- disabled="[[!_owner]]">
- Change Owners</gr-button>
- </span>
- </fieldset>
- <h3 class$="[[_computeHeaderClass(_description)]]">
- Description
- </h3>
- <fieldset>
- <div>
- <iron-autogrow-textarea
- class="description"
- autocomplete="on"
- bind-value="{{_groupConfig.description}}"
- disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></iron-autogrow-textarea>
- </div>
- <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
- <gr-button
- on-click="_handleSaveDescription"
- disabled="[[!_description]]">
- Save Description
- </gr-button>
- </span>
- </fieldset>
- <h3 id="options" class$="[[_computeHeaderClass(_options)]]">
- Group Options
- </h3>
- <fieldset id="visableToAll">
- <section>
- <span class="title">
- Make group visible to all registered users
- </span>
- <span class="value">
- <gr-select
- id="visibleToAll"
- bind-value="{{_groupConfig.options.visible_to_all}}">
- <select disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
- <template is="dom-repeat" items="[[_submitTypes]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
- <gr-button
- on-click="_handleSaveOptions"
- disabled="[[!_options]]">
- Save Group Options
- </gr-button>
- </span>
- </fieldset>
- </fieldset>
- </div>
- </div>
- </main>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-group.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
index 42846f4..5127733 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
@@ -14,238 +14,253 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
- const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
+import '../../../scripts/bundled-polymer.js';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/gr-subpage-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-group_html.js';
- const OPTIONS = {
- submitFalse: {
- value: false,
- label: 'False',
- },
- submitTrue: {
- value: true,
- label: 'True',
- },
- };
+const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
+const OPTIONS = {
+ submitFalse: {
+ value: false,
+ label: 'False',
+ },
+ submitTrue: {
+ value: true,
+ label: 'True',
+ },
+};
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrGroup extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-group'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the group name changes.
+ *
+ * @event name-changed
*/
- class GrGroup extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-group'; }
- /**
- * Fired when the group name changes.
- *
- * @event name-changed
- */
- static get properties() {
- return {
- groupId: Number,
- _rename: {
- type: Boolean,
- value: false,
+ static get properties() {
+ return {
+ groupId: Number,
+ _rename: {
+ type: Boolean,
+ value: false,
+ },
+ _groupIsInternal: Boolean,
+ _description: {
+ type: Boolean,
+ value: false,
+ },
+ _owner: {
+ type: Boolean,
+ value: false,
+ },
+ _options: {
+ type: Boolean,
+ value: false,
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ /** @type {?} */
+ _groupConfig: Object,
+ _groupConfigOwner: String,
+ _groupName: Object,
+ _groupOwner: {
+ type: Boolean,
+ value: false,
+ },
+ _submitTypes: {
+ type: Array,
+ value() {
+ return Object.values(OPTIONS);
},
- _groupIsInternal: Boolean,
- _description: {
- type: Boolean,
- value: false,
+ },
+ _query: {
+ type: Function,
+ value() {
+ return this._getGroupSuggestions.bind(this);
},
- _owner: {
- type: Boolean,
- value: false,
- },
- _options: {
- type: Boolean,
- value: false,
- },
- _loading: {
- type: Boolean,
- value: true,
- },
- /** @type {?} */
- _groupConfig: Object,
- _groupConfigOwner: String,
- _groupName: Object,
- _groupOwner: {
- type: Boolean,
- value: false,
- },
- _submitTypes: {
- type: Array,
- value() {
- return Object.values(OPTIONS);
- },
- },
- _query: {
- type: Function,
- value() {
- return this._getGroupSuggestions.bind(this);
- },
- },
- _isAdmin: {
- type: Boolean,
- value: false,
- },
- };
- }
-
- static get observers() {
- return [
- '_handleConfigName(_groupConfig.name)',
- '_handleConfigOwner(_groupConfig.owner, _groupConfigOwner)',
- '_handleConfigDescription(_groupConfig.description)',
- '_handleConfigOptions(_groupConfig.options.visible_to_all)',
- ];
- }
-
- /** @override */
- attached() {
- super.attached();
- this._loadGroup();
- }
-
- _loadGroup() {
- if (!this.groupId) { return; }
-
- const promises = [];
-
- const errFn = response => {
- this.fire('page-error', {response});
- };
-
- return this.$.restAPI.getGroupConfig(this.groupId, errFn)
- .then(config => {
- if (!config || !config.name) { return Promise.resolve(); }
-
- this._groupName = config.name;
- this._groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
-
- promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
- this._isAdmin = isAdmin ? true : false;
- }));
-
- promises.push(this.$.restAPI.getIsGroupOwner(config.name)
- .then(isOwner => {
- this._groupOwner = isOwner ? true : false;
- }));
-
- // If visible to all is undefined, set to false. If it is defined
- // as false, setting to false is fine. If any optional values
- // are added with a default of true, then this would need to be an
- // undefined check and not a truthy/falsy check.
- if (!config.options.visible_to_all) {
- config.options.visible_to_all = false;
- }
- this._groupConfig = config;
-
- this.fire('title-change', {title: config.name});
-
- return Promise.all(promises).then(() => {
- this._loading = false;
- });
- });
- }
-
- _computeLoadingClass(loading) {
- return loading ? 'loading' : '';
- }
-
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _handleSaveName() {
- return this.$.restAPI.saveGroupName(this.groupId, this._groupConfig.name)
- .then(config => {
- if (config.status === 200) {
- this._groupName = this._groupConfig.name;
- this.fire('name-changed', {name: this._groupConfig.name,
- external: this._groupIsExtenral});
- this._rename = false;
- }
- });
- }
-
- _handleSaveOwner() {
- let owner = this._groupConfig.owner;
- if (this._groupConfigOwner) {
- owner = decodeURIComponent(this._groupConfigOwner);
- }
- return this.$.restAPI.saveGroupOwner(this.groupId,
- owner).then(config => {
- this._owner = false;
- });
- }
-
- _handleSaveDescription() {
- return this.$.restAPI.saveGroupDescription(this.groupId,
- this._groupConfig.description).then(config => {
- this._description = false;
- });
- }
-
- _handleSaveOptions() {
- const visible = this._groupConfig.options.visible_to_all;
-
- const options = {visible_to_all: visible};
-
- return this.$.restAPI.saveGroupOptions(this.groupId,
- options).then(config => {
- this._options = false;
- });
- }
-
- _handleConfigName() {
- if (this._isLoading()) { return; }
- this._rename = true;
- }
-
- _handleConfigOwner() {
- if (this._isLoading()) { return; }
- this._owner = true;
- }
-
- _handleConfigDescription() {
- if (this._isLoading()) { return; }
- this._description = true;
- }
-
- _handleConfigOptions() {
- if (this._isLoading()) { return; }
- this._options = true;
- }
-
- _computeHeaderClass(configChanged) {
- return configChanged ? 'edited' : '';
- }
-
- _getGroupSuggestions(input) {
- return this.$.restAPI.getSuggestedGroups(input)
- .then(response => {
- const groups = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- groups.push({
- name: key,
- value: decodeURIComponent(response[key].id),
- });
- }
- return groups;
- });
- }
-
- _computeGroupDisabled(owner, admin, groupIsInternal) {
- return groupIsInternal && (admin || owner) ? false : true;
- }
+ },
+ _isAdmin: {
+ type: Boolean,
+ value: false,
+ },
+ };
}
- customElements.define(GrGroup.is, GrGroup);
-})();
+ static get observers() {
+ return [
+ '_handleConfigName(_groupConfig.name)',
+ '_handleConfigOwner(_groupConfig.owner, _groupConfigOwner)',
+ '_handleConfigDescription(_groupConfig.description)',
+ '_handleConfigOptions(_groupConfig.options.visible_to_all)',
+ ];
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._loadGroup();
+ }
+
+ _loadGroup() {
+ if (!this.groupId) { return; }
+
+ const promises = [];
+
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
+
+ return this.$.restAPI.getGroupConfig(this.groupId, errFn)
+ .then(config => {
+ if (!config || !config.name) { return Promise.resolve(); }
+
+ this._groupName = config.name;
+ this._groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
+
+ promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
+ this._isAdmin = isAdmin ? true : false;
+ }));
+
+ promises.push(this.$.restAPI.getIsGroupOwner(config.name)
+ .then(isOwner => {
+ this._groupOwner = isOwner ? true : false;
+ }));
+
+ // If visible to all is undefined, set to false. If it is defined
+ // as false, setting to false is fine. If any optional values
+ // are added with a default of true, then this would need to be an
+ // undefined check and not a truthy/falsy check.
+ if (!config.options.visible_to_all) {
+ config.options.visible_to_all = false;
+ }
+ this._groupConfig = config;
+
+ this.fire('title-change', {title: config.name});
+
+ return Promise.all(promises).then(() => {
+ this._loading = false;
+ });
+ });
+ }
+
+ _computeLoadingClass(loading) {
+ return loading ? 'loading' : '';
+ }
+
+ _isLoading() {
+ return this._loading || this._loading === undefined;
+ }
+
+ _handleSaveName() {
+ return this.$.restAPI.saveGroupName(this.groupId, this._groupConfig.name)
+ .then(config => {
+ if (config.status === 200) {
+ this._groupName = this._groupConfig.name;
+ this.fire('name-changed', {name: this._groupConfig.name,
+ external: this._groupIsExtenral});
+ this._rename = false;
+ }
+ });
+ }
+
+ _handleSaveOwner() {
+ let owner = this._groupConfig.owner;
+ if (this._groupConfigOwner) {
+ owner = decodeURIComponent(this._groupConfigOwner);
+ }
+ return this.$.restAPI.saveGroupOwner(this.groupId,
+ owner).then(config => {
+ this._owner = false;
+ });
+ }
+
+ _handleSaveDescription() {
+ return this.$.restAPI.saveGroupDescription(this.groupId,
+ this._groupConfig.description).then(config => {
+ this._description = false;
+ });
+ }
+
+ _handleSaveOptions() {
+ const visible = this._groupConfig.options.visible_to_all;
+
+ const options = {visible_to_all: visible};
+
+ return this.$.restAPI.saveGroupOptions(this.groupId,
+ options).then(config => {
+ this._options = false;
+ });
+ }
+
+ _handleConfigName() {
+ if (this._isLoading()) { return; }
+ this._rename = true;
+ }
+
+ _handleConfigOwner() {
+ if (this._isLoading()) { return; }
+ this._owner = true;
+ }
+
+ _handleConfigDescription() {
+ if (this._isLoading()) { return; }
+ this._description = true;
+ }
+
+ _handleConfigOptions() {
+ if (this._isLoading()) { return; }
+ this._options = true;
+ }
+
+ _computeHeaderClass(configChanged) {
+ return configChanged ? 'edited' : '';
+ }
+
+ _getGroupSuggestions(input) {
+ return this.$.restAPI.getSuggestedGroups(input)
+ .then(response => {
+ const groups = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ groups.push({
+ name: key,
+ value: decodeURIComponent(response[key].id),
+ });
+ }
+ return groups;
+ });
+ }
+
+ _computeGroupDisabled(owner, admin, groupIsInternal) {
+ return groupIsInternal && (admin || owner) ? false : true;
+ }
+}
+
+customElements.define(GrGroup.is, GrGroup);
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
new file mode 100644
index 0000000..dc80235
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
@@ -0,0 +1,115 @@
+/**
+ * @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.js';
+
+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-subpage-styles">
+ h3.edited:after {
+ color: var(--deemphasized-text-color);
+ content: ' *';
+ }
+ .inputUpdateBtn {
+ margin-top: var(--spacing-s);
+ }
+ </style>
+ <style include="gr-form-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <main class="gr-form-styles read-only">
+ <div id="loading" class\$="[[_computeLoadingClass(_loading)]]">
+ Loading...
+ </div>
+ <div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]">
+ <h1 id="Title">[[_groupName]]</h1>
+ <h2 id="configurations">General</h2>
+ <div id="form">
+ <fieldset>
+ <h3 id="groupUUID">Group UUID</h3>
+ <fieldset>
+ <gr-copy-clipboard text="[[groupId]]"></gr-copy-clipboard>
+ </fieldset>
+ <h3 id="groupName" class\$="[[_computeHeaderClass(_rename)]]">
+ Group Name
+ </h3>
+ <fieldset>
+ <span class="value">
+ <gr-autocomplete id="groupNameInput" text="{{_groupConfig.name}}" disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></gr-autocomplete>
+ </span>
+ <span class="value" disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
+ <gr-button id="inputUpdateNameBtn" on-click="_handleSaveName" disabled="[[!_rename]]">
+ Rename Group</gr-button>
+ </span>
+ </fieldset>
+ <h3 class\$="[[_computeHeaderClass(_owner)]]">
+ Owners
+ </h3>
+ <fieldset>
+ <span class="value">
+ <gr-autocomplete id="groupOwnerInput" text="{{_groupConfig.owner}}" value="{{_groupConfigOwner}}" query="[[_query]]" disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
+ </gr-autocomplete>
+ </span>
+ <span class="value" disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
+ <gr-button on-click="_handleSaveOwner" disabled="[[!_owner]]">
+ Change Owners</gr-button>
+ </span>
+ </fieldset>
+ <h3 class\$="[[_computeHeaderClass(_description)]]">
+ Description
+ </h3>
+ <fieldset>
+ <div>
+ <iron-autogrow-textarea class="description" autocomplete="on" bind-value="{{_groupConfig.description}}" disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></iron-autogrow-textarea>
+ </div>
+ <span class="value" disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
+ <gr-button on-click="_handleSaveDescription" disabled="[[!_description]]">
+ Save Description
+ </gr-button>
+ </span>
+ </fieldset>
+ <h3 id="options" class\$="[[_computeHeaderClass(_options)]]">
+ Group Options
+ </h3>
+ <fieldset id="visableToAll">
+ <section>
+ <span class="title">
+ Make group visible to all registered users
+ </span>
+ <span class="value">
+ <gr-select id="visibleToAll" bind-value="{{_groupConfig.options.visible_to_all}}">
+ <select disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
+ <template is="dom-repeat" items="[[_submitTypes]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <span class="value" disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
+ <gr-button on-click="_handleSaveOptions" disabled="[[!_options]]">
+ Save Group Options
+ </gr-button>
+ </span>
+ </fieldset>
+ </fieldset>
+ </div>
+ </div>
+ </main>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
index a6aebbf..6461833 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-group</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-group.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,222 +30,223 @@
</template>
</test-fixture>
-<script>
- suite('gr-group tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let groupStub;
- const group = {
- id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
- url: '#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389',
- options: {},
- description: 'Gerrit Site Administrators',
- group_id: 1,
- owner: 'Administrators',
- owner_id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
- name: 'Administrators',
- };
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-group.js';
+suite('gr-group tests', () => {
+ let element;
+ let sandbox;
+ let groupStub;
+ const group = {
+ id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
+ url: '#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389',
+ options: {},
+ description: 'Gerrit Site Administrators',
+ group_id: 1,
+ owner: 'Administrators',
+ owner_id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
+ name: 'Administrators',
+ };
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- });
- element = fixture('basic');
- groupStub = sandbox.stub(
- element.$.restAPI,
- 'getGroupConfig',
- () => Promise.resolve(group)
- );
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
});
+ element = fixture('basic');
+ groupStub = sandbox.stub(
+ element.$.restAPI,
+ 'getGroupConfig',
+ () => Promise.resolve(group)
+ );
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('loading displays before group config is loaded', () => {
- assert.isTrue(element.$.loading.classList.contains('loading'));
- assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
- assert.isTrue(element.$.loadedContent.classList.contains('loading'));
- assert.isTrue(getComputedStyle(element.$.loadedContent)
- .display === 'none');
- });
+ test('loading displays before group config is loaded', () => {
+ assert.isTrue(element.$.loading.classList.contains('loading'));
+ assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
+ assert.isTrue(element.$.loadedContent.classList.contains('loading'));
+ assert.isTrue(getComputedStyle(element.$.loadedContent)
+ .display === 'none');
+ });
- test('default values are populated with internal group', done => {
- sandbox.stub(
- element.$.restAPI,
- 'getIsGroupOwner',
- () => Promise.resolve(true));
- element.groupId = 1;
- element._loadGroup().then(() => {
- assert.isTrue(element._groupIsInternal);
- assert.isFalse(element.$.visibleToAll.bindValue);
- done();
- });
- });
-
- test('default values with external group', done => {
- const groupExternal = Object.assign({}, group);
- groupExternal.id = 'external-group-id';
- groupStub.restore();
- groupStub = sandbox.stub(
- element.$.restAPI,
- 'getGroupConfig',
- () => Promise.resolve(groupExternal));
- sandbox.stub(
- element.$.restAPI,
- 'getIsGroupOwner',
- () => Promise.resolve(true));
- element.groupId = 1;
- element._loadGroup().then(() => {
- assert.isFalse(element._groupIsInternal);
- assert.isFalse(element.$.visibleToAll.bindValue);
- done();
- });
- });
-
- test('rename group', done => {
- const groupName = 'test-group';
- const groupName2 = 'test-group2';
- element.groupId = 1;
- element._groupConfig = {
- name: groupName,
- };
- element._groupConfigOwner = 'testId';
- element._groupName = groupName;
- element._groupOwner = true;
-
- sandbox.stub(
- element.$.restAPI,
- 'getIsGroupOwner',
- () => Promise.resolve(true));
-
- sandbox.stub(
- element.$.restAPI,
- 'saveGroupName',
- () => Promise.resolve({status: 200}));
-
- const button = element.$.inputUpdateNameBtn;
-
- element._loadGroup().then(() => {
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
-
- element.$.groupNameInput.text = groupName2;
-
- element.$.groupOwnerInput.text = 'testId2';
-
- assert.isFalse(button.hasAttribute('disabled'));
- assert.isTrue(element.$.groupName.classList.contains('edited'));
-
- element._handleSaveName().then(() => {
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- assert.equal(element._groupName, groupName2);
- done();
- });
-
- element._handleSaveOwner().then(() => {
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- assert.equal(element._groupConfigOwner, 'testId2');
- done();
- });
- });
- });
-
- test('test for undefined group name', done => {
- groupStub.restore();
-
- sandbox.stub(
- element.$.restAPI,
- 'getGroupConfig',
- () => Promise.resolve({}));
-
- assert.isUndefined(element.groupId);
-
- element.groupId = 1;
-
- assert.isDefined(element.groupId);
-
- // Test that loading shows instead of filling
- // in group details
- element._loadGroup().then(() => {
- assert.isTrue(element.$.loading.classList.contains('loading'));
-
- assert.isTrue(element._loading);
-
- done();
- });
- });
-
- test('test fire event', done => {
- element._groupConfig = {
- name: 'test-group',
- };
-
- sandbox.stub(element.$.restAPI, 'saveGroupName')
- .returns(Promise.resolve({status: 200}));
-
- const showStub = sandbox.stub(element, 'fire');
- element._handleSaveName()
- .then(() => {
- assert.isTrue(showStub.called);
- done();
- });
- });
-
- test('_computeGroupDisabled', () => {
- let admin = true;
- let owner = false;
- let groupIsInternal = true;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), false);
-
- admin = false;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
-
- owner = true;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), false);
-
- owner = false;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
-
- groupIsInternal = false;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
-
- admin = true;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
- });
-
- test('_computeLoadingClass', () => {
- assert.equal(element._computeLoadingClass(true), 'loading');
- assert.equal(element._computeLoadingClass(false), '');
- });
-
- test('fires page-error', done => {
- groupStub.restore();
-
- element.groupId = 1;
-
- const response = {status: 404};
- sandbox.stub(
- element.$.restAPI, 'getGroupConfig', (group, errFn) => {
- errFn(response);
- });
-
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- element._loadGroup();
+ test('default values are populated with internal group', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'getIsGroupOwner',
+ () => Promise.resolve(true));
+ element.groupId = 1;
+ element._loadGroup().then(() => {
+ assert.isTrue(element._groupIsInternal);
+ assert.isFalse(element.$.visibleToAll.bindValue);
+ done();
});
});
+
+ test('default values with external group', done => {
+ const groupExternal = Object.assign({}, group);
+ groupExternal.id = 'external-group-id';
+ groupStub.restore();
+ groupStub = sandbox.stub(
+ element.$.restAPI,
+ 'getGroupConfig',
+ () => Promise.resolve(groupExternal));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getIsGroupOwner',
+ () => Promise.resolve(true));
+ element.groupId = 1;
+ element._loadGroup().then(() => {
+ assert.isFalse(element._groupIsInternal);
+ assert.isFalse(element.$.visibleToAll.bindValue);
+ done();
+ });
+ });
+
+ test('rename group', done => {
+ const groupName = 'test-group';
+ const groupName2 = 'test-group2';
+ element.groupId = 1;
+ element._groupConfig = {
+ name: groupName,
+ };
+ element._groupConfigOwner = 'testId';
+ element._groupName = groupName;
+ element._groupOwner = true;
+
+ sandbox.stub(
+ element.$.restAPI,
+ 'getIsGroupOwner',
+ () => Promise.resolve(true));
+
+ sandbox.stub(
+ element.$.restAPI,
+ 'saveGroupName',
+ () => Promise.resolve({status: 200}));
+
+ const button = element.$.inputUpdateNameBtn;
+
+ element._loadGroup().then(() => {
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(element.$.Title.classList.contains('edited'));
+
+ element.$.groupNameInput.text = groupName2;
+
+ element.$.groupOwnerInput.text = 'testId2';
+
+ assert.isFalse(button.hasAttribute('disabled'));
+ assert.isTrue(element.$.groupName.classList.contains('edited'));
+
+ element._handleSaveName().then(() => {
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(element.$.Title.classList.contains('edited'));
+ assert.equal(element._groupName, groupName2);
+ done();
+ });
+
+ element._handleSaveOwner().then(() => {
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(element.$.Title.classList.contains('edited'));
+ assert.equal(element._groupConfigOwner, 'testId2');
+ done();
+ });
+ });
+ });
+
+ test('test for undefined group name', done => {
+ groupStub.restore();
+
+ sandbox.stub(
+ element.$.restAPI,
+ 'getGroupConfig',
+ () => Promise.resolve({}));
+
+ assert.isUndefined(element.groupId);
+
+ element.groupId = 1;
+
+ assert.isDefined(element.groupId);
+
+ // Test that loading shows instead of filling
+ // in group details
+ element._loadGroup().then(() => {
+ assert.isTrue(element.$.loading.classList.contains('loading'));
+
+ assert.isTrue(element._loading);
+
+ done();
+ });
+ });
+
+ test('test fire event', done => {
+ element._groupConfig = {
+ name: 'test-group',
+ };
+
+ sandbox.stub(element.$.restAPI, 'saveGroupName')
+ .returns(Promise.resolve({status: 200}));
+
+ const showStub = sandbox.stub(element, 'fire');
+ element._handleSaveName()
+ .then(() => {
+ assert.isTrue(showStub.called);
+ done();
+ });
+ });
+
+ test('_computeGroupDisabled', () => {
+ let admin = true;
+ let owner = false;
+ let groupIsInternal = true;
+ assert.equal(element._computeGroupDisabled(owner, admin,
+ groupIsInternal), false);
+
+ admin = false;
+ assert.equal(element._computeGroupDisabled(owner, admin,
+ groupIsInternal), true);
+
+ owner = true;
+ assert.equal(element._computeGroupDisabled(owner, admin,
+ groupIsInternal), false);
+
+ owner = false;
+ assert.equal(element._computeGroupDisabled(owner, admin,
+ groupIsInternal), true);
+
+ groupIsInternal = false;
+ assert.equal(element._computeGroupDisabled(owner, admin,
+ groupIsInternal), true);
+
+ admin = true;
+ assert.equal(element._computeGroupDisabled(owner, admin,
+ groupIsInternal), true);
+ });
+
+ test('_computeLoadingClass', () => {
+ assert.equal(element._computeLoadingClass(true), 'loading');
+ assert.equal(element._computeLoadingClass(false), '');
+ });
+
+ test('fires page-error', done => {
+ groupStub.restore();
+
+ element.groupId = 1;
+
+ const response = {status: 404};
+ sandbox.stub(
+ element.$.restAPI, 'getGroupConfig', (group, errFn) => {
+ errFn(response);
+ });
+
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
+ });
+
+ element._loadGroup();
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
deleted file mode 100644
index e07f911..0000000
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
+++ /dev/null
@@ -1,150 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
-<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/gr-menu-page-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-rule-editor/gr-rule-editor.html">
-
-<dom-module id="gr-permission">
- <template>
- <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-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]]"></paper-toggle-button>Exclusive
- </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"></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>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-permission.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
index 508c3a2..4c5bbb8 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -14,301 +14,318 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const MAX_AUTOCOMPLETE_RESULTS = 20;
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
+import '@polymer/paper-toggle-button/paper-toggle-button.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/gr-menu-page-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-rule-editor/gr-rule-editor.js';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-permission_html.js';
- const RANGE_NAMES = [
- 'QUERY LIMIT',
- 'BATCH CHANGES LIMIT',
- ];
+const MAX_AUTOCOMPLETE_RESULTS = 20;
+const RANGE_NAMES = [
+ 'QUERY LIMIT',
+ 'BATCH CHANGES LIMIT',
+];
+
+/**
+ * @appliesMixin Gerrit.AccessMixin
+ * @appliesMixin Gerrit.FireMixin
+ */
+/**
+ * Fired when the permission has been modified or removed.
+ *
+ * @event access-modified
+ */
+/**
+ * Fired when a permission that was previously added was removed.
+ *
+ * @event added-permission-removed
+ * @extends Polymer.Element
+ */
+class GrPermission extends mixinBehaviors( [
+ Gerrit.AccessBehavior,
/**
- * @appliesMixin Gerrit.AccessMixin
- * @appliesMixin Gerrit.FireMixin
+ * Unused in this element, but called by other elements in tests
+ * e.g gr-access-section_test.
*/
- /**
- * Fired when the permission has been modified or removed.
- *
- * @event access-modified
- */
- /**
- * Fired when a permission that was previously added was removed.
- *
- * @event added-permission-removed
- * @extends Polymer.Element
- */
- class GrPermission extends Polymer.mixinBehaviors( [
- Gerrit.AccessBehavior,
- /**
- * Unused in this element, but called by other elements in tests
- * e.g gr-access-section_test.
- */
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-permission'; }
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- static get properties() {
- return {
- labels: Object,
- name: String,
- /** @type {?} */
- permission: {
- type: Object,
- observer: '_sortPermission',
- notify: true,
+ static get is() { return 'gr-permission'; }
+
+ static get properties() {
+ return {
+ labels: Object,
+ name: String,
+ /** @type {?} */
+ permission: {
+ type: Object,
+ observer: '_sortPermission',
+ notify: true,
+ },
+ groups: Object,
+ section: String,
+ editing: {
+ type: Boolean,
+ value: false,
+ observer: '_handleEditingChanged',
+ },
+ _label: {
+ type: Object,
+ computed: '_computeLabel(permission, labels)',
+ },
+ _groupFilter: String,
+ _query: {
+ type: Function,
+ value() {
+ return this._getGroupSuggestions.bind(this);
},
- groups: Object,
- section: String,
- editing: {
- type: Boolean,
- value: false,
- observer: '_handleEditingChanged',
- },
- _label: {
- type: Object,
- computed: '_computeLabel(permission, labels)',
- },
- _groupFilter: String,
- _query: {
- type: Function,
- value() {
- return this._getGroupSuggestions.bind(this);
- },
- },
- _rules: Array,
- _groupsWithRules: Object,
- _deleted: {
- type: Boolean,
- value: false,
- },
- _originalExclusiveValue: Boolean,
- };
- }
+ },
+ _rules: Array,
+ _groupsWithRules: Object,
+ _deleted: {
+ type: Boolean,
+ value: false,
+ },
+ _originalExclusiveValue: Boolean,
+ };
+ }
- static get observers() {
- return [
- '_handleRulesChanged(_rules.splices)',
- ];
- }
+ static get observers() {
+ return [
+ '_handleRulesChanged(_rules.splices)',
+ ];
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('access-saved',
- () => this._handleAccessSaved());
- }
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('access-saved',
+ () => this._handleAccessSaved());
+ }
- /** @override */
- ready() {
- super.ready();
- this._setupValues();
- }
+ /** @override */
+ ready() {
+ super.ready();
+ this._setupValues();
+ }
- _setupValues() {
- if (!this.permission) { return; }
- this._originalExclusiveValue = !!this.permission.value.exclusive;
- Polymer.dom.flush();
- }
+ _setupValues() {
+ if (!this.permission) { return; }
+ this._originalExclusiveValue = !!this.permission.value.exclusive;
+ flush();
+ }
- _handleAccessSaved() {
- // Set a new 'original' value to keep track of after the value has been
- // saved.
- this._setupValues();
- }
+ _handleAccessSaved() {
+ // Set a new 'original' value to keep track of after the value has been
+ // saved.
+ this._setupValues();
+ }
- _permissionIsOwnerOrGlobal(permissionId, section) {
- return permissionId === 'owner' || section === 'GLOBAL_CAPABILITIES';
- }
+ _permissionIsOwnerOrGlobal(permissionId, section) {
+ return permissionId === 'owner' || section === 'GLOBAL_CAPABILITIES';
+ }
- _handleEditingChanged(editing, editingOld) {
- // Ignore when editing gets set initially.
- if (!editingOld) { return; }
- // Restore original values if no longer editing.
- if (!editing) {
- this._deleted = false;
- delete this.permission.value.deleted;
- this._groupFilter = '';
- this._rules = this._rules.filter(rule => !rule.value.added);
- for (const key of Object.keys(this.permission.value.rules)) {
- if (this.permission.value.rules[key].added) {
- delete this.permission.value.rules[key];
- }
- }
-
- // Restore exclusive bit to original.
- this.set(['permission', 'value', 'exclusive'],
- this._originalExclusiveValue);
- }
- }
-
- _handleAddedRuleRemoved(e) {
- const index = e.model.index;
- this._rules = this._rules.slice(0, index)
- .concat(this._rules.slice(index + 1, this._rules.length));
- }
-
- _handleValueChange() {
- this.permission.value.modified = true;
- // Allows overall access page to know a change has been made.
- this.dispatchEvent(
- new CustomEvent('access-modified', {bubbles: true, composed: true}));
- }
-
- _handleRemovePermission() {
- if (this.permission.value.added) {
- this.dispatchEvent(new CustomEvent(
- 'added-permission-removed', {bubbles: true, composed: true}));
- }
- this._deleted = true;
- this.permission.value.deleted = true;
- this.dispatchEvent(
- new CustomEvent('access-modified', {bubbles: true, composed: true}));
- }
-
- _handleRulesChanged(changeRecord) {
- // Update the groups to exclude in the autocomplete.
- this._groupsWithRules = this._computeGroupsWithRules(this._rules);
- }
-
- _sortPermission(permission) {
- this._rules = this.toSortedArray(permission.value.rules);
- }
-
- _computeSectionClass(editing, deleted) {
- const classList = [];
- if (editing) {
- classList.push('editing');
- }
- if (deleted) {
- classList.push('deleted');
- }
- return classList.join(' ');
- }
-
- _handleUndoRemove() {
+ _handleEditingChanged(editing, editingOld) {
+ // Ignore when editing gets set initially.
+ if (!editingOld) { return; }
+ // Restore original values if no longer editing.
+ if (!editing) {
this._deleted = false;
delete this.permission.value.deleted;
- }
-
- _computeLabel(permission, labels) {
- if (!labels || !permission ||
- !permission.value || !permission.value.label) { return; }
-
- const labelName = permission.value.label;
-
- // It is possible to have a label name that is not included in the
- // 'labels' object. In this case, treat it like anything else.
- if (!labels[labelName]) { return; }
- const label = {
- name: labelName,
- values: this._computeLabelValues(labels[labelName].values),
- };
- return label;
- }
-
- _computeLabelValues(values) {
- const valuesArr = [];
- const keys = Object.keys(values)
- .sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
-
- for (const key of keys) {
- let text = values[key];
- if (!text) { text = ''; }
- // The value from the server being used to choose which item is
- // selected is in integer form, so this must be converted.
- valuesArr.push({value: parseInt(key, 10), text});
- }
- return valuesArr;
- }
-
- /**
- * @param {!Array} rules
- * @return {!Object} Object with groups with rues as keys, and true as
- * value.
- */
- _computeGroupsWithRules(rules) {
- const groups = {};
- for (const rule of rules) {
- groups[rule.id] = true;
- }
- return groups;
- }
-
- _computeGroupName(groups, groupId) {
- return groups && groups[groupId] && groups[groupId].name ?
- groups[groupId].name : groupId;
- }
-
- _getGroupSuggestions() {
- return this.$.restAPI.getSuggestedGroups(
- this._groupFilter,
- MAX_AUTOCOMPLETE_RESULTS)
- .then(response => {
- const groups = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- groups.push({
- name: key,
- value: response[key],
- });
- }
- // Does not return groups in which we already have rules for.
- return groups
- .filter(group => !this._groupsWithRules[group.value.id]);
- });
- }
-
- /**
- * Handles adding a skeleton item to the dom-repeat.
- * gr-rule-editor handles setting the default values.
- */
- _handleAddRuleItem(e) {
- // The group id is encoded, but have to decode in order for the access
- // API to work as expected.
- const groupId = decodeURIComponent(e.detail.value.id)
- .replace(/\+/g, ' ');
- // We cannot use "this.set(...)" here, because groupId may contain dots,
- // and dots in property path names are totally unsupported by Polymer.
- // Apparently Polymer picks up this change anyway, otherwise we should
- // have looked at using MutableData:
- // https://polymer-library.polymer-project.org/2.0/docs/devguide/data-system#mutable-data
- this.permission.value.rules[groupId] = {};
-
- // Purposely don't recompute sorted array so that the newly added rule
- // is the last item of the array.
- this.push('_rules', {
- id: groupId,
- });
-
- // 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};
+ this._groupFilter = '';
+ this._rules = this._rules.filter(rule => !rule.value.added);
+ for (const key of Object.keys(this.permission.value.rules)) {
+ if (this.permission.value.rules[key].added) {
+ delete this.permission.value.rules[key];
+ }
}
- // 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 propogated
- // back to the section. Since the rule is inside a dom-repeat, a flush
- // is needed.
- Polymer.dom.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.dispatchEvent(
- new CustomEvent('access-modified', {bubbles: true, composed: true}));
- }
-
- _computeHasRange(name) {
- if (!name) { return false; }
-
- return RANGE_NAMES.includes(name.toUpperCase());
+ // Restore exclusive bit to original.
+ this.set(['permission', 'value', 'exclusive'],
+ this._originalExclusiveValue);
}
}
- customElements.define(GrPermission.is, GrPermission);
-})();
+ _handleAddedRuleRemoved(e) {
+ const index = e.model.index;
+ this._rules = this._rules.slice(0, index)
+ .concat(this._rules.slice(index + 1, this._rules.length));
+ }
+
+ _handleValueChange() {
+ this.permission.value.modified = true;
+ // Allows overall access page to know a change has been made.
+ this.dispatchEvent(
+ new CustomEvent('access-modified', {bubbles: true, composed: true}));
+ }
+
+ _handleRemovePermission() {
+ if (this.permission.value.added) {
+ this.dispatchEvent(new CustomEvent(
+ 'added-permission-removed', {bubbles: true, composed: true}));
+ }
+ this._deleted = true;
+ this.permission.value.deleted = true;
+ this.dispatchEvent(
+ new CustomEvent('access-modified', {bubbles: true, composed: true}));
+ }
+
+ _handleRulesChanged(changeRecord) {
+ // Update the groups to exclude in the autocomplete.
+ this._groupsWithRules = this._computeGroupsWithRules(this._rules);
+ }
+
+ _sortPermission(permission) {
+ this._rules = this.toSortedArray(permission.value.rules);
+ }
+
+ _computeSectionClass(editing, deleted) {
+ const classList = [];
+ if (editing) {
+ classList.push('editing');
+ }
+ if (deleted) {
+ classList.push('deleted');
+ }
+ return classList.join(' ');
+ }
+
+ _handleUndoRemove() {
+ this._deleted = false;
+ delete this.permission.value.deleted;
+ }
+
+ _computeLabel(permission, labels) {
+ if (!labels || !permission ||
+ !permission.value || !permission.value.label) { return; }
+
+ const labelName = permission.value.label;
+
+ // It is possible to have a label name that is not included in the
+ // 'labels' object. In this case, treat it like anything else.
+ if (!labels[labelName]) { return; }
+ const label = {
+ name: labelName,
+ values: this._computeLabelValues(labels[labelName].values),
+ };
+ return label;
+ }
+
+ _computeLabelValues(values) {
+ const valuesArr = [];
+ const keys = Object.keys(values)
+ .sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
+
+ for (const key of keys) {
+ let text = values[key];
+ if (!text) { text = ''; }
+ // The value from the server being used to choose which item is
+ // selected is in integer form, so this must be converted.
+ valuesArr.push({value: parseInt(key, 10), text});
+ }
+ return valuesArr;
+ }
+
+ /**
+ * @param {!Array} rules
+ * @return {!Object} Object with groups with rues as keys, and true as
+ * value.
+ */
+ _computeGroupsWithRules(rules) {
+ const groups = {};
+ for (const rule of rules) {
+ groups[rule.id] = true;
+ }
+ return groups;
+ }
+
+ _computeGroupName(groups, groupId) {
+ return groups && groups[groupId] && groups[groupId].name ?
+ groups[groupId].name : groupId;
+ }
+
+ _getGroupSuggestions() {
+ return this.$.restAPI.getSuggestedGroups(
+ this._groupFilter,
+ MAX_AUTOCOMPLETE_RESULTS)
+ .then(response => {
+ const groups = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ groups.push({
+ name: key,
+ value: response[key],
+ });
+ }
+ // Does not return groups in which we already have rules for.
+ return groups
+ .filter(group => !this._groupsWithRules[group.value.id]);
+ });
+ }
+
+ /**
+ * Handles adding a skeleton item to the dom-repeat.
+ * gr-rule-editor handles setting the default values.
+ */
+ _handleAddRuleItem(e) {
+ // The group id is encoded, but have to decode in order for the access
+ // API to work as expected.
+ const groupId = decodeURIComponent(e.detail.value.id)
+ .replace(/\+/g, ' ');
+ // We cannot use "this.set(...)" here, because groupId may contain dots,
+ // and dots in property path names are totally unsupported by Polymer.
+ // Apparently Polymer picks up this change anyway, otherwise we should
+ // have looked at using MutableData:
+ // https://polymer-library.polymer-project.org/2.0/docs/devguide/data-system#mutable-data
+ this.permission.value.rules[groupId] = {};
+
+ // Purposely don't recompute sorted array so that the newly added rule
+ // is the last item of the array.
+ this.push('_rules', {
+ id: groupId,
+ });
+
+ // 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};
+ }
+
+ // 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 propogated
+ // 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.dispatchEvent(
+ new CustomEvent('access-modified', {bubbles: true, composed: true}));
+ }
+
+ _computeHasRange(name) {
+ if (!name) { return false; }
+
+ return RANGE_NAMES.includes(name.toUpperCase());
+ }
+}
+
+customElements.define(GrPermission.is, GrPermission);
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.js
new file mode 100644
index 0000000..1b57336
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.js
@@ -0,0 +1,107 @@
+/**
+ * @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.js';
+
+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-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]]"></paper-toggle-button>Exclusive
+ </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"></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>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
index f3c1e4f..82d89e1 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
@@ -19,16 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-permission</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-permission.html">
-
-<script>void(0);</script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,399 +31,400 @@
</template>
</test-fixture>
-<script>
- suite('gr-permission tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-permission.js';
+suite('gr-permission tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- sandbox.stub(element.$.restAPI, 'getSuggestedGroups').returns(
- Promise.resolve({
- 'Administrators': {
- id: '4c97682e6ce61b7247f3381b6f1789356666de7f',
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ sandbox.stub(element.$.restAPI, 'getSuggestedGroups').returns(
+ Promise.resolve({
+ 'Administrators': {
+ id: '4c97682e6ce61b7247f3381b6f1789356666de7f',
+ },
+ 'Anonymous Users': {
+ id: 'global%3AAnonymous-Users',
+ },
+ }));
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('unit tests', () => {
+ test('_sortPermission', () => {
+ const permission = {
+ id: 'submit',
+ value: {
+ rules: {
+ 'global:Project-Owners': {
+ action: 'ALLOW',
+ force: false,
},
- 'Anonymous Users': {
- id: 'global%3AAnonymous-Users',
+ '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+ action: 'ALLOW',
+ force: false,
},
- }));
+ },
+ },
+ };
+
+ const expectedRules = [
+ {
+ id: '4c97682e6ce6b7247f3381b6f1789356666de7f',
+ value: {action: 'ALLOW', force: false},
+ },
+ {
+ id: 'global:Project-Owners',
+ value: {action: 'ALLOW', force: false},
+ },
+ ];
+
+ element._sortPermission(permission);
+ assert.deepEqual(element._rules, expectedRules);
});
- teardown(() => {
- sandbox.restore();
+ test('_computeLabel and _computeLabelValues', () => {
+ const labels = {
+ 'Code-Review': {
+ default_value: 0,
+ values: {
+ ' 0': 'No score',
+ '-1': 'I would prefer this is not merged as is',
+ '-2': 'This shall not be merged',
+ '+1': 'Looks good to me, but someone else must approve',
+ '+2': 'Looks good to me, approved',
+ },
+ },
+ };
+ let permission = {
+ id: 'label-Code-Review',
+ value: {
+ label: 'Code-Review',
+ rules: {
+ 'global:Project-Owners': {
+ action: 'ALLOW',
+ force: false,
+ min: -2,
+ max: 2,
+ },
+ '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+ action: 'ALLOW',
+ force: false,
+ min: -2,
+ max: 2,
+ },
+ },
+ },
+ };
+
+ const expectedLabelValues = [
+ {value: -2, text: 'This shall not be merged'},
+ {value: -1, text: 'I would prefer this is not merged as is'},
+ {value: 0, text: 'No score'},
+ {value: 1, text: 'Looks good to me, but someone else must approve'},
+ {value: 2, text: 'Looks good to me, approved'},
+ ];
+
+ const expectedLabel = {
+ name: 'Code-Review',
+ values: expectedLabelValues,
+ };
+
+ assert.deepEqual(element._computeLabelValues(
+ labels['Code-Review'].values), expectedLabelValues);
+
+ assert.deepEqual(element._computeLabel(permission, labels),
+ expectedLabel);
+
+ permission = {
+ id: 'label-reviewDB',
+ value: {
+ label: 'reviewDB',
+ rules: {
+ 'global:Project-Owners': {
+ action: 'ALLOW',
+ force: false,
+ },
+ '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+ action: 'ALLOW',
+ force: false,
+ },
+ },
+ },
+ };
+
+ assert.isNotOk(element._computeLabel(permission, labels));
});
- suite('unit tests', () => {
- test('_sortPermission', () => {
- const permission = {
- id: 'submit',
- value: {
- rules: {
- 'global:Project-Owners': {
- action: 'ALLOW',
- force: false,
- },
- '4c97682e6ce6b7247f3381b6f1789356666de7f': {
- action: 'ALLOW',
- force: false,
- },
- },
- },
- };
+ test('_computeSectionClass', () => {
+ let deleted = true;
+ let editing = false;
+ assert.equal(element._computeSectionClass(editing, deleted), 'deleted');
- const expectedRules = [
+ deleted = false;
+ assert.equal(element._computeSectionClass(editing, deleted), '');
+
+ editing = true;
+ assert.equal(element._computeSectionClass(editing, deleted), 'editing');
+
+ deleted = true;
+ assert.equal(element._computeSectionClass(editing, deleted),
+ 'editing deleted');
+ });
+
+ test('_computeGroupName', () => {
+ const groups = {
+ abc123: {name: 'test group'},
+ bcd234: {},
+ };
+ assert.equal(element._computeGroupName(groups, 'abc123'), 'test group');
+ assert.equal(element._computeGroupName(groups, 'bcd234'), 'bcd234');
+ });
+
+ test('_computeGroupsWithRules', () => {
+ const rules = [
+ {
+ id: '4c97682e6ce6b7247f3381b6f1789356666de7f',
+ value: {action: 'ALLOW', force: false},
+ },
+ {
+ id: 'global:Project-Owners',
+ value: {action: 'ALLOW', force: false},
+ },
+ ];
+ const groupsWithRules = {
+ '4c97682e6ce6b7247f3381b6f1789356666de7f': true,
+ 'global:Project-Owners': true,
+ };
+ assert.deepEqual(element._computeGroupsWithRules(rules),
+ groupsWithRules);
+ });
+
+ test('_getGroupSuggestions without existing rules', done => {
+ element._groupsWithRules = {};
+
+ element._getGroupSuggestions().then(groups => {
+ assert.deepEqual(groups, [
{
- id: '4c97682e6ce6b7247f3381b6f1789356666de7f',
- value: {action: 'ALLOW', force: false},
- },
- {
- id: 'global:Project-Owners',
- value: {action: 'ALLOW', force: false},
- },
- ];
-
- element._sortPermission(permission);
- assert.deepEqual(element._rules, expectedRules);
- });
-
- test('_computeLabel and _computeLabelValues', () => {
- const labels = {
- 'Code-Review': {
- default_value: 0,
- values: {
- ' 0': 'No score',
- '-1': 'I would prefer this is not merged as is',
- '-2': 'This shall not be merged',
- '+1': 'Looks good to me, but someone else must approve',
- '+2': 'Looks good to me, approved',
- },
- },
- };
- let permission = {
- id: 'label-Code-Review',
- value: {
- label: 'Code-Review',
- rules: {
- 'global:Project-Owners': {
- action: 'ALLOW',
- force: false,
- min: -2,
- max: 2,
- },
- '4c97682e6ce6b7247f3381b6f1789356666de7f': {
- action: 'ALLOW',
- force: false,
- min: -2,
- max: 2,
- },
- },
- },
- };
-
- const expectedLabelValues = [
- {value: -2, text: 'This shall not be merged'},
- {value: -1, text: 'I would prefer this is not merged as is'},
- {value: 0, text: 'No score'},
- {value: 1, text: 'Looks good to me, but someone else must approve'},
- {value: 2, text: 'Looks good to me, approved'},
- ];
-
- const expectedLabel = {
- name: 'Code-Review',
- values: expectedLabelValues,
- };
-
- assert.deepEqual(element._computeLabelValues(
- labels['Code-Review'].values), expectedLabelValues);
-
- assert.deepEqual(element._computeLabel(permission, labels),
- expectedLabel);
-
- permission = {
- id: 'label-reviewDB',
- value: {
- label: 'reviewDB',
- rules: {
- 'global:Project-Owners': {
- action: 'ALLOW',
- force: false,
- },
- '4c97682e6ce6b7247f3381b6f1789356666de7f': {
- action: 'ALLOW',
- force: false,
- },
- },
- },
- };
-
- assert.isNotOk(element._computeLabel(permission, labels));
- });
-
- test('_computeSectionClass', () => {
- let deleted = true;
- let editing = false;
- assert.equal(element._computeSectionClass(editing, deleted), 'deleted');
-
- deleted = false;
- assert.equal(element._computeSectionClass(editing, deleted), '');
-
- editing = true;
- assert.equal(element._computeSectionClass(editing, deleted), 'editing');
-
- deleted = true;
- assert.equal(element._computeSectionClass(editing, deleted),
- 'editing deleted');
- });
-
- test('_computeGroupName', () => {
- const groups = {
- abc123: {name: 'test group'},
- bcd234: {},
- };
- assert.equal(element._computeGroupName(groups, 'abc123'), 'test group');
- assert.equal(element._computeGroupName(groups, 'bcd234'), 'bcd234');
- });
-
- test('_computeGroupsWithRules', () => {
- const rules = [
- {
- id: '4c97682e6ce6b7247f3381b6f1789356666de7f',
- value: {action: 'ALLOW', force: false},
- },
- {
- id: 'global:Project-Owners',
- value: {action: 'ALLOW', force: false},
- },
- ];
- const groupsWithRules = {
- '4c97682e6ce6b7247f3381b6f1789356666de7f': true,
- 'global:Project-Owners': true,
- };
- assert.deepEqual(element._computeGroupsWithRules(rules),
- groupsWithRules);
- });
-
- test('_getGroupSuggestions without existing rules', done => {
- element._groupsWithRules = {};
-
- element._getGroupSuggestions().then(groups => {
- assert.deepEqual(groups, [
- {
- name: 'Administrators',
- value: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f'},
- }, {
- name: 'Anonymous Users',
- value: {id: 'global%3AAnonymous-Users'},
- },
- ]);
- done();
- });
- });
-
- test('_getGroupSuggestions with existing rules filters them', done => {
- element._groupsWithRules = {
- '4c97682e6ce61b7247f3381b6f1789356666de7f': true,
- };
-
- element._getGroupSuggestions().then(groups => {
- assert.deepEqual(groups, [{
+ name: 'Administrators',
+ value: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f'},
+ }, {
name: 'Anonymous Users',
value: {id: 'global%3AAnonymous-Users'},
- }]);
- done();
- });
- });
-
- test('_handleRemovePermission', () => {
- element.editing = true;
- element.permission = {value: {rules: {}}};
- element._handleRemovePermission();
- assert.isTrue(element._deleted);
- assert.isTrue(element.permission.value.deleted);
-
- element.editing = false;
- assert.isFalse(element._deleted);
- assert.isNotOk(element.permission.value.deleted);
- });
-
- test('_handleUndoRemove', () => {
- element.permission = {value: {deleted: true, rules: {}}};
- element._handleUndoRemove();
- assert.isFalse(element._deleted);
- assert.isNotOk(element.permission.value.deleted);
- });
-
- test('_computeHasRange', () => {
- assert.isTrue(element._computeHasRange('Query Limit'));
-
- assert.isTrue(element._computeHasRange('Batch Changes Limit'));
-
- assert.isFalse(element._computeHasRange('test'));
+ },
+ ]);
+ done();
});
});
- suite('interactions', () => {
- setup(() => {
- sandbox.spy(element, '_computeLabel');
- element.name = 'Priority';
- element.section = 'refs/*';
- element.labels = {
- 'Code-Review': {
- values: {
- ' 0': 'No score',
- '-1': 'I would prefer this is not merged as is',
- '-2': 'This shall not be merged',
- '+1': 'Looks good to me, but someone else must approve',
- '+2': 'Looks good to me, approved',
- },
- default_value: 0,
- },
- };
- element.permission = {
- id: 'label-Code-Review',
- value: {
- label: 'Code-Review',
- rules: {
- 'global:Project-Owners': {
- action: 'ALLOW',
- force: false,
- min: -2,
- max: 2,
- },
- '4c97682e6ce6b7247f3381b6f1789356666de7f': {
- action: 'ALLOW',
- force: false,
- min: -2,
- max: 2,
- },
- },
- },
- };
- element._setupValues();
- flushAsynchronousOperations();
+ test('_getGroupSuggestions with existing rules filters them', done => {
+ element._groupsWithRules = {
+ '4c97682e6ce61b7247f3381b6f1789356666de7f': true,
+ };
+
+ element._getGroupSuggestions().then(groups => {
+ assert.deepEqual(groups, [{
+ name: 'Anonymous Users',
+ value: {id: 'global%3AAnonymous-Users'},
+ }]);
+ done();
});
+ });
- test('adding a rule', () => {
- element.name = 'Priority';
- element.section = 'refs/*';
- element.groups = {};
- element.$.groupAutocomplete.text = 'ldap/tests te.st';
- const e = {
- detail: {
- value: {
- id: 'ldap:CN=test+te.st',
- },
- },
- };
- element.editing = true;
- assert.equal(element._rules.length, 2);
- assert.equal(Object.keys(element._groupsWithRules).length, 2);
- element._handleAddRuleItem(e);
- flushAsynchronousOperations();
- 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.deepEqual(element.permission.value.rules['ldap:CN=test te.st'],
- {action: 'ALLOW', min: -2, max: 2, added: true});
- // New rule should be removed if cancel from editing.
- element.editing = false;
- assert.equal(element._rules.length, 2);
- assert.equal(Object.keys(element.permission.value.rules).length, 2);
- });
+ test('_handleRemovePermission', () => {
+ element.editing = true;
+ element.permission = {value: {rules: {}}};
+ element._handleRemovePermission();
+ assert.isTrue(element._deleted);
+ assert.isTrue(element.permission.value.deleted);
- test('removing an added rule', () => {
- element.name = 'Priority';
- element.section = 'refs/*';
- element.groups = {};
- element.$.groupAutocomplete.text = 'new group name';
- assert.equal(element._rules.length, 2);
- element.shadowRoot
- .querySelector('gr-rule-editor').fire('added-rule-removed');
- flushAsynchronousOperations();
- assert.equal(element._rules.length, 1);
- });
+ element.editing = false;
+ assert.isFalse(element._deleted);
+ assert.isNotOk(element.permission.value.deleted);
+ });
- test('removing an added permission', () => {
- const removeStub = sandbox.stub();
- element.addEventListener('added-permission-removed', removeStub);
- element.editing = true;
- element.name = 'Priority';
- element.section = 'refs/*';
- element.permission.value.added = true;
- MockInteractions.tap(element.$.removeBtn);
- assert.isTrue(removeStub.called);
- });
+ test('_handleUndoRemove', () => {
+ element.permission = {value: {deleted: true, rules: {}}};
+ element._handleUndoRemove();
+ assert.isFalse(element._deleted);
+ assert.isNotOk(element.permission.value.deleted);
+ });
- test('removing the permission', () => {
- element.editing = true;
- element.name = 'Priority';
- element.section = 'refs/*';
+ test('_computeHasRange', () => {
+ assert.isTrue(element._computeHasRange('Query Limit'));
- const removeStub = sandbox.stub();
- element.addEventListener('added-permission-removed', removeStub);
+ assert.isTrue(element._computeHasRange('Batch Changes Limit'));
- assert.isFalse(element.$.permission.classList.contains('deleted'));
- assert.isFalse(element._deleted);
- MockInteractions.tap(element.$.removeBtn);
- assert.isTrue(element.$.permission.classList.contains('deleted'));
- assert.isTrue(element._deleted);
- MockInteractions.tap(element.$.undoRemoveBtn);
- assert.isFalse(element.$.permission.classList.contains('deleted'));
- assert.isFalse(element._deleted);
- assert.isFalse(removeStub.called);
- });
-
- test('modify a permission', () => {
- element.editing = true;
- element.name = 'Priority';
- element.section = 'refs/*';
-
- assert.isFalse(element._originalExclusiveValue);
- assert.isNotOk(element.permission.value.modified);
- MockInteractions.tap(element.shadowRoot
- .querySelector('#exclusiveToggle'));
- flushAsynchronousOperations();
- assert.isTrue(element.permission.value.exclusive);
- assert.isTrue(element.permission.value.modified);
- assert.isFalse(element._originalExclusiveValue);
- element.editing = false;
- assert.isFalse(element.permission.value.exclusive);
- });
-
- test('_handleValueChange', () => {
- const modifiedHandler = sandbox.stub();
- element.permission = {value: {rules: {}}};
- element.addEventListener('access-modified', modifiedHandler);
- assert.isNotOk(element.permission.value.modified);
- element._handleValueChange();
- assert.isTrue(element.permission.value.modified);
- assert.isTrue(modifiedHandler.called);
- });
-
- test('Exclusive hidden for owner permission', () => {
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('#exclusiveToggle')).display,
- 'flex');
- element.set(['permission', 'id'], 'owner');
- flushAsynchronousOperations();
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('#exclusiveToggle')).display,
- 'none');
- });
-
- test('Exclusive hidden for any global permissions', () => {
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('#exclusiveToggle')).display,
- 'flex');
- element.section = 'GLOBAL_CAPABILITIES';
- flushAsynchronousOperations();
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('#exclusiveToggle')).display,
- 'none');
- });
+ assert.isFalse(element._computeHasRange('test'));
});
});
+
+ suite('interactions', () => {
+ setup(() => {
+ sandbox.spy(element, '_computeLabel');
+ element.name = 'Priority';
+ element.section = 'refs/*';
+ element.labels = {
+ 'Code-Review': {
+ values: {
+ ' 0': 'No score',
+ '-1': 'I would prefer this is not merged as is',
+ '-2': 'This shall not be merged',
+ '+1': 'Looks good to me, but someone else must approve',
+ '+2': 'Looks good to me, approved',
+ },
+ default_value: 0,
+ },
+ };
+ element.permission = {
+ id: 'label-Code-Review',
+ value: {
+ label: 'Code-Review',
+ rules: {
+ 'global:Project-Owners': {
+ action: 'ALLOW',
+ force: false,
+ min: -2,
+ max: 2,
+ },
+ '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+ action: 'ALLOW',
+ force: false,
+ min: -2,
+ max: 2,
+ },
+ },
+ },
+ };
+ element._setupValues();
+ flushAsynchronousOperations();
+ });
+
+ test('adding a rule', () => {
+ element.name = 'Priority';
+ element.section = 'refs/*';
+ element.groups = {};
+ element.$.groupAutocomplete.text = 'ldap/tests te.st';
+ const e = {
+ detail: {
+ value: {
+ id: 'ldap:CN=test+te.st',
+ },
+ },
+ };
+ element.editing = true;
+ assert.equal(element._rules.length, 2);
+ assert.equal(Object.keys(element._groupsWithRules).length, 2);
+ element._handleAddRuleItem(e);
+ flushAsynchronousOperations();
+ 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.deepEqual(element.permission.value.rules['ldap:CN=test te.st'],
+ {action: 'ALLOW', min: -2, max: 2, added: true});
+ // New rule should be removed if cancel from editing.
+ element.editing = false;
+ assert.equal(element._rules.length, 2);
+ assert.equal(Object.keys(element.permission.value.rules).length, 2);
+ });
+
+ test('removing an added rule', () => {
+ element.name = 'Priority';
+ element.section = 'refs/*';
+ element.groups = {};
+ element.$.groupAutocomplete.text = 'new group name';
+ assert.equal(element._rules.length, 2);
+ element.shadowRoot
+ .querySelector('gr-rule-editor').fire('added-rule-removed');
+ flushAsynchronousOperations();
+ assert.equal(element._rules.length, 1);
+ });
+
+ test('removing an added permission', () => {
+ const removeStub = sandbox.stub();
+ element.addEventListener('added-permission-removed', removeStub);
+ element.editing = true;
+ element.name = 'Priority';
+ element.section = 'refs/*';
+ element.permission.value.added = true;
+ MockInteractions.tap(element.$.removeBtn);
+ assert.isTrue(removeStub.called);
+ });
+
+ test('removing the permission', () => {
+ element.editing = true;
+ element.name = 'Priority';
+ element.section = 'refs/*';
+
+ const removeStub = sandbox.stub();
+ element.addEventListener('added-permission-removed', removeStub);
+
+ assert.isFalse(element.$.permission.classList.contains('deleted'));
+ assert.isFalse(element._deleted);
+ MockInteractions.tap(element.$.removeBtn);
+ assert.isTrue(element.$.permission.classList.contains('deleted'));
+ assert.isTrue(element._deleted);
+ MockInteractions.tap(element.$.undoRemoveBtn);
+ assert.isFalse(element.$.permission.classList.contains('deleted'));
+ assert.isFalse(element._deleted);
+ assert.isFalse(removeStub.called);
+ });
+
+ test('modify a permission', () => {
+ element.editing = true;
+ element.name = 'Priority';
+ element.section = 'refs/*';
+
+ assert.isFalse(element._originalExclusiveValue);
+ assert.isNotOk(element.permission.value.modified);
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('#exclusiveToggle'));
+ flushAsynchronousOperations();
+ assert.isTrue(element.permission.value.exclusive);
+ assert.isTrue(element.permission.value.modified);
+ assert.isFalse(element._originalExclusiveValue);
+ element.editing = false;
+ assert.isFalse(element.permission.value.exclusive);
+ });
+
+ test('_handleValueChange', () => {
+ const modifiedHandler = sandbox.stub();
+ element.permission = {value: {rules: {}}};
+ element.addEventListener('access-modified', modifiedHandler);
+ assert.isNotOk(element.permission.value.modified);
+ element._handleValueChange();
+ assert.isTrue(element.permission.value.modified);
+ assert.isTrue(modifiedHandler.called);
+ });
+
+ test('Exclusive hidden for owner permission', () => {
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('#exclusiveToggle')).display,
+ 'flex');
+ element.set(['permission', 'id'], 'owner');
+ flushAsynchronousOperations();
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('#exclusiveToggle')).display,
+ 'none');
+ });
+
+ test('Exclusive hidden for any global permissions', () => {
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('#exclusiveToggle')).display,
+ 'flex');
+ element.section = 'GLOBAL_CAPABILITIES';
+ flushAsynchronousOperations();
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('#exclusiveToggle')).display,
+ 'none');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.html b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.html
deleted file mode 100644
index f6c744b..0000000
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.html
+++ /dev/null
@@ -1,106 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-
-<dom-module id="gr-plugin-config-array-editor">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- .wrapper {
- width: 30em;
- }
- .existingItems {
- background: var(--table-header-background-color);
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius);
- }
- gr-button {
- float: right;
- margin-left: var(--spacing-m);
- width: 4.5em;
- }
- .row {
- align-items: center;
- display: flex;
- justify-content: space-between;
- padding: var(--spacing-m) 0;
- width: 100%;
- }
- .existingItems .row {
- padding: var(--spacing-m);
- }
- .existingItems .row:not(:first-of-type) {
- border-top: 1px solid var(--border-color);
- }
- input {
- flex-grow: 1;
- }
- .hide {
- display: none;
- }
- .placeholder {
- color: var(--deemphasized-text-color);
- padding-top: var(--spacing-m);
- }
- </style>
- <div class="wrapper gr-form-styles">
- <template is="dom-if" if="[[pluginOption.info.values.length]]">
- <div class="existingItems">
- <template is="dom-repeat" items="[[pluginOption.info.values]]">
- <div class="row">
- <span>[[item]]</span>
- <gr-button
- link
- disabled$="[[disabled]]"
- data-item$="[[item]]"
- on-click="_handleDelete">Delete</gr-button>
- </div>
- </template>
- </div>
- </template>
- <template is="dom-if" if="[[!pluginOption.info.values.length]]">
- <div class="row placeholder">None configured.</div>
- </template>
- <div class$="row [[_computeShowInputRow(disabled)]]">
- <iron-input
- on-keydown="_handleInputKeydown"
- bind-value="{{_newValue}}">
- <input
- is="iron-input"
- id="input"
- on-keydown="_handleInputKeydown"
- bind-value="{{_newValue}}">
- </iron-input>
- <gr-button
- id="addButton"
- disabled$="[[!_newValue.length]]"
- link
- on-click="_handleAddTap">Add</gr-button>
- </div>
- </div>
- </template>
- <script src="gr-plugin-config-array-editor.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
index 92a8655..318c2c3 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
@@ -14,84 +14,95 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrPluginConfigArrayEditor extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-plugin-config-array-editor'; }
- /**
- * Fired when the plugin config option changes.
- *
- * @event plugin-config-option-changed
- */
+import '@polymer/iron-input/iron-input.js';
+import '@polymer/paper-toggle-button/paper-toggle-button.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-plugin-config-array-editor_html.js';
- static get properties() {
- return {
+/** @extends Polymer.Element */
+class GrPluginConfigArrayEditor extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-plugin-config-array-editor'; }
+ /**
+ * Fired when the plugin config option changes.
+ *
+ * @event plugin-config-option-changed
+ */
+
+ static get properties() {
+ return {
+ /** @type {?} */
+ pluginOption: Object,
+ /** @type {boolean} */
+ disabled: {
+ type: Boolean,
+ computed: '_computeDisabled(pluginOption.*)',
+ },
/** @type {?} */
- pluginOption: Object,
- /** @type {boolean} */
- disabled: {
- type: Boolean,
- computed: '_computeDisabled(pluginOption.*)',
- },
- /** @type {?} */
- _newValue: {
- type: String,
- value: '',
- },
- };
- }
+ _newValue: {
+ type: String,
+ value: '',
+ },
+ };
+ }
- _computeDisabled(record) {
- return !(record && record.base && record.base.info &&
- record.base.info.editable);
- }
+ _computeDisabled(record) {
+ return !(record && record.base && record.base.info &&
+ record.base.info.editable);
+ }
- _handleAddTap(e) {
+ _handleAddTap(e) {
+ e.preventDefault();
+ this._handleAdd();
+ }
+
+ _handleInputKeydown(e) {
+ // Enter.
+ if (e.keyCode === 13) {
e.preventDefault();
this._handleAdd();
}
-
- _handleInputKeydown(e) {
- // Enter.
- if (e.keyCode === 13) {
- e.preventDefault();
- this._handleAdd();
- }
- }
-
- _handleAdd() {
- if (!this._newValue.length) { return; }
- this._dispatchChanged(
- this.pluginOption.info.values.concat([this._newValue]));
- this._newValue = '';
- }
-
- _handleDelete(e) {
- const value = Polymer.dom(e).localTarget.dataset.item;
- this._dispatchChanged(
- this.pluginOption.info.values.filter(str => str !== value));
- }
-
- _dispatchChanged(values) {
- const {_key, info} = this.pluginOption;
- const detail = {
- _key,
- info: Object.assign(info, {values}, {}),
- notifyPath: `${_key}.values`,
- };
- this.dispatchEvent(
- new CustomEvent('plugin-config-option-changed', {detail}));
- }
-
- _computeShowInputRow(disabled) {
- return disabled ? 'hide' : '';
- }
}
- customElements.define(GrPluginConfigArrayEditor.is,
- GrPluginConfigArrayEditor);
-})();
+ _handleAdd() {
+ if (!this._newValue.length) { return; }
+ this._dispatchChanged(
+ this.pluginOption.info.values.concat([this._newValue]));
+ this._newValue = '';
+ }
+
+ _handleDelete(e) {
+ const value = dom(e).localTarget.dataset.item;
+ this._dispatchChanged(
+ this.pluginOption.info.values.filter(str => str !== value));
+ }
+
+ _dispatchChanged(values) {
+ const {_key, info} = this.pluginOption;
+ const detail = {
+ _key,
+ info: Object.assign(info, {values}, {}),
+ notifyPath: `${_key}.values`,
+ };
+ this.dispatchEvent(
+ new CustomEvent('plugin-config-option-changed', {detail}));
+ }
+
+ _computeShowInputRow(disabled) {
+ return disabled ? 'hide' : '';
+ }
+}
+
+customElements.define(GrPluginConfigArrayEditor.is,
+ GrPluginConfigArrayEditor);
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.js b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.js
new file mode 100644
index 0000000..d97e2b37
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.js
@@ -0,0 +1,82 @@
+/**
+ * @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.js';
+
+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">
+ .wrapper {
+ width: 30em;
+ }
+ .existingItems {
+ background: var(--table-header-background-color);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ }
+ gr-button {
+ float: right;
+ margin-left: var(--spacing-m);
+ width: 4.5em;
+ }
+ .row {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+ padding: var(--spacing-m) 0;
+ width: 100%;
+ }
+ .existingItems .row {
+ padding: var(--spacing-m);
+ }
+ .existingItems .row:not(:first-of-type) {
+ border-top: 1px solid var(--border-color);
+ }
+ input {
+ flex-grow: 1;
+ }
+ .hide {
+ display: none;
+ }
+ .placeholder {
+ color: var(--deemphasized-text-color);
+ padding-top: var(--spacing-m);
+ }
+ </style>
+ <div class="wrapper gr-form-styles">
+ <template is="dom-if" if="[[pluginOption.info.values.length]]">
+ <div class="existingItems">
+ <template is="dom-repeat" items="[[pluginOption.info.values]]">
+ <div class="row">
+ <span>[[item]]</span>
+ <gr-button link="" disabled\$="[[disabled]]" data-item\$="[[item]]" on-click="_handleDelete">Delete</gr-button>
+ </div>
+ </template>
+ </div>
+ </template>
+ <template is="dom-if" if="[[!pluginOption.info.values.length]]">
+ <div class="row placeholder">None configured.</div>
+ </template>
+ <div class\$="row [[_computeShowInputRow(disabled)]]">
+ <iron-input on-keydown="_handleInputKeydown" bind-value="{{_newValue}}">
+ <input is="iron-input" id="input" on-keydown="_handleInputKeydown" bind-value="{{_newValue}}">
+ </iron-input>
+ <gr-button id="addButton" disabled\$="[[!_newValue.length]]" link="" on-click="_handleAddTap">Add</gr-button>
+ </div>
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html
index 3342967..ecb2ec3 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-plugin-config-array-editor</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-plugin-config-array-editor.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,112 +30,114 @@
</template>
</test-fixture>
-<script>
- suite('gr-plugin-config-array-editor tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let dispatchStub;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-plugin-config-array-editor.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-plugin-config-array-editor tests', () => {
+ let element;
+ let sandbox;
+ let dispatchStub;
- const getAll = str => Polymer.dom(element.root).querySelectorAll(str);
+ const getAll = str => dom(element.root).querySelectorAll(str);
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.pluginOption = {
+ _key: 'test-key',
+ info: {
+ values: [],
+ },
+ };
+ });
+
+ teardown(() => sandbox.restore());
+
+ test('_computeShowInputRow', () => {
+ assert.equal(element._computeShowInputRow(true), 'hide');
+ assert.equal(element._computeShowInputRow(false), '');
+ });
+
+ test('_computeDisabled', () => {
+ assert.isTrue(element._computeDisabled({}));
+ assert.isTrue(element._computeDisabled({base: {}}));
+ assert.isTrue(element._computeDisabled({base: {info: {}}}));
+ assert.isTrue(
+ element._computeDisabled({base: {info: {editable: false}}}));
+ assert.isFalse(
+ element._computeDisabled({base: {info: {editable: true}}}));
+ });
+
+ suite('adding', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.pluginOption = {
- _key: 'test-key',
- info: {
- values: [],
- },
- };
- });
-
- teardown(() => sandbox.restore());
-
- test('_computeShowInputRow', () => {
- assert.equal(element._computeShowInputRow(true), 'hide');
- assert.equal(element._computeShowInputRow(false), '');
- });
-
- test('_computeDisabled', () => {
- assert.isTrue(element._computeDisabled({}));
- assert.isTrue(element._computeDisabled({base: {}}));
- assert.isTrue(element._computeDisabled({base: {info: {}}}));
- assert.isTrue(
- element._computeDisabled({base: {info: {editable: false}}}));
- assert.isFalse(
- element._computeDisabled({base: {info: {editable: true}}}));
- });
-
- suite('adding', () => {
- setup(() => {
- dispatchStub = sandbox.stub(element, '_dispatchChanged');
- });
-
- test('with enter', () => {
- element._newValue = '';
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
- flushAsynchronousOperations();
-
- assert.isFalse(dispatchStub.called);
- element._newValue = 'test';
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
- flushAsynchronousOperations();
-
- assert.isTrue(dispatchStub.called);
- assert.equal(dispatchStub.lastCall.args[0], 'test');
- assert.equal(element._newValue, '');
- });
-
- test('with add btn', () => {
- element._newValue = '';
- MockInteractions.tap(element.$.addButton);
- flushAsynchronousOperations();
-
- assert.isFalse(dispatchStub.called);
- element._newValue = 'test';
- MockInteractions.tap(element.$.addButton);
- flushAsynchronousOperations();
-
- assert.isTrue(dispatchStub.called);
- assert.equal(dispatchStub.lastCall.args[0], 'test');
- assert.equal(element._newValue, '');
- });
- });
-
- test('deleting', () => {
dispatchStub = sandbox.stub(element, '_dispatchChanged');
- element.pluginOption = {info: {values: ['test', 'test2']}};
- flushAsynchronousOperations();
+ });
- const rows = getAll('.existingItems .row');
- assert.equal(rows.length, 2);
- const button = rows[0].querySelector('gr-button');
-
- MockInteractions.tap(button);
+ test('with enter', () => {
+ element._newValue = '';
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
flushAsynchronousOperations();
assert.isFalse(dispatchStub.called);
- element.pluginOption.info.editable = true;
- element.notifyPath('pluginOption.info.editable');
- flushAsynchronousOperations();
-
- MockInteractions.tap(button);
+ element._newValue = 'test';
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
flushAsynchronousOperations();
assert.isTrue(dispatchStub.called);
- assert.deepEqual(dispatchStub.lastCall.args[0], ['test2']);
+ assert.equal(dispatchStub.lastCall.args[0], 'test');
+ assert.equal(element._newValue, '');
});
- test('_dispatchChanged', () => {
- const eventStub = sandbox.stub(element, 'dispatchEvent');
- element._dispatchChanged(['new-test-value']);
+ test('with add btn', () => {
+ element._newValue = '';
+ MockInteractions.tap(element.$.addButton);
+ flushAsynchronousOperations();
- assert.isTrue(eventStub.called);
- const {detail} = eventStub.lastCall.args[0];
- assert.equal(detail._key, 'test-key');
- assert.deepEqual(detail.info, {values: ['new-test-value']});
- assert.equal(detail.notifyPath, 'test-key.values');
+ assert.isFalse(dispatchStub.called);
+ element._newValue = 'test';
+ MockInteractions.tap(element.$.addButton);
+ flushAsynchronousOperations();
+
+ assert.isTrue(dispatchStub.called);
+ assert.equal(dispatchStub.lastCall.args[0], 'test');
+ assert.equal(element._newValue, '');
});
});
+
+ test('deleting', () => {
+ dispatchStub = sandbox.stub(element, '_dispatchChanged');
+ element.pluginOption = {info: {values: ['test', 'test2']}};
+ flushAsynchronousOperations();
+
+ const rows = getAll('.existingItems .row');
+ assert.equal(rows.length, 2);
+ const button = rows[0].querySelector('gr-button');
+
+ MockInteractions.tap(button);
+ flushAsynchronousOperations();
+
+ assert.isFalse(dispatchStub.called);
+ element.pluginOption.info.editable = true;
+ element.notifyPath('pluginOption.info.editable');
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(button);
+ flushAsynchronousOperations();
+
+ assert.isTrue(dispatchStub.called);
+ assert.deepEqual(dispatchStub.lastCall.args[0], ['test2']);
+ });
+
+ test('_dispatchChanged', () => {
+ const eventStub = sandbox.stub(element, 'dispatchEvent');
+ element._dispatchChanged(['new-test-value']);
+
+ assert.isTrue(eventStub.called);
+ const {detail} = eventStub.lastCall.args[0];
+ assert.equal(detail._key, 'test-key');
+ assert.deepEqual(detail.info, {values: ['new-test-value']});
+ assert.equal(detail.notifyPath, 'test-key.values');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html
deleted file mode 100644
index b056f92..0000000
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html
+++ /dev/null
@@ -1,71 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../styles/gr-table-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-plugin-list">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-table-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <gr-list-view
- filter="[[_filter]]"
- items-per-page="[[_pluginsPerPage]]"
- items="[[_plugins]]"
- loading="[[_loading]]"
- offset="[[_offset]]"
- path="[[_path]]">
- <table id="list" class="genericList">
- <tr class="headerRow">
- <th class="name topHeader">Plugin Name</th>
- <th class="version topHeader">Version</th>
- <th class="status topHeader">Status</th>
- </tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
- <td>Loading...</td>
- </tr>
- <tbody class$="[[computeLoadingClass(_loading)]]">
- <template is="dom-repeat" items="[[_shownPlugins]]">
- <tr class="table">
- <td class="name">
- <template is="dom-if" if="[[item.index_url]]">
- <a href$="[[_computePluginUrl(item.index_url)]]">[[item.id]]</a>
- </template>
- <template is="dom-if" if="[[!item.index_url]]">
- [[item.id]]
- </template>
- </td>
- <td class="version">[[item.version]]</td>
- <td class="status">[[_status(item)]]</td>
- </tr>
- </template>
- </tbody>
- </table>
- </gr-list-view>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-plugin-list.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
index 5dd6ec2..d5a4e08 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
@@ -14,110 +14,122 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.ListViewMixin
- * @extends Polymer.Element
- */
- class GrPluginList extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.ListViewBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-plugin-list'; }
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
+import '../../../styles/gr-table-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-list-view/gr-list-view.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-plugin-list_html.js';
- static get properties() {
- return {
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.ListViewMixin
+ * @extends Polymer.Element
+ */
+class GrPluginList extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.ListViewBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-plugin-list'; }
+
+ static get properties() {
+ return {
+ /**
+ * URL params passed from the router.
+ */
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
/**
- * URL params passed from the router.
+ * Offset of currently visible query results.
*/
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
- /**
- * Offset of currently visible query results.
- */
- _offset: {
- type: Number,
- value: 0,
- },
- _path: {
- type: String,
- readOnly: true,
- value: '/admin/plugins',
- },
- _plugins: Array,
- /**
- * Because we request one more than the pluginsPerPage, _shownPlugins
- * maybe one less than _plugins.
- * */
- _shownPlugins: {
- type: Array,
- computed: 'computeShownItems(_plugins)',
- },
- _pluginsPerPage: {
- type: Number,
- value: 25,
- },
- _loading: {
- type: Boolean,
- value: true,
- },
- _filter: {
- type: String,
- value: '',
- },
- };
- }
-
- /** @override */
- attached() {
- super.attached();
- this.fire('title-change', {title: 'Plugins'});
- }
-
- _paramsChanged(params) {
- this._loading = true;
- this._filter = this.getFilterValue(params);
- this._offset = this.getOffsetValue(params);
-
- return this._getPlugins(this._filter, this._pluginsPerPage,
- this._offset);
- }
-
- _getPlugins(filter, pluginsPerPage, offset) {
- const errFn = response => {
- this.fire('page-error', {response});
- };
- return this.$.restAPI.getPlugins(filter, pluginsPerPage, offset, errFn)
- .then(plugins => {
- if (!plugins) {
- this._plugins = [];
- return;
- }
- this._plugins = Object.keys(plugins)
- .map(key => {
- const plugin = plugins[key];
- plugin.name = key;
- return plugin;
- });
- this._loading = false;
- });
- }
-
- _status(item) {
- return item.disabled === true ? 'Disabled' : 'Enabled';
- }
-
- _computePluginUrl(id) {
- return this.getUrl('/', id);
- }
+ _offset: {
+ type: Number,
+ value: 0,
+ },
+ _path: {
+ type: String,
+ readOnly: true,
+ value: '/admin/plugins',
+ },
+ _plugins: Array,
+ /**
+ * Because we request one more than the pluginsPerPage, _shownPlugins
+ * maybe one less than _plugins.
+ * */
+ _shownPlugins: {
+ type: Array,
+ computed: 'computeShownItems(_plugins)',
+ },
+ _pluginsPerPage: {
+ type: Number,
+ value: 25,
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _filter: {
+ type: String,
+ value: '',
+ },
+ };
}
- customElements.define(GrPluginList.is, GrPluginList);
-})();
+ /** @override */
+ attached() {
+ super.attached();
+ this.fire('title-change', {title: 'Plugins'});
+ }
+
+ _paramsChanged(params) {
+ this._loading = true;
+ this._filter = this.getFilterValue(params);
+ this._offset = this.getOffsetValue(params);
+
+ return this._getPlugins(this._filter, this._pluginsPerPage,
+ this._offset);
+ }
+
+ _getPlugins(filter, pluginsPerPage, offset) {
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
+ return this.$.restAPI.getPlugins(filter, pluginsPerPage, offset, errFn)
+ .then(plugins => {
+ if (!plugins) {
+ this._plugins = [];
+ return;
+ }
+ this._plugins = Object.keys(plugins)
+ .map(key => {
+ const plugin = plugins[key];
+ plugin.name = key;
+ return plugin;
+ });
+ this._loading = false;
+ });
+ }
+
+ _status(item) {
+ return item.disabled === true ? 'Disabled' : 'Enabled';
+ }
+
+ _computePluginUrl(id) {
+ return this.getUrl('/', id);
+ }
+}
+
+customElements.define(GrPluginList.is, GrPluginList);
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js
new file mode 100644
index 0000000..90192c4
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js
@@ -0,0 +1,55 @@
+/**
+ * @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.js';
+
+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-table-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <gr-list-view filter="[[_filter]]" items-per-page="[[_pluginsPerPage]]" items="[[_plugins]]" loading="[[_loading]]" offset="[[_offset]]" path="[[_path]]">
+ <table id="list" class="genericList">
+ <tbody><tr class="headerRow">
+ <th class="name topHeader">Plugin Name</th>
+ <th class="version topHeader">Version</th>
+ <th class="status topHeader">Status</th>
+ </tr>
+ <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]">
+ <td>Loading...</td>
+ </tr>
+ </tbody><tbody class\$="[[computeLoadingClass(_loading)]]">
+ <template is="dom-repeat" items="[[_shownPlugins]]">
+ <tr class="table">
+ <td class="name">
+ <template is="dom-if" if="[[item.index_url]]">
+ <a href\$="[[_computePluginUrl(item.index_url)]]">[[item.id]]</a>
+ </template>
+ <template is="dom-if" if="[[!item.index_url]]">
+ [[item.id]]
+ </template>
+ </td>
+ <td class="version">[[item.version]]</td>
+ <td class="status">[[_status(item)]]</td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ </gr-list-view>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
index 67a6c3f..da626e9 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
@@ -19,16 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-plugin-list</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-plugin-list.html">
-
-<script>void(0);</script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,151 +31,153 @@
</template>
</test-fixture>
-<script>
- let counter;
- const pluginGenerator = () => {
- const plugin = {
- id: `test${++counter}`,
- version: '3.0-SNAPSHOT',
- disabled: false,
- };
-
- if (counter !== 2) {
- plugin.index_url = `plugins/test${counter}/`;
- }
- return plugin;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-plugin-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+let counter;
+const pluginGenerator = () => {
+ const plugin = {
+ id: `test${++counter}`,
+ version: '3.0-SNAPSHOT',
+ disabled: false,
};
- suite('gr-plugin-list tests', async () => {
- await readyToTest();
- let element;
- let plugins;
- let sandbox;
- let value;
+ if (counter !== 2) {
+ plugin.index_url = `plugins/test${counter}/`;
+ }
+ return plugin;
+};
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- counter = 0;
+suite('gr-plugin-list tests', () => {
+ let element;
+ let plugins;
+ let sandbox;
+ let value;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ counter = 0;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('list with plugins', () => {
+ setup(done => {
+ plugins = _.times(26, pluginGenerator);
+
+ stub('gr-rest-api-interface', {
+ getPlugins(num, offset) {
+ return Promise.resolve(plugins);
+ },
+ });
+
+ element._paramsChanged(value).then(() => { flush(done); });
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('list with plugins', () => {
- setup(done => {
- plugins = _.times(26, pluginGenerator);
-
- stub('gr-rest-api-interface', {
- getPlugins(num, offset) {
- return Promise.resolve(plugins);
- },
- });
-
- element._paramsChanged(value).then(() => { flush(done); });
- });
-
- test('plugin in the list is formatted correctly', done => {
- flush(() => {
- assert.equal(element._plugins[2].id, 'test3');
- assert.equal(element._plugins[2].index_url, 'plugins/test3/');
- assert.equal(element._plugins[2].version, '3.0-SNAPSHOT');
- assert.equal(element._plugins[2].disabled, false);
- done();
- });
- });
-
- test('with and without urls', done => {
- flush(() => {
- const names = Polymer.dom(element.root).querySelectorAll('.name');
- assert.isOk(names[1].querySelector('a'));
- assert.equal(names[1].querySelector('a').innerText, 'test1');
- assert.isNotOk(names[2].querySelector('a'));
- assert.equal(names[2].innerText, 'test2');
- done();
- });
- });
-
- test('_shownPlugins', () => {
- assert.equal(element._shownPlugins.length, 25);
+ test('plugin in the list is formatted correctly', done => {
+ flush(() => {
+ assert.equal(element._plugins[2].id, 'test3');
+ assert.equal(element._plugins[2].index_url, 'plugins/test3/');
+ assert.equal(element._plugins[2].version, '3.0-SNAPSHOT');
+ assert.equal(element._plugins[2].disabled, false);
+ done();
});
});
- suite('list with less then 26 plugins', () => {
- setup(done => {
- plugins = _.times(25, pluginGenerator);
-
- stub('gr-rest-api-interface', {
- getPlugins(num, offset) {
- return Promise.resolve(plugins);
- },
- });
-
- element._paramsChanged(value).then(() => { flush(done); });
- });
-
- test('_shownPlugins', () => {
- assert.equal(element._shownPlugins.length, 25);
+ test('with and without urls', done => {
+ flush(() => {
+ const names = dom(element.root).querySelectorAll('.name');
+ assert.isOk(names[1].querySelector('a'));
+ assert.equal(names[1].querySelector('a').innerText, 'test1');
+ assert.isNotOk(names[2].querySelector('a'));
+ assert.equal(names[2].innerText, 'test2');
+ done();
});
});
- suite('filter', () => {
- test('_paramsChanged', done => {
- sandbox.stub(
- element.$.restAPI,
- 'getPlugins',
- () => Promise.resolve(plugins));
- const value = {
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(value).then(() => {
- assert.equal(element.$.restAPI.getPlugins.lastCall.args[0],
- 'test');
- assert.equal(element.$.restAPI.getPlugins.lastCall.args[1],
- 25);
- assert.equal(element.$.restAPI.getPlugins.lastCall.args[2],
- 25);
- done();
- });
+ test('_shownPlugins', () => {
+ assert.equal(element._shownPlugins.length, 25);
+ });
+ });
+
+ suite('list with less then 26 plugins', () => {
+ setup(done => {
+ plugins = _.times(25, pluginGenerator);
+
+ stub('gr-rest-api-interface', {
+ getPlugins(num, offset) {
+ return Promise.resolve(plugins);
+ },
});
+
+ element._paramsChanged(value).then(() => { flush(done); });
});
- suite('loading', () => {
- test('correct contents are displayed', () => {
- assert.isTrue(element._loading);
- assert.equal(element.computeLoadingClass(element._loading), 'loading');
- assert.equal(getComputedStyle(element.$.loading).display, 'block');
-
- element._loading = false;
- element._plugins = _.times(25, pluginGenerator);
-
- flushAsynchronousOperations();
- assert.equal(element.computeLoadingClass(element._loading), '');
- assert.equal(getComputedStyle(element.$.loading).display, 'none');
- });
+ test('_shownPlugins', () => {
+ assert.equal(element._shownPlugins.length, 25);
});
+ });
- suite('404', () => {
- test('fires page-error', done => {
- const response = {status: 404};
- sandbox.stub(element.$.restAPI, 'getPlugins',
- (filter, pluginsPerPage, opt_offset, errFn) => {
- errFn(response);
- });
-
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- const value = {
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(value);
+ suite('filter', () => {
+ test('_paramsChanged', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'getPlugins',
+ () => Promise.resolve(plugins));
+ const value = {
+ filter: 'test',
+ offset: 25,
+ };
+ element._paramsChanged(value).then(() => {
+ assert.equal(element.$.restAPI.getPlugins.lastCall.args[0],
+ 'test');
+ assert.equal(element.$.restAPI.getPlugins.lastCall.args[1],
+ 25);
+ assert.equal(element.$.restAPI.getPlugins.lastCall.args[2],
+ 25);
+ done();
});
});
});
+
+ suite('loading', () => {
+ test('correct contents are displayed', () => {
+ assert.isTrue(element._loading);
+ assert.equal(element.computeLoadingClass(element._loading), 'loading');
+ assert.equal(getComputedStyle(element.$.loading).display, 'block');
+
+ element._loading = false;
+ element._plugins = _.times(25, pluginGenerator);
+
+ flushAsynchronousOperations();
+ assert.equal(element.computeLoadingClass(element._loading), '');
+ assert.equal(getComputedStyle(element.$.loading).display, 'none');
+ });
+ });
+
+ suite('404', () => {
+ test('fires page-error', done => {
+ const response = {status: 404};
+ sandbox.stub(element.$.restAPI, 'getPlugins',
+ (filter, pluginsPerPage, opt_offset, errFn) => {
+ errFn(response);
+ });
+
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
+ });
+
+ const value = {
+ filter: 'test',
+ offset: 25,
+ };
+ element._paramsChanged(value);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
deleted file mode 100644
index 54006b5..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
+++ /dev/null
@@ -1,139 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-
-<link rel="import" href="../../../styles/gr-menu-page-styles.html">
-<link rel="import" href="../../../styles/gr-subpage-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-access-section/gr-access-section.html">
-
-<script src="../../../scripts/util.js"></script>
-
-<dom-module id="gr-repo-access">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-subpage-styles">
- gr-button,
- #inheritsFrom,
- #editInheritFromInput,
- .editing #inheritFromName,
- .weblinks,
- .editing .invisible{
- display: none;
- }
- #inheritsFrom.show {
- display: flex;
- min-height: 2em;
- align-items: center;
- }
- .weblink {
- margin-right: var(--spacing-xs);
- }
- .weblinks.show,
- .referenceContainer {
- display: block;
- }
- .rightsText {
- margin-right: var(--spacing-s);
- }
-
- .editing gr-button,
- .admin #editBtn {
- display: inline-block;
- margin: var(--spacing-l) 0;
- }
- .editing #editInheritFromInput {
- display: inline-block;
- }
- </style>
- <style include="gr-menu-page-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <main class$="[[_computeMainClass(_ownerOf, _canUpload, _editing)]]">
- <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
- Loading...
- </div>
- <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <h3 id="inheritsFrom" class$="[[_computeShowInherit(_inheritsFrom)]]">
- <span class="rightsText">Rights Inherit From</span>
- <a
- href$="[[_computeParentHref(_inheritsFrom.name)]]"
- rel="noopener"
- id="inheritFromName">
- [[_inheritsFrom.name]]</a>
- <gr-autocomplete
- id="editInheritFromInput"
- text="{{_inheritFromFilter}}"
- query="[[_query]]"
- on-commit="_handleUpdateInheritFrom"></gr-autocomplete>
- </h3>
- <div class$="weblinks [[_computeWebLinkClass(_weblinks)]]">
- History:
- <template is="dom-repeat" items="[[_weblinks]]" as="link">
- <a href="[[link.url]]" class="weblink" rel="noopener" target="[[link.target]]">
- [[link.name]]
- </a>
- </template>
- </div>
- <gr-button id="editBtn"
- on-click="_handleEdit">[[_editOrCancel(_editing)]]</gr-button>
- <gr-button id="saveBtn"
- primary
- class$="[[_computeSaveBtnClass(_ownerOf)]]"
- on-click="_handleSave"
- disabled="[[!_modified]]">Save</gr-button>
- <gr-button id="saveReviewBtn"
- primary
- class$="[[_computeSaveReviewBtnClass(_canUpload)]]"
- on-click="_handleSaveForReview"
- disabled="[[!_modified]]">Save for review</gr-button>
- <template
- is="dom-repeat"
- items="{{_sections}}"
- initial-count="5"
- target-framerate="60"
- as="section">
- <gr-access-section
- capabilities="[[_capabilities]]"
- section="{{section}}"
- labels="[[_labels]]"
- can-upload="[[_canUpload]]"
- editing="[[_editing]]"
- owner-of="[[_ownerOf]]"
- groups="[[_groups]]"
- on-added-section-removed="_handleAddedSectionRemoved"></gr-access-section>
- </template>
- <div class="referenceContainer">
- <gr-button id="addReferenceBtn"
- on-click="_handleCreateSection">Add Reference</gr-button>
- </div>
- </div>
- </main>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-repo-access.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
index 02b62e0..6cfa7ff 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
@@ -14,497 +14,515 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const Defs = {};
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../styles/gr-menu-page-styles.js';
+import '../../../styles/gr-subpage-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-access-section/gr-access-section.js';
+import '../../../scripts/util.js';
+import {flush, dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-repo-access_html.js';
- const NOTHING_TO_SAVE = 'No changes to save.';
+const Defs = {};
- const MAX_AUTOCOMPLETE_RESULTS = 50;
+const NOTHING_TO_SAVE = 'No changes to save.';
- /**
- * Fired when save is a no-op
- *
- * @event show-alert
- */
+const MAX_AUTOCOMPLETE_RESULTS = 50;
- /**
- * @typedef {{
- * value: !Object,
- * }}
- */
- Defs.rule;
+/**
+ * Fired when save is a no-op
+ *
+ * @event show-alert
+ */
- /**
- * @typedef {{
- * rules: !Object<string, Defs.rule>
- * }}
- */
- Defs.permission;
+/**
+ * @typedef {{
+ * value: !Object,
+ * }}
+ */
+Defs.rule;
- /**
- * Can be an empty object or consist of permissions.
- *
- * @typedef {{
- * permissions: !Object<string, Defs.permission>
- * }}
- */
- Defs.permissions;
+/**
+ * @typedef {{
+ * rules: !Object<string, Defs.rule>
+ * }}
+ */
+Defs.permission;
- /**
- * Can be an empty object or consist of permissions.
- *
- * @typedef {!Object<string, Defs.permissions>}
- */
- Defs.sections;
+/**
+ * Can be an empty object or consist of permissions.
+ *
+ * @typedef {{
+ * permissions: !Object<string, Defs.permission>
+ * }}
+ */
+Defs.permissions;
- /**
- * @typedef {{
- * remove: !Defs.sections,
- * add: !Defs.sections,
- * }}
- */
- Defs.projectAccessInput;
+/**
+ * Can be an empty object or consist of permissions.
+ *
+ * @typedef {!Object<string, Defs.permissions>}
+ */
+Defs.sections;
- /**
- * @appliesMixin Gerrit.AccessMixin
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrRepoAccess extends Polymer.mixinBehaviors( [
- Gerrit.AccessBehavior,
- Gerrit.BaseUrlBehavior,
- Gerrit.FireBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-repo-access'; }
+/**
+ * @typedef {{
+ * remove: !Defs.sections,
+ * add: !Defs.sections,
+ * }}
+ */
+Defs.projectAccessInput;
- static get properties() {
- return {
- repo: {
- type: String,
- observer: '_repoChanged',
+/**
+ * @appliesMixin Gerrit.AccessMixin
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrRepoAccess extends mixinBehaviors( [
+ Gerrit.AccessBehavior,
+ Gerrit.BaseUrlBehavior,
+ Gerrit.FireBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-repo-access'; }
+
+ static get properties() {
+ return {
+ repo: {
+ type: String,
+ observer: '_repoChanged',
+ },
+ // The current path
+ path: String,
+
+ _canUpload: {
+ type: Boolean,
+ value: false,
+ },
+ _inheritFromFilter: String,
+ _query: {
+ type: Function,
+ value() {
+ return this._getInheritFromSuggestions.bind(this);
},
- // The current path
- path: String,
+ },
+ _ownerOf: Array,
+ _capabilities: Object,
+ _groups: Object,
+ /** @type {?} */
+ _inheritsFrom: Object,
+ _labels: Object,
+ _local: Object,
+ _editing: {
+ type: Boolean,
+ value: false,
+ observer: '_handleEditingChanged',
+ },
+ _modified: {
+ type: Boolean,
+ value: false,
+ },
+ _sections: Array,
+ _weblinks: Array,
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ };
+ }
- _canUpload: {
- type: Boolean,
- value: false,
- },
- _inheritFromFilter: String,
- _query: {
- type: Function,
- value() {
- return this._getInheritFromSuggestions.bind(this);
- },
- },
- _ownerOf: Array,
- _capabilities: Object,
- _groups: Object,
- /** @type {?} */
- _inheritsFrom: Object,
- _labels: Object,
- _local: Object,
- _editing: {
- type: Boolean,
- value: false,
- observer: '_handleEditingChanged',
- },
- _modified: {
- type: Boolean,
- value: false,
- },
- _sections: Array,
- _weblinks: Array,
- _loading: {
- type: Boolean,
- value: true,
- },
- };
- }
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('access-modified',
+ () =>
+ this._handleAccessModified());
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('access-modified',
- () =>
- this._handleAccessModified());
- }
+ _handleAccessModified() {
+ this._modified = true;
+ }
- _handleAccessModified() {
- this._modified = true;
- }
+ /**
+ * @param {string} repo
+ * @return {!Promise}
+ */
+ _repoChanged(repo) {
+ this._loading = true;
- /**
- * @param {string} repo
- * @return {!Promise}
- */
- _repoChanged(repo) {
- this._loading = true;
+ if (!repo) { return Promise.resolve(); }
- if (!repo) { return Promise.resolve(); }
+ return this._reload(repo);
+ }
- return this._reload(repo);
- }
+ _reload(repo) {
+ const promises = [];
- _reload(repo) {
- const promises = [];
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
- const errFn = response => {
- this.fire('page-error', {response});
- };
+ this._editing = false;
- this._editing = false;
+ // Always reset sections when a project changes.
+ this._sections = [];
+ promises.push(this.$.restAPI.getRepoAccessRights(repo, errFn)
+ .then(res => {
+ if (!res) { return Promise.resolve(); }
- // Always reset sections when a project changes.
- this._sections = [];
- promises.push(this.$.restAPI.getRepoAccessRights(repo, errFn)
- .then(res => {
- if (!res) { return Promise.resolve(); }
-
- // Keep a copy of the original inherit from values separate from
- // the ones data bound to gr-autocomplete, so the original value
- // can be restored if the user cancels.
- this._inheritsFrom = res.inherits_from ? Object.assign({},
- res.inherits_from) : null;
- this._originalInheritsFrom = res.inherits_from ? Object.assign({},
- res.inherits_from) : null;
- // Initialize the filter value so when the user clicks edit, the
- // current value appears. If there is no parent repo, it is
- // initialized as an empty string.
- this._inheritFromFilter = res.inherits_from ?
- this._inheritsFrom.name : '';
- this._local = res.local;
- this._groups = res.groups;
- this._weblinks = res.config_web_links || [];
- this._canUpload = res.can_upload;
- this._ownerOf = res.owner_of || [];
- return this.toSortedArray(this._local);
- }));
-
- promises.push(this.$.restAPI.getCapabilities(errFn)
- .then(res => {
- if (!res) { return Promise.resolve(); }
-
- return res;
- }));
-
- promises.push(this.$.restAPI.getRepo(repo, errFn)
- .then(res => {
- if (!res) { return Promise.resolve(); }
-
- return res.labels;
- }));
-
- return Promise.all(promises).then(([sections, capabilities, labels]) => {
- this._capabilities = capabilities;
- this._labels = labels;
- this._sections = sections;
- this._loading = false;
- });
- }
-
- _handleUpdateInheritFrom(e) {
- if (!this._inheritsFrom) {
- this._inheritsFrom = {};
- }
- this._inheritsFrom.id = e.detail.value;
- this._inheritsFrom.name = this._inheritFromFilter;
- this._handleAccessModified();
- }
-
- _getInheritFromSuggestions() {
- return this.$.restAPI.getRepos(
- this._inheritFromFilter,
- MAX_AUTOCOMPLETE_RESULTS)
- .then(response => {
- const projects = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- projects.push({
- name: response[key].name,
- value: response[key].id,
- });
- }
- return projects;
- });
- }
-
- _computeLoadingClass(loading) {
- return loading ? 'loading' : '';
- }
-
- _handleEdit() {
- this._editing = !this._editing;
- }
-
- _editOrCancel(editing) {
- return editing ? 'Cancel' : 'Edit';
- }
-
- _computeWebLinkClass(weblinks) {
- return weblinks && weblinks.length ? 'show' : '';
- }
-
- _computeShowInherit(inheritsFrom) {
- return inheritsFrom ? 'show' : '';
- }
-
- _handleAddedSectionRemoved(e) {
- const index = e.model.index;
- this._sections = this._sections.slice(0, index)
- .concat(this._sections.slice(index + 1, this._sections.length));
- }
-
- _handleEditingChanged(editing, editingOld) {
- // Ignore when editing gets set initially.
- if (!editingOld || editing) { return; }
- // Remove any unsaved but added refs.
- if (this._sections) {
- this._sections = this._sections.filter(p => !p.value.added);
- }
- // Restore inheritFrom.
- if (this._inheritsFrom) {
- this._inheritsFrom = Object.assign({}, this._originalInheritsFrom);
- this._inheritFromFilter = this._inheritsFrom.name;
- }
- for (const key of Object.keys(this._local)) {
- if (this._local[key].added) {
- delete this._local[key];
- }
- }
- }
-
- /**
- * @param {!Defs.projectAccessInput} addRemoveObj
- * @param {!Array} path
- * @param {string} type add or remove
- * @param {!Object=} opt_value value to add if the type is 'add'
- * @return {!Defs.projectAccessInput}
- */
- _updateAddRemoveObj(addRemoveObj, path, type, opt_value) {
- let curPos = addRemoveObj[type];
- for (const item of path) {
- if (!curPos[item]) {
- if (item === path[path.length - 1] && type === 'remove') {
- if (path[path.length - 2] === 'permissions') {
- curPos[item] = {rules: {}};
- } else if (path.length === 1) {
- curPos[item] = {permissions: {}};
- } else {
- curPos[item] = {};
- }
- } else if (item === path[path.length - 1] && type === 'add') {
- curPos[item] = opt_value;
- } else {
- curPos[item] = {};
- }
- }
- curPos = curPos[item];
- }
- return addRemoveObj;
- }
-
- /**
- * Used to recursively remove any objects with a 'deleted' bit.
- */
- _recursivelyRemoveDeleted(obj) {
- for (const k in obj) {
- if (!obj.hasOwnProperty(k)) { continue; }
-
- if (typeof obj[k] == 'object') {
- if (obj[k].deleted) {
- delete obj[k];
- return;
- }
- this._recursivelyRemoveDeleted(obj[k]);
- }
- }
- }
-
- _recursivelyUpdateAddRemoveObj(obj, addRemoveObj, path = []) {
- for (const k in obj) {
- if (!obj.hasOwnProperty(k)) { continue; }
- if (typeof obj[k] == 'object') {
- const updatedId = obj[k].updatedId;
- const ref = updatedId ? updatedId : k;
- if (obj[k].deleted) {
- this._updateAddRemoveObj(addRemoveObj,
- path.concat(k), 'remove');
- continue;
- } else if (obj[k].modified) {
- this._updateAddRemoveObj(addRemoveObj,
- path.concat(k), 'remove');
- this._updateAddRemoveObj(addRemoveObj, path.concat(ref), 'add',
- obj[k]);
- /* Special case for ref changes because they need to be added and
- removed in a different way. The new ref needs to include all
- changes but also the initial state. To do this, instead of
- continuing with the same recursion, just remove anything that is
- deleted in the current state. */
- if (updatedId && updatedId !== k) {
- this._recursivelyRemoveDeleted(addRemoveObj.add[updatedId]);
- }
- continue;
- } else if (obj[k].added) {
- this._updateAddRemoveObj(addRemoveObj,
- path.concat(ref), 'add', obj[k]);
- /**
- * As add / delete both can happen in the new section,
- * so here to make sure it will remove the deleted ones.
- *
- * @see Issue 11339
- */
- this._recursivelyRemoveDeleted(addRemoveObj.add[k]);
- continue;
- }
- this._recursivelyUpdateAddRemoveObj(obj[k], addRemoveObj,
- path.concat(k));
- }
- }
- }
-
- /**
- * Returns an object formatted for saving or submitting access changes for
- * review
- *
- * @return {!Defs.projectAccessInput}
- */
- _computeAddAndRemove() {
- const addRemoveObj = {
- add: {},
- remove: {},
- };
-
- const originalInheritsFromId = this._originalInheritsFrom ?
- this.singleDecodeURL(this._originalInheritsFrom.id) :
- null;
- const inheritsFromId = this._inheritsFrom ?
- this.singleDecodeURL(this._inheritsFrom.id) :
- null;
-
- const inheritFromChanged =
- // Inherit from changed
- (originalInheritsFromId &&
- originalInheritsFromId !== inheritsFromId) ||
- // Inherit from added (did not have one initially);
- (!originalInheritsFromId && inheritsFromId);
-
- this._recursivelyUpdateAddRemoveObj(this._local, addRemoveObj);
-
- if (inheritFromChanged) {
- addRemoveObj.parent = inheritsFromId;
- }
- return addRemoveObj;
- }
-
- _handleCreateSection() {
- let newRef = 'refs/for/*';
- // Avoid using an already used key for the placeholder, since it
- // immediately gets added to an object.
- while (this._local[newRef]) {
- newRef = `${newRef}*`;
- }
- const section = {permissions: {}, added: true};
- this.push('_sections', {id: newRef, value: section});
- this.set(['_local', newRef], section);
- Polymer.dom.flush();
- Polymer.dom(this.root).querySelector('gr-access-section:last-of-type')
- .editReference();
- }
-
- _getObjforSave() {
- const addRemoveObj = this._computeAddAndRemove();
- // If there are no changes, don't actually save.
- if (!Object.keys(addRemoveObj.add).length &&
- !Object.keys(addRemoveObj.remove).length &&
- !addRemoveObj.parent) {
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {message: NOTHING_TO_SAVE},
- bubbles: true,
- composed: true,
+ // Keep a copy of the original inherit from values separate from
+ // the ones data bound to gr-autocomplete, so the original value
+ // can be restored if the user cancels.
+ this._inheritsFrom = res.inherits_from ? Object.assign({},
+ res.inherits_from) : null;
+ this._originalInheritsFrom = res.inherits_from ? Object.assign({},
+ res.inherits_from) : null;
+ // Initialize the filter value so when the user clicks edit, the
+ // current value appears. If there is no parent repo, it is
+ // initialized as an empty string.
+ this._inheritFromFilter = res.inherits_from ?
+ this._inheritsFrom.name : '';
+ this._local = res.local;
+ this._groups = res.groups;
+ this._weblinks = res.config_web_links || [];
+ this._canUpload = res.can_upload;
+ this._ownerOf = res.owner_of || [];
+ return this.toSortedArray(this._local);
}));
- return;
- }
- const obj = {
- add: addRemoveObj.add,
- remove: addRemoveObj.remove,
- };
- if (addRemoveObj.parent) {
- obj.parent = addRemoveObj.parent;
- }
- return obj;
- }
- _handleSave(e) {
- const obj = this._getObjforSave();
- if (!obj) { return; }
- const button = e && e.target;
- if (button) {
- button.loading = true;
+ promises.push(this.$.restAPI.getCapabilities(errFn)
+ .then(res => {
+ if (!res) { return Promise.resolve(); }
+
+ return res;
+ }));
+
+ promises.push(this.$.restAPI.getRepo(repo, errFn)
+ .then(res => {
+ if (!res) { return Promise.resolve(); }
+
+ return res.labels;
+ }));
+
+ return Promise.all(promises).then(([sections, capabilities, labels]) => {
+ this._capabilities = capabilities;
+ this._labels = labels;
+ this._sections = sections;
+ this._loading = false;
+ });
+ }
+
+ _handleUpdateInheritFrom(e) {
+ if (!this._inheritsFrom) {
+ this._inheritsFrom = {};
+ }
+ this._inheritsFrom.id = e.detail.value;
+ this._inheritsFrom.name = this._inheritFromFilter;
+ this._handleAccessModified();
+ }
+
+ _getInheritFromSuggestions() {
+ return this.$.restAPI.getRepos(
+ this._inheritFromFilter,
+ MAX_AUTOCOMPLETE_RESULTS)
+ .then(response => {
+ const projects = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ projects.push({
+ name: response[key].name,
+ value: response[key].id,
+ });
+ }
+ return projects;
+ });
+ }
+
+ _computeLoadingClass(loading) {
+ return loading ? 'loading' : '';
+ }
+
+ _handleEdit() {
+ this._editing = !this._editing;
+ }
+
+ _editOrCancel(editing) {
+ return editing ? 'Cancel' : 'Edit';
+ }
+
+ _computeWebLinkClass(weblinks) {
+ return weblinks && weblinks.length ? 'show' : '';
+ }
+
+ _computeShowInherit(inheritsFrom) {
+ return inheritsFrom ? 'show' : '';
+ }
+
+ _handleAddedSectionRemoved(e) {
+ const index = e.model.index;
+ this._sections = this._sections.slice(0, index)
+ .concat(this._sections.slice(index + 1, this._sections.length));
+ }
+
+ _handleEditingChanged(editing, editingOld) {
+ // Ignore when editing gets set initially.
+ if (!editingOld || editing) { return; }
+ // Remove any unsaved but added refs.
+ if (this._sections) {
+ this._sections = this._sections.filter(p => !p.value.added);
+ }
+ // Restore inheritFrom.
+ if (this._inheritsFrom) {
+ this._inheritsFrom = Object.assign({}, this._originalInheritsFrom);
+ this._inheritFromFilter = this._inheritsFrom.name;
+ }
+ for (const key of Object.keys(this._local)) {
+ if (this._local[key].added) {
+ delete this._local[key];
}
- return this.$.restAPI.setRepoAccessRights(this.repo, obj)
- .then(() => {
- this._reload(this.repo);
- })
- .finally(() => {
- this._modified = false;
- if (button) {
- button.loading = false;
- }
- });
- }
-
- _handleSaveForReview(e) {
- const obj = this._getObjforSave();
- if (!obj) { return; }
- const button = e && e.target;
- if (button) {
- button.loading = true;
- }
- return this.$.restAPI
- .setRepoAccessRightsForReview(this.repo, obj)
- .then(change => {
- Gerrit.Nav.navigateToChange(change);
- })
- .finally(() => {
- this._modified = false;
- if (button) {
- button.loading = false;
- }
- });
- }
-
- _computeSaveReviewBtnClass(canUpload) {
- return !canUpload ? 'invisible' : '';
- }
-
- _computeSaveBtnClass(ownerOf) {
- return ownerOf && ownerOf.length === 0 ? 'invisible' : '';
- }
-
- _computeMainClass(ownerOf, canUpload, editing) {
- const classList = [];
- if (ownerOf && ownerOf.length > 0 || canUpload) {
- classList.push('admin');
- }
- if (editing) {
- classList.push('editing');
- }
- return classList.join(' ');
- }
-
- _computeParentHref(repoName) {
- return this.getBaseUrl() +
- `/admin/repos/${this.encodeURL(repoName, true)},access`;
}
}
- customElements.define(GrRepoAccess.is, GrRepoAccess);
-})();
+ /**
+ * @param {!Defs.projectAccessInput} addRemoveObj
+ * @param {!Array} path
+ * @param {string} type add or remove
+ * @param {!Object=} opt_value value to add if the type is 'add'
+ * @return {!Defs.projectAccessInput}
+ */
+ _updateAddRemoveObj(addRemoveObj, path, type, opt_value) {
+ let curPos = addRemoveObj[type];
+ for (const item of path) {
+ if (!curPos[item]) {
+ if (item === path[path.length - 1] && type === 'remove') {
+ if (path[path.length - 2] === 'permissions') {
+ curPos[item] = {rules: {}};
+ } else if (path.length === 1) {
+ curPos[item] = {permissions: {}};
+ } else {
+ curPos[item] = {};
+ }
+ } else if (item === path[path.length - 1] && type === 'add') {
+ curPos[item] = opt_value;
+ } else {
+ curPos[item] = {};
+ }
+ }
+ curPos = curPos[item];
+ }
+ return addRemoveObj;
+ }
+
+ /**
+ * Used to recursively remove any objects with a 'deleted' bit.
+ */
+ _recursivelyRemoveDeleted(obj) {
+ for (const k in obj) {
+ if (!obj.hasOwnProperty(k)) { continue; }
+
+ if (typeof obj[k] == 'object') {
+ if (obj[k].deleted) {
+ delete obj[k];
+ return;
+ }
+ this._recursivelyRemoveDeleted(obj[k]);
+ }
+ }
+ }
+
+ _recursivelyUpdateAddRemoveObj(obj, addRemoveObj, path = []) {
+ for (const k in obj) {
+ if (!obj.hasOwnProperty(k)) { continue; }
+ if (typeof obj[k] == 'object') {
+ const updatedId = obj[k].updatedId;
+ const ref = updatedId ? updatedId : k;
+ if (obj[k].deleted) {
+ this._updateAddRemoveObj(addRemoveObj,
+ path.concat(k), 'remove');
+ continue;
+ } else if (obj[k].modified) {
+ this._updateAddRemoveObj(addRemoveObj,
+ path.concat(k), 'remove');
+ this._updateAddRemoveObj(addRemoveObj, path.concat(ref), 'add',
+ obj[k]);
+ /* Special case for ref changes because they need to be added and
+ removed in a different way. The new ref needs to include all
+ changes but also the initial state. To do this, instead of
+ continuing with the same recursion, just remove anything that is
+ deleted in the current state. */
+ if (updatedId && updatedId !== k) {
+ this._recursivelyRemoveDeleted(addRemoveObj.add[updatedId]);
+ }
+ continue;
+ } else if (obj[k].added) {
+ this._updateAddRemoveObj(addRemoveObj,
+ path.concat(ref), 'add', obj[k]);
+ /**
+ * As add / delete both can happen in the new section,
+ * so here to make sure it will remove the deleted ones.
+ *
+ * @see Issue 11339
+ */
+ this._recursivelyRemoveDeleted(addRemoveObj.add[k]);
+ continue;
+ }
+ this._recursivelyUpdateAddRemoveObj(obj[k], addRemoveObj,
+ path.concat(k));
+ }
+ }
+ }
+
+ /**
+ * Returns an object formatted for saving or submitting access changes for
+ * review
+ *
+ * @return {!Defs.projectAccessInput}
+ */
+ _computeAddAndRemove() {
+ const addRemoveObj = {
+ add: {},
+ remove: {},
+ };
+
+ const originalInheritsFromId = this._originalInheritsFrom ?
+ this.singleDecodeURL(this._originalInheritsFrom.id) :
+ null;
+ const inheritsFromId = this._inheritsFrom ?
+ this.singleDecodeURL(this._inheritsFrom.id) :
+ null;
+
+ const inheritFromChanged =
+ // Inherit from changed
+ (originalInheritsFromId &&
+ originalInheritsFromId !== inheritsFromId) ||
+ // Inherit from added (did not have one initially);
+ (!originalInheritsFromId && inheritsFromId);
+
+ this._recursivelyUpdateAddRemoveObj(this._local, addRemoveObj);
+
+ if (inheritFromChanged) {
+ addRemoveObj.parent = inheritsFromId;
+ }
+ return addRemoveObj;
+ }
+
+ _handleCreateSection() {
+ let newRef = 'refs/for/*';
+ // Avoid using an already used key for the placeholder, since it
+ // immediately gets added to an object.
+ while (this._local[newRef]) {
+ newRef = `${newRef}*`;
+ }
+ const section = {permissions: {}, added: true};
+ this.push('_sections', {id: newRef, value: section});
+ this.set(['_local', newRef], section);
+ flush();
+ dom(this.root).querySelector('gr-access-section:last-of-type')
+ .editReference();
+ }
+
+ _getObjforSave() {
+ const addRemoveObj = this._computeAddAndRemove();
+ // If there are no changes, don't actually save.
+ if (!Object.keys(addRemoveObj.add).length &&
+ !Object.keys(addRemoveObj.remove).length &&
+ !addRemoveObj.parent) {
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {message: NOTHING_TO_SAVE},
+ bubbles: true,
+ composed: true,
+ }));
+ return;
+ }
+ const obj = {
+ add: addRemoveObj.add,
+ remove: addRemoveObj.remove,
+ };
+ if (addRemoveObj.parent) {
+ obj.parent = addRemoveObj.parent;
+ }
+ return obj;
+ }
+
+ _handleSave(e) {
+ const obj = this._getObjforSave();
+ if (!obj) { return; }
+ const button = e && e.target;
+ if (button) {
+ button.loading = true;
+ }
+ return this.$.restAPI.setRepoAccessRights(this.repo, obj)
+ .then(() => {
+ this._reload(this.repo);
+ })
+ .finally(() => {
+ this._modified = false;
+ if (button) {
+ button.loading = false;
+ }
+ });
+ }
+
+ _handleSaveForReview(e) {
+ const obj = this._getObjforSave();
+ if (!obj) { return; }
+ const button = e && e.target;
+ if (button) {
+ button.loading = true;
+ }
+ return this.$.restAPI
+ .setRepoAccessRightsForReview(this.repo, obj)
+ .then(change => {
+ Gerrit.Nav.navigateToChange(change);
+ })
+ .finally(() => {
+ this._modified = false;
+ if (button) {
+ button.loading = false;
+ }
+ });
+ }
+
+ _computeSaveReviewBtnClass(canUpload) {
+ return !canUpload ? 'invisible' : '';
+ }
+
+ _computeSaveBtnClass(ownerOf) {
+ return ownerOf && ownerOf.length === 0 ? 'invisible' : '';
+ }
+
+ _computeMainClass(ownerOf, canUpload, editing) {
+ const classList = [];
+ if (ownerOf && ownerOf.length > 0 || canUpload) {
+ classList.push('admin');
+ }
+ if (editing) {
+ classList.push('editing');
+ }
+ return classList.join(' ');
+ }
+
+ _computeParentHref(repoName) {
+ return this.getBaseUrl() +
+ `/admin/repos/${this.encodeURL(repoName, true)},access`;
+ }
+}
+
+customElements.define(GrRepoAccess.is, GrRepoAccess);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js
new file mode 100644
index 0000000..9a27371
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js
@@ -0,0 +1,91 @@
+/**
+ * @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.js';
+
+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-subpage-styles">
+ gr-button,
+ #inheritsFrom,
+ #editInheritFromInput,
+ .editing #inheritFromName,
+ .weblinks,
+ .editing .invisible{
+ display: none;
+ }
+ #inheritsFrom.show {
+ display: flex;
+ min-height: 2em;
+ align-items: center;
+ }
+ .weblink {
+ margin-right: var(--spacing-xs);
+ }
+ .weblinks.show,
+ .referenceContainer {
+ display: block;
+ }
+ .rightsText {
+ margin-right: var(--spacing-s);
+ }
+
+ .editing gr-button,
+ .admin #editBtn {
+ display: inline-block;
+ margin: var(--spacing-l) 0;
+ }
+ .editing #editInheritFromInput {
+ display: inline-block;
+ }
+ </style>
+ <style include="gr-menu-page-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <main class\$="[[_computeMainClass(_ownerOf, _canUpload, _editing)]]">
+ <div id="loading" class\$="[[_computeLoadingClass(_loading)]]">
+ Loading...
+ </div>
+ <div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]">
+ <h3 id="inheritsFrom" class\$="[[_computeShowInherit(_inheritsFrom)]]">
+ <span class="rightsText">Rights Inherit From</span>
+ <a href\$="[[_computeParentHref(_inheritsFrom.name)]]" rel="noopener" id="inheritFromName">
+ [[_inheritsFrom.name]]</a>
+ <gr-autocomplete id="editInheritFromInput" text="{{_inheritFromFilter}}" query="[[_query]]" on-commit="_handleUpdateInheritFrom"></gr-autocomplete>
+ </h3>
+ <div class\$="weblinks [[_computeWebLinkClass(_weblinks)]]">
+ History:
+ <template is="dom-repeat" items="[[_weblinks]]" as="link">
+ <a href="[[link.url]]" class="weblink" rel="noopener" target="[[link.target]]">
+ [[link.name]]
+ </a>
+ </template>
+ </div>
+ <gr-button id="editBtn" on-click="_handleEdit">[[_editOrCancel(_editing)]]</gr-button>
+ <gr-button id="saveBtn" primary="" class\$="[[_computeSaveBtnClass(_ownerOf)]]" on-click="_handleSave" disabled="[[!_modified]]">Save</gr-button>
+ <gr-button id="saveReviewBtn" primary="" class\$="[[_computeSaveReviewBtnClass(_canUpload)]]" on-click="_handleSaveForReview" disabled="[[!_modified]]">Save for review</gr-button>
+ <template is="dom-repeat" items="{{_sections}}" initial-count="5" target-framerate="60" as="section">
+ <gr-access-section capabilities="[[_capabilities]]" section="{{section}}" labels="[[_labels]]" can-upload="[[_canUpload]]" editing="[[_editing]]" owner-of="[[_ownerOf]]" groups="[[_groups]]" on-added-section-removed="_handleAddedSectionRemoved"></gr-access-section>
+ </template>
+ <div class="referenceContainer">
+ <gr-button id="addReferenceBtn" on-click="_handleCreateSection">Add Reference</gr-button>
+ </div>
+ </div>
+ </main>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
index d89b5df..89f4df7 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
@@ -19,16 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-access</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-repo-access.html">
-
-<script>void(0);</script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,395 +31,431 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo-access tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let repoStub;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-repo-access.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-repo-access tests', () => {
+ let element;
+ let sandbox;
+ let repoStub;
- const accessRes = {
- local: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 234: {action: 'ALLOW'},
- 123: {action: 'DENY'},
- },
+ const accessRes = {
+ local: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 234: {action: 'ALLOW'},
+ 123: {action: 'DENY'},
},
- read: {
- rules: {
- 234: {action: 'ALLOW'},
+ },
+ read: {
+ rules: {
+ 234: {action: 'ALLOW'},
+ },
+ },
+ },
+ },
+ },
+ groups: {
+ Administrators: {
+ name: 'Administrators',
+ },
+ Maintainers: {
+ name: 'Maintainers',
+ },
+ },
+ config_web_links: [{
+ name: 'gitiles',
+ target: '_blank',
+ url: 'https://my/site/+log/123/project.config',
+ }],
+ can_upload: true,
+ };
+ const accessRes2 = {
+ local: {
+ GLOBAL_CAPABILITIES: {
+ permissions: {
+ accessDatabase: {
+ rules: {
+ group1: {
+ action: 'ALLOW',
},
},
},
},
},
- groups: {
- Administrators: {
- name: 'Administrators',
- },
- Maintainers: {
- name: 'Maintainers',
+ },
+ };
+ const repoRes = {
+ labels: {
+ 'Code-Review': {
+ values: {
+ ' 0': 'No score',
+ '-1': 'I would prefer this is not merged as is',
+ '-2': 'This shall not be merged',
+ '+1': 'Looks good to me, but someone else must approve',
+ '+2': 'Looks good to me, approved',
},
},
- config_web_links: [{
- name: 'gitiles',
- target: '_blank',
- url: 'https://my/site/+log/123/project.config',
- }],
- can_upload: true,
- };
- const accessRes2 = {
- local: {
- GLOBAL_CAPABILITIES: {
- permissions: {
- accessDatabase: {
- rules: {
- group1: {
- action: 'ALLOW',
- },
- },
- },
- },
- },
- },
- };
- const repoRes = {
- labels: {
- 'Code-Review': {
- values: {
- ' 0': 'No score',
- '-1': 'I would prefer this is not merged as is',
- '-2': 'This shall not be merged',
- '+1': 'Looks good to me, but someone else must approve',
- '+2': 'Looks good to me, approved',
- },
- },
- },
- };
+ },
+ };
+ const capabilitiesRes = {
+ accessDatabase: {
+ id: 'accessDatabase',
+ name: 'Access Database',
+ },
+ createAccount: {
+ id: 'createAccount',
+ name: 'Create Account',
+ },
+ };
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve(null); },
+ });
+ repoStub = sandbox.stub(element.$.restAPI, 'getRepo').returns(
+ Promise.resolve(repoRes));
+ element._loading = false;
+ element._ownerOf = [];
+ element._canUpload = false;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_repoChanged called when repo name changes', () => {
+ sandbox.stub(element, '_repoChanged');
+ element.repo = 'New Repo';
+ assert.isTrue(element._repoChanged.called);
+ });
+
+ test('_repoChanged', done => {
+ const accessStub = sandbox.stub(element.$.restAPI,
+ 'getRepoAccessRights');
+
+ accessStub.withArgs('New Repo').returns(
+ Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
+ accessStub.withArgs('Another New Repo')
+ .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
+ const capabilitiesStub = sandbox.stub(element.$.restAPI,
+ 'getCapabilities');
+ capabilitiesStub.returns(Promise.resolve(capabilitiesRes));
+
+ element._repoChanged('New Repo').then(() => {
+ assert.isTrue(accessStub.called);
+ assert.isTrue(capabilitiesStub.called);
+ assert.isTrue(repoStub.called);
+ assert.isNotOk(element._inheritsFrom);
+ assert.deepEqual(element._local, accessRes.local);
+ assert.deepEqual(element._sections,
+ element.toSortedArray(accessRes.local));
+ assert.deepEqual(element._labels, repoRes.labels);
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('.weblinks')).display,
+ 'block');
+ return element._repoChanged('Another New Repo');
+ })
+ .then(() => {
+ assert.deepEqual(element._sections,
+ element.toSortedArray(accessRes2.local));
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('.weblinks')).display,
+ 'none');
+ done();
+ });
+ });
+
+ test('_repoChanged when repo changes to undefined returns', done => {
const capabilitiesRes = {
accessDatabase: {
id: 'accessDatabase',
name: 'Access Database',
},
- createAccount: {
- id: 'createAccount',
- name: 'Create Account',
- },
};
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve(null); },
- });
- repoStub = sandbox.stub(element.$.restAPI, 'getRepo').returns(
- Promise.resolve(repoRes));
- element._loading = false;
- element._ownerOf = [];
- element._canUpload = false;
+ const accessStub = sandbox.stub(element.$.restAPI, 'getRepoAccessRights')
+ .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
+ const capabilitiesStub = sandbox.stub(element.$.restAPI,
+ 'getCapabilities').returns(Promise.resolve(capabilitiesRes));
+
+ element._repoChanged().then(() => {
+ assert.isFalse(accessStub.called);
+ assert.isFalse(capabilitiesStub.called);
+ assert.isFalse(repoStub.called);
+ done();
+ });
+ });
+
+ test('_computeParentHref', () => {
+ const repoName = 'test-repo';
+ assert.equal(element._computeParentHref(repoName),
+ '/admin/repos/test-repo,access');
+ });
+
+ test('_computeMainClass', () => {
+ let ownerOf = ['refs/*'];
+ const editing = true;
+ const canUpload = false;
+ assert.equal(element._computeMainClass(ownerOf, canUpload), 'admin');
+ assert.equal(element._computeMainClass(ownerOf, canUpload, editing),
+ 'admin editing');
+ ownerOf = [];
+ assert.equal(element._computeMainClass(ownerOf, canUpload), '');
+ assert.equal(element._computeMainClass(ownerOf, canUpload, editing),
+ 'editing');
+ });
+
+ test('inherit section', () => {
+ element._local = {};
+ element._ownerOf = [];
+ sandbox.stub(element, '_computeParentHref');
+ // Nothing should appear when no inherit from and not in edit mode.
+ assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none');
+ // The autocomplete should be hidden, and the link should be displayed.
+ assert.isFalse(element._computeParentHref.called);
+ // When it edit mode, the autocomplete should appear.
+ element._editing = true;
+ // When editing, the autocomplete should still not be shown.
+ assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none');
+ element._editing = false;
+ element._inheritsFrom = {
+ name: 'another-repo',
+ };
+ // When there is a parent project, the link should be displayed.
+ flushAsynchronousOperations();
+ assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none');
+ assert.notEqual(getComputedStyle(element.$.inheritFromName).display,
+ 'none');
+ assert.equal(getComputedStyle(element.$.editInheritFromInput).display,
+ 'none');
+ assert.isTrue(element._computeParentHref.called);
+ element._editing = true;
+ // When editing, the autocomplete should be shown.
+ assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none');
+ assert.equal(getComputedStyle(element.$.inheritFromName).display, 'none');
+ assert.notEqual(getComputedStyle(element.$.editInheritFromInput).display,
+ 'none');
+ });
+
+ test('_handleUpdateInheritFrom', () => {
+ element._inheritFromFilter = 'foo bar baz';
+ element._handleUpdateInheritFrom({detail: {value: 'abc+123'}});
+ assert.isOk(element._inheritsFrom);
+ assert.equal(element._inheritsFrom.id, 'abc+123');
+ assert.equal(element._inheritsFrom.name, 'foo bar baz');
+ });
+
+ test('_computeLoadingClass', () => {
+ assert.equal(element._computeLoadingClass(true), 'loading');
+ assert.equal(element._computeLoadingClass(false), '');
+ });
+
+ test('fires page-error', done => {
+ const response = {status: 404};
+
+ sandbox.stub(
+ element.$.restAPI, 'getRepoAccessRights', (repoName, errFn) => {
+ errFn(response);
+ });
+
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
});
- teardown(() => {
- sandbox.restore();
- });
+ element.repo = 'test';
+ });
- test('_repoChanged called when repo name changes', () => {
- sandbox.stub(element, '_repoChanged');
- element.repo = 'New Repo';
- assert.isTrue(element._repoChanged.called);
- });
-
- test('_repoChanged', done => {
- const accessStub = sandbox.stub(element.$.restAPI,
- 'getRepoAccessRights');
-
- accessStub.withArgs('New Repo').returns(
- Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
- accessStub.withArgs('Another New Repo')
- .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
- const capabilitiesStub = sandbox.stub(element.$.restAPI,
- 'getCapabilities');
- capabilitiesStub.returns(Promise.resolve(capabilitiesRes));
-
- element._repoChanged('New Repo').then(() => {
- assert.isTrue(accessStub.called);
- assert.isTrue(capabilitiesStub.called);
- assert.isTrue(repoStub.called);
- assert.isNotOk(element._inheritsFrom);
- assert.deepEqual(element._local, accessRes.local);
- assert.deepEqual(element._sections,
- element.toSortedArray(accessRes.local));
- assert.deepEqual(element._labels, repoRes.labels);
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('.weblinks')).display,
- 'block');
- return element._repoChanged('Another New Repo');
- })
- .then(() => {
- assert.deepEqual(element._sections,
- element.toSortedArray(accessRes2.local));
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('.weblinks')).display,
- 'none');
- done();
- });
- });
-
- test('_repoChanged when repo changes to undefined returns', done => {
- const capabilitiesRes = {
- accessDatabase: {
- id: 'accessDatabase',
- name: 'Access Database',
- },
- };
- const accessStub = sandbox.stub(element.$.restAPI, 'getRepoAccessRights')
- .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
- const capabilitiesStub = sandbox.stub(element.$.restAPI,
- 'getCapabilities').returns(Promise.resolve(capabilitiesRes));
-
- element._repoChanged().then(() => {
- assert.isFalse(accessStub.called);
- assert.isFalse(capabilitiesStub.called);
- assert.isFalse(repoStub.called);
- done();
- });
- });
-
- test('_computeParentHref', () => {
- const repoName = 'test-repo';
- assert.equal(element._computeParentHref(repoName),
- '/admin/repos/test-repo,access');
- });
-
- test('_computeMainClass', () => {
- let ownerOf = ['refs/*'];
- const editing = true;
- const canUpload = false;
- assert.equal(element._computeMainClass(ownerOf, canUpload), 'admin');
- assert.equal(element._computeMainClass(ownerOf, canUpload, editing),
- 'admin editing');
- ownerOf = [];
- assert.equal(element._computeMainClass(ownerOf, canUpload), '');
- assert.equal(element._computeMainClass(ownerOf, canUpload, editing),
- 'editing');
- });
-
- test('inherit section', () => {
- element._local = {};
- element._ownerOf = [];
- sandbox.stub(element, '_computeParentHref');
- // Nothing should appear when no inherit from and not in edit mode.
- assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none');
- // The autocomplete should be hidden, and the link should be displayed.
- assert.isFalse(element._computeParentHref.called);
- // When it edit mode, the autocomplete should appear.
- element._editing = true;
- // When editing, the autocomplete should still not be shown.
- assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none');
- element._editing = false;
- element._inheritsFrom = {
- name: 'another-repo',
- };
- // When there is a parent project, the link should be displayed.
- flushAsynchronousOperations();
- assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none');
- assert.notEqual(getComputedStyle(element.$.inheritFromName).display,
- 'none');
+ suite('with defined sections', () => {
+ const testEditSaveCancelBtns = (shouldShowSave, shouldShowSaveReview) => {
+ // Edit button is visible and Save button is hidden.
+ assert.equal(getComputedStyle(element.$.saveReviewBtn).display, 'none');
+ assert.equal(getComputedStyle(element.$.saveBtn).display, 'none');
+ assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none');
+ assert.equal(element.$.editBtn.innerText, 'EDIT');
assert.equal(getComputedStyle(element.$.editInheritFromInput).display,
'none');
- assert.isTrue(element._computeParentHref.called);
- element._editing = true;
- // When editing, the autocomplete should be shown.
- assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none');
- assert.equal(getComputedStyle(element.$.inheritFromName).display, 'none');
- assert.notEqual(getComputedStyle(element.$.editInheritFromInput).display,
- 'none');
- });
+ element._inheritsFrom = {
+ id: 'test-project',
+ };
+ flushAsynchronousOperations();
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('#editInheritFromInput'))
+ .display, 'none');
- test('_handleUpdateInheritFrom', () => {
- element._inheritFromFilter = 'foo bar baz';
- element._handleUpdateInheritFrom({detail: {value: 'abc+123'}});
- assert.isOk(element._inheritsFrom);
- assert.equal(element._inheritsFrom.id, 'abc+123');
- assert.equal(element._inheritsFrom.name, 'foo bar baz');
- });
+ MockInteractions.tap(element.$.editBtn);
+ flushAsynchronousOperations();
- test('_computeLoadingClass', () => {
- assert.equal(element._computeLoadingClass(true), 'loading');
- assert.equal(element._computeLoadingClass(false), '');
- });
-
- test('fires page-error', done => {
- const response = {status: 404};
-
- sandbox.stub(
- element.$.restAPI, 'getRepoAccessRights', (repoName, errFn) => {
- errFn(response);
- });
-
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- element.repo = 'test';
- });
-
- suite('with defined sections', () => {
- const testEditSaveCancelBtns = (shouldShowSave, shouldShowSaveReview) => {
- // Edit button is visible and Save button is hidden.
- assert.equal(getComputedStyle(element.$.saveReviewBtn).display, 'none');
- assert.equal(getComputedStyle(element.$.saveBtn).display, 'none');
- assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none');
- assert.equal(element.$.editBtn.innerText, 'EDIT');
- assert.equal(getComputedStyle(element.$.editInheritFromInput).display,
+ // Edit button changes to Cancel button, and Save button is visible but
+ // disabled.
+ assert.equal(element.$.editBtn.innerText, 'CANCEL');
+ if (shouldShowSaveReview) {
+ assert.notEqual(getComputedStyle(element.$.saveReviewBtn).display,
'none');
- element._inheritsFrom = {
- id: 'test-project',
- };
- flushAsynchronousOperations();
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('#editInheritFromInput'))
- .display, 'none');
+ assert.isTrue(element.$.saveReviewBtn.disabled);
+ }
+ if (shouldShowSave) {
+ assert.notEqual(getComputedStyle(element.$.saveBtn).display, 'none');
+ assert.isTrue(element.$.saveBtn.disabled);
+ }
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('#editInheritFromInput'))
+ .display, 'none');
- MockInteractions.tap(element.$.editBtn);
- flushAsynchronousOperations();
+ // Save button should be enabled after access is modified
+ element.fire('access-modified');
+ if (shouldShowSaveReview) {
+ assert.isFalse(element.$.saveReviewBtn.disabled);
+ }
+ if (shouldShowSave) {
+ assert.isFalse(element.$.saveBtn.disabled);
+ }
+ };
- // Edit button changes to Cancel button, and Save button is visible but
- // disabled.
- assert.equal(element.$.editBtn.innerText, 'CANCEL');
- if (shouldShowSaveReview) {
- assert.notEqual(getComputedStyle(element.$.saveReviewBtn).display,
- 'none');
- assert.isTrue(element.$.saveReviewBtn.disabled);
- }
- if (shouldShowSave) {
- assert.notEqual(getComputedStyle(element.$.saveBtn).display, 'none');
- assert.isTrue(element.$.saveBtn.disabled);
- }
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('#editInheritFromInput'))
- .display, 'none');
+ setup(() => {
+ // Create deep copies of these objects so the originals are not modified
+ // by any tests.
+ element._local = JSON.parse(JSON.stringify(accessRes.local));
+ element._ownerOf = [];
+ element._sections = element.toSortedArray(element._local);
+ element._groups = JSON.parse(JSON.stringify(accessRes.groups));
+ element._capabilities = JSON.parse(JSON.stringify(capabilitiesRes));
+ element._labels = JSON.parse(JSON.stringify(repoRes.labels));
+ flushAsynchronousOperations();
+ });
- // Save button should be enabled after access is modified
- element.fire('access-modified');
- if (shouldShowSaveReview) {
- assert.isFalse(element.$.saveReviewBtn.disabled);
- }
- if (shouldShowSave) {
- assert.isFalse(element.$.saveBtn.disabled);
- }
+ test('removing an added section', () => {
+ element.editing = true;
+ assert.equal(element._sections.length, 1);
+ element.shadowRoot
+ .querySelector('gr-access-section').fire('added-section-removed');
+ flushAsynchronousOperations();
+ assert.equal(element._sections.length, 0);
+ });
+
+ test('button visibility for non ref owner', () => {
+ assert.equal(getComputedStyle(element.$.saveReviewBtn).display, 'none');
+ assert.equal(getComputedStyle(element.$.editBtn).display, 'none');
+ });
+
+ test('button visibility for non ref owner with upload privilege', () => {
+ element._canUpload = true;
+ testEditSaveCancelBtns(false, true);
+ });
+
+ test('button visibility for ref owner', () => {
+ element._ownerOf = ['refs/for/*'];
+ testEditSaveCancelBtns(true, false);
+ });
+
+ test('button visibility for ref owner and upload', () => {
+ element._ownerOf = ['refs/for/*'];
+ element._canUpload = true;
+ testEditSaveCancelBtns(true, false);
+ });
+
+ test('_handleAccessModified called with event fired', () => {
+ sandbox.spy(element, '_handleAccessModified');
+ element.fire('access-modified');
+ assert.isTrue(element._handleAccessModified.called);
+ });
+
+ test('_handleAccessModified called when parent changes', () => {
+ element._inheritsFrom = {
+ id: 'test-project',
+ };
+ flushAsynchronousOperations();
+ element.shadowRoot.querySelector('#editInheritFromInput').fire('commit');
+ sandbox.spy(element, '_handleAccessModified');
+ element.fire('access-modified');
+ assert.isTrue(element._handleAccessModified.called);
+ });
+
+ test('_handleSaveForReview', () => {
+ const saveStub =
+ sandbox.stub(element.$.restAPI, 'setRepoAccessRightsForReview');
+ sandbox.stub(element, '_computeAddAndRemove').returns({
+ add: {},
+ remove: {},
+ });
+ element._handleSaveForReview();
+ assert.isFalse(saveStub.called);
+ });
+
+ test('_recursivelyRemoveDeleted', () => {
+ const obj = {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 234: {action: 'ALLOW'},
+ 123: {action: 'DENY', deleted: true},
+ },
+ },
+ read: {
+ deleted: true,
+ rules: {
+ 234: {action: 'ALLOW'},
+ },
+ },
+ },
+ },
+ };
+ const expectedResult = {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 234: {action: 'ALLOW'},
+ },
+ },
+ },
+ },
+ };
+ element._recursivelyRemoveDeleted(obj);
+ assert.deepEqual(obj, expectedResult);
+ });
+
+ test('_recursivelyUpdateAddRemoveObj on new added section', () => {
+ const obj = {
+ 'refs/for/*': {
+ permissions: {
+ 'label-Code-Review': {
+ rules: {
+ e798fed07afbc9173a587f876ef8760c78d240c1: {
+ min: -2,
+ max: 2,
+ action: 'ALLOW',
+ added: true,
+ },
+ },
+ added: true,
+ label: 'Code-Review',
+ },
+ 'labelAs-Code-Review': {
+ rules: {
+ 'ldap:gerritcodereview-eng': {
+ min: -2,
+ max: 2,
+ action: 'ALLOW',
+ added: true,
+ deleted: true,
+ },
+ },
+ added: true,
+ label: 'Code-Review',
+ },
+ },
+ added: true,
+ },
};
- setup(() => {
- // Create deep copies of these objects so the originals are not modified
- // by any tests.
- element._local = JSON.parse(JSON.stringify(accessRes.local));
- element._ownerOf = [];
- element._sections = element.toSortedArray(element._local);
- element._groups = JSON.parse(JSON.stringify(accessRes.groups));
- element._capabilities = JSON.parse(JSON.stringify(capabilitiesRes));
- element._labels = JSON.parse(JSON.stringify(repoRes.labels));
- flushAsynchronousOperations();
- });
-
- test('removing an added section', () => {
- element.editing = true;
- assert.equal(element._sections.length, 1);
- element.shadowRoot
- .querySelector('gr-access-section').fire('added-section-removed');
- flushAsynchronousOperations();
- assert.equal(element._sections.length, 0);
- });
-
- test('button visibility for non ref owner', () => {
- assert.equal(getComputedStyle(element.$.saveReviewBtn).display, 'none');
- assert.equal(getComputedStyle(element.$.editBtn).display, 'none');
- });
-
- test('button visibility for non ref owner with upload privilege', () => {
- element._canUpload = true;
- testEditSaveCancelBtns(false, true);
- });
-
- test('button visibility for ref owner', () => {
- element._ownerOf = ['refs/for/*'];
- testEditSaveCancelBtns(true, false);
- });
-
- test('button visibility for ref owner and upload', () => {
- element._ownerOf = ['refs/for/*'];
- element._canUpload = true;
- testEditSaveCancelBtns(true, false);
- });
-
- test('_handleAccessModified called with event fired', () => {
- sandbox.spy(element, '_handleAccessModified');
- element.fire('access-modified');
- assert.isTrue(element._handleAccessModified.called);
- });
-
- test('_handleAccessModified called when parent changes', () => {
- element._inheritsFrom = {
- id: 'test-project',
- };
- flushAsynchronousOperations();
- element.shadowRoot.querySelector('#editInheritFromInput').fire('commit');
- sandbox.spy(element, '_handleAccessModified');
- element.fire('access-modified');
- assert.isTrue(element._handleAccessModified.called);
- });
-
- test('_handleSaveForReview', () => {
- const saveStub =
- sandbox.stub(element.$.restAPI, 'setRepoAccessRightsForReview');
- sandbox.stub(element, '_computeAddAndRemove').returns({
- add: {},
- remove: {},
- });
- element._handleSaveForReview();
- assert.isFalse(saveStub.called);
- });
-
- test('_recursivelyRemoveDeleted', () => {
- const obj = {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 234: {action: 'ALLOW'},
- 123: {action: 'DENY', deleted: true},
- },
- },
- read: {
- deleted: true,
- rules: {
- 234: {action: 'ALLOW'},
- },
- },
- },
- },
- };
- const expectedResult = {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 234: {action: 'ALLOW'},
- },
- },
- },
- },
- };
- element._recursivelyRemoveDeleted(obj);
- assert.deepEqual(obj, expectedResult);
- });
-
- test('_recursivelyUpdateAddRemoveObj on new added section', () => {
- const obj = {
+ const expectedResult = {
+ add: {
'refs/for/*': {
permissions: {
'label-Code-Review': {
@@ -440,798 +471,764 @@
label: 'Code-Review',
},
'labelAs-Code-Review': {
- rules: {
- 'ldap:gerritcodereview-eng': {
- min: -2,
- max: 2,
- action: 'ALLOW',
- added: true,
- deleted: true,
- },
- },
+ rules: {},
added: true,
label: 'Code-Review',
},
},
added: true,
},
- };
+ },
+ remove: {},
+ };
+ const updateObj = {add: {}, remove: {}};
+ element._recursivelyUpdateAddRemoveObj(obj, updateObj);
+ assert.deepEqual(updateObj, expectedResult);
+ });
- const expectedResult = {
- add: {
- 'refs/for/*': {
- permissions: {
- 'label-Code-Review': {
- rules: {
- e798fed07afbc9173a587f876ef8760c78d240c1: {
- min: -2,
- max: 2,
- action: 'ALLOW',
- added: true,
- },
- },
- added: true,
- label: 'Code-Review',
- },
- 'labelAs-Code-Review': {
- rules: {},
- added: true,
- label: 'Code-Review',
- },
- },
- added: true,
- },
- },
- remove: {},
- };
- const updateObj = {add: {}, remove: {}};
- element._recursivelyUpdateAddRemoveObj(obj, updateObj);
- assert.deepEqual(updateObj, expectedResult);
+ test('_handleSaveForReview with no changes', () => {
+ assert.deepEqual(element._computeAddAndRemove(), {add: {}, remove: {}});
+ });
+
+ test('_handleSaveForReview parent change', () => {
+ element._inheritsFrom = {
+ id: 'test-project',
+ };
+ element._originalInheritsFrom = {
+ id: 'test-project-original',
+ };
+ assert.deepEqual(element._computeAddAndRemove(), {
+ parent: 'test-project', add: {}, remove: {},
});
+ });
- test('_handleSaveForReview with no changes', () => {
- assert.deepEqual(element._computeAddAndRemove(), {add: {}, remove: {}});
+ test('_handleSaveForReview new parent with spaces', () => {
+ element._inheritsFrom = {id: 'spaces+in+project+name'};
+ element._originalInheritsFrom = {id: 'old-project'};
+ assert.deepEqual(element._computeAddAndRemove(), {
+ parent: 'spaces in project name', add: {}, remove: {},
});
+ });
- test('_handleSaveForReview parent change', () => {
- element._inheritsFrom = {
- id: 'test-project',
- };
- element._originalInheritsFrom = {
- id: 'test-project-original',
- };
- assert.deepEqual(element._computeAddAndRemove(), {
- parent: 'test-project', add: {}, remove: {},
- });
+ test('_handleSaveForReview rules', () => {
+ // Delete a rule.
+ element._local['refs/*'].permissions.owner.rules[123].deleted = true;
+ let expectedInput = {
+ add: {},
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 123: {},
+ },
+ },
+ },
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Undo deleting a rule.
+ delete element._local['refs/*'].permissions.owner.rules[123].deleted;
+
+ // Modify a rule.
+ element._local['refs/*'].permissions.owner.rules[123].modified = true;
+ expectedInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 123: {action: 'DENY', modified: true},
+ },
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 123: {},
+ },
+ },
+ },
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+ });
+
+ test('_computeAddAndRemove permissions', () => {
+ // Add a new rule to a permission.
+ let expectedInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ Maintainers: {
+ action: 'ALLOW',
+ added: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ remove: {},
+ };
+
+ element.shadowRoot
+ .querySelector('gr-access-section').shadowRoot
+ .querySelector('gr-permission')
+ ._handleAddRuleItem(
+ {detail: {value: {id: 'Maintainers'}}});
+
+ flushAsynchronousOperations();
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Remove the added rule.
+ delete element._local['refs/*'].permissions.owner.rules.Maintainers;
+
+ // Delete a permission.
+ element._local['refs/*'].permissions.owner.deleted = true;
+ expectedInput = {
+ add: {},
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {}},
+ },
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Undo delete permission.
+ delete element._local['refs/*'].permissions.owner.deleted;
+
+ // Modify a permission.
+ element._local['refs/*'].permissions.owner.modified = true;
+ expectedInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ modified: true,
+ rules: {
+ 234: {action: 'ALLOW'},
+ 123: {action: 'DENY'},
+ },
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {}},
+ },
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+ });
+
+ test('_computeAddAndRemove sections', () => {
+ // Add a new permission to a section
+ let expectedInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {},
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {},
+ };
+ element.shadowRoot
+ .querySelector('gr-access-section')._handleAddPermission();
+ flushAsynchronousOperations();
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Add a new rule to the new permission.
+ expectedInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {
+ Maintainers: {
+ min: -2,
+ max: 2,
+ action: 'ALLOW',
+ added: true,
+ },
+ },
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {},
+ };
+ const newPermission =
+ dom(element.shadowRoot
+ .querySelector('gr-access-section').root).querySelectorAll(
+ 'gr-permission')[2];
+ newPermission._handleAddRuleItem(
+ {detail: {value: {id: 'Maintainers'}}});
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Modify a section reference.
+ element._local['refs/*'].updatedId = 'refs/for/bar';
+ element._local['refs/*'].modified = true;
+ expectedInput = {
+ add: {
+ 'refs/for/bar': {
+ modified: true,
+ updatedId: 'refs/for/bar',
+ permissions: {
+ 'owner': {
+ rules: {
+ 234: {action: 'ALLOW'},
+ 123: {action: 'DENY'},
+ },
+ },
+ 'read': {
+ rules: {
+ 234: {action: 'ALLOW'},
+ },
+ },
+ 'label-Code-Review': {
+ added: true,
+ rules: {
+ Maintainers: {
+ min: -2,
+ max: 2,
+ action: 'ALLOW',
+ added: true,
+ },
+ },
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {},
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Delete a section.
+ element._local['refs/*'].deleted = true;
+ expectedInput = {
+ add: {},
+ remove: {
+ 'refs/*': {
+ permissions: {},
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+ });
+
+ test('_computeAddAndRemove new section', () => {
+ // Add a new permission to a section
+ let expectedInput = {
+ add: {
+ 'refs/for/*': {
+ added: true,
+ permissions: {},
+ },
+ },
+ remove: {},
+ };
+ MockInteractions.tap(element.$.addReferenceBtn);
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ expectedInput = {
+ add: {
+ 'refs/for/*': {
+ added: true,
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {},
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {},
+ };
+ const newSection = dom(element.root)
+ .querySelectorAll('gr-access-section')[1];
+ newSection._handleAddPermission();
+ flushAsynchronousOperations();
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Add rule to the new permission.
+ expectedInput = {
+ add: {
+ 'refs/for/*': {
+ added: true,
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {
+ Maintainers: {
+ action: 'ALLOW',
+ added: true,
+ max: 2,
+ min: -2,
+ },
+ },
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {},
+ };
+
+ newSection.shadowRoot
+ .querySelector('gr-permission')._handleAddRuleItem(
+ {detail: {value: {id: 'Maintainers'}}});
+
+ flushAsynchronousOperations();
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Modify a the reference from the default value.
+ element._local['refs/for/*'].updatedId = 'refs/for/new';
+ expectedInput = {
+ add: {
+ 'refs/for/new': {
+ added: true,
+ updatedId: 'refs/for/new',
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {
+ Maintainers: {
+ action: 'ALLOW',
+ added: true,
+ max: 2,
+ min: -2,
+ },
+ },
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {},
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+ });
+
+ test('_computeAddAndRemove combinations', () => {
+ // Modify rule and delete permission that it is inside of.
+ element._local['refs/*'].permissions.owner.rules[123].modified = true;
+ element._local['refs/*'].permissions.owner.deleted = true;
+ let expectedInput = {
+ add: {},
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {}},
+ },
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+ // Delete rule and delete permission that it is inside of.
+ element._local['refs/*'].permissions.owner.rules[123].modified = false;
+ element._local['refs/*'].permissions.owner.rules[123].deleted = true;
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Also modify a different rule inside of another permission.
+ element._local['refs/*'].permissions.read.modified = true;
+ expectedInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ read: {
+ modified: true,
+ rules: {
+ 234: {action: 'ALLOW'},
+ },
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {}},
+ read: {rules: {}},
+ },
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+ // Modify both permissions with an exclusive bit. Owner is still
+ // deleted.
+ element._local['refs/*'].permissions.owner.exclusive = true;
+ element._local['refs/*'].permissions.owner.modified = true;
+ element._local['refs/*'].permissions.read.exclusive = true;
+ element._local['refs/*'].permissions.read.modified = true;
+ expectedInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ read: {
+ exclusive: true,
+ modified: true,
+ rules: {
+ 234: {action: 'ALLOW'},
+ },
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {}},
+ read: {rules: {}},
+ },
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Add a rule to the existing permission;
+ const readPermission =
+ dom(element.shadowRoot
+ .querySelector('gr-access-section').root).querySelectorAll(
+ 'gr-permission')[1];
+ readPermission._handleAddRuleItem(
+ {detail: {value: {id: 'Maintainers'}}});
+
+ expectedInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ read: {
+ exclusive: true,
+ modified: true,
+ rules: {
+ 234: {action: 'ALLOW'},
+ Maintainers: {action: 'ALLOW', added: true},
+ },
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {}},
+ read: {rules: {}},
+ },
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Change one of the refs
+ element._local['refs/*'].updatedId = 'refs/for/bar';
+ element._local['refs/*'].modified = true;
+
+ expectedInput = {
+ add: {
+ 'refs/for/bar': {
+ modified: true,
+ updatedId: 'refs/for/bar',
+ permissions: {
+ read: {
+ exclusive: true,
+ modified: true,
+ rules: {
+ 234: {action: 'ALLOW'},
+ Maintainers: {action: 'ALLOW', added: true},
+ },
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {},
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ expectedInput = {
+ add: {},
+ remove: {
+ 'refs/*': {
+ permissions: {},
+ },
+ },
+ };
+ element._local['refs/*'].deleted = true;
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Add a new section.
+ MockInteractions.tap(element.$.addReferenceBtn);
+ let newSection = dom(element.root)
+ .querySelectorAll('gr-access-section')[1];
+ newSection._handleAddPermission();
+ flushAsynchronousOperations();
+ newSection.shadowRoot
+ .querySelector('gr-permission')._handleAddRuleItem(
+ {detail: {value: {id: 'Maintainers'}}});
+ // Modify a the reference from the default value.
+ element._local['refs/for/*'].updatedId = 'refs/for/new';
+
+ expectedInput = {
+ add: {
+ 'refs/for/new': {
+ added: true,
+ updatedId: 'refs/for/new',
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {
+ Maintainers: {
+ action: 'ALLOW',
+ added: true,
+ max: 2,
+ min: -2,
+ },
+ },
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {},
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Modify newly added rule inside new ref.
+ element._local['refs/for/*'].permissions['label-Code-Review'].
+ rules['Maintainers'].modified = true;
+ expectedInput = {
+ add: {
+ 'refs/for/new': {
+ added: true,
+ updatedId: 'refs/for/new',
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {
+ Maintainers: {
+ action: 'ALLOW',
+ added: true,
+ modified: true,
+ max: 2,
+ min: -2,
+ },
+ },
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {},
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Add a second new section.
+ MockInteractions.tap(element.$.addReferenceBtn);
+ newSection = dom(element.root)
+ .querySelectorAll('gr-access-section')[2];
+ newSection._handleAddPermission();
+ flushAsynchronousOperations();
+ newSection.shadowRoot
+ .querySelector('gr-permission')._handleAddRuleItem(
+ {detail: {value: {id: 'Maintainers'}}});
+ // Modify a the reference from the default value.
+ element._local['refs/for/**'].updatedId = 'refs/for/new2';
+ expectedInput = {
+ add: {
+ 'refs/for/new': {
+ added: true,
+ updatedId: 'refs/for/new',
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {
+ Maintainers: {
+ action: 'ALLOW',
+ added: true,
+ modified: true,
+ max: 2,
+ min: -2,
+ },
+ },
+ label: 'Code-Review',
+ },
+ },
+ },
+ 'refs/for/new2': {
+ added: true,
+ updatedId: 'refs/for/new2',
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {
+ Maintainers: {
+ action: 'ALLOW',
+ added: true,
+ max: 2,
+ min: -2,
+ },
+ },
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {},
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+ });
+
+ test('Unsaved added refs are discarded when edit cancelled', () => {
+ // Unsaved changes are discarded when editing is cancelled.
+ MockInteractions.tap(element.$.editBtn);
+ assert.equal(element._sections.length, 1);
+ assert.equal(Object.keys(element._local).length, 1);
+ MockInteractions.tap(element.$.addReferenceBtn);
+ assert.equal(element._sections.length, 2);
+ assert.equal(Object.keys(element._local).length, 2);
+ MockInteractions.tap(element.$.editBtn);
+ assert.equal(element._sections.length, 1);
+ assert.equal(Object.keys(element._local).length, 1);
+ });
+
+ test('_handleSave', done => {
+ const repoAccessInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 123: {action: 'DENY', modified: true},
+ },
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 123: {},
+ },
+ },
+ },
+ },
+ },
+ };
+ sandbox.stub(element.$.restAPI, 'getRepoAccessRights').returns(
+ Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
+ sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ let resolver;
+ const saveStub = sandbox.stub(element.$.restAPI,
+ 'setRepoAccessRights')
+ .returns(new Promise(r => resolver = r));
+
+ element.repo = 'test-repo';
+ sandbox.stub(element, '_computeAddAndRemove').returns(repoAccessInput);
+
+ element._modified = true;
+ MockInteractions.tap(element.$.saveBtn);
+ assert.equal(element.$.saveBtn.hasAttribute('loading'), true);
+ resolver({_number: 1});
+ flush(() => {
+ assert.isTrue(saveStub.called);
+ assert.isTrue(Gerrit.Nav.navigateToChange.notCalled);
+ done();
});
+ });
- test('_handleSaveForReview new parent with spaces', () => {
- element._inheritsFrom = {id: 'spaces+in+project+name'};
- element._originalInheritsFrom = {id: 'old-project'};
- assert.deepEqual(element._computeAddAndRemove(), {
- parent: 'spaces in project name', add: {}, remove: {},
- });
- });
-
- test('_handleSaveForReview rules', () => {
- // Delete a rule.
- element._local['refs/*'].permissions.owner.rules[123].deleted = true;
- let expectedInput = {
- add: {},
- remove: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 123: {},
- },
+ test('_handleSaveForReview', done => {
+ const repoAccessInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 123: {action: 'DENY', modified: true},
},
},
},
},
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Undo deleting a rule.
- delete element._local['refs/*'].permissions.owner.rules[123].deleted;
-
- // Modify a rule.
- element._local['refs/*'].permissions.owner.rules[123].modified = true;
- expectedInput = {
- add: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 123: {action: 'DENY', modified: true},
- },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 123: {},
},
},
},
},
- remove: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 123: {},
- },
- },
- },
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
- });
+ },
+ };
+ sandbox.stub(element.$.restAPI, 'getRepoAccessRights').returns(
+ Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
+ sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ let resolver;
+ const saveForReviewStub = sandbox.stub(element.$.restAPI,
+ 'setRepoAccessRightsForReview')
+ .returns(new Promise(r => resolver = r));
- test('_computeAddAndRemove permissions', () => {
- // Add a new rule to a permission.
- let expectedInput = {
- add: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- Maintainers: {
- action: 'ALLOW',
- added: true,
- },
- },
- },
- },
- },
- },
- remove: {},
- };
+ element.repo = 'test-repo';
+ sandbox.stub(element, '_computeAddAndRemove').returns(repoAccessInput);
- element.shadowRoot
- .querySelector('gr-access-section').shadowRoot
- .querySelector('gr-permission')
- ._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
-
- flushAsynchronousOperations();
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Remove the added rule.
- delete element._local['refs/*'].permissions.owner.rules.Maintainers;
-
- // Delete a permission.
- element._local['refs/*'].permissions.owner.deleted = true;
- expectedInput = {
- add: {},
- remove: {
- 'refs/*': {
- permissions: {
- owner: {rules: {}},
- },
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Undo delete permission.
- delete element._local['refs/*'].permissions.owner.deleted;
-
- // Modify a permission.
- element._local['refs/*'].permissions.owner.modified = true;
- expectedInput = {
- add: {
- 'refs/*': {
- permissions: {
- owner: {
- modified: true,
- rules: {
- 234: {action: 'ALLOW'},
- 123: {action: 'DENY'},
- },
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {
- owner: {rules: {}},
- },
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
- });
-
- test('_computeAddAndRemove sections', () => {
- // Add a new permission to a section
- let expectedInput = {
- add: {
- 'refs/*': {
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {},
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {},
- };
- element.shadowRoot
- .querySelector('gr-access-section')._handleAddPermission();
- flushAsynchronousOperations();
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Add a new rule to the new permission.
- expectedInput = {
- add: {
- 'refs/*': {
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {
- Maintainers: {
- min: -2,
- max: 2,
- action: 'ALLOW',
- added: true,
- },
- },
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {},
- };
- const newPermission =
- Polymer.dom(element.shadowRoot
- .querySelector('gr-access-section').root).querySelectorAll(
- 'gr-permission')[2];
- newPermission._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Modify a section reference.
- element._local['refs/*'].updatedId = 'refs/for/bar';
- element._local['refs/*'].modified = true;
- expectedInput = {
- add: {
- 'refs/for/bar': {
- modified: true,
- updatedId: 'refs/for/bar',
- permissions: {
- 'owner': {
- rules: {
- 234: {action: 'ALLOW'},
- 123: {action: 'DENY'},
- },
- },
- 'read': {
- rules: {
- 234: {action: 'ALLOW'},
- },
- },
- 'label-Code-Review': {
- added: true,
- rules: {
- Maintainers: {
- min: -2,
- max: 2,
- action: 'ALLOW',
- added: true,
- },
- },
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {},
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Delete a section.
- element._local['refs/*'].deleted = true;
- expectedInput = {
- add: {},
- remove: {
- 'refs/*': {
- permissions: {},
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
- });
-
- test('_computeAddAndRemove new section', () => {
- // Add a new permission to a section
- let expectedInput = {
- add: {
- 'refs/for/*': {
- added: true,
- permissions: {},
- },
- },
- remove: {},
- };
- MockInteractions.tap(element.$.addReferenceBtn);
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- expectedInput = {
- add: {
- 'refs/for/*': {
- added: true,
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {},
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {},
- };
- const newSection = Polymer.dom(element.root)
- .querySelectorAll('gr-access-section')[1];
- newSection._handleAddPermission();
- flushAsynchronousOperations();
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Add rule to the new permission.
- expectedInput = {
- add: {
- 'refs/for/*': {
- added: true,
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {
- Maintainers: {
- action: 'ALLOW',
- added: true,
- max: 2,
- min: -2,
- },
- },
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {},
- };
-
- newSection.shadowRoot
- .querySelector('gr-permission')._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
-
- flushAsynchronousOperations();
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Modify a the reference from the default value.
- element._local['refs/for/*'].updatedId = 'refs/for/new';
- expectedInput = {
- add: {
- 'refs/for/new': {
- added: true,
- updatedId: 'refs/for/new',
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {
- Maintainers: {
- action: 'ALLOW',
- added: true,
- max: 2,
- min: -2,
- },
- },
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {},
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
- });
-
- test('_computeAddAndRemove combinations', () => {
- // Modify rule and delete permission that it is inside of.
- element._local['refs/*'].permissions.owner.rules[123].modified = true;
- element._local['refs/*'].permissions.owner.deleted = true;
- let expectedInput = {
- add: {},
- remove: {
- 'refs/*': {
- permissions: {
- owner: {rules: {}},
- },
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
- // Delete rule and delete permission that it is inside of.
- element._local['refs/*'].permissions.owner.rules[123].modified = false;
- element._local['refs/*'].permissions.owner.rules[123].deleted = true;
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Also modify a different rule inside of another permission.
- element._local['refs/*'].permissions.read.modified = true;
- expectedInput = {
- add: {
- 'refs/*': {
- permissions: {
- read: {
- modified: true,
- rules: {
- 234: {action: 'ALLOW'},
- },
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {
- owner: {rules: {}},
- read: {rules: {}},
- },
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
- // Modify both permissions with an exclusive bit. Owner is still
- // deleted.
- element._local['refs/*'].permissions.owner.exclusive = true;
- element._local['refs/*'].permissions.owner.modified = true;
- element._local['refs/*'].permissions.read.exclusive = true;
- element._local['refs/*'].permissions.read.modified = true;
- expectedInput = {
- add: {
- 'refs/*': {
- permissions: {
- read: {
- exclusive: true,
- modified: true,
- rules: {
- 234: {action: 'ALLOW'},
- },
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {
- owner: {rules: {}},
- read: {rules: {}},
- },
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Add a rule to the existing permission;
- const readPermission =
- Polymer.dom(element.shadowRoot
- .querySelector('gr-access-section').root).querySelectorAll(
- 'gr-permission')[1];
- readPermission._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
-
- expectedInput = {
- add: {
- 'refs/*': {
- permissions: {
- read: {
- exclusive: true,
- modified: true,
- rules: {
- 234: {action: 'ALLOW'},
- Maintainers: {action: 'ALLOW', added: true},
- },
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {
- owner: {rules: {}},
- read: {rules: {}},
- },
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Change one of the refs
- element._local['refs/*'].updatedId = 'refs/for/bar';
- element._local['refs/*'].modified = true;
-
- expectedInput = {
- add: {
- 'refs/for/bar': {
- modified: true,
- updatedId: 'refs/for/bar',
- permissions: {
- read: {
- exclusive: true,
- modified: true,
- rules: {
- 234: {action: 'ALLOW'},
- Maintainers: {action: 'ALLOW', added: true},
- },
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {},
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- expectedInput = {
- add: {},
- remove: {
- 'refs/*': {
- permissions: {},
- },
- },
- };
- element._local['refs/*'].deleted = true;
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Add a new section.
- MockInteractions.tap(element.$.addReferenceBtn);
- let newSection = Polymer.dom(element.root)
- .querySelectorAll('gr-access-section')[1];
- newSection._handleAddPermission();
- flushAsynchronousOperations();
- newSection.shadowRoot
- .querySelector('gr-permission')._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
- // Modify a the reference from the default value.
- element._local['refs/for/*'].updatedId = 'refs/for/new';
-
- expectedInput = {
- add: {
- 'refs/for/new': {
- added: true,
- updatedId: 'refs/for/new',
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {
- Maintainers: {
- action: 'ALLOW',
- added: true,
- max: 2,
- min: -2,
- },
- },
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {},
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Modify newly added rule inside new ref.
- element._local['refs/for/*'].permissions['label-Code-Review'].
- rules['Maintainers'].modified = true;
- expectedInput = {
- add: {
- 'refs/for/new': {
- added: true,
- updatedId: 'refs/for/new',
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {
- Maintainers: {
- action: 'ALLOW',
- added: true,
- modified: true,
- max: 2,
- min: -2,
- },
- },
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {},
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Add a second new section.
- MockInteractions.tap(element.$.addReferenceBtn);
- newSection = Polymer.dom(element.root)
- .querySelectorAll('gr-access-section')[2];
- newSection._handleAddPermission();
- flushAsynchronousOperations();
- newSection.shadowRoot
- .querySelector('gr-permission')._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
- // Modify a the reference from the default value.
- element._local['refs/for/**'].updatedId = 'refs/for/new2';
- expectedInput = {
- add: {
- 'refs/for/new': {
- added: true,
- updatedId: 'refs/for/new',
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {
- Maintainers: {
- action: 'ALLOW',
- added: true,
- modified: true,
- max: 2,
- min: -2,
- },
- },
- label: 'Code-Review',
- },
- },
- },
- 'refs/for/new2': {
- added: true,
- updatedId: 'refs/for/new2',
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {
- Maintainers: {
- action: 'ALLOW',
- added: true,
- max: 2,
- min: -2,
- },
- },
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {},
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
- });
-
- test('Unsaved added refs are discarded when edit cancelled', () => {
- // Unsaved changes are discarded when editing is cancelled.
- MockInteractions.tap(element.$.editBtn);
- assert.equal(element._sections.length, 1);
- assert.equal(Object.keys(element._local).length, 1);
- MockInteractions.tap(element.$.addReferenceBtn);
- assert.equal(element._sections.length, 2);
- assert.equal(Object.keys(element._local).length, 2);
- MockInteractions.tap(element.$.editBtn);
- assert.equal(element._sections.length, 1);
- assert.equal(Object.keys(element._local).length, 1);
- });
-
- test('_handleSave', done => {
- const repoAccessInput = {
- add: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 123: {action: 'DENY', modified: true},
- },
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 123: {},
- },
- },
- },
- },
- },
- };
- sandbox.stub(element.$.restAPI, 'getRepoAccessRights').returns(
- Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
- sandbox.stub(Gerrit.Nav, 'navigateToChange');
- let resolver;
- const saveStub = sandbox.stub(element.$.restAPI,
- 'setRepoAccessRights')
- .returns(new Promise(r => resolver = r));
-
- element.repo = 'test-repo';
- sandbox.stub(element, '_computeAddAndRemove').returns(repoAccessInput);
-
- element._modified = true;
- MockInteractions.tap(element.$.saveBtn);
- assert.equal(element.$.saveBtn.hasAttribute('loading'), true);
- resolver({_number: 1});
- flush(() => {
- assert.isTrue(saveStub.called);
- assert.isTrue(Gerrit.Nav.navigateToChange.notCalled);
- done();
- });
- });
-
- test('_handleSaveForReview', done => {
- const repoAccessInput = {
- add: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 123: {action: 'DENY', modified: true},
- },
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 123: {},
- },
- },
- },
- },
- },
- };
- sandbox.stub(element.$.restAPI, 'getRepoAccessRights').returns(
- Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
- sandbox.stub(Gerrit.Nav, 'navigateToChange');
- let resolver;
- const saveForReviewStub = sandbox.stub(element.$.restAPI,
- 'setRepoAccessRightsForReview')
- .returns(new Promise(r => resolver = r));
-
- element.repo = 'test-repo';
- sandbox.stub(element, '_computeAddAndRemove').returns(repoAccessInput);
-
- element._modified = true;
- MockInteractions.tap(element.$.saveReviewBtn);
- assert.equal(element.$.saveReviewBtn.hasAttribute('loading'), true);
- resolver({_number: 1});
- flush(() => {
- assert.isTrue(saveForReviewStub.called);
- assert.isTrue(Gerrit.Nav.navigateToChange
- .lastCall.calledWithExactly({_number: 1}));
- done();
- });
+ element._modified = true;
+ MockInteractions.tap(element.$.saveReviewBtn);
+ assert.equal(element.$.saveReviewBtn.hasAttribute('loading'), true);
+ resolver({_number: 1});
+ flush(() => {
+ assert.isTrue(saveForReviewStub.called);
+ assert.isTrue(Gerrit.Nav.navigateToChange
+ .lastCall.calledWithExactly({_number: 1}));
+ done();
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.html b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.html
deleted file mode 100644
index 29bc02d..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-
-<dom-module id="gr-repo-command">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- margin-bottom: var(--spacing-xxl);
- }
- </style>
- <h3>[[title]]</h3>
- <gr-button
- title$="[[tooltip]]"
- disabled$="[[disabled]]"
- on-click
- ="_onCommandTap">
- [[title]]
- </gr-button>
- </template>
- <script src="gr-repo-command.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
index 622bfe4..53b4989 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
@@ -14,34 +14,41 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrRepoCommand extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-repo-command'; }
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-repo-command_html.js';
- static get properties() {
- return {
- title: String,
- disabled: Boolean,
- tooltip: String,
- };
- }
+/** @extends Polymer.Element */
+class GrRepoCommand extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /**
- * Fired when command button is tapped.
- *
- * @event command-tap
- */
+ static get is() { return 'gr-repo-command'; }
- _onCommandTap() {
- this.dispatchEvent(
- new CustomEvent('command-tap', {bubbles: true, composed: true}));
- }
+ static get properties() {
+ return {
+ title: String,
+ disabled: Boolean,
+ tooltip: String,
+ };
}
- customElements.define(GrRepoCommand.is, GrRepoCommand);
-})();
+ /**
+ * Fired when command button is tapped.
+ *
+ * @event command-tap
+ */
+
+ _onCommandTap() {
+ this.dispatchEvent(
+ new CustomEvent('command-tap', {bubbles: true, composed: true}));
+ }
+}
+
+customElements.define(GrRepoCommand.is, GrRepoCommand);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
new file mode 100644
index 0000000..10d22fc
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
@@ -0,0 +1,30 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ margin-bottom: var(--spacing-xxl);
+ }
+ </style>
+ <h3>[[title]]</h3>
+ <gr-button title\$="[[tooltip]]" disabled\$="[[disabled]]" on-click="_onCommandTap">
+ [[title]]
+ </gr-button>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
index f4988a5..c260613 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-command</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-repo-command.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,21 +30,23 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo-command tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-repo-command.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-repo-command tests', () => {
+ let element;
- setup(() => {
- element = fixture('basic');
- });
-
- test('dispatched command-tap on button tap', done => {
- element.addEventListener('command-tap', () => {
- done();
- });
- MockInteractions.tap(
- Polymer.dom(element.root).querySelector('gr-button'));
- });
+ setup(() => {
+ element = fixture('basic');
});
+
+ test('dispatched command-tap on button tap', done => {
+ element.addEventListener('command-tap', () => {
+ done();
+ });
+ MockInteractions.tap(
+ dom(element.root).querySelector('gr-button'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html
deleted file mode 100644
index b610460..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html
+++ /dev/null
@@ -1,94 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/gr-subpage-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-create-change-dialog/gr-create-change-dialog.html">
-<link rel="import" href="../gr-repo-command/gr-repo-command.html">
-
-<dom-module id="gr-repo-commands">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-subpage-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>
- <main class="gr-form-styles read-only">
- <h1 id="Title">Repository Commands</h1>
- <div id="loading" class$="[[_computeLoadingClass(_loading)]]">Loading...</div>
- <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <h2 id="options">Command</h2>
- <div id="form">
- <gr-repo-command
- title="Create change"
- on-command-tap="_createNewChange">
- </gr-repo-command>
- <gr-repo-command
- id="editRepoConfig"
- title="Edit repo config"
- on-command-tap="_handleEditRepoConfig">
- </gr-repo-command>
- <gr-repo-command
- title="[[_repoConfig.actions.gc.label]]"
- tooltip="[[_repoConfig.actions.gc.title]]"
- hidden$="[[!_repoConfig.actions.gc.enabled]]"
- on-command-tap="_handleRunningGC">
- </gr-repo-command>
- <gr-endpoint-decorator name="repo-command">
- <gr-endpoint-param name="config" value="[[_repoConfig]]">
- </gr-endpoint-param>
- <gr-endpoint-param name="repoName" value="[[repo]]">
- </gr-endpoint-param>
- </gr-endpoint-decorator>
- </div>
- </div>
- </main>
- <gr-overlay id="createChangeOverlay" with-backdrop>
- <gr-dialog
- id="createChangeDialog"
- confirm-label="Create"
- disabled="[[!_canCreate]]"
- on-confirm="_handleCreateChange"
- on-cancel="_handleCloseCreateChange">
- <div class="header" slot="header">
- Create Change
- </div>
- <div class="main" slot="main">
- <gr-create-change-dialog
- id="createNewChangeModal"
- can-create="{{_canCreate}}"
- repo-name="[[repo]]"></gr-create-change-dialog>
- </div>
- </gr-dialog>
- </gr-overlay>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-repo-commands.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
index 80b187a..de9d8e2 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
@@ -14,113 +14,131 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const GC_MESSAGE = 'Garbage collection completed successfully.';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/gr-subpage-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-create-change-dialog/gr-create-change-dialog.js';
+import '../gr-repo-command/gr-repo-command.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-repo-commands_html.js';
- const CONFIG_BRANCH = 'refs/meta/config';
- const CONFIG_PATH = 'project.config';
- const EDIT_CONFIG_SUBJECT = 'Edit Repo Config';
- const INITIAL_PATCHSET = 1;
- const CREATE_CHANGE_FAILED_MESSAGE = 'Failed to create change.';
- const CREATE_CHANGE_SUCCEEDED_MESSAGE = 'Navigating to change';
+const GC_MESSAGE = 'Garbage collection completed successfully.';
- /**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
- */
- class GrRepoCommands extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-repo-commands'; }
+const CONFIG_BRANCH = 'refs/meta/config';
+const CONFIG_PATH = 'project.config';
+const EDIT_CONFIG_SUBJECT = 'Edit Repo Config';
+const INITIAL_PATCHSET = 1;
+const CREATE_CHANGE_FAILED_MESSAGE = 'Failed to create change.';
+const CREATE_CHANGE_SUCCEEDED_MESSAGE = 'Navigating to change';
- static get properties() {
- return {
- params: Object,
- repo: String,
- _loading: {
- type: Boolean,
- value: true,
- },
- /** @type {?} */
- _repoConfig: Object,
- _canCreate: Boolean,
- };
- }
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrRepoCommands extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** @override */
- attached() {
- super.attached();
- this._loadRepo();
+ static get is() { return 'gr-repo-commands'; }
- this.fire('title-change', {title: 'Repo Commands'});
- }
-
- _loadRepo() {
- if (!this.repo) { return Promise.resolve(); }
-
- const errFn = response => {
- this.fire('page-error', {response});
- };
-
- return this.$.restAPI.getProjectConfig(this.repo, errFn)
- .then(config => {
- if (!config) { return Promise.resolve(); }
-
- this._repoConfig = config;
- this._loading = false;
- });
- }
-
- _computeLoadingClass(loading) {
- return loading ? 'loading' : '';
- }
-
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _handleRunningGC() {
- return this.$.restAPI.runRepoGC(this.repo).then(response => {
- if (response.status === 200) {
- this.dispatchEvent(new CustomEvent(
- 'show-alert',
- {detail: {message: GC_MESSAGE}, bubbles: true, composed: true}));
- }
- });
- }
-
- _createNewChange() {
- this.$.createChangeOverlay.open();
- }
-
- _handleCreateChange() {
- this.$.createNewChangeModal.handleCreateChange();
- this._handleCloseCreateChange();
- }
-
- _handleCloseCreateChange() {
- this.$.createChangeOverlay.close();
- }
-
- _handleEditRepoConfig() {
- return this.$.restAPI.createChange(this.repo, CONFIG_BRANCH,
- EDIT_CONFIG_SUBJECT, undefined, false, true).then(change => {
- const message = change ?
- CREATE_CHANGE_SUCCEEDED_MESSAGE :
- CREATE_CHANGE_FAILED_MESSAGE;
- this.dispatchEvent(new CustomEvent('show-alert',
- {detail: {message}, bubbles: true, composed: true}));
- if (!change) { return; }
-
- Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getEditUrlForDiff(
- change, CONFIG_PATH, INITIAL_PATCHSET));
- });
- }
+ static get properties() {
+ return {
+ params: Object,
+ repo: String,
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ /** @type {?} */
+ _repoConfig: Object,
+ _canCreate: Boolean,
+ };
}
- customElements.define(GrRepoCommands.is, GrRepoCommands);
-})();
+ /** @override */
+ attached() {
+ super.attached();
+ this._loadRepo();
+
+ this.fire('title-change', {title: 'Repo Commands'});
+ }
+
+ _loadRepo() {
+ if (!this.repo) { return Promise.resolve(); }
+
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
+
+ return this.$.restAPI.getProjectConfig(this.repo, errFn)
+ .then(config => {
+ if (!config) { return Promise.resolve(); }
+
+ this._repoConfig = config;
+ this._loading = false;
+ });
+ }
+
+ _computeLoadingClass(loading) {
+ return loading ? 'loading' : '';
+ }
+
+ _isLoading() {
+ return this._loading || this._loading === undefined;
+ }
+
+ _handleRunningGC() {
+ return this.$.restAPI.runRepoGC(this.repo).then(response => {
+ if (response.status === 200) {
+ this.dispatchEvent(new CustomEvent(
+ 'show-alert',
+ {detail: {message: GC_MESSAGE}, bubbles: true, composed: true}));
+ }
+ });
+ }
+
+ _createNewChange() {
+ this.$.createChangeOverlay.open();
+ }
+
+ _handleCreateChange() {
+ this.$.createNewChangeModal.handleCreateChange();
+ this._handleCloseCreateChange();
+ }
+
+ _handleCloseCreateChange() {
+ this.$.createChangeOverlay.close();
+ }
+
+ _handleEditRepoConfig() {
+ return this.$.restAPI.createChange(this.repo, CONFIG_BRANCH,
+ EDIT_CONFIG_SUBJECT, undefined, false, true).then(change => {
+ const message = change ?
+ CREATE_CHANGE_SUCCEEDED_MESSAGE :
+ CREATE_CHANGE_FAILED_MESSAGE;
+ this.dispatchEvent(new CustomEvent('show-alert',
+ {detail: {message}, bubbles: true, composed: true}));
+ if (!change) { return; }
+
+ Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getEditUrlForDiff(
+ change, CONFIG_PATH, INITIAL_PATCHSET));
+ });
+ }
+}
+
+customElements.define(GrRepoCommands.is, GrRepoCommands);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js
new file mode 100644
index 0000000..ce19555
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js
@@ -0,0 +1,61 @@
+/**
+ * @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.js';
+
+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-subpage-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>
+ <main class="gr-form-styles read-only">
+ <h1 id="Title">Repository Commands</h1>
+ <div id="loading" class\$="[[_computeLoadingClass(_loading)]]">Loading...</div>
+ <div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]">
+ <h2 id="options">Command</h2>
+ <div id="form">
+ <gr-repo-command title="Create change" on-command-tap="_createNewChange">
+ </gr-repo-command>
+ <gr-repo-command id="editRepoConfig" title="Edit repo config" on-command-tap="_handleEditRepoConfig">
+ </gr-repo-command>
+ <gr-repo-command title="[[_repoConfig.actions.gc.label]]" tooltip="[[_repoConfig.actions.gc.title]]" hidden\$="[[!_repoConfig.actions.gc.enabled]]" on-command-tap="_handleRunningGC">
+ </gr-repo-command>
+ <gr-endpoint-decorator name="repo-command">
+ <gr-endpoint-param name="config" value="[[_repoConfig]]">
+ </gr-endpoint-param>
+ <gr-endpoint-param name="repoName" value="[[repo]]">
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </div>
+ </div>
+ </main>
+ <gr-overlay id="createChangeOverlay" with-backdrop="">
+ <gr-dialog id="createChangeDialog" confirm-label="Create" disabled="[[!_canCreate]]" on-confirm="_handleCreateChange" on-cancel="_handleCloseCreateChange">
+ <div class="header" slot="header">
+ Create Change
+ </div>
+ <div class="main" slot="main">
+ <gr-create-change-dialog id="createNewChangeModal" can-create="{{_canCreate}}" repo-name="[[repo]]"></gr-create-change-dialog>
+ </div>
+ </gr-dialog>
+ </gr-overlay>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
index da8b57f..ce2493a 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
@@ -19,16 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-commands</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-repo-commands.html">
-
-<script>void(0);</script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,111 +31,112 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo-commands tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let repoStub;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-repo-commands.js';
+suite('gr-repo-commands tests', () => {
+ let element;
+ let sandbox;
+ let repoStub;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ repoStub = sandbox.stub(
+ element.$.restAPI,
+ 'getProjectConfig',
+ () => Promise.resolve({}));
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('create new change dialog', () => {
+ test('_createNewChange opens modal', () => {
+ const openStub = sandbox.stub(element.$.createChangeOverlay, 'open');
+ element._createNewChange();
+ assert.isTrue(openStub.called);
+ });
+
+ test('_handleCreateChange called when confirm fired', () => {
+ sandbox.stub(element, '_handleCreateChange');
+ element.$.createChangeDialog.fire('confirm');
+ assert.isTrue(element._handleCreateChange.called);
+ });
+
+ test('_handleCloseCreateChange called when cancel fired', () => {
+ sandbox.stub(element, '_handleCloseCreateChange');
+ element.$.createChangeDialog.fire('cancel');
+ assert.isTrue(element._handleCloseCreateChange.called);
+ });
+ });
+
+ suite('edit repo config', () => {
+ let createChangeStub;
+ let urlStub;
+ let handleSpy;
+ let alertStub;
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- repoStub = sandbox.stub(
- element.$.restAPI,
- 'getProjectConfig',
- () => Promise.resolve({}));
+ createChangeStub = sandbox.stub(element.$.restAPI, 'createChange');
+ urlStub = sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff');
+ sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
+ handleSpy = sandbox.spy(element, '_handleEditRepoConfig');
+ alertStub = sandbox.stub();
+ element.addEventListener('show-alert', alertStub);
});
- teardown(() => {
- sandbox.restore();
- });
+ test('successful creation of change', () => {
+ const change = {_number: '1'};
+ createChangeStub.returns(Promise.resolve(change));
+ MockInteractions.tap(element.$.editRepoConfig.shadowRoot
+ .querySelector('gr-button'));
+ return handleSpy.lastCall.returnValue.then(() => {
+ flushAsynchronousOperations();
- suite('create new change dialog', () => {
- test('_createNewChange opens modal', () => {
- const openStub = sandbox.stub(element.$.createChangeOverlay, 'open');
- element._createNewChange();
- assert.isTrue(openStub.called);
- });
-
- test('_handleCreateChange called when confirm fired', () => {
- sandbox.stub(element, '_handleCreateChange');
- element.$.createChangeDialog.fire('confirm');
- assert.isTrue(element._handleCreateChange.called);
- });
-
- test('_handleCloseCreateChange called when cancel fired', () => {
- sandbox.stub(element, '_handleCloseCreateChange');
- element.$.createChangeDialog.fire('cancel');
- assert.isTrue(element._handleCloseCreateChange.called);
+ assert.isTrue(alertStub.called);
+ assert.equal(alertStub.lastCall.args[0].detail.message,
+ 'Navigating to change');
+ assert.isTrue(urlStub.called);
+ assert.deepEqual(urlStub.lastCall.args,
+ [change, 'project.config', 1]);
});
});
- suite('edit repo config', () => {
- let createChangeStub;
- let urlStub;
- let handleSpy;
- let alertStub;
+ test('unsuccessful creation of change', () => {
+ createChangeStub.returns(Promise.resolve(null));
+ MockInteractions.tap(element.$.editRepoConfig.shadowRoot
+ .querySelector('gr-button'));
+ return handleSpy.lastCall.returnValue.then(() => {
+ flushAsynchronousOperations();
- setup(() => {
- createChangeStub = sandbox.stub(element.$.restAPI, 'createChange');
- urlStub = sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff');
- sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
- handleSpy = sandbox.spy(element, '_handleEditRepoConfig');
- alertStub = sandbox.stub();
- element.addEventListener('show-alert', alertStub);
- });
-
- test('successful creation of change', () => {
- const change = {_number: '1'};
- createChangeStub.returns(Promise.resolve(change));
- MockInteractions.tap(element.$.editRepoConfig.shadowRoot
- .querySelector('gr-button'));
- return handleSpy.lastCall.returnValue.then(() => {
- flushAsynchronousOperations();
-
- assert.isTrue(alertStub.called);
- assert.equal(alertStub.lastCall.args[0].detail.message,
- 'Navigating to change');
- assert.isTrue(urlStub.called);
- assert.deepEqual(urlStub.lastCall.args,
- [change, 'project.config', 1]);
- });
- });
-
- test('unsuccessful creation of change', () => {
- createChangeStub.returns(Promise.resolve(null));
- MockInteractions.tap(element.$.editRepoConfig.shadowRoot
- .querySelector('gr-button'));
- return handleSpy.lastCall.returnValue.then(() => {
- flushAsynchronousOperations();
-
- assert.isTrue(alertStub.called);
- assert.equal(alertStub.lastCall.args[0].detail.message,
- 'Failed to create change.');
- assert.isFalse(urlStub.called);
- });
- });
- });
-
- suite('404', () => {
- test('fires page-error', done => {
- repoStub.restore();
-
- element.repo = 'test';
-
- const response = {status: 404};
- sandbox.stub(
- element.$.restAPI, 'getProjectConfig', (repo, errFn) => {
- errFn(response);
- });
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- element._loadRepo();
+ assert.isTrue(alertStub.called);
+ assert.equal(alertStub.lastCall.args[0].detail.message,
+ 'Failed to create change.');
+ assert.isFalse(urlStub.called);
});
});
});
+
+ suite('404', () => {
+ test('fires page-error', done => {
+ repoStub.restore();
+
+ element.repo = 'test';
+
+ const response = {status: 404};
+ sandbox.stub(
+ element.$.restAPI, 'getProjectConfig', (repo, errFn) => {
+ errFn(response);
+ });
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
+ });
+
+ element._loadRepo();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html
deleted file mode 100644
index f74f705..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html
+++ /dev/null
@@ -1,72 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-repo-dashboards">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- margin-bottom: var(--spacing-xxl);
- }
- .loading #dashboards,
- #loadingContainer {
- display: none;
- }
- .loading #loadingContainer {
- display: block;
- }
- </style>
- <style include="gr-table-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <table id="list" class$="genericList [[_computeLoadingClass(_loading)]]">
- <tr class="headerRow">
- <th class="topHeader">Dashboard name</th>
- <th class="topHeader">Dashboard title</th>
- <th class="topHeader">Dashboard description</th>
- <th class="topHeader">Inherited from</th>
- <th class="topHeader">Default</th>
- </tr>
- <tr id="loadingContainer">
- <td>Loading...</td>
- </tr>
- <tbody id="dashboards">
- <template is="dom-repeat" items="[[_dashboards]]">
- <tr class="groupHeader">
- <td colspan="5">[[item.section]]</td>
- </tr>
- <template is="dom-repeat" items="[[item.dashboards]]">
- <tr class="table">
- <td class="name"><a href$="[[_getUrl(item.project, item.id)]]">[[item.path]]</a></td>
- <td class="title">[[item.title]]</td>
- <td class="desc">[[item.description]]</td>
- <td class="inherited">[[_computeInheritedFrom(item.project, item.defining_project)]]</td>
- <td class="default">[[_computeIsDefault(item.is_default)]]</td>
- </tr>
- </template>
- </template>
- </tbody>
- </table>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-repo-dashboards.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
index 8e09263..e1f38c9 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright (C) 2018 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.
@@ -14,89 +14,100 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
- */
- class GrRepoDashboards extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-repo-dashboards'; }
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-repo-dashboards_html.js';
- static get properties() {
- return {
- repo: {
- type: String,
- observer: '_repoChanged',
- },
- _loading: {
- type: Boolean,
- value: true,
- },
- _dashboards: Array,
- };
- }
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrRepoDashboards extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _repoChanged(repo) {
- this._loading = true;
- if (!repo) { return Promise.resolve(); }
+ static get is() { return 'gr-repo-dashboards'; }
- const errFn = response => {
- this.fire('page-error', {response});
- };
-
- this.$.restAPI.getRepoDashboards(this.repo, errFn).then(res => {
- if (!res) { return Promise.resolve(); }
-
- // Group by ref and sort by id.
- const dashboards = res.concat.apply([], res).sort((a, b) =>
- (a.id < b.id ? -1 : 1));
- const dashboardsByRef = {};
- dashboards.forEach(d => {
- if (!dashboardsByRef[d.ref]) {
- dashboardsByRef[d.ref] = [];
- }
- dashboardsByRef[d.ref].push(d);
- });
-
- const dashboardBuilder = [];
- Object.keys(dashboardsByRef).sort()
- .forEach(ref => {
- dashboardBuilder.push({
- section: ref,
- dashboards: dashboardsByRef[ref],
- });
- });
-
- this._dashboards = dashboardBuilder;
- this._loading = false;
- Polymer.dom.flush();
- });
- }
-
- _getUrl(project, id) {
- if (!project || !id) { return ''; }
-
- return Gerrit.Nav.getUrlForRepoDashboard(project, id);
- }
-
- _computeLoadingClass(loading) {
- return loading ? 'loading' : '';
- }
-
- _computeInheritedFrom(project, definingProject) {
- return project === definingProject ? '' : definingProject;
- }
-
- _computeIsDefault(isDefault) {
- return isDefault ? '✓' : '';
- }
+ static get properties() {
+ return {
+ repo: {
+ type: String,
+ observer: '_repoChanged',
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _dashboards: Array,
+ };
}
- customElements.define(GrRepoDashboards.is, GrRepoDashboards);
-})();
+ _repoChanged(repo) {
+ this._loading = true;
+ if (!repo) { return Promise.resolve(); }
+
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
+
+ this.$.restAPI.getRepoDashboards(this.repo, errFn).then(res => {
+ if (!res) { return Promise.resolve(); }
+
+ // Group by ref and sort by id.
+ const dashboards = res.concat.apply([], res).sort((a, b) =>
+ (a.id < b.id ? -1 : 1));
+ const dashboardsByRef = {};
+ dashboards.forEach(d => {
+ if (!dashboardsByRef[d.ref]) {
+ dashboardsByRef[d.ref] = [];
+ }
+ dashboardsByRef[d.ref].push(d);
+ });
+
+ const dashboardBuilder = [];
+ Object.keys(dashboardsByRef).sort()
+ .forEach(ref => {
+ dashboardBuilder.push({
+ section: ref,
+ dashboards: dashboardsByRef[ref],
+ });
+ });
+
+ this._dashboards = dashboardBuilder;
+ this._loading = false;
+ flush();
+ });
+ }
+
+ _getUrl(project, id) {
+ if (!project || !id) { return ''; }
+
+ return Gerrit.Nav.getUrlForRepoDashboard(project, id);
+ }
+
+ _computeLoadingClass(loading) {
+ return loading ? 'loading' : '';
+ }
+
+ _computeInheritedFrom(project, definingProject) {
+ return project === definingProject ? '' : definingProject;
+ }
+
+ _computeIsDefault(isDefault) {
+ return isDefault ? '✓' : '';
+ }
+}
+
+customElements.define(GrRepoDashboards.is, GrRepoDashboards);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.js b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.js
new file mode 100644
index 0000000..3bac16c
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.js
@@ -0,0 +1,65 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ margin-bottom: var(--spacing-xxl);
+ }
+ .loading #dashboards,
+ #loadingContainer {
+ display: none;
+ }
+ .loading #loadingContainer {
+ display: block;
+ }
+ </style>
+ <style include="gr-table-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <table id="list" class\$="genericList [[_computeLoadingClass(_loading)]]">
+ <tbody><tr class="headerRow">
+ <th class="topHeader">Dashboard name</th>
+ <th class="topHeader">Dashboard title</th>
+ <th class="topHeader">Dashboard description</th>
+ <th class="topHeader">Inherited from</th>
+ <th class="topHeader">Default</th>
+ </tr>
+ <tr id="loadingContainer">
+ <td>Loading...</td>
+ </tr>
+ </tbody><tbody id="dashboards">
+ <template is="dom-repeat" items="[[_dashboards]]">
+ <tr class="groupHeader">
+ <td colspan="5">[[item.section]]</td>
+ </tr>
+ <template is="dom-repeat" items="[[item.dashboards]]">
+ <tr class="table">
+ <td class="name"><a href\$="[[_getUrl(item.project, item.id)]]">[[item.path]]</a></td>
+ <td class="title">[[item.title]]</td>
+ <td class="desc">[[item.description]]</td>
+ <td class="inherited">[[_computeInheritedFrom(item.project, item.defining_project)]]</td>
+ <td class="default">[[_computeIsDefault(item.is_default)]]</td>
+ </tr>
+ </template>
+ </template>
+ </tbody>
+ </table>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
index 681ee19..14e67e9 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-dashboards</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-repo-dashboards.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,128 +30,129 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo-dashboards tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-repo-dashboards.js';
+suite('gr-repo-dashboards tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('dashboard table', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
+ sandbox.stub(element.$.restAPI, 'getRepoDashboards').returns(
+ Promise.resolve([
+ {
+ id: 'default:contributor',
+ project: 'gerrit',
+ defining_project: 'gerrit',
+ ref: 'default',
+ path: 'contributor',
+ description: 'Own contributions.',
+ foreach: 'owner:self',
+ url: '/dashboard/?params',
+ title: 'Contributor Dashboard',
+ sections: [
+ {
+ name: 'Mine To Rebase',
+ query: 'is:open -is:mergeable',
+ },
+ {
+ name: 'My Recently Merged',
+ query: 'is:merged limit:10',
+ },
+ ],
+ },
+ {
+ id: 'custom:custom2',
+ project: 'gerrit',
+ defining_project: 'Public-Projects',
+ ref: 'custom',
+ path: 'open',
+ description: 'Recent open changes.',
+ url: '/dashboard/?params',
+ title: 'Open Changes',
+ sections: [
+ {
+ name: 'Open Changes',
+ query: 'status:open project:${project} -age:7w',
+ },
+ ],
+ },
+ {
+ id: 'default:abc',
+ project: 'gerrit',
+ ref: 'default',
+ },
+ {
+ id: 'custom:custom1',
+ project: 'gerrit',
+ ref: 'custom',
+ },
+ ]));
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('dashboard table', () => {
- setup(() => {
- sandbox.stub(element.$.restAPI, 'getRepoDashboards').returns(
- Promise.resolve([
- {
- id: 'default:contributor',
- project: 'gerrit',
- defining_project: 'gerrit',
- ref: 'default',
- path: 'contributor',
- description: 'Own contributions.',
- foreach: 'owner:self',
- url: '/dashboard/?params',
- title: 'Contributor Dashboard',
- sections: [
- {
- name: 'Mine To Rebase',
- query: 'is:open -is:mergeable',
- },
- {
- name: 'My Recently Merged',
- query: 'is:merged limit:10',
- },
- ],
- },
- {
- id: 'custom:custom2',
- project: 'gerrit',
- defining_project: 'Public-Projects',
- ref: 'custom',
- path: 'open',
- description: 'Recent open changes.',
- url: '/dashboard/?params',
- title: 'Open Changes',
- sections: [
- {
- name: 'Open Changes',
- query: 'status:open project:${project} -age:7w',
- },
- ],
- },
- {
- id: 'default:abc',
- project: 'gerrit',
- ref: 'default',
- },
- {
- id: 'custom:custom1',
- project: 'gerrit',
- ref: 'custom',
- },
- ]));
- });
-
- test('loading, sections, and ordering', done => {
- assert.isTrue(element._loading);
- assert.notEqual(getComputedStyle(element.$.loadingContainer).display,
+ test('loading, sections, and ordering', done => {
+ assert.isTrue(element._loading);
+ assert.notEqual(getComputedStyle(element.$.loadingContainer).display,
+ 'none');
+ assert.equal(getComputedStyle(element.$.dashboards).display,
+ 'none');
+ element.repo = 'test';
+ flush(() => {
+ assert.equal(getComputedStyle(element.$.loadingContainer).display,
'none');
- assert.equal(getComputedStyle(element.$.dashboards).display,
+ assert.notEqual(getComputedStyle(element.$.dashboards).display,
'none');
- element.repo = 'test';
- flush(() => {
- assert.equal(getComputedStyle(element.$.loadingContainer).display,
- 'none');
- assert.notEqual(getComputedStyle(element.$.dashboards).display,
- 'none');
- assert.equal(element._dashboards.length, 2);
- assert.equal(element._dashboards[0].section, 'custom');
- assert.equal(element._dashboards[1].section, 'default');
+ assert.equal(element._dashboards.length, 2);
+ assert.equal(element._dashboards[0].section, 'custom');
+ assert.equal(element._dashboards[1].section, 'default');
- const dashboards = element._dashboards[0].dashboards;
- assert.equal(dashboards.length, 2);
- assert.equal(dashboards[0].id, 'custom:custom1');
- assert.equal(dashboards[1].id, 'custom:custom2');
+ const dashboards = element._dashboards[0].dashboards;
+ assert.equal(dashboards.length, 2);
+ assert.equal(dashboards[0].id, 'custom:custom1');
+ assert.equal(dashboards[1].id, 'custom:custom2');
- done();
- });
- });
- });
-
- suite('test url', () => {
- test('_getUrl', () => {
- sandbox.stub(Gerrit.Nav, 'getUrlForRepoDashboard',
- () => '/r/dashboard/test');
-
- assert.equal(element._getUrl('/dashboard/test', {}), '/r/dashboard/test');
-
- assert.equal(element._getUrl(undefined, undefined), '');
- });
- });
-
- suite('404', () => {
- test('fires page-error', done => {
- const response = {status: 404};
- sandbox.stub(
- element.$.restAPI, 'getRepoDashboards', (repo, errFn) => {
- errFn(response);
- });
-
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- element.repo = 'test';
+ done();
});
});
});
+
+ suite('test url', () => {
+ test('_getUrl', () => {
+ sandbox.stub(Gerrit.Nav, 'getUrlForRepoDashboard',
+ () => '/r/dashboard/test');
+
+ assert.equal(element._getUrl('/dashboard/test', {}), '/r/dashboard/test');
+
+ assert.equal(element._getUrl(undefined, undefined), '');
+ });
+ });
+
+ suite('404', () => {
+ test('fires page-error', done => {
+ const response = {status: 404};
+ sandbox.stub(
+ element.$.restAPI, 'getRepoDashboards', (repo, errFn) => {
+ errFn(response);
+ });
+
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
+ });
+
+ element.repo = 'test';
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html
deleted file mode 100644
index 467cef0..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html
+++ /dev/null
@@ -1,224 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/gr-table-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-create-pointer-dialog/gr-create-pointer-dialog.html">
-<link rel="import" href="../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html">
-
-<dom-module id="gr-repo-detail-list">
- <template>
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-table-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- .tags td.name {
- min-width: 25em;
- }
- td.name,
- td.revision,
- td.message {
- word-break: break-word;
- }
- td.revision.tags {
- width: 27em;
- }
- td.message,
- td.tagger {
- max-width: 15em;
- }
- .editing .editItem {
- display: inherit;
- }
- .editItem,
- .editing .editBtn,
- .canEdit .revisionNoEditing,
- .editing .revisionWithEditing,
- .revisionEdit,
- .hideItem {
- display: none;
- }
- .revisionEdit gr-button {
- margin-left: var(--spacing-m);
- }
- .editBtn {
- margin-left: var(--spacing-l);
- }
- .canEdit .revisionEdit{
- align-items: center;
- display: flex;
- }
- .deleteButton:not(.show) {
- display: none;
- }
- .tagger.hide {
- display: none;
- }
- </style>
- <style include="gr-table-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <gr-list-view
- create-new="[[_loggedIn]]"
- filter="[[_filter]]"
- items-per-page="[[_itemsPerPage]]"
- items="[[_items]]"
- loading="[[_loading]]"
- offset="[[_offset]]"
- on-create-clicked="_handleCreateClicked"
- path="[[_getPath(_repo, detailType)]]">
- <table id="list" class="genericList gr-form-styles">
- <tr class="headerRow">
- <th class="name topHeader">Name</th>
- <th class="revision topHeader">Revision</th>
- <th class$="message topHeader [[_hideIfBranch(detailType)]]">
- Message</th>
- <th class$="tagger topHeader [[_hideIfBranch(detailType)]]">
- Tagger</th>
- <th class="repositoryBrowser topHeader">
- Repository Browser</th>
- <th class="delete topHeader"></th>
- </tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
- <td>Loading...</td>
- </tr>
- <tbody class$="[[computeLoadingClass(_loading)]]">
- <template is="dom-repeat" items="[[_shownItems]]">
- <tr class="table">
- <td class$="[[detailType]] name">[[_stripRefs(item.ref, detailType)]]</td>
- <td class$="[[detailType]] revision [[_computeCanEditClass(item.ref, detailType, _isOwner)]]">
- <span class="revisionNoEditing">
- [[item.revision]]
- </span>
- <span class$="revisionEdit [[_computeEditingClass(_isEditing)]]">
- <span class="revisionWithEditing">
- [[item.revision]]
- </span>
- <gr-button
- link
- on-click="_handleEditRevision"
- class="editBtn">
- edit
- </gr-button>
- <iron-input
- bind-value="{{_revisedRef}}"
- class="editItem">
- <input
- is="iron-input"
- bind-value="{{_revisedRef}}">
- </iron-input>
- <gr-button
- link
- on-click="_handleCancelRevision"
- class="cancelBtn editItem">
- Cancel
- </gr-button>
- <gr-button
- link
- on-click="_handleSaveRevision"
- class="saveBtn editItem"
- disabled="[[!_revisedRef]]">
- Save
- </gr-button>
- </span>
- </td>
- <td class$="message [[_hideIfBranch(detailType)]]">
- [[_computeMessage(item.message)]]
- </td>
- <td class$="tagger [[_hideIfBranch(detailType)]]">
- <div class$="tagger [[_computeHideTagger(item.tagger)]]">
- <gr-account-link
- account="[[item.tagger]]">
- </gr-account-link>
- (<gr-date-formatter
- has-tooltip
- date-str="[[item.tagger.date]]">
- </gr-date-formatter>)
- </div>
- </td>
- <td class="repositoryBrowser">
- <template is="dom-repeat"
- items="[[_computeWeblink(item)]]" as="link">
- <a href$="[[link.url]]"
- class="webLink"
- rel="noopener"
- target="_blank">
- ([[link.name]])
- </a>
- </template>
- </td>
- <td class="delete">
- <gr-button
- link
- class$="deleteButton [[_computeHideDeleteClass(_isOwner, item.can_delete)]]"
- on-click="_handleDeleteItem">
- Delete
- </gr-button>
- </td>
- </tr>
- </template>
- </tbody>
- </table>
- <gr-overlay id="overlay" with-backdrop>
- <gr-confirm-delete-item-dialog
- class="confirmDialog"
- on-confirm="_handleDeleteItemConfirm"
- on-cancel="_handleConfirmDialogCancel"
- item="[[_refName]]"
- item-type="[[detailType]]"></gr-confirm-delete-item-dialog>
- </gr-overlay>
- </gr-list-view>
- <gr-overlay id="createOverlay" with-backdrop>
- <gr-dialog
- id="createDialog"
- disabled="[[!_hasNewItemName]]"
- confirm-label="Create"
- on-confirm="_handleCreateItem"
- on-cancel="_handleCloseCreate">
- <div class="header" slot="header">
- Create [[_computeItemName(detailType)]]
- </div>
- <div class="main" slot="main">
- <gr-create-pointer-dialog
- id="createNewModal"
- detail-type="[[_computeItemName(detailType)]]"
- has-new-item-name="{{_hasNewItemName}}"
- item-detail="[[detailType]]"
- repo-name="[[_repo]]"></gr-create-pointer-dialog>
- </div>
- </gr-dialog>
- </gr-overlay>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-repo-detail-list.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
index ccfdfc6..82a6a4c 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
@@ -14,279 +14,302 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
- const DETAIL_TYPES = {
- BRANCHES: 'branches',
- TAGS: 'tags',
- };
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/gr-table-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-account-link/gr-account-link.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-list-view/gr-list-view.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-create-pointer-dialog/gr-create-pointer-dialog.js';
+import '../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-repo-detail-list_html.js';
- const PGP_START = '-----BEGIN PGP SIGNATURE-----';
+const DETAIL_TYPES = {
+ BRANCHES: 'branches',
+ TAGS: 'tags',
+};
- /**
- * @appliesMixin Gerrit.ListViewMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrRepoDetailList extends Polymer.mixinBehaviors( [
- Gerrit.ListViewBehavior,
- Gerrit.FireBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-repo-detail-list'; }
+const PGP_START = '-----BEGIN PGP SIGNATURE-----';
- static get properties() {
- return {
+/**
+ * @appliesMixin Gerrit.ListViewMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrRepoDetailList extends mixinBehaviors( [
+ Gerrit.ListViewBehavior,
+ Gerrit.FireBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-repo-detail-list'; }
+
+ static get properties() {
+ return {
+ /**
+ * URL params passed from the router.
+ */
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
/**
- * URL params passed from the router.
+ * The kind of detail we are displaying, possibilities are determined by
+ * the const DETAIL_TYPES.
*/
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
- /**
- * The kind of detail we are displaying, possibilities are determined by
- * the const DETAIL_TYPES.
- */
- detailType: String,
+ detailType: String,
- _editing: {
- type: Boolean,
- value: false,
- },
- _isOwner: {
- type: Boolean,
- value: false,
- },
- _loggedIn: {
- type: Boolean,
- value: false,
- },
- /**
- * Offset of currently visible query results.
- */
- _offset: Number,
- _repo: Object,
- _items: Array,
- /**
- * Because we request one more than the projectsPerPage, _shownProjects
- * maybe one less than _projects.
- */
- _shownItems: {
- type: Array,
- computed: 'computeShownItems(_items)',
- },
- _itemsPerPage: {
- type: Number,
- value: 25,
- },
- _loading: {
- type: Boolean,
- value: true,
- },
- _filter: String,
- _refName: String,
- _hasNewItemName: Boolean,
- _isEditing: Boolean,
- _revisedRef: String,
- };
- }
+ _editing: {
+ type: Boolean,
+ value: false,
+ },
+ _isOwner: {
+ type: Boolean,
+ value: false,
+ },
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+ /**
+ * Offset of currently visible query results.
+ */
+ _offset: Number,
+ _repo: Object,
+ _items: Array,
+ /**
+ * Because we request one more than the projectsPerPage, _shownProjects
+ * maybe one less than _projects.
+ */
+ _shownItems: {
+ type: Array,
+ computed: 'computeShownItems(_items)',
+ },
+ _itemsPerPage: {
+ type: Number,
+ value: 25,
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _filter: String,
+ _refName: String,
+ _hasNewItemName: Boolean,
+ _isEditing: Boolean,
+ _revisedRef: String,
+ };
+ }
- _determineIfOwner(repo) {
- return this.$.restAPI.getRepoAccess(repo)
- .then(access =>
- this._isOwner = access && !!access[repo].is_owner);
- }
+ _determineIfOwner(repo) {
+ return this.$.restAPI.getRepoAccess(repo)
+ .then(access =>
+ this._isOwner = access && !!access[repo].is_owner);
+ }
- _paramsChanged(params) {
- if (!params || !params.repo) { return; }
+ _paramsChanged(params) {
+ if (!params || !params.repo) { return; }
- this._repo = params.repo;
+ this._repo = params.repo;
- this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- if (loggedIn) {
- this._determineIfOwner(this._repo);
- }
+ this._getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ if (loggedIn) {
+ this._determineIfOwner(this._repo);
+ }
+ });
+
+ this.detailType = params.detail;
+
+ this._filter = this.getFilterValue(params);
+ this._offset = this.getOffsetValue(params);
+
+ return this._getItems(this._filter, this._repo,
+ this._itemsPerPage, this._offset, this.detailType);
+ }
+
+ _getItems(filter, repo, itemsPerPage, offset, detailType) {
+ this._loading = true;
+ this._items = [];
+ flush();
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
+ if (detailType === DETAIL_TYPES.BRANCHES) {
+ return this.$.restAPI.getRepoBranches(
+ filter, repo, itemsPerPage, offset, errFn).then(items => {
+ if (!items) { return; }
+ this._items = items;
+ this._loading = false;
});
-
- this.detailType = params.detail;
-
- this._filter = this.getFilterValue(params);
- this._offset = this.getOffsetValue(params);
-
- return this._getItems(this._filter, this._repo,
- this._itemsPerPage, this._offset, this.detailType);
- }
-
- _getItems(filter, repo, itemsPerPage, offset, detailType) {
- this._loading = true;
- this._items = [];
- Polymer.dom.flush();
- const errFn = response => {
- this.fire('page-error', {response});
- };
- if (detailType === DETAIL_TYPES.BRANCHES) {
- return this.$.restAPI.getRepoBranches(
- filter, repo, itemsPerPage, offset, errFn).then(items => {
- if (!items) { return; }
- this._items = items;
- this._loading = false;
- });
- } else if (detailType === DETAIL_TYPES.TAGS) {
- return this.$.restAPI.getRepoTags(
- filter, repo, itemsPerPage, offset, errFn).then(items => {
- if (!items) { return; }
- this._items = items;
- this._loading = false;
- });
- }
- }
-
- _getPath(repo) {
- return `/admin/repos/${this.encodeURL(repo, false)},` +
- `${this.detailType}`;
- }
-
- _computeWeblink(repo) {
- if (!repo.web_links) { return ''; }
- const webLinks = repo.web_links;
- return webLinks.length ? webLinks : null;
- }
-
- _computeMessage(message) {
- if (!message) { return; }
- // Strip PGP info.
- return message.split(PGP_START)[0];
- }
-
- _stripRefs(item, detailType) {
- if (detailType === DETAIL_TYPES.BRANCHES) {
- return item.replace('refs/heads/', '');
- } else if (detailType === DETAIL_TYPES.TAGS) {
- return item.replace('refs/tags/', '');
- }
- }
-
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- _computeEditingClass(isEditing) {
- return isEditing ? 'editing' : '';
- }
-
- _computeCanEditClass(ref, detailType, isOwner) {
- return isOwner && this._stripRefs(ref, detailType) === 'HEAD' ?
- 'canEdit' : '';
- }
-
- _handleEditRevision(e) {
- this._revisedRef = e.model.get('item.revision');
- this._isEditing = true;
- }
-
- _handleCancelRevision() {
- this._isEditing = false;
- }
-
- _handleSaveRevision(e) {
- this._setRepoHead(this._repo, this._revisedRef, e);
- }
-
- _setRepoHead(repo, ref, e) {
- return this.$.restAPI.setRepoHead(repo, ref).then(res => {
- if (res.status < 400) {
- this._isEditing = false;
- e.model.set('item.revision', ref);
- // This is needed to refresh _items property with fresh data,
- // specifically can_delete from the json response.
- this._getItems(
- this._filter, this._repo, this._itemsPerPage,
- this._offset, this.detailType);
- }
+ } else if (detailType === DETAIL_TYPES.TAGS) {
+ return this.$.restAPI.getRepoTags(
+ filter, repo, itemsPerPage, offset, errFn).then(items => {
+ if (!items) { return; }
+ this._items = items;
+ this._loading = false;
});
}
-
- _computeItemName(detailType) {
- if (detailType === DETAIL_TYPES.BRANCHES) {
- return 'Branch';
- } else if (detailType === DETAIL_TYPES.TAGS) {
- return 'Tag';
- }
- }
-
- _handleDeleteItemConfirm() {
- this.$.overlay.close();
- if (this.detailType === DETAIL_TYPES.BRANCHES) {
- return this.$.restAPI.deleteRepoBranches(this._repo, this._refName)
- .then(itemDeleted => {
- if (itemDeleted.status === 204) {
- this._getItems(
- this._filter, this._repo, this._itemsPerPage,
- this._offset, this.detailType);
- }
- });
- } else if (this.detailType === DETAIL_TYPES.TAGS) {
- return this.$.restAPI.deleteRepoTags(this._repo, this._refName)
- .then(itemDeleted => {
- if (itemDeleted.status === 204) {
- this._getItems(
- this._filter, this._repo, this._itemsPerPage,
- this._offset, this.detailType);
- }
- });
- }
- }
-
- _handleConfirmDialogCancel() {
- this.$.overlay.close();
- }
-
- _handleDeleteItem(e) {
- const name = this._stripRefs(e.model.get('item.ref'), this.detailType);
- if (!name) { return; }
- this._refName = name;
- this.$.overlay.open();
- }
-
- _computeHideDeleteClass(owner, canDelete) {
- if (canDelete || owner) {
- return 'show';
- }
-
- return '';
- }
-
- _handleCreateItem() {
- this.$.createNewModal.handleCreateItem();
- this._handleCloseCreate();
- }
-
- _handleCloseCreate() {
- this.$.createOverlay.close();
- }
-
- _handleCreateClicked() {
- this.$.createOverlay.open();
- }
-
- _hideIfBranch(type) {
- if (type === DETAIL_TYPES.BRANCHES) {
- return 'hideItem';
- }
-
- return '';
- }
-
- _computeHideTagger(tagger) {
- return tagger ? '' : 'hide';
- }
}
- customElements.define(GrRepoDetailList.is, GrRepoDetailList);
-})();
+ _getPath(repo) {
+ return `/admin/repos/${this.encodeURL(repo, false)},` +
+ `${this.detailType}`;
+ }
+
+ _computeWeblink(repo) {
+ if (!repo.web_links) { return ''; }
+ const webLinks = repo.web_links;
+ return webLinks.length ? webLinks : null;
+ }
+
+ _computeMessage(message) {
+ if (!message) { return; }
+ // Strip PGP info.
+ return message.split(PGP_START)[0];
+ }
+
+ _stripRefs(item, detailType) {
+ if (detailType === DETAIL_TYPES.BRANCHES) {
+ return item.replace('refs/heads/', '');
+ } else if (detailType === DETAIL_TYPES.TAGS) {
+ return item.replace('refs/tags/', '');
+ }
+ }
+
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _computeEditingClass(isEditing) {
+ return isEditing ? 'editing' : '';
+ }
+
+ _computeCanEditClass(ref, detailType, isOwner) {
+ return isOwner && this._stripRefs(ref, detailType) === 'HEAD' ?
+ 'canEdit' : '';
+ }
+
+ _handleEditRevision(e) {
+ this._revisedRef = e.model.get('item.revision');
+ this._isEditing = true;
+ }
+
+ _handleCancelRevision() {
+ this._isEditing = false;
+ }
+
+ _handleSaveRevision(e) {
+ this._setRepoHead(this._repo, this._revisedRef, e);
+ }
+
+ _setRepoHead(repo, ref, e) {
+ return this.$.restAPI.setRepoHead(repo, ref).then(res => {
+ if (res.status < 400) {
+ this._isEditing = false;
+ e.model.set('item.revision', ref);
+ // This is needed to refresh _items property with fresh data,
+ // specifically can_delete from the json response.
+ this._getItems(
+ this._filter, this._repo, this._itemsPerPage,
+ this._offset, this.detailType);
+ }
+ });
+ }
+
+ _computeItemName(detailType) {
+ if (detailType === DETAIL_TYPES.BRANCHES) {
+ return 'Branch';
+ } else if (detailType === DETAIL_TYPES.TAGS) {
+ return 'Tag';
+ }
+ }
+
+ _handleDeleteItemConfirm() {
+ this.$.overlay.close();
+ if (this.detailType === DETAIL_TYPES.BRANCHES) {
+ return this.$.restAPI.deleteRepoBranches(this._repo, this._refName)
+ .then(itemDeleted => {
+ if (itemDeleted.status === 204) {
+ this._getItems(
+ this._filter, this._repo, this._itemsPerPage,
+ this._offset, this.detailType);
+ }
+ });
+ } else if (this.detailType === DETAIL_TYPES.TAGS) {
+ return this.$.restAPI.deleteRepoTags(this._repo, this._refName)
+ .then(itemDeleted => {
+ if (itemDeleted.status === 204) {
+ this._getItems(
+ this._filter, this._repo, this._itemsPerPage,
+ this._offset, this.detailType);
+ }
+ });
+ }
+ }
+
+ _handleConfirmDialogCancel() {
+ this.$.overlay.close();
+ }
+
+ _handleDeleteItem(e) {
+ const name = this._stripRefs(e.model.get('item.ref'), this.detailType);
+ if (!name) { return; }
+ this._refName = name;
+ this.$.overlay.open();
+ }
+
+ _computeHideDeleteClass(owner, canDelete) {
+ if (canDelete || owner) {
+ return 'show';
+ }
+
+ return '';
+ }
+
+ _handleCreateItem() {
+ this.$.createNewModal.handleCreateItem();
+ this._handleCloseCreate();
+ }
+
+ _handleCloseCreate() {
+ this.$.createOverlay.close();
+ }
+
+ _handleCreateClicked() {
+ this.$.createOverlay.open();
+ }
+
+ _hideIfBranch(type) {
+ if (type === DETAIL_TYPES.BRANCHES) {
+ return 'hideItem';
+ }
+
+ return '';
+ }
+
+ _computeHideTagger(tagger) {
+ return tagger ? '' : 'hide';
+ }
+}
+
+customElements.define(GrRepoDetailList.is, GrRepoDetailList);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.js
new file mode 100644
index 0000000..0d232f2
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.js
@@ -0,0 +1,157 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="gr-form-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="gr-table-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="shared-styles">
+ .tags td.name {
+ min-width: 25em;
+ }
+ td.name,
+ td.revision,
+ td.message {
+ word-break: break-word;
+ }
+ td.revision.tags {
+ width: 27em;
+ }
+ td.message,
+ td.tagger {
+ max-width: 15em;
+ }
+ .editing .editItem {
+ display: inherit;
+ }
+ .editItem,
+ .editing .editBtn,
+ .canEdit .revisionNoEditing,
+ .editing .revisionWithEditing,
+ .revisionEdit,
+ .hideItem {
+ display: none;
+ }
+ .revisionEdit gr-button {
+ margin-left: var(--spacing-m);
+ }
+ .editBtn {
+ margin-left: var(--spacing-l);
+ }
+ .canEdit .revisionEdit{
+ align-items: center;
+ display: flex;
+ }
+ .deleteButton:not(.show) {
+ display: none;
+ }
+ .tagger.hide {
+ display: none;
+ }
+ </style>
+ <style include="gr-table-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <gr-list-view create-new="[[_loggedIn]]" filter="[[_filter]]" items-per-page="[[_itemsPerPage]]" items="[[_items]]" loading="[[_loading]]" offset="[[_offset]]" on-create-clicked="_handleCreateClicked" path="[[_getPath(_repo, detailType)]]">
+ <table id="list" class="genericList gr-form-styles">
+ <tbody><tr class="headerRow">
+ <th class="name topHeader">Name</th>
+ <th class="revision topHeader">Revision</th>
+ <th class\$="message topHeader [[_hideIfBranch(detailType)]]">
+ Message</th>
+ <th class\$="tagger topHeader [[_hideIfBranch(detailType)]]">
+ Tagger</th>
+ <th class="repositoryBrowser topHeader">
+ Repository Browser</th>
+ <th class="delete topHeader"></th>
+ </tr>
+ <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]">
+ <td>Loading...</td>
+ </tr>
+ </tbody><tbody class\$="[[computeLoadingClass(_loading)]]">
+ <template is="dom-repeat" items="[[_shownItems]]">
+ <tr class="table">
+ <td class\$="[[detailType]] name">[[_stripRefs(item.ref, detailType)]]</td>
+ <td class\$="[[detailType]] revision [[_computeCanEditClass(item.ref, detailType, _isOwner)]]">
+ <span class="revisionNoEditing">
+ [[item.revision]]
+ </span>
+ <span class\$="revisionEdit [[_computeEditingClass(_isEditing)]]">
+ <span class="revisionWithEditing">
+ [[item.revision]]
+ </span>
+ <gr-button link="" on-click="_handleEditRevision" class="editBtn">
+ edit
+ </gr-button>
+ <iron-input bind-value="{{_revisedRef}}" class="editItem">
+ <input is="iron-input" bind-value="{{_revisedRef}}">
+ </iron-input>
+ <gr-button link="" on-click="_handleCancelRevision" class="cancelBtn editItem">
+ Cancel
+ </gr-button>
+ <gr-button link="" on-click="_handleSaveRevision" class="saveBtn editItem" disabled="[[!_revisedRef]]">
+ Save
+ </gr-button>
+ </span>
+ </td>
+ <td class\$="message [[_hideIfBranch(detailType)]]">
+ [[_computeMessage(item.message)]]
+ </td>
+ <td class\$="tagger [[_hideIfBranch(detailType)]]">
+ <div class\$="tagger [[_computeHideTagger(item.tagger)]]">
+ <gr-account-link account="[[item.tagger]]">
+ </gr-account-link>
+ (<gr-date-formatter has-tooltip="" date-str="[[item.tagger.date]]">
+ </gr-date-formatter>)
+ </div>
+ </td>
+ <td class="repositoryBrowser">
+ <template is="dom-repeat" items="[[_computeWeblink(item)]]" as="link">
+ <a href\$="[[link.url]]" class="webLink" rel="noopener" target="_blank">
+ ([[link.name]])
+ </a>
+ </template>
+ </td>
+ <td class="delete">
+ <gr-button link="" class\$="deleteButton [[_computeHideDeleteClass(_isOwner, item.can_delete)]]" on-click="_handleDeleteItem">
+ Delete
+ </gr-button>
+ </td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ <gr-overlay id="overlay" with-backdrop="">
+ <gr-confirm-delete-item-dialog class="confirmDialog" on-confirm="_handleDeleteItemConfirm" on-cancel="_handleConfirmDialogCancel" item="[[_refName]]" item-type="[[detailType]]"></gr-confirm-delete-item-dialog>
+ </gr-overlay>
+ </gr-list-view>
+ <gr-overlay id="createOverlay" with-backdrop="">
+ <gr-dialog id="createDialog" disabled="[[!_hasNewItemName]]" confirm-label="Create" on-confirm="_handleCreateItem" on-cancel="_handleCloseCreate">
+ <div class="header" slot="header">
+ Create [[_computeItemName(detailType)]]
+ </div>
+ <div class="main" slot="main">
+ <gr-create-pointer-dialog id="createNewModal" detail-type="[[_computeItemName(detailType)]]" has-new-item-name="{{_hasNewItemName}}" item-detail="[[detailType]]" repo-name="[[_repo]]"></gr-create-pointer-dialog>
+ </div>
+ </gr-dialog>
+ </gr-overlay>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
index d466f28..27a3498 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
@@ -19,16 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-detail-list</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-repo-detail-list.html">
-
-<script>void(0);</script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,533 +31,535 @@
</template>
</test-fixture>
-<script>
- let counter;
- const branchGenerator = () => {
- return {
- ref: `refs/heads/test${++counter}`,
- revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d',
- web_links: [
- {
- name: 'diffusion',
- url: `https://git.example.org/branch/test;refs/heads/test${counter}`,
- },
- ],
- };
- };
- const tagGenerator = () => {
- return {
- ref: `refs/tags/test${++counter}`,
- revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d',
- web_links: [
- {
- name: 'diffusion',
- url: `https://git.example.org/tag/test;refs/tags/test${counter}`,
- },
- ],
- message: 'Annotated tag',
- tagger: {
- name: 'Test User',
- email: 'test.user@gmail.com',
- date: '2017-09-19 14:54:00.000000000',
- tz: 540,
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-repo-detail-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+let counter;
+const branchGenerator = () => {
+ return {
+ ref: `refs/heads/test${++counter}`,
+ revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d',
+ web_links: [
+ {
+ name: 'diffusion',
+ url: `https://git.example.org/branch/test;refs/heads/test${counter}`,
},
- };
+ ],
};
+};
+const tagGenerator = () => {
+ return {
+ ref: `refs/tags/test${++counter}`,
+ revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d',
+ web_links: [
+ {
+ name: 'diffusion',
+ url: `https://git.example.org/tag/test;refs/tags/test${counter}`,
+ },
+ ],
+ message: 'Annotated tag',
+ tagger: {
+ name: 'Test User',
+ email: 'test.user@gmail.com',
+ date: '2017-09-19 14:54:00.000000000',
+ tz: 540,
+ },
+ };
+};
- suite('gr-repo-detail-list', async () => {
- await readyToTest();
- suite('Branches', () => {
- let element;
- let branches;
- let sandbox;
+suite('gr-repo-detail-list', () => {
+ suite('Branches', () => {
+ let element;
+ let branches;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.detailType = 'branches';
- counter = 0;
- sandbox.stub(page, 'show');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.detailType = 'branches';
+ counter = 0;
+ sandbox.stub(page, 'show');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('list of repo branches', () => {
+ setup(done => {
+ branches = [{
+ ref: 'HEAD',
+ revision: 'master',
+ }].concat(_.times(25, branchGenerator));
+
+ stub('gr-rest-api-interface', {
+ getRepoBranches(num, project, offset) {
+ return Promise.resolve(branches);
+ },
+ });
+
+ const params = {
+ repo: 'test',
+ detail: 'branches',
+ };
+
+ element._paramsChanged(params).then(() => { flush(done); });
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('list of repo branches', () => {
- setup(done => {
- branches = [{
- ref: 'HEAD',
- revision: 'master',
- }].concat(_.times(25, branchGenerator));
-
- stub('gr-rest-api-interface', {
- getRepoBranches(num, project, offset) {
- return Promise.resolve(branches);
- },
- });
-
- const params = {
- repo: 'test',
- detail: 'branches',
- };
-
- element._paramsChanged(params).then(() => { flush(done); });
- });
-
- test('test for branch in the list', done => {
- flush(() => {
- assert.equal(element._items[2].ref, 'refs/heads/test2');
- done();
- });
- });
-
- test('test for web links in the branches list', done => {
- flush(() => {
- assert.equal(element._items[2].web_links[0].url,
- 'https://git.example.org/branch/test;refs/heads/test2');
- done();
- });
- });
-
- test('test for refs/heads/ being striped from ref', done => {
- flush(() => {
- assert.equal(element._stripRefs(element._items[2].ref,
- element.detailType), 'test2');
- done();
- });
- });
-
- test('_shownItems', () => {
- assert.equal(element._shownItems.length, 25);
- });
-
- test('Edit HEAD button not admin', done => {
- sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
- sandbox.stub(element.$.restAPI, 'getRepoAccess').returns(
- Promise.resolve({
- test: {is_owner: false},
- }));
- element._determineIfOwner('test').then(() => {
- assert.equal(element._isOwner, false);
- assert.equal(getComputedStyle(Polymer.dom(element.root)
- .querySelector('.revisionNoEditing')).display, 'inline');
- assert.equal(getComputedStyle(Polymer.dom(element.root)
- .querySelector('.revisionEdit')).display, 'none');
- done();
- });
- });
-
- test('Edit HEAD button admin', done => {
- const saveBtn = Polymer.dom(element.root).querySelector('.saveBtn');
- const cancelBtn = Polymer.dom(element.root).querySelector('.cancelBtn');
- const editBtn = Polymer.dom(element.root).querySelector('.editBtn');
- const revisionNoEditing = Polymer.dom(element.root)
- .querySelector('.revisionNoEditing');
- const revisionWithEditing = Polymer.dom(element.root)
- .querySelector('.revisionWithEditing');
-
- sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
- sandbox.stub(element.$.restAPI, 'getRepoAccess').returns(
- Promise.resolve({
- test: {is_owner: true},
- }));
- sandbox.stub(element, '_handleSaveRevision');
- element._determineIfOwner('test').then(() => {
- assert.equal(element._isOwner, true);
- // The revision container for non-editing enabled row is not visible.
- assert.equal(getComputedStyle(revisionNoEditing).display, 'none');
-
- // The revision container for editing enabled row is visible.
- assert.notEqual(getComputedStyle(Polymer.dom(element.root)
- .querySelector('.revisionEdit')).display, 'none');
-
- // The revision and edit button are visible.
- assert.notEqual(getComputedStyle(revisionWithEditing).display,
- 'none');
- assert.notEqual(getComputedStyle(editBtn).display, 'none');
-
- // The input, cancel, and save buttons are not visible.
- const hiddenElements = Polymer.dom(element.root)
- .querySelectorAll('.canEdit .editItem');
-
- for (const item of hiddenElements) {
- assert.equal(getComputedStyle(item).display, 'none');
- }
-
- MockInteractions.tap(editBtn);
- flushAsynchronousOperations();
- // The revision and edit button are not visible.
- assert.equal(getComputedStyle(revisionWithEditing).display, 'none');
- assert.equal(getComputedStyle(editBtn).display, 'none');
-
- // The input, cancel, and save buttons are not visible.
- for (const item of hiddenElements) {
- assert.notEqual(getComputedStyle(item).display, 'none');
- }
-
- // The revised ref was set correctly
- assert.equal(element._revisedRef, 'master');
-
- assert.isFalse(saveBtn.disabled);
-
- // Delete the ref.
- element._revisedRef = '';
- assert.isTrue(saveBtn.disabled);
-
- // Change the ref to something else
- element._revisedRef = 'newRef';
- element._repo = 'test';
- assert.isFalse(saveBtn.disabled);
-
- // Save button calls handleSave. since this is stubbed, the edit
- // section remains open.
- MockInteractions.tap(saveBtn);
- assert.isTrue(element._handleSaveRevision.called);
-
- // When cancel is tapped, the edit secion closes.
- MockInteractions.tap(cancelBtn);
- flushAsynchronousOperations();
-
- // The revision and edit button are visible.
- assert.notEqual(getComputedStyle(revisionWithEditing).display,
- 'none');
- assert.notEqual(getComputedStyle(editBtn).display, 'none');
-
- // The input, cancel, and save buttons are not visible.
- for (const item of hiddenElements) {
- assert.equal(getComputedStyle(item).display, 'none');
- }
- done();
- });
- });
-
- test('_handleSaveRevision with invalid rev', done => {
- const event = {model: {set: sandbox.stub()}};
- element._isEditing = true;
- sandbox.stub(element.$.restAPI, 'setRepoHead').returns(
- Promise.resolve({
- status: 400,
- })
- );
-
- element._setRepoHead('test', 'newRef', event).then(() => {
- assert.isTrue(element._isEditing);
- assert.isFalse(event.model.set.called);
- done();
- });
- });
-
- test('_handleSaveRevision with valid rev', done => {
- const event = {model: {set: sandbox.stub()}};
- element._isEditing = true;
- sandbox.stub(element.$.restAPI, 'setRepoHead').returns(
- Promise.resolve({
- status: 200,
- })
- );
-
- element._setRepoHead('test', 'newRef', event).then(() => {
- assert.isFalse(element._isEditing);
- assert.isTrue(event.model.set.called);
- done();
- });
- });
-
- test('test _computeItemName', () => {
- assert.deepEqual(element._computeItemName('branches'), 'Branch');
- assert.deepEqual(element._computeItemName('tags'), 'Tag');
+ test('test for branch in the list', done => {
+ flush(() => {
+ assert.equal(element._items[2].ref, 'refs/heads/test2');
+ done();
});
});
- suite('list with less then 25 branches', () => {
- setup(done => {
- branches = _.times(25, branchGenerator);
-
- stub('gr-rest-api-interface', {
- getRepoBranches(num, repo, offset) {
- return Promise.resolve(branches);
- },
- });
-
- const params = {
- repo: 'test',
- detail: 'branches',
- };
-
- element._paramsChanged(params).then(() => { flush(done); });
- });
-
- test('_shownItems', () => {
- assert.equal(element._shownItems.length, 25);
+ test('test for web links in the branches list', done => {
+ flush(() => {
+ assert.equal(element._items[2].web_links[0].url,
+ 'https://git.example.org/branch/test;refs/heads/test2');
+ done();
});
});
- suite('filter', () => {
- test('_paramsChanged', done => {
- sandbox.stub(
- element.$.restAPI,
- 'getRepoBranches',
- () => Promise.resolve(branches));
- const params = {
- detail: 'branches',
- repo: 'test',
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(params).then(() => {
- assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[0],
- 'test');
- assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[1],
- 'test');
- assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[2],
- 25);
- assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[3],
- 25);
- done();
- });
+ test('test for refs/heads/ being striped from ref', done => {
+ flush(() => {
+ assert.equal(element._stripRefs(element._items[2].ref,
+ element.detailType), 'test2');
+ done();
});
});
- suite('404', () => {
- test('fires page-error', done => {
- const response = {status: 404};
- sandbox.stub(element.$.restAPI, 'getRepoBranches',
- (filter, repo, reposBranchesPerPage, opt_offset, errFn) => {
- errFn(response);
- });
+ test('_shownItems', () => {
+ assert.equal(element._shownItems.length, 25);
+ });
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
+ test('Edit HEAD button not admin', done => {
+ sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+ sandbox.stub(element.$.restAPI, 'getRepoAccess').returns(
+ Promise.resolve({
+ test: {is_owner: false},
+ }));
+ element._determineIfOwner('test').then(() => {
+ assert.equal(element._isOwner, false);
+ assert.equal(getComputedStyle(dom(element.root)
+ .querySelector('.revisionNoEditing')).display, 'inline');
+ assert.equal(getComputedStyle(dom(element.root)
+ .querySelector('.revisionEdit')).display, 'none');
+ done();
+ });
+ });
- const params = {
- detail: 'branches',
- repo: 'test',
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(params);
+ test('Edit HEAD button admin', done => {
+ const saveBtn = dom(element.root).querySelector('.saveBtn');
+ const cancelBtn = dom(element.root).querySelector('.cancelBtn');
+ const editBtn = dom(element.root).querySelector('.editBtn');
+ const revisionNoEditing = dom(element.root)
+ .querySelector('.revisionNoEditing');
+ const revisionWithEditing = dom(element.root)
+ .querySelector('.revisionWithEditing');
+
+ sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+ sandbox.stub(element.$.restAPI, 'getRepoAccess').returns(
+ Promise.resolve({
+ test: {is_owner: true},
+ }));
+ sandbox.stub(element, '_handleSaveRevision');
+ element._determineIfOwner('test').then(() => {
+ assert.equal(element._isOwner, true);
+ // The revision container for non-editing enabled row is not visible.
+ assert.equal(getComputedStyle(revisionNoEditing).display, 'none');
+
+ // The revision container for editing enabled row is visible.
+ assert.notEqual(getComputedStyle(dom(element.root)
+ .querySelector('.revisionEdit')).display, 'none');
+
+ // The revision and edit button are visible.
+ assert.notEqual(getComputedStyle(revisionWithEditing).display,
+ 'none');
+ assert.notEqual(getComputedStyle(editBtn).display, 'none');
+
+ // The input, cancel, and save buttons are not visible.
+ const hiddenElements = dom(element.root)
+ .querySelectorAll('.canEdit .editItem');
+
+ for (const item of hiddenElements) {
+ assert.equal(getComputedStyle(item).display, 'none');
+ }
+
+ MockInteractions.tap(editBtn);
+ flushAsynchronousOperations();
+ // The revision and edit button are not visible.
+ assert.equal(getComputedStyle(revisionWithEditing).display, 'none');
+ assert.equal(getComputedStyle(editBtn).display, 'none');
+
+ // The input, cancel, and save buttons are not visible.
+ for (const item of hiddenElements) {
+ assert.notEqual(getComputedStyle(item).display, 'none');
+ }
+
+ // The revised ref was set correctly
+ assert.equal(element._revisedRef, 'master');
+
+ assert.isFalse(saveBtn.disabled);
+
+ // Delete the ref.
+ element._revisedRef = '';
+ assert.isTrue(saveBtn.disabled);
+
+ // Change the ref to something else
+ element._revisedRef = 'newRef';
+ element._repo = 'test';
+ assert.isFalse(saveBtn.disabled);
+
+ // Save button calls handleSave. since this is stubbed, the edit
+ // section remains open.
+ MockInteractions.tap(saveBtn);
+ assert.isTrue(element._handleSaveRevision.called);
+
+ // When cancel is tapped, the edit secion closes.
+ MockInteractions.tap(cancelBtn);
+ flushAsynchronousOperations();
+
+ // The revision and edit button are visible.
+ assert.notEqual(getComputedStyle(revisionWithEditing).display,
+ 'none');
+ assert.notEqual(getComputedStyle(editBtn).display, 'none');
+
+ // The input, cancel, and save buttons are not visible.
+ for (const item of hiddenElements) {
+ assert.equal(getComputedStyle(item).display, 'none');
+ }
+ done();
+ });
+ });
+
+ test('_handleSaveRevision with invalid rev', done => {
+ const event = {model: {set: sandbox.stub()}};
+ element._isEditing = true;
+ sandbox.stub(element.$.restAPI, 'setRepoHead').returns(
+ Promise.resolve({
+ status: 400,
+ })
+ );
+
+ element._setRepoHead('test', 'newRef', event).then(() => {
+ assert.isTrue(element._isEditing);
+ assert.isFalse(event.model.set.called);
+ done();
+ });
+ });
+
+ test('_handleSaveRevision with valid rev', done => {
+ const event = {model: {set: sandbox.stub()}};
+ element._isEditing = true;
+ sandbox.stub(element.$.restAPI, 'setRepoHead').returns(
+ Promise.resolve({
+ status: 200,
+ })
+ );
+
+ element._setRepoHead('test', 'newRef', event).then(() => {
+ assert.isFalse(element._isEditing);
+ assert.isTrue(event.model.set.called);
+ done();
+ });
+ });
+
+ test('test _computeItemName', () => {
+ assert.deepEqual(element._computeItemName('branches'), 'Branch');
+ assert.deepEqual(element._computeItemName('tags'), 'Tag');
+ });
+ });
+
+ suite('list with less then 25 branches', () => {
+ setup(done => {
+ branches = _.times(25, branchGenerator);
+
+ stub('gr-rest-api-interface', {
+ getRepoBranches(num, repo, offset) {
+ return Promise.resolve(branches);
+ },
+ });
+
+ const params = {
+ repo: 'test',
+ detail: 'branches',
+ };
+
+ element._paramsChanged(params).then(() => { flush(done); });
+ });
+
+ test('_shownItems', () => {
+ assert.equal(element._shownItems.length, 25);
+ });
+ });
+
+ suite('filter', () => {
+ test('_paramsChanged', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'getRepoBranches',
+ () => Promise.resolve(branches));
+ const params = {
+ detail: 'branches',
+ repo: 'test',
+ filter: 'test',
+ offset: 25,
+ };
+ element._paramsChanged(params).then(() => {
+ assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[0],
+ 'test');
+ assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[1],
+ 'test');
+ assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[2],
+ 25);
+ assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[3],
+ 25);
+ done();
});
});
});
- suite('Tags', () => {
- let element;
- let tags;
- let sandbox;
+ suite('404', () => {
+ test('fires page-error', done => {
+ const response = {status: 404};
+ sandbox.stub(element.$.restAPI, 'getRepoBranches',
+ (filter, repo, reposBranchesPerPage, opt_offset, errFn) => {
+ errFn(response);
+ });
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.detailType = 'tags';
- counter = 0;
- sandbox.stub(page, 'show');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('_computeMessage', () => {
- let message = 'v2.15-rc1↵-----BEGIN PGP SIGNATURE-----↵Version: GnuPG v' +
- '1↵↵iQIcBAABAgAGBQJZ27O7AAoJEF/XxZqaEoiMy6kQAMoQCpGr3J6JITI4BVWsr7QM↵xy' +
- 'EcWH5YPUko5EPTbkABHmaVyFmKGkuIQdn6c+NIbqJOk+5XT4oUyRSo1T569HPJ↵3kyxEJi' +
- 'T1ryvp5BIHwdvHx58fjw1+YkiWLZuZq1FFkUYqnWTYCrkv7Fok98pdOmV↵CL1Hgugi5uK8' +
- '/kxf1M7+Nv6piaZ140pwSb1h6QdAjaZVfaBCnoxlG4LRUqHvEYay↵f4QYgFT67auHIGkZ4' +
- 'moUcsp2Du/1jSsCWL/CPwjPFGbbckVAjLCMT9yD3NKwpEZF↵pfsiZyHI9dL0M+QjVrM+RD' +
- 'HwIIJwra8R0IMkDlQ6MDrFlKNqNBbo588S6UPrm71L↵YuiwWlcrK9ZIybxT6LzbR65Rvez' +
- 'DSitQ+xeIfpZE19/X6BCnvlARLE8k/tC2JksI↵lEZi7Lf3FQdIcwwyt98tJkS9HX9v9jbC' +
- '5QXifnoj3Li8tHSLuQ1dJCxHQiis6ojI↵OWUFkm0IHBXVNHA2dqYBdM+pL12mlI3wp6Ica' +
- '4cdEVDwzu+j1xnVSFUa+d+Y2xJF↵7mytuyhHiKG4hm+zbhMv6WD8Q3FoDsJZeLY99l0hYQ' +
- 'SnnkMduFVroIs45pAs8gUA↵RvYla8mm9w/543IJAPzzFarPVLSsSyQ7tJl3UBzjKRNH/rX' +
- 'W+F22qyWD1zyHPUIR↵C00ItmwlAvveImYKpQAH↵=L+K9↵-----END PGP SIGNATURE---' +
- '--';
- assert.equal(element._computeMessage(message), 'v2.15-rc1↵');
- message = 'v2.15-rc1';
- assert.equal(element._computeMessage(message), 'v2.15-rc1');
- });
-
- suite('list of repo tags', () => {
- setup(done => {
- tags = _.times(26, tagGenerator);
-
- stub('gr-rest-api-interface', {
- getRepoTags(num, repo, offset) {
- return Promise.resolve(tags);
- },
- });
-
- const params = {
- repo: 'test',
- detail: 'tags',
- };
-
- element._paramsChanged(params).then(() => { flush(done); });
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
});
- test('test for tag in the list', done => {
- flush(() => {
- assert.equal(element._items[1].ref, 'refs/tags/test2');
- done();
- });
- });
-
- test('test for tag message in the list', done => {
- flush(() => {
- assert.equal(element._items[1].message, 'Annotated tag');
- done();
- });
- });
-
- test('test for tagger in the tag list', done => {
- const tagger = {
- name: 'Test User',
- email: 'test.user@gmail.com',
- date: '2017-09-19 14:54:00.000000000',
- tz: 540,
- };
- flush(() => {
- assert.deepEqual(element._items[1].tagger, tagger);
- done();
- });
- });
-
- test('test for web links in the tags list', done => {
- flush(() => {
- assert.equal(element._items[1].web_links[0].url,
- 'https://git.example.org/tag/test;refs/tags/test2');
- done();
- });
- });
-
- test('test for refs/tags/ being striped from ref', done => {
- flush(() => {
- assert.equal(element._stripRefs(element._items[1].ref,
- element.detailType), 'test2');
- done();
- });
- });
-
- test('_shownItems', () => {
- assert.equal(element._shownItems.length, 25);
- });
-
- test('_computeHideTagger', () => {
- const testObject1 = {
- tagger: 'test',
- };
- assert.equal(element._computeHideTagger(testObject1), '');
-
- assert.equal(element._computeHideTagger(undefined), 'hide');
- });
- });
-
- suite('list with less then 25 tags', () => {
- setup(done => {
- tags = _.times(25, tagGenerator);
-
- stub('gr-rest-api-interface', {
- getRepoTags(num, project, offset) {
- return Promise.resolve(tags);
- },
- });
-
- const params = {
- repo: 'test',
- detail: 'tags',
- };
-
- element._paramsChanged(params).then(() => { flush(done); });
- });
-
- test('_shownItems', () => {
- assert.equal(element._shownItems.length, 25);
- });
- });
-
- suite('filter', () => {
- test('_paramsChanged', done => {
- sandbox.stub(
- element.$.restAPI,
- 'getRepoTags',
- () => Promise.resolve(tags));
- const params = {
- repo: 'test',
- detail: 'tags',
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(params).then(() => {
- assert.equal(element.$.restAPI.getRepoTags.lastCall.args[0],
- 'test');
- assert.equal(element.$.restAPI.getRepoTags.lastCall.args[1],
- 'test');
- assert.equal(element.$.restAPI.getRepoTags.lastCall.args[2],
- 25);
- assert.equal(element.$.restAPI.getRepoTags.lastCall.args[3],
- 25);
- done();
- });
- });
- });
-
- suite('create new', () => {
- test('_handleCreateClicked called when create-click fired', () => {
- sandbox.stub(element, '_handleCreateClicked');
- element.shadowRoot
- .querySelector('gr-list-view').fire('create-clicked');
- assert.isTrue(element._handleCreateClicked.called);
- });
-
- test('_handleCreateClicked opens modal', () => {
- const openStub = sandbox.stub(element.$.createOverlay, 'open');
- element._handleCreateClicked();
- assert.isTrue(openStub.called);
- });
-
- test('_handleCreateItem called when confirm fired', () => {
- sandbox.stub(element, '_handleCreateItem');
- element.$.createDialog.fire('confirm');
- assert.isTrue(element._handleCreateItem.called);
- });
-
- test('_handleCloseCreate called when cancel fired', () => {
- sandbox.stub(element, '_handleCloseCreate');
- element.$.createDialog.fire('cancel');
- assert.isTrue(element._handleCloseCreate.called);
- });
- });
-
- suite('404', () => {
- test('fires page-error', done => {
- const response = {status: 404};
- sandbox.stub(element.$.restAPI, 'getRepoTags',
- (filter, repo, reposTagsPerPage, opt_offset, errFn) => {
- errFn(response);
- });
-
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- const params = {
- repo: 'test',
- detail: 'tags',
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(params);
- });
- });
-
- test('test _computeHideDeleteClass', () => {
- assert.deepEqual(element._computeHideDeleteClass(true, false), 'show');
- assert.deepEqual(element._computeHideDeleteClass(false, true), 'show');
- assert.deepEqual(element._computeHideDeleteClass(false, false), '');
+ const params = {
+ detail: 'branches',
+ repo: 'test',
+ filter: 'test',
+ offset: 25,
+ };
+ element._paramsChanged(params);
});
});
});
+
+ suite('Tags', () => {
+ let element;
+ let tags;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.detailType = 'tags';
+ counter = 0;
+ sandbox.stub(page, 'show');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_computeMessage', () => {
+ let message = 'v2.15-rc1↵-----BEGIN PGP SIGNATURE-----↵Version: GnuPG v' +
+ '1↵↵iQIcBAABAgAGBQJZ27O7AAoJEF/XxZqaEoiMy6kQAMoQCpGr3J6JITI4BVWsr7QM↵xy' +
+ 'EcWH5YPUko5EPTbkABHmaVyFmKGkuIQdn6c+NIbqJOk+5XT4oUyRSo1T569HPJ↵3kyxEJi' +
+ 'T1ryvp5BIHwdvHx58fjw1+YkiWLZuZq1FFkUYqnWTYCrkv7Fok98pdOmV↵CL1Hgugi5uK8' +
+ '/kxf1M7+Nv6piaZ140pwSb1h6QdAjaZVfaBCnoxlG4LRUqHvEYay↵f4QYgFT67auHIGkZ4' +
+ 'moUcsp2Du/1jSsCWL/CPwjPFGbbckVAjLCMT9yD3NKwpEZF↵pfsiZyHI9dL0M+QjVrM+RD' +
+ 'HwIIJwra8R0IMkDlQ6MDrFlKNqNBbo588S6UPrm71L↵YuiwWlcrK9ZIybxT6LzbR65Rvez' +
+ 'DSitQ+xeIfpZE19/X6BCnvlARLE8k/tC2JksI↵lEZi7Lf3FQdIcwwyt98tJkS9HX9v9jbC' +
+ '5QXifnoj3Li8tHSLuQ1dJCxHQiis6ojI↵OWUFkm0IHBXVNHA2dqYBdM+pL12mlI3wp6Ica' +
+ '4cdEVDwzu+j1xnVSFUa+d+Y2xJF↵7mytuyhHiKG4hm+zbhMv6WD8Q3FoDsJZeLY99l0hYQ' +
+ 'SnnkMduFVroIs45pAs8gUA↵RvYla8mm9w/543IJAPzzFarPVLSsSyQ7tJl3UBzjKRNH/rX' +
+ 'W+F22qyWD1zyHPUIR↵C00ItmwlAvveImYKpQAH↵=L+K9↵-----END PGP SIGNATURE---' +
+ '--';
+ assert.equal(element._computeMessage(message), 'v2.15-rc1↵');
+ message = 'v2.15-rc1';
+ assert.equal(element._computeMessage(message), 'v2.15-rc1');
+ });
+
+ suite('list of repo tags', () => {
+ setup(done => {
+ tags = _.times(26, tagGenerator);
+
+ stub('gr-rest-api-interface', {
+ getRepoTags(num, repo, offset) {
+ return Promise.resolve(tags);
+ },
+ });
+
+ const params = {
+ repo: 'test',
+ detail: 'tags',
+ };
+
+ element._paramsChanged(params).then(() => { flush(done); });
+ });
+
+ test('test for tag in the list', done => {
+ flush(() => {
+ assert.equal(element._items[1].ref, 'refs/tags/test2');
+ done();
+ });
+ });
+
+ test('test for tag message in the list', done => {
+ flush(() => {
+ assert.equal(element._items[1].message, 'Annotated tag');
+ done();
+ });
+ });
+
+ test('test for tagger in the tag list', done => {
+ const tagger = {
+ name: 'Test User',
+ email: 'test.user@gmail.com',
+ date: '2017-09-19 14:54:00.000000000',
+ tz: 540,
+ };
+ flush(() => {
+ assert.deepEqual(element._items[1].tagger, tagger);
+ done();
+ });
+ });
+
+ test('test for web links in the tags list', done => {
+ flush(() => {
+ assert.equal(element._items[1].web_links[0].url,
+ 'https://git.example.org/tag/test;refs/tags/test2');
+ done();
+ });
+ });
+
+ test('test for refs/tags/ being striped from ref', done => {
+ flush(() => {
+ assert.equal(element._stripRefs(element._items[1].ref,
+ element.detailType), 'test2');
+ done();
+ });
+ });
+
+ test('_shownItems', () => {
+ assert.equal(element._shownItems.length, 25);
+ });
+
+ test('_computeHideTagger', () => {
+ const testObject1 = {
+ tagger: 'test',
+ };
+ assert.equal(element._computeHideTagger(testObject1), '');
+
+ assert.equal(element._computeHideTagger(undefined), 'hide');
+ });
+ });
+
+ suite('list with less then 25 tags', () => {
+ setup(done => {
+ tags = _.times(25, tagGenerator);
+
+ stub('gr-rest-api-interface', {
+ getRepoTags(num, project, offset) {
+ return Promise.resolve(tags);
+ },
+ });
+
+ const params = {
+ repo: 'test',
+ detail: 'tags',
+ };
+
+ element._paramsChanged(params).then(() => { flush(done); });
+ });
+
+ test('_shownItems', () => {
+ assert.equal(element._shownItems.length, 25);
+ });
+ });
+
+ suite('filter', () => {
+ test('_paramsChanged', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'getRepoTags',
+ () => Promise.resolve(tags));
+ const params = {
+ repo: 'test',
+ detail: 'tags',
+ filter: 'test',
+ offset: 25,
+ };
+ element._paramsChanged(params).then(() => {
+ assert.equal(element.$.restAPI.getRepoTags.lastCall.args[0],
+ 'test');
+ assert.equal(element.$.restAPI.getRepoTags.lastCall.args[1],
+ 'test');
+ assert.equal(element.$.restAPI.getRepoTags.lastCall.args[2],
+ 25);
+ assert.equal(element.$.restAPI.getRepoTags.lastCall.args[3],
+ 25);
+ done();
+ });
+ });
+ });
+
+ suite('create new', () => {
+ test('_handleCreateClicked called when create-click fired', () => {
+ sandbox.stub(element, '_handleCreateClicked');
+ element.shadowRoot
+ .querySelector('gr-list-view').fire('create-clicked');
+ assert.isTrue(element._handleCreateClicked.called);
+ });
+
+ test('_handleCreateClicked opens modal', () => {
+ const openStub = sandbox.stub(element.$.createOverlay, 'open');
+ element._handleCreateClicked();
+ assert.isTrue(openStub.called);
+ });
+
+ test('_handleCreateItem called when confirm fired', () => {
+ sandbox.stub(element, '_handleCreateItem');
+ element.$.createDialog.fire('confirm');
+ assert.isTrue(element._handleCreateItem.called);
+ });
+
+ test('_handleCloseCreate called when cancel fired', () => {
+ sandbox.stub(element, '_handleCloseCreate');
+ element.$.createDialog.fire('cancel');
+ assert.isTrue(element._handleCloseCreate.called);
+ });
+ });
+
+ suite('404', () => {
+ test('fires page-error', done => {
+ const response = {status: 404};
+ sandbox.stub(element.$.restAPI, 'getRepoTags',
+ (filter, repo, reposTagsPerPage, opt_offset, errFn) => {
+ errFn(response);
+ });
+
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
+ });
+
+ const params = {
+ repo: 'test',
+ detail: 'tags',
+ filter: 'test',
+ offset: 25,
+ };
+ element._paramsChanged(params);
+ });
+ });
+
+ test('test _computeHideDeleteClass', () => {
+ assert.deepEqual(element._computeHideDeleteClass(true, false), 'show');
+ assert.deepEqual(element._computeHideDeleteClass(false, true), 'show');
+ assert.deepEqual(element._computeHideDeleteClass(false, false), '');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.html b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.html
deleted file mode 100644
index 08fd45c..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.html
+++ /dev/null
@@ -1,117 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../styles/gr-table-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-create-repo-dialog/gr-create-repo-dialog.html">
-
-<dom-module id="gr-repo-list">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-table-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style>
- .genericList tr td:last-of-type {
- text-align: left;
- }
- .genericList tr th:last-of-type {
- text-align: left;
- }
- .readOnly {
- text-align: center;
- }
- .changesLink, .name, .repositoryBrowser, .readOnly {
- white-space:nowrap;
- }
- </style>
- <gr-list-view
- create-new=[[_createNewCapability]]
- filter="[[_filter]]"
- items-per-page="[[_reposPerPage]]"
- items="[[_repos]]"
- loading="[[_loading]]"
- offset="[[_offset]]"
- on-create-clicked="_handleCreateClicked"
- path="[[_path]]">
- <table id="list" class="genericList">
- <tr class="headerRow">
- <th class="name topHeader">Repository Name</th>
- <th class="repositoryBrowser topHeader">Repository Browser</th>
- <th class="changesLink topHeader">Changes</th>
- <th class="topHeader readOnly">Read only</th>
- <th class="description topHeader">Repository Description</th>
- </tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
- <td>Loading...</td>
- </tr>
- <tbody class$="[[computeLoadingClass(_loading)]]">
- <template is="dom-repeat" items="[[_shownRepos]]">
- <tr class="table">
- <td class="name">
- <a href$="[[_computeRepoUrl(item.name)]]">[[item.name]]</a>
- </td>
- <td class="repositoryBrowser">
- <template is="dom-repeat"
- items="[[_computeWeblink(item)]]" as="link">
- <a href$="[[link.url]]"
- class="webLink"
- rel="noopener"
- target="_blank">
- [[link.name]]
- </a>
- </template>
- </td>
- <td class="changesLink"><a href$="[[_computeChangesLink(item.name)]]">view all</a></td>
- <td class="readOnly">[[_readOnly(item)]]</td>
- <td class="description">[[item.description]]</td>
- </tr>
- </template>
- </tbody>
- </table>
- </gr-list-view>
- <gr-overlay id="createOverlay" with-backdrop>
- <gr-dialog
- id="createDialog"
- class="confirmDialog"
- disabled="[[!_hasNewRepoName]]"
- confirm-label="Create"
- on-confirm="_handleCreateRepo"
- on-cancel="_handleCloseCreate">
- <div class="header" slot="header">
- Create Repository
- </div>
- <div class="main" slot="main">
- <gr-create-repo-dialog
- has-new-repo-name="{{_hasNewRepoName}}"
- params="[[params]]"
- id="createNewModal"></gr-create-repo-dialog>
- </div>
- </gr-dialog>
- </gr-overlay>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-repo-list.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
index c509717..24cc9a8 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
@@ -14,160 +14,174 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
+import '../../../styles/gr-table-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-list-view/gr-list-view.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-create-repo-dialog/gr-create-repo-dialog.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-repo-list_html.js';
+
+/**
+ * @appliesMixin Gerrit.ListViewMixin
+ * @extends Polymer.Element
+ */
+class GrRepoList extends mixinBehaviors( [
+ Gerrit.ListViewBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-repo-list'; }
+
+ static get properties() {
+ return {
+ /**
+ * URL params passed from the router.
+ */
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
+
+ /**
+ * Offset of currently visible query results.
+ */
+ _offset: Number,
+ _path: {
+ type: String,
+ readOnly: true,
+ value: '/admin/repos',
+ },
+ _hasNewRepoName: Boolean,
+ _createNewCapability: {
+ type: Boolean,
+ value: false,
+ },
+ _repos: Array,
+
+ /**
+ * Because we request one more than the projectsPerPage, _shownProjects
+ * maybe one less than _projects.
+ * */
+ _shownRepos: {
+ type: Array,
+ computed: 'computeShownItems(_repos)',
+ },
+
+ _reposPerPage: {
+ type: Number,
+ value: 25,
+ },
+
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _filter: {
+ type: String,
+ value: '',
+ },
+ };
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._getCreateRepoCapability();
+ this.fire('title-change', {title: 'Repos'});
+ this._maybeOpenCreateOverlay(this.params);
+ }
+
+ _paramsChanged(params) {
+ this._loading = true;
+ this._filter = this.getFilterValue(params);
+ this._offset = this.getOffsetValue(params);
+
+ return this._getRepos(this._filter, this._reposPerPage,
+ this._offset);
+ }
/**
- * @appliesMixin Gerrit.ListViewMixin
- * @extends Polymer.Element
+ * Opens the create overlay if the route has a hash 'create'
+ *
+ * @param {!Object} params
*/
- class GrRepoList extends Polymer.mixinBehaviors( [
- Gerrit.ListViewBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-repo-list'; }
-
- static get properties() {
- return {
- /**
- * URL params passed from the router.
- */
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
-
- /**
- * Offset of currently visible query results.
- */
- _offset: Number,
- _path: {
- type: String,
- readOnly: true,
- value: '/admin/repos',
- },
- _hasNewRepoName: Boolean,
- _createNewCapability: {
- type: Boolean,
- value: false,
- },
- _repos: Array,
-
- /**
- * Because we request one more than the projectsPerPage, _shownProjects
- * maybe one less than _projects.
- * */
- _shownRepos: {
- type: Array,
- computed: 'computeShownItems(_repos)',
- },
-
- _reposPerPage: {
- type: Number,
- value: 25,
- },
-
- _loading: {
- type: Boolean,
- value: true,
- },
- _filter: {
- type: String,
- value: '',
- },
- };
- }
-
- /** @override */
- attached() {
- super.attached();
- this._getCreateRepoCapability();
- this.fire('title-change', {title: 'Repos'});
- this._maybeOpenCreateOverlay(this.params);
- }
-
- _paramsChanged(params) {
- this._loading = true;
- this._filter = this.getFilterValue(params);
- this._offset = this.getOffsetValue(params);
-
- return this._getRepos(this._filter, this._reposPerPage,
- this._offset);
- }
-
- /**
- * Opens the create overlay if the route has a hash 'create'
- *
- * @param {!Object} params
- */
- _maybeOpenCreateOverlay(params) {
- if (params && params.openCreateModal) {
- this.$.createOverlay.open();
- }
- }
-
- _computeRepoUrl(name) {
- return this.getUrl(this._path + '/', name);
- }
-
- _computeChangesLink(name) {
- return Gerrit.Nav.getUrlForProjectChanges(name);
- }
-
- _getCreateRepoCapability() {
- return this.$.restAPI.getAccount().then(account => {
- if (!account) { return; }
- return this.$.restAPI.getAccountCapabilities(['createProject'])
- .then(capabilities => {
- if (capabilities.createProject) {
- this._createNewCapability = true;
- }
- });
- });
- }
-
- _getRepos(filter, reposPerPage, offset) {
- this._repos = [];
- return this.$.restAPI.getRepos(filter, reposPerPage, offset)
- .then(repos => {
- // Late response.
- if (filter !== this._filter || !repos) { return; }
- this._repos = repos;
- this._loading = false;
- });
- }
-
- _refreshReposList() {
- this.$.restAPI.invalidateReposCache();
- return this._getRepos(this._filter, this._reposPerPage,
- this._offset);
- }
-
- _handleCreateRepo() {
- this.$.createNewModal.handleCreateRepo().then(() => {
- this._refreshReposList();
- });
- }
-
- _handleCloseCreate() {
- this.$.createOverlay.close();
- }
-
- _handleCreateClicked() {
+ _maybeOpenCreateOverlay(params) {
+ if (params && params.openCreateModal) {
this.$.createOverlay.open();
}
-
- _readOnly(item) {
- return item.state === 'READ_ONLY' ? 'Y' : '';
- }
-
- _computeWeblink(repo) {
- if (!repo.web_links) { return ''; }
- const webLinks = repo.web_links;
- return webLinks.length ? webLinks : null;
- }
}
- customElements.define(GrRepoList.is, GrRepoList);
-})();
+ _computeRepoUrl(name) {
+ return this.getUrl(this._path + '/', name);
+ }
+
+ _computeChangesLink(name) {
+ return Gerrit.Nav.getUrlForProjectChanges(name);
+ }
+
+ _getCreateRepoCapability() {
+ return this.$.restAPI.getAccount().then(account => {
+ if (!account) { return; }
+ return this.$.restAPI.getAccountCapabilities(['createProject'])
+ .then(capabilities => {
+ if (capabilities.createProject) {
+ this._createNewCapability = true;
+ }
+ });
+ });
+ }
+
+ _getRepos(filter, reposPerPage, offset) {
+ this._repos = [];
+ return this.$.restAPI.getRepos(filter, reposPerPage, offset)
+ .then(repos => {
+ // Late response.
+ if (filter !== this._filter || !repos) { return; }
+ this._repos = repos;
+ this._loading = false;
+ });
+ }
+
+ _refreshReposList() {
+ this.$.restAPI.invalidateReposCache();
+ return this._getRepos(this._filter, this._reposPerPage,
+ this._offset);
+ }
+
+ _handleCreateRepo() {
+ this.$.createNewModal.handleCreateRepo().then(() => {
+ this._refreshReposList();
+ });
+ }
+
+ _handleCloseCreate() {
+ this.$.createOverlay.close();
+ }
+
+ _handleCreateClicked() {
+ this.$.createOverlay.open();
+ }
+
+ _readOnly(item) {
+ return item.state === 'READ_ONLY' ? 'Y' : '';
+ }
+
+ _computeWeblink(repo) {
+ if (!repo.web_links) { return ''; }
+ const webLinks = repo.web_links;
+ return webLinks.length ? webLinks : null;
+ }
+}
+
+customElements.define(GrRepoList.is, GrRepoList);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.js
new file mode 100644
index 0000000..d498869
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.js
@@ -0,0 +1,84 @@
+/**
+ * @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.js';
+
+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-table-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style>
+ .genericList tr td:last-of-type {
+ text-align: left;
+ }
+ .genericList tr th:last-of-type {
+ text-align: left;
+ }
+ .readOnly {
+ text-align: center;
+ }
+ .changesLink, .name, .repositoryBrowser, .readOnly {
+ white-space:nowrap;
+ }
+ </style>
+ <gr-list-view create-new="[[_createNewCapability]]" filter="[[_filter]]" items-per-page="[[_reposPerPage]]" items="[[_repos]]" loading="[[_loading]]" offset="[[_offset]]" on-create-clicked="_handleCreateClicked" path="[[_path]]">
+ <table id="list" class="genericList">
+ <tbody><tr class="headerRow">
+ <th class="name topHeader">Repository Name</th>
+ <th class="repositoryBrowser topHeader">Repository Browser</th>
+ <th class="changesLink topHeader">Changes</th>
+ <th class="topHeader readOnly">Read only</th>
+ <th class="description topHeader">Repository Description</th>
+ </tr>
+ <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]">
+ <td>Loading...</td>
+ </tr>
+ </tbody><tbody class\$="[[computeLoadingClass(_loading)]]">
+ <template is="dom-repeat" items="[[_shownRepos]]">
+ <tr class="table">
+ <td class="name">
+ <a href\$="[[_computeRepoUrl(item.name)]]">[[item.name]]</a>
+ </td>
+ <td class="repositoryBrowser">
+ <template is="dom-repeat" items="[[_computeWeblink(item)]]" as="link">
+ <a href\$="[[link.url]]" class="webLink" rel="noopener" target="_blank">
+ [[link.name]]
+ </a>
+ </template>
+ </td>
+ <td class="changesLink"><a href\$="[[_computeChangesLink(item.name)]]">view all</a></td>
+ <td class="readOnly">[[_readOnly(item)]]</td>
+ <td class="description">[[item.description]]</td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ </gr-list-view>
+ <gr-overlay id="createOverlay" with-backdrop="">
+ <gr-dialog id="createDialog" class="confirmDialog" disabled="[[!_hasNewRepoName]]" confirm-label="Create" on-confirm="_handleCreateRepo" on-cancel="_handleCloseCreate">
+ <div class="header" slot="header">
+ Create Repository
+ </div>
+ <div class="main" slot="main">
+ <gr-create-repo-dialog has-new-repo-name="{{_hasNewRepoName}}" params="[[params]]" id="createNewModal"></gr-create-repo-dialog>
+ </div>
+ </gr-dialog>
+ </gr-overlay>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
index 4003b15..b5991b2 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
@@ -19,16 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-list</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-repo-list.html">
-
-<script>void(0);</script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,166 +31,167 @@
</template>
</test-fixture>
-<script>
- let counter;
- const repoGenerator = () => {
- return {
- id: `test${++counter}`,
- state: 'ACTIVE',
- web_links: [
- {
- name: 'diffusion',
- url: `https://phabricator.example.org/r/project/test${counter}`,
- },
- ],
- };
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-repo-list.js';
+let counter;
+const repoGenerator = () => {
+ return {
+ id: `test${++counter}`,
+ state: 'ACTIVE',
+ web_links: [
+ {
+ name: 'diffusion',
+ url: `https://phabricator.example.org/r/project/test${counter}`,
+ },
+ ],
};
+};
- suite('gr-repo-list tests', async () => {
- await readyToTest();
- let element;
- let repos;
- let sandbox;
- let value;
+suite('gr-repo-list tests', () => {
+ let element;
+ let repos;
+ let sandbox;
+ let value;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(page, 'show');
+ element = fixture('basic');
+ counter = 0;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('list with repos', () => {
+ setup(done => {
+ repos = _.times(26, repoGenerator);
+ stub('gr-rest-api-interface', {
+ getRepos(num, offset) {
+ return Promise.resolve(repos);
+ },
+ });
+ element._paramsChanged(value).then(() => { flush(done); });
+ });
+
+ test('test for test repo in the list', done => {
+ flush(() => {
+ assert.equal(element._repos[1].id, 'test2');
+ done();
+ });
+ });
+
+ test('_shownRepos', () => {
+ assert.equal(element._shownRepos.length, 25);
+ });
+
+ test('_maybeOpenCreateOverlay', () => {
+ const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
+ element._maybeOpenCreateOverlay();
+ assert.isFalse(overlayOpen.called);
+ const params = {};
+ element._maybeOpenCreateOverlay(params);
+ assert.isFalse(overlayOpen.called);
+ params.openCreateModal = true;
+ element._maybeOpenCreateOverlay(params);
+ assert.isTrue(overlayOpen.called);
+ });
+ });
+
+ suite('list with less then 25 repos', () => {
+ setup(done => {
+ repos = _.times(25, repoGenerator);
+
+ stub('gr-rest-api-interface', {
+ getRepos(num, offset) {
+ return Promise.resolve(repos);
+ },
+ });
+
+ element._paramsChanged(value).then(() => { flush(done); });
+ });
+
+ test('_shownRepos', () => {
+ assert.equal(element._shownRepos.length, 25);
+ });
+ });
+
+ suite('filter', () => {
+ let reposFiltered;
setup(() => {
- sandbox = sinon.sandbox.create();
- sandbox.stub(page, 'show');
- element = fixture('basic');
- counter = 0;
+ repos = _.times(25, repoGenerator);
+ reposFiltered = _.times(1, repoGenerator);
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('list with repos', () => {
- setup(done => {
- repos = _.times(26, repoGenerator);
- stub('gr-rest-api-interface', {
- getRepos(num, offset) {
- return Promise.resolve(repos);
- },
- });
- element._paramsChanged(value).then(() => { flush(done); });
- });
-
- test('test for test repo in the list', done => {
- flush(() => {
- assert.equal(element._repos[1].id, 'test2');
- done();
- });
- });
-
- test('_shownRepos', () => {
- assert.equal(element._shownRepos.length, 25);
- });
-
- test('_maybeOpenCreateOverlay', () => {
- const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
- element._maybeOpenCreateOverlay();
- assert.isFalse(overlayOpen.called);
- const params = {};
- element._maybeOpenCreateOverlay(params);
- assert.isFalse(overlayOpen.called);
- params.openCreateModal = true;
- element._maybeOpenCreateOverlay(params);
- assert.isTrue(overlayOpen.called);
+ test('_paramsChanged', done => {
+ sandbox.stub(element.$.restAPI, 'getRepos', () => Promise.resolve(repos));
+ const value = {
+ filter: 'test',
+ offset: 25,
+ };
+ element._paramsChanged(value).then(() => {
+ assert.isTrue(element.$.restAPI.getRepos.lastCall
+ .calledWithExactly('test', 25, 25));
+ done();
});
});
- suite('list with less then 25 repos', () => {
- setup(done => {
- repos = _.times(25, repoGenerator);
+ test('latest repos requested are always set', done => {
+ const repoStub = sandbox.stub(element.$.restAPI, 'getRepos');
+ repoStub.withArgs('test').returns(Promise.resolve(repos));
+ repoStub.withArgs('filter').returns(Promise.resolve(reposFiltered));
+ element._filter = 'test';
- stub('gr-rest-api-interface', {
- getRepos(num, offset) {
- return Promise.resolve(repos);
- },
- });
-
- element._paramsChanged(value).then(() => { flush(done); });
- });
-
- test('_shownRepos', () => {
- assert.equal(element._shownRepos.length, 25);
- });
- });
-
- suite('filter', () => {
- let reposFiltered;
- setup(() => {
- repos = _.times(25, repoGenerator);
- reposFiltered = _.times(1, repoGenerator);
- });
-
- test('_paramsChanged', done => {
- sandbox.stub(element.$.restAPI, 'getRepos', () => Promise.resolve(repos));
- const value = {
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(value).then(() => {
- assert.isTrue(element.$.restAPI.getRepos.lastCall
- .calledWithExactly('test', 25, 25));
- done();
- });
- });
-
- test('latest repos requested are always set', done => {
- const repoStub = sandbox.stub(element.$.restAPI, 'getRepos');
- repoStub.withArgs('test').returns(Promise.resolve(repos));
- repoStub.withArgs('filter').returns(Promise.resolve(reposFiltered));
- element._filter = 'test';
-
- // Repos are not set because the element._filter differs.
- element._getRepos('filter', 25, 0).then(() => {
- assert.deepEqual(element._repos, []);
- done();
- });
- });
- });
-
- suite('loading', () => {
- test('correct contents are displayed', () => {
- assert.isTrue(element._loading);
- assert.equal(element.computeLoadingClass(element._loading), 'loading');
- assert.equal(getComputedStyle(element.$.loading).display, 'block');
-
- element._loading = false;
- element._repos = _.times(25, repoGenerator);
-
- flushAsynchronousOperations();
- assert.equal(element.computeLoadingClass(element._loading), '');
- assert.equal(getComputedStyle(element.$.loading).display, 'none');
- });
- });
-
- suite('create new', () => {
- test('_handleCreateClicked called when create-click fired', () => {
- sandbox.stub(element, '_handleCreateClicked');
- element.shadowRoot
- .querySelector('gr-list-view').fire('create-clicked');
- assert.isTrue(element._handleCreateClicked.called);
- });
-
- test('_handleCreateClicked opens modal', () => {
- const openStub = sandbox.stub(element.$.createOverlay, 'open');
- element._handleCreateClicked();
- assert.isTrue(openStub.called);
- });
-
- test('_handleCreateRepo called when confirm fired', () => {
- sandbox.stub(element, '_handleCreateRepo');
- element.$.createDialog.fire('confirm');
- assert.isTrue(element._handleCreateRepo.called);
- });
-
- test('_handleCloseCreate called when cancel fired', () => {
- sandbox.stub(element, '_handleCloseCreate');
- element.$.createDialog.fire('cancel');
- assert.isTrue(element._handleCloseCreate.called);
+ // Repos are not set because the element._filter differs.
+ element._getRepos('filter', 25, 0).then(() => {
+ assert.deepEqual(element._repos, []);
+ done();
});
});
});
+
+ suite('loading', () => {
+ test('correct contents are displayed', () => {
+ assert.isTrue(element._loading);
+ assert.equal(element.computeLoadingClass(element._loading), 'loading');
+ assert.equal(getComputedStyle(element.$.loading).display, 'block');
+
+ element._loading = false;
+ element._repos = _.times(25, repoGenerator);
+
+ flushAsynchronousOperations();
+ assert.equal(element.computeLoadingClass(element._loading), '');
+ assert.equal(getComputedStyle(element.$.loading).display, 'none');
+ });
+ });
+
+ suite('create new', () => {
+ test('_handleCreateClicked called when create-click fired', () => {
+ sandbox.stub(element, '_handleCreateClicked');
+ element.shadowRoot
+ .querySelector('gr-list-view').fire('create-clicked');
+ assert.isTrue(element._handleCreateClicked.called);
+ });
+
+ test('_handleCreateClicked opens modal', () => {
+ const openStub = sandbox.stub(element.$.createOverlay, 'open');
+ element._handleCreateClicked();
+ assert.isTrue(openStub.called);
+ });
+
+ test('_handleCreateRepo called when confirm fired', () => {
+ sandbox.stub(element, '_handleCreateRepo');
+ element.$.createDialog.fire('confirm');
+ assert.isTrue(element._handleCreateRepo.called);
+ });
+
+ test('_handleCloseCreate called when cancel fired', () => {
+ sandbox.stub(element, '_handleCloseCreate');
+ element.$.createDialog.fire('cancel');
+ assert.isTrue(element._handleCloseCreate.called);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html
deleted file mode 100644
index ef5b755..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html
+++ /dev/null
@@ -1,119 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
-
-<link rel="import" href="../../../behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/gr-subpage-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../gr-plugin-config-array-editor/gr-plugin-config-array-editor.html">
-
-<dom-module id="gr-repo-plugin-config">
- <template>
- <style include="shared-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-subpage-styles">
- .inherited {
- color: var(--deemphasized-text-color);
- margin-left: var(--spacing-m);
- }
- section.section:not(.ARRAY) .title {
- align-items: center;
- display: flex;
- }
- section.section.ARRAY .title {
- padding-top: var(--spacing-m);
- }
- </style>
- <div class="gr-form-styles">
- <fieldset>
- <h4>[[pluginData.name]]</h4>
- <template is="dom-repeat" items="[[_pluginConfigOptions]]" as="option">
- <section class$="section [[option.info.type]]">
- <span class="title">
- <gr-tooltip-content
- has-tooltip="[[option.info.description]]"
- show-icon="[[option.info.description]]"
- title="[[option.info.description]]">
- <span>[[option.info.display_name]]</span>
- </gr-tooltip-content>
- </span>
- <span class="value">
- <template is="dom-if" if="[[_isArray(option.info.type)]]">
- <gr-plugin-config-array-editor
- on-plugin-config-option-changed="_handleArrayChange"
- plugin-option="[[option]]"></gr-plugin-config-array-editor>
- </template>
- <template is="dom-if" if="[[_isBoolean(option.info.type)]]">
- <paper-toggle-button
- checked="[[_computeChecked(option.info.value)]]"
- on-change="_handleBooleanChange"
- data-option-key$="[[option._key]]"
- disabled$="[[_computeDisabled(option.info.editable)]]"></paper-toggle-button>
- </template>
- <template is="dom-if" if="[[_isList(option.info.type)]]">
- <gr-select
- bind-value$="[[option.info.value]]"
- on-change="_handleListChange">
- <select
- data-option-key$="[[option._key]]"
- disabled$="[[_computeDisabled(option.info.editable)]]">
- <template is="dom-repeat"
- items="[[option.info.permitted_values]]"
- as="value">
- <option value$="[[value]]">[[value]]</option>
- </template>
- </select>
- </gr-select>
- </template>
- <template is="dom-if" if="[[_isString(option.info.type)]]">
- <iron-input
- bind-value="[[option.info.value]]"
- on-input="_handleStringChange"
- data-option-key$="[[option._key]]"
- disabled$="[[_computeDisabled(option.info.editable)]]">
- <input
- is="iron-input"
- value="[[option.info.value]]"
- on-input="_handleStringChange"
- data-option-key$="[[option._key]]"
- disabled$="[[_computeDisabled(option.info.editable)]]">
- </iron-input>
- </template>
- <template is="dom-if" if="[[option.info.inherited_value]]">
- <span class="inherited">
- (Inherited: [[option.info.inherited_value]])
- </span>
- </template>
- </span>
- </section>
- </template>
- </fieldset>
- </div>
- </template>
- <script src="gr-repo-plugin-config.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
index 7368eb8..826bf93 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
@@ -14,128 +14,145 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '@polymer/iron-input/iron-input.js';
+import '@polymer/paper-toggle-button/paper-toggle-button.js';
+import '../../../behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/gr-subpage-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-select/gr-select.js';
+import '../../shared/gr-tooltip-content/gr-tooltip-content.js';
+import '../gr-plugin-config-array-editor/gr-plugin-config-array-editor.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-repo-plugin-config_html.js';
+
+/**
+ * @appliesMixin Gerrit.RepoPluginConfigMixin
+ * @extends Polymer.Element
+ */
+class GrRepoPluginConfig extends mixinBehaviors( [
+ Gerrit.RepoPluginConfig,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-repo-plugin-config'; }
/**
- * @appliesMixin Gerrit.RepoPluginConfigMixin
- * @extends Polymer.Element
+ * Fired when the plugin config changes.
+ *
+ * @event plugin-config-changed
*/
- class GrRepoPluginConfig extends Polymer.mixinBehaviors( [
- Gerrit.RepoPluginConfig,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-repo-plugin-config'; }
- /**
- * Fired when the plugin config changes.
- *
- * @event plugin-config-changed
- */
- static get properties() {
- return {
- /** @type {?} */
- pluginData: Object,
- /** @type {Array} */
- _pluginConfigOptions: {
- type: Array,
- computed: '_computePluginConfigOptions(pluginData.*)',
- },
- };
- }
-
- _computePluginConfigOptions(dataRecord) {
- if (!dataRecord || !dataRecord.base || !dataRecord.base.config) {
- return [];
- }
- const {config} = dataRecord.base;
- return Object.keys(config)
- .map(_key => { return {_key, info: config[_key]}; });
- }
-
- _isArray(type) {
- return type === this.ENTRY_TYPES.ARRAY;
- }
-
- _isBoolean(type) {
- return type === this.ENTRY_TYPES.BOOLEAN;
- }
-
- _isList(type) {
- return type === this.ENTRY_TYPES.LIST;
- }
-
- _isString(type) {
- // Treat numbers like strings for simplicity.
- return type === this.ENTRY_TYPES.STRING ||
- type === this.ENTRY_TYPES.INT ||
- type === this.ENTRY_TYPES.LONG;
- }
-
- _computeDisabled(editable) {
- return editable === 'false';
- }
-
- /**
- * @param {string} value - fallback to 'false' if undefined
- */
- _computeChecked(value = 'false') {
- return JSON.parse(value);
- }
-
- _handleStringChange(e) {
- const el = Polymer.dom(e).localTarget;
- const _key = el.getAttribute('data-option-key');
- const configChangeInfo =
- this._buildConfigChangeInfo(el.value, _key);
- this._handleChange(configChangeInfo);
- }
-
- _handleListChange(e) {
- const el = Polymer.dom(e).localTarget;
- const _key = el.getAttribute('data-option-key');
- const configChangeInfo =
- this._buildConfigChangeInfo(el.value, _key);
- this._handleChange(configChangeInfo);
- }
-
- _handleBooleanChange(e) {
- const el = Polymer.dom(e).localTarget;
- const _key = el.getAttribute('data-option-key');
- const configChangeInfo =
- this._buildConfigChangeInfo(JSON.stringify(el.checked), _key);
- this._handleChange(configChangeInfo);
- }
-
- _buildConfigChangeInfo(value, _key) {
- const info = this.pluginData.config[_key];
- info.value = value;
- return {
- _key,
- info,
- notifyPath: `${_key}.value`,
- };
- }
-
- _handleArrayChange({detail}) {
- this._handleChange(detail);
- }
-
- _handleChange({_key, info, notifyPath}) {
- const {name, config} = this.pluginData;
-
- /** @type {Object} */
- const detail = {
- name,
- config: Object.assign(config, {[_key]: info}, {}),
- notifyPath: `${name}.${notifyPath}`,
- };
-
- this.dispatchEvent(new CustomEvent(
- this.PLUGIN_CONFIG_CHANGED, {detail, bubbles: true, composed: true}));
- }
+ static get properties() {
+ return {
+ /** @type {?} */
+ pluginData: Object,
+ /** @type {Array} */
+ _pluginConfigOptions: {
+ type: Array,
+ computed: '_computePluginConfigOptions(pluginData.*)',
+ },
+ };
}
- customElements.define(GrRepoPluginConfig.is, GrRepoPluginConfig);
-})();
+ _computePluginConfigOptions(dataRecord) {
+ if (!dataRecord || !dataRecord.base || !dataRecord.base.config) {
+ return [];
+ }
+ const {config} = dataRecord.base;
+ return Object.keys(config)
+ .map(_key => { return {_key, info: config[_key]}; });
+ }
+
+ _isArray(type) {
+ return type === this.ENTRY_TYPES.ARRAY;
+ }
+
+ _isBoolean(type) {
+ return type === this.ENTRY_TYPES.BOOLEAN;
+ }
+
+ _isList(type) {
+ return type === this.ENTRY_TYPES.LIST;
+ }
+
+ _isString(type) {
+ // Treat numbers like strings for simplicity.
+ return type === this.ENTRY_TYPES.STRING ||
+ type === this.ENTRY_TYPES.INT ||
+ type === this.ENTRY_TYPES.LONG;
+ }
+
+ _computeDisabled(editable) {
+ return editable === 'false';
+ }
+
+ /**
+ * @param {string} value - fallback to 'false' if undefined
+ */
+ _computeChecked(value = 'false') {
+ return JSON.parse(value);
+ }
+
+ _handleStringChange(e) {
+ const el = dom(e).localTarget;
+ const _key = el.getAttribute('data-option-key');
+ const configChangeInfo =
+ this._buildConfigChangeInfo(el.value, _key);
+ this._handleChange(configChangeInfo);
+ }
+
+ _handleListChange(e) {
+ const el = dom(e).localTarget;
+ const _key = el.getAttribute('data-option-key');
+ const configChangeInfo =
+ this._buildConfigChangeInfo(el.value, _key);
+ this._handleChange(configChangeInfo);
+ }
+
+ _handleBooleanChange(e) {
+ const el = dom(e).localTarget;
+ const _key = el.getAttribute('data-option-key');
+ const configChangeInfo =
+ this._buildConfigChangeInfo(JSON.stringify(el.checked), _key);
+ this._handleChange(configChangeInfo);
+ }
+
+ _buildConfigChangeInfo(value, _key) {
+ const info = this.pluginData.config[_key];
+ info.value = value;
+ return {
+ _key,
+ info,
+ notifyPath: `${_key}.value`,
+ };
+ }
+
+ _handleArrayChange({detail}) {
+ this._handleChange(detail);
+ }
+
+ _handleChange({_key, info, notifyPath}) {
+ const {name, config} = this.pluginData;
+
+ /** @type {Object} */
+ const detail = {
+ name,
+ config: Object.assign(config, {[_key]: info}, {}),
+ notifyPath: `${name}.${notifyPath}`,
+ };
+
+ this.dispatchEvent(new CustomEvent(
+ this.PLUGIN_CONFIG_CHANGED, {detail, bubbles: true, composed: true}));
+ }
+}
+
+customElements.define(GrRepoPluginConfig.is, GrRepoPluginConfig);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.js b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.js
new file mode 100644
index 0000000..fa4617d
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.js
@@ -0,0 +1,80 @@
+/**
+ * @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.js';
+
+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">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="gr-subpage-styles">
+ .inherited {
+ color: var(--deemphasized-text-color);
+ margin-left: var(--spacing-m);
+ }
+ section.section:not(.ARRAY) .title {
+ align-items: center;
+ display: flex;
+ }
+ section.section.ARRAY .title {
+ padding-top: var(--spacing-m);
+ }
+ </style>
+ <div class="gr-form-styles">
+ <fieldset>
+ <h4>[[pluginData.name]]</h4>
+ <template is="dom-repeat" items="[[_pluginConfigOptions]]" as="option">
+ <section class\$="section [[option.info.type]]">
+ <span class="title">
+ <gr-tooltip-content has-tooltip="[[option.info.description]]" show-icon="[[option.info.description]]" title="[[option.info.description]]">
+ <span>[[option.info.display_name]]</span>
+ </gr-tooltip-content>
+ </span>
+ <span class="value">
+ <template is="dom-if" if="[[_isArray(option.info.type)]]">
+ <gr-plugin-config-array-editor on-plugin-config-option-changed="_handleArrayChange" plugin-option="[[option]]"></gr-plugin-config-array-editor>
+ </template>
+ <template is="dom-if" if="[[_isBoolean(option.info.type)]]">
+ <paper-toggle-button checked="[[_computeChecked(option.info.value)]]" on-change="_handleBooleanChange" data-option-key\$="[[option._key]]" disabled\$="[[_computeDisabled(option.info.editable)]]"></paper-toggle-button>
+ </template>
+ <template is="dom-if" if="[[_isList(option.info.type)]]">
+ <gr-select bind-value\$="[[option.info.value]]" on-change="_handleListChange">
+ <select data-option-key\$="[[option._key]]" disabled\$="[[_computeDisabled(option.info.editable)]]">
+ <template is="dom-repeat" items="[[option.info.permitted_values]]" as="value">
+ <option value\$="[[value]]">[[value]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </template>
+ <template is="dom-if" if="[[_isString(option.info.type)]]">
+ <iron-input bind-value="[[option.info.value]]" on-input="_handleStringChange" data-option-key\$="[[option._key]]" disabled\$="[[_computeDisabled(option.info.editable)]]">
+ <input is="iron-input" value="[[option.info.value]]" on-input="_handleStringChange" data-option-key\$="[[option._key]]" disabled\$="[[_computeDisabled(option.info.editable)]]">
+ </iron-input>
+ </template>
+ <template is="dom-if" if="[[option.info.inherited_value]]">
+ <span class="inherited">
+ (Inherited: [[option.info.inherited_value]])
+ </span>
+ </template>
+ </span>
+ </section>
+ </template>
+ </fieldset>
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
index 8313edf..c2abef2 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-plugin-config</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-repo-plugin-config.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,150 +30,151 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo-plugin-config tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-repo-plugin-config.js';
+suite('gr-repo-plugin-config tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => sandbox.restore());
+
+ test('_computePluginConfigOptions', () => {
+ assert.deepEqual(element._computePluginConfigOptions(), []);
+ assert.deepEqual(element._computePluginConfigOptions({}), []);
+ assert.deepEqual(element._computePluginConfigOptions({base: {}}), []);
+ assert.deepEqual(element._computePluginConfigOptions(
+ {base: {config: {}}}), []);
+ assert.deepEqual(element._computePluginConfigOptions(
+ {base: {config: {testKey: 'testInfo'}}}),
+ [{_key: 'testKey', info: 'testInfo'}]);
+ });
+
+ test('_computeDisabled', () => {
+ assert.isFalse(element._computeDisabled('true'));
+ assert.isTrue(element._computeDisabled('false'));
+ });
+
+ test('_handleChange', () => {
+ const eventStub = sandbox.stub(element, 'dispatchEvent');
+ element.pluginData = {
+ name: 'testName',
+ config: {plugin: {value: 'test'}},
+ };
+ element._handleChange({
+ _key: 'plugin',
+ info: {value: 'newTest'},
+ notifyPath: 'plugin.value',
+ });
+
+ assert.isTrue(eventStub.called);
+
+ const {detail} = eventStub.lastCall.args[0];
+ assert.equal(detail.name, 'testName');
+ assert.deepEqual(detail.config, {plugin: {value: 'newTest'}});
+ assert.equal(detail.notifyPath, 'testName.plugin.value');
+ });
+
+ suite('option types', () => {
+ let changeStub;
+ let buildStub;
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
+ changeStub = sandbox.stub(element, '_handleChange');
+ buildStub = sandbox.stub(element, '_buildConfigChangeInfo');
});
- teardown(() => sandbox.restore());
-
- test('_computePluginConfigOptions', () => {
- assert.deepEqual(element._computePluginConfigOptions(), []);
- assert.deepEqual(element._computePluginConfigOptions({}), []);
- assert.deepEqual(element._computePluginConfigOptions({base: {}}), []);
- assert.deepEqual(element._computePluginConfigOptions(
- {base: {config: {}}}), []);
- assert.deepEqual(element._computePluginConfigOptions(
- {base: {config: {testKey: 'testInfo'}}}),
- [{_key: 'testKey', info: 'testInfo'}]);
- });
-
- test('_computeDisabled', () => {
- assert.isFalse(element._computeDisabled('true'));
- assert.isTrue(element._computeDisabled('false'));
- });
-
- test('_handleChange', () => {
- const eventStub = sandbox.stub(element, 'dispatchEvent');
+ test('ARRAY type option', () => {
element.pluginData = {
name: 'testName',
- config: {plugin: {value: 'test'}},
+ config: {plugin: {value: 'test', type: 'ARRAY'}},
};
- element._handleChange({
- _key: 'plugin',
- info: {value: 'newTest'},
- notifyPath: 'plugin.value',
- });
+ flushAsynchronousOperations();
- assert.isTrue(eventStub.called);
-
- const {detail} = eventStub.lastCall.args[0];
- assert.equal(detail.name, 'testName');
- assert.deepEqual(detail.config, {plugin: {value: 'newTest'}});
- assert.equal(detail.notifyPath, 'testName.plugin.value');
+ const editor = element.shadowRoot
+ .querySelector('gr-plugin-config-array-editor');
+ assert.ok(editor);
+ element._handleArrayChange({detail: 'test'});
+ assert.isTrue(changeStub.called);
+ assert.equal(changeStub.lastCall.args[0], 'test');
});
- suite('option types', () => {
- let changeStub;
- let buildStub;
-
- setup(() => {
- changeStub = sandbox.stub(element, '_handleChange');
- buildStub = sandbox.stub(element, '_buildConfigChangeInfo');
- });
-
- test('ARRAY type option', () => {
- element.pluginData = {
- name: 'testName',
- config: {plugin: {value: 'test', type: 'ARRAY'}},
- };
- flushAsynchronousOperations();
-
- const editor = element.shadowRoot
- .querySelector('gr-plugin-config-array-editor');
- assert.ok(editor);
- element._handleArrayChange({detail: 'test'});
- assert.isTrue(changeStub.called);
- assert.equal(changeStub.lastCall.args[0], 'test');
- });
-
- test('BOOLEAN type option', () => {
- element.pluginData = {
- name: 'testName',
- config: {plugin: {value: 'true', type: 'BOOLEAN'}},
- };
- flushAsynchronousOperations();
-
- const toggle = element.shadowRoot
- .querySelector('paper-toggle-button');
- assert.ok(toggle);
- toggle.click();
- flushAsynchronousOperations();
-
- assert.isTrue(buildStub.called);
- assert.deepEqual(buildStub.lastCall.args, ['false', 'plugin']);
-
- assert.isTrue(changeStub.called);
- });
-
- test('INT/LONG/STRING type option', () => {
- element.pluginData = {
- name: 'testName',
- config: {plugin: {value: 'test', type: 'STRING'}},
- };
- flushAsynchronousOperations();
-
- const input = element.shadowRoot
- .querySelector('input');
- assert.ok(input);
- input.value = 'newTest';
- input.dispatchEvent(new Event('input'));
- flushAsynchronousOperations();
-
- assert.isTrue(buildStub.called);
- assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
-
- assert.isTrue(changeStub.called);
- });
-
- test('LIST type option', () => {
- const permitted_values = ['test', 'newTest'];
- element.pluginData = {
- name: 'testName',
- config: {plugin: {value: 'test', type: 'LIST', permitted_values}},
- };
- flushAsynchronousOperations();
-
- const select = element.shadowRoot
- .querySelector('select');
- assert.ok(select);
- select.value = 'newTest';
- select.dispatchEvent(new Event(
- 'change', {bubbles: true, composed: true}));
- flushAsynchronousOperations();
-
- assert.isTrue(buildStub.called);
- assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
-
- assert.isTrue(changeStub.called);
- });
- });
-
- test('_buildConfigChangeInfo', () => {
+ test('BOOLEAN type option', () => {
element.pluginData = {
name: 'testName',
- config: {plugin: {value: 'test'}},
+ config: {plugin: {value: 'true', type: 'BOOLEAN'}},
};
- const detail = element._buildConfigChangeInfo('newTest', 'plugin');
- assert.equal(detail._key, 'plugin');
- assert.deepEqual(detail.info, {value: 'newTest'});
- assert.equal(detail.notifyPath, 'plugin.value');
+ flushAsynchronousOperations();
+
+ const toggle = element.shadowRoot
+ .querySelector('paper-toggle-button');
+ assert.ok(toggle);
+ toggle.click();
+ flushAsynchronousOperations();
+
+ assert.isTrue(buildStub.called);
+ assert.deepEqual(buildStub.lastCall.args, ['false', 'plugin']);
+
+ assert.isTrue(changeStub.called);
+ });
+
+ test('INT/LONG/STRING type option', () => {
+ element.pluginData = {
+ name: 'testName',
+ config: {plugin: {value: 'test', type: 'STRING'}},
+ };
+ flushAsynchronousOperations();
+
+ const input = element.shadowRoot
+ .querySelector('input');
+ assert.ok(input);
+ input.value = 'newTest';
+ input.dispatchEvent(new Event('input'));
+ flushAsynchronousOperations();
+
+ assert.isTrue(buildStub.called);
+ assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
+
+ assert.isTrue(changeStub.called);
+ });
+
+ test('LIST type option', () => {
+ const permitted_values = ['test', 'newTest'];
+ element.pluginData = {
+ name: 'testName',
+ config: {plugin: {value: 'test', type: 'LIST', permitted_values}},
+ };
+ flushAsynchronousOperations();
+
+ const select = element.shadowRoot
+ .querySelector('select');
+ assert.ok(select);
+ select.value = 'newTest';
+ select.dispatchEvent(new Event(
+ 'change', {bubbles: true, composed: true}));
+ flushAsynchronousOperations();
+
+ assert.isTrue(buildStub.called);
+ assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
+
+ assert.isTrue(changeStub.called);
});
});
+
+ test('_buildConfigChangeInfo', () => {
+ element.pluginData = {
+ name: 'testName',
+ config: {plugin: {value: 'test'}},
+ };
+ const detail = element._buildConfigChangeInfo('newTest', 'plugin');
+ assert.equal(detail._key, 'plugin');
+ assert.deepEqual(detail.info, {value: 'newTest'});
+ assert.equal(detail.notifyPath, 'plugin.value');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
deleted file mode 100644
index 5e37261..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
+++ /dev/null
@@ -1,386 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="../../shared/gr-download-commands/gr-download-commands.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/gr-subpage-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../gr-repo-plugin-config/gr-repo-plugin-config.html">
-
-<dom-module id="gr-repo">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-subpage-styles">
- h2.edited:after {
- color: var(--deemphasized-text-color);
- content: ' *';
- }
- .loading,
- .hide {
- display: none;
- }
- #loading.loading {
- display: block;
- }
- #loading:not(.loading) {
- display: none;
- }
- #options .repositorySettings {
- display: none;
- }
- #options .repositorySettings.showConfig {
- display: block;
- }
- </style>
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <main class="gr-form-styles read-only">
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <div class="info">
- <h1 id="Title" class$="name">
- [[repo]]
- <hr/>
- </h1>
- <div>
- <a href$="[[_computeChangesUrl(repo)]]">(view changes)</a>
- </div>
- </div>
- <div id="loading" class$="[[_computeLoadingClass(_loading)]]">Loading...</div>
- <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <div id="downloadContent" class$="[[_computeHideClass(_schemes)]]">
- <h2 id="download">Download</h2>
- <fieldset>
- <gr-download-commands
- id="downloadCommands"
- commands="[[_computeCommands(repo, _schemesObj, _selectedScheme)]]"
- schemes="[[_schemes]]"
- selected-scheme="{{_selectedScheme}}"></gr-download-commands>
- </fieldset>
- </div>
- <h2 id="configurations"
- class$="[[_computeHeaderClass(_configChanged)]]">Configurations</h2>
- <div id="form">
- <fieldset>
- <h3 id="Description">Description</h3>
- <fieldset>
- <iron-autogrow-textarea
- id="descriptionInput"
- class="description"
- autocomplete="on"
- placeholder="<Insert repo description here>"
- bind-value="{{_repoConfig.description}}"
- disabled$="[[_readOnly]]"></iron-autogrow-textarea>
- </fieldset>
- <h3 id="Options">Repository Options</h3>
- <fieldset id="options">
- <section>
- <span class="title">State</span>
- <span class="value">
- <gr-select
- id="stateSelect"
- bind-value="{{_repoConfig.state}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat" items=[[_states]]>
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Submit type</span>
- <span class="value">
- <gr-select
- id="submitTypeSelect"
- bind-value="{{_repoConfig.submit_type}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatSubmitTypeSelect(_repoConfig)]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Allow content merges</span>
- <span class="value">
- <gr-select
- id="contentMergeSelect"
- bind-value="{{_repoConfig.use_content_merge.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.use_content_merge)]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">
- Create a new change for every commit not in the target branch
- </span>
- <span class="value">
- <gr-select
- id="newChangeSelect"
- bind-value="{{_repoConfig.create_new_change_for_all_not_in_target.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.create_new_change_for_all_not_in_target)]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Require Change-Id in commit message</span>
- <span class="value">
- <gr-select
- id="requireChangeIdSelect"
- bind-value="{{_repoConfig.require_change_id.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.require_change_id)]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section
- id="enableSignedPushSettings"
- class$="repositorySettings [[_computeRepositoriesClass(_repoConfig.enable_signed_push)]]">
- <span class="title">Enable signed push</span>
- <span class="value">
- <gr-select
- id="enableSignedPush"
- bind-value="{{_repoConfig.enable_signed_push.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.enable_signed_push)]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section
- id="requireSignedPushSettings"
- class$="repositorySettings [[_computeRepositoriesClass(_repoConfig.require_signed_push)]]">
- <span class="title">Require signed push</span>
- <span class="value">
- <gr-select
- id="requireSignedPush"
- bind-value="{{_repoConfig.require_signed_push.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.require_signed_push)]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">
- Reject implicit merges when changes are pushed for review</span>
- <span class="value">
- <gr-select
- id="rejectImplicitMergesSelect"
- bind-value="{{_repoConfig.reject_implicit_merges.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.reject_implicit_merges)]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">
- Enable adding unregistered users as reviewers and CCs on changes</span>
- <span class="value">
- <gr-select
- id="unRegisteredCcSelect"
- bind-value="{{_repoConfig.enable_reviewer_by_email.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.enable_reviewer_by_email)]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">
- Set all new changes private by default</span>
- <span class="value">
- <gr-select
- id="setAllnewChangesPrivateByDefaultSelect"
- bind-value="{{_repoConfig.private_by_default.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.private_by_default)]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">
- Set new changes to "work in progress" by default</span>
- <span class="value">
- <gr-select
- id="setAllNewChangesWorkInProgressByDefaultSelect"
- bind-value="{{_repoConfig.work_in_progress_by_default.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.work_in_progress_by_default)]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Maximum Git object size limit</span>
- <span class="value">
- <iron-input
- id="maxGitObjSizeIronInput"
- bind-value="{{_repoConfig.max_object_size_limit.configured_value}}"
- type="text"
- disabled$="[[_readOnly]]">
- <input
- id="maxGitObjSizeInput"
- bind-value="{{_repoConfig.max_object_size_limit.configured_value}}"
- is="iron-input"
- type="text"
- disabled$="[[_readOnly]]">
- </iron-input>
- <template is="dom-if" if="[[_repoConfig.max_object_size_limit.value]]">
- effective: [[_repoConfig.max_object_size_limit.value]] bytes
- </template>
- </span>
- </section>
- <section>
- <span class="title">Match authored date with committer date upon submit</span>
- <span class="value">
- <gr-select
- id="matchAuthoredDateWithCommitterDateSelect"
- bind-value="{{_repoConfig.match_author_to_committer_date.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.match_author_to_committer_date)]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Reject empty commit upon submit</span>
- <span class="value">
- <gr-select
- id="rejectEmptyCommitSelect"
- bind-value="{{_repoConfig.reject_empty_commit.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.reject_empty_commit)]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- </fieldset>
- <h3 id="Options">Contributor Agreements</h3>
- <fieldset id="agreements">
- <section>
- <span class="title">
- Require a valid contributor agreement to upload</span>
- <span class="value">
- <gr-select
- id="contributorAgreementSelect"
- bind-value="{{_repoConfig.use_contributor_agreements.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.use_contributor_agreements)]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Require Signed-off-by in commit message</span>
- <span class="value">
- <gr-select
- id="useSignedOffBySelect"
- bind-value="{{_repoConfig.use_signed_off_by.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.use_signed_off_by)]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- </fieldset>
- <div
- class$="pluginConfig [[_computeHideClass(_pluginData)]]"
- on-plugin-config-changed="_handlePluginConfigChanged">
- <h3>Plugins</h3>
- <template is="dom-repeat" items="[[_pluginData]]" as="data">
- <gr-repo-plugin-config
- plugin-data="[[data]]"></gr-repo-plugin-config>
- </template>
- </div>
- <gr-button
- on-click="_handleSaveRepoConfig"
- disabled$="[[_computeButtonDisabled(_readOnly, _configChanged)]]">Save changes</gr-button>
- </fieldset>
- <gr-endpoint-decorator name="repo-config">
- <gr-endpoint-param name="repoName" value="[[repo]]"></gr-endpoint-param>
- <gr-endpoint-param name="readOnly" value="[[_readOnly]]"></gr-endpoint-param>
- </gr-endpoint-decorator>
- </div>
- </div>
- </main>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-repo.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
index f6328de..fd85f1a 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
@@ -14,348 +14,366 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const STATES = {
- active: {value: 'ACTIVE', label: 'Active'},
- readOnly: {value: 'READ_ONLY', label: 'Read Only'},
- hidden: {value: 'HIDDEN', label: 'Hidden'},
- };
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
+import '../../shared/gr-download-commands/gr-download-commands.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/gr-subpage-styles.js';
+import '../../../styles/shared-styles.js';
+import '../gr-repo-plugin-config/gr-repo-plugin-config.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-repo_html.js';
- const SUBMIT_TYPES = {
- // Exclude INHERIT, which is handled specially.
- mergeIfNecessary: {
- value: 'MERGE_IF_NECESSARY',
- label: 'Merge if necessary',
- },
- fastForwardOnly: {
- value: 'FAST_FORWARD_ONLY',
- label: 'Fast forward only',
- },
- rebaseAlways: {
- value: 'REBASE_ALWAYS',
- label: 'Rebase Always',
- },
- rebaseIfNecessary: {
- value: 'REBASE_IF_NECESSARY',
- label: 'Rebase if necessary',
- },
- mergeAlways: {
- value: 'MERGE_ALWAYS',
- label: 'Merge always',
- },
- cherryPick: {
- value: 'CHERRY_PICK',
- label: 'Cherry pick',
- },
- };
+const STATES = {
+ active: {value: 'ACTIVE', label: 'Active'},
+ readOnly: {value: 'READ_ONLY', label: 'Read Only'},
+ hidden: {value: 'HIDDEN', label: 'Hidden'},
+};
- /**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
- */
- class GrRepo extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-repo'; }
+const SUBMIT_TYPES = {
+ // Exclude INHERIT, which is handled specially.
+ mergeIfNecessary: {
+ value: 'MERGE_IF_NECESSARY',
+ label: 'Merge if necessary',
+ },
+ fastForwardOnly: {
+ value: 'FAST_FORWARD_ONLY',
+ label: 'Fast forward only',
+ },
+ rebaseAlways: {
+ value: 'REBASE_ALWAYS',
+ label: 'Rebase Always',
+ },
+ rebaseIfNecessary: {
+ value: 'REBASE_IF_NECESSARY',
+ label: 'Rebase if necessary',
+ },
+ mergeAlways: {
+ value: 'MERGE_ALWAYS',
+ label: 'Merge always',
+ },
+ cherryPick: {
+ value: 'CHERRY_PICK',
+ label: 'Cherry pick',
+ },
+};
- static get properties() {
- return {
- params: Object,
- repo: String,
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrRepo extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _configChanged: {
- type: Boolean,
- value: false,
+ static get is() { return 'gr-repo'; }
+
+ static get properties() {
+ return {
+ params: Object,
+ repo: String,
+
+ _configChanged: {
+ type: Boolean,
+ value: false,
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ observer: '_loggedInChanged',
+ },
+ /** @type {?} */
+ _repoConfig: Object,
+ /** @type {?} */
+ _pluginData: {
+ type: Array,
+ computed: '_computePluginData(_repoConfig.plugin_config.*)',
+ },
+ _readOnly: {
+ type: Boolean,
+ value: true,
+ },
+ _states: {
+ type: Array,
+ value() {
+ return Object.values(STATES);
},
- _loading: {
- type: Boolean,
- value: true,
+ },
+ _submitTypes: {
+ type: Array,
+ value() {
+ return Object.values(SUBMIT_TYPES);
},
- _loggedIn: {
- type: Boolean,
- value: false,
- observer: '_loggedInChanged',
- },
- /** @type {?} */
- _repoConfig: Object,
- /** @type {?} */
- _pluginData: {
- type: Array,
- computed: '_computePluginData(_repoConfig.plugin_config.*)',
- },
- _readOnly: {
- type: Boolean,
- value: true,
- },
- _states: {
- type: Array,
- value() {
- return Object.values(STATES);
- },
- },
- _submitTypes: {
- type: Array,
- value() {
- return Object.values(SUBMIT_TYPES);
- },
- },
- _schemes: {
- type: Array,
- value() { return []; },
- computed: '_computeSchemes(_schemesObj)',
- observer: '_schemesChanged',
- },
- _selectedCommand: {
- type: String,
- value: 'Clone',
- },
- _selectedScheme: String,
- _schemesObj: Object,
- };
- }
+ },
+ _schemes: {
+ type: Array,
+ value() { return []; },
+ computed: '_computeSchemes(_schemesObj)',
+ observer: '_schemesChanged',
+ },
+ _selectedCommand: {
+ type: String,
+ value: 'Clone',
+ },
+ _selectedScheme: String,
+ _schemesObj: Object,
+ };
+ }
- static get observers() {
- return [
- '_handleConfigChanged(_repoConfig.*)',
- ];
- }
+ static get observers() {
+ return [
+ '_handleConfigChanged(_repoConfig.*)',
+ ];
+ }
- /** @override */
- attached() {
- super.attached();
- this._loadRepo();
+ /** @override */
+ attached() {
+ super.attached();
+ this._loadRepo();
- this.fire('title-change', {title: this.repo});
- }
+ this.fire('title-change', {title: this.repo});
+ }
- _computePluginData(configRecord) {
- if (!configRecord ||
- !configRecord.base) { return []; }
+ _computePluginData(configRecord) {
+ if (!configRecord ||
+ !configRecord.base) { return []; }
- const pluginConfig = configRecord.base;
- return Object.keys(pluginConfig)
- .map(name => { return {name, config: pluginConfig[name]}; });
- }
+ const pluginConfig = configRecord.base;
+ return Object.keys(pluginConfig)
+ .map(name => { return {name, config: pluginConfig[name]}; });
+ }
- _loadRepo() {
- if (!this.repo) { return Promise.resolve(); }
+ _loadRepo() {
+ if (!this.repo) { return Promise.resolve(); }
- const promises = [];
+ const promises = [];
- const errFn = response => {
- this.fire('page-error', {response});
- };
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
- promises.push(this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- if (loggedIn) {
- this.$.restAPI.getRepoAccess(this.repo).then(access => {
- if (!access) { return Promise.resolve(); }
+ promises.push(this._getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ if (loggedIn) {
+ this.$.restAPI.getRepoAccess(this.repo).then(access => {
+ if (!access) { return Promise.resolve(); }
- // If the user is not an owner, is_owner is not a property.
- this._readOnly = !access[this.repo].is_owner;
- });
- }
- }));
-
- promises.push(this.$.restAPI.getProjectConfig(this.repo, errFn)
- .then(config => {
- if (!config) { return Promise.resolve(); }
-
- if (config.default_submit_type) {
- // The gr-select is bound to submit_type, which needs to be the
- // *configured* submit type. When default_submit_type is
- // present, the server reports the *effective* submit type in
- // submit_type, so we need to overwrite it before storing the
- // config in this.
- config.submit_type =
- config.default_submit_type.configured_value;
- }
- if (!config.state) {
- config.state = STATES.active.value;
- }
- this._repoConfig = config;
- this._loading = false;
- }));
-
- promises.push(this.$.restAPI.getConfig().then(config => {
- if (!config) { return Promise.resolve(); }
-
- this._schemesObj = config.download.schemes;
- }));
-
- return Promise.all(promises);
- }
-
- _computeLoadingClass(loading) {
- return loading ? 'loading' : '';
- }
-
- _computeHideClass(arr) {
- return !arr || !arr.length ? 'hide' : '';
- }
-
- _loggedInChanged(_loggedIn) {
- if (!_loggedIn) { return; }
- this.$.restAPI.getPreferences().then(prefs => {
- if (prefs.download_scheme) {
- // Note (issue 5180): normalize the download scheme with lower-case.
- this._selectedScheme = prefs.download_scheme.toLowerCase();
- }
- });
- }
-
- _formatBooleanSelect(item) {
- if (!item) { return; }
- let inheritLabel = 'Inherit';
- if (!(item.inherited_value === undefined)) {
- inheritLabel = `Inherit (${item.inherited_value})`;
- }
- return [
- {
- label: inheritLabel,
- value: 'INHERIT',
- },
- {
- label: 'True',
- value: 'TRUE',
- }, {
- label: 'False',
- value: 'FALSE',
- },
- ];
- }
-
- _formatSubmitTypeSelect(projectConfig) {
- if (!projectConfig) { return; }
- const allValues = Object.values(SUBMIT_TYPES);
- const type = projectConfig.default_submit_type;
- if (!type) {
- // Server is too old to report default_submit_type, so assume INHERIT
- // is not a valid value.
- return allValues;
- }
-
- let inheritLabel = 'Inherit';
- if (type.inherited_value) {
- let inherited = type.inherited_value;
- for (const val of allValues) {
- if (val.value === type.inherited_value) {
- inherited = val.label;
- break;
- }
- }
- inheritLabel = `Inherit (${inherited})`;
- }
- return [
- {
- label: inheritLabel,
- value: 'INHERIT',
- },
- ...allValues,
- ];
- }
-
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- _formatRepoConfigForSave(repoConfig) {
- const configInputObj = {};
- for (const key in repoConfig) {
- if (repoConfig.hasOwnProperty(key)) {
- if (key === 'default_submit_type') {
- // default_submit_type is not in the input type, and the
- // configured value was already copied to submit_type by
- // _loadProject. Omit this property when saving.
- continue;
- }
- if (key === 'plugin_config') {
- configInputObj.plugin_config_values = repoConfig[key];
- } else if (typeof repoConfig[key] === 'object') {
- configInputObj[key] = repoConfig[key].configured_value;
- } else {
- configInputObj[key] = repoConfig[key];
- }
- }
- }
- return configInputObj;
- }
-
- _handleSaveRepoConfig() {
- return this.$.restAPI.saveRepoConfig(this.repo,
- this._formatRepoConfigForSave(this._repoConfig)).then(() => {
- this._configChanged = false;
- });
- }
-
- _handleConfigChanged() {
- if (this._isLoading()) { return; }
- this._configChanged = true;
- }
-
- _computeButtonDisabled(readOnly, configChanged) {
- return readOnly || !configChanged;
- }
-
- _computeHeaderClass(configChanged) {
- return configChanged ? 'edited' : '';
- }
-
- _computeSchemes(schemesObj) {
- return Object.keys(schemesObj);
- }
-
- _schemesChanged(schemes) {
- if (schemes.length === 0) { return; }
- if (!schemes.includes(this._selectedScheme)) {
- this._selectedScheme = schemes.sort()[0];
- }
- }
-
- _computeCommands(repo, schemesObj, _selectedScheme) {
- if (!schemesObj || !repo || !_selectedScheme) {
- return [];
- }
- const commands = [];
- let commandObj;
- if (schemesObj.hasOwnProperty(_selectedScheme)) {
- commandObj = schemesObj[_selectedScheme].clone_commands;
- }
- for (const title in commandObj) {
- if (!commandObj.hasOwnProperty(title)) { continue; }
- commands.push({
- title,
- command: commandObj[title]
- .replace(/\$\{project\}/gi, encodeURI(repo))
- .replace(/\$\{project-base-name\}/gi,
- encodeURI(repo.substring(repo.lastIndexOf('/') + 1))),
+ // If the user is not an owner, is_owner is not a property.
+ this._readOnly = !access[this.repo].is_owner;
});
}
- return commands;
+ }));
+
+ promises.push(this.$.restAPI.getProjectConfig(this.repo, errFn)
+ .then(config => {
+ if (!config) { return Promise.resolve(); }
+
+ if (config.default_submit_type) {
+ // The gr-select is bound to submit_type, which needs to be the
+ // *configured* submit type. When default_submit_type is
+ // present, the server reports the *effective* submit type in
+ // submit_type, so we need to overwrite it before storing the
+ // config in this.
+ config.submit_type =
+ config.default_submit_type.configured_value;
+ }
+ if (!config.state) {
+ config.state = STATES.active.value;
+ }
+ this._repoConfig = config;
+ this._loading = false;
+ }));
+
+ promises.push(this.$.restAPI.getConfig().then(config => {
+ if (!config) { return Promise.resolve(); }
+
+ this._schemesObj = config.download.schemes;
+ }));
+
+ return Promise.all(promises);
+ }
+
+ _computeLoadingClass(loading) {
+ return loading ? 'loading' : '';
+ }
+
+ _computeHideClass(arr) {
+ return !arr || !arr.length ? 'hide' : '';
+ }
+
+ _loggedInChanged(_loggedIn) {
+ if (!_loggedIn) { return; }
+ this.$.restAPI.getPreferences().then(prefs => {
+ if (prefs.download_scheme) {
+ // Note (issue 5180): normalize the download scheme with lower-case.
+ this._selectedScheme = prefs.download_scheme.toLowerCase();
+ }
+ });
+ }
+
+ _formatBooleanSelect(item) {
+ if (!item) { return; }
+ let inheritLabel = 'Inherit';
+ if (!(item.inherited_value === undefined)) {
+ inheritLabel = `Inherit (${item.inherited_value})`;
+ }
+ return [
+ {
+ label: inheritLabel,
+ value: 'INHERIT',
+ },
+ {
+ label: 'True',
+ value: 'TRUE',
+ }, {
+ label: 'False',
+ value: 'FALSE',
+ },
+ ];
+ }
+
+ _formatSubmitTypeSelect(projectConfig) {
+ if (!projectConfig) { return; }
+ const allValues = Object.values(SUBMIT_TYPES);
+ const type = projectConfig.default_submit_type;
+ if (!type) {
+ // Server is too old to report default_submit_type, so assume INHERIT
+ // is not a valid value.
+ return allValues;
}
- _computeRepositoriesClass(config) {
- return config ? 'showConfig': '';
+ let inheritLabel = 'Inherit';
+ if (type.inherited_value) {
+ let inherited = type.inherited_value;
+ for (const val of allValues) {
+ if (val.value === type.inherited_value) {
+ inherited = val.label;
+ break;
+ }
+ }
+ inheritLabel = `Inherit (${inherited})`;
}
+ return [
+ {
+ label: inheritLabel,
+ value: 'INHERIT',
+ },
+ ...allValues,
+ ];
+ }
- _computeChangesUrl(name) {
- return Gerrit.Nav.getUrlForProjectChanges(name);
+ _isLoading() {
+ return this._loading || this._loading === undefined;
+ }
+
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _formatRepoConfigForSave(repoConfig) {
+ const configInputObj = {};
+ for (const key in repoConfig) {
+ if (repoConfig.hasOwnProperty(key)) {
+ if (key === 'default_submit_type') {
+ // default_submit_type is not in the input type, and the
+ // configured value was already copied to submit_type by
+ // _loadProject. Omit this property when saving.
+ continue;
+ }
+ if (key === 'plugin_config') {
+ configInputObj.plugin_config_values = repoConfig[key];
+ } else if (typeof repoConfig[key] === 'object') {
+ configInputObj[key] = repoConfig[key].configured_value;
+ } else {
+ configInputObj[key] = repoConfig[key];
+ }
+ }
}
+ return configInputObj;
+ }
- _handlePluginConfigChanged({detail: {name, config, notifyPath}}) {
- this._repoConfig.plugin_config[name] = config;
- this.notifyPath('_repoConfig.plugin_config.' + notifyPath);
+ _handleSaveRepoConfig() {
+ return this.$.restAPI.saveRepoConfig(this.repo,
+ this._formatRepoConfigForSave(this._repoConfig)).then(() => {
+ this._configChanged = false;
+ });
+ }
+
+ _handleConfigChanged() {
+ if (this._isLoading()) { return; }
+ this._configChanged = true;
+ }
+
+ _computeButtonDisabled(readOnly, configChanged) {
+ return readOnly || !configChanged;
+ }
+
+ _computeHeaderClass(configChanged) {
+ return configChanged ? 'edited' : '';
+ }
+
+ _computeSchemes(schemesObj) {
+ return Object.keys(schemesObj);
+ }
+
+ _schemesChanged(schemes) {
+ if (schemes.length === 0) { return; }
+ if (!schemes.includes(this._selectedScheme)) {
+ this._selectedScheme = schemes.sort()[0];
}
}
- customElements.define(GrRepo.is, GrRepo);
-})();
+ _computeCommands(repo, schemesObj, _selectedScheme) {
+ if (!schemesObj || !repo || !_selectedScheme) {
+ return [];
+ }
+ const commands = [];
+ let commandObj;
+ if (schemesObj.hasOwnProperty(_selectedScheme)) {
+ commandObj = schemesObj[_selectedScheme].clone_commands;
+ }
+ for (const title in commandObj) {
+ if (!commandObj.hasOwnProperty(title)) { continue; }
+ commands.push({
+ title,
+ command: commandObj[title]
+ .replace(/\$\{project\}/gi, encodeURI(repo))
+ .replace(/\$\{project-base-name\}/gi,
+ encodeURI(repo.substring(repo.lastIndexOf('/') + 1))),
+ });
+ }
+ return commands;
+ }
+
+ _computeRepositoriesClass(config) {
+ return config ? 'showConfig': '';
+ }
+
+ _computeChangesUrl(name) {
+ return Gerrit.Nav.getUrlForProjectChanges(name);
+ }
+
+ _handlePluginConfigChanged({detail: {name, config, notifyPath}}) {
+ this._repoConfig.plugin_config[name] = config;
+ this.notifyPath('_repoConfig.plugin_config.' + notifyPath);
+ }
+}
+
+customElements.define(GrRepo.is, GrRepo);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js
new file mode 100644
index 0000000..2c7540f
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js
@@ -0,0 +1,296 @@
+/**
+ * @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.js';
+
+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-subpage-styles">
+ h2.edited:after {
+ color: var(--deemphasized-text-color);
+ content: ' *';
+ }
+ .loading,
+ .hide {
+ display: none;
+ }
+ #loading.loading {
+ display: block;
+ }
+ #loading:not(.loading) {
+ display: none;
+ }
+ #options .repositorySettings {
+ display: none;
+ }
+ #options .repositorySettings.showConfig {
+ display: block;
+ }
+ </style>
+ <style include="gr-form-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <main class="gr-form-styles read-only">
+ <style include="shared-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <div class="info">
+ <h1 id="Title" class\$="name">
+ [[repo]]
+ <hr>
+ </h1>
+ <div>
+ <a href\$="[[_computeChangesUrl(repo)]]">(view changes)</a>
+ </div>
+ </div>
+ <div id="loading" class\$="[[_computeLoadingClass(_loading)]]">Loading...</div>
+ <div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]">
+ <div id="downloadContent" class\$="[[_computeHideClass(_schemes)]]">
+ <h2 id="download">Download</h2>
+ <fieldset>
+ <gr-download-commands id="downloadCommands" commands="[[_computeCommands(repo, _schemesObj, _selectedScheme)]]" schemes="[[_schemes]]" selected-scheme="{{_selectedScheme}}"></gr-download-commands>
+ </fieldset>
+ </div>
+ <h2 id="configurations" class\$="[[_computeHeaderClass(_configChanged)]]">Configurations</h2>
+ <div id="form">
+ <fieldset>
+ <h3 id="Description">Description</h3>
+ <fieldset>
+ <iron-autogrow-textarea id="descriptionInput" class="description" autocomplete="on" placeholder="<Insert repo description here>" bind-value="{{_repoConfig.description}}" disabled\$="[[_readOnly]]"></iron-autogrow-textarea>
+ </fieldset>
+ <h3 id="Options">Repository Options</h3>
+ <fieldset id="options">
+ <section>
+ <span class="title">State</span>
+ <span class="value">
+ <gr-select id="stateSelect" bind-value="{{_repoConfig.state}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_states]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title">Submit type</span>
+ <span class="value">
+ <gr-select id="submitTypeSelect" bind-value="{{_repoConfig.submit_type}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatSubmitTypeSelect(_repoConfig)]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title">Allow content merges</span>
+ <span class="value">
+ <gr-select id="contentMergeSelect" bind-value="{{_repoConfig.use_content_merge.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.use_content_merge)]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title">
+ Create a new change for every commit not in the target branch
+ </span>
+ <span class="value">
+ <gr-select id="newChangeSelect" bind-value="{{_repoConfig.create_new_change_for_all_not_in_target.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.create_new_change_for_all_not_in_target)]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title">Require Change-Id in commit message</span>
+ <span class="value">
+ <gr-select id="requireChangeIdSelect" bind-value="{{_repoConfig.require_change_id.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.require_change_id)]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section id="enableSignedPushSettings" class\$="repositorySettings [[_computeRepositoriesClass(_repoConfig.enable_signed_push)]]">
+ <span class="title">Enable signed push</span>
+ <span class="value">
+ <gr-select id="enableSignedPush" bind-value="{{_repoConfig.enable_signed_push.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.enable_signed_push)]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section id="requireSignedPushSettings" class\$="repositorySettings [[_computeRepositoriesClass(_repoConfig.require_signed_push)]]">
+ <span class="title">Require signed push</span>
+ <span class="value">
+ <gr-select id="requireSignedPush" bind-value="{{_repoConfig.require_signed_push.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.require_signed_push)]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title">
+ Reject implicit merges when changes are pushed for review</span>
+ <span class="value">
+ <gr-select id="rejectImplicitMergesSelect" bind-value="{{_repoConfig.reject_implicit_merges.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.reject_implicit_merges)]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title">
+ Enable adding unregistered users as reviewers and CCs on changes</span>
+ <span class="value">
+ <gr-select id="unRegisteredCcSelect" bind-value="{{_repoConfig.enable_reviewer_by_email.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.enable_reviewer_by_email)]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title">
+ Set all new changes private by default</span>
+ <span class="value">
+ <gr-select id="setAllnewChangesPrivateByDefaultSelect" bind-value="{{_repoConfig.private_by_default.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.private_by_default)]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title">
+ Set new changes to "work in progress" by default</span>
+ <span class="value">
+ <gr-select id="setAllNewChangesWorkInProgressByDefaultSelect" bind-value="{{_repoConfig.work_in_progress_by_default.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.work_in_progress_by_default)]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title">Maximum Git object size limit</span>
+ <span class="value">
+ <iron-input id="maxGitObjSizeIronInput" bind-value="{{_repoConfig.max_object_size_limit.configured_value}}" type="text" disabled\$="[[_readOnly]]">
+ <input id="maxGitObjSizeInput" bind-value="{{_repoConfig.max_object_size_limit.configured_value}}" is="iron-input" type="text" disabled\$="[[_readOnly]]">
+ </iron-input>
+ <template is="dom-if" if="[[_repoConfig.max_object_size_limit.value]]">
+ effective: [[_repoConfig.max_object_size_limit.value]] bytes
+ </template>
+ </span>
+ </section>
+ <section>
+ <span class="title">Match authored date with committer date upon submit</span>
+ <span class="value">
+ <gr-select id="matchAuthoredDateWithCommitterDateSelect" bind-value="{{_repoConfig.match_author_to_committer_date.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.match_author_to_committer_date)]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title">Reject empty commit upon submit</span>
+ <span class="value">
+ <gr-select id="rejectEmptyCommitSelect" bind-value="{{_repoConfig.reject_empty_commit.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.reject_empty_commit)]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ </fieldset>
+ <h3 id="Options">Contributor Agreements</h3>
+ <fieldset id="agreements">
+ <section>
+ <span class="title">
+ Require a valid contributor agreement to upload</span>
+ <span class="value">
+ <gr-select id="contributorAgreementSelect" bind-value="{{_repoConfig.use_contributor_agreements.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.use_contributor_agreements)]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title">Require Signed-off-by in commit message</span>
+ <span class="value">
+ <gr-select id="useSignedOffBySelect" bind-value="{{_repoConfig.use_signed_off_by.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.use_signed_off_by)]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ </fieldset>
+ <div class\$="pluginConfig [[_computeHideClass(_pluginData)]]" on-plugin-config-changed="_handlePluginConfigChanged">
+ <h3>Plugins</h3>
+ <template is="dom-repeat" items="[[_pluginData]]" as="data">
+ <gr-repo-plugin-config plugin-data="[[data]]"></gr-repo-plugin-config>
+ </template>
+ </div>
+ <gr-button on-click="_handleSaveRepoConfig" disabled\$="[[_computeButtonDisabled(_readOnly, _configChanged)]]">Save changes</gr-button>
+ </fieldset>
+ <gr-endpoint-decorator name="repo-config">
+ <gr-endpoint-param name="repoName" value="[[repo]]"></gr-endpoint-param>
+ <gr-endpoint-param name="readOnly" value="[[_readOnly]]"></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </div>
+ </div>
+ </main>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
index da0c271..1dccb7a 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-repo.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,367 +30,370 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let repoStub;
- const repoConf = {
- description: 'Access inherited by all other projects.',
- use_contributor_agreements: {
- value: false,
- configured_value: 'FALSE',
- },
- use_content_merge: {
- value: false,
- configured_value: 'FALSE',
- },
- use_signed_off_by: {
- value: false,
- configured_value: 'FALSE',
- },
- create_new_change_for_all_not_in_target: {
- value: false,
- configured_value: 'FALSE',
- },
- require_change_id: {
- value: false,
- configured_value: 'FALSE',
- },
- enable_signed_push: {
- value: false,
- configured_value: 'FALSE',
- },
- require_signed_push: {
- value: false,
- configured_value: 'FALSE',
- },
- reject_implicit_merges: {
- value: false,
- configured_value: 'FALSE',
- },
- private_by_default: {
- value: false,
- configured_value: 'FALSE',
- },
- match_author_to_committer_date: {
- value: false,
- configured_value: 'FALSE',
- },
- reject_empty_commit: {
- value: false,
- configured_value: 'FALSE',
- },
- enable_reviewer_by_email: {
- value: false,
- configured_value: 'FALSE',
- },
- max_object_size_limit: {},
- submit_type: 'MERGE_IF_NECESSARY',
- default_submit_type: {
- value: 'MERGE_IF_NECESSARY',
- configured_value: 'INHERIT',
- inherited_value: 'MERGE_IF_NECESSARY',
- },
- };
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-repo.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+suite('gr-repo tests', () => {
+ let element;
+ let sandbox;
+ let repoStub;
+ const repoConf = {
+ description: 'Access inherited by all other projects.',
+ use_contributor_agreements: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ use_content_merge: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ use_signed_off_by: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ create_new_change_for_all_not_in_target: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ require_change_id: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ enable_signed_push: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ require_signed_push: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ reject_implicit_merges: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ private_by_default: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ match_author_to_committer_date: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ reject_empty_commit: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ enable_reviewer_by_email: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ max_object_size_limit: {},
+ submit_type: 'MERGE_IF_NECESSARY',
+ default_submit_type: {
+ value: 'MERGE_IF_NECESSARY',
+ configured_value: 'INHERIT',
+ inherited_value: 'MERGE_IF_NECESSARY',
+ },
+ };
- const REPO = 'test-repo';
- const SCHEMES = {http: {}, repo: {}, ssh: {}};
+ const REPO = 'test-repo';
+ const SCHEMES = {http: {}, repo: {}, ssh: {}};
- function getFormFields() {
- const selects = Array.from(
- Polymer.dom(element.root).querySelectorAll('select'));
- const textareas = Array.from(
- Polymer.dom(element.root).querySelectorAll('iron-autogrow-textarea'));
- const inputs = Array.from(
- Polymer.dom(element.root).querySelectorAll('input'));
- return inputs.concat(textareas).concat(selects);
- }
+ function getFormFields() {
+ const selects = Array.from(
+ dom(element.root).querySelectorAll('select'));
+ const textareas = Array.from(
+ dom(element.root).querySelectorAll('iron-autogrow-textarea'));
+ const inputs = Array.from(
+ dom(element.root).querySelectorAll('input'));
+ return inputs.concat(textareas).concat(selects);
+ }
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(false); },
- getConfig() {
- return Promise.resolve({download: {}});
- },
- });
- element = fixture('basic');
- repoStub = sandbox.stub(
- element.$.restAPI,
- 'getProjectConfig',
- () => Promise.resolve(repoConf));
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(false); },
+ getConfig() {
+ return Promise.resolve({download: {}});
+ },
});
+ element = fixture('basic');
+ repoStub = sandbox.stub(
+ element.$.restAPI,
+ 'getProjectConfig',
+ () => Promise.resolve(repoConf));
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('_computePluginData', () => {
- assert.deepEqual(element._computePluginData(), []);
- assert.deepEqual(element._computePluginData({}), []);
- assert.deepEqual(element._computePluginData({base: {}}), []);
- assert.deepEqual(element._computePluginData({base: {plugin: 'data'}}),
- [{name: 'plugin', config: 'data'}]);
- });
+ test('_computePluginData', () => {
+ assert.deepEqual(element._computePluginData(), []);
+ assert.deepEqual(element._computePluginData({}), []);
+ assert.deepEqual(element._computePluginData({base: {}}), []);
+ assert.deepEqual(element._computePluginData({base: {plugin: 'data'}}),
+ [{name: 'plugin', config: 'data'}]);
+ });
- test('_handlePluginConfigChanged', () => {
- const notifyStub = sandbox.stub(element, 'notifyPath');
- element._repoConfig = {plugin_config: {}};
- element._handlePluginConfigChanged({detail: {
- name: 'test',
- config: 'data',
- notifyPath: 'path',
- }});
- flushAsynchronousOperations();
+ test('_handlePluginConfigChanged', () => {
+ const notifyStub = sandbox.stub(element, 'notifyPath');
+ element._repoConfig = {plugin_config: {}};
+ element._handlePluginConfigChanged({detail: {
+ name: 'test',
+ config: 'data',
+ notifyPath: 'path',
+ }});
+ flushAsynchronousOperations();
- assert.equal(element._repoConfig.plugin_config.test, 'data');
- assert.equal(notifyStub.lastCall.args[0],
- '_repoConfig.plugin_config.path');
- });
+ assert.equal(element._repoConfig.plugin_config.test, 'data');
+ assert.equal(notifyStub.lastCall.args[0],
+ '_repoConfig.plugin_config.path');
+ });
- test('loading displays before repo config is loaded', () => {
- assert.isTrue(element.$.loading.classList.contains('loading'));
- assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
- assert.isTrue(element.$.loadedContent.classList.contains('loading'));
- assert.isTrue(getComputedStyle(element.$.loadedContent)
- .display === 'none');
- });
+ test('loading displays before repo config is loaded', () => {
+ assert.isTrue(element.$.loading.classList.contains('loading'));
+ assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
+ assert.isTrue(element.$.loadedContent.classList.contains('loading'));
+ assert.isTrue(getComputedStyle(element.$.loadedContent)
+ .display === 'none');
+ });
- test('download commands visibility', () => {
- element._loading = false;
- flushAsynchronousOperations();
- assert.isTrue(element.$.downloadContent.classList.contains('hide'));
- assert.isTrue(getComputedStyle(element.$.downloadContent)
- .display == 'none');
- element._schemesObj = SCHEMES;
- flushAsynchronousOperations();
- assert.isFalse(element.$.downloadContent.classList.contains('hide'));
- assert.isFalse(getComputedStyle(element.$.downloadContent)
- .display == 'none');
- });
+ test('download commands visibility', () => {
+ element._loading = false;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.downloadContent.classList.contains('hide'));
+ assert.isTrue(getComputedStyle(element.$.downloadContent)
+ .display == 'none');
+ element._schemesObj = SCHEMES;
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.downloadContent.classList.contains('hide'));
+ assert.isFalse(getComputedStyle(element.$.downloadContent)
+ .display == 'none');
+ });
- test('form defaults to read only', () => {
+ test('form defaults to read only', () => {
+ assert.isTrue(element._readOnly);
+ });
+
+ test('form defaults to read only when not logged in', done => {
+ element.repo = REPO;
+ element._loadRepo().then(() => {
assert.isTrue(element._readOnly);
+ done();
+ });
+ });
+
+ test('form defaults to read only when logged in and not admin', done => {
+ element.repo = REPO;
+ sandbox.stub(element, '_getLoggedIn', () => Promise.resolve(true));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getRepoAccess',
+ () => Promise.resolve({'test-repo': {}}));
+ element._loadRepo().then(() => {
+ assert.isTrue(element._readOnly);
+ done();
+ });
+ });
+
+ test('all form elements are disabled when not admin', done => {
+ element.repo = REPO;
+ element._loadRepo().then(() => {
+ flushAsynchronousOperations();
+ const formFields = getFormFields();
+ for (const field of formFields) {
+ assert.isTrue(field.hasAttribute('disabled'));
+ }
+ done();
+ });
+ });
+
+ test('_formatBooleanSelect', () => {
+ let item = {inherited_value: true};
+ assert.deepEqual(element._formatBooleanSelect(item), [
+ {
+ label: 'Inherit (true)',
+ value: 'INHERIT',
+ },
+ {
+ label: 'True',
+ value: 'TRUE',
+ }, {
+ label: 'False',
+ value: 'FALSE',
+ },
+ ]);
+
+ item = {inherited_value: false};
+ assert.deepEqual(element._formatBooleanSelect(item), [
+ {
+ label: 'Inherit (false)',
+ value: 'INHERIT',
+ },
+ {
+ label: 'True',
+ value: 'TRUE',
+ }, {
+ label: 'False',
+ value: 'FALSE',
+ },
+ ]);
+
+ // For items without inherited values
+ item = {};
+ assert.deepEqual(element._formatBooleanSelect(item), [
+ {
+ label: 'Inherit',
+ value: 'INHERIT',
+ },
+ {
+ label: 'True',
+ value: 'TRUE',
+ }, {
+ label: 'False',
+ value: 'FALSE',
+ },
+ ]);
+ });
+
+ test('fires page-error', done => {
+ repoStub.restore();
+
+ element.repo = 'test';
+
+ const response = {status: 404};
+ sandbox.stub(
+ element.$.restAPI, 'getProjectConfig', (repo, errFn) => {
+ errFn(response);
+ });
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
});
- test('form defaults to read only when not logged in', done => {
- element.repo = REPO;
- element._loadRepo().then(() => {
- assert.isTrue(element._readOnly);
- done();
- });
- });
+ element._loadRepo();
+ });
- test('form defaults to read only when logged in and not admin', done => {
+ suite('admin', () => {
+ setup(() => {
element.repo = REPO;
sandbox.stub(element, '_getLoggedIn', () => Promise.resolve(true));
sandbox.stub(
element.$.restAPI,
'getRepoAccess',
- () => Promise.resolve({'test-repo': {}}));
- element._loadRepo().then(() => {
- assert.isTrue(element._readOnly);
- done();
- });
+ () => Promise.resolve({'test-repo': {is_owner: true}}));
});
- test('all form elements are disabled when not admin', done => {
- element.repo = REPO;
+ test('all form elements are enabled', done => {
element._loadRepo().then(() => {
flushAsynchronousOperations();
const formFields = getFormFields();
for (const field of formFields) {
- assert.isTrue(field.hasAttribute('disabled'));
+ assert.isFalse(field.hasAttribute('disabled'));
}
+ assert.isFalse(element._loading);
done();
});
});
- test('_formatBooleanSelect', () => {
- let item = {inherited_value: true};
- assert.deepEqual(element._formatBooleanSelect(item), [
- {
- label: 'Inherit (true)',
- value: 'INHERIT',
- },
- {
- label: 'True',
- value: 'TRUE',
- }, {
- label: 'False',
- value: 'FALSE',
- },
- ]);
-
- item = {inherited_value: false};
- assert.deepEqual(element._formatBooleanSelect(item), [
- {
- label: 'Inherit (false)',
- value: 'INHERIT',
- },
- {
- label: 'True',
- value: 'TRUE',
- }, {
- label: 'False',
- value: 'FALSE',
- },
- ]);
-
- // For items without inherited values
- item = {};
- assert.deepEqual(element._formatBooleanSelect(item), [
- {
- label: 'Inherit',
- value: 'INHERIT',
- },
- {
- label: 'True',
- value: 'TRUE',
- }, {
- label: 'False',
- value: 'FALSE',
- },
- ]);
+ test('state gets set correctly', done => {
+ element._loadRepo().then(() => {
+ assert.equal(element._repoConfig.state, 'ACTIVE');
+ assert.equal(element.$.stateSelect.bindValue, 'ACTIVE');
+ done();
+ });
});
- test('fires page-error', done => {
- repoStub.restore();
-
- element.repo = 'test';
-
- const response = {status: 404};
- sandbox.stub(
- element.$.restAPI, 'getProjectConfig', (repo, errFn) => {
- errFn(response);
+ test('inherited submit type value is calculated correctly', done => {
+ element
+ ._loadRepo().then(() => {
+ const sel = element.$.submitTypeSelect;
+ assert.equal(sel.bindValue, 'INHERIT');
+ assert.equal(
+ sel.nativeSelect.options[0].text,
+ 'Inherit (Merge if necessary)'
+ );
+ done();
});
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- element._loadRepo();
});
- suite('admin', () => {
- setup(() => {
- element.repo = REPO;
- sandbox.stub(element, '_getLoggedIn', () => Promise.resolve(true));
- sandbox.stub(
- element.$.restAPI,
- 'getRepoAccess',
- () => Promise.resolve({'test-repo': {is_owner: true}}));
- });
+ test('fields update and save correctly', () => {
+ const configInputObj = {
+ description: 'new description',
+ use_contributor_agreements: 'TRUE',
+ use_content_merge: 'TRUE',
+ use_signed_off_by: 'TRUE',
+ create_new_change_for_all_not_in_target: 'TRUE',
+ require_change_id: 'TRUE',
+ enable_signed_push: 'TRUE',
+ require_signed_push: 'TRUE',
+ reject_implicit_merges: 'TRUE',
+ private_by_default: 'TRUE',
+ match_author_to_committer_date: 'TRUE',
+ reject_empty_commit: 'TRUE',
+ max_object_size_limit: 10,
+ submit_type: 'FAST_FORWARD_ONLY',
+ state: 'READ_ONLY',
+ enable_reviewer_by_email: 'TRUE',
+ };
- test('all form elements are enabled', done => {
- element._loadRepo().then(() => {
- flushAsynchronousOperations();
- const formFields = getFormFields();
- for (const field of formFields) {
- assert.isFalse(field.hasAttribute('disabled'));
- }
- assert.isFalse(element._loading);
- done();
- });
- });
+ const saveStub = sandbox.stub(element.$.restAPI, 'saveRepoConfig'
+ , () => Promise.resolve({}));
- test('state gets set correctly', done => {
- element._loadRepo().then(() => {
- assert.equal(element._repoConfig.state, 'ACTIVE');
- assert.equal(element.$.stateSelect.bindValue, 'ACTIVE');
- done();
- });
- });
+ const button = dom(element.root).querySelector('gr-button');
- test('inherited submit type value is calculated correctly', done => {
- element
- ._loadRepo().then(() => {
- const sel = element.$.submitTypeSelect;
- assert.equal(sel.bindValue, 'INHERIT');
- assert.equal(
- sel.nativeSelect.options[0].text,
- 'Inherit (Merge if necessary)'
- );
- done();
- });
- });
+ return element._loadRepo().then(() => {
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(element.$.Title.classList.contains('edited'));
+ element.$.descriptionInput.bindValue = configInputObj.description;
+ element.$.stateSelect.bindValue = configInputObj.state;
+ element.$.submitTypeSelect.bindValue = configInputObj.submit_type;
+ element.$.contentMergeSelect.bindValue =
+ configInputObj.use_content_merge;
+ element.$.newChangeSelect.bindValue =
+ configInputObj.create_new_change_for_all_not_in_target;
+ element.$.requireChangeIdSelect.bindValue =
+ configInputObj.require_change_id;
+ element.$.enableSignedPush.bindValue =
+ configInputObj.enable_signed_push;
+ element.$.requireSignedPush.bindValue =
+ configInputObj.require_signed_push;
+ element.$.rejectImplicitMergesSelect.bindValue =
+ configInputObj.reject_implicit_merges;
+ element.$.setAllnewChangesPrivateByDefaultSelect.bindValue =
+ configInputObj.private_by_default;
+ element.$.matchAuthoredDateWithCommitterDateSelect.bindValue =
+ configInputObj.match_author_to_committer_date;
+ const inputElement = PolymerElement ?
+ element.$.maxGitObjSizeIronInput : element.$.maxGitObjSizeInput;
+ inputElement.bindValue = configInputObj.max_object_size_limit;
+ element.$.contributorAgreementSelect.bindValue =
+ configInputObj.use_contributor_agreements;
+ element.$.useSignedOffBySelect.bindValue =
+ configInputObj.use_signed_off_by;
+ element.$.rejectEmptyCommitSelect.bindValue =
+ configInputObj.reject_empty_commit;
+ element.$.unRegisteredCcSelect.bindValue =
+ configInputObj.enable_reviewer_by_email;
- test('fields update and save correctly', () => {
- const configInputObj = {
- description: 'new description',
- use_contributor_agreements: 'TRUE',
- use_content_merge: 'TRUE',
- use_signed_off_by: 'TRUE',
- create_new_change_for_all_not_in_target: 'TRUE',
- require_change_id: 'TRUE',
- enable_signed_push: 'TRUE',
- require_signed_push: 'TRUE',
- reject_implicit_merges: 'TRUE',
- private_by_default: 'TRUE',
- match_author_to_committer_date: 'TRUE',
- reject_empty_commit: 'TRUE',
- max_object_size_limit: 10,
- submit_type: 'FAST_FORWARD_ONLY',
- state: 'READ_ONLY',
- enable_reviewer_by_email: 'TRUE',
- };
+ assert.isFalse(button.hasAttribute('disabled'));
+ assert.isTrue(element.$.configurations.classList.contains('edited'));
- const saveStub = sandbox.stub(element.$.restAPI, 'saveRepoConfig'
- , () => Promise.resolve({}));
+ const formattedObj =
+ element._formatRepoConfigForSave(element._repoConfig);
+ assert.deepEqual(formattedObj, configInputObj);
- const button = Polymer.dom(element.root).querySelector('gr-button');
-
- return element._loadRepo().then(() => {
+ return element._handleSaveRepoConfig().then(() => {
assert.isTrue(button.hasAttribute('disabled'));
assert.isFalse(element.$.Title.classList.contains('edited'));
- element.$.descriptionInput.bindValue = configInputObj.description;
- element.$.stateSelect.bindValue = configInputObj.state;
- element.$.submitTypeSelect.bindValue = configInputObj.submit_type;
- element.$.contentMergeSelect.bindValue =
- configInputObj.use_content_merge;
- element.$.newChangeSelect.bindValue =
- configInputObj.create_new_change_for_all_not_in_target;
- element.$.requireChangeIdSelect.bindValue =
- configInputObj.require_change_id;
- element.$.enableSignedPush.bindValue =
- configInputObj.enable_signed_push;
- element.$.requireSignedPush.bindValue =
- configInputObj.require_signed_push;
- element.$.rejectImplicitMergesSelect.bindValue =
- configInputObj.reject_implicit_merges;
- element.$.setAllnewChangesPrivateByDefaultSelect.bindValue =
- configInputObj.private_by_default;
- element.$.matchAuthoredDateWithCommitterDateSelect.bindValue =
- configInputObj.match_author_to_committer_date;
- const inputElement = Polymer.Element ?
- element.$.maxGitObjSizeIronInput : element.$.maxGitObjSizeInput;
- inputElement.bindValue = configInputObj.max_object_size_limit;
- element.$.contributorAgreementSelect.bindValue =
- configInputObj.use_contributor_agreements;
- element.$.useSignedOffBySelect.bindValue =
- configInputObj.use_signed_off_by;
- element.$.rejectEmptyCommitSelect.bindValue =
- configInputObj.reject_empty_commit;
- element.$.unRegisteredCcSelect.bindValue =
- configInputObj.enable_reviewer_by_email;
-
- assert.isFalse(button.hasAttribute('disabled'));
- assert.isTrue(element.$.configurations.classList.contains('edited'));
-
- const formattedObj =
- element._formatRepoConfigForSave(element._repoConfig);
- assert.deepEqual(formattedObj, configInputObj);
-
- return element._handleSaveRepoConfig().then(() => {
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- assert.isTrue(saveStub.lastCall.calledWithExactly(REPO,
- configInputObj));
- });
+ assert.isTrue(saveStub.lastCall.calledWithExactly(REPO,
+ configInputObj));
});
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
deleted file mode 100644
index 9820e31..0000000
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
+++ /dev/null
@@ -1,164 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-rule-editor">
- <template>
- <style include="shared-styles">
- :host {
- border-bottom: 1px solid var(--border-color);
- padding: var(--spacing-m);
- display: block;
- }
- #removeBtn {
- display: none;
- }
- .editing #removeBtn {
- display: flex;
- }
- #options {
- align-items: baseline;
- display: flex;
- }
- #options > * {
- margin-right: var(--spacing-m);
- }
- #mainContainer {
- align-items: baseline;
- display: flex;
- flex-wrap: nowrap;
- justify-content: space-between;
- }
- #deletedContainer.deleted {
- align-items: baseline;
- display: flex;
- justify-content: space-between;
- }
- #undoBtn,
- #force,
- #deletedContainer,
- #mainContainer.deleted {
- display: none;
- }
- #undoBtn.modified,
- #force.force {
- display: block;
- }
- .groupPath {
- color: var(--deemphasized-text-color);
- }
- </style>
- <style include="gr-form-styles">
- iron-autogrow-textarea {
- width: 14em;
- }
- </style>
- <div id="mainContainer"
- class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
- <div id="options">
- <gr-select id="action"
- bind-value="{{rule.value.action}}"
- on-change="_handleValueChange">
- <select disabled$="[[!editing]]">
- <template is="dom-repeat" items="[[_computeOptions(permission)]]">
- <option value="[[item]]">[[item]]</option>
- </template>
- </select>
- </gr-select>
- <template is="dom-if" if="[[label]]">
- <gr-select
- id="labelMin"
- bind-value="{{rule.value.min}}"
- on-change="_handleValueChange">
- <select disabled$="[[!editing]]">
- <template is="dom-repeat" items="[[label.values]]">
- <option value="[[item.value]]">[[item.value]]</option>
- </template>
- </select>
- </gr-select>
- <gr-select
- id="labelMax"
- bind-value="{{rule.value.max}}"
- on-change="_handleValueChange">
- <select disabled$="[[!editing]]">
- <template is="dom-repeat" items="[[label.values]]">
- <option value="[[item.value]]">[[item.value]]</option>
- </template>
- </select>
- </gr-select>
- </template>
- <template is="dom-if" if="[[hasRange]]">
- <iron-autogrow-textarea
- id="minInput"
- class="min"
- autocomplete="on"
- placeholder="Min value"
- bind-value="{{rule.value.min}}"
- disabled$="[[!editing]]"></iron-autogrow-textarea>
- <iron-autogrow-textarea
- id="maxInput"
- class="max"
- autocomplete="on"
- placeholder="Max value"
- bind-value="{{rule.value.max}}"
- disabled$="[[!editing]]"></iron-autogrow-textarea>
- </template>
- <a class="groupPath" href$="[[_computeGroupPath(groupId)]]">
- [[groupName]]
- </a>
- <gr-select
- id="force"
- class$="[[_computeForceClass(permission, rule.value.action)]]"
- bind-value="{{rule.value.force}}"
- on-change="_handleValueChange">
- <select disabled$="[[!editing]]">
- <template
- is="dom-repeat"
- items="[[_computeForceOptions(permission, rule.value.action)]]">
- <option value="[[item.value]]">[[item.name]]</option>
- </template>
- </select>
- </gr-select>
- </div>
- <gr-button
- link
- id="removeBtn"
- on-click="_handleRemoveRule">Remove</gr-button>
- </div>
- <div
- id="deletedContainer"
- class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
- [[groupName]] was deleted
- <gr-button link
- id="undoRemoveBtn" on-click="_handleUndoRemove">Undo</gr-button>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-rule-editor.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
index ac98d33..2421c46 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
@@ -14,270 +14,286 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-select/gr-select.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-rule-editor_html.js';
+
+/**
+ * Fired when the rule has been modified or removed.
+ *
+ * @event access-modified
+ */
+
+/**
+ * Fired when a rule that was previously added was removed.
+ *
+ * @event added-rule-removed
+ */
+
+const PRIORITY_OPTIONS = [
+ 'BATCH',
+ 'INTERACTIVE',
+];
+
+const Action = {
+ ALLOW: 'ALLOW',
+ DENY: 'DENY',
+ BLOCK: 'BLOCK',
+};
+
+const DROPDOWN_OPTIONS = [Action.ALLOW, Action.DENY, Action.BLOCK];
+
+const ForcePushOptions = {
+ ALLOW: [
+ {name: 'Allow pushing (but not force pushing)', value: false},
+ {name: 'Allow pushing with or without force', value: true},
+ ],
+ BLOCK: [
+ {name: 'Block pushing with or without force', value: false},
+ {name: 'Block force pushing', value: true},
+ ],
+};
+
+const FORCE_EDIT_OPTIONS = [
+ {
+ name: 'No Force Edit',
+ value: false,
+ },
+ {
+ name: 'Force Edit',
+ value: true,
+ },
+];
+
+/**
+ * @appliesMixin Gerrit.AccessMixin
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrRuleEditor extends mixinBehaviors( [
+ Gerrit.AccessBehavior,
+ Gerrit.BaseUrlBehavior,
/**
- * Fired when the rule has been modified or removed.
- *
- * @event access-modified
+ * Unused in this element, but called by other elements in tests
+ * e.g gr-permission_test.
*/
+ Gerrit.FireBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /**
- * Fired when a rule that was previously added was removed.
- *
- * @event added-rule-removed
- */
+ static get is() { return 'gr-rule-editor'; }
- const PRIORITY_OPTIONS = [
- 'BATCH',
- 'INTERACTIVE',
- ];
+ static get properties() {
+ return {
+ hasRange: Boolean,
+ /** @type {?} */
+ label: Object,
+ editing: {
+ type: Boolean,
+ value: false,
+ observer: '_handleEditingChanged',
+ },
+ groupId: String,
+ groupName: String,
+ permission: String,
+ /** @type {?} */
+ rule: {
+ type: Object,
+ notify: true,
+ },
+ section: String,
- const Action = {
- ALLOW: 'ALLOW',
- DENY: 'DENY',
- BLOCK: 'BLOCK',
- };
+ _deleted: {
+ type: Boolean,
+ value: false,
+ },
+ _originalRuleValues: Object,
+ };
+ }
- const DROPDOWN_OPTIONS = [Action.ALLOW, Action.DENY, Action.BLOCK];
+ static get observers() {
+ return [
+ '_handleValueChange(rule.value.*)',
+ ];
+ }
- const ForcePushOptions = {
- ALLOW: [
- {name: 'Allow pushing (but not force pushing)', value: false},
- {name: 'Allow pushing with or without force', value: true},
- ],
- BLOCK: [
- {name: 'Block pushing with or without force', value: false},
- {name: 'Block force pushing', value: true},
- ],
- };
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('access-saved',
+ () => this._handleAccessSaved());
+ }
- const FORCE_EDIT_OPTIONS = [
- {
- name: 'No Force Edit',
- value: false,
- },
- {
- name: 'Force Edit',
- value: true,
- },
- ];
+ /** @override */
+ ready() {
+ super.ready();
+ // Called on ready rather than the observer because when new rules are
+ // added, the observer is triggered prior to being ready.
+ if (!this.rule) { return; } // Check needed for test purposes.
+ this._setupValues(this.rule);
+ }
- /**
- * @appliesMixin Gerrit.AccessMixin
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrRuleEditor extends Polymer.mixinBehaviors( [
- Gerrit.AccessBehavior,
- Gerrit.BaseUrlBehavior,
- /**
- * Unused in this element, but called by other elements in tests
- * e.g gr-permission_test.
- */
- Gerrit.FireBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-rule-editor'; }
-
- static get properties() {
- return {
- hasRange: Boolean,
- /** @type {?} */
- label: Object,
- editing: {
- type: Boolean,
- value: false,
- observer: '_handleEditingChanged',
- },
- groupId: String,
- groupName: String,
- permission: String,
- /** @type {?} */
- rule: {
- type: Object,
- notify: true,
- },
- section: String,
-
- _deleted: {
- type: Boolean,
- value: false,
- },
- _originalRuleValues: Object,
- };
- }
-
- static get observers() {
- return [
- '_handleValueChange(rule.value.*)',
- ];
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('access-saved',
- () => this._handleAccessSaved());
- }
-
- /** @override */
- ready() {
- super.ready();
- // Called on ready rather than the observer because when new rules are
- // added, the observer is triggered prior to being ready.
- if (!this.rule) { return; } // Check needed for test purposes.
- this._setupValues(this.rule);
- }
-
- /** @override */
- attached() {
- super.attached();
- if (!this.rule) { return; } // Check needed for test purposes.
- if (!this._originalRuleValues) {
- // Observer _handleValueChange is called after the ready()
- // method finishes. Original values must be set later to
- // avoid set .modified flag to true
- this._setOriginalRuleValues(this.rule.value);
- }
- }
-
- _setupValues(rule) {
- if (!rule.value) {
- this._setDefaultRuleValues();
- }
- }
-
- _computeForce(permission, action) {
- if (this.permissionValues.push.id === permission &&
- action !== Action.DENY) {
- return true;
- }
-
- return this.permissionValues.editTopicName.id === permission;
- }
-
- _computeForceClass(permission, action) {
- return this._computeForce(permission, action) ? 'force' : '';
- }
-
- _computeGroupPath(group) {
- return `${this.getBaseUrl()}/admin/groups/${this.encodeURL(group, true)}`;
- }
-
- _handleAccessSaved() {
- // Set a new 'original' value to keep track of after the value has been
- // saved.
+ /** @override */
+ attached() {
+ super.attached();
+ if (!this.rule) { return; } // Check needed for test purposes.
+ if (!this._originalRuleValues) {
+ // Observer _handleValueChange is called after the ready()
+ // method finishes. Original values must be set later to
+ // avoid set .modified flag to true
this._setOriginalRuleValues(this.rule.value);
}
-
- _handleEditingChanged(editing, editingOld) {
- // Ignore when editing gets set initially.
- if (!editingOld) { return; }
- // Restore original values if no longer editing.
- if (!editing) {
- this._handleUndoChange();
- }
- }
-
- _computeSectionClass(editing, deleted) {
- const classList = [];
- if (editing) {
- classList.push('editing');
- }
- if (deleted) {
- classList.push('deleted');
- }
- return classList.join(' ');
- }
-
- _computeForceOptions(permission, action) {
- if (permission === this.permissionValues.push.id) {
- if (action === Action.ALLOW) {
- return ForcePushOptions.ALLOW;
- } else if (action === Action.BLOCK) {
- return ForcePushOptions.BLOCK;
- } else {
- return [];
- }
- } else if (permission === this.permissionValues.editTopicName.id) {
- return FORCE_EDIT_OPTIONS;
- }
- return [];
- }
-
- _getDefaultRuleValues(permission, label) {
- const ruleAction = Action.ALLOW;
- const value = {};
- if (permission === 'priority') {
- value.action = PRIORITY_OPTIONS[0];
- return value;
- } else if (label) {
- value.min = label.values[0].value;
- value.max = label.values[label.values.length - 1].value;
- } else if (this._computeForce(permission, ruleAction)) {
- value.force =
- this._computeForceOptions(permission, ruleAction)[0].value;
- }
- value.action = DROPDOWN_OPTIONS[0];
- return value;
- }
-
- _setDefaultRuleValues() {
- this.set('rule.value', this._getDefaultRuleValues(this.permission,
- this.label));
- }
-
- _computeOptions(permission) {
- if (permission === 'priority') {
- return PRIORITY_OPTIONS;
- }
- return DROPDOWN_OPTIONS;
- }
-
- _handleRemoveRule() {
- if (this.rule.value.added) {
- this.dispatchEvent(new CustomEvent(
- 'added-rule-removed', {bubbles: true, composed: true}));
- }
- this._deleted = true;
- this.rule.value.deleted = true;
- this.dispatchEvent(
- new CustomEvent('access-modified', {bubbles: true, composed: true}));
- }
-
- _handleUndoRemove() {
- this._deleted = false;
- delete this.rule.value.deleted;
- }
-
- _handleUndoChange() {
- // gr-permission will take care of removing rules that were added but
- // unsaved. We need to keep the added bit for the filter.
- if (this.rule.value.added) { return; }
- this.set('rule.value', Object.assign({}, this._originalRuleValues));
- this._deleted = false;
- delete this.rule.value.deleted;
- delete this.rule.value.modified;
- }
-
- _handleValueChange() {
- if (!this._originalRuleValues) { return; }
- this.rule.value.modified = true;
- // Allows overall access page to know a change has been made.
- this.dispatchEvent(
- new CustomEvent('access-modified', {bubbles: true, composed: true}));
- }
-
- _setOriginalRuleValues(value) {
- this._originalRuleValues = Object.assign({}, value);
- }
}
- customElements.define(GrRuleEditor.is, GrRuleEditor);
-})();
+ _setupValues(rule) {
+ if (!rule.value) {
+ this._setDefaultRuleValues();
+ }
+ }
+
+ _computeForce(permission, action) {
+ if (this.permissionValues.push.id === permission &&
+ action !== Action.DENY) {
+ return true;
+ }
+
+ return this.permissionValues.editTopicName.id === permission;
+ }
+
+ _computeForceClass(permission, action) {
+ return this._computeForce(permission, action) ? 'force' : '';
+ }
+
+ _computeGroupPath(group) {
+ return `${this.getBaseUrl()}/admin/groups/${this.encodeURL(group, true)}`;
+ }
+
+ _handleAccessSaved() {
+ // Set a new 'original' value to keep track of after the value has been
+ // saved.
+ this._setOriginalRuleValues(this.rule.value);
+ }
+
+ _handleEditingChanged(editing, editingOld) {
+ // Ignore when editing gets set initially.
+ if (!editingOld) { return; }
+ // Restore original values if no longer editing.
+ if (!editing) {
+ this._handleUndoChange();
+ }
+ }
+
+ _computeSectionClass(editing, deleted) {
+ const classList = [];
+ if (editing) {
+ classList.push('editing');
+ }
+ if (deleted) {
+ classList.push('deleted');
+ }
+ return classList.join(' ');
+ }
+
+ _computeForceOptions(permission, action) {
+ if (permission === this.permissionValues.push.id) {
+ if (action === Action.ALLOW) {
+ return ForcePushOptions.ALLOW;
+ } else if (action === Action.BLOCK) {
+ return ForcePushOptions.BLOCK;
+ } else {
+ return [];
+ }
+ } else if (permission === this.permissionValues.editTopicName.id) {
+ return FORCE_EDIT_OPTIONS;
+ }
+ return [];
+ }
+
+ _getDefaultRuleValues(permission, label) {
+ const ruleAction = Action.ALLOW;
+ const value = {};
+ if (permission === 'priority') {
+ value.action = PRIORITY_OPTIONS[0];
+ return value;
+ } else if (label) {
+ value.min = label.values[0].value;
+ value.max = label.values[label.values.length - 1].value;
+ } else if (this._computeForce(permission, ruleAction)) {
+ value.force =
+ this._computeForceOptions(permission, ruleAction)[0].value;
+ }
+ value.action = DROPDOWN_OPTIONS[0];
+ return value;
+ }
+
+ _setDefaultRuleValues() {
+ this.set('rule.value', this._getDefaultRuleValues(this.permission,
+ this.label));
+ }
+
+ _computeOptions(permission) {
+ if (permission === 'priority') {
+ return PRIORITY_OPTIONS;
+ }
+ return DROPDOWN_OPTIONS;
+ }
+
+ _handleRemoveRule() {
+ if (this.rule.value.added) {
+ this.dispatchEvent(new CustomEvent(
+ 'added-rule-removed', {bubbles: true, composed: true}));
+ }
+ this._deleted = true;
+ this.rule.value.deleted = true;
+ this.dispatchEvent(
+ new CustomEvent('access-modified', {bubbles: true, composed: true}));
+ }
+
+ _handleUndoRemove() {
+ this._deleted = false;
+ delete this.rule.value.deleted;
+ }
+
+ _handleUndoChange() {
+ // gr-permission will take care of removing rules that were added but
+ // unsaved. We need to keep the added bit for the filter.
+ if (this.rule.value.added) { return; }
+ this.set('rule.value', Object.assign({}, this._originalRuleValues));
+ this._deleted = false;
+ delete this.rule.value.deleted;
+ delete this.rule.value.modified;
+ }
+
+ _handleValueChange() {
+ if (!this._originalRuleValues) { return; }
+ this.rule.value.modified = true;
+ // Allows overall access page to know a change has been made.
+ this.dispatchEvent(
+ new CustomEvent('access-modified', {bubbles: true, composed: true}));
+ }
+
+ _setOriginalRuleValues(value) {
+ this._originalRuleValues = Object.assign({}, value);
+ }
+}
+
+customElements.define(GrRuleEditor.is, GrRuleEditor);
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.js
new file mode 100644
index 0000000..4ea13b1
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.js
@@ -0,0 +1,116 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ border-bottom: 1px solid var(--border-color);
+ padding: var(--spacing-m);
+ display: block;
+ }
+ #removeBtn {
+ display: none;
+ }
+ .editing #removeBtn {
+ display: flex;
+ }
+ #options {
+ align-items: baseline;
+ display: flex;
+ }
+ #options > * {
+ margin-right: var(--spacing-m);
+ }
+ #mainContainer {
+ align-items: baseline;
+ display: flex;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ }
+ #deletedContainer.deleted {
+ align-items: baseline;
+ display: flex;
+ justify-content: space-between;
+ }
+ #undoBtn,
+ #force,
+ #deletedContainer,
+ #mainContainer.deleted {
+ display: none;
+ }
+ #undoBtn.modified,
+ #force.force {
+ display: block;
+ }
+ .groupPath {
+ color: var(--deemphasized-text-color);
+ }
+ </style>
+ <style include="gr-form-styles">
+ iron-autogrow-textarea {
+ width: 14em;
+ }
+ </style>
+ <div id="mainContainer" class\$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
+ <div id="options">
+ <gr-select id="action" bind-value="{{rule.value.action}}" on-change="_handleValueChange">
+ <select disabled\$="[[!editing]]">
+ <template is="dom-repeat" items="[[_computeOptions(permission)]]">
+ <option value="[[item]]">[[item]]</option>
+ </template>
+ </select>
+ </gr-select>
+ <template is="dom-if" if="[[label]]">
+ <gr-select id="labelMin" bind-value="{{rule.value.min}}" on-change="_handleValueChange">
+ <select disabled\$="[[!editing]]">
+ <template is="dom-repeat" items="[[label.values]]">
+ <option value="[[item.value]]">[[item.value]]</option>
+ </template>
+ </select>
+ </gr-select>
+ <gr-select id="labelMax" bind-value="{{rule.value.max}}" on-change="_handleValueChange">
+ <select disabled\$="[[!editing]]">
+ <template is="dom-repeat" items="[[label.values]]">
+ <option value="[[item.value]]">[[item.value]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </template>
+ <template is="dom-if" if="[[hasRange]]">
+ <iron-autogrow-textarea id="minInput" class="min" autocomplete="on" placeholder="Min value" bind-value="{{rule.value.min}}" disabled\$="[[!editing]]"></iron-autogrow-textarea>
+ <iron-autogrow-textarea id="maxInput" class="max" autocomplete="on" placeholder="Max value" bind-value="{{rule.value.max}}" disabled\$="[[!editing]]"></iron-autogrow-textarea>
+ </template>
+ <a class="groupPath" href\$="[[_computeGroupPath(groupId)]]">
+ [[groupName]]
+ </a>
+ <gr-select id="force" class\$="[[_computeForceClass(permission, rule.value.action)]]" bind-value="{{rule.value.force}}" on-change="_handleValueChange">
+ <select disabled\$="[[!editing]]">
+ <template is="dom-repeat" items="[[_computeForceOptions(permission, rule.value.action)]]">
+ <option value="[[item.value]]">[[item.name]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </div>
+ <gr-button link="" id="removeBtn" on-click="_handleRemoveRule">Remove</gr-button>
+ </div>
+ <div id="deletedContainer" class\$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
+ [[groupName]] was deleted
+ <gr-button link="" id="undoRemoveBtn" on-click="_handleUndoRemove">Undo</gr-button>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
index 9f02ddc..052eb96 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
@@ -19,16 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-rule-editor</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-rule-editor.html">
-
-<script>void(0);</script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,593 +31,595 @@
</template>
</test-fixture>
-<script>
- suite('gr-rule-editor tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-rule-editor.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-rule-editor tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- suite('unit tests', () => {
- test('_computeForce, _computeForceClass, and _computeForceOptions',
- () => {
- const ForcePushOptions = {
- ALLOW: [
- {name: 'Allow pushing (but not force pushing)', value: false},
- {name: 'Allow pushing with or without force', value: true},
- ],
- BLOCK: [
- {name: 'Block pushing with or without force', value: false},
- {name: 'Block force pushing', value: true},
- ],
- };
+ suite('unit tests', () => {
+ test('_computeForce, _computeForceClass, and _computeForceOptions',
+ () => {
+ const ForcePushOptions = {
+ ALLOW: [
+ {name: 'Allow pushing (but not force pushing)', value: false},
+ {name: 'Allow pushing with or without force', value: true},
+ ],
+ BLOCK: [
+ {name: 'Block pushing with or without force', value: false},
+ {name: 'Block force pushing', value: true},
+ ],
+ };
- const FORCE_EDIT_OPTIONS = [
- {
- name: 'No Force Edit',
- value: false,
- },
- {
- name: 'Force Edit',
- value: true,
- },
- ];
- let permission = 'push';
- let action = 'ALLOW';
- assert.isTrue(element._computeForce(permission, action));
- assert.equal(element._computeForceClass(permission, action),
- 'force');
- assert.deepEqual(element._computeForceOptions(permission, action),
- ForcePushOptions.ALLOW);
+ const FORCE_EDIT_OPTIONS = [
+ {
+ name: 'No Force Edit',
+ value: false,
+ },
+ {
+ name: 'Force Edit',
+ value: true,
+ },
+ ];
+ let permission = 'push';
+ let action = 'ALLOW';
+ assert.isTrue(element._computeForce(permission, action));
+ assert.equal(element._computeForceClass(permission, action),
+ 'force');
+ assert.deepEqual(element._computeForceOptions(permission, action),
+ ForcePushOptions.ALLOW);
- action = 'BLOCK';
- assert.isTrue(element._computeForce(permission, action));
- assert.equal(element._computeForceClass(permission, action),
- 'force');
- assert.deepEqual(element._computeForceOptions(permission, action),
- ForcePushOptions.BLOCK);
+ action = 'BLOCK';
+ assert.isTrue(element._computeForce(permission, action));
+ assert.equal(element._computeForceClass(permission, action),
+ 'force');
+ assert.deepEqual(element._computeForceOptions(permission, action),
+ ForcePushOptions.BLOCK);
- action = 'DENY';
- assert.isFalse(element._computeForce(permission, action));
- assert.equal(element._computeForceClass(permission, action), '');
- assert.equal(
- element._computeForceOptions(permission, action).length, 0);
-
- permission = 'editTopicName';
- assert.isTrue(element._computeForce(permission));
- assert.equal(element._computeForceClass(permission), 'force');
- assert.deepEqual(element._computeForceOptions(permission),
- FORCE_EDIT_OPTIONS);
- permission = 'submit';
- assert.isFalse(element._computeForce(permission));
- assert.equal(element._computeForceClass(permission), '');
- assert.deepEqual(element._computeForceOptions(permission), []);
- });
-
- test('_computeSectionClass', () => {
- let deleted = true;
- let editing = false;
- assert.equal(element._computeSectionClass(editing, deleted), 'deleted');
-
- deleted = false;
- assert.equal(element._computeSectionClass(editing, deleted), '');
-
- editing = true;
- assert.equal(element._computeSectionClass(editing, deleted), 'editing');
-
- deleted = true;
- assert.equal(element._computeSectionClass(editing, deleted),
- 'editing deleted');
- });
-
- test('_getDefaultRuleValues', () => {
- let permission = 'priority';
- let label;
- assert.deepEqual(element._getDefaultRuleValues(permission, label),
- {action: 'BATCH'});
- permission = 'label-Code-Review';
- label = {values: [
- {value: -2, text: 'This shall not be merged'},
- {value: -1, text: 'I would prefer this is not merged as is'},
- {value: -0, text: 'No score'},
- {value: 1, text: 'Looks good to me, but someone else must approve'},
- {value: 2, text: 'Looks good to me, approved'},
- ]};
- assert.deepEqual(element._getDefaultRuleValues(permission, label),
- {action: 'ALLOW', max: 2, min: -2});
- permission = 'push';
- label = undefined;
- assert.deepEqual(element._getDefaultRuleValues(permission, label),
- {action: 'ALLOW', force: false});
- permission = 'submit';
- assert.deepEqual(element._getDefaultRuleValues(permission, label),
- {action: 'ALLOW'});
- });
-
- test('_setDefaultRuleValues', () => {
- element.rule = {id: 123};
- const defaultValue = {action: 'ALLOW'};
- sandbox.stub(element, '_getDefaultRuleValues').returns(defaultValue);
- element._setDefaultRuleValues();
- assert.isTrue(element._getDefaultRuleValues.called);
- assert.equal(element.rule.value, defaultValue);
- });
-
- test('_computeOptions', () => {
- const PRIORITY_OPTIONS = [
- 'BATCH',
- 'INTERACTIVE',
- ];
- const DROPDOWN_OPTIONS = [
- 'ALLOW',
- 'DENY',
- 'BLOCK',
- ];
- let permission = 'priority';
- assert.deepEqual(element._computeOptions(permission), PRIORITY_OPTIONS);
- permission = 'submit';
- assert.deepEqual(element._computeOptions(permission), DROPDOWN_OPTIONS);
- });
-
- test('_handleValueChange', () => {
- const modifiedHandler = sandbox.stub();
- element.rule = {value: {}};
- element.addEventListener('access-modified', modifiedHandler);
- element._handleValueChange();
- assert.isNotOk(element.rule.value.modified);
- element._originalRuleValues = {};
- element._handleValueChange();
- assert.isTrue(element.rule.value.modified);
- assert.isTrue(modifiedHandler.called);
- });
-
- test('_handleAccessSaved', () => {
- const originalValue = {action: 'DENY'};
- const newValue = {action: 'ALLOW'};
- element._originalRuleValues = originalValue;
- element.rule = {value: newValue};
- element._handleAccessSaved();
- assert.deepEqual(element._originalRuleValues, newValue);
- });
-
- test('_setOriginalRuleValues', () => {
- const value = {
- action: 'ALLOW',
- force: false,
- };
- element._setOriginalRuleValues(value);
- assert.deepEqual(element._originalRuleValues, value);
- });
- });
-
- suite('already existing generic rule', () => {
- setup(done => {
- element.group = 'Group Name';
- element.permission = 'submit';
- element.rule = {
- id: '123',
- value: {
- action: 'ALLOW',
- force: false,
- },
- };
- element.section = 'refs/*';
-
- // Typically called on ready since elements will have properies defined
- // by the parent element.
- element._setupValues(element.rule);
- flushAsynchronousOperations();
- flush(() => {
- element.attached();
- done();
- });
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- });
-
- test('values are set correctly', () => {
- assert.equal(element.$.action.bindValue, element.rule.value.action);
- assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMin'));
- assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMax'));
- assert.isFalse(element.$.force.classList.contains('force'));
- });
-
- test('modify and cancel restores original values', () => {
- element.editing = true;
- assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none');
- assert.isNotOk(element.rule.value.modified);
- element.$.action.bindValue = 'DENY';
- assert.isTrue(element.rule.value.modified);
- element.editing = false;
- assert.equal(getComputedStyle(element.$.removeBtn).display, 'none');
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- assert.equal(element.$.action.bindValue, 'ALLOW');
- assert.isNotOk(element.rule.value.modified);
- });
-
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- element.$.action.bindValue = 'DENY';
- flushAsynchronousOperations();
- assert.isTrue(element.rule.value.modified);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
-
- test('all selects are disabled when not in edit mode', () => {
- const selects = Polymer.dom(element.root).querySelectorAll('select');
- for (const select of selects) {
- assert.isTrue(select.disabled);
- }
- element.editing = true;
- for (const select of selects) {
- assert.isFalse(select.disabled);
- }
- });
-
- test('remove rule and undo remove', () => {
- element.editing = true;
- element.rule = {id: 123, value: {action: 'ALLOW'}};
- assert.isFalse(
- element.$.deletedContainer.classList.contains('deleted'));
- MockInteractions.tap(element.$.removeBtn);
- assert.isTrue(element.$.deletedContainer.classList.contains('deleted'));
- assert.isTrue(element._deleted);
- assert.isTrue(element.rule.value.deleted);
-
- MockInteractions.tap(element.$.undoRemoveBtn);
- assert.isFalse(element._deleted);
- assert.isNotOk(element.rule.value.deleted);
- });
-
- test('remove rule and cancel', () => {
- element.editing = true;
- assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none');
- assert.equal(getComputedStyle(element.$.deletedContainer).display,
- 'none');
-
- element.rule = {id: 123, value: {action: 'ALLOW'}};
- MockInteractions.tap(element.$.removeBtn);
- assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none');
- assert.notEqual(getComputedStyle(element.$.deletedContainer).display,
- 'none');
- assert.isTrue(element._deleted);
- assert.isTrue(element.rule.value.deleted);
-
- element.editing = false;
- assert.isFalse(element._deleted);
- assert.isNotOk(element.rule.value.deleted);
- assert.isNotOk(element.rule.value.modified);
-
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- assert.equal(getComputedStyle(element.$.removeBtn).display, 'none');
- assert.equal(getComputedStyle(element.$.deletedContainer).display,
- 'none');
- });
-
- test('_computeGroupPath', () => {
- const group = '123';
- assert.equal(element._computeGroupPath(group),
- `/admin/groups/123`);
- });
- });
-
- suite('new edit rule', () => {
- setup(done => {
- element.group = 'Group Name';
- element.permission = 'editTopicName';
- element.rule = {
- id: '123',
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- flushAsynchronousOperations();
- element.rule.value.added = true;
- flush(() => {
- element.attached();
- done();
- });
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- // Since the element does not already have default values, they should
- // be set. The original values should be set to those too.
- assert.isNotOk(element.rule.value.modified);
- const expectedRuleValue = {
- action: 'ALLOW',
- force: false,
- added: true,
- };
- assert.deepEqual(element.rule.value, expectedRuleValue);
- test('values are set correctly', () => {
- assert.equal(element.$.action.bindValue, expectedRuleValue.action);
- assert.equal(element.$.force.bindValue, expectedRuleValue.action);
- });
- });
-
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- element.$.force.bindValue = true;
- flushAsynchronousOperations();
- assert.isTrue(element.rule.value.modified);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
-
- test('remove value', () => {
- element.editing = true;
- const removeStub = sandbox.stub();
- element.addEventListener('added-rule-removed', removeStub);
- MockInteractions.tap(element.$.removeBtn);
- flushAsynchronousOperations();
- assert.isTrue(removeStub.called);
- });
- });
-
- suite('already existing rule with labels', () => {
- setup(done => {
- element.label = {values: [
- {value: -2, text: 'This shall not be merged'},
- {value: -1, text: 'I would prefer this is not merged as is'},
- {value: -0, text: 'No score'},
- {value: 1, text: 'Looks good to me, but someone else must approve'},
- {value: 2, text: 'Looks good to me, approved'},
- ]};
- element.group = 'Group Name';
- element.permission = 'label-Code-Review';
- element.rule = {
- id: '123',
- value: {
- action: 'ALLOW',
- force: false,
- max: 2,
- min: -2,
- },
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- flushAsynchronousOperations();
- flush(() => {
- element.attached();
- done();
- });
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- });
-
- test('values are set correctly', () => {
- assert.equal(element.$.action.bindValue, element.rule.value.action);
- assert.equal(
- Polymer.dom(element.root).querySelector('#labelMin').bindValue,
- element.rule.value.min);
- assert.equal(
- Polymer.dom(element.root).querySelector('#labelMax').bindValue,
- element.rule.value.max);
- assert.isFalse(element.$.force.classList.contains('force'));
- });
-
- test('modify value', () => {
- const removeStub = sandbox.stub();
- element.addEventListener('added-rule-removed', removeStub);
- assert.isNotOk(element.rule.value.modified);
- Polymer.dom(element.root).querySelector('#labelMin').bindValue = 1;
- flushAsynchronousOperations();
- assert.isTrue(element.rule.value.modified);
- assert.isFalse(removeStub.called);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
- });
-
- suite('new rule with labels', () => {
- setup(done => {
- sandbox.spy(element, '_setDefaultRuleValues');
- element.label = {values: [
- {value: -2, text: 'This shall not be merged'},
- {value: -1, text: 'I would prefer this is not merged as is'},
- {value: -0, text: 'No score'},
- {value: 1, text: 'Looks good to me, but someone else must approve'},
- {value: 2, text: 'Looks good to me, approved'},
- ]};
- element.group = 'Group Name';
- element.permission = 'label-Code-Review';
- element.rule = {
- id: '123',
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- flushAsynchronousOperations();
- element.rule.value.added = true;
- flush(() => {
- element.attached();
- done();
- });
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- // Since the element does not already have default values, they should
- // be set. The original values should be set to those too.
- assert.isNotOk(element.rule.value.modified);
- assert.isTrue(element._setDefaultRuleValues.called);
-
- const expectedRuleValue = {
- max: element.label.values[element.label.values.length - 1].value,
- min: element.label.values[0].value,
- action: 'ALLOW',
- added: true,
- };
- assert.deepEqual(element.rule.value, expectedRuleValue);
- test('values are set correctly', () => {
+ action = 'DENY';
+ assert.isFalse(element._computeForce(permission, action));
+ assert.equal(element._computeForceClass(permission, action), '');
assert.equal(
- element.$.action.bindValue,
- expectedRuleValue.action);
- assert.equal(
- Polymer.dom(element.root).querySelector('#labelMin').bindValue,
- expectedRuleValue.min);
- assert.equal(
- Polymer.dom(element.root).querySelector('#labelMax').bindValue,
- expectedRuleValue.max);
+ element._computeForceOptions(permission, action).length, 0);
+
+ permission = 'editTopicName';
+ assert.isTrue(element._computeForce(permission));
+ assert.equal(element._computeForceClass(permission), 'force');
+ assert.deepEqual(element._computeForceOptions(permission),
+ FORCE_EDIT_OPTIONS);
+ permission = 'submit';
+ assert.isFalse(element._computeForce(permission));
+ assert.equal(element._computeForceClass(permission), '');
+ assert.deepEqual(element._computeForceOptions(permission), []);
});
- });
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- Polymer.dom(element.root).querySelector('#labelMin').bindValue = 1;
- flushAsynchronousOperations();
- assert.isTrue(element.rule.value.modified);
+ test('_computeSectionClass', () => {
+ let deleted = true;
+ let editing = false;
+ assert.equal(element._computeSectionClass(editing, deleted), 'deleted');
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
+ deleted = false;
+ assert.equal(element._computeSectionClass(editing, deleted), '');
+
+ editing = true;
+ assert.equal(element._computeSectionClass(editing, deleted), 'editing');
+
+ deleted = true;
+ assert.equal(element._computeSectionClass(editing, deleted),
+ 'editing deleted');
});
- suite('already existing push rule', () => {
- setup(done => {
- element.group = 'Group Name';
- element.permission = 'push';
- element.rule = {
- id: '123',
- value: {
- action: 'ALLOW',
- force: true,
- },
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- flushAsynchronousOperations();
- flush(() => {
- element.attached();
- done();
- });
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- });
-
- test('values are set correctly', () => {
- assert.isTrue(element.$.force.classList.contains('force'));
- assert.equal(element.$.action.bindValue, element.rule.value.action);
- assert.equal(
- Polymer.dom(element.root).querySelector('#force').bindValue,
- element.rule.value.force);
- assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMin'));
- assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMax'));
- });
-
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- element.$.action.bindValue = false;
- flushAsynchronousOperations();
- assert.isTrue(element.rule.value.modified);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
+ test('_getDefaultRuleValues', () => {
+ let permission = 'priority';
+ let label;
+ assert.deepEqual(element._getDefaultRuleValues(permission, label),
+ {action: 'BATCH'});
+ permission = 'label-Code-Review';
+ label = {values: [
+ {value: -2, text: 'This shall not be merged'},
+ {value: -1, text: 'I would prefer this is not merged as is'},
+ {value: -0, text: 'No score'},
+ {value: 1, text: 'Looks good to me, but someone else must approve'},
+ {value: 2, text: 'Looks good to me, approved'},
+ ]};
+ assert.deepEqual(element._getDefaultRuleValues(permission, label),
+ {action: 'ALLOW', max: 2, min: -2});
+ permission = 'push';
+ label = undefined;
+ assert.deepEqual(element._getDefaultRuleValues(permission, label),
+ {action: 'ALLOW', force: false});
+ permission = 'submit';
+ assert.deepEqual(element._getDefaultRuleValues(permission, label),
+ {action: 'ALLOW'});
});
- suite('new push rule', () => {
- setup(done => {
- element.group = 'Group Name';
- element.permission = 'push';
- element.rule = {
- id: '123',
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- flushAsynchronousOperations();
- element.rule.value.added = true;
- flush(() => {
- element.attached();
- done();
- });
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- // Since the element does not already have default values, they should
- // be set. The original values should be set to those too.
- assert.isNotOk(element.rule.value.modified);
- const expectedRuleValue = {
- action: 'ALLOW',
- force: false,
- added: true,
- };
- assert.deepEqual(element.rule.value, expectedRuleValue);
- test('values are set correctly', () => {
- assert.equal(element.$.action.bindValue, expectedRuleValue.action);
- assert.equal(element.$.force.bindValue, expectedRuleValue.action);
- });
- });
-
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- element.$.force.bindValue = true;
- flushAsynchronousOperations();
- assert.isTrue(element.rule.value.modified);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
+ test('_setDefaultRuleValues', () => {
+ element.rule = {id: 123};
+ const defaultValue = {action: 'ALLOW'};
+ sandbox.stub(element, '_getDefaultRuleValues').returns(defaultValue);
+ element._setDefaultRuleValues();
+ assert.isTrue(element._getDefaultRuleValues.called);
+ assert.equal(element.rule.value, defaultValue);
});
- suite('already existing edit rule', () => {
- setup(done => {
- element.group = 'Group Name';
- element.permission = 'editTopicName';
- element.rule = {
- id: '123',
- value: {
- action: 'ALLOW',
- force: true,
- },
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- flushAsynchronousOperations();
- flush(() => {
- element.attached();
- done();
- });
- });
+ test('_computeOptions', () => {
+ const PRIORITY_OPTIONS = [
+ 'BATCH',
+ 'INTERACTIVE',
+ ];
+ const DROPDOWN_OPTIONS = [
+ 'ALLOW',
+ 'DENY',
+ 'BLOCK',
+ ];
+ let permission = 'priority';
+ assert.deepEqual(element._computeOptions(permission), PRIORITY_OPTIONS);
+ permission = 'submit';
+ assert.deepEqual(element._computeOptions(permission), DROPDOWN_OPTIONS);
+ });
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- });
+ test('_handleValueChange', () => {
+ const modifiedHandler = sandbox.stub();
+ element.rule = {value: {}};
+ element.addEventListener('access-modified', modifiedHandler);
+ element._handleValueChange();
+ assert.isNotOk(element.rule.value.modified);
+ element._originalRuleValues = {};
+ element._handleValueChange();
+ assert.isTrue(element.rule.value.modified);
+ assert.isTrue(modifiedHandler.called);
+ });
- test('values are set correctly', () => {
- assert.isTrue(element.$.force.classList.contains('force'));
- assert.equal(element.$.action.bindValue, element.rule.value.action);
- assert.equal(
- Polymer.dom(element.root).querySelector('#force').bindValue,
- element.rule.value.force);
- assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMin'));
- assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMax'));
- });
+ test('_handleAccessSaved', () => {
+ const originalValue = {action: 'DENY'};
+ const newValue = {action: 'ALLOW'};
+ element._originalRuleValues = originalValue;
+ element.rule = {value: newValue};
+ element._handleAccessSaved();
+ assert.deepEqual(element._originalRuleValues, newValue);
+ });
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- element.$.action.bindValue = false;
- flushAsynchronousOperations();
- assert.isTrue(element.rule.value.modified);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
+ test('_setOriginalRuleValues', () => {
+ const value = {
+ action: 'ALLOW',
+ force: false,
+ };
+ element._setOriginalRuleValues(value);
+ assert.deepEqual(element._originalRuleValues, value);
});
});
+
+ suite('already existing generic rule', () => {
+ setup(done => {
+ element.group = 'Group Name';
+ element.permission = 'submit';
+ element.rule = {
+ id: '123',
+ value: {
+ action: 'ALLOW',
+ force: false,
+ },
+ };
+ element.section = 'refs/*';
+
+ // Typically called on ready since elements will have properies defined
+ // by the parent element.
+ element._setupValues(element.rule);
+ flushAsynchronousOperations();
+ flush(() => {
+ element.attached();
+ done();
+ });
+ });
+
+ test('_ruleValues and _originalRuleValues are set correctly', () => {
+ assert.deepEqual(element._originalRuleValues, element.rule.value);
+ });
+
+ test('values are set correctly', () => {
+ assert.equal(element.$.action.bindValue, element.rule.value.action);
+ assert.isNotOk(dom(element.root).querySelector('#labelMin'));
+ assert.isNotOk(dom(element.root).querySelector('#labelMax'));
+ assert.isFalse(element.$.force.classList.contains('force'));
+ });
+
+ test('modify and cancel restores original values', () => {
+ element.editing = true;
+ assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none');
+ assert.isNotOk(element.rule.value.modified);
+ element.$.action.bindValue = 'DENY';
+ assert.isTrue(element.rule.value.modified);
+ element.editing = false;
+ assert.equal(getComputedStyle(element.$.removeBtn).display, 'none');
+ assert.deepEqual(element._originalRuleValues, element.rule.value);
+ assert.equal(element.$.action.bindValue, 'ALLOW');
+ assert.isNotOk(element.rule.value.modified);
+ });
+
+ test('modify value', () => {
+ assert.isNotOk(element.rule.value.modified);
+ element.$.action.bindValue = 'DENY';
+ flushAsynchronousOperations();
+ assert.isTrue(element.rule.value.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element._originalRuleValues, element.rule.value);
+ });
+
+ test('all selects are disabled when not in edit mode', () => {
+ const selects = dom(element.root).querySelectorAll('select');
+ for (const select of selects) {
+ assert.isTrue(select.disabled);
+ }
+ element.editing = true;
+ for (const select of selects) {
+ assert.isFalse(select.disabled);
+ }
+ });
+
+ test('remove rule and undo remove', () => {
+ element.editing = true;
+ element.rule = {id: 123, value: {action: 'ALLOW'}};
+ assert.isFalse(
+ element.$.deletedContainer.classList.contains('deleted'));
+ MockInteractions.tap(element.$.removeBtn);
+ assert.isTrue(element.$.deletedContainer.classList.contains('deleted'));
+ assert.isTrue(element._deleted);
+ assert.isTrue(element.rule.value.deleted);
+
+ MockInteractions.tap(element.$.undoRemoveBtn);
+ assert.isFalse(element._deleted);
+ assert.isNotOk(element.rule.value.deleted);
+ });
+
+ test('remove rule and cancel', () => {
+ element.editing = true;
+ assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none');
+ assert.equal(getComputedStyle(element.$.deletedContainer).display,
+ 'none');
+
+ element.rule = {id: 123, value: {action: 'ALLOW'}};
+ MockInteractions.tap(element.$.removeBtn);
+ assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none');
+ assert.notEqual(getComputedStyle(element.$.deletedContainer).display,
+ 'none');
+ assert.isTrue(element._deleted);
+ assert.isTrue(element.rule.value.deleted);
+
+ element.editing = false;
+ assert.isFalse(element._deleted);
+ assert.isNotOk(element.rule.value.deleted);
+ assert.isNotOk(element.rule.value.modified);
+
+ assert.deepEqual(element._originalRuleValues, element.rule.value);
+ assert.equal(getComputedStyle(element.$.removeBtn).display, 'none');
+ assert.equal(getComputedStyle(element.$.deletedContainer).display,
+ 'none');
+ });
+
+ test('_computeGroupPath', () => {
+ const group = '123';
+ assert.equal(element._computeGroupPath(group),
+ `/admin/groups/123`);
+ });
+ });
+
+ suite('new edit rule', () => {
+ setup(done => {
+ element.group = 'Group Name';
+ element.permission = 'editTopicName';
+ element.rule = {
+ id: '123',
+ };
+ element.section = 'refs/*';
+ element._setupValues(element.rule);
+ flushAsynchronousOperations();
+ element.rule.value.added = true;
+ flush(() => {
+ element.attached();
+ done();
+ });
+ });
+
+ test('_ruleValues and _originalRuleValues are set correctly', () => {
+ // Since the element does not already have default values, they should
+ // be set. The original values should be set to those too.
+ assert.isNotOk(element.rule.value.modified);
+ const expectedRuleValue = {
+ action: 'ALLOW',
+ force: false,
+ added: true,
+ };
+ assert.deepEqual(element.rule.value, expectedRuleValue);
+ test('values are set correctly', () => {
+ assert.equal(element.$.action.bindValue, expectedRuleValue.action);
+ assert.equal(element.$.force.bindValue, expectedRuleValue.action);
+ });
+ });
+
+ test('modify value', () => {
+ assert.isNotOk(element.rule.value.modified);
+ element.$.force.bindValue = true;
+ flushAsynchronousOperations();
+ assert.isTrue(element.rule.value.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element._originalRuleValues, element.rule.value);
+ });
+
+ test('remove value', () => {
+ element.editing = true;
+ const removeStub = sandbox.stub();
+ element.addEventListener('added-rule-removed', removeStub);
+ MockInteractions.tap(element.$.removeBtn);
+ flushAsynchronousOperations();
+ assert.isTrue(removeStub.called);
+ });
+ });
+
+ suite('already existing rule with labels', () => {
+ setup(done => {
+ element.label = {values: [
+ {value: -2, text: 'This shall not be merged'},
+ {value: -1, text: 'I would prefer this is not merged as is'},
+ {value: -0, text: 'No score'},
+ {value: 1, text: 'Looks good to me, but someone else must approve'},
+ {value: 2, text: 'Looks good to me, approved'},
+ ]};
+ element.group = 'Group Name';
+ element.permission = 'label-Code-Review';
+ element.rule = {
+ id: '123',
+ value: {
+ action: 'ALLOW',
+ force: false,
+ max: 2,
+ min: -2,
+ },
+ };
+ element.section = 'refs/*';
+ element._setupValues(element.rule);
+ flushAsynchronousOperations();
+ flush(() => {
+ element.attached();
+ done();
+ });
+ });
+
+ test('_ruleValues and _originalRuleValues are set correctly', () => {
+ assert.deepEqual(element._originalRuleValues, element.rule.value);
+ });
+
+ test('values are set correctly', () => {
+ assert.equal(element.$.action.bindValue, element.rule.value.action);
+ assert.equal(
+ dom(element.root).querySelector('#labelMin').bindValue,
+ element.rule.value.min);
+ assert.equal(
+ dom(element.root).querySelector('#labelMax').bindValue,
+ element.rule.value.max);
+ assert.isFalse(element.$.force.classList.contains('force'));
+ });
+
+ test('modify value', () => {
+ const removeStub = sandbox.stub();
+ element.addEventListener('added-rule-removed', removeStub);
+ assert.isNotOk(element.rule.value.modified);
+ dom(element.root).querySelector('#labelMin').bindValue = 1;
+ flushAsynchronousOperations();
+ assert.isTrue(element.rule.value.modified);
+ assert.isFalse(removeStub.called);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element._originalRuleValues, element.rule.value);
+ });
+ });
+
+ suite('new rule with labels', () => {
+ setup(done => {
+ sandbox.spy(element, '_setDefaultRuleValues');
+ element.label = {values: [
+ {value: -2, text: 'This shall not be merged'},
+ {value: -1, text: 'I would prefer this is not merged as is'},
+ {value: -0, text: 'No score'},
+ {value: 1, text: 'Looks good to me, but someone else must approve'},
+ {value: 2, text: 'Looks good to me, approved'},
+ ]};
+ element.group = 'Group Name';
+ element.permission = 'label-Code-Review';
+ element.rule = {
+ id: '123',
+ };
+ element.section = 'refs/*';
+ element._setupValues(element.rule);
+ flushAsynchronousOperations();
+ element.rule.value.added = true;
+ flush(() => {
+ element.attached();
+ done();
+ });
+ });
+
+ test('_ruleValues and _originalRuleValues are set correctly', () => {
+ // Since the element does not already have default values, they should
+ // be set. The original values should be set to those too.
+ assert.isNotOk(element.rule.value.modified);
+ assert.isTrue(element._setDefaultRuleValues.called);
+
+ const expectedRuleValue = {
+ max: element.label.values[element.label.values.length - 1].value,
+ min: element.label.values[0].value,
+ action: 'ALLOW',
+ added: true,
+ };
+ assert.deepEqual(element.rule.value, expectedRuleValue);
+ test('values are set correctly', () => {
+ assert.equal(
+ element.$.action.bindValue,
+ expectedRuleValue.action);
+ assert.equal(
+ dom(element.root).querySelector('#labelMin').bindValue,
+ expectedRuleValue.min);
+ assert.equal(
+ dom(element.root).querySelector('#labelMax').bindValue,
+ expectedRuleValue.max);
+ });
+ });
+
+ test('modify value', () => {
+ assert.isNotOk(element.rule.value.modified);
+ dom(element.root).querySelector('#labelMin').bindValue = 1;
+ flushAsynchronousOperations();
+ assert.isTrue(element.rule.value.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element._originalRuleValues, element.rule.value);
+ });
+ });
+
+ suite('already existing push rule', () => {
+ setup(done => {
+ element.group = 'Group Name';
+ element.permission = 'push';
+ element.rule = {
+ id: '123',
+ value: {
+ action: 'ALLOW',
+ force: true,
+ },
+ };
+ element.section = 'refs/*';
+ element._setupValues(element.rule);
+ flushAsynchronousOperations();
+ flush(() => {
+ element.attached();
+ done();
+ });
+ });
+
+ test('_ruleValues and _originalRuleValues are set correctly', () => {
+ assert.deepEqual(element._originalRuleValues, element.rule.value);
+ });
+
+ test('values are set correctly', () => {
+ assert.isTrue(element.$.force.classList.contains('force'));
+ assert.equal(element.$.action.bindValue, element.rule.value.action);
+ assert.equal(
+ dom(element.root).querySelector('#force').bindValue,
+ element.rule.value.force);
+ assert.isNotOk(dom(element.root).querySelector('#labelMin'));
+ assert.isNotOk(dom(element.root).querySelector('#labelMax'));
+ });
+
+ test('modify value', () => {
+ assert.isNotOk(element.rule.value.modified);
+ element.$.action.bindValue = false;
+ flushAsynchronousOperations();
+ assert.isTrue(element.rule.value.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element._originalRuleValues, element.rule.value);
+ });
+ });
+
+ suite('new push rule', () => {
+ setup(done => {
+ element.group = 'Group Name';
+ element.permission = 'push';
+ element.rule = {
+ id: '123',
+ };
+ element.section = 'refs/*';
+ element._setupValues(element.rule);
+ flushAsynchronousOperations();
+ element.rule.value.added = true;
+ flush(() => {
+ element.attached();
+ done();
+ });
+ });
+
+ test('_ruleValues and _originalRuleValues are set correctly', () => {
+ // Since the element does not already have default values, they should
+ // be set. The original values should be set to those too.
+ assert.isNotOk(element.rule.value.modified);
+ const expectedRuleValue = {
+ action: 'ALLOW',
+ force: false,
+ added: true,
+ };
+ assert.deepEqual(element.rule.value, expectedRuleValue);
+ test('values are set correctly', () => {
+ assert.equal(element.$.action.bindValue, expectedRuleValue.action);
+ assert.equal(element.$.force.bindValue, expectedRuleValue.action);
+ });
+ });
+
+ test('modify value', () => {
+ assert.isNotOk(element.rule.value.modified);
+ element.$.force.bindValue = true;
+ flushAsynchronousOperations();
+ assert.isTrue(element.rule.value.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element._originalRuleValues, element.rule.value);
+ });
+ });
+
+ suite('already existing edit rule', () => {
+ setup(done => {
+ element.group = 'Group Name';
+ element.permission = 'editTopicName';
+ element.rule = {
+ id: '123',
+ value: {
+ action: 'ALLOW',
+ force: true,
+ },
+ };
+ element.section = 'refs/*';
+ element._setupValues(element.rule);
+ flushAsynchronousOperations();
+ flush(() => {
+ element.attached();
+ done();
+ });
+ });
+
+ test('_ruleValues and _originalRuleValues are set correctly', () => {
+ assert.deepEqual(element._originalRuleValues, element.rule.value);
+ });
+
+ test('values are set correctly', () => {
+ assert.isTrue(element.$.force.classList.contains('force'));
+ assert.equal(element.$.action.bindValue, element.rule.value.action);
+ assert.equal(
+ dom(element.root).querySelector('#force').bindValue,
+ element.rule.value.force);
+ assert.isNotOk(dom(element.root).querySelector('#labelMin'));
+ assert.isNotOk(dom(element.root).querySelector('#labelMax'));
+ });
+
+ test('modify value', () => {
+ assert.isNotOk(element.rule.value.modified);
+ element.$.action.bindValue = false;
+ flushAsynchronousOperations();
+ assert.isTrue(element.rule.value.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element._originalRuleValues, element.rule.value);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
deleted file mode 100644
index 9022cf2..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
+++ /dev/null
@@ -1,237 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.html">
-<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/gr-change-list-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
-<link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
-<link rel="import" href="../../shared/gr-change-status/gr-change-status.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-limited-text/gr-limited-text.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-
-<dom-module id="gr-change-list-item">
- <template>
- <style include="shared-styles">
- :host {
- display: table-row;
- color: var(--primary-text-color);
- }
- :host(:focus) {
- outline: none;
- }
- :host(:hover) {
- background-color: var(--hover-background-color);
- }
- :host([needs-review]) {
- font-weight: var(--font-weight-bold);
- color: var(--primary-text-color);
- }
- :host([highlight]) {
- background-color: var(--assignee-highlight-color);
- }
- .container {
- position: relative;
- }
- .content {
- overflow: hidden;
- position: absolute;
- text-overflow: ellipsis;
- white-space: nowrap;
- width: 100%;
- }
- .content a {
- display: block;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- width: 100%;
- }
- .spacer {
- height: 0;
- overflow: hidden;
- }
- .status {
- align-items: center;
- display: inline-flex;
- }
- .status .comma {
- padding-right: var(--spacing-xs);
- }
- /* Used to hide the leading separator comma for statuses. */
- .status .comma:first-of-type {
- display: none;
- }
- .size gr-tooltip-content {
- margin: -.4rem -.6rem;
- max-width: 2.5rem;
- padding: var(--spacing-m) var(--spacing-l);
- }
- a {
- color: inherit;
- cursor: pointer;
- text-decoration: none;
- }
- a:hover {
- text-decoration: underline;
- }
- .u-monospace {
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-mono);
- line-height: var(--line-height-mono);
- }
- .u-green {
- color: var(--vote-text-color-recommended);
- }
- .u-red {
- color: var(--vote-text-color-disliked);
- }
- .u-gray-background {
- background-color: var(--table-header-background-color);
- }
- .comma,
- .placeholder {
- color: var(--deemphasized-text-color);
- }
- .cell.label {
- font-weight: var(--font-weight-normal);
- }
- @media only screen and (max-width: 50em) {
- :host {
- display: flex;
- }
- }
- </style>
- <style include="gr-change-list-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <td class="cell leftPadding"></td>
- <td class="cell star" hidden$="[[!showStar]]" hidden>
- <gr-change-star change="{{change}}"></gr-change-star>
- </td>
- <td class="cell number" hidden$="[[!showNumber]]" hidden>
- <a href$="[[changeURL]]">[[change._number]]</a>
- </td>
- <td class="cell subject"
- hidden$="[[isColumnHidden('Subject', visibleChangeTableColumns)]]">
- <div class="container">
- <div class="content">
- <a title$="[[change.subject]]" href$="[[changeURL]]">
- [[change.subject]]
- </a>
- </div>
- <div class="spacer">
- [[change.subject]]
- </div>
- <span> </span>
- </div>
- </td>
- <td class="cell status"
- hidden$="[[isColumnHidden('Status', visibleChangeTableColumns)]]">
- <template is="dom-repeat" items="[[statuses]]" as="status">
- <div class="comma">,</div>
- <gr-change-status flat status="[[status]]"></gr-change-status>
- </template>
- <template is="dom-if" if="[[!statuses.length]]">
- <span class="placeholder">--</span>
- </template>
- </td>
- <td class="cell owner"
- hidden$="[[isColumnHidden('Owner', visibleChangeTableColumns)]]">
- <gr-account-link
- account="[[change.owner]]"></gr-account-link>
- </td>
- <td class="cell assignee"
- hidden$="[[isColumnHidden('Assignee', visibleChangeTableColumns)]]">
- <template is="dom-if" if="[[change.assignee]]">
- <gr-account-link
- id="assigneeAccountLink"
- account="[[change.assignee]]"></gr-account-link>
- </template>
- <template is="dom-if" if="[[!change.assignee]]">
- <span class="placeholder">--</span>
- </template>
- </td>
- <td class="cell repo"
- hidden$="[[isColumnHidden('Repo', visibleChangeTableColumns)]]">
- <a class="fullRepo" href$="[[_computeRepoUrl(change)]]">
- [[_computeRepoDisplay(change)]]
- </a>
- <a
- class="truncatedRepo"
- href$="[[_computeRepoUrl(change)]]"
- title$="[[_computeRepoDisplay(change)]]">
- [[_computeRepoDisplay(change, 'true')]]
- </a>
- </td>
- <td class="cell branch"
- hidden$="[[isColumnHidden('Branch', visibleChangeTableColumns)]]">
- <a href$="[[_computeRepoBranchURL(change)]]">
- [[change.branch]]
- </a>
- <template is="dom-if" if="[[change.topic]]">
- (<a href$="[[_computeTopicURL(change)]]"><!--
- --><gr-limited-text limit="50" text="[[change.topic]]">
- </gr-limited-text><!--
- --></a>)
- </template>
- </td>
- <td class="cell updated"
- hidden$="[[isColumnHidden('Updated', visibleChangeTableColumns)]]">
- <gr-date-formatter
- has-tooltip
- date-str="[[change.updated]]"></gr-date-formatter>
- </td>
- <td class="cell size"
- hidden$="[[isColumnHidden('Size', visibleChangeTableColumns)]]">
- <gr-tooltip-content
- has-tooltip
- title="[[_computeSizeTooltip(change)]]">
- <template is="dom-if" if="[[_changeSize]]">
- <span>[[_changeSize]]</span>
- </template>
- <template is="dom-if" if="[[!_changeSize]]">
- <span class="placeholder">--</span>
- </template>
- </gr-tooltip-content>
- </td>
- <template is="dom-repeat" items="[[labelNames]]" as="labelName">
- <td title$="[[_computeLabelTitle(change, labelName)]]"
- class$="[[_computeLabelClass(change, labelName)]]">
- [[_computeLabelValue(change, labelName)]]
- </td>
- </template>
- <template is="dom-repeat" items="[[_dynamicCellEndpoints]]"
- as="pluginEndpointName">
- <td class="cell endpoint">
- <gr-endpoint-decorator name$="[[pluginEndpointName]]">
- <gr-endpoint-param name="change" value="[[change]]">
- </gr-endpoint-param>
- </gr-endpoint-decorator>
- </td>
- </template>
- </template>
- <script src="gr-change-list-item.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index 013b44b..6e2f11c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,210 +14,232 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- const CHANGE_SIZE = {
- XS: 10,
- SMALL: 50,
- MEDIUM: 250,
- LARGE: 1000,
- };
+import '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
+import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '../../../scripts/bundled-polymer.js';
+import '../../../styles/gr-change-list-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-account-link/gr-account-link.js';
+import '../../shared/gr-change-star/gr-change-star.js';
+import '../../shared/gr-change-status/gr-change-status.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-limited-text/gr-limited-text.js';
+import '../../shared/gr-tooltip-content/gr-tooltip-content.js';
+import '../../../styles/shared-styles.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-change-list-item_html.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.ChangeTableMixin
- * @appliesMixin Gerrit.PathListMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrChangeListItem extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.ChangeTableBehavior,
- Gerrit.PathListBehavior,
- Gerrit.RESTClientBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-change-list-item'; }
+const CHANGE_SIZE = {
+ XS: 10,
+ SMALL: 50,
+ MEDIUM: 250,
+ LARGE: 1000,
+};
- static get properties() {
- return {
- visibleChangeTableColumns: Array,
- labelNames: {
- type: Array,
- },
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.ChangeTableMixin
+ * @appliesMixin Gerrit.PathListMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrChangeListItem extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.ChangeTableBehavior,
+ Gerrit.PathListBehavior,
+ Gerrit.RESTClientBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** @type {?} */
- change: Object,
- changeURL: {
- type: String,
- computed: '_computeChangeURL(change)',
- },
- statuses: {
- type: Array,
- computed: 'changeStatuses(change)',
- },
- showStar: {
- type: Boolean,
- value: false,
- },
- showNumber: Boolean,
- _changeSize: {
- type: String,
- computed: '_computeChangeSize(change)',
- },
- _dynamicCellEndpoints: {
- type: Array,
- },
- };
+ static get is() { return 'gr-change-list-item'; }
+
+ static get properties() {
+ return {
+ visibleChangeTableColumns: Array,
+ labelNames: {
+ type: Array,
+ },
+
+ /** @type {?} */
+ change: Object,
+ changeURL: {
+ type: String,
+ computed: '_computeChangeURL(change)',
+ },
+ statuses: {
+ type: Array,
+ computed: 'changeStatuses(change)',
+ },
+ showStar: {
+ type: Boolean,
+ value: false,
+ },
+ showNumber: Boolean,
+ _changeSize: {
+ type: String,
+ computed: '_computeChangeSize(change)',
+ },
+ _dynamicCellEndpoints: {
+ type: Array,
+ },
+ };
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ Gerrit.awaitPluginsLoaded().then(() => {
+ this._dynamicCellEndpoints = Gerrit._endpoints.getDynamicEndpoints(
+ 'change-list-item-cell');
+ });
+ }
+
+ _computeChangeURL(change) {
+ return Gerrit.Nav.getUrlForChange(change);
+ }
+
+ _computeLabelTitle(change, labelName) {
+ const label = change.labels[labelName];
+ if (!label) { return 'Label not applicable'; }
+ const significantLabel = label.rejected || label.approved ||
+ label.disliked || label.recommended;
+ if (significantLabel && significantLabel.name) {
+ return labelName + '\nby ' + significantLabel.name;
}
+ return labelName;
+ }
- /** @override */
- attached() {
- super.attached();
- Gerrit.awaitPluginsLoaded().then(() => {
- this._dynamicCellEndpoints = Gerrit._endpoints.getDynamicEndpoints(
- 'change-list-item-cell');
- });
- }
-
- _computeChangeURL(change) {
- return Gerrit.Nav.getUrlForChange(change);
- }
-
- _computeLabelTitle(change, labelName) {
- const label = change.labels[labelName];
- if (!label) { return 'Label not applicable'; }
- const significantLabel = label.rejected || label.approved ||
- label.disliked || label.recommended;
- if (significantLabel && significantLabel.name) {
- return labelName + '\nby ' + significantLabel.name;
- }
- return labelName;
- }
-
- _computeLabelClass(change, labelName) {
- const label = change.labels[labelName];
- // Mimic a Set.
- const classes = {
- cell: true,
- label: true,
- };
- if (label) {
- if (label.approved) {
- classes['u-green'] = true;
- }
- if (label.value == 1) {
- classes['u-monospace'] = true;
- classes['u-green'] = true;
- } else if (label.value == -1) {
- classes['u-monospace'] = true;
- classes['u-red'] = true;
- }
- if (label.rejected) {
- classes['u-red'] = true;
- }
- } else {
- classes['u-gray-background'] = true;
- }
- return Object.keys(classes).sort()
- .join(' ');
- }
-
- _computeLabelValue(change, labelName) {
- const label = change.labels[labelName];
- if (!label) { return ''; }
+ _computeLabelClass(change, labelName) {
+ const label = change.labels[labelName];
+ // Mimic a Set.
+ const classes = {
+ cell: true,
+ label: true,
+ };
+ if (label) {
if (label.approved) {
- return '✓';
+ classes['u-green'] = true;
+ }
+ if (label.value == 1) {
+ classes['u-monospace'] = true;
+ classes['u-green'] = true;
+ } else if (label.value == -1) {
+ classes['u-monospace'] = true;
+ classes['u-red'] = true;
}
if (label.rejected) {
- return '✕';
+ classes['u-red'] = true;
}
- if (label.value > 0) {
- return '+' + label.value;
- }
- if (label.value < 0) {
- return label.value;
- }
- return '';
+ } else {
+ classes['u-gray-background'] = true;
}
+ return Object.keys(classes).sort()
+ .join(' ');
+ }
- _computeRepoUrl(change) {
- return Gerrit.Nav.getUrlForProjectChanges(change.project, true,
- change.internalHost);
+ _computeLabelValue(change, labelName) {
+ const label = change.labels[labelName];
+ if (!label) { return ''; }
+ if (label.approved) {
+ return '✓';
}
-
- _computeRepoBranchURL(change) {
- return Gerrit.Nav.getUrlForBranch(change.branch, change.project, null,
- change.internalHost);
+ if (label.rejected) {
+ return '✕';
}
-
- _computeTopicURL(change) {
- if (!change.topic) { return ''; }
- return Gerrit.Nav.getUrlForTopic(change.topic, change.internalHost);
+ if (label.value > 0) {
+ return '+' + label.value;
}
-
- /**
- * Computes the display string for the project column. If there is a host
- * specified in the change detail, the string will be prefixed with it.
- *
- * @param {!Object} change
- * @param {string=} truncate whether or not the project name should be
- * truncated. If this value is truthy, the name will be truncated.
- * @return {string}
- */
- _computeRepoDisplay(change, truncate) {
- if (!change || !change.project) { return ''; }
- let str = '';
- if (change.internalHost) { str += change.internalHost + '/'; }
- str += truncate ? this.truncatePath(change.project, 2) : change.project;
- return str;
+ if (label.value < 0) {
+ return label.value;
}
+ return '';
+ }
- _computeSizeTooltip(change) {
- if (change.insertions + change.deletions === 0 ||
- isNaN(change.insertions + change.deletions)) {
- return 'Size unknown';
- } else {
- return `+${change.insertions}, -${change.deletions}`;
- }
- }
+ _computeRepoUrl(change) {
+ return Gerrit.Nav.getUrlForProjectChanges(change.project, true,
+ change.internalHost);
+ }
- /**
- * TShirt sizing is based on the following paper:
- * http://dirkriehle.com/wp-content/uploads/2008/09/hicss-42-csdistr-final-web.pdf
- */
- _computeChangeSize(change) {
- const delta = change.insertions + change.deletions;
- if (isNaN(delta) || delta === 0) {
- return null; // Unknown
- }
- if (delta < CHANGE_SIZE.XS) {
- return 'XS';
- } else if (delta < CHANGE_SIZE.SMALL) {
- return 'S';
- } else if (delta < CHANGE_SIZE.MEDIUM) {
- return 'M';
- } else if (delta < CHANGE_SIZE.LARGE) {
- return 'L';
- } else {
- return 'XL';
- }
- }
+ _computeRepoBranchURL(change) {
+ return Gerrit.Nav.getUrlForBranch(change.branch, change.project, null,
+ change.internalHost);
+ }
- toggleReviewed() {
- const newVal = !this.change.reviewed;
- this.set('change.reviewed', newVal);
- this.dispatchEvent(new CustomEvent('toggle-reviewed', {
- bubbles: true,
- composed: true,
- detail: {change: this.change, reviewed: newVal},
- }));
+ _computeTopicURL(change) {
+ if (!change.topic) { return ''; }
+ return Gerrit.Nav.getUrlForTopic(change.topic, change.internalHost);
+ }
+
+ /**
+ * Computes the display string for the project column. If there is a host
+ * specified in the change detail, the string will be prefixed with it.
+ *
+ * @param {!Object} change
+ * @param {string=} truncate whether or not the project name should be
+ * truncated. If this value is truthy, the name will be truncated.
+ * @return {string}
+ */
+ _computeRepoDisplay(change, truncate) {
+ if (!change || !change.project) { return ''; }
+ let str = '';
+ if (change.internalHost) { str += change.internalHost + '/'; }
+ str += truncate ? this.truncatePath(change.project, 2) : change.project;
+ return str;
+ }
+
+ _computeSizeTooltip(change) {
+ if (change.insertions + change.deletions === 0 ||
+ isNaN(change.insertions + change.deletions)) {
+ return 'Size unknown';
+ } else {
+ return `+${change.insertions}, -${change.deletions}`;
}
}
- customElements.define(GrChangeListItem.is, GrChangeListItem);
-})();
+ /**
+ * TShirt sizing is based on the following paper:
+ * http://dirkriehle.com/wp-content/uploads/2008/09/hicss-42-csdistr-final-web.pdf
+ */
+ _computeChangeSize(change) {
+ const delta = change.insertions + change.deletions;
+ if (isNaN(delta) || delta === 0) {
+ return null; // Unknown
+ }
+ if (delta < CHANGE_SIZE.XS) {
+ return 'XS';
+ } else if (delta < CHANGE_SIZE.SMALL) {
+ return 'S';
+ } else if (delta < CHANGE_SIZE.MEDIUM) {
+ return 'M';
+ } else if (delta < CHANGE_SIZE.LARGE) {
+ return 'L';
+ } else {
+ return 'XL';
+ }
+ }
+
+ toggleReviewed() {
+ const newVal = !this.change.reviewed;
+ this.set('change.reviewed', newVal);
+ this.dispatchEvent(new CustomEvent('toggle-reviewed', {
+ bubbles: true,
+ composed: true,
+ detail: {change: this.change, reviewed: newVal},
+ }));
+ }
+}
+
+customElements.define(GrChangeListItem.is, GrChangeListItem);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js
new file mode 100644
index 0000000..f76189c
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js
@@ -0,0 +1,198 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: table-row;
+ color: var(--primary-text-color);
+ }
+ :host(:focus) {
+ outline: none;
+ }
+ :host(:hover) {
+ background-color: var(--hover-background-color);
+ }
+ :host([needs-review]) {
+ font-weight: var(--font-weight-bold);
+ color: var(--primary-text-color);
+ }
+ :host([highlight]) {
+ background-color: var(--assignee-highlight-color);
+ }
+ .container {
+ position: relative;
+ }
+ .content {
+ overflow: hidden;
+ position: absolute;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 100%;
+ }
+ .content a {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 100%;
+ }
+ .spacer {
+ height: 0;
+ overflow: hidden;
+ }
+ .status {
+ align-items: center;
+ display: inline-flex;
+ }
+ .status .comma {
+ padding-right: var(--spacing-xs);
+ }
+ /* Used to hide the leading separator comma for statuses. */
+ .status .comma:first-of-type {
+ display: none;
+ }
+ .size gr-tooltip-content {
+ margin: -.4rem -.6rem;
+ max-width: 2.5rem;
+ padding: var(--spacing-m) var(--spacing-l);
+ }
+ a {
+ color: inherit;
+ cursor: pointer;
+ text-decoration: none;
+ }
+ a:hover {
+ text-decoration: underline;
+ }
+ .u-monospace {
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-mono);
+ line-height: var(--line-height-mono);
+ }
+ .u-green {
+ color: var(--vote-text-color-recommended);
+ }
+ .u-red {
+ color: var(--vote-text-color-disliked);
+ }
+ .u-gray-background {
+ background-color: var(--table-header-background-color);
+ }
+ .comma,
+ .placeholder {
+ color: var(--deemphasized-text-color);
+ }
+ .cell.label {
+ font-weight: var(--font-weight-normal);
+ }
+ @media only screen and (max-width: 50em) {
+ :host {
+ display: flex;
+ }
+ }
+ </style>
+ <style include="gr-change-list-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <td class="cell leftPadding"></td>
+ <td class="cell star" hidden\$="[[!showStar]]" hidden="">
+ <gr-change-star change="{{change}}"></gr-change-star>
+ </td>
+ <td class="cell number" hidden\$="[[!showNumber]]" hidden="">
+ <a href\$="[[changeURL]]">[[change._number]]</a>
+ </td>
+ <td class="cell subject" hidden\$="[[isColumnHidden('Subject', visibleChangeTableColumns)]]">
+ <div class="container">
+ <div class="content">
+ <a title\$="[[change.subject]]" href\$="[[changeURL]]">
+ [[change.subject]]
+ </a>
+ </div>
+ <div class="spacer">
+ [[change.subject]]
+ </div>
+ <span> </span>
+ </div>
+ </td>
+ <td class="cell status" hidden\$="[[isColumnHidden('Status', visibleChangeTableColumns)]]">
+ <template is="dom-repeat" items="[[statuses]]" as="status">
+ <div class="comma">,</div>
+ <gr-change-status flat="" status="[[status]]"></gr-change-status>
+ </template>
+ <template is="dom-if" if="[[!statuses.length]]">
+ <span class="placeholder">--</span>
+ </template>
+ </td>
+ <td class="cell owner" hidden\$="[[isColumnHidden('Owner', visibleChangeTableColumns)]]">
+ <gr-account-link account="[[change.owner]]"></gr-account-link>
+ </td>
+ <td class="cell assignee" hidden\$="[[isColumnHidden('Assignee', visibleChangeTableColumns)]]">
+ <template is="dom-if" if="[[change.assignee]]">
+ <gr-account-link id="assigneeAccountLink" account="[[change.assignee]]"></gr-account-link>
+ </template>
+ <template is="dom-if" if="[[!change.assignee]]">
+ <span class="placeholder">--</span>
+ </template>
+ </td>
+ <td class="cell repo" hidden\$="[[isColumnHidden('Repo', visibleChangeTableColumns)]]">
+ <a class="fullRepo" href\$="[[_computeRepoUrl(change)]]">
+ [[_computeRepoDisplay(change)]]
+ </a>
+ <a class="truncatedRepo" href\$="[[_computeRepoUrl(change)]]" title\$="[[_computeRepoDisplay(change)]]">
+ [[_computeRepoDisplay(change, 'true')]]
+ </a>
+ </td>
+ <td class="cell branch" hidden\$="[[isColumnHidden('Branch', visibleChangeTableColumns)]]">
+ <a href\$="[[_computeRepoBranchURL(change)]]">
+ [[change.branch]]
+ </a>
+ <template is="dom-if" if="[[change.topic]]">
+ (<a href\$="[[_computeTopicURL(change)]]"><!--
+ --><gr-limited-text limit="50" text="[[change.topic]]">
+ </gr-limited-text><!--
+ --></a>)
+ </template>
+ </td>
+ <td class="cell updated" hidden\$="[[isColumnHidden('Updated', visibleChangeTableColumns)]]">
+ <gr-date-formatter has-tooltip="" date-str="[[change.updated]]"></gr-date-formatter>
+ </td>
+ <td class="cell size" hidden\$="[[isColumnHidden('Size', visibleChangeTableColumns)]]">
+ <gr-tooltip-content has-tooltip="" title="[[_computeSizeTooltip(change)]]">
+ <template is="dom-if" if="[[_changeSize]]">
+ <span>[[_changeSize]]</span>
+ </template>
+ <template is="dom-if" if="[[!_changeSize]]">
+ <span class="placeholder">--</span>
+ </template>
+ </gr-tooltip-content>
+ </td>
+ <template is="dom-repeat" items="[[labelNames]]" as="labelName">
+ <td title\$="[[_computeLabelTitle(change, labelName)]]" class\$="[[_computeLabelClass(change, labelName)]]">
+ [[_computeLabelValue(change, labelName)]]
+ </td>
+ </template>
+ <template is="dom-repeat" items="[[_dynamicCellEndpoints]]" as="pluginEndpointName">
+ <td class="cell endpoint">
+ <gr-endpoint-decorator name\$="[[pluginEndpointName]]">
+ <gr-endpoint-param name="change" value="[[change]]">
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </td>
+ </template>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
index 076c478..d26fc17 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -19,17 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-list-item</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="gr-change-list-item.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -37,244 +30,246 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-list-item tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-change-list-item.js';
+suite('gr-change-list-item tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getLoggedIn() { return Promise.resolve(false); },
- });
- element = fixture('basic');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getLoggedIn() { return Promise.resolve(false); },
});
+ element = fixture('basic');
+ });
- teardown(() => { sandbox.restore(); });
+ teardown(() => { sandbox.restore(); });
- test('computed fields', () => {
- assert.equal(element._computeLabelClass({labels: {}}),
- 'cell label u-gray-background');
- assert.equal(element._computeLabelClass(
- {labels: {}}, 'Verified'), 'cell label u-gray-background');
- assert.equal(element._computeLabelClass(
- {labels: {Verified: {approved: true, value: 1}}}, 'Verified'),
- 'cell label u-green u-monospace');
- assert.equal(element._computeLabelClass(
- {labels: {Verified: {rejected: true, value: -1}}}, 'Verified'),
- 'cell label u-monospace u-red');
- assert.equal(element._computeLabelClass(
- {labels: {'Code-Review': {value: 1}}}, 'Code-Review'),
- 'cell label u-green u-monospace');
- assert.equal(element._computeLabelClass(
- {labels: {'Code-Review': {value: -1}}}, 'Code-Review'),
- 'cell label u-monospace u-red');
- assert.equal(element._computeLabelClass(
- {labels: {'Code-Review': {value: -1}}}, 'Verified'),
- 'cell label u-gray-background');
+ test('computed fields', () => {
+ assert.equal(element._computeLabelClass({labels: {}}),
+ 'cell label u-gray-background');
+ assert.equal(element._computeLabelClass(
+ {labels: {}}, 'Verified'), 'cell label u-gray-background');
+ assert.equal(element._computeLabelClass(
+ {labels: {Verified: {approved: true, value: 1}}}, 'Verified'),
+ 'cell label u-green u-monospace');
+ assert.equal(element._computeLabelClass(
+ {labels: {Verified: {rejected: true, value: -1}}}, 'Verified'),
+ 'cell label u-monospace u-red');
+ assert.equal(element._computeLabelClass(
+ {labels: {'Code-Review': {value: 1}}}, 'Code-Review'),
+ 'cell label u-green u-monospace');
+ assert.equal(element._computeLabelClass(
+ {labels: {'Code-Review': {value: -1}}}, 'Code-Review'),
+ 'cell label u-monospace u-red');
+ assert.equal(element._computeLabelClass(
+ {labels: {'Code-Review': {value: -1}}}, 'Verified'),
+ 'cell label u-gray-background');
- assert.equal(element._computeLabelTitle({labels: {}}, 'Verified'),
- 'Label not applicable');
- assert.equal(element._computeLabelTitle(
- {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Verified'),
- 'Verified\nby Diffy');
- assert.equal(element._computeLabelTitle(
- {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Code-Review'),
- 'Label not applicable');
- assert.equal(element._computeLabelTitle(
- {labels: {Verified: {rejected: {name: 'Diffy'}}}}, 'Verified'),
- 'Verified\nby Diffy');
- assert.equal(element._computeLabelTitle(
- {labels: {'Code-Review': {disliked: {name: 'Diffy'}, value: -1}}},
- 'Code-Review'), 'Code-Review\nby Diffy');
- assert.equal(element._computeLabelTitle(
- {labels: {'Code-Review': {recommended: {name: 'Diffy'}, value: 1}}},
- 'Code-Review'), 'Code-Review\nby Diffy');
- assert.equal(element._computeLabelTitle(
- {labels: {'Code-Review': {recommended: {name: 'Diffy'},
- rejected: {name: 'Admin'}}}}, 'Code-Review'),
- 'Code-Review\nby Admin');
- assert.equal(element._computeLabelTitle(
- {labels: {'Code-Review': {approved: {name: 'Diffy'},
- rejected: {name: 'Admin'}}}}, 'Code-Review'),
- 'Code-Review\nby Admin');
- assert.equal(element._computeLabelTitle(
- {labels: {'Code-Review': {recommended: {name: 'Diffy'},
- disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'),
- 'Code-Review\nby Admin');
- assert.equal(element._computeLabelTitle(
- {labels: {'Code-Review': {approved: {name: 'Diffy'},
- disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'),
- 'Code-Review\nby Diffy');
+ assert.equal(element._computeLabelTitle({labels: {}}, 'Verified'),
+ 'Label not applicable');
+ assert.equal(element._computeLabelTitle(
+ {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Verified'),
+ 'Verified\nby Diffy');
+ assert.equal(element._computeLabelTitle(
+ {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Code-Review'),
+ 'Label not applicable');
+ assert.equal(element._computeLabelTitle(
+ {labels: {Verified: {rejected: {name: 'Diffy'}}}}, 'Verified'),
+ 'Verified\nby Diffy');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {disliked: {name: 'Diffy'}, value: -1}}},
+ 'Code-Review'), 'Code-Review\nby Diffy');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {recommended: {name: 'Diffy'}, value: 1}}},
+ 'Code-Review'), 'Code-Review\nby Diffy');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {recommended: {name: 'Diffy'},
+ rejected: {name: 'Admin'}}}}, 'Code-Review'),
+ 'Code-Review\nby Admin');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {approved: {name: 'Diffy'},
+ rejected: {name: 'Admin'}}}}, 'Code-Review'),
+ 'Code-Review\nby Admin');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {recommended: {name: 'Diffy'},
+ disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'),
+ 'Code-Review\nby Admin');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {approved: {name: 'Diffy'},
+ disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'),
+ 'Code-Review\nby Diffy');
- assert.equal(element._computeLabelValue({labels: {}}), '');
- assert.equal(element._computeLabelValue({labels: {}}, 'Verified'), '');
- assert.equal(element._computeLabelValue(
- {labels: {Verified: {approved: true, value: 1}}}, 'Verified'), '✓');
- assert.equal(element._computeLabelValue(
- {labels: {Verified: {value: 1}}}, 'Verified'), '+1');
- assert.equal(element._computeLabelValue(
- {labels: {Verified: {value: -1}}}, 'Verified'), '-1');
- assert.equal(element._computeLabelValue(
- {labels: {Verified: {approved: true}}}, 'Verified'), '✓');
- assert.equal(element._computeLabelValue(
- {labels: {Verified: {rejected: true}}}, 'Verified'), '✕');
- });
+ assert.equal(element._computeLabelValue({labels: {}}), '');
+ assert.equal(element._computeLabelValue({labels: {}}, 'Verified'), '');
+ assert.equal(element._computeLabelValue(
+ {labels: {Verified: {approved: true, value: 1}}}, 'Verified'), '✓');
+ assert.equal(element._computeLabelValue(
+ {labels: {Verified: {value: 1}}}, 'Verified'), '+1');
+ assert.equal(element._computeLabelValue(
+ {labels: {Verified: {value: -1}}}, 'Verified'), '-1');
+ assert.equal(element._computeLabelValue(
+ {labels: {Verified: {approved: true}}}, 'Verified'), '✓');
+ assert.equal(element._computeLabelValue(
+ {labels: {Verified: {rejected: true}}}, 'Verified'), '✕');
+ });
- test('no hidden columns', () => {
- element.visibleChangeTableColumns = [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Repo',
- 'Branch',
- 'Updated',
- 'Size',
- ];
+ test('no hidden columns', () => {
+ element.visibleChangeTableColumns = [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Repo',
+ 'Branch',
+ 'Updated',
+ 'Size',
+ ];
- flushAsynchronousOperations();
+ flushAsynchronousOperations();
- for (const column of element.columnNames) {
- const elementClass = '.' + column.toLowerCase();
- assert.isOk(element.shadowRoot
- .querySelector(elementClass),
- `Expect ${elementClass} element to be found`);
+ for (const column of element.columnNames) {
+ const elementClass = '.' + column.toLowerCase();
+ assert.isOk(element.shadowRoot
+ .querySelector(elementClass),
+ `Expect ${elementClass} element to be found`);
+ assert.isFalse(element.shadowRoot
+ .querySelector(elementClass).hidden);
+ }
+ });
+
+ test('repo column hidden', () => {
+ element.visibleChangeTableColumns = [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Branch',
+ 'Updated',
+ 'Size',
+ ];
+
+ flushAsynchronousOperations();
+
+ for (const column of element.columnNames) {
+ const elementClass = '.' + column.toLowerCase();
+ if (column === 'Repo') {
+ assert.isTrue(element.shadowRoot
+ .querySelector(elementClass).hidden);
+ } else {
assert.isFalse(element.shadowRoot
.querySelector(elementClass).hidden);
}
- });
-
- test('repo column hidden', () => {
- element.visibleChangeTableColumns = [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Branch',
- 'Updated',
- 'Size',
- ];
-
- flushAsynchronousOperations();
-
- for (const column of element.columnNames) {
- const elementClass = '.' + column.toLowerCase();
- if (column === 'Repo') {
- assert.isTrue(element.shadowRoot
- .querySelector(elementClass).hidden);
- } else {
- assert.isFalse(element.shadowRoot
- .querySelector(elementClass).hidden);
- }
- }
- });
-
- test('random column does not exist', () => {
- element.visibleChangeTableColumns = [
- 'Bad',
- ];
-
- flushAsynchronousOperations();
- const elementClass = '.bad';
- assert.isNotOk(element.shadowRoot
- .querySelector(elementClass));
- });
-
- test('assignee only displayed if there is one', () => {
- element.change = {};
- flushAsynchronousOperations();
- assert.isNotOk(element.shadowRoot
- .querySelector('.assignee gr-account-link'));
- assert.equal(element.shadowRoot
- .querySelector('.assignee').textContent.trim(), '--');
- element.change = {
- assignee: {
- name: 'test',
- status: 'test',
- },
- };
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('.assignee gr-account-link'));
- });
-
- test('TShirt sizing tooltip', () => {
- assert.equal(element._computeSizeTooltip({
- insertions: 'foo',
- deletions: 'bar',
- }), 'Size unknown');
- assert.equal(element._computeSizeTooltip({
- insertions: 0,
- deletions: 0,
- }), 'Size unknown');
- assert.equal(element._computeSizeTooltip({
- insertions: 1,
- deletions: 2,
- }), '+1, -2');
- });
-
- test('TShirt sizing', () => {
- assert.equal(element._computeChangeSize({
- insertions: 'foo',
- deletions: 'bar',
- }), null);
- assert.equal(element._computeChangeSize({
- insertions: 1,
- deletions: 1,
- }), 'XS');
- assert.equal(element._computeChangeSize({
- insertions: 9,
- deletions: 1,
- }), 'S');
- assert.equal(element._computeChangeSize({
- insertions: 10,
- deletions: 200,
- }), 'M');
- assert.equal(element._computeChangeSize({
- insertions: 99,
- deletions: 900,
- }), 'L');
- assert.equal(element._computeChangeSize({
- insertions: 99,
- deletions: 999,
- }), 'XL');
- });
-
- test('change params passed to gr-navigation', () => {
- sandbox.stub(Gerrit.Nav);
- const change = {
- internalHost: 'test-host',
- project: 'test-repo',
- topic: 'test-topic',
- branch: 'test-branch',
- };
- element.change = change;
- flushAsynchronousOperations();
-
- assert.deepEqual(Gerrit.Nav.getUrlForChange.lastCall.args, [change]);
- assert.deepEqual(Gerrit.Nav.getUrlForProjectChanges.lastCall.args,
- [change.project, true, change.internalHost]);
- assert.deepEqual(Gerrit.Nav.getUrlForBranch.lastCall.args,
- [change.branch, change.project, null, change.internalHost]);
- assert.deepEqual(Gerrit.Nav.getUrlForTopic.lastCall.args,
- [change.topic, change.internalHost]);
- });
-
- test('_computeRepoDisplay', () => {
- const change = {
- project: 'a/test/repo',
- internalHost: 'host',
- };
- assert.equal(element._computeRepoDisplay(change), 'host/a/test/repo');
- assert.equal(element._computeRepoDisplay(change, true),
- 'host/…/test/repo');
- delete change.internalHost;
- assert.equal(element._computeRepoDisplay(change), 'a/test/repo');
- assert.equal(element._computeRepoDisplay(change, true),
- '…/test/repo');
- });
+ }
});
+
+ test('random column does not exist', () => {
+ element.visibleChangeTableColumns = [
+ 'Bad',
+ ];
+
+ flushAsynchronousOperations();
+ const elementClass = '.bad';
+ assert.isNotOk(element.shadowRoot
+ .querySelector(elementClass));
+ });
+
+ test('assignee only displayed if there is one', () => {
+ element.change = {};
+ flushAsynchronousOperations();
+ assert.isNotOk(element.shadowRoot
+ .querySelector('.assignee gr-account-link'));
+ assert.equal(element.shadowRoot
+ .querySelector('.assignee').textContent.trim(), '--');
+ element.change = {
+ assignee: {
+ name: 'test',
+ status: 'test',
+ },
+ };
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('.assignee gr-account-link'));
+ });
+
+ test('TShirt sizing tooltip', () => {
+ assert.equal(element._computeSizeTooltip({
+ insertions: 'foo',
+ deletions: 'bar',
+ }), 'Size unknown');
+ assert.equal(element._computeSizeTooltip({
+ insertions: 0,
+ deletions: 0,
+ }), 'Size unknown');
+ assert.equal(element._computeSizeTooltip({
+ insertions: 1,
+ deletions: 2,
+ }), '+1, -2');
+ });
+
+ test('TShirt sizing', () => {
+ assert.equal(element._computeChangeSize({
+ insertions: 'foo',
+ deletions: 'bar',
+ }), null);
+ assert.equal(element._computeChangeSize({
+ insertions: 1,
+ deletions: 1,
+ }), 'XS');
+ assert.equal(element._computeChangeSize({
+ insertions: 9,
+ deletions: 1,
+ }), 'S');
+ assert.equal(element._computeChangeSize({
+ insertions: 10,
+ deletions: 200,
+ }), 'M');
+ assert.equal(element._computeChangeSize({
+ insertions: 99,
+ deletions: 900,
+ }), 'L');
+ assert.equal(element._computeChangeSize({
+ insertions: 99,
+ deletions: 999,
+ }), 'XL');
+ });
+
+ test('change params passed to gr-navigation', () => {
+ sandbox.stub(Gerrit.Nav);
+ const change = {
+ internalHost: 'test-host',
+ project: 'test-repo',
+ topic: 'test-topic',
+ branch: 'test-branch',
+ };
+ element.change = change;
+ flushAsynchronousOperations();
+
+ assert.deepEqual(Gerrit.Nav.getUrlForChange.lastCall.args, [change]);
+ assert.deepEqual(Gerrit.Nav.getUrlForProjectChanges.lastCall.args,
+ [change.project, true, change.internalHost]);
+ assert.deepEqual(Gerrit.Nav.getUrlForBranch.lastCall.args,
+ [change.branch, change.project, null, change.internalHost]);
+ assert.deepEqual(Gerrit.Nav.getUrlForTopic.lastCall.args,
+ [change.topic, change.internalHost]);
+ });
+
+ test('_computeRepoDisplay', () => {
+ const change = {
+ project: 'a/test/repo',
+ internalHost: 'host',
+ };
+ assert.equal(element._computeRepoDisplay(change), 'host/a/test/repo');
+ assert.equal(element._computeRepoDisplay(change, true),
+ 'host/…/test/repo');
+ delete change.internalHost;
+ assert.equal(element._computeRepoDisplay(change), 'a/test/repo');
+ assert.equal(element._computeRepoDisplay(change, true),
+ '…/test/repo');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
deleted file mode 100644
index 6c9d975..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
+++ /dev/null
@@ -1,108 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-change-list/gr-change-list.html">
-<link rel="import" href="../gr-repo-header/gr-repo-header.html">
-<link rel="import" href="../gr-user-header/gr-user-header.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-change-list-view">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- }
- .loading {
- color: var(--deemphasized-text-color);
- padding: var(--spacing-l);
- }
- gr-change-list {
- width: 100%;
- }
- gr-user-header,
- gr-repo-header {
- border-bottom: 1px solid var(--border-color);
- }
- nav {
- align-items: center;
- display: flex;
- height: 3rem;
- justify-content: flex-end;
- margin-right: 20px;
- }
- nav,
- iron-icon {
- color: var(--deemphasized-text-color);
- }
- iron-icon {
- height: 1.85rem;
- margin-left: 16px;
- width: 1.85rem;
- }
- .hide {
- display: none;
- }
- @media only screen and (max-width: 50em) {
- .loading,
- .error {
- padding: 0 var(--spacing-l);
- }
- }
- </style>
- <div class="loading" hidden$="[[!_loading]]" hidden>Loading...</div>
- <div hidden$="[[_loading]]" hidden>
- <gr-repo-header
- repo="[[_repo]]"
- class$="[[_computeHeaderClass(_repo)]]"></gr-repo-header>
- <gr-user-header
- user-id="[[_userId]]"
- show-dashboard-link
- logged-in="[[_loggedIn]]"
- class$="[[_computeHeaderClass(_userId)]]"></gr-user-header>
- <gr-change-list
- account="[[account]]"
- changes="{{_changes}}"
- preferences="[[preferences]]"
- selected-index="{{viewState.selectedChangeIndex}}"
- show-star="[[_loggedIn]]"
- on-toggle-star="_handleToggleStar"
- on-toggle-reviewed="_handleToggleReviewed"></gr-change-list>
- <nav class$="[[_computeNavClass(_loading)]]">
- Page [[_computePage(_offset, _changesPerPage)]]
- <a id="prevArrow"
- href$="[[_computeNavLink(_query, _offset, -1, _changesPerPage)]]"
- class$="[[_computePrevArrowClass(_offset)]]">
- <iron-icon icon="gr-icons:chevron-left"></iron-icon>
- </a>
- <a id="nextArrow"
- href$="[[_computeNavLink(_query, _offset, 1, _changesPerPage)]]"
- class$="[[_computeNextArrowClass(_changes)]]">
- <iron-icon icon="gr-icons:chevron-right"></iron-icon>
- </a>
- </nav>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-change-list-view.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index b82593d..383aafa 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,282 +14,298 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- const LookupQueryPatterns = {
- CHANGE_ID: /^\s*i?[0-9a-f]{7,40}\s*$/i,
- CHANGE_NUM: /^\s*[1-9][0-9]*\s*$/g,
- };
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-change-list/gr-change-list.js';
+import '../gr-repo-header/gr-repo-header.js';
+import '../gr-user-header/gr-user-header.js';
+import '../../../styles/shared-styles.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-change-list-view_html.js';
- const USER_QUERY_PATTERN = /^owner:\s?("[^"]+"|[^ ]+)$/;
+const LookupQueryPatterns = {
+ CHANGE_ID: /^\s*i?[0-9a-f]{7,40}\s*$/i,
+ CHANGE_NUM: /^\s*[1-9][0-9]*\s*$/g,
+};
- const REPO_QUERY_PATTERN =
- /^project:\s?("[^"]+"|[^ ]+)(\sstatus\s?:(open|"open"))?$/;
+const USER_QUERY_PATTERN = /^owner:\s?("[^"]+"|[^ ]+)$/;
- const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i;
+const REPO_QUERY_PATTERN =
+ /^project:\s?("[^"]+"|[^ ]+)(\sstatus\s?:(open|"open"))?$/;
+const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i;
+
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrChangeListView extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.FireBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-change-list-view'; }
/**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
+ * Fired when the title of the page should change.
+ *
+ * @event title-change
*/
- class GrChangeListView extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.FireBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-change-list-view'; }
+
+ static get properties() {
+ return {
/**
- * Fired when the title of the page should change.
- *
- * @event title-change
+ * URL params passed from the router.
*/
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
- static get properties() {
- return {
/**
- * URL params passed from the router.
+ * True when user is logged in.
*/
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
+ _loggedIn: {
+ type: Boolean,
+ computed: '_computeLoggedIn(account)',
+ },
- /**
- * True when user is logged in.
- */
- _loggedIn: {
- type: Boolean,
- computed: '_computeLoggedIn(account)',
- },
+ account: {
+ type: Object,
+ value: null,
+ },
- account: {
- type: Object,
- value: null,
- },
+ /**
+ * State persisted across restamps of the element.
+ *
+ * Need sub-property declaration since it is used in template before
+ * assignment.
+ *
+ * @type {{ selectedChangeIndex: (number|undefined) }}
+ *
+ */
+ viewState: {
+ type: Object,
+ notify: true,
+ value() { return {}; },
+ },
- /**
- * State persisted across restamps of the element.
- *
- * Need sub-property declaration since it is used in template before
- * assignment.
- *
- * @type {{ selectedChangeIndex: (number|undefined) }}
- *
- */
- viewState: {
- type: Object,
- notify: true,
- value() { return {}; },
- },
+ preferences: Object,
- preferences: Object,
+ _changesPerPage: Number,
- _changesPerPage: Number,
+ /**
+ * Currently active query.
+ */
+ _query: {
+ type: String,
+ value: '',
+ },
- /**
- * Currently active query.
- */
- _query: {
- type: String,
- value: '',
- },
+ /**
+ * Offset of currently visible query results.
+ */
+ _offset: Number,
- /**
- * Offset of currently visible query results.
- */
- _offset: Number,
+ /**
+ * Change objects loaded from the server.
+ */
+ _changes: {
+ type: Array,
+ observer: '_changesChanged',
+ },
- /**
- * Change objects loaded from the server.
- */
- _changes: {
- type: Array,
- observer: '_changesChanged',
- },
+ /**
+ * For showing a "loading..." string during ajax requests.
+ */
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
- /**
- * For showing a "loading..." string during ajax requests.
- */
- _loading: {
- type: Boolean,
- value: true,
- },
+ /** @type {?string} */
+ _userId: {
+ type: String,
+ value: null,
+ },
- /** @type {?string} */
- _userId: {
- type: String,
- value: null,
- },
+ /** @type {?string} */
+ _repo: {
+ type: String,
+ value: null,
+ },
+ };
+ }
- /** @type {?string} */
- _repo: {
- type: String,
- value: null,
- },
- };
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('next-page',
+ () => this._handleNextPage());
+ this.addEventListener('previous-page',
+ () => this._handlePreviousPage());
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._loadPreferences();
+ }
+
+ _paramsChanged(value) {
+ if (value.view !== Gerrit.Nav.View.SEARCH) { return; }
+
+ this._loading = true;
+ this._query = value.query;
+ this._offset = value.offset || 0;
+ if (this.viewState.query != this._query ||
+ this.viewState.offset != this._offset) {
+ this.set('viewState.selectedChangeIndex', 0);
+ this.set('viewState.query', this._query);
+ this.set('viewState.offset', this._offset);
}
- /** @override */
- created() {
- super.created();
- this.addEventListener('next-page',
- () => this._handleNextPage());
- this.addEventListener('previous-page',
- () => this._handlePreviousPage());
- }
+ // NOTE: This method may be called before attachment. Fire title-change
+ // in an async so that attachment to the DOM can take place first.
+ this.async(() => this.fire('title-change', {title: this._query}));
- /** @override */
- attached() {
- super.attached();
- this._loadPreferences();
- }
-
- _paramsChanged(value) {
- if (value.view !== Gerrit.Nav.View.SEARCH) { return; }
-
- this._loading = true;
- this._query = value.query;
- this._offset = value.offset || 0;
- if (this.viewState.query != this._query ||
- this.viewState.offset != this._offset) {
- this.set('viewState.selectedChangeIndex', 0);
- this.set('viewState.query', this._query);
- this.set('viewState.offset', this._offset);
- }
-
- // NOTE: This method may be called before attachment. Fire title-change
- // in an async so that attachment to the DOM can take place first.
- this.async(() => this.fire('title-change', {title: this._query}));
-
- this._getPreferences()
- .then(prefs => {
- this._changesPerPage = prefs.changes_per_page;
- return this._getChanges();
- })
- .then(changes => {
- changes = changes || [];
- if (this._query && changes.length === 1) {
- for (const query in LookupQueryPatterns) {
- if (LookupQueryPatterns.hasOwnProperty(query) &&
- this._query.match(LookupQueryPatterns[query])) {
- Gerrit.Nav.navigateToChange(changes[0]);
- return;
- }
+ this._getPreferences()
+ .then(prefs => {
+ this._changesPerPage = prefs.changes_per_page;
+ return this._getChanges();
+ })
+ .then(changes => {
+ changes = changes || [];
+ if (this._query && changes.length === 1) {
+ for (const query in LookupQueryPatterns) {
+ if (LookupQueryPatterns.hasOwnProperty(query) &&
+ this._query.match(LookupQueryPatterns[query])) {
+ Gerrit.Nav.navigateToChange(changes[0]);
+ return;
}
}
- this._changes = changes;
- this._loading = false;
- });
- }
+ }
+ this._changes = changes;
+ this._loading = false;
+ });
+ }
- _loadPreferences() {
- return this.$.restAPI.getLoggedIn().then(loggedIn => {
- if (loggedIn) {
- this._getPreferences().then(preferences => {
- this.preferences = preferences;
- });
- } else {
- this.preferences = {};
- }
- });
- }
-
- _getChanges() {
- return this.$.restAPI.getChanges(this._changesPerPage, this._query,
- this._offset);
- }
-
- _getPreferences() {
- return this.$.restAPI.getPreferences();
- }
-
- _limitFor(query, defaultLimit) {
- const match = query.match(LIMIT_OPERATOR_PATTERN);
- if (!match) {
- return defaultLimit;
+ _loadPreferences() {
+ return this.$.restAPI.getLoggedIn().then(loggedIn => {
+ if (loggedIn) {
+ this._getPreferences().then(preferences => {
+ this.preferences = preferences;
+ });
+ } else {
+ this.preferences = {};
}
- return parseInt(match[1], 10);
- }
+ });
+ }
- _computeNavLink(query, offset, direction, changesPerPage) {
- // Offset could be a string when passed from the router.
- offset = +(offset || 0);
- const limit = this._limitFor(query, changesPerPage);
- const newOffset = Math.max(0, offset + (limit * direction));
- return Gerrit.Nav.getUrlForSearchQuery(query, newOffset);
- }
+ _getChanges() {
+ return this.$.restAPI.getChanges(this._changesPerPage, this._query,
+ this._offset);
+ }
- _computePrevArrowClass(offset) {
- return offset === 0 ? 'hide' : '';
- }
+ _getPreferences() {
+ return this.$.restAPI.getPreferences();
+ }
- _computeNextArrowClass(changes) {
- const more = changes.length && changes[changes.length - 1]._more_changes;
- return more ? '' : 'hide';
+ _limitFor(query, defaultLimit) {
+ const match = query.match(LIMIT_OPERATOR_PATTERN);
+ if (!match) {
+ return defaultLimit;
}
+ return parseInt(match[1], 10);
+ }
- _computeNavClass(loading) {
- return loading || !this._changes || !this._changes.length ? 'hide' : '';
+ _computeNavLink(query, offset, direction, changesPerPage) {
+ // Offset could be a string when passed from the router.
+ offset = +(offset || 0);
+ const limit = this._limitFor(query, changesPerPage);
+ const newOffset = Math.max(0, offset + (limit * direction));
+ return Gerrit.Nav.getUrlForSearchQuery(query, newOffset);
+ }
+
+ _computePrevArrowClass(offset) {
+ return offset === 0 ? 'hide' : '';
+ }
+
+ _computeNextArrowClass(changes) {
+ const more = changes.length && changes[changes.length - 1]._more_changes;
+ return more ? '' : 'hide';
+ }
+
+ _computeNavClass(loading) {
+ return loading || !this._changes || !this._changes.length ? 'hide' : '';
+ }
+
+ _handleNextPage() {
+ if (this.$.nextArrow.hidden) { return; }
+ page.show(this._computeNavLink(
+ this._query, this._offset, 1, this._changesPerPage));
+ }
+
+ _handlePreviousPage() {
+ if (this.$.prevArrow.hidden) { return; }
+ page.show(this._computeNavLink(
+ this._query, this._offset, -1, this._changesPerPage));
+ }
+
+ _changesChanged(changes) {
+ this._userId = null;
+ this._repo = null;
+ if (!changes || !changes.length) {
+ return;
}
-
- _handleNextPage() {
- if (this.$.nextArrow.hidden) { return; }
- page.show(this._computeNavLink(
- this._query, this._offset, 1, this._changesPerPage));
- }
-
- _handlePreviousPage() {
- if (this.$.prevArrow.hidden) { return; }
- page.show(this._computeNavLink(
- this._query, this._offset, -1, this._changesPerPage));
- }
-
- _changesChanged(changes) {
- this._userId = null;
- this._repo = null;
- if (!changes || !changes.length) {
+ if (USER_QUERY_PATTERN.test(this._query)) {
+ const owner = changes[0].owner;
+ const userId = owner._account_id ? owner._account_id : owner.email;
+ if (userId) {
+ this._userId = userId;
return;
}
- if (USER_QUERY_PATTERN.test(this._query)) {
- const owner = changes[0].owner;
- const userId = owner._account_id ? owner._account_id : owner.email;
- if (userId) {
- this._userId = userId;
- return;
- }
- }
- if (REPO_QUERY_PATTERN.test(this._query)) {
- this._repo = changes[0].project;
- }
}
-
- _computeHeaderClass(id) {
- return id ? '' : 'hide';
- }
-
- _computePage(offset, changesPerPage) {
- return offset / changesPerPage + 1;
- }
-
- _computeLoggedIn(account) {
- return !!(account && Object.keys(account).length > 0);
- }
-
- _handleToggleStar(e) {
- this.$.restAPI.saveChangeStarred(e.detail.change._number,
- e.detail.starred);
- }
-
- _handleToggleReviewed(e) {
- this.$.restAPI.saveChangeReviewed(e.detail.change._number,
- e.detail.reviewed);
+ if (REPO_QUERY_PATTERN.test(this._query)) {
+ this._repo = changes[0].project;
}
}
- customElements.define(GrChangeListView.is, GrChangeListView);
-})();
+ _computeHeaderClass(id) {
+ return id ? '' : 'hide';
+ }
+
+ _computePage(offset, changesPerPage) {
+ return offset / changesPerPage + 1;
+ }
+
+ _computeLoggedIn(account) {
+ return !!(account && Object.keys(account).length > 0);
+ }
+
+ _handleToggleStar(e) {
+ this.$.restAPI.saveChangeStarred(e.detail.change._number,
+ e.detail.starred);
+ }
+
+ _handleToggleReviewed(e) {
+ this.$.restAPI.saveChangeReviewed(e.detail.change._number,
+ e.detail.reviewed);
+ }
+}
+
+customElements.define(GrChangeListView.is, GrChangeListView);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js
new file mode 100644
index 0000000..bed3985
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js
@@ -0,0 +1,77 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ }
+ .loading {
+ color: var(--deemphasized-text-color);
+ padding: var(--spacing-l);
+ }
+ gr-change-list {
+ width: 100%;
+ }
+ gr-user-header,
+ gr-repo-header {
+ border-bottom: 1px solid var(--border-color);
+ }
+ nav {
+ align-items: center;
+ display: flex;
+ height: 3rem;
+ justify-content: flex-end;
+ margin-right: 20px;
+ }
+ nav,
+ iron-icon {
+ color: var(--deemphasized-text-color);
+ }
+ iron-icon {
+ height: 1.85rem;
+ margin-left: 16px;
+ width: 1.85rem;
+ }
+ .hide {
+ display: none;
+ }
+ @media only screen and (max-width: 50em) {
+ .loading,
+ .error {
+ padding: 0 var(--spacing-l);
+ }
+ }
+ </style>
+ <div class="loading" hidden\$="[[!_loading]]" hidden="">Loading...</div>
+ <div hidden\$="[[_loading]]" hidden="">
+ <gr-repo-header repo="[[_repo]]" class\$="[[_computeHeaderClass(_repo)]]"></gr-repo-header>
+ <gr-user-header user-id="[[_userId]]" show-dashboard-link="" logged-in="[[_loggedIn]]" class\$="[[_computeHeaderClass(_userId)]]"></gr-user-header>
+ <gr-change-list account="[[account]]" changes="{{_changes}}" preferences="[[preferences]]" selected-index="{{viewState.selectedChangeIndex}}" show-star="[[_loggedIn]]" on-toggle-star="_handleToggleStar" on-toggle-reviewed="_handleToggleReviewed"></gr-change-list>
+ <nav class\$="[[_computeNavClass(_loading)]]">
+ Page [[_computePage(_offset, _changesPerPage)]]
+ <a id="prevArrow" href\$="[[_computeNavLink(_query, _offset, -1, _changesPerPage)]]" class\$="[[_computePrevArrowClass(_offset)]]">
+ <iron-icon icon="gr-icons:chevron-left"></iron-icon>
+ </a>
+ <a id="nextArrow" href\$="[[_computeNavLink(_query, _offset, 1, _changesPerPage)]]" class\$="[[_computeNextArrowClass(_changes)]]">
+ <iron-icon icon="gr-icons:chevron-right"></iron-icon>
+ </a>
+ </nav>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
index 1d81ab7..44ebff7 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
@@ -19,16 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-list-view</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-change-list-view.html">
-
-<script>void(0);</script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,26 +31,169 @@
</template>
</test-fixture>
-<script>
- const CHANGE_ID = 'IcA3dAB3edAB9f60B8dcdA6ef71A75980e4B7127';
- const COMMIT_HASH = '12345678';
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-change-list-view.js';
+const CHANGE_ID = 'IcA3dAB3edAB9f60B8dcdA6ef71A75980e4B7127';
+const COMMIT_HASH = '12345678';
- suite('gr-change-list-view tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+suite('gr-change-list-view tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(false); },
+ getChanges(num, query) {
+ return Promise.resolve([]);
+ },
+ getAccountDetails() { return Promise.resolve({}); },
+ getAccountStatus() { return Promise.resolve({}); },
+ });
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(done => {
+ flush(() => {
+ sandbox.restore();
+ done();
+ });
+ });
+
+ test('_computePage', () => {
+ assert.equal(element._computePage(0, 25), 1);
+ assert.equal(element._computePage(50, 25), 3);
+ });
+
+ test('_limitFor', () => {
+ const defaultLimit = 25;
+ const _limitFor = q => element._limitFor(q, defaultLimit);
+ assert.equal(_limitFor(''), defaultLimit);
+ assert.equal(_limitFor('limit:10'), 10);
+ assert.equal(_limitFor('xlimit:10'), defaultLimit);
+ assert.equal(_limitFor('x(limit:10'), 10);
+ });
+
+ test('_computeNavLink', () => {
+ const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForSearchQuery')
+ .returns('');
+ const query = 'status:open';
+ let offset = 0;
+ let direction = 1;
+ const changesPerPage = 5;
+
+ element._computeNavLink(query, offset, direction, changesPerPage);
+ assert.equal(getUrlStub.lastCall.args[1], 5);
+
+ direction = -1;
+ element._computeNavLink(query, offset, direction, changesPerPage);
+ assert.equal(getUrlStub.lastCall.args[1], 0);
+
+ offset = 5;
+ direction = 1;
+ element._computeNavLink(query, offset, direction, changesPerPage);
+ assert.equal(getUrlStub.lastCall.args[1], 10);
+ });
+
+ test('_computePrevArrowClass', () => {
+ let offset = 0;
+ assert.equal(element._computePrevArrowClass(offset), 'hide');
+ offset = 5;
+ assert.equal(element._computePrevArrowClass(offset), '');
+ });
+
+ test('_computeNextArrowClass', () => {
+ let changes = _.times(25, _.constant({_more_changes: true}));
+ assert.equal(element._computeNextArrowClass(changes), '');
+ changes = _.times(25, _.constant({}));
+ assert.equal(element._computeNextArrowClass(changes), 'hide');
+ });
+
+ test('_computeNavClass', () => {
+ let loading = true;
+ assert.equal(element._computeNavClass(loading), 'hide');
+ loading = false;
+ assert.equal(element._computeNavClass(loading), 'hide');
+ element._changes = [];
+ assert.equal(element._computeNavClass(loading), 'hide');
+ element._changes = _.times(5, _.constant({}));
+ assert.equal(element._computeNavClass(loading), '');
+ });
+
+ test('_handleNextPage', () => {
+ const showStub = sandbox.stub(page, 'show');
+ element.$.nextArrow.hidden = true;
+ element._handleNextPage();
+ assert.isFalse(showStub.called);
+ element.$.nextArrow.hidden = false;
+ element._handleNextPage();
+ assert.isTrue(showStub.called);
+ });
+
+ test('_handlePreviousPage', () => {
+ const showStub = sandbox.stub(page, 'show');
+ element.$.prevArrow.hidden = true;
+ element._handlePreviousPage();
+ assert.isFalse(showStub.called);
+ element.$.prevArrow.hidden = false;
+ element._handlePreviousPage();
+ assert.isTrue(showStub.called);
+ });
+
+ test('_userId query', done => {
+ assert.isNull(element._userId);
+ element._query = 'owner: foo@bar';
+ element._changes = [{owner: {email: 'foo@bar'}}];
+ flush(() => {
+ assert.equal(element._userId, 'foo@bar');
+
+ element._query = 'foo bar baz';
+ element._changes = [{owner: {email: 'foo@bar'}}];
+ assert.isNull(element._userId);
+
+ done();
+ });
+ });
+
+ test('_userId query without email', done => {
+ assert.isNull(element._userId);
+ element._query = 'owner: foo@bar';
+ element._changes = [{owner: {}}];
+ flush(() => {
+ assert.isNull(element._userId);
+ done();
+ });
+ });
+
+ test('_repo query', done => {
+ assert.isNull(element._repo);
+ element._query = 'project: test-repo';
+ element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}];
+ flush(() => {
+ assert.equal(element._repo, 'test-repo');
+ element._query = 'foo bar baz';
+ element._changes = [{owner: {email: 'foo@bar'}}];
+ assert.isNull(element._repo);
+ done();
+ });
+ });
+
+ test('_repo query with open status', done => {
+ assert.isNull(element._repo);
+ element._query = 'project:test-repo status:open';
+ element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}];
+ flush(() => {
+ assert.equal(element._repo, 'test-repo');
+ element._query = 'foo bar baz';
+ element._changes = [{owner: {email: 'foo@bar'}}];
+ assert.isNull(element._repo);
+ done();
+ });
+ });
+
+ suite('query based navigation', () => {
setup(() => {
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(false); },
- getChanges(num, query) {
- return Promise.resolve([]);
- },
- getAccountDetails() { return Promise.resolve({}); },
- getAccountStatus() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
});
teardown(done => {
@@ -65,205 +203,63 @@
});
});
- test('_computePage', () => {
- assert.equal(element._computePage(0, 25), 1);
- assert.equal(element._computePage(50, 25), 3);
- });
-
- test('_limitFor', () => {
- const defaultLimit = 25;
- const _limitFor = q => element._limitFor(q, defaultLimit);
- assert.equal(_limitFor(''), defaultLimit);
- assert.equal(_limitFor('limit:10'), 10);
- assert.equal(_limitFor('xlimit:10'), defaultLimit);
- assert.equal(_limitFor('x(limit:10'), 10);
- });
-
- test('_computeNavLink', () => {
- const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForSearchQuery')
- .returns('');
- const query = 'status:open';
- let offset = 0;
- let direction = 1;
- const changesPerPage = 5;
-
- element._computeNavLink(query, offset, direction, changesPerPage);
- assert.equal(getUrlStub.lastCall.args[1], 5);
-
- direction = -1;
- element._computeNavLink(query, offset, direction, changesPerPage);
- assert.equal(getUrlStub.lastCall.args[1], 0);
-
- offset = 5;
- direction = 1;
- element._computeNavLink(query, offset, direction, changesPerPage);
- assert.equal(getUrlStub.lastCall.args[1], 10);
- });
-
- test('_computePrevArrowClass', () => {
- let offset = 0;
- assert.equal(element._computePrevArrowClass(offset), 'hide');
- offset = 5;
- assert.equal(element._computePrevArrowClass(offset), '');
- });
-
- test('_computeNextArrowClass', () => {
- let changes = _.times(25, _.constant({_more_changes: true}));
- assert.equal(element._computeNextArrowClass(changes), '');
- changes = _.times(25, _.constant({}));
- assert.equal(element._computeNextArrowClass(changes), 'hide');
- });
-
- test('_computeNavClass', () => {
- let loading = true;
- assert.equal(element._computeNavClass(loading), 'hide');
- loading = false;
- assert.equal(element._computeNavClass(loading), 'hide');
- element._changes = [];
- assert.equal(element._computeNavClass(loading), 'hide');
- element._changes = _.times(5, _.constant({}));
- assert.equal(element._computeNavClass(loading), '');
- });
-
- test('_handleNextPage', () => {
- const showStub = sandbox.stub(page, 'show');
- element.$.nextArrow.hidden = true;
- element._handleNextPage();
- assert.isFalse(showStub.called);
- element.$.nextArrow.hidden = false;
- element._handleNextPage();
- assert.isTrue(showStub.called);
- });
-
- test('_handlePreviousPage', () => {
- const showStub = sandbox.stub(page, 'show');
- element.$.prevArrow.hidden = true;
- element._handlePreviousPage();
- assert.isFalse(showStub.called);
- element.$.prevArrow.hidden = false;
- element._handlePreviousPage();
- assert.isTrue(showStub.called);
- });
-
- test('_userId query', done => {
- assert.isNull(element._userId);
- element._query = 'owner: foo@bar';
- element._changes = [{owner: {email: 'foo@bar'}}];
- flush(() => {
- assert.equal(element._userId, 'foo@bar');
-
- element._query = 'foo bar baz';
- element._changes = [{owner: {email: 'foo@bar'}}];
- assert.isNull(element._userId);
-
+ test('Searching for a change ID redirects to change', done => {
+ const change = {_number: 1};
+ sandbox.stub(element, '_getChanges')
+ .returns(Promise.resolve([change]));
+ sandbox.stub(Gerrit.Nav, 'navigateToChange', url => {
+ assert.equal(url, change);
done();
});
+
+ element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID};
});
- test('_userId query without email', done => {
- assert.isNull(element._userId);
- element._query = 'owner: foo@bar';
- element._changes = [{owner: {}}];
- flush(() => {
- assert.isNull(element._userId);
+ test('Searching for a change num redirects to change', done => {
+ const change = {_number: 1};
+ sandbox.stub(element, '_getChanges')
+ .returns(Promise.resolve([change]));
+ sandbox.stub(Gerrit.Nav, 'navigateToChange', url => {
+ assert.equal(url, change);
done();
});
+
+ element.params = {view: Gerrit.Nav.View.SEARCH, query: '1'};
});
- test('_repo query', done => {
- assert.isNull(element._repo);
- element._query = 'project: test-repo';
- element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}];
- flush(() => {
- assert.equal(element._repo, 'test-repo');
- element._query = 'foo bar baz';
- element._changes = [{owner: {email: 'foo@bar'}}];
- assert.isNull(element._repo);
+ test('Commit hash redirects to change', done => {
+ const change = {_number: 1};
+ sandbox.stub(element, '_getChanges')
+ .returns(Promise.resolve([change]));
+ sandbox.stub(Gerrit.Nav, 'navigateToChange', url => {
+ assert.equal(url, change);
done();
});
+
+ element.params = {view: Gerrit.Nav.View.SEARCH, query: COMMIT_HASH};
});
- test('_repo query with open status', done => {
- assert.isNull(element._repo);
- element._query = 'project:test-repo status:open';
- element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}];
- flush(() => {
- assert.equal(element._repo, 'test-repo');
- element._query = 'foo bar baz';
- element._changes = [{owner: {email: 'foo@bar'}}];
- assert.isNull(element._repo);
- done();
- });
+ test('Searching for an invalid change ID searches', () => {
+ sandbox.stub(element, '_getChanges')
+ .returns(Promise.resolve([]));
+ const stub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+
+ element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID};
+ flushAsynchronousOperations();
+
+ assert.isFalse(stub.called);
});
- suite('query based navigation', () => {
- setup(() => {
- });
+ test('Change ID with multiple search results searches', () => {
+ sandbox.stub(element, '_getChanges')
+ .returns(Promise.resolve([{}, {}]));
+ const stub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
- teardown(done => {
- flush(() => {
- sandbox.restore();
- done();
- });
- });
+ element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID};
+ flushAsynchronousOperations();
- test('Searching for a change ID redirects to change', done => {
- const change = {_number: 1};
- sandbox.stub(element, '_getChanges')
- .returns(Promise.resolve([change]));
- sandbox.stub(Gerrit.Nav, 'navigateToChange', url => {
- assert.equal(url, change);
- done();
- });
-
- element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID};
- });
-
- test('Searching for a change num redirects to change', done => {
- const change = {_number: 1};
- sandbox.stub(element, '_getChanges')
- .returns(Promise.resolve([change]));
- sandbox.stub(Gerrit.Nav, 'navigateToChange', url => {
- assert.equal(url, change);
- done();
- });
-
- element.params = {view: Gerrit.Nav.View.SEARCH, query: '1'};
- });
-
- test('Commit hash redirects to change', done => {
- const change = {_number: 1};
- sandbox.stub(element, '_getChanges')
- .returns(Promise.resolve([change]));
- sandbox.stub(Gerrit.Nav, 'navigateToChange', url => {
- assert.equal(url, change);
- done();
- });
-
- element.params = {view: Gerrit.Nav.View.SEARCH, query: COMMIT_HASH};
- });
-
- test('Searching for an invalid change ID searches', () => {
- sandbox.stub(element, '_getChanges')
- .returns(Promise.resolve([]));
- const stub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
-
- element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID};
- flushAsynchronousOperations();
-
- assert.isFalse(stub.called);
- });
-
- test('Change ID with multiple search results searches', () => {
- sandbox.stub(element, '_getChanges')
- .returns(Promise.resolve([{}, {}]));
- const stub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
-
- element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID};
- flushAsynchronousOperations();
-
- assert.isFalse(stub.called);
- });
+ assert.isFalse(stub.called);
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
deleted file mode 100644
index 61b9960..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
+++ /dev/null
@@ -1,140 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/gr-change-list-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
-<link rel="import" href="../gr-change-list-item/gr-change-list-item.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-
-<dom-module id="gr-change-list">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-change-list-styles">
- #changeList {
- border-collapse: collapse;
- width: 100%;
- }
- .section-count-label {
- color: var(--deemphasized-text-color);
- font-family: var(--font-family);
- font-size: var(--font-size-small);
- font-weight: var(--font-weight-normal);
- line-height: var(--line-height-small);
- }
- a.section-title:hover {
- text-decoration: none;
- }
- a.section-title:hover .section-count-label {
- text-decoration: none;
- }
- a.section-title:hover .section-name {
- text-decoration: underline;
- }
- </style>
- <table id="changeList">
- <template is="dom-repeat" items="[[sections]]" as="changeSection"
- index-as="sectionIndex">
- <template is="dom-if" if="[[changeSection.name]]">
- <tbody>
- <tr class="groupHeader">
- <td class="leftPadding"></td>
- <td class="star" hidden$="[[!showStar]]" hidden></td>
- <td class="cell"
- colspan$="[[_computeColspan(changeTableColumns, labelNames)]]">
- <a href$="[[_sectionHref(changeSection.query)]]" class="section-title">
- <span class="section-name">[[changeSection.name]]</span>
- <span class="section-count-label">[[changeSection.countLabel]]</span>
- </a>
- </td>
- </tr>
- </tbody>
- </template>
- <tbody class="groupContent">
- <template is="dom-if" if="[[_isEmpty(changeSection)]]">
- <tr class="noChanges">
- <td class="leftPadding"></td>
- <td class="star" hidden$="[[!showStar]]" hidden></td>
- <td class="cell"
- colspan$="[[_computeColspan(changeTableColumns, labelNames)]]">
- <template is="dom-if" if="[[_isOutgoing(changeSection)]]">
- <slot name="empty-outgoing"></slot>
- </template>
- <template is="dom-if" if="[[!_isOutgoing(changeSection)]]">
- No changes
- </template>
- </td>
- </tr>
- </template>
- <template is="dom-if" if="[[!_isEmpty(changeSection)]]">
- <tr class="groupTitle">
- <td class="leftPadding"></td>
- <td class="star" hidden$="[[!showStar]]" hidden></td>
- <td class="number" hidden$="[[!showNumber]]" hidden>#</td>
- <template is="dom-repeat" items="[[changeTableColumns]]" as="item">
- <td class$="[[_lowerCase(item)]]"
- hidden$="[[isColumnHidden(item, visibleChangeTableColumns)]]">
- [[item]]
- </td>
- </template>
- <template is="dom-repeat" items="[[labelNames]]" as="labelName">
- <td class="label" title$="[[labelName]]">
- [[_computeLabelShortcut(labelName)]]
- </td>
- </template>
- <template is="dom-repeat" items="[[_dynamicHeaderEndpoints]]"
- as="pluginHeader">
- <td class="endpoint">
- <gr-endpoint-decorator name$="[[pluginHeader]]">
- </gr-endpoint-decorator>
- </td>
- </template>
- </tr>
- </template>
- <template is="dom-repeat" items="[[changeSection.results]]" as="change">
- <gr-change-list-item
- selected$="[[_computeItemSelected(sectionIndex, index, selectedIndex)]]"
- highlight$="[[_computeItemHighlight(account, change)]]"
- needs-review$="[[_computeItemNeedsReview(account, change, showReviewedState)]]"
- change="[[change]]"
- visible-change-table-columns="[[visibleChangeTableColumns]]"
- show-number="[[showNumber]]"
- show-star="[[showStar]]"
- tabindex="0"
- label-names="[[labelNames]]"></gr-change-list-item>
- </template>
- </tbody>
- </template>
- </table>
- <gr-cursor-manager
- id="cursor"
- index="{{selectedIndex}}"
- scroll-behavior="keep-visible"
- focus-on-move></gr-cursor-manager>
- </template>
- <script src="gr-change-list.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index 30df8fd..33c2ea2 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,403 +14,423 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- const NUMBER_FIXED_COLUMNS = 3;
- const CLOSED_STATUS = ['MERGED', 'ABANDONED'];
- const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
- const MAX_SHORTCUT_CHARS = 5;
+import '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/gr-change-list-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-cursor-manager/gr-cursor-manager.js';
+import '../gr-change-list-item/gr-change-list-item.js';
+import '../../../styles/shared-styles.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-change-list_html.js';
+
+const NUMBER_FIXED_COLUMNS = 3;
+const CLOSED_STATUS = ['MERGED', 'ABANDONED'];
+const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
+const MAX_SHORTCUT_CHARS = 5;
+
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.ChangeTableMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrChangeList extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.ChangeTableBehavior,
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+ Gerrit.RESTClientBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-change-list'; }
+ /**
+ * Fired when next page key shortcut was pressed.
+ *
+ * @event next-page
+ */
/**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.ChangeTableMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
+ * Fired when previous page key shortcut was pressed.
+ *
+ * @event previous-page
*/
- class GrChangeList extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.ChangeTableBehavior,
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- Gerrit.RESTClientBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-change-list'; }
- /**
- * Fired when next page key shortcut was pressed.
- *
- * @event next-page
- */
+ static get properties() {
+ return {
/**
- * Fired when previous page key shortcut was pressed.
- *
- * @event previous-page
+ * The logged-in user's account, or an empty object if no user is logged
+ * in.
*/
-
- static get properties() {
- return {
+ account: {
+ type: Object,
+ value: null,
+ },
/**
- * The logged-in user's account, or an empty object if no user is logged
- * in.
+ * An array of ChangeInfo objects to render.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
*/
- account: {
- type: Object,
- value: null,
- },
- /**
- * An array of ChangeInfo objects to render.
- * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
- */
- changes: {
- type: Array,
- observer: '_changesChanged',
- },
- /**
- * ChangeInfo objects grouped into arrays. The sections and changes
- * properties should not be used together.
- *
- * @type {!Array<{
- * name: string,
- * query: string,
- * results: !Array<!Object>
- * }>}
- */
- sections: {
- type: Array,
- value() { return []; },
- },
- labelNames: {
- type: Array,
- computed: '_computeLabelNames(sections)',
- },
- _dynamicHeaderEndpoints: {
- type: Array,
- },
- selectedIndex: {
- type: Number,
- notify: true,
- },
- showNumber: Boolean, // No default value to prevent flickering.
- showStar: {
- type: Boolean,
- value: false,
- },
- showReviewedState: {
- type: Boolean,
- value: false,
- },
- keyEventTarget: {
- type: Object,
- value() { return document.body; },
- },
- changeTableColumns: Array,
- visibleChangeTableColumns: Array,
- preferences: Object,
- };
- }
+ changes: {
+ type: Array,
+ observer: '_changesChanged',
+ },
+ /**
+ * ChangeInfo objects grouped into arrays. The sections and changes
+ * properties should not be used together.
+ *
+ * @type {!Array<{
+ * name: string,
+ * query: string,
+ * results: !Array<!Object>
+ * }>}
+ */
+ sections: {
+ type: Array,
+ value() { return []; },
+ },
+ labelNames: {
+ type: Array,
+ computed: '_computeLabelNames(sections)',
+ },
+ _dynamicHeaderEndpoints: {
+ type: Array,
+ },
+ selectedIndex: {
+ type: Number,
+ notify: true,
+ },
+ showNumber: Boolean, // No default value to prevent flickering.
+ showStar: {
+ type: Boolean,
+ value: false,
+ },
+ showReviewedState: {
+ type: Boolean,
+ value: false,
+ },
+ keyEventTarget: {
+ type: Object,
+ value() { return document.body; },
+ },
+ changeTableColumns: Array,
+ visibleChangeTableColumns: Array,
+ preferences: Object,
+ };
+ }
- static get observers() {
- return [
- '_sectionsChanged(sections.*)',
- '_computePreferences(account, preferences)',
- ];
- }
+ static get observers() {
+ return [
+ '_sectionsChanged(sections.*)',
+ '_computePreferences(account, preferences)',
+ ];
+ }
- keyboardShortcuts() {
- return {
- [this.Shortcut.CURSOR_NEXT_CHANGE]: '_nextChange',
- [this.Shortcut.CURSOR_PREV_CHANGE]: '_prevChange',
- [this.Shortcut.NEXT_PAGE]: '_nextPage',
- [this.Shortcut.PREV_PAGE]: '_prevPage',
- [this.Shortcut.OPEN_CHANGE]: '_openChange',
- [this.Shortcut.TOGGLE_CHANGE_REVIEWED]: '_toggleChangeReviewed',
- [this.Shortcut.TOGGLE_CHANGE_STAR]: '_toggleChangeStar',
- [this.Shortcut.REFRESH_CHANGE_LIST]: '_refreshChangeList',
- };
- }
+ keyboardShortcuts() {
+ return {
+ [this.Shortcut.CURSOR_NEXT_CHANGE]: '_nextChange',
+ [this.Shortcut.CURSOR_PREV_CHANGE]: '_prevChange',
+ [this.Shortcut.NEXT_PAGE]: '_nextPage',
+ [this.Shortcut.PREV_PAGE]: '_prevPage',
+ [this.Shortcut.OPEN_CHANGE]: '_openChange',
+ [this.Shortcut.TOGGLE_CHANGE_REVIEWED]: '_toggleChangeReviewed',
+ [this.Shortcut.TOGGLE_CHANGE_STAR]: '_toggleChangeStar',
+ [this.Shortcut.REFRESH_CHANGE_LIST]: '_refreshChangeList',
+ };
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('keydown',
- e => this._scopedKeydownHandler(e));
- }
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('keydown',
+ e => this._scopedKeydownHandler(e));
+ }
- /** @override */
- ready() {
- super.ready();
- this._ensureAttribute('tabindex', 0);
- }
+ /** @override */
+ ready() {
+ super.ready();
+ this._ensureAttribute('tabindex', 0);
+ }
- /** @override */
- attached() {
- super.attached();
- Gerrit.awaitPluginsLoaded().then(() => {
- this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints(
- 'change-list-header');
- });
- }
+ /** @override */
+ attached() {
+ super.attached();
+ Gerrit.awaitPluginsLoaded().then(() => {
+ this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints(
+ 'change-list-header');
+ });
+ }
- /**
- * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
- * events must be scoped to a component level (e.g. `enter`) in order to not
- * override native browser functionality.
- *
- * Context: Issue 7294
- */
- _scopedKeydownHandler(e) {
- if (e.keyCode === 13) {
- // Enter.
- this._openChange(e);
- }
- }
-
- _lowerCase(column) {
- return column.toLowerCase();
- }
-
- _computePreferences(account, preferences) {
- // Polymer 2: check for undefined
- if ([account, preferences].some(arg => arg === undefined)) {
- return;
- }
-
- this.changeTableColumns = this.columnNames;
-
- if (account) {
- this.showNumber = !!(preferences &&
- preferences.legacycid_in_change_table);
- if (preferences.change_table &&
- preferences.change_table.length > 0) {
- this.visibleChangeTableColumns =
- this.getVisibleColumns(preferences.change_table);
- } else {
- this.visibleChangeTableColumns = this.columnNames;
- }
- } else {
- // Not logged in.
- this.showNumber = false;
- this.visibleChangeTableColumns = this.columnNames;
- }
- }
-
- _computeColspan(changeTableColumns, labelNames) {
- if (!changeTableColumns || !labelNames) return;
- return changeTableColumns.length + labelNames.length +
- NUMBER_FIXED_COLUMNS;
- }
-
- _computeLabelNames(sections) {
- if (!sections) { return []; }
- let labels = [];
- const nonExistingLabel = function(item) {
- return !labels.includes(item);
- };
- for (const section of sections) {
- if (!section.results) { continue; }
- for (const change of section.results) {
- if (!change.labels) { continue; }
- const currentLabels = Object.keys(change.labels);
- labels = labels.concat(currentLabels.filter(nonExistingLabel));
- }
- }
- return labels.sort();
- }
-
- _computeLabelShortcut(labelName) {
- if (labelName.startsWith(LABEL_PREFIX_INVALID_PROLOG)) {
- labelName = labelName.slice(LABEL_PREFIX_INVALID_PROLOG.length);
- }
- return labelName.split('-')
- .reduce((a, i) => {
- if (!i) { return a; }
- return a + i[0].toUpperCase();
- }, '')
- .slice(0, MAX_SHORTCUT_CHARS);
- }
-
- _changesChanged(changes) {
- this.sections = changes ? [{results: changes}] : [];
- }
-
- _processQuery(query) {
- let tokens = query.split(' ');
- const invalidTokens = ['limit:', 'age:', '-age:'];
- tokens = tokens.filter(token => !invalidTokens
- .some(invalidToken => token.startsWith(invalidToken)));
- return tokens.join(' ');
- }
-
- _sectionHref(query) {
- return Gerrit.Nav.getUrlForSearchQuery(this._processQuery(query));
- }
-
- /**
- * Maps an index local to a particular section to the absolute index
- * across all the changes on the page.
- *
- * @param {number} sectionIndex index of section
- * @param {number} localIndex index of row within section
- * @return {number} absolute index of row in the aggregate dashboard
- */
- _computeItemAbsoluteIndex(sectionIndex, localIndex) {
- let idx = 0;
- for (let i = 0; i < sectionIndex; i++) {
- idx += this.sections[i].results.length;
- }
- return idx + localIndex;
- }
-
- _computeItemSelected(sectionIndex, index, selectedIndex) {
- const idx = this._computeItemAbsoluteIndex(sectionIndex, index);
- return idx == selectedIndex;
- }
-
- _computeItemNeedsReview(account, change, showReviewedState) {
- return showReviewedState && !change.reviewed &&
- !change.work_in_progress &&
- this.changeIsOpen(change) &&
- (!account || account._account_id != change.owner._account_id);
- }
-
- _computeItemHighlight(account, change) {
- // Do not show the assignee highlight if the change is not open.
- if (!change ||!change.assignee ||
- !account ||
- CLOSED_STATUS.indexOf(change.status) !== -1) {
- return false;
- }
- return account._account_id === change.assignee._account_id;
- }
-
- _nextChange(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.cursor.next();
- }
-
- _prevChange(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.cursor.previous();
- }
-
- _openChange(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- Gerrit.Nav.navigateToChange(this._changeForIndex(this.selectedIndex));
- }
-
- _nextPage(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) {
- return;
- }
-
- e.preventDefault();
- this.fire('next-page');
- }
-
- _prevPage(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) {
- return;
- }
-
- e.preventDefault();
- this.fire('previous-page');
- }
-
- _toggleChangeReviewed(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this._toggleReviewedForIndex(this.selectedIndex);
- }
-
- _toggleReviewedForIndex(index) {
- const changeEls = this._getListItems();
- if (index >= changeEls.length || !changeEls[index]) {
- return;
- }
-
- const changeEl = changeEls[index];
- changeEl.toggleReviewed();
- }
-
- _refreshChangeList(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- this._reloadWindow();
- }
-
- _reloadWindow() {
- window.location.reload();
- }
-
- _toggleChangeStar(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this._toggleStarForIndex(this.selectedIndex);
- }
-
- _toggleStarForIndex(index) {
- const changeEls = this._getListItems();
- if (index >= changeEls.length || !changeEls[index]) {
- return;
- }
-
- const changeEl = changeEls[index];
- changeEl.shadowRoot
- .querySelector('gr-change-star').toggleStar();
- }
-
- _changeForIndex(index) {
- const changeEls = this._getListItems();
- if (index < changeEls.length && changeEls[index]) {
- return changeEls[index].change;
- }
- return null;
- }
-
- _getListItems() {
- return Array.from(
- Polymer.dom(this.root).querySelectorAll('gr-change-list-item'));
- }
-
- _sectionsChanged() {
- // Flush DOM operations so that the list item elements will be loaded.
- Polymer.RenderStatus.afterNextRender(this, () => {
- this.$.cursor.stops = this._getListItems();
- this.$.cursor.moveToStart();
- });
- }
-
- _isOutgoing(section) {
- return !!section.isOutgoing;
- }
-
- _isEmpty(section) {
- return !section.results.length;
+ /**
+ * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
+ * events must be scoped to a component level (e.g. `enter`) in order to not
+ * override native browser functionality.
+ *
+ * Context: Issue 7294
+ */
+ _scopedKeydownHandler(e) {
+ if (e.keyCode === 13) {
+ // Enter.
+ this._openChange(e);
}
}
- customElements.define(GrChangeList.is, GrChangeList);
-})();
+ _lowerCase(column) {
+ return column.toLowerCase();
+ }
+
+ _computePreferences(account, preferences) {
+ // Polymer 2: check for undefined
+ if ([account, preferences].some(arg => arg === undefined)) {
+ return;
+ }
+
+ this.changeTableColumns = this.columnNames;
+
+ if (account) {
+ this.showNumber = !!(preferences &&
+ preferences.legacycid_in_change_table);
+ if (preferences.change_table &&
+ preferences.change_table.length > 0) {
+ this.visibleChangeTableColumns =
+ this.getVisibleColumns(preferences.change_table);
+ } else {
+ this.visibleChangeTableColumns = this.columnNames;
+ }
+ } else {
+ // Not logged in.
+ this.showNumber = false;
+ this.visibleChangeTableColumns = this.columnNames;
+ }
+ }
+
+ _computeColspan(changeTableColumns, labelNames) {
+ if (!changeTableColumns || !labelNames) return;
+ return changeTableColumns.length + labelNames.length +
+ NUMBER_FIXED_COLUMNS;
+ }
+
+ _computeLabelNames(sections) {
+ if (!sections) { return []; }
+ let labels = [];
+ const nonExistingLabel = function(item) {
+ return !labels.includes(item);
+ };
+ for (const section of sections) {
+ if (!section.results) { continue; }
+ for (const change of section.results) {
+ if (!change.labels) { continue; }
+ const currentLabels = Object.keys(change.labels);
+ labels = labels.concat(currentLabels.filter(nonExistingLabel));
+ }
+ }
+ return labels.sort();
+ }
+
+ _computeLabelShortcut(labelName) {
+ if (labelName.startsWith(LABEL_PREFIX_INVALID_PROLOG)) {
+ labelName = labelName.slice(LABEL_PREFIX_INVALID_PROLOG.length);
+ }
+ return labelName.split('-')
+ .reduce((a, i) => {
+ if (!i) { return a; }
+ return a + i[0].toUpperCase();
+ }, '')
+ .slice(0, MAX_SHORTCUT_CHARS);
+ }
+
+ _changesChanged(changes) {
+ this.sections = changes ? [{results: changes}] : [];
+ }
+
+ _processQuery(query) {
+ let tokens = query.split(' ');
+ const invalidTokens = ['limit:', 'age:', '-age:'];
+ tokens = tokens.filter(token => !invalidTokens
+ .some(invalidToken => token.startsWith(invalidToken)));
+ return tokens.join(' ');
+ }
+
+ _sectionHref(query) {
+ return Gerrit.Nav.getUrlForSearchQuery(this._processQuery(query));
+ }
+
+ /**
+ * Maps an index local to a particular section to the absolute index
+ * across all the changes on the page.
+ *
+ * @param {number} sectionIndex index of section
+ * @param {number} localIndex index of row within section
+ * @return {number} absolute index of row in the aggregate dashboard
+ */
+ _computeItemAbsoluteIndex(sectionIndex, localIndex) {
+ let idx = 0;
+ for (let i = 0; i < sectionIndex; i++) {
+ idx += this.sections[i].results.length;
+ }
+ return idx + localIndex;
+ }
+
+ _computeItemSelected(sectionIndex, index, selectedIndex) {
+ const idx = this._computeItemAbsoluteIndex(sectionIndex, index);
+ return idx == selectedIndex;
+ }
+
+ _computeItemNeedsReview(account, change, showReviewedState) {
+ return showReviewedState && !change.reviewed &&
+ !change.work_in_progress &&
+ this.changeIsOpen(change) &&
+ (!account || account._account_id != change.owner._account_id);
+ }
+
+ _computeItemHighlight(account, change) {
+ // Do not show the assignee highlight if the change is not open.
+ if (!change ||!change.assignee ||
+ !account ||
+ CLOSED_STATUS.indexOf(change.status) !== -1) {
+ return false;
+ }
+ return account._account_id === change.assignee._account_id;
+ }
+
+ _nextChange(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this.$.cursor.next();
+ }
+
+ _prevChange(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this.$.cursor.previous();
+ }
+
+ _openChange(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ Gerrit.Nav.navigateToChange(this._changeForIndex(this.selectedIndex));
+ }
+
+ _nextPage(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) {
+ return;
+ }
+
+ e.preventDefault();
+ this.fire('next-page');
+ }
+
+ _prevPage(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) {
+ return;
+ }
+
+ e.preventDefault();
+ this.fire('previous-page');
+ }
+
+ _toggleChangeReviewed(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this._toggleReviewedForIndex(this.selectedIndex);
+ }
+
+ _toggleReviewedForIndex(index) {
+ const changeEls = this._getListItems();
+ if (index >= changeEls.length || !changeEls[index]) {
+ return;
+ }
+
+ const changeEl = changeEls[index];
+ changeEl.toggleReviewed();
+ }
+
+ _refreshChangeList(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ this._reloadWindow();
+ }
+
+ _reloadWindow() {
+ window.location.reload();
+ }
+
+ _toggleChangeStar(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this._toggleStarForIndex(this.selectedIndex);
+ }
+
+ _toggleStarForIndex(index) {
+ const changeEls = this._getListItems();
+ if (index >= changeEls.length || !changeEls[index]) {
+ return;
+ }
+
+ const changeEl = changeEls[index];
+ changeEl.shadowRoot
+ .querySelector('gr-change-star').toggleStar();
+ }
+
+ _changeForIndex(index) {
+ const changeEls = this._getListItems();
+ if (index < changeEls.length && changeEls[index]) {
+ return changeEls[index].change;
+ }
+ return null;
+ }
+
+ _getListItems() {
+ return Array.from(
+ dom(this.root).querySelectorAll('gr-change-list-item'));
+ }
+
+ _sectionsChanged() {
+ // Flush DOM operations so that the list item elements will be loaded.
+ afterNextRender(this, () => {
+ this.$.cursor.stops = this._getListItems();
+ this.$.cursor.moveToStart();
+ });
+ }
+
+ _isOutgoing(section) {
+ return !!section.isOutgoing;
+ }
+
+ _isEmpty(section) {
+ return !section.results.length;
+ }
+}
+
+customElements.define(GrChangeList.is, GrChangeList);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js
new file mode 100644
index 0000000..ac37827
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js
@@ -0,0 +1,106 @@
+/**
+ * @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.js';
+
+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-change-list-styles">
+ #changeList {
+ border-collapse: collapse;
+ width: 100%;
+ }
+ .section-count-label {
+ color: var(--deemphasized-text-color);
+ font-family: var(--font-family);
+ font-size: var(--font-size-small);
+ font-weight: var(--font-weight-normal);
+ line-height: var(--line-height-small);
+ }
+ a.section-title:hover {
+ text-decoration: none;
+ }
+ a.section-title:hover .section-count-label {
+ text-decoration: none;
+ }
+ a.section-title:hover .section-name {
+ text-decoration: underline;
+ }
+ </style>
+ <table id="changeList">
+ <template is="dom-repeat" items="[[sections]]" as="changeSection" index-as="sectionIndex">
+ <template is="dom-if" if="[[changeSection.name]]">
+ <tbody>
+ <tr class="groupHeader">
+ <td class="leftPadding"></td>
+ <td class="star" hidden\$="[[!showStar]]" hidden=""></td>
+ <td class="cell" colspan\$="[[_computeColspan(changeTableColumns, labelNames)]]">
+ <a href\$="[[_sectionHref(changeSection.query)]]" class="section-title">
+ <span class="section-name">[[changeSection.name]]</span>
+ <span class="section-count-label">[[changeSection.countLabel]]</span>
+ </a>
+ </td>
+ </tr>
+ </tbody>
+ </template>
+ <tbody class="groupContent">
+ <template is="dom-if" if="[[_isEmpty(changeSection)]]">
+ <tr class="noChanges">
+ <td class="leftPadding"></td>
+ <td class="star" hidden\$="[[!showStar]]" hidden=""></td>
+ <td class="cell" colspan\$="[[_computeColspan(changeTableColumns, labelNames)]]">
+ <template is="dom-if" if="[[_isOutgoing(changeSection)]]">
+ <slot name="empty-outgoing"></slot>
+ </template>
+ <template is="dom-if" if="[[!_isOutgoing(changeSection)]]">
+ No changes
+ </template>
+ </td>
+ </tr>
+ </template>
+ <template is="dom-if" if="[[!_isEmpty(changeSection)]]">
+ <tr class="groupTitle">
+ <td class="leftPadding"></td>
+ <td class="star" hidden\$="[[!showStar]]" hidden=""></td>
+ <td class="number" hidden\$="[[!showNumber]]" hidden="">#</td>
+ <template is="dom-repeat" items="[[changeTableColumns]]" as="item">
+ <td class\$="[[_lowerCase(item)]]" hidden\$="[[isColumnHidden(item, visibleChangeTableColumns)]]">
+ [[item]]
+ </td>
+ </template>
+ <template is="dom-repeat" items="[[labelNames]]" as="labelName">
+ <td class="label" title\$="[[labelName]]">
+ [[_computeLabelShortcut(labelName)]]
+ </td>
+ </template>
+ <template is="dom-repeat" items="[[_dynamicHeaderEndpoints]]" as="pluginHeader">
+ <td class="endpoint">
+ <gr-endpoint-decorator name\$="[[pluginHeader]]">
+ </gr-endpoint-decorator>
+ </td>
+ </template>
+ </tr>
+ </template>
+ <template is="dom-repeat" items="[[changeSection.results]]" as="change">
+ <gr-change-list-item selected\$="[[_computeItemSelected(sectionIndex, index, selectedIndex)]]" highlight\$="[[_computeItemHighlight(account, change)]]" needs-review\$="[[_computeItemNeedsReview(account, change, showReviewedState)]]" change="[[change]]" visible-change-table-columns="[[visibleChangeTableColumns]]" show-number="[[showNumber]]" show-star="[[showStar]]" tabindex="0" label-names="[[labelNames]]"></gr-change-list-item>
+ </template>
+ </tbody>
+ </template>
+ </table>
+ <gr-cursor-manager id="cursor" index="{{selectedIndex}}" scroll-behavior="keep-visible" focus-on-move=""></gr-cursor-manager>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index 23a5046..d077f6b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -19,17 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-list</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="/bower_components/page/page.js"></script>
-
-<link rel="import" href="gr-change-list.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script src="/node_modules/page/page.js"></script>
<test-fixture id="basic">
<template>
@@ -43,20 +37,419 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-list basic tests', async () => {
- await readyToTest();
- // Define keybindings before attaching other fixtures.
- const kb = window.Gerrit.KeyboardShortcutBinder;
- kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_CHANGE, 'j');
- kb.bindShortcut(kb.Shortcut.CURSOR_PREV_CHANGE, 'k');
- kb.bindShortcut(kb.Shortcut.OPEN_CHANGE, 'o');
- kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE_LIST, 'shift+r');
- kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r');
- kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
- kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'n');
- kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'p');
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-change-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.js';
+suite('gr-change-list basic tests', () => {
+ // Define keybindings before attaching other fixtures.
+ const kb = window.Gerrit.KeyboardShortcutBinder;
+ kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_CHANGE, 'j');
+ kb.bindShortcut(kb.Shortcut.CURSOR_PREV_CHANGE, 'k');
+ kb.bindShortcut(kb.Shortcut.OPEN_CHANGE, 'o');
+ kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE_LIST, 'shift+r');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
+ kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'n');
+ kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'p');
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => { sandbox.restore(); });
+
+ suite('test show change number not logged in', () => {
+ setup(() => {
+ element = fixture('basic');
+ element.account = null;
+ element.preferences = null;
+ });
+
+ test('show number disabled', () => {
+ assert.isFalse(element.showNumber);
+ });
+ });
+
+ suite('test show change number preference enabled', () => {
+ setup(() => {
+ element = fixture('basic');
+ element.preferences = {
+ legacycid_in_change_table: true,
+ time_format: 'HHMM_12',
+ change_table: [],
+ };
+ element.account = {_account_id: 1001};
+ flushAsynchronousOperations();
+ });
+
+ test('show number enabled', () => {
+ assert.isTrue(element.showNumber);
+ });
+ });
+
+ suite('test show change number preference disabled', () => {
+ setup(() => {
+ element = fixture('basic');
+ // legacycid_in_change_table is not set when false.
+ element.preferences = {
+ time_format: 'HHMM_12',
+ change_table: [],
+ };
+ element.account = {_account_id: 1001};
+ flushAsynchronousOperations();
+ });
+
+ test('show number disabled', () => {
+ assert.isFalse(element.showNumber);
+ });
+ });
+
+ test('computed fields', () => {
+ assert.equal(element._computeLabelNames(
+ [{results: [{_number: 0, labels: {}}]}]).length, 0);
+ assert.equal(element._computeLabelNames([
+ {results: [
+ {_number: 0, labels: {Verified: {approved: {}}}},
+ {
+ _number: 1,
+ labels: {
+ 'Verified': {approved: {}},
+ 'Code-Review': {approved: {}},
+ },
+ },
+ {
+ _number: 2,
+ labels: {
+ 'Verified': {approved: {}},
+ 'Library-Compliance': {approved: {}},
+ },
+ },
+ ]},
+ ]).length, 3);
+
+ assert.equal(element._computeLabelShortcut('Code-Review'), 'CR');
+ assert.equal(element._computeLabelShortcut('Verified'), 'V');
+ assert.equal(element._computeLabelShortcut('Library-Compliance'), 'LC');
+ assert.equal(element._computeLabelShortcut('PolyGerrit-Review'), 'PR');
+ assert.equal(element._computeLabelShortcut('polygerrit-review'), 'PR');
+ assert.equal(element._computeLabelShortcut(
+ 'Invalid-Prolog-Rules-Label-Name--Verified'), 'V');
+ assert.equal(element._computeLabelShortcut(
+ 'Some-Special-Label-7'), 'SSL7');
+ assert.equal(element._computeLabelShortcut('--Too----many----dashes---'),
+ 'TMD');
+ assert.equal(element._computeLabelShortcut(
+ 'Really-rather-entirely-too-long-of-a-label-name'), 'RRETL');
+ });
+
+ test('colspans', () => {
+ element.sections = [
+ {results: [{}]},
+ ];
+ flushAsynchronousOperations();
+ const tdItemCount = dom(element.root).querySelectorAll(
+ 'td').length;
+
+ const changeTableColumns = [];
+ const labelNames = [];
+ assert.equal(tdItemCount, element._computeColspan(
+ changeTableColumns, labelNames));
+ });
+
+ test('keyboard shortcuts', done => {
+ sandbox.stub(element, '_computeLabelNames');
+ element.sections = [
+ {results: new Array(1)},
+ {results: new Array(2)},
+ ];
+ element.selectedIndex = 0;
+ element.changes = [
+ {_number: 0},
+ {_number: 1},
+ {_number: 2},
+ ];
+ flushAsynchronousOperations();
+ afterNextRender(element, () => {
+ const elementItems = dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(elementItems.length, 3);
+
+ assert.isTrue(elementItems[0].hasAttribute('selected'));
+ MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
+ assert.equal(element.selectedIndex, 1);
+ assert.isTrue(elementItems[1].hasAttribute('selected'));
+ MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
+ assert.equal(element.selectedIndex, 2);
+ assert.isTrue(elementItems[2].hasAttribute('selected'));
+
+ const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ assert.equal(element.selectedIndex, 2);
+ MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
+ assert.deepEqual(navStub.lastCall.args[0], {_number: 2},
+ 'Should navigate to /c/2/');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ assert.equal(element.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
+ assert.deepEqual(navStub.lastCall.args[0], {_number: 1},
+ 'Should navigate to /c/1/');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ assert.equal(element.selectedIndex, 0);
+
+ const reloadStub = sandbox.stub(element, '_reloadWindow');
+ MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
+ assert.isTrue(reloadStub.called);
+
+ done();
+ });
+ });
+
+ test('changes needing review', () => {
+ element.changes = [
+ {
+ _number: 0,
+ status: 'NEW',
+ reviewed: true,
+ owner: {_account_id: 0},
+ },
+ {
+ _number: 1,
+ status: 'NEW',
+ owner: {_account_id: 0},
+ },
+ {
+ _number: 2,
+ status: 'MERGED',
+ owner: {_account_id: 0},
+ },
+ {
+ _number: 3,
+ status: 'ABANDONED',
+ owner: {_account_id: 0},
+ },
+ {
+ _number: 4,
+ status: 'NEW',
+ work_in_progress: true,
+ owner: {_account_id: 0},
+ },
+ ];
+ flushAsynchronousOperations();
+ let elementItems = dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(elementItems.length, 5);
+ for (let i = 0; i < elementItems.length; i++) {
+ assert.isFalse(elementItems[i].hasAttribute('needs-review'));
+ }
+
+ element.showReviewedState = true;
+ elementItems = dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(elementItems.length, 5);
+ assert.isFalse(elementItems[0].hasAttribute('needs-review'));
+ assert.isTrue(elementItems[1].hasAttribute('needs-review'));
+ assert.isFalse(elementItems[2].hasAttribute('needs-review'));
+ assert.isFalse(elementItems[3].hasAttribute('needs-review'));
+ assert.isFalse(elementItems[4].hasAttribute('needs-review'));
+
+ element.account = {_account_id: 42};
+ elementItems = dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(elementItems.length, 5);
+ assert.isFalse(elementItems[0].hasAttribute('needs-review'));
+ assert.isTrue(elementItems[1].hasAttribute('needs-review'));
+ assert.isFalse(elementItems[2].hasAttribute('needs-review'));
+ assert.isFalse(elementItems[3].hasAttribute('needs-review'));
+ assert.isFalse(elementItems[4].hasAttribute('needs-review'));
+ });
+
+ test('no changes', () => {
+ element.changes = [];
+ flushAsynchronousOperations();
+ const listItems = dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(listItems.length, 0);
+ const noChangesMsg =
+ dom(element.root).querySelector('.noChanges');
+ assert.ok(noChangesMsg);
+ });
+
+ test('empty sections', () => {
+ element.sections = [{results: []}, {results: []}];
+ flushAsynchronousOperations();
+ const listItems = dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(listItems.length, 0);
+ const noChangesMsg = dom(element.root).querySelectorAll(
+ '.noChanges');
+ assert.equal(noChangesMsg.length, 2);
+ });
+
+ suite('empty outgoing', () => {
+ test('not shown on empty non-outgoing sections', () => {
+ const section = {results: []};
+ assert.isTrue(element._isEmpty(section));
+ assert.isFalse(element._isOutgoing(section));
+ });
+
+ test('shown on empty outgoing sections', () => {
+ const section = {results: [], isOutgoing: true};
+ assert.isTrue(element._isEmpty(section));
+ assert.isTrue(element._isOutgoing(section));
+ });
+
+ test('not shown on non-empty outgoing sections', () => {
+ const section = {isOutgoing: true, results: [
+ {_number: 0, labels: {Verified: {approved: {}}}}]};
+ assert.isFalse(element._isEmpty(section));
+ assert.isTrue(element._isOutgoing(section));
+ });
+ });
+
+ test('_isOutgoing', () => {
+ assert.isTrue(element._isOutgoing({results: [], isOutgoing: true}));
+ assert.isFalse(element._isOutgoing({results: []}));
+ });
+
+ suite('empty column preference', () => {
+ let element;
+
+ setup(() => {
+ element = fixture('basic');
+ element.sections = [
+ {results: [{}]},
+ ];
+ element.account = {_account_id: 1001};
+ element.preferences = {
+ legacycid_in_change_table: true,
+ time_format: 'HHMM_12',
+ change_table: [],
+ };
+ flushAsynchronousOperations();
+ });
+
+ test('show number enabled', () => {
+ assert.isTrue(element.showNumber);
+ });
+
+ test('all columns visible', () => {
+ for (const column of element.columnNames) {
+ const elementClass = '.' + element._lowerCase(column);
+ assert.isFalse(element.shadowRoot
+ .querySelector(elementClass).hidden);
+ }
+ });
+ });
+
+ suite('full column preference', () => {
+ let element;
+
+ setup(() => {
+ element = fixture('basic');
+ element.sections = [
+ {results: [{}]},
+ ];
+ element.account = {_account_id: 1001};
+ element.preferences = {
+ legacycid_in_change_table: true,
+ time_format: 'HHMM_12',
+ change_table: [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Repo',
+ 'Branch',
+ 'Updated',
+ 'Size',
+ ],
+ };
+ flushAsynchronousOperations();
+ });
+
+ test('all columns visible', () => {
+ for (const column of element.changeTableColumns) {
+ const elementClass = '.' + element._lowerCase(column);
+ assert.isFalse(element.shadowRoot
+ .querySelector(elementClass).hidden);
+ }
+ });
+ });
+
+ suite('partial column preference', () => {
+ let element;
+
+ setup(() => {
+ element = fixture('basic');
+ element.sections = [
+ {results: [{}]},
+ ];
+ element.account = {_account_id: 1001};
+ element.preferences = {
+ legacycid_in_change_table: true,
+ time_format: 'HHMM_12',
+ change_table: [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Branch',
+ 'Updated',
+ 'Size',
+ ],
+ };
+ flushAsynchronousOperations();
+ });
+
+ test('all columns except repo visible', () => {
+ for (const column of element.changeTableColumns) {
+ const elementClass = '.' + column.toLowerCase();
+ if (column === 'Repo') {
+ assert.isTrue(element.shadowRoot
+ .querySelector(elementClass).hidden);
+ } else {
+ assert.isFalse(element.shadowRoot
+ .querySelector(elementClass).hidden);
+ }
+ }
+ });
+ });
+
+ suite('random column does not exist', () => {
+ let element;
+
+ /* This would only exist if somebody manually updated the config
+ file. */
+ setup(() => {
+ element = fixture('basic');
+ element.account = {_account_id: 1001};
+ element.preferences = {
+ legacycid_in_change_table: true,
+ time_format: 'HHMM_12',
+ change_table: [
+ 'Bad',
+ ],
+ };
+ flushAsynchronousOperations();
+ });
+
+ test('bad column does not exist', () => {
+ const elementClass = '.bad';
+ assert.isNotOk(element.shadowRoot
+ .querySelector(elementClass));
+ });
+ });
+
+ suite('dashboard queries', () => {
let element;
let sandbox;
@@ -67,576 +460,180 @@
teardown(() => { sandbox.restore(); });
- suite('test show change number not logged in', () => {
- setup(() => {
- element = fixture('basic');
- element.account = null;
- element.preferences = null;
- });
-
- test('show number disabled', () => {
- assert.isFalse(element.showNumber);
- });
+ test('query without age and limit unchanged', () => {
+ const query = 'status:closed owner:me';
+ assert.deepEqual(element._processQuery(query), query);
});
- suite('test show change number preference enabled', () => {
- setup(() => {
- element = fixture('basic');
- element.preferences = {
- legacycid_in_change_table: true,
- time_format: 'HHMM_12',
- change_table: [],
- };
- element.account = {_account_id: 1001};
- flushAsynchronousOperations();
- });
-
- test('show number enabled', () => {
- assert.isTrue(element.showNumber);
- });
+ test('query with age and limit', () => {
+ const query = 'status:closed age:1week limit:10 owner:me';
+ const expectedQuery = 'status:closed owner:me';
+ assert.deepEqual(element._processQuery(query), expectedQuery);
});
- suite('test show change number preference disabled', () => {
- setup(() => {
- element = fixture('basic');
- // legacycid_in_change_table is not set when false.
- element.preferences = {
- time_format: 'HHMM_12',
- change_table: [],
- };
- element.account = {_account_id: 1001};
- flushAsynchronousOperations();
- });
-
- test('show number disabled', () => {
- assert.isFalse(element.showNumber);
- });
+ test('query with age', () => {
+ const query = 'status:closed age:1week owner:me';
+ const expectedQuery = 'status:closed owner:me';
+ assert.deepEqual(element._processQuery(query), expectedQuery);
});
- test('computed fields', () => {
- assert.equal(element._computeLabelNames(
- [{results: [{_number: 0, labels: {}}]}]).length, 0);
- assert.equal(element._computeLabelNames([
- {results: [
- {_number: 0, labels: {Verified: {approved: {}}}},
- {
- _number: 1,
- labels: {
- 'Verified': {approved: {}},
- 'Code-Review': {approved: {}},
- },
- },
- {
- _number: 2,
- labels: {
- 'Verified': {approved: {}},
- 'Library-Compliance': {approved: {}},
- },
- },
- ]},
- ]).length, 3);
-
- assert.equal(element._computeLabelShortcut('Code-Review'), 'CR');
- assert.equal(element._computeLabelShortcut('Verified'), 'V');
- assert.equal(element._computeLabelShortcut('Library-Compliance'), 'LC');
- assert.equal(element._computeLabelShortcut('PolyGerrit-Review'), 'PR');
- assert.equal(element._computeLabelShortcut('polygerrit-review'), 'PR');
- assert.equal(element._computeLabelShortcut(
- 'Invalid-Prolog-Rules-Label-Name--Verified'), 'V');
- assert.equal(element._computeLabelShortcut(
- 'Some-Special-Label-7'), 'SSL7');
- assert.equal(element._computeLabelShortcut('--Too----many----dashes---'),
- 'TMD');
- assert.equal(element._computeLabelShortcut(
- 'Really-rather-entirely-too-long-of-a-label-name'), 'RRETL');
+ test('query with limit', () => {
+ const query = 'status:closed limit:10 owner:me';
+ const expectedQuery = 'status:closed owner:me';
+ assert.deepEqual(element._processQuery(query), expectedQuery);
});
- test('colspans', () => {
- element.sections = [
- {results: [{}]},
- ];
- flushAsynchronousOperations();
- const tdItemCount = Polymer.dom(element.root).querySelectorAll(
- 'td').length;
-
- const changeTableColumns = [];
- const labelNames = [];
- assert.equal(tdItemCount, element._computeColspan(
- changeTableColumns, labelNames));
+ test('query with age as value and not key', () => {
+ const query = 'status:closed random:age';
+ const expectedQuery = 'status:closed random:age';
+ assert.deepEqual(element._processQuery(query), expectedQuery);
});
+ test('query with limit as value and not key', () => {
+ const query = 'status:closed random:limit';
+ const expectedQuery = 'status:closed random:limit';
+ assert.deepEqual(element._processQuery(query), expectedQuery);
+ });
+
+ test('query with -age key', () => {
+ const query = 'status:closed -age:1week';
+ const expectedQuery = 'status:closed';
+ assert.deepEqual(element._processQuery(query), expectedQuery);
+ });
+ });
+
+ suite('gr-change-list sections', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => { sandbox.restore(); });
+
test('keyboard shortcuts', done => {
- sandbox.stub(element, '_computeLabelNames');
- element.sections = [
- {results: new Array(1)},
- {results: new Array(2)},
- ];
element.selectedIndex = 0;
- element.changes = [
- {_number: 0},
- {_number: 1},
- {_number: 2},
+ element.sections = [
+ {
+ results: [
+ {_number: 0},
+ {_number: 1},
+ {_number: 2},
+ ],
+ },
+ {
+ results: [
+ {_number: 3},
+ {_number: 4},
+ {_number: 5},
+ ],
+ },
+ {
+ results: [
+ {_number: 6},
+ {_number: 7},
+ {_number: 8},
+ ],
+ },
];
flushAsynchronousOperations();
- Polymer.RenderStatus.afterNextRender(element, () => {
- const elementItems = Polymer.dom(element.root).querySelectorAll(
+ afterNextRender(element, () => {
+ const elementItems = dom(element.root).querySelectorAll(
'gr-change-list-item');
- assert.equal(elementItems.length, 3);
+ assert.equal(elementItems.length, 9);
- assert.isTrue(elementItems[0].hasAttribute('selected'));
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
assert.equal(element.selectedIndex, 1);
- assert.isTrue(elementItems[1].hasAttribute('selected'));
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- assert.equal(element.selectedIndex, 2);
- assert.isTrue(elementItems[2].hasAttribute('selected'));
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
assert.equal(element.selectedIndex, 2);
- MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
assert.deepEqual(navStub.lastCall.args[0], {_number: 2},
'Should navigate to /c/2/');
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k'
assert.equal(element.selectedIndex, 1);
- MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
assert.deepEqual(navStub.lastCall.args[0], {_number: 1},
'Should navigate to /c/1/');
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- assert.equal(element.selectedIndex, 0);
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ assert.equal(element.selectedIndex, 4);
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
+ assert.deepEqual(navStub.lastCall.args[0], {_number: 4},
+ 'Should navigate to /c/4/');
- const reloadStub = sandbox.stub(element, '_reloadWindow');
- MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
- assert.isTrue(reloadStub.called);
-
+ MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r'
+ const change = element._changeForIndex(element.selectedIndex);
+ assert.equal(change.reviewed, true,
+ 'Should mark change as reviewed');
+ MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r'
+ assert.equal(change.reviewed, false,
+ 'Should mark change as unreviewed');
done();
});
});
- test('changes needing review', () => {
+ test('highlight attribute is updated correctly', () => {
element.changes = [
{
_number: 0,
status: 'NEW',
- reviewed: true,
owner: {_account_id: 0},
},
{
_number: 1,
- status: 'NEW',
- owner: {_account_id: 0},
- },
- {
- _number: 2,
- status: 'MERGED',
- owner: {_account_id: 0},
- },
- {
- _number: 3,
status: 'ABANDONED',
owner: {_account_id: 0},
},
- {
- _number: 4,
- status: 'NEW',
- work_in_progress: true,
- owner: {_account_id: 0},
- },
];
- flushAsynchronousOperations();
- let elementItems = Polymer.dom(element.root).querySelectorAll(
- 'gr-change-list-item');
- assert.equal(elementItems.length, 5);
- for (let i = 0; i < elementItems.length; i++) {
- assert.isFalse(elementItems[i].hasAttribute('needs-review'));
- }
-
- element.showReviewedState = true;
- elementItems = Polymer.dom(element.root).querySelectorAll(
- 'gr-change-list-item');
- assert.equal(elementItems.length, 5);
- assert.isFalse(elementItems[0].hasAttribute('needs-review'));
- assert.isTrue(elementItems[1].hasAttribute('needs-review'));
- assert.isFalse(elementItems[2].hasAttribute('needs-review'));
- assert.isFalse(elementItems[3].hasAttribute('needs-review'));
- assert.isFalse(elementItems[4].hasAttribute('needs-review'));
-
element.account = {_account_id: 42};
- elementItems = Polymer.dom(element.root).querySelectorAll(
- 'gr-change-list-item');
- assert.equal(elementItems.length, 5);
- assert.isFalse(elementItems[0].hasAttribute('needs-review'));
- assert.isTrue(elementItems[1].hasAttribute('needs-review'));
- assert.isFalse(elementItems[2].hasAttribute('needs-review'));
- assert.isFalse(elementItems[3].hasAttribute('needs-review'));
- assert.isFalse(elementItems[4].hasAttribute('needs-review'));
- });
-
- test('no changes', () => {
- element.changes = [];
flushAsynchronousOperations();
- const listItems = Polymer.dom(element.root).querySelectorAll(
- 'gr-change-list-item');
- assert.equal(listItems.length, 0);
- const noChangesMsg =
- Polymer.dom(element.root).querySelector('.noChanges');
- assert.ok(noChangesMsg);
- });
+ let items = element._getListItems();
+ assert.equal(items.length, 2);
+ assert.isFalse(items[0].hasAttribute('highlight'));
+ assert.isFalse(items[1].hasAttribute('highlight'));
- test('empty sections', () => {
- element.sections = [{results: []}, {results: []}];
+ // Assign all issues to the user, but only the first one is highlighted
+ // because the second one is abandoned.
+ element.set(['changes', 0, 'assignee'], {_account_id: 12});
+ element.set(['changes', 1, 'assignee'], {_account_id: 12});
+ element.account = {_account_id: 12};
flushAsynchronousOperations();
- const listItems = Polymer.dom(element.root).querySelectorAll(
- 'gr-change-list-item');
- assert.equal(listItems.length, 0);
- const noChangesMsg = Polymer.dom(element.root).querySelectorAll(
- '.noChanges');
- assert.equal(noChangesMsg.length, 2);
+ items = element._getListItems();
+ assert.isTrue(items[0].hasAttribute('highlight'));
+ assert.isFalse(items[1].hasAttribute('highlight'));
});
- suite('empty outgoing', () => {
- test('not shown on empty non-outgoing sections', () => {
- const section = {results: []};
- assert.isTrue(element._isEmpty(section));
- assert.isFalse(element._isOutgoing(section));
- });
-
- test('shown on empty outgoing sections', () => {
- const section = {results: [], isOutgoing: true};
- assert.isTrue(element._isEmpty(section));
- assert.isTrue(element._isOutgoing(section));
- });
-
- test('not shown on non-empty outgoing sections', () => {
- const section = {isOutgoing: true, results: [
- {_number: 0, labels: {Verified: {approved: {}}}}]};
- assert.isFalse(element._isEmpty(section));
- assert.isTrue(element._isOutgoing(section));
- });
+ test('_computeItemHighlight gives false for null account', () => {
+ assert.isFalse(
+ element._computeItemHighlight(null, {assignee: {_account_id: 42}}));
});
- test('_isOutgoing', () => {
- assert.isTrue(element._isOutgoing({results: [], isOutgoing: true}));
- assert.isFalse(element._isOutgoing({results: []}));
- });
+ test('_computeItemAbsoluteIndex', () => {
+ sandbox.stub(element, '_computeLabelNames');
+ element.sections = [
+ {results: new Array(1)},
+ {results: new Array(2)},
+ {results: new Array(3)},
+ ];
- suite('empty column preference', () => {
- let element;
+ assert.equal(element._computeItemAbsoluteIndex(0, 0), 0);
+ // Out of range but no matter.
+ assert.equal(element._computeItemAbsoluteIndex(0, 1), 1);
- setup(() => {
- element = fixture('basic');
- element.sections = [
- {results: [{}]},
- ];
- element.account = {_account_id: 1001};
- element.preferences = {
- legacycid_in_change_table: true,
- time_format: 'HHMM_12',
- change_table: [],
- };
- flushAsynchronousOperations();
- });
-
- test('show number enabled', () => {
- assert.isTrue(element.showNumber);
- });
-
- test('all columns visible', () => {
- for (const column of element.columnNames) {
- const elementClass = '.' + element._lowerCase(column);
- assert.isFalse(element.shadowRoot
- .querySelector(elementClass).hidden);
- }
- });
- });
-
- suite('full column preference', () => {
- let element;
-
- setup(() => {
- element = fixture('basic');
- element.sections = [
- {results: [{}]},
- ];
- element.account = {_account_id: 1001};
- element.preferences = {
- legacycid_in_change_table: true,
- time_format: 'HHMM_12',
- change_table: [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Repo',
- 'Branch',
- 'Updated',
- 'Size',
- ],
- };
- flushAsynchronousOperations();
- });
-
- test('all columns visible', () => {
- for (const column of element.changeTableColumns) {
- const elementClass = '.' + element._lowerCase(column);
- assert.isFalse(element.shadowRoot
- .querySelector(elementClass).hidden);
- }
- });
- });
-
- suite('partial column preference', () => {
- let element;
-
- setup(() => {
- element = fixture('basic');
- element.sections = [
- {results: [{}]},
- ];
- element.account = {_account_id: 1001};
- element.preferences = {
- legacycid_in_change_table: true,
- time_format: 'HHMM_12',
- change_table: [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Branch',
- 'Updated',
- 'Size',
- ],
- };
- flushAsynchronousOperations();
- });
-
- test('all columns except repo visible', () => {
- for (const column of element.changeTableColumns) {
- const elementClass = '.' + column.toLowerCase();
- if (column === 'Repo') {
- assert.isTrue(element.shadowRoot
- .querySelector(elementClass).hidden);
- } else {
- assert.isFalse(element.shadowRoot
- .querySelector(elementClass).hidden);
- }
- }
- });
- });
-
- suite('random column does not exist', () => {
- let element;
-
- /* This would only exist if somebody manually updated the config
- file. */
- setup(() => {
- element = fixture('basic');
- element.account = {_account_id: 1001};
- element.preferences = {
- legacycid_in_change_table: true,
- time_format: 'HHMM_12',
- change_table: [
- 'Bad',
- ],
- };
- flushAsynchronousOperations();
- });
-
- test('bad column does not exist', () => {
- const elementClass = '.bad';
- assert.isNotOk(element.shadowRoot
- .querySelector(elementClass));
- });
- });
-
- suite('dashboard queries', () => {
- let element;
- let sandbox;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('query without age and limit unchanged', () => {
- const query = 'status:closed owner:me';
- assert.deepEqual(element._processQuery(query), query);
- });
-
- test('query with age and limit', () => {
- const query = 'status:closed age:1week limit:10 owner:me';
- const expectedQuery = 'status:closed owner:me';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
-
- test('query with age', () => {
- const query = 'status:closed age:1week owner:me';
- const expectedQuery = 'status:closed owner:me';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
-
- test('query with limit', () => {
- const query = 'status:closed limit:10 owner:me';
- const expectedQuery = 'status:closed owner:me';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
-
- test('query with age as value and not key', () => {
- const query = 'status:closed random:age';
- const expectedQuery = 'status:closed random:age';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
-
- test('query with limit as value and not key', () => {
- const query = 'status:closed random:limit';
- const expectedQuery = 'status:closed random:limit';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
-
- test('query with -age key', () => {
- const query = 'status:closed -age:1week';
- const expectedQuery = 'status:closed';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
- });
-
- suite('gr-change-list sections', () => {
- let element;
- let sandbox;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('keyboard shortcuts', done => {
- element.selectedIndex = 0;
- element.sections = [
- {
- results: [
- {_number: 0},
- {_number: 1},
- {_number: 2},
- ],
- },
- {
- results: [
- {_number: 3},
- {_number: 4},
- {_number: 5},
- ],
- },
- {
- results: [
- {_number: 6},
- {_number: 7},
- {_number: 8},
- ],
- },
- ];
- flushAsynchronousOperations();
- Polymer.RenderStatus.afterNextRender(element, () => {
- const elementItems = Polymer.dom(element.root).querySelectorAll(
- 'gr-change-list-item');
- assert.equal(elementItems.length, 9);
-
- MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
- assert.equal(element.selectedIndex, 1);
- MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
-
- const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
- assert.equal(element.selectedIndex, 2);
-
- MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
- assert.deepEqual(navStub.lastCall.args[0], {_number: 2},
- 'Should navigate to /c/2/');
-
- MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k'
- assert.equal(element.selectedIndex, 1);
- MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
- assert.deepEqual(navStub.lastCall.args[0], {_number: 1},
- 'Should navigate to /c/1/');
-
- MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
- MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
- MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
- assert.equal(element.selectedIndex, 4);
- MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
- assert.deepEqual(navStub.lastCall.args[0], {_number: 4},
- 'Should navigate to /c/4/');
-
- MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r'
- const change = element._changeForIndex(element.selectedIndex);
- assert.equal(change.reviewed, true,
- 'Should mark change as reviewed');
- MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r'
- assert.equal(change.reviewed, false,
- 'Should mark change as unreviewed');
- done();
- });
- });
-
- test('highlight attribute is updated correctly', () => {
- element.changes = [
- {
- _number: 0,
- status: 'NEW',
- owner: {_account_id: 0},
- },
- {
- _number: 1,
- status: 'ABANDONED',
- owner: {_account_id: 0},
- },
- ];
- element.account = {_account_id: 42};
- flushAsynchronousOperations();
- let items = element._getListItems();
- assert.equal(items.length, 2);
- assert.isFalse(items[0].hasAttribute('highlight'));
- assert.isFalse(items[1].hasAttribute('highlight'));
-
- // Assign all issues to the user, but only the first one is highlighted
- // because the second one is abandoned.
- element.set(['changes', 0, 'assignee'], {_account_id: 12});
- element.set(['changes', 1, 'assignee'], {_account_id: 12});
- element.account = {_account_id: 12};
- flushAsynchronousOperations();
- items = element._getListItems();
- assert.isTrue(items[0].hasAttribute('highlight'));
- assert.isFalse(items[1].hasAttribute('highlight'));
- });
-
- test('_computeItemHighlight gives false for null account', () => {
- assert.isFalse(
- element._computeItemHighlight(null, {assignee: {_account_id: 42}}));
- });
-
- test('_computeItemAbsoluteIndex', () => {
- sandbox.stub(element, '_computeLabelNames');
- element.sections = [
- {results: new Array(1)},
- {results: new Array(2)},
- {results: new Array(3)},
- ];
-
- assert.equal(element._computeItemAbsoluteIndex(0, 0), 0);
- // Out of range but no matter.
- assert.equal(element._computeItemAbsoluteIndex(0, 1), 1);
-
- assert.equal(element._computeItemAbsoluteIndex(1, 0), 1);
- assert.equal(element._computeItemAbsoluteIndex(1, 1), 2);
- assert.equal(element._computeItemAbsoluteIndex(1, 2), 3);
- assert.equal(element._computeItemAbsoluteIndex(2, 0), 3);
- assert.equal(element._computeItemAbsoluteIndex(3, 0), 6);
- });
+ assert.equal(element._computeItemAbsoluteIndex(1, 0), 1);
+ assert.equal(element._computeItemAbsoluteIndex(1, 1), 2);
+ assert.equal(element._computeItemAbsoluteIndex(1, 2), 3);
+ assert.equal(element._computeItemAbsoluteIndex(2, 0), 3);
+ assert.equal(element._computeItemAbsoluteIndex(3, 0), 6);
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
index 64d2486..3758a78 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
@@ -14,27 +14,35 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrCreateChangeHelp extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-create-change-help'; }
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-icons/gr-icons.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-create-change-help_html.js';
- /**
- * Fired when the "Create change" button is tapped.
- *
- * @event create-tap
- */
+/** @extends Polymer.Element */
+class GrCreateChangeHelp extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _handleCreateTap(e) {
- e.preventDefault();
- this.dispatchEvent(
- new CustomEvent('create-tap', {bubbles: true, composed: true}));
- }
+ static get is() { return 'gr-create-change-help'; }
+
+ /**
+ * Fired when the "Create change" button is tapped.
+ *
+ * @event create-tap
+ */
+
+ _handleCreateTap(e) {
+ e.preventDefault();
+ this.dispatchEvent(
+ new CustomEvent('create-tap', {bubbles: true, composed: true}));
}
+}
- customElements.define(GrCreateChangeHelp.is, GrCreateChangeHelp);
-})();
+customElements.define(GrCreateChangeHelp.is, GrCreateChangeHelp);
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.html b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js
similarity index 60%
rename from polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.html
rename to polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js
index 842c402..f40bda7 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.html
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js
@@ -1,28 +1,22 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
+/**
+ * @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.js';
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-
-<dom-module id="gr-create-change-help">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -82,11 +76,9 @@
<h1>Push your first change for code review</h1>
<p>
Pushing a change for review is easy, but a little different from
- other git code review tools. Click on the `Create Change' button
+ other git code review tools. Click on the \`Create Change' button
and follow the step by step instructions.
</p>
<gr-button on-click="_handleCreateTap">Create Change</gr-button>
</div>
- </template>
- <script src="gr-create-change-help.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html
index abc4a9b..213ac07 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html
@@ -19,17 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-create-change-help</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="gr-create-change-help.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -37,19 +30,21 @@
</template>
</test-fixture>
-<script>
- suite('gr-create-change-help tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-create-change-help.js';
+suite('gr-create-change-help tests', () => {
+ let element;
- setup(() => {
- element = fixture('basic');
- });
-
- test('Create change tap', done => {
- element.addEventListener('create-tap', () => done());
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button'));
- });
+ setup(() => {
+ element = fixture('basic');
});
+
+ test('Create change tap', done => {
+ element.addEventListener('create-tap', () => done());
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.html b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.html
deleted file mode 100644
index 9e86058..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.html
+++ /dev/null
@@ -1,87 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-shell-command/gr-shell-command.html">
-
-<dom-module id="gr-create-commands-dialog">
- <template>
- <style include="shared-styles">
- ol {
- list-style: decimal;
- margin-left: var(--spacing-l);
- }
- p {
- margin-bottom: var(--spacing-m);
- }
- #commandsDialog {
- max-width: 40em;
- }
- </style>
- <gr-overlay id="commandsOverlay" with-backdrop>
- <gr-dialog
- id="commandsDialog"
- confirm-label="Done"
- cancel-label=""
- confirm-on-enter
- on-confirm="_handleClose">
- <div class="header" slot="header">
- Create change commands
- </div>
- <div class="main" slot="main">
- <ol>
- <li>
- <p>
- Make the changes to the files on your machine
- </p>
- </li>
- <li>
- <p>
- If you are making a new commit use
- </p>
- <gr-shell-command command="[[_createNewCommitCommand]]"></gr-shell-command>
- <p>
- Or to amend an existing commit use
- </p>
- <gr-shell-command command="[[_amendExistingCommitCommand]]"></gr-shell-command>
- <p>
- Please make sure you add a commit message as it becomes the
- description for your change.
- </p>
- </li>
- <li>
- <p>
- Push the change for code review
- </p>
- <gr-shell-command command="[[_pushCommand]]"></gr-shell-command>
- </li>
- <li>
- <p>
- Close this dialog and you should be able to see your recently
- created change in the 'Outgoing changes' section on the
- 'Your changes' page.
- </p>
- </li>
- </ol>
- </div>
- </gr-dialog>
- </gr-overlay>
- </template>
- <script src="gr-create-commands-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
index 7abd784..7e5e749 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
@@ -14,53 +14,61 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const Commands = {
- CREATE: 'git commit',
- AMEND: 'git commit --amend',
- PUSH_PREFIX: 'git push origin HEAD:refs/for/',
- };
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-shell-command/gr-shell-command.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-create-commands-dialog_html.js';
- /** @extends Polymer.Element */
- class GrCreateCommandsDialog extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-create-commands-dialog'; }
+const Commands = {
+ CREATE: 'git commit',
+ AMEND: 'git commit --amend',
+ PUSH_PREFIX: 'git push origin HEAD:refs/for/',
+};
- static get properties() {
- return {
- branch: String,
- _createNewCommitCommand: {
- type: String,
- readonly: true,
- value: Commands.CREATE,
- },
- _amendExistingCommitCommand: {
- type: String,
- readonly: true,
- value: Commands.AMEND,
- },
- _pushCommand: {
- type: String,
- computed: '_computePushCommand(branch)',
- },
- };
- }
+/** @extends Polymer.Element */
+class GrCreateCommandsDialog extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- open() {
- this.$.commandsOverlay.open();
- }
+ static get is() { return 'gr-create-commands-dialog'; }
- _handleClose() {
- this.$.commandsOverlay.close();
- }
-
- _computePushCommand(branch) {
- return Commands.PUSH_PREFIX + branch;
- }
+ static get properties() {
+ return {
+ branch: String,
+ _createNewCommitCommand: {
+ type: String,
+ readonly: true,
+ value: Commands.CREATE,
+ },
+ _amendExistingCommitCommand: {
+ type: String,
+ readonly: true,
+ value: Commands.AMEND,
+ },
+ _pushCommand: {
+ type: String,
+ computed: '_computePushCommand(branch)',
+ },
+ };
}
- customElements.define(GrCreateCommandsDialog.is, GrCreateCommandsDialog);
-})();
+ open() {
+ this.$.commandsOverlay.open();
+ }
+
+ _handleClose() {
+ this.$.commandsOverlay.close();
+ }
+
+ _computePushCommand(branch) {
+ return Commands.PUSH_PREFIX + branch;
+ }
+}
+
+customElements.define(GrCreateCommandsDialog.is, GrCreateCommandsDialog);
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_html.js b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_html.js
new file mode 100644
index 0000000..aa13dca
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_html.js
@@ -0,0 +1,75 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ ol {
+ list-style: decimal;
+ margin-left: var(--spacing-l);
+ }
+ p {
+ margin-bottom: var(--spacing-m);
+ }
+ #commandsDialog {
+ max-width: 40em;
+ }
+ </style>
+ <gr-overlay id="commandsOverlay" with-backdrop="">
+ <gr-dialog id="commandsDialog" confirm-label="Done" cancel-label="" confirm-on-enter="" on-confirm="_handleClose">
+ <div class="header" slot="header">
+ Create change commands
+ </div>
+ <div class="main" slot="main">
+ <ol>
+ <li>
+ <p>
+ Make the changes to the files on your machine
+ </p>
+ </li>
+ <li>
+ <p>
+ If you are making a new commit use
+ </p>
+ <gr-shell-command command="[[_createNewCommitCommand]]"></gr-shell-command>
+ <p>
+ Or to amend an existing commit use
+ </p>
+ <gr-shell-command command="[[_amendExistingCommitCommand]]"></gr-shell-command>
+ <p>
+ Please make sure you add a commit message as it becomes the
+ description for your change.
+ </p>
+ </li>
+ <li>
+ <p>
+ Push the change for code review
+ </p>
+ <gr-shell-command command="[[_pushCommand]]"></gr-shell-command>
+ </li>
+ <li>
+ <p>
+ Close this dialog and you should be able to see your recently
+ created change in the 'Outgoing changes' section on the
+ 'Your changes' page.
+ </p>
+ </li>
+ </ol>
+ </div>
+ </gr-dialog>
+ </gr-overlay>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html
index 41709a6..620e6fe 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-create-commands-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-create-commands-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,23 +30,24 @@
</template>
</test-fixture>
-<script>
- suite('gr-create-commands-dialog tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-create-commands-dialog.js';
+suite('gr-create-commands-dialog tests', () => {
+ let element;
- setup(() => {
- element = fixture('basic');
- });
-
- test('_computePushCommand', () => {
- element.branch = 'master';
- assert.equal(element._pushCommand,
- 'git push origin HEAD:refs/for/master');
-
- element.branch = 'stable-2.15';
- assert.equal(element._pushCommand,
- 'git push origin HEAD:refs/for/stable-2.15');
- });
+ setup(() => {
+ element = fixture('basic');
});
+
+ test('_computePushCommand', () => {
+ element.branch = 'master';
+ assert.equal(element._pushCommand,
+ 'git push origin HEAD:refs/for/master');
+
+ element.branch = 'stable-2.15';
+ assert.equal(element._pushCommand,
+ 'git push origin HEAD:refs/for/stable-2.15');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.html b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.html
deleted file mode 100644
index def5228..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.html
+++ /dev/null
@@ -1,48 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-repo-branch-picker/gr-repo-branch-picker.html">
-
-<dom-module id="gr-create-destination-dialog">
- <template>
- <style include="shared-styles">
- </style>
- <gr-overlay id="createOverlay" with-backdrop>
- <gr-dialog
- confirm-label="View commands"
- on-confirm="_pickerConfirm"
- on-cancel="_handleClose"
- disabled="[[!_repoAndBranchSelected]]">
- <div class="header" slot="header">
- Create change
- </div>
- <div class="main" slot="main">
- <gr-repo-branch-picker
- repo="{{_repo}}"
- branch="{{_branch}}"></gr-repo-branch-picker>
- <p>
- If you haven't done so, you will need to clone the repository.
- </p>
- </div>
- </gr-dialog>
- </gr-overlay>
- </template>
- <script src="gr-create-destination-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
index 35f7450..f8757ba 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
@@ -14,58 +14,66 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * Fired when a destination has been picked. Event details contain the repo
- * name and the branch name.
- *
- * @event confirm
- * @extends Polymer.Element
- */
- class GrCreateDestinationDialog extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-create-destination-dialog'; }
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-repo-branch-picker/gr-repo-branch-picker.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-create-destination-dialog_html.js';
- static get properties() {
- return {
- _repo: String,
- _branch: String,
- _repoAndBranchSelected: {
- type: Boolean,
- value: false,
- computed: '_computeRepoAndBranchSelected(_repo, _branch)',
- },
- };
- }
+/**
+ * Fired when a destination has been picked. Event details contain the repo
+ * name and the branch name.
+ *
+ * @event confirm
+ * @extends Polymer.Element
+ */
+class GrCreateDestinationDialog extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- open() {
- this._repo = '';
- this._branch = '';
- this.$.createOverlay.open();
- }
+ static get is() { return 'gr-create-destination-dialog'; }
- _handleClose() {
- this.$.createOverlay.close();
- }
-
- _pickerConfirm(e) {
- this.$.createOverlay.close();
- const detail = {repo: this._repo, branch: this._branch};
- // e is a 'confirm' event from gr-dialog. We want to fire a more detailed
- // 'confirm' event here, so let's stop propagation of the bare event.
- e.preventDefault();
- e.stopPropagation();
- this.dispatchEvent(new CustomEvent('confirm', {detail, bubbles: false}));
- }
-
- _computeRepoAndBranchSelected(repo, branch) {
- return !!(repo && branch);
- }
+ static get properties() {
+ return {
+ _repo: String,
+ _branch: String,
+ _repoAndBranchSelected: {
+ type: Boolean,
+ value: false,
+ computed: '_computeRepoAndBranchSelected(_repo, _branch)',
+ },
+ };
}
- customElements.define(GrCreateDestinationDialog.is,
- GrCreateDestinationDialog);
-})();
+ open() {
+ this._repo = '';
+ this._branch = '';
+ this.$.createOverlay.open();
+ }
+
+ _handleClose() {
+ this.$.createOverlay.close();
+ }
+
+ _pickerConfirm(e) {
+ this.$.createOverlay.close();
+ const detail = {repo: this._repo, branch: this._branch};
+ // e is a 'confirm' event from gr-dialog. We want to fire a more detailed
+ // 'confirm' event here, so let's stop propagation of the bare event.
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(new CustomEvent('confirm', {detail, bubbles: false}));
+ }
+
+ _computeRepoAndBranchSelected(repo, branch) {
+ return !!(repo && branch);
+ }
+}
+
+customElements.define(GrCreateDestinationDialog.is,
+ GrCreateDestinationDialog);
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_html.js b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_html.js
new file mode 100644
index 0000000..73f6ec0
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_html.js
@@ -0,0 +1,35 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ </style>
+ <gr-overlay id="createOverlay" with-backdrop="">
+ <gr-dialog confirm-label="View commands" on-confirm="_pickerConfirm" on-cancel="_handleClose" disabled="[[!_repoAndBranchSelected]]">
+ <div class="header" slot="header">
+ Create change
+ </div>
+ <div class="main" slot="main">
+ <gr-repo-branch-picker repo="{{_repo}}" branch="{{_branch}}"></gr-repo-branch-picker>
+ <p>
+ If you haven't done so, you will need to clone the repository.
+ </p>
+ </div>
+ </gr-dialog>
+ </gr-overlay>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
deleted file mode 100644
index f119e98..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
+++ /dev/null
@@ -1,133 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../change-list/gr-change-list/gr-change-list.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-create-commands-dialog/gr-create-commands-dialog.html">
-<link rel="import" href="../gr-create-change-help/gr-create-change-help.html">
-<link rel="import" href="../gr-create-destination-dialog/gr-create-destination-dialog.html">
-<link rel="import" href="../gr-user-header/gr-user-header.html">
-
-<dom-module id="gr-dashboard-view">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- }
- .loading {
- color: var(--deemphasized-text-color);
- padding: var(--spacing-l);
- }
- gr-change-list {
- width: 100%;
- }
- gr-user-header {
- border-bottom: 1px solid var(--border-color);
- }
- .banner {
- align-items: center;
- background-color: var(--comment-background-color);
- border-bottom: 1px solid var(--border-color);
- display: flex;
- justify-content: space-between;
- padding: var(--spacing-xs) var(--spacing-l);
- }
- .banner gr-button {
- --gr-button: {
- color: var(--primary-text-color);
- }
- }
- .hide {
- display: none;
- }
- #emptyOutgoing {
- display: block;
- }
- @media only screen and (max-width: 50em) {
- .loading {
- padding: 0 var(--spacing-l);
- }
- }
- </style>
- <div class$="banner [[_computeBannerClass(_showDraftsBanner)]]">
- <div>
- You have draft comments on closed changes.
- <a href$="[[_computeDraftsLink(_showDraftsBanner)]]" target="_blank">(view all)</a>
- </div>
- <div>
- <gr-button
- class="delete"
- link
- on-click="_handleOpenDeleteDialog">Delete All</gr-button>
- </div>
- </div>
- <div class="loading" hidden$="[[!_loading]]">Loading...</div>
- <div hidden$="[[_loading]]" hidden>
- <gr-user-header
- user-id="[[params.user]]"
- class$="[[_computeUserHeaderClass(params)]]"></gr-user-header>
- <gr-change-list
- show-star
- show-reviewed-state
- account="[[account]]"
- preferences="[[preferences]]"
- selected-index="{{viewState.selectedChangeIndex}}"
- sections="[[_results]]"
- on-toggle-star="_handleToggleStar"
- on-toggle-reviewed="_handleToggleReviewed">
- <div id="emptyOutgoing" slot="empty-outgoing">
- <template is="dom-if" if="[[_showNewUserHelp]]">
- <gr-create-change-help on-create-tap="createChangeTap"></gr-create-change-help>
- </template>
- <template is="dom-if" if="[[!_showNewUserHelp]]">
- No changes
- </template>
- </div>
- </gr-change-list>
- </div>
- <gr-overlay id="confirmDeleteOverlay" with-backdrop>
- <gr-dialog
- id="confirmDeleteDialog"
- confirm-label="Delete"
- on-confirm="_handleConfirmDelete"
- on-cancel="_closeConfirmDeleteOverlay">
- <div class="header" slot="header">
- Delete comments
- </div>
- <div class="main" slot="main">
- Are you sure you want to delete all your draft comments in closed changes? This action
- cannot be undone.
- </div>
- </gr-dialog>
- </gr-overlay>
- <gr-create-destination-dialog
- id="destinationDialog"
- on-confirm="_handleDestinationConfirm"></gr-create-destination-dialog>
- <gr-create-commands-dialog id="commandsDialog"></gr-create-commands-dialog>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- <gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="gr-dashboard-view.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
index d0e1db2..a4ed814 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,306 +14,325 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const PROJECT_PLACEHOLDER_PATTERN = /\$\{project\}/g;
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../gr-change-list/gr-change-list.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-create-commands-dialog/gr-create-commands-dialog.js';
+import '../gr-create-change-help/gr-create-change-help.js';
+import '../gr-create-destination-dialog/gr-create-destination-dialog.js';
+import '../gr-user-header/gr-user-header.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-dashboard-view_html.js';
+const PROJECT_PLACEHOLDER_PATTERN = /\$\{project\}/g;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrDashboardView extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-dashboard-view'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * Fired when the title of the page should change.
+ *
+ * @event title-change
*/
- class GrDashboardView extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-dashboard-view'; }
- /**
- * Fired when the title of the page should change.
- *
- * @event title-change
- */
- static get properties() {
- return {
- account: {
- type: Object,
- value: null,
+ static get properties() {
+ return {
+ account: {
+ type: Object,
+ value: null,
+ },
+ preferences: Object,
+ /** @type {{ selectedChangeIndex: number }} */
+ viewState: Object,
+
+ /** @type {{ project: string, user: string }} */
+ params: {
+ type: Object,
+ },
+
+ createChangeTap: {
+ type: Function,
+ value() {
+ return this._createChangeTap.bind(this);
},
- preferences: Object,
- /** @type {{ selectedChangeIndex: number }} */
- viewState: Object,
+ },
- /** @type {{ project: string, user: string }} */
- params: {
- type: Object,
- },
+ _results: Array,
- createChangeTap: {
- type: Function,
- value() {
- return this._createChangeTap.bind(this);
- },
- },
+ /**
+ * For showing a "loading..." string during ajax requests.
+ */
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
- _results: Array,
+ _showDraftsBanner: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * For showing a "loading..." string during ajax requests.
- */
- _loading: {
- type: Boolean,
- value: true,
- },
-
- _showDraftsBanner: {
- type: Boolean,
- value: false,
- },
-
- _showNewUserHelp: {
- type: Boolean,
- value: false,
- },
- };
- }
-
- static get observers() {
- return [
- '_paramsChanged(params.*)',
- ];
- }
-
- get options() {
- return this.listChangesOptionsToHex(
- this.ListChangesOption.LABELS,
- this.ListChangesOption.DETAILED_ACCOUNTS,
- this.ListChangesOption.REVIEWED
- );
- }
-
- /** @override */
- attached() {
- super.attached();
- this._loadPreferences();
- }
-
- _loadPreferences() {
- return this.$.restAPI.getLoggedIn().then(loggedIn => {
- if (loggedIn) {
- this.$.restAPI.getPreferences().then(preferences => {
- this.preferences = preferences;
- });
- } else {
- this.preferences = {};
- }
- });
- }
-
- _getProjectDashboard(project, dashboard) {
- const errFn = response => {
- this.fire('page-error', {response});
- };
- return this.$.restAPI.getDashboard(
- project, dashboard, errFn).then(response => {
- if (!response) {
- return;
- }
- return {
- title: response.title,
- sections: response.sections.map(section => {
- const suffix = response.foreach ? ' ' + response.foreach : '';
- return {
- name: section.name,
- query: (section.query + suffix).replace(
- PROJECT_PLACEHOLDER_PATTERN, project),
- };
- }),
- };
- });
- }
-
- _computeTitle(user) {
- if (!user || user === 'self') {
- return 'My Reviews';
- }
- return 'Dashboard for ' + user;
- }
-
- _isViewActive(params) {
- return params.view === Gerrit.Nav.View.DASHBOARD;
- }
-
- _paramsChanged(paramsChangeRecord) {
- const params = paramsChangeRecord.base;
-
- if (!this._isViewActive(params)) {
- return Promise.resolve();
- }
-
- return this._reload();
- }
-
- /**
- * Reloads the element.
- *
- * @return {Promise<!Object>}
- */
- _reload() {
- this._loading = true;
- const {project, dashboard, title, user, sections} = this.params;
- const dashboardPromise = project ?
- this._getProjectDashboard(project, dashboard) :
- Promise.resolve(Gerrit.Nav.getUserDashboard(
- user,
- sections,
- title || this._computeTitle(user)));
-
- const checkForNewUser = !project && user === 'self';
- return dashboardPromise
- .then(res => {
- if (res && res.title) {
- this.fire('title-change', {title: res.title});
- }
- return this._fetchDashboardChanges(res, checkForNewUser);
- })
- .then(() => {
- this._maybeShowDraftsBanner();
- this.$.reporting.dashboardDisplayed();
- })
- .catch(err => {
- this.fire('title-change', {
- title: title || this._computeTitle(user),
- });
- console.warn(err);
- })
- .then(() => { this._loading = false; });
- }
-
- /**
- * Fetches the changes for each dashboard section and sets this._results
- * with the response.
- *
- * @param {!Object} res
- * @param {boolean} checkForNewUser
- * @return {Promise}
- */
- _fetchDashboardChanges(res, checkForNewUser) {
- if (!res) { return Promise.resolve(); }
-
- const queries = res.sections
- .map(section => (section.suffixForDashboard ?
- section.query + ' ' + section.suffixForDashboard :
- section.query));
-
- if (checkForNewUser) {
- queries.push('owner:self limit:1');
- }
-
- return this.$.restAPI.getChanges(null, queries, null, this.options)
- .then(changes => {
- if (checkForNewUser) {
- // Last set of results is not meant for dashboard display.
- const lastResultSet = changes.pop();
- this._showNewUserHelp = lastResultSet.length == 0;
- }
- this._results = changes.map((results, i) => {
- return {
- name: res.sections[i].name,
- countLabel: this._computeSectionCountLabel(results),
- query: res.sections[i].query,
- results,
- isOutgoing: res.sections[i].isOutgoing,
- };
- }).filter((section, i) => i < res.sections.length && (
- !res.sections[i].hideIfEmpty ||
- section.results.length));
- });
- }
-
- _computeSectionCountLabel(changes) {
- if (!changes || !changes.length || changes.length == 0) {
- return '';
- }
- const more = changes[changes.length - 1]._more_changes;
- const numChanges = changes.length;
- const andMore = more ? ' and more' : '';
- return `(${numChanges}${andMore})`;
- }
-
- _computeUserHeaderClass(params) {
- if (!params || !!params.project || !params.user ||
- params.user === 'self') {
- return 'hide';
- }
- return '';
- }
-
- _handleToggleStar(e) {
- this.$.restAPI.saveChangeStarred(e.detail.change._number,
- e.detail.starred);
- }
-
- _handleToggleReviewed(e) {
- this.$.restAPI.saveChangeReviewed(e.detail.change._number,
- e.detail.reviewed);
- }
-
- /**
- * Banner is shown if a user is on their own dashboard and they have draft
- * comments on closed changes.
- */
- _maybeShowDraftsBanner() {
- this._showDraftsBanner = false;
- if (!(this.params.user === 'self')) { return; }
-
- const draftSection = this._results
- .find(section => section.query === 'has:draft');
- if (!draftSection || !draftSection.results.length) { return; }
-
- const closedChanges = draftSection.results
- .filter(change => !this.changeIsOpen(change));
- if (!closedChanges.length) { return; }
-
- this._showDraftsBanner = true;
- }
-
- _computeBannerClass(show) {
- return show ? '' : 'hide';
- }
-
- _handleOpenDeleteDialog() {
- this.$.confirmDeleteOverlay.open();
- }
-
- _handleConfirmDelete() {
- this.$.confirmDeleteDialog.disabled = true;
- return this.$.restAPI.deleteDraftComments('-is:open').then(() => {
- this._closeConfirmDeleteOverlay();
- this._reload();
- });
- }
-
- _closeConfirmDeleteOverlay() {
- this.$.confirmDeleteOverlay.close();
- }
-
- _computeDraftsLink() {
- return Gerrit.Nav.getUrlForSearchQuery('has:draft -is:open');
- }
-
- _createChangeTap(e) {
- this.$.destinationDialog.open();
- }
-
- _handleDestinationConfirm(e) {
- this.$.commandsDialog.branch = e.detail.branch;
- this.$.commandsDialog.open();
- }
+ _showNewUserHelp: {
+ type: Boolean,
+ value: false,
+ },
+ };
}
- customElements.define(GrDashboardView.is, GrDashboardView);
-})();
+ static get observers() {
+ return [
+ '_paramsChanged(params.*)',
+ ];
+ }
+
+ get options() {
+ return this.listChangesOptionsToHex(
+ this.ListChangesOption.LABELS,
+ this.ListChangesOption.DETAILED_ACCOUNTS,
+ this.ListChangesOption.REVIEWED
+ );
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._loadPreferences();
+ }
+
+ _loadPreferences() {
+ return this.$.restAPI.getLoggedIn().then(loggedIn => {
+ if (loggedIn) {
+ this.$.restAPI.getPreferences().then(preferences => {
+ this.preferences = preferences;
+ });
+ } else {
+ this.preferences = {};
+ }
+ });
+ }
+
+ _getProjectDashboard(project, dashboard) {
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
+ return this.$.restAPI.getDashboard(
+ project, dashboard, errFn).then(response => {
+ if (!response) {
+ return;
+ }
+ return {
+ title: response.title,
+ sections: response.sections.map(section => {
+ const suffix = response.foreach ? ' ' + response.foreach : '';
+ return {
+ name: section.name,
+ query: (section.query + suffix).replace(
+ PROJECT_PLACEHOLDER_PATTERN, project),
+ };
+ }),
+ };
+ });
+ }
+
+ _computeTitle(user) {
+ if (!user || user === 'self') {
+ return 'My Reviews';
+ }
+ return 'Dashboard for ' + user;
+ }
+
+ _isViewActive(params) {
+ return params.view === Gerrit.Nav.View.DASHBOARD;
+ }
+
+ _paramsChanged(paramsChangeRecord) {
+ const params = paramsChangeRecord.base;
+
+ if (!this._isViewActive(params)) {
+ return Promise.resolve();
+ }
+
+ return this._reload();
+ }
+
+ /**
+ * Reloads the element.
+ *
+ * @return {Promise<!Object>}
+ */
+ _reload() {
+ this._loading = true;
+ const {project, dashboard, title, user, sections} = this.params;
+ const dashboardPromise = project ?
+ this._getProjectDashboard(project, dashboard) :
+ Promise.resolve(Gerrit.Nav.getUserDashboard(
+ user,
+ sections,
+ title || this._computeTitle(user)));
+
+ const checkForNewUser = !project && user === 'self';
+ return dashboardPromise
+ .then(res => {
+ if (res && res.title) {
+ this.fire('title-change', {title: res.title});
+ }
+ return this._fetchDashboardChanges(res, checkForNewUser);
+ })
+ .then(() => {
+ this._maybeShowDraftsBanner();
+ this.$.reporting.dashboardDisplayed();
+ })
+ .catch(err => {
+ this.fire('title-change', {
+ title: title || this._computeTitle(user),
+ });
+ console.warn(err);
+ })
+ .then(() => { this._loading = false; });
+ }
+
+ /**
+ * Fetches the changes for each dashboard section and sets this._results
+ * with the response.
+ *
+ * @param {!Object} res
+ * @param {boolean} checkForNewUser
+ * @return {Promise}
+ */
+ _fetchDashboardChanges(res, checkForNewUser) {
+ if (!res) { return Promise.resolve(); }
+
+ const queries = res.sections
+ .map(section => (section.suffixForDashboard ?
+ section.query + ' ' + section.suffixForDashboard :
+ section.query));
+
+ if (checkForNewUser) {
+ queries.push('owner:self limit:1');
+ }
+
+ return this.$.restAPI.getChanges(null, queries, null, this.options)
+ .then(changes => {
+ if (checkForNewUser) {
+ // Last set of results is not meant for dashboard display.
+ const lastResultSet = changes.pop();
+ this._showNewUserHelp = lastResultSet.length == 0;
+ }
+ this._results = changes.map((results, i) => {
+ return {
+ name: res.sections[i].name,
+ countLabel: this._computeSectionCountLabel(results),
+ query: res.sections[i].query,
+ results,
+ isOutgoing: res.sections[i].isOutgoing,
+ };
+ }).filter((section, i) => i < res.sections.length && (
+ !res.sections[i].hideIfEmpty ||
+ section.results.length));
+ });
+ }
+
+ _computeSectionCountLabel(changes) {
+ if (!changes || !changes.length || changes.length == 0) {
+ return '';
+ }
+ const more = changes[changes.length - 1]._more_changes;
+ const numChanges = changes.length;
+ const andMore = more ? ' and more' : '';
+ return `(${numChanges}${andMore})`;
+ }
+
+ _computeUserHeaderClass(params) {
+ if (!params || !!params.project || !params.user ||
+ params.user === 'self') {
+ return 'hide';
+ }
+ return '';
+ }
+
+ _handleToggleStar(e) {
+ this.$.restAPI.saveChangeStarred(e.detail.change._number,
+ e.detail.starred);
+ }
+
+ _handleToggleReviewed(e) {
+ this.$.restAPI.saveChangeReviewed(e.detail.change._number,
+ e.detail.reviewed);
+ }
+
+ /**
+ * Banner is shown if a user is on their own dashboard and they have draft
+ * comments on closed changes.
+ */
+ _maybeShowDraftsBanner() {
+ this._showDraftsBanner = false;
+ if (!(this.params.user === 'self')) { return; }
+
+ const draftSection = this._results
+ .find(section => section.query === 'has:draft');
+ if (!draftSection || !draftSection.results.length) { return; }
+
+ const closedChanges = draftSection.results
+ .filter(change => !this.changeIsOpen(change));
+ if (!closedChanges.length) { return; }
+
+ this._showDraftsBanner = true;
+ }
+
+ _computeBannerClass(show) {
+ return show ? '' : 'hide';
+ }
+
+ _handleOpenDeleteDialog() {
+ this.$.confirmDeleteOverlay.open();
+ }
+
+ _handleConfirmDelete() {
+ this.$.confirmDeleteDialog.disabled = true;
+ return this.$.restAPI.deleteDraftComments('-is:open').then(() => {
+ this._closeConfirmDeleteOverlay();
+ this._reload();
+ });
+ }
+
+ _closeConfirmDeleteOverlay() {
+ this.$.confirmDeleteOverlay.close();
+ }
+
+ _computeDraftsLink() {
+ return Gerrit.Nav.getUrlForSearchQuery('has:draft -is:open');
+ }
+
+ _createChangeTap(e) {
+ this.$.destinationDialog.open();
+ }
+
+ _handleDestinationConfirm(e) {
+ this.$.commandsDialog.branch = e.detail.branch;
+ this.$.commandsDialog.open();
+ }
+}
+
+customElements.define(GrDashboardView.is, GrDashboardView);
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js
new file mode 100644
index 0000000..07d638c
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js
@@ -0,0 +1,97 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ }
+ .loading {
+ color: var(--deemphasized-text-color);
+ padding: var(--spacing-l);
+ }
+ gr-change-list {
+ width: 100%;
+ }
+ gr-user-header {
+ border-bottom: 1px solid var(--border-color);
+ }
+ .banner {
+ align-items: center;
+ background-color: var(--comment-background-color);
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ padding: var(--spacing-xs) var(--spacing-l);
+ }
+ .banner gr-button {
+ --gr-button: {
+ color: var(--primary-text-color);
+ }
+ }
+ .hide {
+ display: none;
+ }
+ #emptyOutgoing {
+ display: block;
+ }
+ @media only screen and (max-width: 50em) {
+ .loading {
+ padding: 0 var(--spacing-l);
+ }
+ }
+ </style>
+ <div class\$="banner [[_computeBannerClass(_showDraftsBanner)]]">
+ <div>
+ You have draft comments on closed changes.
+ <a href\$="[[_computeDraftsLink(_showDraftsBanner)]]" target="_blank">(view all)</a>
+ </div>
+ <div>
+ <gr-button class="delete" link="" on-click="_handleOpenDeleteDialog">Delete All</gr-button>
+ </div>
+ </div>
+ <div class="loading" hidden\$="[[!_loading]]">Loading...</div>
+ <div hidden\$="[[_loading]]" hidden="">
+ <gr-user-header user-id="[[params.user]]" class\$="[[_computeUserHeaderClass(params)]]"></gr-user-header>
+ <gr-change-list show-star="" show-reviewed-state="" account="[[account]]" preferences="[[preferences]]" selected-index="{{viewState.selectedChangeIndex}}" sections="[[_results]]" on-toggle-star="_handleToggleStar" on-toggle-reviewed="_handleToggleReviewed">
+ <div id="emptyOutgoing" slot="empty-outgoing">
+ <template is="dom-if" if="[[_showNewUserHelp]]">
+ <gr-create-change-help on-create-tap="createChangeTap"></gr-create-change-help>
+ </template>
+ <template is="dom-if" if="[[!_showNewUserHelp]]">
+ No changes
+ </template>
+ </div>
+ </gr-change-list>
+ </div>
+ <gr-overlay id="confirmDeleteOverlay" with-backdrop="">
+ <gr-dialog id="confirmDeleteDialog" confirm-label="Delete" on-confirm="_handleConfirmDelete" on-cancel="_closeConfirmDeleteOverlay">
+ <div class="header" slot="header">
+ Delete comments
+ </div>
+ <div class="main" slot="main">
+ Are you sure you want to delete all your draft comments in closed changes? This action
+ cannot be undone.
+ </div>
+ </gr-dialog>
+ </gr-overlay>
+ <gr-create-destination-dialog id="destinationDialog" on-confirm="_handleDestinationConfirm"></gr-create-destination-dialog>
+ <gr-create-commands-dialog id="commandsDialog"></gr-create-commands-dialog>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ <gr-reporting id="reporting"></gr-reporting>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
index 3d83e99..8a0cfa0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-dashboard-view</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-dashboard-view.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,351 +30,352 @@
</template>
</test-fixture>
-<script>
- suite('gr-dashboard-view tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let paramsChangedPromise;
- let getChangesStub;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-dashboard-view.js';
+suite('gr-dashboard-view tests', () => {
+ let element;
+ let sandbox;
+ let paramsChangedPromise;
+ let getChangesStub;
- setup(() => {
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(false); },
- getAccountDetails() { return Promise.resolve({}); },
- getAccountStatus() { return Promise.resolve(false); },
- });
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- getChangesStub = sandbox.stub(element.$.restAPI, 'getChanges',
- (_, qs) => Promise.resolve(qs.map(() => [])));
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(false); },
+ getAccountDetails() { return Promise.resolve({}); },
+ getAccountStatus() { return Promise.resolve(false); },
+ });
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ getChangesStub = sandbox.stub(element.$.restAPI, 'getChanges',
+ (_, qs) => Promise.resolve(qs.map(() => [])));
- let resolver;
- paramsChangedPromise = new Promise(resolve => {
- resolver = resolve;
+ let resolver;
+ paramsChangedPromise = new Promise(resolve => {
+ resolver = resolve;
+ });
+ const paramsChanged = element._paramsChanged.bind(element);
+ sandbox.stub(element, '_paramsChanged', params => {
+ paramsChanged(params).then(() => resolver());
+ });
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('drafts banner functionality', () => {
+ suite('_maybeShowDraftsBanner', () => {
+ test('not dashboard/self', () => {
+ element.params = {user: 'notself'};
+ element._maybeShowDraftsBanner();
+ assert.isFalse(element._showDraftsBanner);
});
- const paramsChanged = element._paramsChanged.bind(element);
- sandbox.stub(element, '_paramsChanged', params => {
- paramsChanged(params).then(() => resolver());
+
+ test('no drafts at all', () => {
+ element.params = {user: 'self'};
+ element._results = [];
+ element._maybeShowDraftsBanner();
+ assert.isFalse(element._showDraftsBanner);
+ });
+
+ test('no drafts on open changes', () => {
+ element.params = {user: 'self'};
+ element._results = [{query: 'has:draft', results: [{status: '_'}]}];
+ sandbox.stub(element, 'changeIsOpen').returns(true);
+ element._maybeShowDraftsBanner();
+ assert.isFalse(element._showDraftsBanner);
+ });
+
+ test('no drafts on open changes', () => {
+ element.params = {user: 'self'};
+ element._results = [{query: 'has:draft', results: [{status: '_'}]}];
+ sandbox.stub(element, 'changeIsOpen').returns(false);
+ element._maybeShowDraftsBanner();
+ assert.isTrue(element._showDraftsBanner);
});
});
- teardown(() => {
- sandbox.restore();
+ test('_showDraftsBanner', () => {
+ element._showDraftsBanner = false;
+ flushAsynchronousOperations();
+ assert.isTrue(isHidden(element.shadowRoot
+ .querySelector('.banner')));
+
+ element._showDraftsBanner = true;
+ flushAsynchronousOperations();
+ assert.isFalse(isHidden(element.shadowRoot
+ .querySelector('.banner')));
});
- suite('drafts banner functionality', () => {
- suite('_maybeShowDraftsBanner', () => {
- test('not dashboard/self', () => {
- element.params = {user: 'notself'};
- element._maybeShowDraftsBanner();
- assert.isFalse(element._showDraftsBanner);
- });
+ test('delete tap opens dialog', () => {
+ sandbox.stub(element, '_handleOpenDeleteDialog');
+ element._showDraftsBanner = true;
+ flushAsynchronousOperations();
- test('no drafts at all', () => {
- element.params = {user: 'self'};
- element._results = [];
- element._maybeShowDraftsBanner();
- assert.isFalse(element._showDraftsBanner);
- });
-
- test('no drafts on open changes', () => {
- element.params = {user: 'self'};
- element._results = [{query: 'has:draft', results: [{status: '_'}]}];
- sandbox.stub(element, 'changeIsOpen').returns(true);
- element._maybeShowDraftsBanner();
- assert.isFalse(element._showDraftsBanner);
- });
-
- test('no drafts on open changes', () => {
- element.params = {user: 'self'};
- element._results = [{query: 'has:draft', results: [{status: '_'}]}];
- sandbox.stub(element, 'changeIsOpen').returns(false);
- element._maybeShowDraftsBanner();
- assert.isTrue(element._showDraftsBanner);
- });
- });
-
- test('_showDraftsBanner', () => {
- element._showDraftsBanner = false;
- flushAsynchronousOperations();
- assert.isTrue(isHidden(element.shadowRoot
- .querySelector('.banner')));
-
- element._showDraftsBanner = true;
- flushAsynchronousOperations();
- assert.isFalse(isHidden(element.shadowRoot
- .querySelector('.banner')));
- });
-
- test('delete tap opens dialog', () => {
- sandbox.stub(element, '_handleOpenDeleteDialog');
- element._showDraftsBanner = true;
- flushAsynchronousOperations();
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('.banner .delete'));
- assert.isTrue(element._handleOpenDeleteDialog.called);
- });
-
- test('delete comments flow', async () => {
- sandbox.spy(element, '_handleConfirmDelete');
- sandbox.stub(element, '_reload');
-
- // Set up control over timing of when RPC resolves.
- let deleteDraftCommentsPromiseResolver;
- const deleteDraftCommentsPromise = new Promise(resolve => {
- deleteDraftCommentsPromiseResolver = resolve;
- });
- sandbox.stub(element.$.restAPI, 'deleteDraftComments')
- .returns(deleteDraftCommentsPromise);
-
- // Open confirmation dialog and tap confirm button.
- await element.$.confirmDeleteOverlay.open();
- MockInteractions.tap(element.$.confirmDeleteDialog.$.confirm);
- flushAsynchronousOperations();
- assert.isTrue(element.$.restAPI.deleteDraftComments
- .calledWithExactly('-is:open'));
- assert.isTrue(element.$.confirmDeleteDialog.disabled);
- assert.equal(element._reload.callCount, 0);
-
- // Verify state after RPC resolves.
- deleteDraftCommentsPromiseResolver([]);
- await deleteDraftCommentsPromise;
- assert.equal(element._reload.callCount, 1);
- });
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.banner .delete'));
+ assert.isTrue(element._handleOpenDeleteDialog.called);
});
- test('_computeTitle', () => {
- assert.equal(element._computeTitle('self'), 'My Reviews');
- assert.equal(element._computeTitle('not self'), 'Dashboard for not self');
+ test('delete comments flow', async () => {
+ sandbox.spy(element, '_handleConfirmDelete');
+ sandbox.stub(element, '_reload');
+
+ // Set up control over timing of when RPC resolves.
+ let deleteDraftCommentsPromiseResolver;
+ const deleteDraftCommentsPromise = new Promise(resolve => {
+ deleteDraftCommentsPromiseResolver = resolve;
+ });
+ sandbox.stub(element.$.restAPI, 'deleteDraftComments')
+ .returns(deleteDraftCommentsPromise);
+
+ // Open confirmation dialog and tap confirm button.
+ await element.$.confirmDeleteOverlay.open();
+ MockInteractions.tap(element.$.confirmDeleteDialog.$.confirm);
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.restAPI.deleteDraftComments
+ .calledWithExactly('-is:open'));
+ assert.isTrue(element.$.confirmDeleteDialog.disabled);
+ assert.equal(element._reload.callCount, 0);
+
+ // Verify state after RPC resolves.
+ deleteDraftCommentsPromiseResolver([]);
+ await deleteDraftCommentsPromise;
+ assert.equal(element._reload.callCount, 1);
+ });
+ });
+
+ test('_computeTitle', () => {
+ assert.equal(element._computeTitle('self'), 'My Reviews');
+ assert.equal(element._computeTitle('not self'), 'Dashboard for not self');
+ });
+
+ suite('_computeSectionCountLabel', () => {
+ test('empty changes dont count label', () => {
+ assert.equal('', element._computeSectionCountLabel([]));
});
- suite('_computeSectionCountLabel', () => {
- test('empty changes dont count label', () => {
- assert.equal('', element._computeSectionCountLabel([]));
- });
-
- test('1 change', () => {
- assert.equal('(1)',
- element._computeSectionCountLabel(['1']));
- });
-
- test('2 changes', () => {
- assert.equal('(2)',
- element._computeSectionCountLabel(['1', '2']));
- });
-
- test('1 change and more', () => {
- assert.equal('(1 and more)',
- element._computeSectionCountLabel([{_more_changes: true}]));
- });
+ test('1 change', () => {
+ assert.equal('(1)',
+ element._computeSectionCountLabel(['1']));
});
- suite('_isViewActive', () => {
- test('nothing happens when user param is falsy', () => {
- element.params = {};
- flushAsynchronousOperations();
- assert.equal(getChangesStub.callCount, 0);
-
- element.params = {user: ''};
- flushAsynchronousOperations();
- assert.equal(getChangesStub.callCount, 0);
- });
-
- test('content is refreshed when user param is updated', () => {
- element.params = {
- view: Gerrit.Nav.View.DASHBOARD,
- user: 'self',
- };
- return paramsChangedPromise.then(() => {
- assert.equal(getChangesStub.callCount, 1);
- });
- });
+ test('2 changes', () => {
+ assert.equal('(2)',
+ element._computeSectionCountLabel(['1', '2']));
});
- suite('selfOnly sections', () => {
- test('viewing self dashboard includes selfOnly sections', () => {
- element.params = {
- view: Gerrit.Nav.View.DASHBOARD,
- sections: [
- {query: '1'},
- {query: '2', selfOnly: true},
- ],
- user: 'self',
- };
- return paramsChangedPromise.then(() => {
- assert.isTrue(
- getChangesStub.calledWith(
- null, ['1', '2', 'owner:self limit:1'], null, element.options));
- });
- });
+ test('1 change and more', () => {
+ assert.equal('(1 and more)',
+ element._computeSectionCountLabel([{_more_changes: true}]));
+ });
+ });
- test('viewing another user\'s dashboard omits selfOnly sections', () => {
- element.params = {
- view: Gerrit.Nav.View.DASHBOARD,
- sections: [
- {query: '1'},
- {query: '2', selfOnly: true},
- ],
- user: 'user',
- };
- return paramsChangedPromise.then(() => {
- assert.isTrue(
- getChangesStub.calledWith(
- null, ['1'], null, element.options));
- });
- });
+ suite('_isViewActive', () => {
+ test('nothing happens when user param is falsy', () => {
+ element.params = {};
+ flushAsynchronousOperations();
+ assert.equal(getChangesStub.callCount, 0);
+
+ element.params = {user: ''};
+ flushAsynchronousOperations();
+ assert.equal(getChangesStub.callCount, 0);
});
- test('suffixForDashboard is included in getChanges query', () => {
+ test('content is refreshed when user param is updated', () => {
+ element.params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ user: 'self',
+ };
+ return paramsChangedPromise.then(() => {
+ assert.equal(getChangesStub.callCount, 1);
+ });
+ });
+ });
+
+ suite('selfOnly sections', () => {
+ test('viewing self dashboard includes selfOnly sections', () => {
element.params = {
view: Gerrit.Nav.View.DASHBOARD,
sections: [
{query: '1'},
- {query: '2', suffixForDashboard: 'suffix'},
+ {query: '2', selfOnly: true},
],
+ user: 'self',
};
return paramsChangedPromise.then(() => {
- assert.isTrue(getChangesStub.calledOnce);
- assert.deepEqual(
- getChangesStub.firstCall.args,
- [null, ['1', '2 suffix'], null, element.options]);
+ assert.isTrue(
+ getChangesStub.calledWith(
+ null, ['1', '2', 'owner:self limit:1'], null, element.options));
});
});
- suite('_getProjectDashboard', () => {
- test('dashboard with foreach', () => {
- sandbox.stub(element.$.restAPI, 'getDashboard', () => Promise.resolve({
- title: 'title',
- foreach: 'foreach for ${project}',
- sections: [
- {name: 'section 1', query: 'query 1'},
- {name: 'section 2', query: '${project} query 2'},
- ],
- }));
- return element._getProjectDashboard('project', '').then(dashboard => {
- assert.deepEqual(
- dashboard,
- {
- title: 'title',
- sections: [
- {name: 'section 1', query: 'query 1 foreach for project'},
- {
- name: 'section 2',
- query: 'project query 2 foreach for project',
- },
- ],
- });
- });
- });
-
- test('dashboard without foreach', () => {
- sandbox.stub(element.$.restAPI, 'getDashboard', () => Promise.resolve({
- title: 'title',
- sections: [
- {name: 'section 1', query: 'query 1'},
- {name: 'section 2', query: '${project} query 2'},
- ],
- }));
- return element._getProjectDashboard('project', '').then(dashboard => {
- assert.deepEqual(
- dashboard,
- {
- title: 'title',
- sections: [
- {name: 'section 1', query: 'query 1'},
- {name: 'section 2', query: 'project query 2'},
- ],
- });
- });
- });
- });
-
- test('hideIfEmpty sections', () => {
- const sections = [
- {name: 'test1', query: 'test1', hideIfEmpty: true},
- {name: 'test2', query: 'test2', hideIfEmpty: true},
- ];
- getChangesStub.restore();
- sandbox.stub(element.$.restAPI, 'getChanges')
- .returns(Promise.resolve([[], ['nonempty']]));
-
- return element._fetchDashboardChanges({sections}, false).then(() => {
- assert.equal(element._results.length, 1);
- assert.equal(element._results[0].name, 'test2');
- });
- });
-
- test('preserve isOutgoing sections', () => {
- const sections = [
- {name: 'test1', query: 'test1', isOutgoing: true},
- {name: 'test2', query: 'test2'},
- ];
- getChangesStub.restore();
- sandbox.stub(element.$.restAPI, 'getChanges')
- .returns(Promise.resolve([[], []]));
-
- return element._fetchDashboardChanges({sections}, false).then(() => {
- assert.equal(element._results.length, 2);
- assert.isTrue(element._results[0].isOutgoing);
- assert.isNotOk(element._results[1].isOutgoing);
- });
- });
-
- test('_showNewUserHelp', () => {
- element._loading = false;
- element._showNewUserHelp = false;
- flushAsynchronousOperations();
-
- assert.equal(element.$.emptyOutgoing.textContent.trim(), 'No changes');
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-create-change-help'));
- element._showNewUserHelp = true;
- flushAsynchronousOperations();
-
- assert.notEqual(element.$.emptyOutgoing.textContent.trim(), 'No changes');
- assert.isOk(element.shadowRoot
- .querySelector('gr-create-change-help'));
- });
-
- test('_computeUserHeaderClass', () => {
- assert.equal(element._computeUserHeaderClass(undefined), 'hide');
- assert.equal(element._computeUserHeaderClass({}), 'hide');
- assert.equal(element._computeUserHeaderClass({user: 'self'}), 'hide');
- assert.equal(element._computeUserHeaderClass({user: 'user'}), '');
- assert.equal(
- element._computeUserHeaderClass({project: 'p', user: 'user'}),
- 'hide');
- });
-
- test('404 page', done => {
- const response = {status: 404};
- sandbox.stub(element.$.restAPI, 'getDashboard',
- async (project, dashboard, errFn) => {
- errFn(response);
- });
- element.addEventListener('page-error', e => {
- assert.strictEqual(e.detail.response, response);
- done();
- });
+ test('viewing another user\'s dashboard omits selfOnly sections', () => {
element.params = {
view: Gerrit.Nav.View.DASHBOARD,
- project: 'project',
- dashboard: 'dashboard',
- };
- });
-
- test('params change triggers dashboardDisplayed()', () => {
- sandbox.stub(element.$.reporting, 'dashboardDisplayed');
- element.params = {
- view: Gerrit.Nav.View.DASHBOARD,
- project: 'project',
- dashboard: 'dashboard',
+ sections: [
+ {query: '1'},
+ {query: '2', selfOnly: true},
+ ],
+ user: 'user',
};
return paramsChangedPromise.then(() => {
- assert.isTrue(element.$.reporting.dashboardDisplayed.calledOnce);
+ assert.isTrue(
+ getChangesStub.calledWith(
+ null, ['1'], null, element.options));
});
});
});
+
+ test('suffixForDashboard is included in getChanges query', () => {
+ element.params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ sections: [
+ {query: '1'},
+ {query: '2', suffixForDashboard: 'suffix'},
+ ],
+ };
+ return paramsChangedPromise.then(() => {
+ assert.isTrue(getChangesStub.calledOnce);
+ assert.deepEqual(
+ getChangesStub.firstCall.args,
+ [null, ['1', '2 suffix'], null, element.options]);
+ });
+ });
+
+ suite('_getProjectDashboard', () => {
+ test('dashboard with foreach', () => {
+ sandbox.stub(element.$.restAPI, 'getDashboard', () => Promise.resolve({
+ title: 'title',
+ foreach: 'foreach for ${project}',
+ sections: [
+ {name: 'section 1', query: 'query 1'},
+ {name: 'section 2', query: '${project} query 2'},
+ ],
+ }));
+ return element._getProjectDashboard('project', '').then(dashboard => {
+ assert.deepEqual(
+ dashboard,
+ {
+ title: 'title',
+ sections: [
+ {name: 'section 1', query: 'query 1 foreach for project'},
+ {
+ name: 'section 2',
+ query: 'project query 2 foreach for project',
+ },
+ ],
+ });
+ });
+ });
+
+ test('dashboard without foreach', () => {
+ sandbox.stub(element.$.restAPI, 'getDashboard', () => Promise.resolve({
+ title: 'title',
+ sections: [
+ {name: 'section 1', query: 'query 1'},
+ {name: 'section 2', query: '${project} query 2'},
+ ],
+ }));
+ return element._getProjectDashboard('project', '').then(dashboard => {
+ assert.deepEqual(
+ dashboard,
+ {
+ title: 'title',
+ sections: [
+ {name: 'section 1', query: 'query 1'},
+ {name: 'section 2', query: 'project query 2'},
+ ],
+ });
+ });
+ });
+ });
+
+ test('hideIfEmpty sections', () => {
+ const sections = [
+ {name: 'test1', query: 'test1', hideIfEmpty: true},
+ {name: 'test2', query: 'test2', hideIfEmpty: true},
+ ];
+ getChangesStub.restore();
+ sandbox.stub(element.$.restAPI, 'getChanges')
+ .returns(Promise.resolve([[], ['nonempty']]));
+
+ return element._fetchDashboardChanges({sections}, false).then(() => {
+ assert.equal(element._results.length, 1);
+ assert.equal(element._results[0].name, 'test2');
+ });
+ });
+
+ test('preserve isOutgoing sections', () => {
+ const sections = [
+ {name: 'test1', query: 'test1', isOutgoing: true},
+ {name: 'test2', query: 'test2'},
+ ];
+ getChangesStub.restore();
+ sandbox.stub(element.$.restAPI, 'getChanges')
+ .returns(Promise.resolve([[], []]));
+
+ return element._fetchDashboardChanges({sections}, false).then(() => {
+ assert.equal(element._results.length, 2);
+ assert.isTrue(element._results[0].isOutgoing);
+ assert.isNotOk(element._results[1].isOutgoing);
+ });
+ });
+
+ test('_showNewUserHelp', () => {
+ element._loading = false;
+ element._showNewUserHelp = false;
+ flushAsynchronousOperations();
+
+ assert.equal(element.$.emptyOutgoing.textContent.trim(), 'No changes');
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-create-change-help'));
+ element._showNewUserHelp = true;
+ flushAsynchronousOperations();
+
+ assert.notEqual(element.$.emptyOutgoing.textContent.trim(), 'No changes');
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-create-change-help'));
+ });
+
+ test('_computeUserHeaderClass', () => {
+ assert.equal(element._computeUserHeaderClass(undefined), 'hide');
+ assert.equal(element._computeUserHeaderClass({}), 'hide');
+ assert.equal(element._computeUserHeaderClass({user: 'self'}), 'hide');
+ assert.equal(element._computeUserHeaderClass({user: 'user'}), '');
+ assert.equal(
+ element._computeUserHeaderClass({project: 'p', user: 'user'}),
+ 'hide');
+ });
+
+ test('404 page', done => {
+ const response = {status: 404};
+ sandbox.stub(element.$.restAPI, 'getDashboard',
+ async (project, dashboard, errFn) => {
+ errFn(response);
+ });
+ element.addEventListener('page-error', e => {
+ assert.strictEqual(e.detail.response, response);
+ done();
+ });
+ element.params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ project: 'project',
+ dashboard: 'dashboard',
+ };
+ });
+
+ test('params change triggers dashboardDisplayed()', () => {
+ sandbox.stub(element.$.reporting, 'dashboardDisplayed');
+ element.params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ project: 'project',
+ dashboard: 'dashboard',
+ };
+ return paramsChangedPromise.then(() => {
+ assert.isTrue(element.$.reporting.dashboardDisplayed.calledOnce);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.html b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.html
deleted file mode 100644
index d445185..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../change-list/gr-change-list/gr-change-list.html">
-<link rel="import" href="../gr-create-change-help/gr-create-change-help.html">
-
-<dom-module id="gr-embed-dashboard">
- <template>
- <gr-change-list
- show-star
- account="[[account]]"
- preferences="[[preferences]]"
- sections="[[sections]]">
- <div id="emptyOutgoing" slot="empty-outgoing">
- <template is="dom-if" if="[[showNewUserHelp]]">
- <gr-create-change-help></gr-create-change-help>
- </template>
- <template is="dom-if" if="[[!showNewUserHelp]]">
- No changes
- </template>
- </div>
- </gr-change-list>
- </template>
- <script src="gr-embed-dashboard.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
index de0a56e..2523700 100644
--- a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
+++ b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
@@ -14,24 +14,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrEmbedDashboard extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-embed-dashboard'; }
+import '../gr-change-list/gr-change-list.js';
+import '../gr-create-change-help/gr-create-change-help.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-embed-dashboard_html.js';
- static get properties() {
- return {
- account: Object,
- sections: Array,
- preferences: Object,
- showNewUserHelp: Boolean,
- };
- }
+/** @extends Polymer.Element */
+class GrEmbedDashboard extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-embed-dashboard'; }
+
+ static get properties() {
+ return {
+ account: Object,
+ sections: Array,
+ preferences: Object,
+ showNewUserHelp: Boolean,
+ };
}
+}
- customElements.define(GrEmbedDashboard.is, GrEmbedDashboard);
-})();
+customElements.define(GrEmbedDashboard.is, GrEmbedDashboard);
diff --git a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard_html.js b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard_html.js
new file mode 100644
index 0000000..2c122f3
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard_html.js
@@ -0,0 +1,30 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <gr-change-list show-star="" account="[[account]]" preferences="[[preferences]]" sections="[[sections]]">
+ <div id="emptyOutgoing" slot="empty-outgoing">
+ <template is="dom-if" if="[[showNewUserHelp]]">
+ <gr-create-change-help></gr-create-change-help>
+ </template>
+ <template is="dom-if" if="[[!showNewUserHelp]]">
+ No changes
+ </template>
+ </div>
+ </gr-change-list>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html
deleted file mode 100644
index 5d4b8a3..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html
+++ /dev/null
@@ -1,45 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/dashboard-header-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-repo-header">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="dashboard-header-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <div class="info">
- <h1 class$="name">
- [[repo]]
- <hr/>
- </h1>
- <div>
- <span>Detail:</span> <a href$="[[_repoUrl]]">Repo settings</a>
- </div>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-repo-header.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
index c0e472a..e9a0387 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
@@ -14,35 +14,45 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrRepoHeader extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-repo-header'; }
+import '../../../styles/dashboard-header-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-repo-header_html.js';
- static get properties() {
- return {
- /** @type {?string} */
- repo: {
- type: String,
- observer: '_repoChanged',
- },
- /** @type {string|null} */
- _repoUrl: String,
- };
- }
+/** @extends Polymer.Element */
+class GrRepoHeader extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _repoChanged(repoName) {
- if (!repoName) {
- this._repoUrl = null;
- return;
- }
- this._repoUrl = Gerrit.Nav.getUrlForRepo(repoName);
- }
+ static get is() { return 'gr-repo-header'; }
+
+ static get properties() {
+ return {
+ /** @type {?string} */
+ repo: {
+ type: String,
+ observer: '_repoChanged',
+ },
+ /** @type {string|null} */
+ _repoUrl: String,
+ };
}
- customElements.define(GrRepoHeader.is, GrRepoHeader);
-})();
+ _repoChanged(repoName) {
+ if (!repoName) {
+ this._repoUrl = null;
+ return;
+ }
+ this._repoUrl = Gerrit.Nav.getUrlForRepo(repoName);
+ }
+}
+
+customElements.define(GrRepoHeader.is, GrRepoHeader);
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js
new file mode 100644
index 0000000..d1274ee
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js
@@ -0,0 +1,36 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="dashboard-header-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <div class="info">
+ <h1 class\$="name">
+ [[repo]]
+ <hr>
+ </h1>
+ <div>
+ <span>Detail:</span> <a href\$="[[_repoUrl]]">Repo settings</a>
+ </div>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html
index d9cd75e..a075870 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-header</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-repo-header.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,26 +30,27 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo-header tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-repo-header.js';
+suite('gr-repo-header tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('repoUrl reset once repo changed', () => {
- sandbox.stub(Gerrit.Nav, 'getUrlForRepo',
- repoName => `http://test.com/${repoName}`
- );
- assert.equal(element._repoUrl, undefined);
- element.repo = 'test';
- assert.equal(element._repoUrl, 'http://test.com/test');
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('repoUrl reset once repo changed', () => {
+ sandbox.stub(Gerrit.Nav, 'getUrlForRepo',
+ repoName => `http://test.com/${repoName}`
+ );
+ assert.equal(element._repoUrl, undefined);
+ element.repo = 'test';
+ assert.equal(element._repoUrl, 'http://test.com/test');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
deleted file mode 100644
index 8175849..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
+++ /dev/null
@@ -1,84 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="../../shared/gr-avatar/gr-avatar.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/dashboard-header-styles.html">
-
-<dom-module id="gr-user-header">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="dashboard-header-styles">
- .name {
- display: inline-block;
- }
- .name hr {
- width: 100%;
- }
- .status.hide,
- .name.hide,
- .dashboardLink.hide {
- display: none;
- }
- </style>
- <gr-avatar
- account="[[_accountDetails]]"
- image-size="100"
- aria-label="Account avatar"></gr-avatar>
- <div class="info">
- <h1 class="name">
- [[_computeDetail(_accountDetails, 'name')]]
- </h1>
- <hr/>
- <div class$="status [[_computeStatusClass(_accountDetails)]]">
- <span>Status:</span> [[_status]]
- </div>
- <div>
- <span>Email:</span>
- <a href="mailto:[[_computeDetail(_accountDetails, 'email')]]"><!--
- -->[[_computeDetail(_accountDetails, 'email')]]</a>
- </div>
- <div>
- <span>Joined:</span>
- <gr-date-formatter
- date-str="[[_computeDetail(_accountDetails, 'registered_on')]]">
- </gr-date-formatter>
- </div>
- <gr-endpoint-decorator name="user-header">
- <gr-endpoint-param name="accountDetails" value="[[_accountDetails]]">
- </gr-endpoint-param>
- <gr-endpoint-param name="loggedIn" value="[[loggedIn]]">
- </gr-endpoint-param>
- </gr-endpoint-decorator>
- </div>
- <div class="info">
- <div class$="[[_computeDashboardLinkClass(showDashboardLink, loggedIn)]]">
- <a href$="[[_computeDashboardUrl(_accountDetails)]]">View dashboard</a>
- </div>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-user-header.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
index 2fc8170..3fc4291 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
@@ -14,91 +14,104 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @extends Polymer.Element
- */
- class GrUserHeader extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-user-header'; }
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
+import '../../shared/gr-avatar/gr-avatar.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/dashboard-header-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-user-header_html.js';
- static get properties() {
- return {
+/**
+ * @extends Polymer.Element
+ */
+class GrUserHeader extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-user-header'; }
+
+ static get properties() {
+ return {
+ /** @type {?string} */
+ userId: {
+ type: String,
+ observer: '_accountChanged',
+ },
+
+ showDashboardLink: {
+ type: Boolean,
+ value: false,
+ },
+
+ loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+
+ /**
+ * @type {?{name: ?, email: ?, registered_on: ?}}
+ */
+ _accountDetails: {
+ type: Object,
+ value: null,
+ },
+
/** @type {?string} */
- userId: {
- type: String,
- observer: '_accountChanged',
- },
-
- showDashboardLink: {
- type: Boolean,
- value: false,
- },
-
- loggedIn: {
- type: Boolean,
- value: false,
- },
-
- /**
- * @type {?{name: ?, email: ?, registered_on: ?}}
- */
- _accountDetails: {
- type: Object,
- value: null,
- },
-
- /** @type {?string} */
- _status: {
- type: String,
- value: null,
- },
- };
- }
-
- _accountChanged(userId) {
- if (!userId) {
- this._accountDetails = null;
- this._status = null;
- return;
- }
-
- this.$.restAPI.getAccountDetails(userId).then(details => {
- this._accountDetails = details;
- });
- this.$.restAPI.getAccountStatus(userId).then(status => {
- this._status = status;
- });
- }
-
- _computeDisplayClass(status) {
- return status ? ' ' : 'hide';
- }
-
- _computeDetail(accountDetails, name) {
- return accountDetails ? accountDetails[name] : '';
- }
-
- _computeStatusClass(accountDetails) {
- return this._computeDetail(accountDetails, 'status') ? '' : 'hide';
- }
-
- _computeDashboardUrl(accountDetails) {
- if (!accountDetails) { return null; }
- const id = accountDetails._account_id;
- const email = accountDetails.email;
- if (!id && !email ) { return null; }
- return Gerrit.Nav.getUrlForUserDashboard(id ? id : email);
- }
-
- _computeDashboardLinkClass(showDashboardLink, loggedIn) {
- return showDashboardLink && loggedIn ?
- 'dashboardLink' : 'dashboardLink hide';
- }
+ _status: {
+ type: String,
+ value: null,
+ },
+ };
}
- customElements.define(GrUserHeader.is, GrUserHeader);
-})();
+ _accountChanged(userId) {
+ if (!userId) {
+ this._accountDetails = null;
+ this._status = null;
+ return;
+ }
+
+ this.$.restAPI.getAccountDetails(userId).then(details => {
+ this._accountDetails = details;
+ });
+ this.$.restAPI.getAccountStatus(userId).then(status => {
+ this._status = status;
+ });
+ }
+
+ _computeDisplayClass(status) {
+ return status ? ' ' : 'hide';
+ }
+
+ _computeDetail(accountDetails, name) {
+ return accountDetails ? accountDetails[name] : '';
+ }
+
+ _computeStatusClass(accountDetails) {
+ return this._computeDetail(accountDetails, 'status') ? '' : 'hide';
+ }
+
+ _computeDashboardUrl(accountDetails) {
+ if (!accountDetails) { return null; }
+ const id = accountDetails._account_id;
+ const email = accountDetails.email;
+ if (!id && !email ) { return null; }
+ return Gerrit.Nav.getUrlForUserDashboard(id ? id : email);
+ }
+
+ _computeDashboardLinkClass(showDashboardLink, loggedIn) {
+ return showDashboardLink && loggedIn ?
+ 'dashboardLink' : 'dashboardLink hide';
+ }
+}
+
+customElements.define(GrUserHeader.is, GrUserHeader);
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js
new file mode 100644
index 0000000..3de4277
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js
@@ -0,0 +1,68 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="dashboard-header-styles">
+ .name {
+ display: inline-block;
+ }
+ .name hr {
+ width: 100%;
+ }
+ .status.hide,
+ .name.hide,
+ .dashboardLink.hide {
+ display: none;
+ }
+ </style>
+ <gr-avatar account="[[_accountDetails]]" image-size="100" aria-label="Account avatar"></gr-avatar>
+ <div class="info">
+ <h1 class="name">
+ [[_computeDetail(_accountDetails, 'name')]]
+ </h1>
+ <hr>
+ <div class\$="status [[_computeStatusClass(_accountDetails)]]">
+ <span>Status:</span> [[_status]]
+ </div>
+ <div>
+ <span>Email:</span>
+ <a href="mailto:[[_computeDetail(_accountDetails, 'email')]]"><!--
+ -->[[_computeDetail(_accountDetails, 'email')]]</a>
+ </div>
+ <div>
+ <span>Joined:</span>
+ <gr-date-formatter date-str="[[_computeDetail(_accountDetails, 'registered_on')]]">
+ </gr-date-formatter>
+ </div>
+ <gr-endpoint-decorator name="user-header">
+ <gr-endpoint-param name="accountDetails" value="[[_accountDetails]]">
+ </gr-endpoint-param>
+ <gr-endpoint-param name="loggedIn" value="[[loggedIn]]">
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </div>
+ <div class="info">
+ <div class\$="[[_computeDashboardLinkClass(showDashboardLink, loggedIn)]]">
+ <a href\$="[[_computeDashboardUrl(_accountDetails)]]">View dashboard</a>
+ </div>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
index 26ebeb2..aa32a94 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-user-header</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-user-header.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,50 +30,51 @@
</template>
</test-fixture>
-<script>
- suite('gr-user-header tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-user-header.js';
+suite('gr-user-header tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
- teardown(() => { sandbox.restore(); });
+ teardown(() => { sandbox.restore(); });
- test('loads and clears account info', done => {
- sandbox.stub(element.$.restAPI, 'getAccountDetails')
- .returns(Promise.resolve({
- name: 'foo',
- email: 'bar',
- registered_on: '2015-03-12 18:32:08.000000000',
- }));
- sandbox.stub(element.$.restAPI, 'getAccountStatus')
- .returns(Promise.resolve('baz'));
+ test('loads and clears account info', done => {
+ sandbox.stub(element.$.restAPI, 'getAccountDetails')
+ .returns(Promise.resolve({
+ name: 'foo',
+ email: 'bar',
+ registered_on: '2015-03-12 18:32:08.000000000',
+ }));
+ sandbox.stub(element.$.restAPI, 'getAccountStatus')
+ .returns(Promise.resolve('baz'));
- element.userId = 'foo.bar@baz';
+ element.userId = 'foo.bar@baz';
+ flush(() => {
+ assert.isOk(element._accountDetails);
+ assert.isOk(element._status);
+
+ element.userId = null;
flush(() => {
- assert.isOk(element._accountDetails);
- assert.isOk(element._status);
+ flushAsynchronousOperations();
+ assert.isNull(element._accountDetails);
+ assert.isNull(element._status);
- element.userId = null;
- flush(() => {
- flushAsynchronousOperations();
- assert.isNull(element._accountDetails);
- assert.isNull(element._status);
-
- done();
- });
+ done();
});
});
-
- test('_computeDashboardLinkClass', () => {
- assert.include(element._computeDashboardLinkClass(false, false), 'hide');
- assert.include(element._computeDashboardLinkClass(true, false), 'hide');
- assert.include(element._computeDashboardLinkClass(false, true), 'hide');
- assert.notInclude(element._computeDashboardLinkClass(true, true), 'hide');
- });
});
+
+ test('_computeDashboardLinkClass', () => {
+ assert.include(element._computeDashboardLinkClass(false, false), 'hide');
+ assert.include(element._computeDashboardLinkClass(true, false), 'hide');
+ assert.include(element._computeDashboardLinkClass(false, true), 'hide');
+ assert.notInclude(element._computeDashboardLinkClass(true, true), 'hide');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
deleted file mode 100644
index 097b92c..0000000
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ /dev/null
@@ -1,269 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../admin/gr-create-change-dialog/gr-create-change-dialog.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html">
-<link rel="import" href="../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html">
-<link rel="import" href="../gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html">
-<link rel="import" href="../gr-confirm-move-dialog/gr-confirm-move-dialog.html">
-<link rel="import" href="../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html">
-<link rel="import" href="../gr-confirm-revert-dialog/gr-confirm-revert-dialog.html">
-<link rel="import" href="../gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.html">
-<link rel="import" href="../gr-confirm-submit-dialog/gr-confirm-submit-dialog.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-change-actions">
- <template>
- <style include="shared-styles">
- :host {
- display: flex;
- font-family: var(--font-family);
- }
- #actionLoadingMessage,
- #mainContent,
- section {
- display: flex;
- }
- #actionLoadingMessage,
- gr-button,
- gr-dropdown {
- /* px because don't have the same font size */
- margin-left: 8px;
- }
- #actionLoadingMessage {
- align-items: center;
- color: var(--deemphasized-text-color);
- }
- #confirmSubmitDialog .changeSubject {
- margin: var(--spacing-l);
- text-align: center;
- }
- iron-icon {
- color: inherit;
- margin-right: var(--spacing-xs);
- }
- #moreActions iron-icon {
- margin: 0;
- }
- #moreMessage,
- .hidden {
- display: none;
- }
- @media screen and (max-width: 50em) {
- #mainContent {
- flex-wrap: wrap;
- }
- gr-button {
- --gr-button: {
- padding: var(--spacing-m);
- white-space: nowrap;
- }
- }
- gr-button,
- gr-dropdown {
- margin: 0;
- }
- #actionLoadingMessage {
- margin: var(--spacing-m);
- text-align: center;
- }
- #moreMessage {
- display: inline;
- }
- }
- </style>
- <div id="mainContent">
- <span
- id="actionLoadingMessage"
- hidden$="[[!_actionLoadingMessage]]">
- [[_actionLoadingMessage]]</span>
- <section id="primaryActions"
- hidden$="[[_shouldHideActions(_topLevelActions.*, _loading)]]">
- <template
- is="dom-repeat"
- items="[[_topLevelPrimaryActions]]"
- as="action">
- <gr-button
- link
- title$="[[action.title]]"
- has-tooltip="[[_computeHasTooltip(action.title)]]"
- position-below="true"
- data-action-key$="[[action.__key]]"
- data-action-type$="[[action.__type]]"
- data-label$="[[action.label]]"
- disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
- on-click="_handleActionTap">
- <iron-icon class$="[[_computeHasIcon(action)]]" icon$="gr-icons:[[action.icon]]"></iron-icon>
- [[action.label]]
- </gr-button>
- </template>
- </section>
- <section id="secondaryActions"
- hidden$="[[_shouldHideActions(_topLevelActions.*, _loading)]]">
- <template
- is="dom-repeat"
- items="[[_topLevelSecondaryActions]]"
- as="action">
- <gr-button
- link
- title$="[[action.title]]"
- has-tooltip="[[_computeHasTooltip(action.title)]]"
- position-below="true"
- data-action-key$="[[action.__key]]"
- data-action-type$="[[action.__type]]"
- data-label$="[[action.label]]"
- disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
- on-click="_handleActionTap">
- <iron-icon class$="[[_computeHasIcon(action)]]" icon$="gr-icons:[[action.icon]]"></iron-icon>
- [[action.label]]
- </gr-button>
- </template>
- </section>
- <gr-button hidden$="[[!_loading]]" disabled>Loading actions...</gr-button>
- <gr-dropdown
- id="moreActions"
- link
- tabindex="0"
- vertical-offset="32"
- horizontal-align="right"
- on-tap-item="_handleOveflowItemTap"
- hidden$="[[_shouldHideActions(_menuActions.*, _loading)]]"
- disabled-ids="[[_disabledMenuActions]]"
- items="[[_menuActions]]">
- <iron-icon icon="gr-icons:more-vert"></iron-icon>
- <span id="moreMessage">More</span>
- </gr-dropdown>
- </div>
- <gr-overlay id="overlay" with-backdrop>
- <gr-confirm-rebase-dialog id="confirmRebase"
- class="confirmDialog"
- change-number="[[change._number]]"
- on-confirm="_handleRebaseConfirm"
- on-cancel="_handleConfirmDialogCancel"
- branch="[[change.branch]]"
- has-parent="[[hasParent]]"
- rebase-on-current="[[_revisionRebaseAction.rebaseOnCurrent]]"
- hidden></gr-confirm-rebase-dialog>
- <gr-confirm-cherrypick-dialog id="confirmCherrypick"
- class="confirmDialog"
- change-status="[[changeStatus]]"
- commit-message="[[commitMessage]]"
- commit-num="[[commitNum]]"
- on-confirm="_handleCherrypickConfirm"
- on-cancel="_handleConfirmDialogCancel"
- project="[[change.project]]"
- hidden></gr-confirm-cherrypick-dialog>
- <gr-confirm-cherrypick-conflict-dialog id="confirmCherrypickConflict"
- class="confirmDialog"
- on-confirm="_handleCherrypickConflictConfirm"
- on-cancel="_handleConfirmDialogCancel"
- hidden></gr-confirm-cherrypick-conflict-dialog>
- <gr-confirm-move-dialog id="confirmMove"
- class="confirmDialog"
- on-confirm="_handleMoveConfirm"
- on-cancel="_handleConfirmDialogCancel"
- project="[[change.project]]"
- hidden></gr-confirm-move-dialog>
- <gr-confirm-revert-dialog id="confirmRevertDialog"
- class="confirmDialog"
- on-confirm="_handleRevertDialogConfirm"
- on-cancel="_handleConfirmDialogCancel"
- hidden></gr-confirm-revert-dialog>
- <gr-confirm-revert-submission-dialog id="confirmRevertSubmissionDialog"
- class="confirmDialog"
- commit-message="[[commitMessage]]"
- on-confirm="_handleRevertSubmissionDialogConfirm"
- on-cancel="_handleConfirmDialogCancel"
- hidden></gr-confirm-revert-submission-dialog>
- <gr-confirm-abandon-dialog id="confirmAbandonDialog"
- class="confirmDialog"
- on-confirm="_handleAbandonDialogConfirm"
- on-cancel="_handleConfirmDialogCancel"
- hidden></gr-confirm-abandon-dialog>
- <gr-confirm-submit-dialog
- id="confirmSubmitDialog"
- class="confirmDialog"
- change="[[change]]"
- action="[[_revisionSubmitAction]]"
- on-cancel="_handleConfirmDialogCancel"
- on-confirm="_handleSubmitConfirm" hidden></gr-confirm-submit-dialog>
- <gr-dialog id="createFollowUpDialog"
- class="confirmDialog"
- confirm-label="Create"
- on-confirm="_handleCreateFollowUpChange"
- on-cancel="_handleCloseCreateFollowUpChange">
- <div class="header" slot="header">
- Create Follow-Up Change
- </div>
- <div class="main" slot="main">
- <gr-create-change-dialog
- id="createFollowUpChange"
- branch="[[change.branch]]"
- base-change="[[change.id]]"
- repo-name="[[change.project]]"
- private-by-default="[[privateByDefault]]"></gr-create-change-dialog>
- </div>
- </gr-dialog>
- <gr-dialog
- id="confirmDeleteDialog"
- class="confirmDialog"
- confirm-label="Delete"
- confirm-on-enter
- on-cancel="_handleConfirmDialogCancel"
- on-confirm="_handleDeleteConfirm">
- <div class="header" slot="header">
- Delete Change
- </div>
- <div class="main" slot="main">
- Do you really want to delete the change?
- </div>
- </gr-dialog>
- <gr-dialog
- id="confirmDeleteEditDialog"
- class="confirmDialog"
- confirm-label="Delete"
- confirm-on-enter
- on-cancel="_handleConfirmDialogCancel"
- on-confirm="_handleDeleteEditConfirm">
- <div class="header" slot="header">
- Delete Change Edit
- </div>
- <div class="main" slot="main">
- Do you really want to delete the edit?
- </div>
- </gr-dialog>
- </gr-overlay>
- <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- <gr-reporting id="reporting" category="change-actions"></gr-reporting>
- </template>
- <script src="gr-change-actions.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 12c6b58..a3db19f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -14,1613 +14,1637 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.';
- const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
- const ERR_REVISION_ACTIONS = 'Couldn’t load revision actions.';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '../../admin/gr-create-change-dialog/gr-create-change-dialog.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-dropdown/gr-dropdown.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js';
+import '../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js';
+import '../gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js';
+import '../gr-confirm-move-dialog/gr-confirm-move-dialog.js';
+import '../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js';
+import '../gr-confirm-revert-dialog/gr-confirm-revert-dialog.js';
+import '../gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js';
+import '../gr-confirm-submit-dialog/gr-confirm-submit-dialog.js';
+import '../../../styles/shared-styles.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-change-actions_html.js';
+
+const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.';
+const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
+const ERR_REVISION_ACTIONS = 'Couldn’t load revision actions.';
+/**
+ * @enum {string}
+ */
+const LabelStatus = {
/**
- * @enum {string}
+ * This label provides what is necessary for submission.
*/
- const LabelStatus = {
- /**
- * This label provides what is necessary for submission.
- */
- OK: 'OK',
- /**
- * This label prevents the change from being submitted.
- */
- REJECT: 'REJECT',
- /**
- * The label may be set, but it's neither necessary for submission
- * nor does it block submission if set.
- */
- MAY: 'MAY',
- /**
- * The label is required for submission, but has not been satisfied.
- */
- NEED: 'NEED',
- /**
- * The label is required for submission, but is impossible to complete.
- * The likely cause is access has not been granted correctly by the
- * project owner or site administrator.
- */
- IMPOSSIBLE: 'IMPOSSIBLE',
- OPTIONAL: 'OPTIONAL',
- };
+ OK: 'OK',
+ /**
+ * This label prevents the change from being submitted.
+ */
+ REJECT: 'REJECT',
+ /**
+ * The label may be set, but it's neither necessary for submission
+ * nor does it block submission if set.
+ */
+ MAY: 'MAY',
+ /**
+ * The label is required for submission, but has not been satisfied.
+ */
+ NEED: 'NEED',
+ /**
+ * The label is required for submission, but is impossible to complete.
+ * The likely cause is access has not been granted correctly by the
+ * project owner or site administrator.
+ */
+ IMPOSSIBLE: 'IMPOSSIBLE',
+ OPTIONAL: 'OPTIONAL',
+};
- const ChangeActions = {
- ABANDON: 'abandon',
- DELETE: '/',
- DELETE_EDIT: 'deleteEdit',
- EDIT: 'edit',
- FOLLOW_UP: 'followup',
- IGNORE: 'ignore',
- MOVE: 'move',
- PRIVATE: 'private',
- PRIVATE_DELETE: 'private.delete',
- PUBLISH_EDIT: 'publishEdit',
- REBASE_EDIT: 'rebaseEdit',
- RESTORE: 'restore',
- REVERT: 'revert',
- REVERT_SUBMISSION: 'revert_submission',
- REVIEWED: 'reviewed',
- STOP_EDIT: 'stopEdit',
- UNIGNORE: 'unignore',
- UNREVIEWED: 'unreviewed',
- WIP: 'wip',
- };
+const ChangeActions = {
+ ABANDON: 'abandon',
+ DELETE: '/',
+ DELETE_EDIT: 'deleteEdit',
+ EDIT: 'edit',
+ FOLLOW_UP: 'followup',
+ IGNORE: 'ignore',
+ MOVE: 'move',
+ PRIVATE: 'private',
+ PRIVATE_DELETE: 'private.delete',
+ PUBLISH_EDIT: 'publishEdit',
+ REBASE_EDIT: 'rebaseEdit',
+ RESTORE: 'restore',
+ REVERT: 'revert',
+ REVERT_SUBMISSION: 'revert_submission',
+ REVIEWED: 'reviewed',
+ STOP_EDIT: 'stopEdit',
+ UNIGNORE: 'unignore',
+ UNREVIEWED: 'unreviewed',
+ WIP: 'wip',
+};
- const RevisionActions = {
- CHERRYPICK: 'cherrypick',
- REBASE: 'rebase',
- SUBMIT: 'submit',
- DOWNLOAD: 'download',
- };
+const RevisionActions = {
+ CHERRYPICK: 'cherrypick',
+ REBASE: 'rebase',
+ SUBMIT: 'submit',
+ DOWNLOAD: 'download',
+};
- const ActionLoadingLabels = {
- abandon: 'Abandoning...',
- cherrypick: 'Cherry-picking...',
- delete: 'Deleting...',
- move: 'Moving..',
- rebase: 'Rebasing...',
- restore: 'Restoring...',
- revert: 'Reverting...',
- revert_submission: 'Reverting Submission...',
- submit: 'Submitting...',
- };
+const ActionLoadingLabels = {
+ abandon: 'Abandoning...',
+ cherrypick: 'Cherry-picking...',
+ delete: 'Deleting...',
+ move: 'Moving..',
+ rebase: 'Rebasing...',
+ restore: 'Restoring...',
+ revert: 'Reverting...',
+ revert_submission: 'Reverting Submission...',
+ submit: 'Submitting...',
+};
- const ActionType = {
- CHANGE: 'change',
- REVISION: 'revision',
- };
+const ActionType = {
+ CHANGE: 'change',
+ REVISION: 'revision',
+};
- const ADDITIONAL_ACTION_KEY_PREFIX = '__additionalAction_';
+const ADDITIONAL_ACTION_KEY_PREFIX = '__additionalAction_';
- const QUICK_APPROVE_ACTION = {
- __key: 'review',
- __type: 'change',
- enabled: true,
- key: 'review',
- label: 'Quick approve',
- method: 'POST',
- };
+const QUICK_APPROVE_ACTION = {
+ __key: 'review',
+ __type: 'change',
+ enabled: true,
+ key: 'review',
+ label: 'Quick approve',
+ method: 'POST',
+};
- const ActionPriority = {
- CHANGE: 2,
- DEFAULT: 0,
- PRIMARY: 3,
- REVIEW: -3,
- REVISION: 1,
- };
+const ActionPriority = {
+ CHANGE: 2,
+ DEFAULT: 0,
+ PRIMARY: 3,
+ REVIEW: -3,
+ REVISION: 1,
+};
- const DOWNLOAD_ACTION = {
- enabled: true,
- label: 'Download patch',
- title: 'Open download dialog',
- __key: 'download',
- __primary: false,
- __type: 'revision',
- };
+const DOWNLOAD_ACTION = {
+ enabled: true,
+ label: 'Download patch',
+ title: 'Open download dialog',
+ __key: 'download',
+ __primary: false,
+ __type: 'revision',
+};
- const REBASE_EDIT = {
- enabled: true,
- label: 'Rebase edit',
- title: 'Rebase change edit',
- __key: 'rebaseEdit',
- __primary: false,
- __type: 'change',
- method: 'POST',
- };
+const REBASE_EDIT = {
+ enabled: true,
+ label: 'Rebase edit',
+ title: 'Rebase change edit',
+ __key: 'rebaseEdit',
+ __primary: false,
+ __type: 'change',
+ method: 'POST',
+};
- const PUBLISH_EDIT = {
- enabled: true,
- label: 'Publish edit',
- title: 'Publish change edit',
- __key: 'publishEdit',
- __primary: false,
- __type: 'change',
- method: 'POST',
- };
+const PUBLISH_EDIT = {
+ enabled: true,
+ label: 'Publish edit',
+ title: 'Publish change edit',
+ __key: 'publishEdit',
+ __primary: false,
+ __type: 'change',
+ method: 'POST',
+};
- const DELETE_EDIT = {
- enabled: true,
- label: 'Delete edit',
- title: 'Delete change edit',
- __key: 'deleteEdit',
- __primary: false,
- __type: 'change',
- method: 'DELETE',
- };
+const DELETE_EDIT = {
+ enabled: true,
+ label: 'Delete edit',
+ title: 'Delete change edit',
+ __key: 'deleteEdit',
+ __primary: false,
+ __type: 'change',
+ method: 'DELETE',
+};
- const EDIT = {
- enabled: true,
- label: 'Edit',
- title: 'Edit this change',
- __key: 'edit',
- __primary: false,
- __type: 'change',
- };
+const EDIT = {
+ enabled: true,
+ label: 'Edit',
+ title: 'Edit this change',
+ __key: 'edit',
+ __primary: false,
+ __type: 'change',
+};
- const STOP_EDIT = {
- enabled: true,
- label: 'Stop editing',
- title: 'Stop editing this change',
- __key: 'stopEdit',
- __primary: false,
- __type: 'change',
- };
+const STOP_EDIT = {
+ enabled: true,
+ label: 'Stop editing',
+ title: 'Stop editing this change',
+ __key: 'stopEdit',
+ __primary: false,
+ __type: 'change',
+};
- // Set of keys that have icons. As more icons are added to gr-icons.html, this
- // set should be expanded.
- const ACTIONS_WITH_ICONS = new Set([
- ChangeActions.ABANDON,
- ChangeActions.DELETE_EDIT,
- ChangeActions.EDIT,
- ChangeActions.PUBLISH_EDIT,
- ChangeActions.REBASE_EDIT,
- ChangeActions.RESTORE,
- ChangeActions.REVERT,
- ChangeActions.REVERT_SUBMISSION,
- ChangeActions.STOP_EDIT,
- QUICK_APPROVE_ACTION.key,
- RevisionActions.REBASE,
- RevisionActions.SUBMIT,
- ]);
+// Set of keys that have icons. As more icons are added to gr-icons.html, this
+// set should be expanded.
+const ACTIONS_WITH_ICONS = new Set([
+ ChangeActions.ABANDON,
+ ChangeActions.DELETE_EDIT,
+ ChangeActions.EDIT,
+ ChangeActions.PUBLISH_EDIT,
+ ChangeActions.REBASE_EDIT,
+ ChangeActions.RESTORE,
+ ChangeActions.REVERT,
+ ChangeActions.REVERT_SUBMISSION,
+ ChangeActions.STOP_EDIT,
+ QUICK_APPROVE_ACTION.key,
+ RevisionActions.REBASE,
+ RevisionActions.SUBMIT,
+]);
- const AWAIT_CHANGE_ATTEMPTS = 5;
- const AWAIT_CHANGE_TIMEOUT_MS = 1000;
+const AWAIT_CHANGE_ATTEMPTS = 5;
+const AWAIT_CHANGE_TIMEOUT_MS = 1000;
- const REVERT_TYPES = {
- REVERT_SINGLE_CHANGE: 1,
- REVERT_SUBMISSION: 2,
- };
+const REVERT_TYPES = {
+ REVERT_SINGLE_CHANGE: 1,
+ REVERT_SUBMISSION: 2,
+};
- /* Revert submission is skipped as the normal revert dialog will now show
- the user a choice between reverting single change or an entire submission.
- Hence, a second button is not needed.
- */
- const SKIP_ACTION_KEYS = [ChangeActions.REVERT_SUBMISSION];
+/* Revert submission is skipped as the normal revert dialog will now show
+the user a choice between reverting single change or an entire submission.
+Hence, a second button is not needed.
+*/
+const SKIP_ACTION_KEYS = [ChangeActions.REVERT_SUBMISSION];
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrChangeActions extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-change-actions'; }
+ /**
+ * Fired when the change should be reloaded.
+ *
+ * @event reload-change
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * Fired when an action is tapped.
+ *
+ * @event custom-tap - naming pattern: <action key>-tap
*/
- class GrChangeActions extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-change-actions'; }
+
+ /**
+ * Fires to show an alert when a send is attempted on the non-latest patch.
+ *
+ * @event show-alert
+ */
+
+ /**
+ * Fires when a change action fails.
+ *
+ * @event show-error
+ */
+
+ constructor() {
+ super();
+ this.ActionType = ActionType;
+ this.ChangeActions = ChangeActions;
+ this.RevisionActions = RevisionActions;
+ }
+
+ static get properties() {
+ return {
/**
- * Fired when the change should be reloaded.
- *
- * @event reload-change
+ * @type {{
+ * _number: number,
+ * branch: string,
+ * id: string,
+ * project: string,
+ * subject: string,
+ * }}
*/
+ change: Object,
+ actions: {
+ type: Object,
+ value() { return {}; },
+ },
+ primaryActionKeys: {
+ type: Array,
+ value() {
+ return [
+ RevisionActions.SUBMIT,
+ ];
+ },
+ },
+ disableEdit: {
+ type: Boolean,
+ value: false,
+ },
+ _hasKnownChainState: {
+ type: Boolean,
+ value: false,
+ },
+ _hideQuickApproveAction: {
+ type: Boolean,
+ value: false,
+ },
+ changeNum: String,
+ changeStatus: String,
+ commitNum: String,
+ hasParent: {
+ type: Boolean,
+ observer: '_computeChainState',
+ },
+ latestPatchNum: String,
+ commitMessage: {
+ type: String,
+ value: '',
+ },
+ /** @type {?} */
+ revisionActions: {
+ type: Object,
+ notify: true,
+ value() { return {}; },
+ },
+ // If property binds directly to [[revisionActions.submit]] it is not
+ // updated when revisionActions doesn't contain submit action.
+ /** @type {?} */
+ _revisionSubmitAction: {
+ type: Object,
+ computed: '_getSubmitAction(revisionActions)',
+ },
+ // If property binds directly to [[revisionActions.rebase]] it is not
+ // updated when revisionActions doesn't contain rebase action.
+ /** @type {?} */
+ _revisionRebaseAction: {
+ type: Object,
+ computed: '_getRebaseAction(revisionActions)',
+ },
+ privateByDefault: String,
- /**
- * Fired when an action is tapped.
- *
- * @event custom-tap - naming pattern: <action key>-tap
- */
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _actionLoadingMessage: {
+ type: String,
+ value: '',
+ },
+ _allActionValues: {
+ type: Array,
+ computed: '_computeAllActions(actions.*, revisionActions.*,' +
+ 'primaryActionKeys.*, _additionalActions.*, change, ' +
+ '_actionPriorityOverrides.*)',
+ },
+ _topLevelActions: {
+ type: Array,
+ computed: '_computeTopLevelActions(_allActionValues.*, ' +
+ '_hiddenActions.*, _overflowActions.*)',
+ observer: '_filterPrimaryActions',
+ },
+ _topLevelPrimaryActions: Array,
+ _topLevelSecondaryActions: Array,
+ _menuActions: {
+ type: Array,
+ computed: '_computeMenuActions(_allActionValues.*, ' +
+ '_hiddenActions.*, _overflowActions.*)',
+ },
+ _overflowActions: {
+ type: Array,
+ value() {
+ const value = [
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.WIP,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.DELETE,
+ },
+ {
+ type: ActionType.REVISION,
+ key: RevisionActions.CHERRYPICK,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.MOVE,
+ },
+ {
+ type: ActionType.REVISION,
+ key: RevisionActions.DOWNLOAD,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.IGNORE,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.UNIGNORE,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.REVIEWED,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.UNREVIEWED,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.PRIVATE,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.PRIVATE_DELETE,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.FOLLOW_UP,
+ },
+ ];
+ return value;
+ },
+ },
+ _actionPriorityOverrides: {
+ type: Array,
+ value() { return []; },
+ },
+ _additionalActions: {
+ type: Array,
+ value() { return []; },
+ },
+ _hiddenActions: {
+ type: Array,
+ value() { return []; },
+ },
+ _disabledMenuActions: {
+ type: Array,
+ value() { return []; },
+ },
+ // editPatchsetLoaded == "does the current selected patch range have
+ // 'edit' as one of either basePatchNum or patchNum".
+ editPatchsetLoaded: {
+ type: Boolean,
+ value: false,
+ },
+ // editMode == "is edit mode enabled in the file list".
+ editMode: {
+ type: Boolean,
+ value: false,
+ },
+ editBasedOnCurrentPatchSet: {
+ type: Boolean,
+ value: true,
+ },
+ _revertChanges: Array,
+ };
+ }
- /**
- * Fires to show an alert when a send is attempted on the non-latest patch.
- *
- * @event show-alert
- */
+ static get observers() {
+ return [
+ '_actionsChanged(actions.*, revisionActions.*, _additionalActions.*)',
+ '_changeChanged(change)',
+ '_editStatusChanged(editMode, editPatchsetLoaded, ' +
+ 'editBasedOnCurrentPatchSet, disableEdit, actions.*, change.*)',
+ ];
+ }
- /**
- * Fires when a change action fails.
- *
- * @event show-error
- */
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('fullscreen-overlay-opened',
+ () => this._handleHideBackgroundContent());
+ this.addEventListener('fullscreen-overlay-closed',
+ () => this._handleShowBackgroundContent());
+ }
- constructor() {
- super();
- this.ActionType = ActionType;
- this.ChangeActions = ChangeActions;
- this.RevisionActions = RevisionActions;
+ /** @override */
+ ready() {
+ super.ready();
+ this.$.jsAPI.addElement(this.$.jsAPI.Element.CHANGE_ACTIONS, this);
+ this._handleLoadingComplete();
+ }
+
+ _getSubmitAction(revisionActions) {
+ return this._getRevisionAction(revisionActions, 'submit', null);
+ }
+
+ _getRebaseAction(revisionActions) {
+ return this._getRevisionAction(revisionActions, 'rebase', null);
+ }
+
+ _getRevisionAction(revisionActions, actionName, emptyActionValue) {
+ if (!revisionActions) {
+ return undefined;
+ }
+ if (revisionActions[actionName] === undefined) {
+ // Return null to fire an event when reveisionActions was loaded
+ // but doesn't contain actionName. undefined doesn't fire an event
+ return emptyActionValue;
+ }
+ return revisionActions[actionName];
+ }
+
+ reload() {
+ if (!this.changeNum || !this.latestPatchNum) {
+ return Promise.resolve();
}
- static get properties() {
- return {
- /**
- * @type {{
- * _number: number,
- * branch: string,
- * id: string,
- * project: string,
- * subject: string,
- * }}
- */
- change: Object,
- actions: {
- type: Object,
- value() { return {}; },
- },
- primaryActionKeys: {
- type: Array,
- value() {
- return [
- RevisionActions.SUBMIT,
- ];
- },
- },
- disableEdit: {
- type: Boolean,
- value: false,
- },
- _hasKnownChainState: {
- type: Boolean,
- value: false,
- },
- _hideQuickApproveAction: {
- type: Boolean,
- value: false,
- },
- changeNum: String,
- changeStatus: String,
- commitNum: String,
- hasParent: {
- type: Boolean,
- observer: '_computeChainState',
- },
- latestPatchNum: String,
- commitMessage: {
- type: String,
- value: '',
- },
- /** @type {?} */
- revisionActions: {
- type: Object,
- notify: true,
- value() { return {}; },
- },
- // If property binds directly to [[revisionActions.submit]] it is not
- // updated when revisionActions doesn't contain submit action.
- /** @type {?} */
- _revisionSubmitAction: {
- type: Object,
- computed: '_getSubmitAction(revisionActions)',
- },
- // If property binds directly to [[revisionActions.rebase]] it is not
- // updated when revisionActions doesn't contain rebase action.
- /** @type {?} */
- _revisionRebaseAction: {
- type: Object,
- computed: '_getRebaseAction(revisionActions)',
- },
- privateByDefault: String,
+ this._loading = true;
+ return this._getRevisionActions()
+ .then(revisionActions => {
+ if (!revisionActions) { return; }
- _loading: {
- type: Boolean,
- value: true,
- },
- _actionLoadingMessage: {
- type: String,
- value: '',
- },
- _allActionValues: {
- type: Array,
- computed: '_computeAllActions(actions.*, revisionActions.*,' +
- 'primaryActionKeys.*, _additionalActions.*, change, ' +
- '_actionPriorityOverrides.*)',
- },
- _topLevelActions: {
- type: Array,
- computed: '_computeTopLevelActions(_allActionValues.*, ' +
- '_hiddenActions.*, _overflowActions.*)',
- observer: '_filterPrimaryActions',
- },
- _topLevelPrimaryActions: Array,
- _topLevelSecondaryActions: Array,
- _menuActions: {
- type: Array,
- computed: '_computeMenuActions(_allActionValues.*, ' +
- '_hiddenActions.*, _overflowActions.*)',
- },
- _overflowActions: {
- type: Array,
- value() {
- const value = [
- {
- type: ActionType.CHANGE,
- key: ChangeActions.WIP,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.DELETE,
- },
- {
- type: ActionType.REVISION,
- key: RevisionActions.CHERRYPICK,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.MOVE,
- },
- {
- type: ActionType.REVISION,
- key: RevisionActions.DOWNLOAD,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.IGNORE,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.UNIGNORE,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.REVIEWED,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.UNREVIEWED,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.PRIVATE,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.PRIVATE_DELETE,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.FOLLOW_UP,
- },
- ];
- return value;
- },
- },
- _actionPriorityOverrides: {
- type: Array,
- value() { return []; },
- },
- _additionalActions: {
- type: Array,
- value() { return []; },
- },
- _hiddenActions: {
- type: Array,
- value() { return []; },
- },
- _disabledMenuActions: {
- type: Array,
- value() { return []; },
- },
- // editPatchsetLoaded == "does the current selected patch range have
- // 'edit' as one of either basePatchNum or patchNum".
- editPatchsetLoaded: {
- type: Boolean,
- value: false,
- },
- // editMode == "is edit mode enabled in the file list".
- editMode: {
- type: Boolean,
- value: false,
- },
- editBasedOnCurrentPatchSet: {
- type: Boolean,
- value: true,
- },
- _revertChanges: Array,
- };
- }
-
- static get observers() {
- return [
- '_actionsChanged(actions.*, revisionActions.*, _additionalActions.*)',
- '_changeChanged(change)',
- '_editStatusChanged(editMode, editPatchsetLoaded, ' +
- 'editBasedOnCurrentPatchSet, disableEdit, actions.*, change.*)',
- ];
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('fullscreen-overlay-opened',
- () => this._handleHideBackgroundContent());
- this.addEventListener('fullscreen-overlay-closed',
- () => this._handleShowBackgroundContent());
- }
-
- /** @override */
- ready() {
- super.ready();
- this.$.jsAPI.addElement(this.$.jsAPI.Element.CHANGE_ACTIONS, this);
- this._handleLoadingComplete();
- }
-
- _getSubmitAction(revisionActions) {
- return this._getRevisionAction(revisionActions, 'submit', null);
- }
-
- _getRebaseAction(revisionActions) {
- return this._getRevisionAction(revisionActions, 'rebase',
- {rebaseOnCurrent: null}
- );
- }
-
- _getRevisionAction(revisionActions, actionName, emptyActionValue) {
- if (!revisionActions) {
- return undefined;
- }
- if (revisionActions[actionName] === undefined) {
- // Return null to fire an event when reveisionActions was loaded
- // but doesn't contain actionName. undefined doesn't fire an event
- return emptyActionValue;
- }
- return revisionActions[actionName];
- }
-
- reload() {
- if (!this.changeNum || !this.latestPatchNum) {
- return Promise.resolve();
- }
-
- this._loading = true;
- return this._getRevisionActions()
- .then(revisionActions => {
- if (!revisionActions) { return; }
-
- this.revisionActions = this._updateRebaseAction(revisionActions);
- this._sendShowRevisionActions({
- change: this.change,
- revisionActions,
- });
- this._handleLoadingComplete();
- })
- .catch(err => {
- this.fire('show-alert', {message: ERR_REVISION_ACTIONS});
- this._loading = false;
- throw err;
+ this.revisionActions = revisionActions;
+ this._sendShowRevisionActions({
+ change: this.change,
+ revisionActions,
});
- }
+ this._handleLoadingComplete();
+ })
+ .catch(err => {
+ this.fire('show-alert', {message: ERR_REVISION_ACTIONS});
+ this._loading = false;
+ throw err;
+ });
+ }
- _handleLoadingComplete() {
- Gerrit.awaitPluginsLoaded().then(() => this._loading = false);
- }
+ _handleLoadingComplete() {
+ Gerrit.awaitPluginsLoaded().then(() => this._loading = false);
+ }
- _sendShowRevisionActions(detail) {
- this.$.jsAPI.handleEvent(
- this.$.jsAPI.EventType.SHOW_REVISION_ACTIONS,
- detail
- );
- }
+ _sendShowRevisionActions(detail) {
+ this.$.jsAPI.handleEvent(
+ this.$.jsAPI.EventType.SHOW_REVISION_ACTIONS,
+ detail
+ );
+ }
- _updateRebaseAction(revisionActions) {
- if (revisionActions && revisionActions.rebase) {
- revisionActions.rebase.rebaseOnCurrent =
- !!revisionActions.rebase.enabled;
- this._parentIsCurrent = !revisionActions.rebase.enabled;
- revisionActions.rebase.enabled = true;
- } else {
- this._parentIsCurrent = true;
- }
- return revisionActions;
- }
+ _changeChanged() {
+ this.reload();
+ }
- _changeChanged() {
- this.reload();
+ addActionButton(type, label) {
+ if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
+ throw Error(`Invalid action type: ${type}`);
}
+ const action = {
+ enabled: true,
+ label,
+ __type: type,
+ __key: ADDITIONAL_ACTION_KEY_PREFIX +
+ Math.random().toString(36)
+ .substr(2),
+ };
+ this.push('_additionalActions', action);
+ return action.__key;
+ }
- addActionButton(type, label) {
- if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
- throw Error(`Invalid action type: ${type}`);
- }
- const action = {
- enabled: true,
- label,
- __type: type,
- __key: ADDITIONAL_ACTION_KEY_PREFIX +
- Math.random().toString(36)
- .substr(2),
- };
- this.push('_additionalActions', action);
- return action.__key;
+ removeActionButton(key) {
+ const idx = this._indexOfActionButtonWithKey(key);
+ if (idx === -1) {
+ return;
}
+ this.splice('_additionalActions', idx, 1);
+ }
- removeActionButton(key) {
- const idx = this._indexOfActionButtonWithKey(key);
- if (idx === -1) {
- return;
- }
- this.splice('_additionalActions', idx, 1);
- }
+ setActionButtonProp(key, prop, value) {
+ this.set([
+ '_additionalActions',
+ this._indexOfActionButtonWithKey(key),
+ prop,
+ ], value);
+ }
- setActionButtonProp(key, prop, value) {
- this.set([
- '_additionalActions',
- this._indexOfActionButtonWithKey(key),
- prop,
- ], value);
+ setActionOverflow(type, key, overflow) {
+ if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
+ throw Error(`Invalid action type given: ${type}`);
}
-
- setActionOverflow(type, key, overflow) {
- if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
- throw Error(`Invalid action type given: ${type}`);
- }
- const index = this._getActionOverflowIndex(type, key);
- const action = {
- type,
- key,
- overflow,
- };
- if (!overflow && index !== -1) {
- this.splice('_overflowActions', index, 1);
- } else if (overflow) {
- this.push('_overflowActions', action);
- }
- }
-
- setActionPriority(type, key, priority) {
- if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
- throw Error(`Invalid action type given: ${type}`);
- }
- const index = this._actionPriorityOverrides
- .findIndex(action => action.type === type && action.key === key);
- const action = {
- type,
- key,
- priority,
- };
- if (index !== -1) {
- this.set('_actionPriorityOverrides', index, action);
- } else {
- this.push('_actionPriorityOverrides', action);
- }
- }
-
- setActionHidden(type, key, hidden) {
- if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
- throw Error(`Invalid action type given: ${type}`);
- }
-
- const idx = this._hiddenActions.indexOf(key);
- if (hidden && idx === -1) {
- this.push('_hiddenActions', key);
- } else if (!hidden && idx !== -1) {
- this.splice('_hiddenActions', idx, 1);
- }
- }
-
- getActionDetails(action) {
- if (this.revisionActions[action]) {
- return this.revisionActions[action];
- } else if (this.actions[action]) {
- return this.actions[action];
- }
- }
-
- _indexOfActionButtonWithKey(key) {
- for (let i = 0; i < this._additionalActions.length; i++) {
- if (this._additionalActions[i].__key === key) {
- return i;
- }
- }
- return -1;
- }
-
- _getRevisionActions() {
- return this.$.restAPI.getChangeRevisionActions(this.changeNum,
- this.latestPatchNum);
- }
-
- _shouldHideActions(actions, loading) {
- return loading || !actions || !actions.base || !actions.base.length;
- }
-
- _keyCount(changeRecord) {
- return Object.keys((changeRecord && changeRecord.base) || {}).length;
- }
-
- _actionsChanged(actionsChangeRecord, revisionActionsChangeRecord,
- additionalActionsChangeRecord) {
- // Polymer 2: check for undefined
- if ([
- actionsChangeRecord,
- revisionActionsChangeRecord,
- additionalActionsChangeRecord,
- ].some(arg => arg === undefined)) {
- return;
- }
-
- const additionalActions = (additionalActionsChangeRecord &&
- additionalActionsChangeRecord.base) || [];
- this.hidden = this._keyCount(actionsChangeRecord) === 0 &&
- this._keyCount(revisionActionsChangeRecord) === 0 &&
- additionalActions.length === 0;
- this._actionLoadingMessage = '';
- this._disabledMenuActions = [];
-
- const revisionActions = revisionActionsChangeRecord.base || {};
- if (Object.keys(revisionActions).length !== 0) {
- if (!revisionActions.download) {
- this.set('revisionActions.download', DOWNLOAD_ACTION);
- }
- }
- }
-
- /**
- * @param {string=} actionName
- */
- _deleteAndNotify(actionName) {
- if (this.actions && this.actions[actionName]) {
- delete this.actions[actionName];
- // We assign a fake value of 'false' to support Polymer 2
- // see https://github.com/Polymer/polymer/issues/2631
- this.notifyPath('actions.' + actionName, false);
- }
- }
-
- _editStatusChanged(editMode, editPatchsetLoaded,
- editBasedOnCurrentPatchSet, disableEdit) {
- // Polymer 2: check for undefined
- if ([
- editMode,
- editBasedOnCurrentPatchSet,
- disableEdit,
- ].some(arg => arg === undefined)) {
- return;
- }
-
- if (disableEdit) {
- this._deleteAndNotify('publishEdit');
- this._deleteAndNotify('rebaseEdit');
- this._deleteAndNotify('deleteEdit');
- this._deleteAndNotify('stopEdit');
- this._deleteAndNotify('edit');
- return;
- }
- if (this.actions && editPatchsetLoaded) {
- // Only show actions that mutate an edit if an actual edit patch set
- // is loaded.
- if (this.changeIsOpen(this.change)) {
- if (editBasedOnCurrentPatchSet) {
- if (!this.actions.publishEdit) {
- this.set('actions.publishEdit', PUBLISH_EDIT);
- }
- this._deleteAndNotify('rebaseEdit');
- } else {
- if (!this.actions.rebaseEdit) {
- this.set('actions.rebaseEdit', REBASE_EDIT);
- }
- this._deleteAndNotify('publishEdit');
- }
- }
- if (!this.actions.deleteEdit) {
- this.set('actions.deleteEdit', DELETE_EDIT);
- }
- } else {
- this._deleteAndNotify('publishEdit');
- this._deleteAndNotify('rebaseEdit');
- this._deleteAndNotify('deleteEdit');
- }
-
- if (this.actions && this.changeIsOpen(this.change)) {
- // Only show edit button if there is no edit patchset loaded and the
- // file list is not in edit mode.
- if (editPatchsetLoaded || editMode) {
- this._deleteAndNotify('edit');
- } else {
- if (!this.actions.edit) { this.set('actions.edit', EDIT); }
- }
- // Only show STOP_EDIT if edit mode is enabled, but no edit patch set
- // is loaded.
- if (editMode && !editPatchsetLoaded) {
- if (!this.actions.stopEdit) {
- this.set('actions.stopEdit', STOP_EDIT);
- }
- } else {
- this._deleteAndNotify('stopEdit');
- }
- } else {
- // Remove edit button.
- this._deleteAndNotify('edit');
- }
- }
-
- _getValuesFor(obj) {
- return Object.keys(obj).map(key => obj[key]);
- }
-
- _getLabelStatus(label) {
- if (label.approved) {
- return LabelStatus.OK;
- } else if (label.rejected) {
- return LabelStatus.REJECT;
- } else if (label.optional) {
- return LabelStatus.OPTIONAL;
- } else {
- return LabelStatus.NEED;
- }
- }
-
- /**
- * Get highest score for last missing permitted label for current change.
- * Returns null if no labels permitted or more than one label missing.
- *
- * @return {{label: string, score: string}|null}
- */
- _getTopMissingApproval() {
- if (!this.change ||
- !this.change.labels ||
- !this.change.permitted_labels) {
- return null;
- }
- let result;
- for (const label in this.change.labels) {
- if (!(label in this.change.permitted_labels)) {
- continue;
- }
- if (this.change.permitted_labels[label].length === 0) {
- continue;
- }
- const status = this._getLabelStatus(this.change.labels[label]);
- if (status === LabelStatus.NEED) {
- if (result) {
- // More than one label is missing, so it's unclear which to quick
- // approve, return null;
- return null;
- }
- result = label;
- } else if (status === LabelStatus.REJECT ||
- status === LabelStatus.IMPOSSIBLE) {
- return null;
- }
- }
- if (result) {
- const score = this.change.permitted_labels[result].slice(-1)[0];
- const maxScore =
- Object.keys(this.change.labels[result].values).slice(-1)[0];
- if (score === maxScore) {
- // Allow quick approve only for maximal score.
- return {
- label: result,
- score,
- };
- }
- }
- return null;
- }
-
- hideQuickApproveAction() {
- this._topLevelSecondaryActions =
- this._topLevelSecondaryActions
- .filter(sa => sa.key !== QUICK_APPROVE_ACTION.key);
- this._hideQuickApproveAction = true;
- }
-
- _getQuickApproveAction() {
- if (this._hideQuickApproveAction) {
- return null;
- }
- const approval = this._getTopMissingApproval();
- if (!approval) {
- return null;
- }
- const action = Object.assign({}, QUICK_APPROVE_ACTION);
- action.label = approval.label + approval.score;
- const review = {
- drafts: 'PUBLISH_ALL_REVISIONS',
- labels: {},
- };
- review.labels[approval.label] = approval.score;
- action.payload = review;
- return action;
- }
-
- _getActionValues(actionsChangeRecord, primariesChangeRecord,
- additionalActionsChangeRecord, type) {
- if (!actionsChangeRecord || !primariesChangeRecord) { return []; }
-
- const actions = actionsChangeRecord.base || {};
- const primaryActionKeys = primariesChangeRecord.base || [];
- const result = [];
- const values = this._getValuesFor(
- type === ActionType.CHANGE ? ChangeActions : RevisionActions);
- const pluginActions = [];
- Object.keys(actions).forEach(a => {
- actions[a].__key = a;
- actions[a].__type = type;
- actions[a].__primary = primaryActionKeys.includes(a);
- // Plugin actions always contain ~ in the key.
- if (a.indexOf('~') !== -1) {
- this._populateActionUrl(actions[a]);
- pluginActions.push(actions[a]);
- // Add server-side provided plugin actions to overflow menu.
- this._overflowActions.push({
- type,
- key: a,
- });
- return;
- } else if (!values.includes(a)) {
- return;
- }
- actions[a].label = this._getActionLabel(actions[a]);
-
- // Triggers a re-render by ensuring object inequality.
- result.push(Object.assign({}, actions[a]));
- });
-
- let additionalActions = (additionalActionsChangeRecord &&
- additionalActionsChangeRecord.base) || [];
- additionalActions = additionalActions
- .filter(a => a.__type === type)
- .map(a => {
- a.__primary = primaryActionKeys.includes(a.__key);
- // Triggers a re-render by ensuring object inequality.
- return Object.assign({}, a);
- });
- return result.concat(additionalActions).concat(pluginActions);
- }
-
- _populateActionUrl(action) {
- const patchNum =
- action.__type === ActionType.REVISION ? this.latestPatchNum : null;
- this.$.restAPI.getChangeActionURL(
- this.changeNum, patchNum, '/' + action.__key)
- .then(url => action.__url = url);
- }
-
- /**
- * Given a change action, return a display label that uses the appropriate
- * casing or includes explanatory details.
- */
- _getActionLabel(action) {
- if (action.label === 'Delete') {
- // This label is common within change and revision actions. Make it more
- // explicit to the user.
- return 'Delete change';
- } else if (action.label === 'WIP') {
- return 'Mark as work in progress';
- }
- // Otherwise, just map the name to sentence case.
- return this._toSentenceCase(action.label);
- }
-
- /**
- * Capitalize the first letter and lowecase all others.
- *
- * @param {string} s
- * @return {string}
- */
- _toSentenceCase(s) {
- if (!s.length) { return ''; }
- return s[0].toUpperCase() + s.slice(1).toLowerCase();
- }
-
- _computeLoadingLabel(action) {
- return ActionLoadingLabels[action] || 'Working...';
- }
-
- _canSubmitChange() {
- return this.$.jsAPI.canSubmitChange(this.change,
- this._getRevision(this.change, this.latestPatchNum));
- }
-
- _getRevision(change, patchNum) {
- for (const rev of Object.values(change.revisions)) {
- if (this.patchNumEquals(rev._number, patchNum)) {
- return rev;
- }
- }
- return null;
- }
-
- showRevertDialog() {
- const query = 'submissionid:' + this.change.submission_id;
- /* A chromium plugin expects that the modifyRevertMsg hook will only
- be called after the revert button is pressed, hence we populate the
- revert dialog after revert button is pressed. */
- this.$.restAPI.getChanges('', query)
- .then(changes => {
- this.$.confirmRevertDialog.populate(this.change,
- this.commitMessage, changes);
- this._showActionDialog(this.$.confirmRevertDialog);
- });
- }
-
- showRevertSubmissionDialog() {
- const query = 'submissionid:' + this.change.submission_id;
- this.$.restAPI.getChanges('', query)
- .then(changes => {
- this.$.confirmRevertSubmissionDialog.
- _populateRevertSubmissionMessage(
- this.commitMessage, this.change, changes);
- this._showActionDialog(this.$.confirmRevertSubmissionDialog);
- });
- }
-
- _handleActionTap(e) {
- e.preventDefault();
- let el = Polymer.dom(e).localTarget;
- while (el.tagName.toLowerCase() !== 'gr-button') {
- if (!el.parentElement) { return; }
- el = el.parentElement;
- }
-
- const key = el.getAttribute('data-action-key');
- if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) ||
- key.indexOf('~') !== -1) {
- this.fire(`${key}-tap`, {node: el});
- return;
- }
- const type = el.getAttribute('data-action-type');
- this._handleAction(type, key);
- }
-
- _handleOveflowItemTap(e) {
- e.preventDefault();
- const el = Polymer.dom(e).localTarget;
- const key = e.detail.action.__key;
- if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) ||
- key.indexOf('~') !== -1) {
- this.fire(`${key}-tap`, {node: el});
- return;
- }
- this._handleAction(e.detail.action.__type, e.detail.action.__key);
- }
-
- _handleAction(type, key) {
- this.$.reporting.reportInteraction(`${type}-${key}`);
- switch (type) {
- case ActionType.REVISION:
- this._handleRevisionAction(key);
- break;
- case ActionType.CHANGE:
- this._handleChangeAction(key);
- break;
- default:
- this._fireAction(this._prependSlash(key), this.actions[key], false);
- }
- }
-
- _handleChangeAction(key) {
- let action;
- switch (key) {
- case ChangeActions.REVERT:
- this.showRevertDialog();
- break;
- case ChangeActions.REVERT_SUBMISSION:
- this.showRevertSubmissionDialog();
- break;
- case ChangeActions.ABANDON:
- this._showActionDialog(this.$.confirmAbandonDialog);
- break;
- case QUICK_APPROVE_ACTION.key:
- action = this._allActionValues.find(o => o.key === key);
- this._fireAction(
- this._prependSlash(key), action, true, action.payload);
- break;
- case ChangeActions.EDIT:
- this._handleEditTap();
- break;
- case ChangeActions.STOP_EDIT:
- this._handleStopEditTap();
- break;
- case ChangeActions.DELETE:
- this._handleDeleteTap();
- break;
- case ChangeActions.DELETE_EDIT:
- this._handleDeleteEditTap();
- break;
- case ChangeActions.FOLLOW_UP:
- this._handleFollowUpTap();
- break;
- case ChangeActions.WIP:
- this._handleWipTap();
- break;
- case ChangeActions.MOVE:
- this._handleMoveTap();
- break;
- case ChangeActions.PUBLISH_EDIT:
- this._handlePublishEditTap();
- break;
- case ChangeActions.REBASE_EDIT:
- this._handleRebaseEditTap();
- break;
- default:
- this._fireAction(this._prependSlash(key), this.actions[key], false);
- }
- }
-
- _handleRevisionAction(key) {
- switch (key) {
- case RevisionActions.REBASE:
- this._showActionDialog(this.$.confirmRebase);
- this.$.confirmRebase.fetchRecentChanges();
- break;
- case RevisionActions.CHERRYPICK:
- this._handleCherrypickTap();
- break;
- case RevisionActions.DOWNLOAD:
- this._handleDownloadTap();
- break;
- case RevisionActions.SUBMIT:
- if (!this._canSubmitChange()) { return; }
- this._showActionDialog(this.$.confirmSubmitDialog);
- break;
- default:
- this._fireAction(this._prependSlash(key),
- this.revisionActions[key], true);
- }
- }
-
- _prependSlash(key) {
- return key === '/' ? key : `/${key}`;
- }
-
- /**
- * _hasKnownChainState set to true true if hasParent is defined (can be
- * either true or false). set to false otherwise.
- */
- _computeChainState(hasParent) {
- this._hasKnownChainState = true;
- }
-
- _calculateDisabled(action, hasKnownChainState) {
- if (action.__key === 'rebase' && hasKnownChainState === false) {
- return true;
- }
- return !action.enabled;
- }
-
- _handleConfirmDialogCancel() {
- this._hideAllDialogs();
- }
-
- _hideAllDialogs() {
- const dialogEls =
- Polymer.dom(this.root).querySelectorAll('.confirmDialog');
- for (const dialogEl of dialogEls) { dialogEl.hidden = true; }
- this.$.overlay.close();
- }
-
- _handleRebaseConfirm(e) {
- const el = this.$.confirmRebase;
- const payload = {base: e.detail.base};
- this.$.overlay.close();
- el.hidden = true;
- this._fireAction('/rebase', this.revisionActions.rebase, true, payload);
- }
-
- _handleCherrypickConfirm() {
- this._handleCherryPickRestApi(false);
- }
-
- _handleCherrypickConflictConfirm() {
- this._handleCherryPickRestApi(true);
- }
-
- _handleCherryPickRestApi(conflicts) {
- const el = this.$.confirmCherrypick;
- if (!el.branch) {
- this.fire('show-alert', {message: ERR_BRANCH_EMPTY});
- return;
- }
- if (!el.message) {
- this.fire('show-alert', {message: ERR_COMMIT_EMPTY});
- return;
- }
- this.$.overlay.close();
- el.hidden = true;
- this._fireAction(
- '/cherrypick',
- this.revisionActions.cherrypick,
- true,
- {
- destination: el.branch,
- base: el.baseCommit ? el.baseCommit : null,
- message: el.message,
- allow_conflicts: conflicts,
- }
- );
- }
-
- _handleMoveConfirm() {
- const el = this.$.confirmMove;
- if (!el.branch) {
- this.fire('show-alert', {message: ERR_BRANCH_EMPTY});
- return;
- }
- this.$.overlay.close();
- el.hidden = true;
- this._fireAction(
- '/move',
- this.actions.move,
- false,
- {
- destination_branch: el.branch,
- message: el.message,
- }
- );
- }
-
- _handleRevertDialogConfirm(e) {
- const revertType = e.detail.revertType;
- const message = e.detail.message;
- const el = this.$.confirmRevertDialog;
- this.$.overlay.close();
- el.hidden = true;
- switch (revertType) {
- case REVERT_TYPES.REVERT_SINGLE_CHANGE:
- this._fireAction('/revert', this.actions.revert, false,
- {message});
- break;
- case REVERT_TYPES.REVERT_SUBMISSION:
- this._fireAction('/revert_submission', this.actions.revert_submission,
- false, {message});
- break;
- default:
- console.error('invalid revert type');
- }
- }
-
- _handleRevertSubmissionDialogConfirm() {
- const el = this.$.confirmRevertSubmissionDialog;
- this.$.overlay.close();
- el.hidden = true;
- this._fireAction('/revert_submission', this.actions.revert_submission,
- false, {message: el.message});
- }
-
- _handleAbandonDialogConfirm() {
- const el = this.$.confirmAbandonDialog;
- this.$.overlay.close();
- el.hidden = true;
- this._fireAction('/abandon', this.actions.abandon, false,
- {message: el.message});
- }
-
- _handleCreateFollowUpChange() {
- this.$.createFollowUpChange.handleCreateChange();
- this._handleCloseCreateFollowUpChange();
- }
-
- _handleCloseCreateFollowUpChange() {
- this.$.overlay.close();
- }
-
- _handleDeleteConfirm() {
- this._fireAction('/', this.actions[ChangeActions.DELETE], false);
- }
-
- _handleDeleteEditConfirm() {
- this._hideAllDialogs();
-
- this._fireAction('/edit', this.actions.deleteEdit, false);
- }
-
- _handleSubmitConfirm() {
- if (!this._canSubmitChange()) { return; }
- this._hideAllDialogs();
- this._fireAction('/submit', this.revisionActions.submit, true);
- }
-
- _getActionOverflowIndex(type, key) {
- return this._overflowActions
- .findIndex(action => action.type === type && action.key === key);
- }
-
- _setLoadingOnButtonWithKey(type, key) {
- this._actionLoadingMessage = this._computeLoadingLabel(key);
- let buttonKey = key;
- // TODO(dhruvsri): clean this up later
- // If key is revert-submission, then button key should be 'revert'
- if (buttonKey === ChangeActions.REVERT_SUBMISSION) {
- // Revert submission button no longer exists
- buttonKey = ChangeActions.REVERT;
- }
-
- // If the action appears in the overflow menu.
- if (this._getActionOverflowIndex(type, buttonKey) !== -1) {
- this.push('_disabledMenuActions', buttonKey === '/' ? 'delete' :
- buttonKey);
- return function() {
- this._actionLoadingMessage = '';
- this._disabledMenuActions = [];
- }.bind(this);
- }
-
- // Otherwise it's a top-level action.
- const buttonEl = this.shadowRoot
- .querySelector(`[data-action-key="${buttonKey}"]`);
- buttonEl.setAttribute('loading', true);
- buttonEl.disabled = true;
- return function() {
- this._actionLoadingMessage = '';
- buttonEl.removeAttribute('loading');
- buttonEl.disabled = false;
- }.bind(this);
- }
-
- /**
- * @param {string} endpoint
- * @param {!Object|undefined} action
- * @param {boolean} revAction
- * @param {!Object|string=} opt_payload
- */
- _fireAction(endpoint, action, revAction, opt_payload) {
- const cleanupFn =
- this._setLoadingOnButtonWithKey(action.__type, action.__key);
-
- this._send(action.method, opt_payload, endpoint, revAction, cleanupFn,
- action).then(this._handleResponse.bind(this, action));
- }
-
- _showActionDialog(dialog) {
- this._hideAllDialogs();
-
- dialog.hidden = false;
- this.$.overlay.open().then(() => {
- if (dialog.resetFocus) {
- dialog.resetFocus();
- }
- });
- }
-
- // TODO(rmistry): Redo this after
- // https://bugs.chromium.org/p/gerrit/issues/detail?id=4671 is resolved.
- _setLabelValuesOnRevert(newChangeId) {
- const labels = this.$.jsAPI.getLabelValuesPostRevert(this.change);
- if (!labels) { return Promise.resolve(); }
- return this.$.restAPI.saveChangeReview(newChangeId, 'current', {labels});
- }
-
- _handleResponse(action, response) {
- if (!response) { return; }
- return this.$.restAPI.getResponseObject(response).then(obj => {
- switch (action.__key) {
- case ChangeActions.REVERT:
- this._waitForChangeReachable(obj._number)
- .then(() => this._setLabelValuesOnRevert(obj._number))
- .then(() => {
- Gerrit.Nav.navigateToChange(obj);
- });
- break;
- case RevisionActions.CHERRYPICK:
- this._waitForChangeReachable(obj._number).then(() => {
- Gerrit.Nav.navigateToChange(obj);
- });
- break;
- case ChangeActions.DELETE:
- if (action.__type === ActionType.CHANGE) {
- Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getUrlForRoot());
- }
- break;
- case ChangeActions.WIP:
- case ChangeActions.DELETE_EDIT:
- case ChangeActions.PUBLISH_EDIT:
- case ChangeActions.REBASE_EDIT:
- Gerrit.Nav.navigateToChange(this.change);
- break;
- case ChangeActions.REVERT_SUBMISSION:
- if (!obj.revert_changes || !obj.revert_changes.length) return;
- /* If there is only 1 change then gerrit will automatically
- redirect to that change */
- Gerrit.Nav.navigateToSearchQuery('topic: ' +
- obj.revert_changes[0].topic);
- break;
- default:
- this.dispatchEvent(new CustomEvent('reload-change',
- {detail: {action: action.__key}, bubbles: false}));
- break;
- }
- });
- }
-
- _handleShowRevertSubmissionChangesConfirm() {
- this._hideAllDialogs();
- }
-
- _handleResponseError(action, response, body) {
- if (action && action.__key === RevisionActions.CHERRYPICK) {
- if (response && response.status === 409 &&
- body && !body.allow_conflicts) {
- return this._showActionDialog(
- this.$.confirmCherrypickConflict);
- }
- }
- return response.text().then(errText => {
- this.fire('show-error',
- {message: `Could not perform action: ${errText}`});
- if (!errText.startsWith('Change is already up to date')) {
- throw Error(errText);
- }
- });
- }
-
- /**
- * @param {string} method
- * @param {string|!Object|undefined} payload
- * @param {string} actionEndpoint
- * @param {boolean} revisionAction
- * @param {?Function} cleanupFn
- * @param {!Object|undefined} action
- */
- _send(method, payload, actionEndpoint, revisionAction, cleanupFn, action) {
- const handleError = response => {
- cleanupFn.call(this);
- this._handleResponseError(action, response, payload);
- };
-
- return this.fetchChangeUpdates(this.change, this.$.restAPI)
- .then(result => {
- if (!result.isLatest) {
- this.fire('show-alert', {
- message: 'Cannot set label: a newer patch has been ' +
- 'uploaded to this change.',
- action: 'Reload',
- callback: () => {
- // Load the current change without any patch range.
- Gerrit.Nav.navigateToChange(this.change);
- },
- });
-
- // Because this is not a network error, call the cleanup function
- // but not the error handler.
- cleanupFn();
-
- return Promise.resolve();
- }
- const patchNum = revisionAction ? this.latestPatchNum : null;
- return this.$.restAPI.executeChangeAction(this.changeNum, method,
- actionEndpoint, patchNum, payload, handleError)
- .then(response => {
- cleanupFn.call(this);
- return response;
- });
- });
- }
-
- _handleAbandonTap() {
- this._showActionDialog(this.$.confirmAbandonDialog);
- }
-
- _handleCherrypickTap() {
- this.$.confirmCherrypick.branch = '';
- this._showActionDialog(this.$.confirmCherrypick);
- }
-
- _handleMoveTap() {
- this.$.confirmMove.branch = '';
- this.$.confirmMove.message = '';
- this._showActionDialog(this.$.confirmMove);
- }
-
- _handleDownloadTap() {
- this.fire('download-tap', null, {bubbles: false});
- }
-
- _handleDeleteTap() {
- this._showActionDialog(this.$.confirmDeleteDialog);
- }
-
- _handleDeleteEditTap() {
- this._showActionDialog(this.$.confirmDeleteEditDialog);
- }
-
- _handleFollowUpTap() {
- this._showActionDialog(this.$.createFollowUpDialog);
- }
-
- _handleWipTap() {
- this._fireAction('/wip', this.actions.wip, false);
- }
-
- _handlePublishEditTap() {
- this._fireAction('/edit:publish', this.actions.publishEdit, false);
- }
-
- _handleRebaseEditTap() {
- this._fireAction('/edit:rebase', this.actions.rebaseEdit, false);
- }
-
- _handleHideBackgroundContent() {
- this.$.mainContent.classList.add('overlayOpen');
- }
-
- _handleShowBackgroundContent() {
- this.$.mainContent.classList.remove('overlayOpen');
- }
-
- /**
- * Merge sources of change actions into a single ordered array of action
- * values.
- *
- * @param {!Array} changeActionsRecord
- * @param {!Array} revisionActionsRecord
- * @param {!Array} primariesRecord
- * @param {!Array} additionalActionsRecord
- * @param {!Object} change The change object.
- * @return {!Array}
- */
- _computeAllActions(changeActionsRecord, revisionActionsRecord,
- primariesRecord, additionalActionsRecord, change) {
- // Polymer 2: check for undefined
- if ([
- changeActionsRecord,
- revisionActionsRecord,
- primariesRecord,
- additionalActionsRecord,
- change,
- ].some(arg => arg === undefined)) {
- return [];
- }
-
- const revisionActionValues = this._getActionValues(revisionActionsRecord,
- primariesRecord, additionalActionsRecord, ActionType.REVISION);
- const changeActionValues = this._getActionValues(changeActionsRecord,
- primariesRecord, additionalActionsRecord, ActionType.CHANGE);
- const quickApprove = this._getQuickApproveAction();
- if (quickApprove) {
- changeActionValues.unshift(quickApprove);
- }
-
- return revisionActionValues
- .concat(changeActionValues)
- .sort(this._actionComparator.bind(this))
- .map(action => {
- if (ACTIONS_WITH_ICONS.has(action.__key)) {
- action.icon = action.__key;
- }
- return action;
- })
- .filter(action => !this._shouldSkipAction(action));
- }
-
- _getActionPriority(action) {
- if (action.__type && action.__key) {
- const overrideAction = this._actionPriorityOverrides
- .find(i => i.type === action.__type && i.key === action.__key);
-
- if (overrideAction !== undefined) {
- return overrideAction.priority;
- }
- }
- if (action.__key === 'review') {
- return ActionPriority.REVIEW;
- } else if (action.__primary) {
- return ActionPriority.PRIMARY;
- } else if (action.__type === ActionType.CHANGE) {
- return ActionPriority.CHANGE;
- } else if (action.__type === ActionType.REVISION) {
- return ActionPriority.REVISION;
- }
- return ActionPriority.DEFAULT;
- }
-
- /**
- * Sort comparator to define the order of change actions.
- */
- _actionComparator(actionA, actionB) {
- const priorityDelta = this._getActionPriority(actionA) -
- this._getActionPriority(actionB);
- // Sort by the button label if same priority.
- if (priorityDelta === 0) {
- return actionA.label > actionB.label ? 1 : -1;
- } else {
- return priorityDelta;
- }
- }
-
- _shouldSkipAction(action) {
- return SKIP_ACTION_KEYS.includes(action.__key);
- }
-
- _computeTopLevelActions(actionRecord, hiddenActionsRecord) {
- const hiddenActions = hiddenActionsRecord.base || [];
- return actionRecord.base.filter(a => {
- const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1;
- return !(overflow || hiddenActions.includes(a.__key));
- });
- }
-
- _filterPrimaryActions(_topLevelActions) {
- this._topLevelPrimaryActions = _topLevelActions.filter(action =>
- action.__primary);
- this._topLevelSecondaryActions = _topLevelActions.filter(action =>
- !action.__primary);
- }
-
- _computeMenuActions(actionRecord, hiddenActionsRecord) {
- const hiddenActions = hiddenActionsRecord.base || [];
- return actionRecord.base.filter(a => {
- const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1;
- return overflow && !hiddenActions.includes(a.__key);
- }).map(action => {
- let key = action.__key;
- if (key === '/') { key = 'delete'; }
- return {
- name: action.label,
- id: `${key}-${action.__type}`,
- action,
- tooltip: action.title,
- };
- });
- }
-
- /**
- * Occasionally, a change created by a change action is not yet knwon to the
- * API for a brief time. Wait for the given change number to be recognized.
- *
- * Returns a promise that resolves with true if a request is recognized, or
- * false if the change was never recognized after all attempts.
- *
- * @param {number} changeNum
- * @return {Promise<boolean>}
- */
- _waitForChangeReachable(changeNum) {
- let attempsRemaining = AWAIT_CHANGE_ATTEMPTS;
- return new Promise(resolve => {
- const check = () => {
- attempsRemaining--;
- // Pass a no-op error handler to avoid the "not found" error toast.
- this.$.restAPI.getChange(changeNum, () => {}).then(response => {
- // If the response is 404, the response will be undefined.
- if (response) {
- resolve(true);
- return;
- }
-
- if (attempsRemaining) {
- this.async(check, AWAIT_CHANGE_TIMEOUT_MS);
- } else {
- resolve(false);
- }
- });
- };
- check();
- });
- }
-
- _handleEditTap() {
- this.dispatchEvent(new CustomEvent('edit-tap', {bubbles: false}));
- }
-
- _handleStopEditTap() {
- this.dispatchEvent(new CustomEvent('stop-edit-tap', {bubbles: false}));
- }
-
- _computeHasTooltip(title) {
- return !!title;
- }
-
- _computeHasIcon(action) {
- return action.icon ? '' : 'hidden';
+ const index = this._getActionOverflowIndex(type, key);
+ const action = {
+ type,
+ key,
+ overflow,
+ };
+ if (!overflow && index !== -1) {
+ this.splice('_overflowActions', index, 1);
+ } else if (overflow) {
+ this.push('_overflowActions', action);
}
}
- customElements.define(GrChangeActions.is, GrChangeActions);
-})();
+ setActionPriority(type, key, priority) {
+ if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
+ throw Error(`Invalid action type given: ${type}`);
+ }
+ const index = this._actionPriorityOverrides
+ .findIndex(action => action.type === type && action.key === key);
+ const action = {
+ type,
+ key,
+ priority,
+ };
+ if (index !== -1) {
+ this.set('_actionPriorityOverrides', index, action);
+ } else {
+ this.push('_actionPriorityOverrides', action);
+ }
+ }
+
+ setActionHidden(type, key, hidden) {
+ if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
+ throw Error(`Invalid action type given: ${type}`);
+ }
+
+ const idx = this._hiddenActions.indexOf(key);
+ if (hidden && idx === -1) {
+ this.push('_hiddenActions', key);
+ } else if (!hidden && idx !== -1) {
+ this.splice('_hiddenActions', idx, 1);
+ }
+ }
+
+ getActionDetails(action) {
+ if (this.revisionActions[action]) {
+ return this.revisionActions[action];
+ } else if (this.actions[action]) {
+ return this.actions[action];
+ }
+ }
+
+ _indexOfActionButtonWithKey(key) {
+ for (let i = 0; i < this._additionalActions.length; i++) {
+ if (this._additionalActions[i].__key === key) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ _getRevisionActions() {
+ return this.$.restAPI.getChangeRevisionActions(this.changeNum,
+ this.latestPatchNum);
+ }
+
+ _shouldHideActions(actions, loading) {
+ return loading || !actions || !actions.base || !actions.base.length;
+ }
+
+ _keyCount(changeRecord) {
+ return Object.keys((changeRecord && changeRecord.base) || {}).length;
+ }
+
+ _actionsChanged(actionsChangeRecord, revisionActionsChangeRecord,
+ additionalActionsChangeRecord) {
+ // Polymer 2: check for undefined
+ if ([
+ actionsChangeRecord,
+ revisionActionsChangeRecord,
+ additionalActionsChangeRecord,
+ ].some(arg => arg === undefined)) {
+ return;
+ }
+
+ const additionalActions = (additionalActionsChangeRecord &&
+ additionalActionsChangeRecord.base) || [];
+ this.hidden = this._keyCount(actionsChangeRecord) === 0 &&
+ this._keyCount(revisionActionsChangeRecord) === 0 &&
+ additionalActions.length === 0;
+ this._actionLoadingMessage = '';
+ this._disabledMenuActions = [];
+
+ const revisionActions = revisionActionsChangeRecord.base || {};
+ if (Object.keys(revisionActions).length !== 0) {
+ if (!revisionActions.download) {
+ this.set('revisionActions.download', DOWNLOAD_ACTION);
+ }
+ }
+ }
+
+ /**
+ * @param {string=} actionName
+ */
+ _deleteAndNotify(actionName) {
+ if (this.actions && this.actions[actionName]) {
+ delete this.actions[actionName];
+ // We assign a fake value of 'false' to support Polymer 2
+ // see https://github.com/Polymer/polymer/issues/2631
+ this.notifyPath('actions.' + actionName, false);
+ }
+ }
+
+ _editStatusChanged(editMode, editPatchsetLoaded,
+ editBasedOnCurrentPatchSet, disableEdit) {
+ // Polymer 2: check for undefined
+ if ([
+ editMode,
+ editBasedOnCurrentPatchSet,
+ disableEdit,
+ ].some(arg => arg === undefined)) {
+ return;
+ }
+
+ if (disableEdit) {
+ this._deleteAndNotify('publishEdit');
+ this._deleteAndNotify('rebaseEdit');
+ this._deleteAndNotify('deleteEdit');
+ this._deleteAndNotify('stopEdit');
+ this._deleteAndNotify('edit');
+ return;
+ }
+ if (this.actions && editPatchsetLoaded) {
+ // Only show actions that mutate an edit if an actual edit patch set
+ // is loaded.
+ if (this.changeIsOpen(this.change)) {
+ if (editBasedOnCurrentPatchSet) {
+ if (!this.actions.publishEdit) {
+ this.set('actions.publishEdit', PUBLISH_EDIT);
+ }
+ this._deleteAndNotify('rebaseEdit');
+ } else {
+ if (!this.actions.rebaseEdit) {
+ this.set('actions.rebaseEdit', REBASE_EDIT);
+ }
+ this._deleteAndNotify('publishEdit');
+ }
+ }
+ if (!this.actions.deleteEdit) {
+ this.set('actions.deleteEdit', DELETE_EDIT);
+ }
+ } else {
+ this._deleteAndNotify('publishEdit');
+ this._deleteAndNotify('rebaseEdit');
+ this._deleteAndNotify('deleteEdit');
+ }
+
+ if (this.actions && this.changeIsOpen(this.change)) {
+ // Only show edit button if there is no edit patchset loaded and the
+ // file list is not in edit mode.
+ if (editPatchsetLoaded || editMode) {
+ this._deleteAndNotify('edit');
+ } else {
+ if (!this.actions.edit) { this.set('actions.edit', EDIT); }
+ }
+ // Only show STOP_EDIT if edit mode is enabled, but no edit patch set
+ // is loaded.
+ if (editMode && !editPatchsetLoaded) {
+ if (!this.actions.stopEdit) {
+ this.set('actions.stopEdit', STOP_EDIT);
+ }
+ } else {
+ this._deleteAndNotify('stopEdit');
+ }
+ } else {
+ // Remove edit button.
+ this._deleteAndNotify('edit');
+ }
+ }
+
+ _getValuesFor(obj) {
+ return Object.keys(obj).map(key => obj[key]);
+ }
+
+ _getLabelStatus(label) {
+ if (label.approved) {
+ return LabelStatus.OK;
+ } else if (label.rejected) {
+ return LabelStatus.REJECT;
+ } else if (label.optional) {
+ return LabelStatus.OPTIONAL;
+ } else {
+ return LabelStatus.NEED;
+ }
+ }
+
+ /**
+ * Get highest score for last missing permitted label for current change.
+ * Returns null if no labels permitted or more than one label missing.
+ *
+ * @return {{label: string, score: string}|null}
+ */
+ _getTopMissingApproval() {
+ if (!this.change ||
+ !this.change.labels ||
+ !this.change.permitted_labels) {
+ return null;
+ }
+ let result;
+ for (const label in this.change.labels) {
+ if (!(label in this.change.permitted_labels)) {
+ continue;
+ }
+ if (this.change.permitted_labels[label].length === 0) {
+ continue;
+ }
+ const status = this._getLabelStatus(this.change.labels[label]);
+ if (status === LabelStatus.NEED) {
+ if (result) {
+ // More than one label is missing, so it's unclear which to quick
+ // approve, return null;
+ return null;
+ }
+ result = label;
+ } else if (status === LabelStatus.REJECT ||
+ status === LabelStatus.IMPOSSIBLE) {
+ return null;
+ }
+ }
+ if (result) {
+ const score = this.change.permitted_labels[result].slice(-1)[0];
+ const maxScore =
+ Object.keys(this.change.labels[result].values).slice(-1)[0];
+ if (score === maxScore) {
+ // Allow quick approve only for maximal score.
+ return {
+ label: result,
+ score,
+ };
+ }
+ }
+ return null;
+ }
+
+ hideQuickApproveAction() {
+ this._topLevelSecondaryActions =
+ this._topLevelSecondaryActions
+ .filter(sa => sa.key !== QUICK_APPROVE_ACTION.key);
+ this._hideQuickApproveAction = true;
+ }
+
+ _getQuickApproveAction() {
+ if (this._hideQuickApproveAction) {
+ return null;
+ }
+ const approval = this._getTopMissingApproval();
+ if (!approval) {
+ return null;
+ }
+ const action = Object.assign({}, QUICK_APPROVE_ACTION);
+ action.label = approval.label + approval.score;
+ const review = {
+ drafts: 'PUBLISH_ALL_REVISIONS',
+ labels: {},
+ };
+ review.labels[approval.label] = approval.score;
+ action.payload = review;
+ return action;
+ }
+
+ _getActionValues(actionsChangeRecord, primariesChangeRecord,
+ additionalActionsChangeRecord, type) {
+ if (!actionsChangeRecord || !primariesChangeRecord) { return []; }
+
+ const actions = actionsChangeRecord.base || {};
+ const primaryActionKeys = primariesChangeRecord.base || [];
+ const result = [];
+ const values = this._getValuesFor(
+ type === ActionType.CHANGE ? ChangeActions : RevisionActions);
+ const pluginActions = [];
+ Object.keys(actions).forEach(a => {
+ actions[a].__key = a;
+ actions[a].__type = type;
+ actions[a].__primary = primaryActionKeys.includes(a);
+ // Plugin actions always contain ~ in the key.
+ if (a.indexOf('~') !== -1) {
+ this._populateActionUrl(actions[a]);
+ pluginActions.push(actions[a]);
+ // Add server-side provided plugin actions to overflow menu.
+ this._overflowActions.push({
+ type,
+ key: a,
+ });
+ return;
+ } else if (!values.includes(a)) {
+ return;
+ }
+ actions[a].label = this._getActionLabel(actions[a]);
+
+ // Triggers a re-render by ensuring object inequality.
+ result.push(Object.assign({}, actions[a]));
+ });
+
+ let additionalActions = (additionalActionsChangeRecord &&
+ additionalActionsChangeRecord.base) || [];
+ additionalActions = additionalActions
+ .filter(a => a.__type === type)
+ .map(a => {
+ a.__primary = primaryActionKeys.includes(a.__key);
+ // Triggers a re-render by ensuring object inequality.
+ return Object.assign({}, a);
+ });
+ return result.concat(additionalActions).concat(pluginActions);
+ }
+
+ _populateActionUrl(action) {
+ const patchNum =
+ action.__type === ActionType.REVISION ? this.latestPatchNum : null;
+ this.$.restAPI.getChangeActionURL(
+ this.changeNum, patchNum, '/' + action.__key)
+ .then(url => action.__url = url);
+ }
+
+ /**
+ * Given a change action, return a display label that uses the appropriate
+ * casing or includes explanatory details.
+ */
+ _getActionLabel(action) {
+ if (action.label === 'Delete') {
+ // This label is common within change and revision actions. Make it more
+ // explicit to the user.
+ return 'Delete change';
+ } else if (action.label === 'WIP') {
+ return 'Mark as work in progress';
+ }
+ // Otherwise, just map the name to sentence case.
+ return this._toSentenceCase(action.label);
+ }
+
+ /**
+ * Capitalize the first letter and lowecase all others.
+ *
+ * @param {string} s
+ * @return {string}
+ */
+ _toSentenceCase(s) {
+ if (!s.length) { return ''; }
+ return s[0].toUpperCase() + s.slice(1).toLowerCase();
+ }
+
+ _computeLoadingLabel(action) {
+ return ActionLoadingLabels[action] || 'Working...';
+ }
+
+ _canSubmitChange() {
+ return this.$.jsAPI.canSubmitChange(this.change,
+ this._getRevision(this.change, this.latestPatchNum));
+ }
+
+ _getRevision(change, patchNum) {
+ for (const rev of Object.values(change.revisions)) {
+ if (this.patchNumEquals(rev._number, patchNum)) {
+ return rev;
+ }
+ }
+ return null;
+ }
+
+ showRevertDialog() {
+ // The search is still broken if there is a " in the topic.
+ const query = `submissionid: "${this.change.submission_id}"`;
+ /* A chromium plugin expects that the modifyRevertMsg hook will only
+ be called after the revert button is pressed, hence we populate the
+ revert dialog after revert button is pressed. */
+ this.$.restAPI.getChanges('', query)
+ .then(changes => {
+ this.$.confirmRevertDialog.populate(this.change,
+ this.commitMessage, changes);
+ this._showActionDialog(this.$.confirmRevertDialog);
+ });
+ }
+
+ showRevertSubmissionDialog() {
+ const query = 'submissionid:' + this.change.submission_id;
+ this.$.restAPI.getChanges('', query)
+ .then(changes => {
+ this.$.confirmRevertSubmissionDialog.
+ _populateRevertSubmissionMessage(
+ this.commitMessage, this.change, changes);
+ this._showActionDialog(this.$.confirmRevertSubmissionDialog);
+ });
+ }
+
+ _handleActionTap(e) {
+ e.preventDefault();
+ let el = dom(e).localTarget;
+ while (el.tagName.toLowerCase() !== 'gr-button') {
+ if (!el.parentElement) { return; }
+ el = el.parentElement;
+ }
+
+ const key = el.getAttribute('data-action-key');
+ if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) ||
+ key.indexOf('~') !== -1) {
+ this.fire(`${key}-tap`, {node: el});
+ return;
+ }
+ const type = el.getAttribute('data-action-type');
+ this._handleAction(type, key);
+ }
+
+ _handleOveflowItemTap(e) {
+ e.preventDefault();
+ const el = dom(e).localTarget;
+ const key = e.detail.action.__key;
+ if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) ||
+ key.indexOf('~') !== -1) {
+ this.fire(`${key}-tap`, {node: el});
+ return;
+ }
+ this._handleAction(e.detail.action.__type, e.detail.action.__key);
+ }
+
+ _handleAction(type, key) {
+ this.$.reporting.reportInteraction(`${type}-${key}`);
+ switch (type) {
+ case ActionType.REVISION:
+ this._handleRevisionAction(key);
+ break;
+ case ActionType.CHANGE:
+ this._handleChangeAction(key);
+ break;
+ default:
+ this._fireAction(this._prependSlash(key), this.actions[key], false);
+ }
+ }
+
+ _handleChangeAction(key) {
+ let action;
+ switch (key) {
+ case ChangeActions.REVERT:
+ this.showRevertDialog();
+ break;
+ case ChangeActions.REVERT_SUBMISSION:
+ this.showRevertSubmissionDialog();
+ break;
+ case ChangeActions.ABANDON:
+ this._showActionDialog(this.$.confirmAbandonDialog);
+ break;
+ case QUICK_APPROVE_ACTION.key:
+ action = this._allActionValues.find(o => o.key === key);
+ this._fireAction(
+ this._prependSlash(key), action, true, action.payload);
+ break;
+ case ChangeActions.EDIT:
+ this._handleEditTap();
+ break;
+ case ChangeActions.STOP_EDIT:
+ this._handleStopEditTap();
+ break;
+ case ChangeActions.DELETE:
+ this._handleDeleteTap();
+ break;
+ case ChangeActions.DELETE_EDIT:
+ this._handleDeleteEditTap();
+ break;
+ case ChangeActions.FOLLOW_UP:
+ this._handleFollowUpTap();
+ break;
+ case ChangeActions.WIP:
+ this._handleWipTap();
+ break;
+ case ChangeActions.MOVE:
+ this._handleMoveTap();
+ break;
+ case ChangeActions.PUBLISH_EDIT:
+ this._handlePublishEditTap();
+ break;
+ case ChangeActions.REBASE_EDIT:
+ this._handleRebaseEditTap();
+ break;
+ default:
+ this._fireAction(this._prependSlash(key), this.actions[key], false);
+ }
+ }
+
+ _handleRevisionAction(key) {
+ switch (key) {
+ case RevisionActions.REBASE:
+ this._showActionDialog(this.$.confirmRebase);
+ this.$.confirmRebase.fetchRecentChanges();
+ break;
+ case RevisionActions.CHERRYPICK:
+ this._handleCherrypickTap();
+ break;
+ case RevisionActions.DOWNLOAD:
+ this._handleDownloadTap();
+ break;
+ case RevisionActions.SUBMIT:
+ if (!this._canSubmitChange()) { return; }
+ this._showActionDialog(this.$.confirmSubmitDialog);
+ break;
+ default:
+ this._fireAction(this._prependSlash(key),
+ this.revisionActions[key], true);
+ }
+ }
+
+ _prependSlash(key) {
+ return key === '/' ? key : `/${key}`;
+ }
+
+ /**
+ * _hasKnownChainState set to true true if hasParent is defined (can be
+ * either true or false). set to false otherwise.
+ */
+ _computeChainState(hasParent) {
+ this._hasKnownChainState = true;
+ }
+
+ _calculateDisabled(action, hasKnownChainState) {
+ if (action.__key === 'rebase') {
+ // Rebase button is only disabled when change has no parent(s).
+ return hasKnownChainState === false;
+ }
+ return !action.enabled;
+ }
+
+ _handleConfirmDialogCancel() {
+ this._hideAllDialogs();
+ }
+
+ _hideAllDialogs() {
+ const dialogEls =
+ dom(this.root).querySelectorAll('.confirmDialog');
+ for (const dialogEl of dialogEls) { dialogEl.hidden = true; }
+ this.$.overlay.close();
+ }
+
+ _handleRebaseConfirm(e) {
+ const el = this.$.confirmRebase;
+ const payload = {base: e.detail.base};
+ this.$.overlay.close();
+ el.hidden = true;
+ this._fireAction('/rebase', this.revisionActions.rebase, true, payload);
+ }
+
+ _handleCherrypickConfirm() {
+ this._handleCherryPickRestApi(false);
+ }
+
+ _handleCherrypickConflictConfirm() {
+ this._handleCherryPickRestApi(true);
+ }
+
+ _handleCherryPickRestApi(conflicts) {
+ const el = this.$.confirmCherrypick;
+ if (!el.branch) {
+ this.fire('show-alert', {message: ERR_BRANCH_EMPTY});
+ return;
+ }
+ if (!el.message) {
+ this.fire('show-alert', {message: ERR_COMMIT_EMPTY});
+ return;
+ }
+ this.$.overlay.close();
+ el.hidden = true;
+ this._fireAction(
+ '/cherrypick',
+ this.revisionActions.cherrypick,
+ true,
+ {
+ destination: el.branch,
+ base: el.baseCommit ? el.baseCommit : null,
+ message: el.message,
+ allow_conflicts: conflicts,
+ }
+ );
+ }
+
+ _handleMoveConfirm() {
+ const el = this.$.confirmMove;
+ if (!el.branch) {
+ this.fire('show-alert', {message: ERR_BRANCH_EMPTY});
+ return;
+ }
+ this.$.overlay.close();
+ el.hidden = true;
+ this._fireAction(
+ '/move',
+ this.actions.move,
+ false,
+ {
+ destination_branch: el.branch,
+ message: el.message,
+ }
+ );
+ }
+
+ _handleRevertDialogConfirm(e) {
+ const revertType = e.detail.revertType;
+ const message = e.detail.message;
+ const el = this.$.confirmRevertDialog;
+ this.$.overlay.close();
+ el.hidden = true;
+ switch (revertType) {
+ case REVERT_TYPES.REVERT_SINGLE_CHANGE:
+ this._fireAction('/revert', this.actions.revert, false,
+ {message});
+ break;
+ case REVERT_TYPES.REVERT_SUBMISSION:
+ this._fireAction('/revert_submission', this.actions.revert_submission,
+ false, {message});
+ break;
+ default:
+ console.error('invalid revert type');
+ }
+ }
+
+ _handleRevertSubmissionDialogConfirm() {
+ const el = this.$.confirmRevertSubmissionDialog;
+ this.$.overlay.close();
+ el.hidden = true;
+ this._fireAction('/revert_submission', this.actions.revert_submission,
+ false, {message: el.message});
+ }
+
+ _handleAbandonDialogConfirm() {
+ const el = this.$.confirmAbandonDialog;
+ this.$.overlay.close();
+ el.hidden = true;
+ this._fireAction('/abandon', this.actions.abandon, false,
+ {message: el.message});
+ }
+
+ _handleCreateFollowUpChange() {
+ this.$.createFollowUpChange.handleCreateChange();
+ this._handleCloseCreateFollowUpChange();
+ }
+
+ _handleCloseCreateFollowUpChange() {
+ this.$.overlay.close();
+ }
+
+ _handleDeleteConfirm() {
+ this._fireAction('/', this.actions[ChangeActions.DELETE], false);
+ }
+
+ _handleDeleteEditConfirm() {
+ this._hideAllDialogs();
+
+ this._fireAction('/edit', this.actions.deleteEdit, false);
+ }
+
+ _handleSubmitConfirm() {
+ if (!this._canSubmitChange()) { return; }
+ this._hideAllDialogs();
+ this._fireAction('/submit', this.revisionActions.submit, true);
+ }
+
+ _getActionOverflowIndex(type, key) {
+ return this._overflowActions
+ .findIndex(action => action.type === type && action.key === key);
+ }
+
+ _setLoadingOnButtonWithKey(type, key) {
+ this._actionLoadingMessage = this._computeLoadingLabel(key);
+ let buttonKey = key;
+ // TODO(dhruvsri): clean this up later
+ // If key is revert-submission, then button key should be 'revert'
+ if (buttonKey === ChangeActions.REVERT_SUBMISSION) {
+ // Revert submission button no longer exists
+ buttonKey = ChangeActions.REVERT;
+ }
+
+ // If the action appears in the overflow menu.
+ if (this._getActionOverflowIndex(type, buttonKey) !== -1) {
+ this.push('_disabledMenuActions', buttonKey === '/' ? 'delete' :
+ buttonKey);
+ return function() {
+ this._actionLoadingMessage = '';
+ this._disabledMenuActions = [];
+ }.bind(this);
+ }
+
+ // Otherwise it's a top-level action.
+ const buttonEl = this.shadowRoot
+ .querySelector(`[data-action-key="${buttonKey}"]`);
+ buttonEl.setAttribute('loading', true);
+ buttonEl.disabled = true;
+ return function() {
+ this._actionLoadingMessage = '';
+ buttonEl.removeAttribute('loading');
+ buttonEl.disabled = false;
+ }.bind(this);
+ }
+
+ /**
+ * @param {string} endpoint
+ * @param {!Object|undefined} action
+ * @param {boolean} revAction
+ * @param {!Object|string=} opt_payload
+ */
+ _fireAction(endpoint, action, revAction, opt_payload) {
+ const cleanupFn =
+ this._setLoadingOnButtonWithKey(action.__type, action.__key);
+
+ this._send(action.method, opt_payload, endpoint, revAction, cleanupFn,
+ action).then(this._handleResponse.bind(this, action));
+ }
+
+ _showActionDialog(dialog) {
+ this._hideAllDialogs();
+
+ dialog.hidden = false;
+ this.$.overlay.open().then(() => {
+ if (dialog.resetFocus) {
+ dialog.resetFocus();
+ }
+ });
+ }
+
+ // TODO(rmistry): Redo this after
+ // https://bugs.chromium.org/p/gerrit/issues/detail?id=4671 is resolved.
+ _setLabelValuesOnRevert(newChangeId) {
+ const labels = this.$.jsAPI.getLabelValuesPostRevert(this.change);
+ if (!labels) { return Promise.resolve(); }
+ return this.$.restAPI.saveChangeReview(newChangeId, 'current', {labels});
+ }
+
+ _handleResponse(action, response) {
+ if (!response) { return; }
+ return this.$.restAPI.getResponseObject(response).then(obj => {
+ switch (action.__key) {
+ case ChangeActions.REVERT:
+ this._waitForChangeReachable(obj._number)
+ .then(() => this._setLabelValuesOnRevert(obj._number))
+ .then(() => {
+ Gerrit.Nav.navigateToChange(obj);
+ });
+ break;
+ case RevisionActions.CHERRYPICK:
+ this._waitForChangeReachable(obj._number).then(() => {
+ Gerrit.Nav.navigateToChange(obj);
+ });
+ break;
+ case ChangeActions.DELETE:
+ if (action.__type === ActionType.CHANGE) {
+ Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getUrlForRoot());
+ }
+ break;
+ case ChangeActions.WIP:
+ case ChangeActions.DELETE_EDIT:
+ case ChangeActions.PUBLISH_EDIT:
+ case ChangeActions.REBASE_EDIT:
+ Gerrit.Nav.navigateToChange(this.change);
+ break;
+ case ChangeActions.REVERT_SUBMISSION:
+ if (!obj.revert_changes || !obj.revert_changes.length) return;
+ /* If there is only 1 change then gerrit will automatically
+ redirect to that change */
+ Gerrit.Nav.navigateToSearchQuery('topic: ' +
+ obj.revert_changes[0].topic);
+ break;
+ default:
+ this.dispatchEvent(new CustomEvent('reload-change',
+ {detail: {action: action.__key}, bubbles: false}));
+ break;
+ }
+ });
+ }
+
+ _handleShowRevertSubmissionChangesConfirm() {
+ this._hideAllDialogs();
+ }
+
+ _handleResponseError(action, response, body) {
+ if (action && action.__key === RevisionActions.CHERRYPICK) {
+ if (response && response.status === 409 &&
+ body && !body.allow_conflicts) {
+ return this._showActionDialog(
+ this.$.confirmCherrypickConflict);
+ }
+ }
+ return response.text().then(errText => {
+ this.fire('show-error',
+ {message: `Could not perform action: ${errText}`});
+ if (!errText.startsWith('Change is already up to date')) {
+ throw Error(errText);
+ }
+ });
+ }
+
+ /**
+ * @param {string} method
+ * @param {string|!Object|undefined} payload
+ * @param {string} actionEndpoint
+ * @param {boolean} revisionAction
+ * @param {?Function} cleanupFn
+ * @param {!Object|undefined} action
+ */
+ _send(method, payload, actionEndpoint, revisionAction, cleanupFn, action) {
+ const handleError = response => {
+ cleanupFn.call(this);
+ this._handleResponseError(action, response, payload);
+ };
+
+ return this.fetchChangeUpdates(this.change, this.$.restAPI)
+ .then(result => {
+ if (!result.isLatest) {
+ this.fire('show-alert', {
+ message: 'Cannot set label: a newer patch has been ' +
+ 'uploaded to this change.',
+ action: 'Reload',
+ callback: () => {
+ // Load the current change without any patch range.
+ Gerrit.Nav.navigateToChange(this.change);
+ },
+ });
+
+ // Because this is not a network error, call the cleanup function
+ // but not the error handler.
+ cleanupFn();
+
+ return Promise.resolve();
+ }
+ const patchNum = revisionAction ? this.latestPatchNum : null;
+ return this.$.restAPI.executeChangeAction(this.changeNum, method,
+ actionEndpoint, patchNum, payload, handleError)
+ .then(response => {
+ cleanupFn.call(this);
+ return response;
+ });
+ });
+ }
+
+ _handleAbandonTap() {
+ this._showActionDialog(this.$.confirmAbandonDialog);
+ }
+
+ _handleCherrypickTap() {
+ this.$.confirmCherrypick.branch = '';
+ this._showActionDialog(this.$.confirmCherrypick);
+ }
+
+ _handleMoveTap() {
+ this.$.confirmMove.branch = '';
+ this.$.confirmMove.message = '';
+ this._showActionDialog(this.$.confirmMove);
+ }
+
+ _handleDownloadTap() {
+ this.fire('download-tap', null, {bubbles: false});
+ }
+
+ _handleDeleteTap() {
+ this._showActionDialog(this.$.confirmDeleteDialog);
+ }
+
+ _handleDeleteEditTap() {
+ this._showActionDialog(this.$.confirmDeleteEditDialog);
+ }
+
+ _handleFollowUpTap() {
+ this._showActionDialog(this.$.createFollowUpDialog);
+ }
+
+ _handleWipTap() {
+ this._fireAction('/wip', this.actions.wip, false);
+ }
+
+ _handlePublishEditTap() {
+ this._fireAction('/edit:publish', this.actions.publishEdit, false);
+ }
+
+ _handleRebaseEditTap() {
+ this._fireAction('/edit:rebase', this.actions.rebaseEdit, false);
+ }
+
+ _handleHideBackgroundContent() {
+ this.$.mainContent.classList.add('overlayOpen');
+ }
+
+ _handleShowBackgroundContent() {
+ this.$.mainContent.classList.remove('overlayOpen');
+ }
+
+ /**
+ * Merge sources of change actions into a single ordered array of action
+ * values.
+ *
+ * @param {!Array} changeActionsRecord
+ * @param {!Array} revisionActionsRecord
+ * @param {!Array} primariesRecord
+ * @param {!Array} additionalActionsRecord
+ * @param {!Object} change The change object.
+ * @return {!Array}
+ */
+ _computeAllActions(changeActionsRecord, revisionActionsRecord,
+ primariesRecord, additionalActionsRecord, change) {
+ // Polymer 2: check for undefined
+ if ([
+ changeActionsRecord,
+ revisionActionsRecord,
+ primariesRecord,
+ additionalActionsRecord,
+ change,
+ ].some(arg => arg === undefined)) {
+ return [];
+ }
+
+ const revisionActionValues = this._getActionValues(revisionActionsRecord,
+ primariesRecord, additionalActionsRecord, ActionType.REVISION);
+ const changeActionValues = this._getActionValues(changeActionsRecord,
+ primariesRecord, additionalActionsRecord, ActionType.CHANGE);
+ const quickApprove = this._getQuickApproveAction();
+ if (quickApprove) {
+ changeActionValues.unshift(quickApprove);
+ }
+
+ return revisionActionValues
+ .concat(changeActionValues)
+ .sort(this._actionComparator.bind(this))
+ .map(action => {
+ if (ACTIONS_WITH_ICONS.has(action.__key)) {
+ action.icon = action.__key;
+ }
+ return action;
+ })
+ .filter(action => !this._shouldSkipAction(action));
+ }
+
+ _getActionPriority(action) {
+ if (action.__type && action.__key) {
+ const overrideAction = this._actionPriorityOverrides
+ .find(i => i.type === action.__type && i.key === action.__key);
+
+ if (overrideAction !== undefined) {
+ return overrideAction.priority;
+ }
+ }
+ if (action.__key === 'review') {
+ return ActionPriority.REVIEW;
+ } else if (action.__primary) {
+ return ActionPriority.PRIMARY;
+ } else if (action.__type === ActionType.CHANGE) {
+ return ActionPriority.CHANGE;
+ } else if (action.__type === ActionType.REVISION) {
+ return ActionPriority.REVISION;
+ }
+ return ActionPriority.DEFAULT;
+ }
+
+ /**
+ * Sort comparator to define the order of change actions.
+ */
+ _actionComparator(actionA, actionB) {
+ const priorityDelta = this._getActionPriority(actionA) -
+ this._getActionPriority(actionB);
+ // Sort by the button label if same priority.
+ if (priorityDelta === 0) {
+ return actionA.label > actionB.label ? 1 : -1;
+ } else {
+ return priorityDelta;
+ }
+ }
+
+ _shouldSkipAction(action) {
+ return SKIP_ACTION_KEYS.includes(action.__key);
+ }
+
+ _computeTopLevelActions(actionRecord, hiddenActionsRecord) {
+ const hiddenActions = hiddenActionsRecord.base || [];
+ return actionRecord.base.filter(a => {
+ const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1;
+ return !(overflow || hiddenActions.includes(a.__key));
+ });
+ }
+
+ _filterPrimaryActions(_topLevelActions) {
+ this._topLevelPrimaryActions = _topLevelActions.filter(action =>
+ action.__primary);
+ this._topLevelSecondaryActions = _topLevelActions.filter(action =>
+ !action.__primary);
+ }
+
+ _computeMenuActions(actionRecord, hiddenActionsRecord) {
+ const hiddenActions = hiddenActionsRecord.base || [];
+ return actionRecord.base.filter(a => {
+ const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1;
+ return overflow && !hiddenActions.includes(a.__key);
+ }).map(action => {
+ let key = action.__key;
+ if (key === '/') { key = 'delete'; }
+ return {
+ name: action.label,
+ id: `${key}-${action.__type}`,
+ action,
+ tooltip: action.title,
+ };
+ });
+ }
+
+ _computeRebaseOnCurrent(revisionRebaseAction) {
+ if (revisionRebaseAction) {
+ return !!revisionRebaseAction.enabled;
+ }
+ return null;
+ }
+
+ /**
+ * Occasionally, a change created by a change action is not yet knwon to the
+ * API for a brief time. Wait for the given change number to be recognized.
+ *
+ * Returns a promise that resolves with true if a request is recognized, or
+ * false if the change was never recognized after all attempts.
+ *
+ * @param {number} changeNum
+ * @return {Promise<boolean>}
+ */
+ _waitForChangeReachable(changeNum) {
+ let attempsRemaining = AWAIT_CHANGE_ATTEMPTS;
+ return new Promise(resolve => {
+ const check = () => {
+ attempsRemaining--;
+ // Pass a no-op error handler to avoid the "not found" error toast.
+ this.$.restAPI.getChange(changeNum, () => {}).then(response => {
+ // If the response is 404, the response will be undefined.
+ if (response) {
+ resolve(true);
+ return;
+ }
+
+ if (attempsRemaining) {
+ this.async(check, AWAIT_CHANGE_TIMEOUT_MS);
+ } else {
+ resolve(false);
+ }
+ });
+ };
+ check();
+ });
+ }
+
+ _handleEditTap() {
+ this.dispatchEvent(new CustomEvent('edit-tap', {bubbles: false}));
+ }
+
+ _handleStopEditTap() {
+ this.dispatchEvent(new CustomEvent('stop-edit-tap', {bubbles: false}));
+ }
+
+ _computeHasTooltip(title) {
+ return !!title;
+ }
+
+ _computeHasIcon(action) {
+ return action.icon ? '' : 'hidden';
+ }
+}
+
+customElements.define(GrChangeActions.is, GrChangeActions);
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js
new file mode 100644
index 0000000..b66beed
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js
@@ -0,0 +1,140 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: flex;
+ font-family: var(--font-family);
+ }
+ #actionLoadingMessage,
+ #mainContent,
+ section {
+ display: flex;
+ }
+ #actionLoadingMessage,
+ gr-button,
+ gr-dropdown {
+ /* px because don't have the same font size */
+ margin-left: 8px;
+ }
+ #actionLoadingMessage {
+ align-items: center;
+ color: var(--deemphasized-text-color);
+ }
+ #confirmSubmitDialog .changeSubject {
+ margin: var(--spacing-l);
+ text-align: center;
+ }
+ iron-icon {
+ color: inherit;
+ margin-right: var(--spacing-xs);
+ }
+ #moreActions iron-icon {
+ margin: 0;
+ }
+ #moreMessage,
+ .hidden {
+ display: none;
+ }
+ @media screen and (max-width: 50em) {
+ #mainContent {
+ flex-wrap: wrap;
+ }
+ gr-button {
+ --gr-button: {
+ padding: var(--spacing-m);
+ white-space: nowrap;
+ }
+ }
+ gr-button,
+ gr-dropdown {
+ margin: 0;
+ }
+ #actionLoadingMessage {
+ margin: var(--spacing-m);
+ text-align: center;
+ }
+ #moreMessage {
+ display: inline;
+ }
+ }
+ </style>
+ <div id="mainContent">
+ <span id="actionLoadingMessage" hidden\$="[[!_actionLoadingMessage]]">
+ [[_actionLoadingMessage]]</span>
+ <section id="primaryActions" hidden\$="[[_shouldHideActions(_topLevelActions.*, _loading)]]">
+ <template is="dom-repeat" items="[[_topLevelPrimaryActions]]" as="action">
+ <gr-button link="" title\$="[[action.title]]" has-tooltip="[[_computeHasTooltip(action.title)]]" position-below="true" data-action-key\$="[[action.__key]]" data-action-type\$="[[action.__type]]" data-label\$="[[action.label]]" disabled\$="[[_calculateDisabled(action, _hasKnownChainState)]]" on-click="_handleActionTap">
+ <iron-icon class\$="[[_computeHasIcon(action)]]" icon\$="gr-icons:[[action.icon]]"></iron-icon>
+ [[action.label]]
+ </gr-button>
+ </template>
+ </section>
+ <section id="secondaryActions" hidden\$="[[_shouldHideActions(_topLevelActions.*, _loading)]]">
+ <template is="dom-repeat" items="[[_topLevelSecondaryActions]]" as="action">
+ <gr-button link="" title\$="[[action.title]]" has-tooltip="[[_computeHasTooltip(action.title)]]" position-below="true" data-action-key\$="[[action.__key]]" data-action-type\$="[[action.__type]]" data-label\$="[[action.label]]" disabled\$="[[_calculateDisabled(action, _hasKnownChainState)]]" on-click="_handleActionTap">
+ <iron-icon class\$="[[_computeHasIcon(action)]]" icon\$="gr-icons:[[action.icon]]"></iron-icon>
+ [[action.label]]
+ </gr-button>
+ </template>
+ </section>
+ <gr-button hidden\$="[[!_loading]]" disabled="">Loading actions...</gr-button>
+ <gr-dropdown id="moreActions" link="" tabindex="0" vertical-offset="32" horizontal-align="right" on-tap-item="_handleOveflowItemTap" hidden\$="[[_shouldHideActions(_menuActions.*, _loading)]]" disabled-ids="[[_disabledMenuActions]]" items="[[_menuActions]]">
+ <iron-icon icon="gr-icons:more-vert"></iron-icon>
+ <span id="moreMessage">More</span>
+ </gr-dropdown>
+ </div>
+ <gr-overlay id="overlay" with-backdrop="">
+ <gr-confirm-rebase-dialog id="confirmRebase" class="confirmDialog" change-number="[[change._number]]" on-confirm="_handleRebaseConfirm" on-cancel="_handleConfirmDialogCancel" branch="[[change.branch]]" has-parent="[[hasParent]]" rebase-on-current="[[_computeRebaseOnCurrent(_revisionRebaseAction)]]" hidden=""></gr-confirm-rebase-dialog>
+ <gr-confirm-cherrypick-dialog id="confirmCherrypick" class="confirmDialog" change-status="[[changeStatus]]" commit-message="[[commitMessage]]" commit-num="[[commitNum]]" on-confirm="_handleCherrypickConfirm" on-cancel="_handleConfirmDialogCancel" project="[[change.project]]" hidden=""></gr-confirm-cherrypick-dialog>
+ <gr-confirm-cherrypick-conflict-dialog id="confirmCherrypickConflict" class="confirmDialog" on-confirm="_handleCherrypickConflictConfirm" on-cancel="_handleConfirmDialogCancel" hidden=""></gr-confirm-cherrypick-conflict-dialog>
+ <gr-confirm-move-dialog id="confirmMove" class="confirmDialog" on-confirm="_handleMoveConfirm" on-cancel="_handleConfirmDialogCancel" project="[[change.project]]" hidden=""></gr-confirm-move-dialog>
+ <gr-confirm-revert-dialog id="confirmRevertDialog" class="confirmDialog" on-confirm="_handleRevertDialogConfirm" on-cancel="_handleConfirmDialogCancel" hidden=""></gr-confirm-revert-dialog>
+ <gr-confirm-revert-submission-dialog id="confirmRevertSubmissionDialog" class="confirmDialog" commit-message="[[commitMessage]]" on-confirm="_handleRevertSubmissionDialogConfirm" on-cancel="_handleConfirmDialogCancel" hidden=""></gr-confirm-revert-submission-dialog>
+ <gr-confirm-abandon-dialog id="confirmAbandonDialog" class="confirmDialog" on-confirm="_handleAbandonDialogConfirm" on-cancel="_handleConfirmDialogCancel" hidden=""></gr-confirm-abandon-dialog>
+ <gr-confirm-submit-dialog id="confirmSubmitDialog" class="confirmDialog" change="[[change]]" action="[[_revisionSubmitAction]]" on-cancel="_handleConfirmDialogCancel" on-confirm="_handleSubmitConfirm" hidden=""></gr-confirm-submit-dialog>
+ <gr-dialog id="createFollowUpDialog" class="confirmDialog" confirm-label="Create" on-confirm="_handleCreateFollowUpChange" on-cancel="_handleCloseCreateFollowUpChange">
+ <div class="header" slot="header">
+ Create Follow-Up Change
+ </div>
+ <div class="main" slot="main">
+ <gr-create-change-dialog id="createFollowUpChange" branch="[[change.branch]]" base-change="[[change.id]]" repo-name="[[change.project]]" private-by-default="[[privateByDefault]]"></gr-create-change-dialog>
+ </div>
+ </gr-dialog>
+ <gr-dialog id="confirmDeleteDialog" class="confirmDialog" confirm-label="Delete" confirm-on-enter="" on-cancel="_handleConfirmDialogCancel" on-confirm="_handleDeleteConfirm">
+ <div class="header" slot="header">
+ Delete Change
+ </div>
+ <div class="main" slot="main">
+ Do you really want to delete the change?
+ </div>
+ </gr-dialog>
+ <gr-dialog id="confirmDeleteEditDialog" class="confirmDialog" confirm-label="Delete" confirm-on-enter="" on-cancel="_handleConfirmDialogCancel" on-confirm="_handleDeleteEditConfirm">
+ <div class="header" slot="header">
+ Delete Change Edit
+ </div>
+ <div class="main" slot="main">
+ Do you really want to delete the edit?
+ </div>
+ </gr-dialog>
+ </gr-overlay>
+ <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ <gr-reporting id="reporting" category="change-actions"></gr-reporting>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index ee036cf..f8f991f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -19,17 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-actions</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="gr-change-actions.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -37,885 +30,1704 @@
</template>
</test-fixture>
-<script>
- // TODO(dhruvsri): remove use of _populateRevertMessage as it's private
- suite('gr-change-actions tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-change-actions.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+// TODO(dhruvsri): remove use of _populateRevertMessage as it's private
+suite('gr-change-actions tests', () => {
+ let element;
+ let sandbox;
- suite('basic tests', () => {
- setup(() => {
- stub('gr-rest-api-interface', {
- getChangeRevisionActions() {
+ suite('basic tests', () => {
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getChangeRevisionActions() {
+ return Promise.resolve({
+ cherrypick: {
+ method: 'POST',
+ label: 'Cherry Pick',
+ title: 'Cherry pick change to a different branch',
+ enabled: true,
+ },
+ rebase: {
+ method: 'POST',
+ label: 'Rebase',
+ title: 'Rebase onto tip of branch or parent change',
+ enabled: true,
+ },
+ submit: {
+ method: 'POST',
+ label: 'Submit',
+ title: 'Submit patch set 2 into master',
+ enabled: true,
+ },
+ revert_submission: {
+ method: 'POST',
+ label: 'Revert submission',
+ title: 'Revert this submission',
+ enabled: true,
+ },
+ });
+ },
+ send(method, url, payload) {
+ if (method !== 'POST') {
+ return Promise.reject(new Error('bad method'));
+ }
+
+ if (url === '/changes/test~42/revisions/2/submit') {
return Promise.resolve({
- cherrypick: {
- method: 'POST',
- label: 'Cherry Pick',
- title: 'Cherry pick change to a different branch',
- enabled: true,
- },
- rebase: {
- method: 'POST',
- label: 'Rebase',
- title: 'Rebase onto tip of branch or parent change',
- enabled: true,
- },
- submit: {
- method: 'POST',
- label: 'Submit',
- title: 'Submit patch set 2 into master',
- enabled: true,
- },
- revert_submission: {
- method: 'POST',
- label: 'Revert submission',
- title: 'Revert this submission',
- enabled: true,
- },
+ ok: true,
+ text() { return Promise.resolve(')]}\'\n{}'); },
});
- },
- send(method, url, payload) {
- if (method !== 'POST') {
- return Promise.reject(new Error('bad method'));
- }
+ } else if (url === '/changes/test~42/revisions/2/rebase') {
+ return Promise.resolve({
+ ok: true,
+ text() { return Promise.resolve(')]}\'\n{}'); },
+ });
+ }
- if (url === '/changes/test~42/revisions/2/submit') {
- return Promise.resolve({
- ok: true,
- text() { return Promise.resolve(')]}\'\n{}'); },
- });
- } else if (url === '/changes/test~42/revisions/2/rebase') {
- return Promise.resolve({
- ok: true,
- text() { return Promise.resolve(')]}\'\n{}'); },
- });
- }
-
- return Promise.reject(new Error('bad url'));
- },
- getProjectConfig() { return Promise.resolve({}); },
- });
-
- sandbox = sinon.sandbox.create();
- sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
-
- element = fixture('basic');
- element.change = {};
- element.changeNum = '42';
- element.latestPatchNum = '2';
- element.actions = {
- '/': {
- method: 'DELETE',
- label: 'Delete Change',
- title: 'Delete change X_X',
- enabled: true,
- },
- };
- sandbox.stub(element.$.confirmCherrypick.$.restAPI,
- 'getRepoBranches').returns(Promise.resolve([]));
- sandbox.stub(element.$.confirmMove.$.restAPI,
- 'getRepoBranches').returns(Promise.resolve([]));
-
- return element.reload();
+ return Promise.reject(new Error('bad url'));
+ },
+ getProjectConfig() { return Promise.resolve({}); },
});
- teardown(() => {
- sandbox.restore();
- });
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
- test('show-revision-actions event should fire', done => {
- const spy = sinon.spy(element, '_sendShowRevisionActions');
- element.reload();
- flush(() => {
- assert.isTrue(spy.called);
- done();
- });
- });
+ element = fixture('basic');
+ element.change = {};
+ element.changeNum = '42';
+ element.latestPatchNum = '2';
+ element.actions = {
+ '/': {
+ method: 'DELETE',
+ label: 'Delete Change',
+ title: 'Delete change X_X',
+ enabled: true,
+ },
+ };
+ sandbox.stub(element.$.confirmCherrypick.$.restAPI,
+ 'getRepoBranches').returns(Promise.resolve([]));
+ sandbox.stub(element.$.confirmMove.$.restAPI,
+ 'getRepoBranches').returns(Promise.resolve([]));
- test('primary and secondary actions split properly', () => {
- // Submit should be the only primary action.
- assert.equal(element._topLevelPrimaryActions.length, 1);
- assert.equal(element._topLevelPrimaryActions[0].label, 'Submit');
- assert.equal(element._topLevelSecondaryActions.length,
- element._topLevelActions.length - 1);
- });
+ return element.reload();
+ });
- test('revert submission action is skipped', () => {
- assert.isFalse(element._allActionValues.includes(action =>
- action.key === 'revert_submission'));
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('_shouldHideActions', () => {
- assert.isTrue(element._shouldHideActions(undefined, true));
- assert.isTrue(element._shouldHideActions({base: {}}, false));
- assert.isFalse(element._shouldHideActions({base: ['test']}, false));
+ test('show-revision-actions event should fire', done => {
+ const spy = sinon.spy(element, '_sendShowRevisionActions');
+ element.reload();
+ flush(() => {
+ assert.isTrue(spy.called);
+ done();
});
+ });
- test('plugin revision actions', done => {
- sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns(
- Promise.resolve('the-url'));
- element.revisionActions = {
- 'plugin~action': {},
- };
- assert.isOk(element.revisionActions['plugin~action']);
- flush(() => {
- assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith(
- element.changeNum, element.latestPatchNum, '/plugin~action'));
- assert.equal(element.revisionActions['plugin~action'].__url, 'the-url');
- done();
- });
+ test('primary and secondary actions split properly', () => {
+ // Submit should be the only primary action.
+ assert.equal(element._topLevelPrimaryActions.length, 1);
+ assert.equal(element._topLevelPrimaryActions[0].label, 'Submit');
+ assert.equal(element._topLevelSecondaryActions.length,
+ element._topLevelActions.length - 1);
+ });
+
+ test('revert submission action is skipped', () => {
+ assert.isFalse(element._allActionValues.includes(action =>
+ action.key === 'revert_submission'));
+ });
+
+ test('_shouldHideActions', () => {
+ assert.isTrue(element._shouldHideActions(undefined, true));
+ assert.isTrue(element._shouldHideActions({base: {}}, false));
+ assert.isFalse(element._shouldHideActions({base: ['test']}, false));
+ });
+
+ test('plugin revision actions', done => {
+ sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns(
+ Promise.resolve('the-url'));
+ element.revisionActions = {
+ 'plugin~action': {},
+ };
+ assert.isOk(element.revisionActions['plugin~action']);
+ flush(() => {
+ assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith(
+ element.changeNum, element.latestPatchNum, '/plugin~action'));
+ assert.equal(element.revisionActions['plugin~action'].__url, 'the-url');
+ done();
});
+ });
- test('plugin change actions', done => {
- sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns(
- Promise.resolve('the-url'));
- element.actions = {
- 'plugin~action': {},
- };
- assert.isOk(element.actions['plugin~action']);
- flush(() => {
- assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith(
- element.changeNum, null, '/plugin~action'));
- assert.equal(element.actions['plugin~action'].__url, 'the-url');
- done();
- });
+ test('plugin change actions', done => {
+ sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns(
+ Promise.resolve('the-url'));
+ element.actions = {
+ 'plugin~action': {},
+ };
+ assert.isOk(element.actions['plugin~action']);
+ flush(() => {
+ assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith(
+ element.changeNum, null, '/plugin~action'));
+ assert.equal(element.actions['plugin~action'].__url, 'the-url');
+ done();
});
+ });
- test('not supported actions are filtered out', () => {
- element.revisionActions = {followup: {}};
- assert.equal(element.querySelectorAll(
- 'section gr-button[data-action-type="revision"]').length, 0);
- });
+ test('not supported actions are filtered out', () => {
+ element.revisionActions = {followup: {}};
+ assert.equal(element.querySelectorAll(
+ 'section gr-button[data-action-type="revision"]').length, 0);
+ });
- test('getActionDetails', () => {
- element.revisionActions = Object.assign({
- 'plugin~action': {},
- }, element.revisionActions);
- assert.isUndefined(element.getActionDetails('rubbish'));
- assert.strictEqual(element.revisionActions['plugin~action'],
- element.getActionDetails('plugin~action'));
- assert.strictEqual(element.revisionActions['rebase'],
- element.getActionDetails('rebase'));
- });
+ test('getActionDetails', () => {
+ element.revisionActions = Object.assign({
+ 'plugin~action': {},
+ }, element.revisionActions);
+ assert.isUndefined(element.getActionDetails('rubbish'));
+ assert.strictEqual(element.revisionActions['plugin~action'],
+ element.getActionDetails('plugin~action'));
+ assert.strictEqual(element.revisionActions['rebase'],
+ element.getActionDetails('rebase'));
+ });
- test('hide revision action', done => {
+ test('hide revision action', done => {
+ flush(() => {
+ const buttonEl = element.shadowRoot
+ .querySelector('[data-action-key="submit"]');
+ assert.isOk(buttonEl);
+ assert.throws(element.setActionHidden.bind(element, 'invalid type'));
+ element.setActionHidden(element.ActionType.REVISION,
+ element.RevisionActions.SUBMIT, true);
+ assert.lengthOf(element._hiddenActions, 1);
+ element.setActionHidden(element.ActionType.REVISION,
+ element.RevisionActions.SUBMIT, true);
+ assert.lengthOf(element._hiddenActions, 1);
flush(() => {
const buttonEl = element.shadowRoot
.querySelector('[data-action-key="submit"]');
- assert.isOk(buttonEl);
- assert.throws(element.setActionHidden.bind(element, 'invalid type'));
+ assert.isNotOk(buttonEl);
+
element.setActionHidden(element.ActionType.REVISION,
- element.RevisionActions.SUBMIT, true);
- assert.lengthOf(element._hiddenActions, 1);
- element.setActionHidden(element.ActionType.REVISION,
- element.RevisionActions.SUBMIT, true);
- assert.lengthOf(element._hiddenActions, 1);
+ element.RevisionActions.SUBMIT, false);
flush(() => {
const buttonEl = element.shadowRoot
.querySelector('[data-action-key="submit"]');
- assert.isNotOk(buttonEl);
-
- element.setActionHidden(element.ActionType.REVISION,
- element.RevisionActions.SUBMIT, false);
- flush(() => {
- const buttonEl = element.shadowRoot
- .querySelector('[data-action-key="submit"]');
- assert.isOk(buttonEl);
- assert.isFalse(buttonEl.hasAttribute('hidden'));
- done();
- });
+ assert.isOk(buttonEl);
+ assert.isFalse(buttonEl.hasAttribute('hidden'));
+ done();
});
});
});
+ });
- test('buttons exist', done => {
- element._loading = false;
- flush(() => {
- const buttonEls = Polymer.dom(element.root)
- .querySelectorAll('gr-button');
- const menuItems = element.$.moreActions.items;
+ test('buttons exist', done => {
+ element._loading = false;
+ flush(() => {
+ const buttonEls = dom(element.root)
+ .querySelectorAll('gr-button');
+ const menuItems = element.$.moreActions.items;
- // Total button number is one greater than the number of total actions
- // due to the existence of the overflow menu trigger.
- assert.equal(buttonEls.length + menuItems.length,
- element._allActionValues.length + 1);
- assert.isFalse(element.hidden);
- done();
- });
+ // Total button number is one greater than the number of total actions
+ // due to the existence of the overflow menu trigger.
+ assert.equal(buttonEls.length + menuItems.length,
+ element._allActionValues.length + 1);
+ assert.isFalse(element.hidden);
+ done();
});
+ });
- test('delete buttons have explicit labels', done => {
- flush(() => {
- const deleteItems = element.$.moreActions.items
- .filter(item => item.id.startsWith('delete'));
- assert.equal(deleteItems.length, 1);
- assert.notEqual(deleteItems[0].name);
- assert.equal(deleteItems[0].name, 'Delete change');
- done();
- });
+ test('delete buttons have explicit labels', done => {
+ flush(() => {
+ const deleteItems = element.$.moreActions.items
+ .filter(item => item.id.startsWith('delete'));
+ assert.equal(deleteItems.length, 1);
+ assert.notEqual(deleteItems[0].name);
+ assert.equal(deleteItems[0].name, 'Delete change');
+ done();
});
+ });
- test('get revision object from change', () => {
- const revObj = {_number: 2, foo: 'bar'};
- const change = {
- revisions: {
- rev1: {_number: 1},
- rev2: revObj,
- },
- };
- assert.deepEqual(element._getRevision(change, '2'), revObj);
- });
+ test('get revision object from change', () => {
+ const revObj = {_number: 2, foo: 'bar'};
+ const change = {
+ revisions: {
+ rev1: {_number: 1},
+ rev2: revObj,
+ },
+ };
+ assert.deepEqual(element._getRevision(change, '2'), revObj);
+ });
- test('_actionComparator sort order', () => {
- const actions = [
- {label: '123', __type: 'change', __key: 'review'},
- {label: 'abc-ro', __type: 'revision'},
- {label: 'abc', __type: 'change'},
- {label: 'def', __type: 'change'},
- {label: 'def-p', __type: 'change', __primary: true},
- ];
+ test('_actionComparator sort order', () => {
+ const actions = [
+ {label: '123', __type: 'change', __key: 'review'},
+ {label: 'abc-ro', __type: 'revision'},
+ {label: 'abc', __type: 'change'},
+ {label: 'def', __type: 'change'},
+ {label: 'def-p', __type: 'change', __primary: true},
+ ];
- const result = actions.slice();
- result.reverse();
- result.sort(element._actionComparator.bind(element));
- assert.deepEqual(result, actions);
- });
+ const result = actions.slice();
+ result.reverse();
+ result.sort(element._actionComparator.bind(element));
+ assert.deepEqual(result, actions);
+ });
- test('submit change', () => {
- const showSpy = sandbox.spy(element, '_showActionDialog');
- sandbox.stub(element.$.restAPI, 'getFromProjectLookup')
- .returns(Promise.resolve('test'));
- sandbox.stub(element, 'fetchChangeUpdates',
- () => Promise.resolve({isLatest: true}));
- sandbox.stub(element.$.overlay, 'open').returns(Promise.resolve());
- element.change = {
- revisions: {
- rev1: {_number: 1},
- rev2: {_number: 2},
- },
- };
- element.latestPatchNum = '2';
+ test('submit change', () => {
+ const showSpy = sandbox.spy(element, '_showActionDialog');
+ sandbox.stub(element.$.restAPI, 'getFromProjectLookup')
+ .returns(Promise.resolve('test'));
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => Promise.resolve({isLatest: true}));
+ sandbox.stub(element.$.overlay, 'open').returns(Promise.resolve());
+ element.change = {
+ revisions: {
+ rev1: {_number: 1},
+ rev2: {_number: 2},
+ },
+ };
+ element.latestPatchNum = '2';
+ const submitButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="submit"]');
+ assert.ok(submitButton);
+ MockInteractions.tap(submitButton);
+
+ flushAsynchronousOperations();
+ assert.isTrue(showSpy.calledWith(element.$.confirmSubmitDialog));
+ });
+
+ test('submit change, tap on icon', done => {
+ sandbox.stub(element.$.confirmSubmitDialog, 'resetFocus', done);
+ sandbox.stub(element.$.restAPI, 'getFromProjectLookup')
+ .returns(Promise.resolve('test'));
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => Promise.resolve({isLatest: true}));
+ sandbox.stub(element.$.overlay, 'open').returns(Promise.resolve());
+ element.change = {
+ revisions: {
+ rev1: {_number: 1},
+ rev2: {_number: 2},
+ },
+ };
+ element.latestPatchNum = '2';
+
+ const submitIcon =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key="submit"] iron-icon');
+ assert.ok(submitIcon);
+ MockInteractions.tap(submitIcon);
+ });
+
+ test('_handleSubmitConfirm', () => {
+ const fireStub = sandbox.stub(element, '_fireAction');
+ sandbox.stub(element, '_canSubmitChange').returns(true);
+ element._handleSubmitConfirm();
+ assert.isTrue(fireStub.calledOnce);
+ assert.deepEqual(fireStub.lastCall.args,
+ ['/submit', element.revisionActions.submit, true]);
+ });
+
+ test('_handleSubmitConfirm when not able to submit', () => {
+ const fireStub = sandbox.stub(element, '_fireAction');
+ sandbox.stub(element, '_canSubmitChange').returns(false);
+ element._handleSubmitConfirm();
+ assert.isFalse(fireStub.called);
+ });
+
+ test('submit change with plugin hook', done => {
+ sandbox.stub(element, '_canSubmitChange',
+ () => false);
+ const fireActionStub = sandbox.stub(element, '_fireAction');
+ flush(() => {
const submitButton = element.shadowRoot
.querySelector('gr-button[data-action-key="submit"]');
assert.ok(submitButton);
MockInteractions.tap(submitButton);
+ assert.equal(fireActionStub.callCount, 0);
- flushAsynchronousOperations();
- assert.isTrue(showSpy.calledWith(element.$.confirmSubmitDialog));
+ done();
});
+ });
- test('submit change, tap on icon', done => {
- sandbox.stub(element.$.confirmSubmitDialog, 'resetFocus', done);
- sandbox.stub(element.$.restAPI, 'getFromProjectLookup')
- .returns(Promise.resolve('test'));
- sandbox.stub(element, 'fetchChangeUpdates',
- () => Promise.resolve({isLatest: true}));
- sandbox.stub(element.$.overlay, 'open').returns(Promise.resolve());
- element.change = {
- revisions: {
- rev1: {_number: 1},
- rev2: {_number: 2},
- },
- };
- element.latestPatchNum = '2';
+ test('chain state', () => {
+ assert.equal(element._hasKnownChainState, false);
+ element.hasParent = true;
+ assert.equal(element._hasKnownChainState, true);
+ element.hasParent = false;
+ });
- const submitIcon =
- element.shadowRoot
- .querySelector('gr-button[data-action-key="submit"] iron-icon');
- assert.ok(submitIcon);
- MockInteractions.tap(submitIcon);
- });
+ test('_calculateDisabled', () => {
+ let hasKnownChainState = false;
+ const action = {__key: 'rebase', enabled: true};
+ assert.equal(
+ element._calculateDisabled(action, hasKnownChainState), true);
- test('_handleSubmitConfirm', () => {
- const fireStub = sandbox.stub(element, '_fireAction');
- sandbox.stub(element, '_canSubmitChange').returns(true);
- element._handleSubmitConfirm();
- assert.isTrue(fireStub.calledOnce);
- assert.deepEqual(fireStub.lastCall.args,
- ['/submit', element.revisionActions.submit, true]);
- });
+ action.__key = 'delete';
+ assert.equal(
+ element._calculateDisabled(action, hasKnownChainState), false);
- test('_handleSubmitConfirm when not able to submit', () => {
- const fireStub = sandbox.stub(element, '_fireAction');
- sandbox.stub(element, '_canSubmitChange').returns(false);
- element._handleSubmitConfirm();
- assert.isFalse(fireStub.called);
- });
+ action.__key = 'rebase';
+ hasKnownChainState = true;
+ assert.equal(
+ element._calculateDisabled(action, hasKnownChainState), false);
- test('submit change with plugin hook', done => {
- sandbox.stub(element, '_canSubmitChange',
- () => false);
- const fireActionStub = sandbox.stub(element, '_fireAction');
- flush(() => {
- const submitButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="submit"]');
- assert.ok(submitButton);
- MockInteractions.tap(submitButton);
- assert.equal(fireActionStub.callCount, 0);
+ action.enabled = false;
+ assert.equal(
+ element._calculateDisabled(action, hasKnownChainState), false);
+ });
- done();
- });
- });
-
- test('chain state', () => {
- assert.equal(element._hasKnownChainState, false);
- element.hasParent = true;
- assert.equal(element._hasKnownChainState, true);
- element.hasParent = false;
- });
-
- test('_calculateDisabled', () => {
- let hasKnownChainState = false;
- const action = {__key: 'rebase', enabled: true};
- assert.equal(
- element._calculateDisabled(action, hasKnownChainState), true);
-
- action.__key = 'delete';
- assert.equal(
- element._calculateDisabled(action, hasKnownChainState), false);
-
- action.__key = 'rebase';
- hasKnownChainState = true;
- assert.equal(
- element._calculateDisabled(action, hasKnownChainState), false);
-
- action.enabled = false;
- assert.equal(
- element._calculateDisabled(action, hasKnownChainState), true);
- });
-
- test('rebase change', done => {
- const fireActionStub = sandbox.stub(element, '_fireAction');
- const fetchChangesStub = sandbox.stub(element.$.confirmRebase,
- 'fetchRecentChanges').returns(Promise.resolve([]));
- element._hasKnownChainState = true;
- flush(() => {
- const rebaseButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="rebase"]');
- MockInteractions.tap(rebaseButton);
- const rebaseAction = {
- __key: 'rebase',
- __type: 'revision',
- __primary: false,
- enabled: true,
- label: 'Rebase',
- method: 'POST',
- title: 'Rebase onto tip of branch or parent change',
- };
- assert.isTrue(fetchChangesStub.called);
- element._handleRebaseConfirm({detail: {base: '1234'}});
- rebaseAction.rebaseOnCurrent = true;
- assert.deepEqual(fireActionStub.lastCall.args,
- ['/rebase', rebaseAction, true, {base: '1234'}]);
- done();
- });
- });
-
- test(`rebase dialog gets recent changes each time it's opened`, done => {
- const fetchChangesStub = sandbox.stub(element.$.confirmRebase,
- 'fetchRecentChanges').returns(Promise.resolve([]));
- element._hasKnownChainState = true;
+ test('rebase change', done => {
+ const fireActionStub = sandbox.stub(element, '_fireAction');
+ const fetchChangesStub = sandbox.stub(element.$.confirmRebase,
+ 'fetchRecentChanges').returns(Promise.resolve([]));
+ element._hasKnownChainState = true;
+ flush(() => {
const rebaseButton = element.shadowRoot
.querySelector('gr-button[data-action-key="rebase"]');
MockInteractions.tap(rebaseButton);
- assert.isTrue(fetchChangesStub.calledOnce);
+ const rebaseAction = {
+ __key: 'rebase',
+ __type: 'revision',
+ __primary: false,
+ enabled: true,
+ label: 'Rebase',
+ method: 'POST',
+ title: 'Rebase onto tip of branch or parent change',
+ };
+ assert.isTrue(fetchChangesStub.called);
+ element._handleRebaseConfirm({detail: {base: '1234'}});
+ assert.deepEqual(fireActionStub.lastCall.args,
+ ['/rebase', rebaseAction, true, {base: '1234'}]);
+ done();
+ });
+ });
+ test(`rebase dialog gets recent changes each time it's opened`, done => {
+ const fetchChangesStub = sandbox.stub(element.$.confirmRebase,
+ 'fetchRecentChanges').returns(Promise.resolve([]));
+ element._hasKnownChainState = true;
+ const rebaseButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="rebase"]');
+ MockInteractions.tap(rebaseButton);
+ assert.isTrue(fetchChangesStub.calledOnce);
+
+ flush(() => {
+ element.$.confirmRebase.fire('cancel');
+ MockInteractions.tap(rebaseButton);
+ assert.isTrue(fetchChangesStub.calledTwice);
+ done();
+ });
+ });
+
+ test('two dialogs are not shown at the same time', done => {
+ element._hasKnownChainState = true;
+ flush(() => {
+ const rebaseButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="rebase"]');
+ assert.ok(rebaseButton);
+ MockInteractions.tap(rebaseButton);
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.confirmRebase.hidden);
+
+ element._handleCherrypickTap();
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.confirmRebase.hidden);
+ assert.isFalse(element.$.confirmCherrypick.hidden);
+ done();
+ });
+ });
+
+ test('fullscreen-overlay-opened hides content', () => {
+ sandbox.spy(element, '_handleHideBackgroundContent');
+ element.$.overlay.fire('fullscreen-overlay-opened');
+ assert.isTrue(element._handleHideBackgroundContent.called);
+ assert.isTrue(element.$.mainContent.classList.contains('overlayOpen'));
+ });
+
+ test('fullscreen-overlay-closed shows content', () => {
+ sandbox.spy(element, '_handleShowBackgroundContent');
+ element.$.overlay.fire('fullscreen-overlay-closed');
+ assert.isTrue(element._handleShowBackgroundContent.called);
+ assert.isFalse(element.$.mainContent.classList.contains('overlayOpen'));
+ });
+
+ test('_setLabelValuesOnRevert', () => {
+ const labels = {'Foo': 1, 'Bar-Baz': -2};
+ const changeId = 1234;
+ sandbox.stub(element.$.jsAPI, 'getLabelValuesPostRevert').returns(labels);
+ const saveStub = sandbox.stub(element.$.restAPI, 'saveChangeReview')
+ .returns(Promise.resolve());
+ return element._setLabelValuesOnRevert(changeId).then(() => {
+ assert.isTrue(saveStub.calledOnce);
+ assert.equal(saveStub.lastCall.args[0], changeId);
+ assert.deepEqual(saveStub.lastCall.args[2], {labels});
+ });
+ });
+
+ suite('change edits', () => {
+ test('disableEdit', () => {
+ element.set('editMode', false);
+ element.set('editPatchsetLoaded', false);
+ element.change = {status: 'NEW'};
+ element.set('disableEdit', true);
+ flushAsynchronousOperations();
+
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="publishEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="rebaseEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="deleteEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="stopEdit"]'));
+ });
+
+ test('shows confirm dialog for delete edit', () => {
+ element.set('editMode', true);
+ element.set('editPatchsetLoaded', true);
+
+ const fireActionStub = sandbox.stub(element, '_fireAction');
+ element._handleDeleteEditTap();
+ assert.isFalse(element.$.confirmDeleteEditDialog.hidden);
+ MockInteractions.tap(
+ element.shadowRoot
+ .querySelector('#confirmDeleteEditDialog')
+ .shadowRoot
+ .querySelector('gr-button[primary]'));
+ flushAsynchronousOperations();
+
+ assert.equal(fireActionStub.lastCall.args[0], '/edit');
+ });
+
+ test('hide publishEdit and rebaseEdit if change is not open', () => {
+ element.set('editMode', true);
+ element.set('editPatchsetLoaded', true);
+ element.change = {status: 'MERGED'};
+ flushAsynchronousOperations();
+
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="publishEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="rebaseEdit"]'));
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="deleteEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]'));
+ });
+
+ test('edit patchset is loaded, needs rebase', () => {
+ element.set('editMode', true);
+ element.set('editPatchsetLoaded', true);
+ element.change = {status: 'NEW'};
+ element.editBasedOnCurrentPatchSet = false;
+ flushAsynchronousOperations();
+
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="publishEdit"]'));
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="rebaseEdit"]'));
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="deleteEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="stopEdit"]'));
+ });
+
+ test('edit patchset is loaded, does not need rebase', () => {
+ element.set('editMode', true);
+ element.set('editPatchsetLoaded', true);
+ element.change = {status: 'NEW'};
+ element.editBasedOnCurrentPatchSet = true;
+ flushAsynchronousOperations();
+
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="publishEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="rebaseEdit"]'));
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="deleteEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="stopEdit"]'));
+ });
+
+ test('edit mode is loaded, no edit patchset', () => {
+ element.set('editMode', true);
+ element.set('editPatchsetLoaded', false);
+ element.change = {status: 'NEW'};
+ flushAsynchronousOperations();
+
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="publishEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="rebaseEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="deleteEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]'));
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="stopEdit"]'));
+ });
+
+ test('normal patch set', () => {
+ element.set('editMode', false);
+ element.set('editPatchsetLoaded', false);
+ element.change = {status: 'NEW'};
+ flushAsynchronousOperations();
+
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="publishEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="rebaseEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="deleteEdit"]'));
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="stopEdit"]'));
+ });
+
+ test('edit action', done => {
+ element.addEventListener('edit-tap', () => { done(); });
+ element.set('editMode', true);
+ element.change = {status: 'NEW'};
+ flushAsynchronousOperations();
+
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]'));
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="stopEdit"]'));
+ element.change = {status: 'MERGED'};
+ flushAsynchronousOperations();
+
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]'));
+ element.change = {status: 'NEW'};
+ element.set('editMode', false);
+ flushAsynchronousOperations();
+
+ const editButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]');
+ assert.isOk(editButton);
+ MockInteractions.tap(editButton);
+ });
+ });
+
+ suite('cherry-pick', () => {
+ let fireActionStub;
+
+ setup(() => {
+ fireActionStub = sandbox.stub(element, '_fireAction');
+ sandbox.stub(window, 'alert');
+ });
+
+ test('works', () => {
+ element._handleCherrypickTap();
+ const action = {
+ __key: 'cherrypick',
+ __type: 'revision',
+ __primary: false,
+ enabled: true,
+ label: 'Cherry pick',
+ method: 'POST',
+ title: 'Cherry pick change to a different branch',
+ };
+
+ element._handleCherrypickConfirm();
+ assert.equal(fireActionStub.callCount, 0);
+
+ element.$.confirmCherrypick.branch = 'master';
+ element._handleCherrypickConfirm();
+ assert.equal(fireActionStub.callCount, 0); // Still needs a message.
+
+ // Add attributes that are used to determine the message.
+ element.$.confirmCherrypick.commitMessage = 'foo message';
+ element.$.confirmCherrypick.changeStatus = 'OPEN';
+ element.$.confirmCherrypick.commitNum = '123';
+
+ element._handleCherrypickConfirm();
+
+ assert.equal(element.$.confirmCherrypick.$.messageInput.value,
+ 'foo message');
+
+ assert.deepEqual(fireActionStub.lastCall.args, [
+ '/cherrypick', action, true, {
+ destination: 'master',
+ base: null,
+ message: 'foo message',
+ allow_conflicts: false,
+ },
+ ]);
+ });
+
+ test('cherry pick even with conflicts', () => {
+ element._handleCherrypickTap();
+ const action = {
+ __key: 'cherrypick',
+ __type: 'revision',
+ __primary: false,
+ enabled: true,
+ label: 'Cherry pick',
+ method: 'POST',
+ title: 'Cherry pick change to a different branch',
+ };
+
+ element.$.confirmCherrypick.branch = 'master';
+
+ // Add attributes that are used to determine the message.
+ element.$.confirmCherrypick.commitMessage = 'foo message';
+ element.$.confirmCherrypick.changeStatus = 'OPEN';
+ element.$.confirmCherrypick.commitNum = '123';
+
+ element._handleCherrypickConflictConfirm();
+
+ assert.deepEqual(fireActionStub.lastCall.args, [
+ '/cherrypick', action, true, {
+ destination: 'master',
+ base: null,
+ message: 'foo message',
+ allow_conflicts: true,
+ },
+ ]);
+ });
+
+ test('branch name cleared when re-open cherrypick', () => {
+ const emptyBranchName = '';
+ element.$.confirmCherrypick.branch = 'master';
+
+ element._handleCherrypickTap();
+ assert.equal(element.$.confirmCherrypick.branch, emptyBranchName);
+ });
+ });
+
+ suite('move change', () => {
+ let fireActionStub;
+
+ setup(() => {
+ fireActionStub = sandbox.stub(element, '_fireAction');
+ sandbox.stub(window, 'alert');
+ });
+
+ test('works', () => {
+ element._handleMoveTap();
+
+ element._handleMoveConfirm();
+ assert.equal(fireActionStub.callCount, 0);
+
+ element.$.confirmMove.branch = 'master';
+ element._handleMoveConfirm();
+ assert.equal(fireActionStub.callCount, 1);
+ });
+
+ test('branch name cleared when re-open move', () => {
+ const emptyBranchName = '';
+ element.$.confirmMove.branch = 'master';
+
+ element._handleMoveTap();
+ assert.equal(element.$.confirmMove.branch, emptyBranchName);
+ });
+ });
+
+ test('custom actions', done => {
+ // Add a button with the same key as a server-based one to ensure
+ // collisions are taken care of.
+ const key = element.addActionButton(element.ActionType.REVISION, 'Bork!');
+ element.addEventListener(key + '-tap', e => {
+ assert.equal(e.detail.node.getAttribute('data-action-key'), key);
+ element.removeActionButton(key);
flush(() => {
- element.$.confirmRebase.fire('cancel');
- MockInteractions.tap(rebaseButton);
- assert.isTrue(fetchChangesStub.calledTwice);
- done();
- });
- });
-
- test('two dialogs are not shown at the same time', done => {
- element._hasKnownChainState = true;
- flush(() => {
- const rebaseButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="rebase"]');
- assert.ok(rebaseButton);
- MockInteractions.tap(rebaseButton);
- flushAsynchronousOperations();
- assert.isFalse(element.$.confirmRebase.hidden);
-
- element._handleCherrypickTap();
- flushAsynchronousOperations();
- assert.isTrue(element.$.confirmRebase.hidden);
- assert.isFalse(element.$.confirmCherrypick.hidden);
- done();
- });
- });
-
- test('fullscreen-overlay-opened hides content', () => {
- sandbox.spy(element, '_handleHideBackgroundContent');
- element.$.overlay.fire('fullscreen-overlay-opened');
- assert.isTrue(element._handleHideBackgroundContent.called);
- assert.isTrue(element.$.mainContent.classList.contains('overlayOpen'));
- });
-
- test('fullscreen-overlay-closed shows content', () => {
- sandbox.spy(element, '_handleShowBackgroundContent');
- element.$.overlay.fire('fullscreen-overlay-closed');
- assert.isTrue(element._handleShowBackgroundContent.called);
- assert.isFalse(element.$.mainContent.classList.contains('overlayOpen'));
- });
-
- test('_setLabelValuesOnRevert', () => {
- const labels = {'Foo': 1, 'Bar-Baz': -2};
- const changeId = 1234;
- sandbox.stub(element.$.jsAPI, 'getLabelValuesPostRevert').returns(labels);
- const saveStub = sandbox.stub(element.$.restAPI, 'saveChangeReview')
- .returns(Promise.resolve());
- return element._setLabelValuesOnRevert(changeId).then(() => {
- assert.isTrue(saveStub.calledOnce);
- assert.equal(saveStub.lastCall.args[0], changeId);
- assert.deepEqual(saveStub.lastCall.args[2], {labels});
- });
- });
-
- suite('change edits', () => {
- test('disableEdit', () => {
- element.set('editMode', false);
- element.set('editPatchsetLoaded', false);
- element.change = {status: 'NEW'};
- element.set('disableEdit', true);
- flushAsynchronousOperations();
-
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="publishEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="rebaseEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="deleteEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="stopEdit"]'));
- });
-
- test('shows confirm dialog for delete edit', () => {
- element.set('editMode', true);
- element.set('editPatchsetLoaded', true);
-
- const fireActionStub = sandbox.stub(element, '_fireAction');
- element._handleDeleteEditTap();
- assert.isFalse(element.$.confirmDeleteEditDialog.hidden);
- MockInteractions.tap(
- element.shadowRoot
- .querySelector('#confirmDeleteEditDialog')
- .shadowRoot
- .querySelector('gr-button[primary]'));
- flushAsynchronousOperations();
-
- assert.equal(fireActionStub.lastCall.args[0], '/edit');
- });
-
- test('hide publishEdit and rebaseEdit if change is not open', () => {
- element.set('editMode', true);
- element.set('editPatchsetLoaded', true);
- element.change = {status: 'MERGED'};
- flushAsynchronousOperations();
-
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="publishEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="rebaseEdit"]'));
- assert.isOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="deleteEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]'));
- });
-
- test('edit patchset is loaded, needs rebase', () => {
- element.set('editMode', true);
- element.set('editPatchsetLoaded', true);
- element.change = {status: 'NEW'};
- element.editBasedOnCurrentPatchSet = false;
- flushAsynchronousOperations();
-
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="publishEdit"]'));
- assert.isOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="rebaseEdit"]'));
- assert.isOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="deleteEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="stopEdit"]'));
- });
-
- test('edit patchset is loaded, does not need rebase', () => {
- element.set('editMode', true);
- element.set('editPatchsetLoaded', true);
- element.change = {status: 'NEW'};
- element.editBasedOnCurrentPatchSet = true;
- flushAsynchronousOperations();
-
- assert.isOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="publishEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="rebaseEdit"]'));
- assert.isOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="deleteEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="stopEdit"]'));
- });
-
- test('edit mode is loaded, no edit patchset', () => {
- element.set('editMode', true);
- element.set('editPatchsetLoaded', false);
- element.change = {status: 'NEW'};
- flushAsynchronousOperations();
-
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="publishEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="rebaseEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="deleteEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]'));
- assert.isOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="stopEdit"]'));
- });
-
- test('normal patch set', () => {
- element.set('editMode', false);
- element.set('editPatchsetLoaded', false);
- element.change = {status: 'NEW'};
- flushAsynchronousOperations();
-
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="publishEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="rebaseEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="deleteEdit"]'));
- assert.isOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="stopEdit"]'));
- });
-
- test('edit action', done => {
- element.addEventListener('edit-tap', () => { done(); });
- element.set('editMode', true);
- element.change = {status: 'NEW'};
- flushAsynchronousOperations();
-
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]'));
- assert.isOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="stopEdit"]'));
- element.change = {status: 'MERGED'};
- flushAsynchronousOperations();
-
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]'));
- element.change = {status: 'NEW'};
- element.set('editMode', false);
- flushAsynchronousOperations();
-
- const editButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]');
- assert.isOk(editButton);
- MockInteractions.tap(editButton);
- });
- });
-
- suite('cherry-pick', () => {
- let fireActionStub;
-
- setup(() => {
- fireActionStub = sandbox.stub(element, '_fireAction');
- sandbox.stub(window, 'alert');
- });
-
- test('works', () => {
- element._handleCherrypickTap();
- const action = {
- __key: 'cherrypick',
- __type: 'revision',
- __primary: false,
- enabled: true,
- label: 'Cherry pick',
- method: 'POST',
- title: 'Cherry pick change to a different branch',
- };
-
- element._handleCherrypickConfirm();
- assert.equal(fireActionStub.callCount, 0);
-
- element.$.confirmCherrypick.branch = 'master';
- element._handleCherrypickConfirm();
- assert.equal(fireActionStub.callCount, 0); // Still needs a message.
-
- // Add attributes that are used to determine the message.
- element.$.confirmCherrypick.commitMessage = 'foo message';
- element.$.confirmCherrypick.changeStatus = 'OPEN';
- element.$.confirmCherrypick.commitNum = '123';
-
- element._handleCherrypickConfirm();
-
- assert.equal(element.$.confirmCherrypick.$.messageInput.value,
- 'foo message');
-
- assert.deepEqual(fireActionStub.lastCall.args, [
- '/cherrypick', action, true, {
- destination: 'master',
- base: null,
- message: 'foo message',
- allow_conflicts: false,
- },
- ]);
- });
-
- test('cherry pick even with conflicts', () => {
- element._handleCherrypickTap();
- const action = {
- __key: 'cherrypick',
- __type: 'revision',
- __primary: false,
- enabled: true,
- label: 'Cherry pick',
- method: 'POST',
- title: 'Cherry pick change to a different branch',
- };
-
- element.$.confirmCherrypick.branch = 'master';
-
- // Add attributes that are used to determine the message.
- element.$.confirmCherrypick.commitMessage = 'foo message';
- element.$.confirmCherrypick.changeStatus = 'OPEN';
- element.$.confirmCherrypick.commitNum = '123';
-
- element._handleCherrypickConflictConfirm();
-
- assert.deepEqual(fireActionStub.lastCall.args, [
- '/cherrypick', action, true, {
- destination: 'master',
- base: null,
- message: 'foo message',
- allow_conflicts: true,
- },
- ]);
- });
-
- test('branch name cleared when re-open cherrypick', () => {
- const emptyBranchName = '';
- element.$.confirmCherrypick.branch = 'master';
-
- element._handleCherrypickTap();
- assert.equal(element.$.confirmCherrypick.branch, emptyBranchName);
- });
- });
-
- suite('move change', () => {
- let fireActionStub;
-
- setup(() => {
- fireActionStub = sandbox.stub(element, '_fireAction');
- sandbox.stub(window, 'alert');
- });
-
- test('works', () => {
- element._handleMoveTap();
-
- element._handleMoveConfirm();
- assert.equal(fireActionStub.callCount, 0);
-
- element.$.confirmMove.branch = 'master';
- element._handleMoveConfirm();
- assert.equal(fireActionStub.callCount, 1);
- });
-
- test('branch name cleared when re-open move', () => {
- const emptyBranchName = '';
- element.$.confirmMove.branch = 'master';
-
- element._handleMoveTap();
- assert.equal(element.$.confirmMove.branch, emptyBranchName);
- });
- });
-
- test('custom actions', done => {
- // Add a button with the same key as a server-based one to ensure
- // collisions are taken care of.
- const key = element.addActionButton(element.ActionType.REVISION, 'Bork!');
- element.addEventListener(key + '-tap', e => {
- assert.equal(e.detail.node.getAttribute('data-action-key'), key);
- element.removeActionButton(key);
- flush(() => {
- assert.notOk(element.shadowRoot
- .querySelector('[data-action-key="' + key + '"]'));
- done();
- });
- });
- flush(() => {
- MockInteractions.tap(element.shadowRoot
+ assert.notOk(element.shadowRoot
.querySelector('[data-action-key="' + key + '"]'));
+ done();
});
});
+ flush(() => {
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('[data-action-key="' + key + '"]'));
+ });
+ });
- test('_setLoadingOnButtonWithKey top-level', () => {
- const key = 'rebase';
- const type = 'revision';
- const cleanup = element._setLoadingOnButtonWithKey(type, key);
- assert.equal(element._actionLoadingMessage, 'Rebasing...');
+ test('_setLoadingOnButtonWithKey top-level', () => {
+ const key = 'rebase';
+ const type = 'revision';
+ const cleanup = element._setLoadingOnButtonWithKey(type, key);
+ assert.equal(element._actionLoadingMessage, 'Rebasing...');
- const button = element.shadowRoot
- .querySelector('[data-action-key="' + key + '"]');
- assert.isTrue(button.hasAttribute('loading'));
- assert.isTrue(button.disabled);
+ const button = element.shadowRoot
+ .querySelector('[data-action-key="' + key + '"]');
+ assert.isTrue(button.hasAttribute('loading'));
+ assert.isTrue(button.disabled);
- assert.isOk(cleanup);
- assert.isFunction(cleanup);
- cleanup();
+ assert.isOk(cleanup);
+ assert.isFunction(cleanup);
+ cleanup();
- assert.isFalse(button.hasAttribute('loading'));
- assert.isFalse(button.disabled);
- assert.isNotOk(element._actionLoadingMessage);
+ assert.isFalse(button.hasAttribute('loading'));
+ assert.isFalse(button.disabled);
+ assert.isNotOk(element._actionLoadingMessage);
+ });
+
+ test('_setLoadingOnButtonWithKey overflow menu', () => {
+ const key = 'cherrypick';
+ const type = 'revision';
+ const cleanup = element._setLoadingOnButtonWithKey(type, key);
+ assert.equal(element._actionLoadingMessage, 'Cherry-picking...');
+ assert.include(element._disabledMenuActions, 'cherrypick');
+ assert.isFunction(cleanup);
+
+ cleanup();
+
+ assert.notOk(element._actionLoadingMessage);
+ assert.notInclude(element._disabledMenuActions, 'cherrypick');
+ });
+
+ suite('abandon change', () => {
+ let alertStub;
+ let fireActionStub;
+
+ setup(() => {
+ fireActionStub = sandbox.stub(element, '_fireAction');
+ alertStub = sandbox.stub(window, 'alert');
+ element.actions = {
+ abandon: {
+ method: 'POST',
+ label: 'Abandon',
+ title: 'Abandon the change',
+ enabled: true,
+ },
+ };
+ return element.reload();
});
- test('_setLoadingOnButtonWithKey overflow menu', () => {
- const key = 'cherrypick';
- const type = 'revision';
- const cleanup = element._setLoadingOnButtonWithKey(type, key);
- assert.equal(element._actionLoadingMessage, 'Cherry-picking...');
- assert.include(element._disabledMenuActions, 'cherrypick');
- assert.isFunction(cleanup);
-
- cleanup();
-
- assert.notOk(element._actionLoadingMessage);
- assert.notInclude(element._disabledMenuActions, 'cherrypick');
- });
-
- suite('abandon change', () => {
- let alertStub;
- let fireActionStub;
-
- setup(() => {
- fireActionStub = sandbox.stub(element, '_fireAction');
- alertStub = sandbox.stub(window, 'alert');
- element.actions = {
- abandon: {
- method: 'POST',
- label: 'Abandon',
- title: 'Abandon the change',
- enabled: true,
- },
- };
- return element.reload();
- });
-
- test('abandon change with message', done => {
- const newAbandonMsg = 'Test Abandon Message';
- element.$.confirmAbandonDialog.message = newAbandonMsg;
- flush(() => {
- const abandonButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key="abandon"]');
- MockInteractions.tap(abandonButton);
-
- assert.equal(element.$.confirmAbandonDialog.message, newAbandonMsg);
- done();
- });
- });
-
- test('abandon change with no message', done => {
- flush(() => {
- const abandonButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key="abandon"]');
- MockInteractions.tap(abandonButton);
-
- assert.isUndefined(element.$.confirmAbandonDialog.message);
- done();
- });
- });
-
- test('works', () => {
- element.$.confirmAbandonDialog.message = 'original message';
- const restoreButton =
+ test('abandon change with message', done => {
+ const newAbandonMsg = 'Test Abandon Message';
+ element.$.confirmAbandonDialog.message = newAbandonMsg;
+ flush(() => {
+ const abandonButton =
element.shadowRoot
.querySelector('gr-button[data-action-key="abandon"]');
- MockInteractions.tap(restoreButton);
+ MockInteractions.tap(abandonButton);
- element.$.confirmAbandonDialog.message = 'foo message';
- element._handleAbandonDialogConfirm();
- assert.notOk(alertStub.called);
-
- const action = {
- __key: 'abandon',
- __type: 'change',
- __primary: false,
- enabled: true,
- label: 'Abandon',
- method: 'POST',
- title: 'Abandon the change',
- };
- assert.deepEqual(fireActionStub.lastCall.args, [
- '/abandon', action, false, {
- message: 'foo message',
- }]);
+ assert.equal(element.$.confirmAbandonDialog.message, newAbandonMsg);
+ done();
});
});
- suite('revert change', () => {
- let fireActionStub;
+ test('abandon change with no message', done => {
+ flush(() => {
+ const abandonButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key="abandon"]');
+ MockInteractions.tap(abandonButton);
- setup(() => {
- fireActionStub = sandbox.stub(element, '_fireAction');
- element.commitMessage = 'random commit message';
- element.change.current_revision = 'abcdef';
- element.actions = {
- revert: {
- method: 'POST',
- label: 'Revert',
- title: 'Revert the change',
- enabled: true,
- },
- };
- return element.reload();
+ assert.isUndefined(element.$.confirmAbandonDialog.message);
+ done();
});
+ });
- test('revert change with plugin hook', done => {
- const newRevertMsg = 'Modified revert msg';
- sandbox.stub(element.$.confirmRevertDialog, '_modifyRevertMsg',
- () => newRevertMsg);
+ test('works', () => {
+ element.$.confirmAbandonDialog.message = 'original message';
+ const restoreButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key="abandon"]');
+ MockInteractions.tap(restoreButton);
+
+ element.$.confirmAbandonDialog.message = 'foo message';
+ element._handleAbandonDialogConfirm();
+ assert.notOk(alertStub.called);
+
+ const action = {
+ __key: 'abandon',
+ __type: 'change',
+ __primary: false,
+ enabled: true,
+ label: 'Abandon',
+ method: 'POST',
+ title: 'Abandon the change',
+ };
+ assert.deepEqual(fireActionStub.lastCall.args, [
+ '/abandon', action, false, {
+ message: 'foo message',
+ }]);
+ });
+ });
+
+ suite('revert change', () => {
+ let fireActionStub;
+
+ setup(() => {
+ fireActionStub = sandbox.stub(element, '_fireAction');
+ element.commitMessage = 'random commit message';
+ element.change.current_revision = 'abcdef';
+ element.actions = {
+ revert: {
+ method: 'POST',
+ label: 'Revert',
+ title: 'Revert the change',
+ enabled: true,
+ },
+ };
+ return element.reload();
+ });
+
+ test('revert change with plugin hook', done => {
+ const newRevertMsg = 'Modified revert msg';
+ sandbox.stub(element.$.confirmRevertDialog, '_modifyRevertMsg',
+ () => newRevertMsg);
+ element.change = {
+ current_revision: 'abc1234',
+ };
+ sandbox.stub(element.$.restAPI, 'getChanges')
+ .returns(Promise.resolve([
+ {change_id: '12345678901234', topic: 'T', subject: 'random'},
+ {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
+ ]));
+ sandbox.stub(element.$.confirmRevertDialog,
+ '_populateRevertSubmissionMessage', () => 'original msg');
+ flush(() => {
+ const revertButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="revert"]');
+ MockInteractions.tap(revertButton);
+ flush(() => {
+ assert.equal(element.$.confirmRevertDialog._message, newRevertMsg);
+ done();
+ });
+ });
+ });
+
+ suite('revert change submitted together', () => {
+ let getChangesStub;
+ setup(() => {
element.change = {
- current_revision: 'abc1234',
+ submission_id: '199 0',
+ current_revision: '2000',
};
- sandbox.stub(element.$.restAPI, 'getChanges')
+ getChangesStub = sandbox.stub(element.$.restAPI, 'getChanges')
.returns(Promise.resolve([
{change_id: '12345678901234', topic: 'T', subject: 'random'},
{change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
]));
- sandbox.stub(element.$.confirmRevertDialog,
- '_populateRevertSubmissionMessage', () => 'original msg');
+ });
+
+ test('confirm revert dialog shows both options', done => {
+ const revertButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="revert"]');
+ MockInteractions.tap(revertButton);
flush(() => {
- const revertButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="revert"]');
- MockInteractions.tap(revertButton);
+ assert.equal(getChangesStub.args[0][1], 'submissionid: "199 0"');
+ const confirmRevertDialog = element.$.confirmRevertDialog;
+ const revertSingleChangeLabel = confirmRevertDialog
+ .shadowRoot.querySelector('.revertSingleChange');
+ const revertSubmissionLabel = confirmRevertDialog.
+ shadowRoot.querySelector('.revertSubmission');
+ assert(revertSingleChangeLabel.innerText.trim() ===
+ 'Revert single change');
+ assert(revertSubmissionLabel.innerText.trim() ===
+ 'Revert entire submission (2 Changes)');
+ let expectedMsg = 'Revert submission 199 0' + '\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>' + '\n' +
+ 'Reverted Changes:' + '\n' +
+ '1234567890:random' + '\n' +
+ '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
+ '\n';
+ assert.equal(confirmRevertDialog._message, expectedMsg);
+ const radioInputs = confirmRevertDialog.shadowRoot
+ .querySelectorAll('input[name="revertOptions"]');
+ MockInteractions.tap(radioInputs[0]);
flush(() => {
- assert.equal(element.$.confirmRevertDialog._message, newRevertMsg);
+ expectedMsg = 'Revert "random commit message"\n\nThis reverts '
+ + 'commit 2000.\n\nReason'
+ + ' for revert: <INSERT REASONING HERE>\n';
+ assert.equal(confirmRevertDialog._message, expectedMsg);
done();
});
});
});
- suite('revert change submitted together', () => {
+ test('submit fails if message is not edited', done => {
+ const revertButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="revert"]');
+ const confirmRevertDialog = element.$.confirmRevertDialog;
+ MockInteractions.tap(revertButton);
+ const fireStub = sandbox.stub(confirmRevertDialog, 'fire');
+ flush(() => {
+ const confirmButton = element.$.confirmRevertDialog.shadowRoot
+ .querySelector('gr-dialog')
+ .shadowRoot.querySelector('#confirm');
+ MockInteractions.tap(confirmButton);
+ flush(() => {
+ assert.isTrue(confirmRevertDialog._showErrorMessage);
+ assert.isFalse(fireStub.called);
+ done();
+ });
+ });
+ });
+
+ test('message modification is retained on switching', done => {
+ const revertButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="revert"]');
+ const confirmRevertDialog = element.$.confirmRevertDialog;
+ MockInteractions.tap(revertButton);
+ flush(() => {
+ const radioInputs = confirmRevertDialog.shadowRoot
+ .querySelectorAll('input[name="revertOptions"]');
+ const revertSubmissionMsg = 'Revert submission 199 0' + '\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>' + '\n' +
+ 'Reverted Changes:' + '\n' +
+ '1234567890:random' + '\n' +
+ '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
+ '\n';
+ const singleChangeMsg =
+ 'Revert "random commit message"\n\nThis reverts '
+ + 'commit 2000.\n\nReason'
+ + ' for revert: <INSERT REASONING HERE>\n';
+ assert.equal(confirmRevertDialog._message, revertSubmissionMsg);
+ const newRevertMsg = revertSubmissionMsg + 'random';
+ const newSingleChangeMsg = singleChangeMsg + 'random';
+ confirmRevertDialog._message = newRevertMsg;
+ MockInteractions.tap(radioInputs[0]);
+ flush(() => {
+ assert.equal(confirmRevertDialog._message, singleChangeMsg);
+ confirmRevertDialog._message = newSingleChangeMsg;
+ MockInteractions.tap(radioInputs[1]);
+ flush(() => {
+ assert.equal(confirmRevertDialog._message, newRevertMsg);
+ MockInteractions.tap(radioInputs[0]);
+ flush(() => {
+ assert.equal(
+ confirmRevertDialog._message,
+ newSingleChangeMsg
+ );
+ done();
+ });
+ });
+ });
+ });
+ });
+ });
+
+ suite('revert single change', () => {
+ setup(() => {
+ element.change = {
+ submission_id: '199',
+ current_revision: '2000',
+ };
+ sandbox.stub(element.$.restAPI, 'getChanges')
+ .returns(Promise.resolve([
+ {change_id: '12345678901234', topic: 'T', subject: 'random'},
+ ]));
+ });
+
+ test('submit fails if message is not edited', done => {
+ const revertButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="revert"]');
+ const confirmRevertDialog = element.$.confirmRevertDialog;
+ MockInteractions.tap(revertButton);
+ const fireStub = sandbox.stub(confirmRevertDialog, 'fire');
+ flush(() => {
+ const confirmButton = element.$.confirmRevertDialog.shadowRoot
+ .querySelector('gr-dialog')
+ .shadowRoot.querySelector('#confirm');
+ MockInteractions.tap(confirmButton);
+ flush(() => {
+ assert.isTrue(confirmRevertDialog._showErrorMessage);
+ assert.isFalse(fireStub.called);
+ done();
+ });
+ });
+ });
+
+ test('confirm revert dialog shows no radio button', done => {
+ const revertButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="revert"]');
+ MockInteractions.tap(revertButton);
+ flush(() => {
+ const confirmRevertDialog = element.$.confirmRevertDialog;
+ const radioInputs = confirmRevertDialog.shadowRoot
+ .querySelectorAll('input[name="revertOptions"]');
+ assert.equal(radioInputs.length, 0);
+ const msg = 'Revert "random commit message"\n\n'
+ + 'This reverts commit 2000.\n\nReason '
+ + 'for revert: <INSERT REASONING HERE>\n';
+ assert.equal(confirmRevertDialog._message, msg);
+ const editedMsg = msg + 'hello';
+ confirmRevertDialog._message += 'hello';
+ const confirmButton = element.$.confirmRevertDialog.shadowRoot
+ .querySelector('gr-dialog')
+ .shadowRoot.querySelector('#confirm');
+ MockInteractions.tap(confirmButton);
+ flush(() => {
+ assert.equal(fireActionStub.getCall(0).args[0], '/revert');
+ assert.equal(fireActionStub.getCall(0).args[1].__key, 'revert');
+ assert.equal(fireActionStub.getCall(0).args[3].message,
+ editedMsg);
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ suite('mark change private', () => {
+ setup(() => {
+ const privateAction = {
+ __key: 'private',
+ __type: 'change',
+ __primary: false,
+ method: 'POST',
+ label: 'Mark private',
+ title: 'Working...',
+ enabled: true,
+ };
+
+ element.actions = {
+ private: privateAction,
+ };
+
+ element.change.is_private = false;
+
+ element.changeNum = '2';
+ element.latestPatchNum = '2';
+
+ return element.reload();
+ });
+
+ test('make sure the mark private change button is not outside of the ' +
+ 'overflow menu', done => {
+ flush(() => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('[data-action-key="private"]'));
+ done();
+ });
+ });
+
+ test('private change', done => {
+ flush(() => {
+ assert.isOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="private-change"]'));
+ element.setActionOverflow('change', 'private', false);
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="private"]'));
+ assert.isNotOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="private-change"]'));
+ done();
+ });
+ });
+ });
+
+ suite('unmark private change', () => {
+ setup(() => {
+ const unmarkPrivateAction = {
+ __key: 'private.delete',
+ __type: 'change',
+ __primary: false,
+ method: 'POST',
+ label: 'Unmark private',
+ title: 'Working...',
+ enabled: true,
+ };
+
+ element.actions = {
+ 'private.delete': unmarkPrivateAction,
+ };
+
+ element.change.is_private = true;
+
+ element.changeNum = '2';
+ element.latestPatchNum = '2';
+
+ return element.reload();
+ });
+
+ test('make sure the unmark private change button is not outside of the ' +
+ 'overflow menu', done => {
+ flush(() => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('[data-action-key="private.delete"]'));
+ done();
+ });
+ });
+
+ test('unmark the private change', done => {
+ flush(() => {
+ assert.isOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="private.delete-change"]')
+ );
+ element.setActionOverflow('change', 'private.delete', false);
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="private.delete"]'));
+ assert.isNotOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="private.delete-change"]')
+ );
+ done();
+ });
+ });
+ });
+
+ suite('delete change', () => {
+ let fireActionStub;
+ let deleteAction;
+
+ setup(() => {
+ fireActionStub = sandbox.stub(element, '_fireAction');
+ element.change = {
+ current_revision: 'abc1234',
+ };
+ deleteAction = {
+ method: 'DELETE',
+ label: 'Delete Change',
+ title: 'Delete change X_X',
+ enabled: true,
+ };
+ element.actions = {
+ '/': deleteAction,
+ };
+ });
+
+ test('does not delete on action', () => {
+ element._handleDeleteTap();
+ assert.isFalse(fireActionStub.called);
+ });
+
+ test('shows confirm dialog', () => {
+ element._handleDeleteTap();
+ assert.isFalse(element.shadowRoot
+ .querySelector('#confirmDeleteDialog').hidden);
+ MockInteractions.tap(
+ element.shadowRoot
+ .querySelector('#confirmDeleteDialog')
+ .shadowRoot
+ .querySelector('gr-button[primary]'));
+ flushAsynchronousOperations();
+ assert.isTrue(fireActionStub.calledWith('/', deleteAction, false));
+ });
+
+ test('hides delete confirm on cancel', () => {
+ element._handleDeleteTap();
+ MockInteractions.tap(
+ element.shadowRoot
+ .querySelector('#confirmDeleteDialog')
+ .shadowRoot
+ .querySelector('gr-button:not([primary])'));
+ flushAsynchronousOperations();
+ assert.isTrue(element.shadowRoot
+ .querySelector('#confirmDeleteDialog').hidden);
+ assert.isFalse(fireActionStub.called);
+ });
+ });
+
+ suite('ignore change', () => {
+ setup(done => {
+ sandbox.stub(element, '_fireAction');
+
+ const IgnoreAction = {
+ __key: 'ignore',
+ __type: 'change',
+ __primary: false,
+ method: 'PUT',
+ label: 'Ignore',
+ title: 'Working...',
+ enabled: true,
+ };
+
+ element.actions = {
+ ignore: IgnoreAction,
+ };
+
+ element.changeNum = '2';
+ element.latestPatchNum = '2';
+
+ element.reload().then(() => { flush(done); });
+ });
+
+ test('make sure the ignore button is not outside of the overflow menu',
+ () => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('[data-action-key="ignore"]'));
+ });
+
+ test('ignoring change', () => {
+ assert.isOk(element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="ignore-change"]'));
+ element.setActionOverflow('change', 'ignore', false);
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="ignore"]'));
+ assert.isNotOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="ignore-change"]'));
+ });
+ });
+
+ suite('unignore change', () => {
+ setup(done => {
+ sandbox.stub(element, '_fireAction');
+
+ const UnignoreAction = {
+ __key: 'unignore',
+ __type: 'change',
+ __primary: false,
+ method: 'PUT',
+ label: 'Unignore',
+ title: 'Working...',
+ enabled: true,
+ };
+
+ element.actions = {
+ unignore: UnignoreAction,
+ };
+
+ element.changeNum = '2';
+ element.latestPatchNum = '2';
+
+ element.reload().then(() => { flush(done); });
+ });
+
+ test('unignore button is not outside of the overflow menu', () => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('[data-action-key="unignore"]'));
+ });
+
+ test('unignoring change', () => {
+ assert.isOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="unignore-change"]'));
+ element.setActionOverflow('change', 'unignore', false);
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="unignore"]'));
+ assert.isNotOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="unignore-change"]'));
+ });
+ });
+
+ suite('reviewed change', () => {
+ setup(done => {
+ sandbox.stub(element, '_fireAction');
+
+ const ReviewedAction = {
+ __key: 'reviewed',
+ __type: 'change',
+ __primary: false,
+ method: 'PUT',
+ label: 'Mark reviewed',
+ title: 'Working...',
+ enabled: true,
+ };
+
+ element.actions = {
+ reviewed: ReviewedAction,
+ };
+
+ element.changeNum = '2';
+ element.latestPatchNum = '2';
+
+ element.reload().then(() => { flush(done); });
+ });
+
+ test('make sure the reviewed button is not outside of the overflow menu',
+ () => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('[data-action-key="reviewed"]'));
+ });
+
+ test('reviewing change', () => {
+ assert.isOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="reviewed-change"]'));
+ element.setActionOverflow('change', 'reviewed', false);
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="reviewed"]'));
+ assert.isNotOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="reviewed-change"]'));
+ });
+ });
+
+ suite('unreviewed change', () => {
+ setup(done => {
+ sandbox.stub(element, '_fireAction');
+
+ const UnreviewedAction = {
+ __key: 'unreviewed',
+ __type: 'change',
+ __primary: false,
+ method: 'PUT',
+ label: 'Mark unreviewed',
+ title: 'Working...',
+ enabled: true,
+ };
+
+ element.actions = {
+ unreviewed: UnreviewedAction,
+ };
+
+ element.changeNum = '2';
+ element.latestPatchNum = '2';
+
+ element.reload().then(() => { flush(done); });
+ });
+
+ test('unreviewed button not outside of the overflow menu', () => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('[data-action-key="unreviewed"]'));
+ });
+
+ test('unreviewed change', () => {
+ assert.isOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="unreviewed-change"]'));
+ element.setActionOverflow('change', 'unreviewed', false);
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="unreviewed"]'));
+ assert.isNotOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="unreviewed-change"]'));
+ });
+ });
+
+ suite('quick approve', () => {
+ setup(() => {
+ element.change = {
+ current_revision: 'abc1234',
+ };
+ element.change = {
+ current_revision: 'abc1234',
+ labels: {
+ foo: {
+ values: {
+ '-1': '',
+ ' 0': '',
+ '+1': '',
+ },
+ },
+ },
+ permitted_labels: {
+ foo: ['-1', ' 0', '+1'],
+ },
+ };
+ flushAsynchronousOperations();
+ });
+
+ test('added when can approve', () => {
+ const approveButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.isNotNull(approveButton);
+ });
+
+ test('hide quick approve', () => {
+ const approveButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.isNotNull(approveButton);
+ assert.isFalse(element._hideQuickApproveAction);
+
+ // Assert approve button gets removed from list of buttons.
+ element.hideQuickApproveAction();
+ flushAsynchronousOperations();
+ const approveButtonUpdated =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.isNull(approveButtonUpdated);
+ assert.isTrue(element._hideQuickApproveAction);
+ });
+
+ test('is first in list of secondary actions', () => {
+ const approveButton = element.$.secondaryActions
+ .querySelector('gr-button');
+ assert.equal(approveButton.getAttribute('data-label'), 'foo+1');
+ });
+
+ test('not added when already approved', () => {
+ element.change = {
+ current_revision: 'abc1234',
+ labels: {
+ foo: {
+ approved: {},
+ values: {},
+ },
+ },
+ permitted_labels: {
+ foo: [' 0', '+1'],
+ },
+ };
+ flushAsynchronousOperations();
+ const approveButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.isNull(approveButton);
+ });
+
+ test('not added when label not permitted', () => {
+ element.change = {
+ current_revision: 'abc1234',
+ labels: {
+ foo: {values: {}},
+ },
+ permitted_labels: {
+ bar: [],
+ },
+ };
+ flushAsynchronousOperations();
+ const approveButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.isNull(approveButton);
+ });
+
+ test('approves when tapped', () => {
+ const fireActionStub = sandbox.stub(element, '_fireAction');
+ MockInteractions.tap(
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']'));
+ flushAsynchronousOperations();
+ assert.isTrue(fireActionStub.called);
+ assert.isTrue(fireActionStub.calledWith('/review'));
+ const payload = fireActionStub.lastCall.args[3];
+ assert.deepEqual(payload.labels, {foo: '+1'});
+ });
+
+ test('not added when multiple labels are required', () => {
+ element.change = {
+ current_revision: 'abc1234',
+ labels: {
+ foo: {values: {}},
+ bar: {values: {}},
+ },
+ permitted_labels: {
+ foo: [' 0', '+1'],
+ bar: [' 0', '+1', '+2'],
+ },
+ };
+ flushAsynchronousOperations();
+ const approveButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.isNull(approveButton);
+ });
+
+ test('button label for missing approval', () => {
+ element.change = {
+ current_revision: 'abc1234',
+ labels: {
+ foo: {
+ values: {
+ ' 0': '',
+ '+1': '',
+ },
+ },
+ bar: {approved: {}, values: {}},
+ },
+ permitted_labels: {
+ foo: [' 0', '+1'],
+ bar: [' 0', '+1', '+2'],
+ },
+ };
+ flushAsynchronousOperations();
+ const approveButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.equal(approveButton.getAttribute('data-label'), 'foo+1');
+ });
+
+ test('no quick approve if score is not maximal for a label', () => {
+ element.change = {
+ current_revision: 'abc1234',
+ labels: {
+ bar: {
+ value: 1,
+ values: {
+ ' 0': '',
+ '+1': '',
+ '+2': '',
+ },
+ },
+ },
+ permitted_labels: {
+ bar: [' 0', '+1'],
+ },
+ };
+ flushAsynchronousOperations();
+ const approveButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.isNull(approveButton);
+ });
+
+ test('approving label with a non-max score', () => {
+ element.change = {
+ current_revision: 'abc1234',
+ labels: {
+ bar: {
+ value: 1,
+ values: {
+ ' 0': '',
+ '+1': '',
+ '+2': '',
+ },
+ },
+ },
+ permitted_labels: {
+ bar: [' 0', '+1', '+2'],
+ },
+ };
+ flushAsynchronousOperations();
+ const approveButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.equal(approveButton.getAttribute('data-label'), 'bar+2');
+ });
+ });
+
+ test('adds download revision action', () => {
+ const handler = sandbox.stub();
+ element.addEventListener('download-tap', handler);
+ assert.ok(element.revisionActions.download);
+ element._handleDownloadTap();
+ flushAsynchronousOperations();
+
+ assert.isTrue(handler.called);
+ });
+
+ test('changing changeNum or patchNum does not reload', () => {
+ const reloadStub = sandbox.stub(element, 'reload');
+ element.changeNum = 123;
+ assert.isFalse(reloadStub.called);
+ element.latestPatchNum = 456;
+ assert.isFalse(reloadStub.called);
+ });
+
+ test('_toSentenceCase', () => {
+ assert.equal(element._toSentenceCase('blah blah'), 'Blah blah');
+ assert.equal(element._toSentenceCase('BLAH BLAH'), 'Blah blah');
+ assert.equal(element._toSentenceCase('b'), 'B');
+ assert.equal(element._toSentenceCase(''), '');
+ assert.equal(element._toSentenceCase('!@#$%^&*()'), '!@#$%^&*()');
+ });
+
+ suite('setActionOverflow', () => {
+ test('move action from overflow', () => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('[data-action-key="cherrypick"]'));
+ assert.strictEqual(
+ element.$.moreActions.items[0].id, 'cherrypick-revision');
+ element.setActionOverflow('revision', 'cherrypick', false);
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="cherrypick"]'));
+ assert.notEqual(
+ element.$.moreActions.items[0].id, 'cherrypick-revision');
+ });
+
+ test('move action to overflow', () => {
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="submit"]'));
+ element.setActionOverflow('revision', 'submit', true);
+ flushAsynchronousOperations();
+ assert.isNotOk(element.shadowRoot
+ .querySelector('[data-action-key="submit"]'));
+ assert.strictEqual(
+ element.$.moreActions.items[3].id, 'submit-revision');
+ });
+
+ suite('_waitForChangeReachable', () => {
+ setup(() => {
+ sandbox.stub(element, 'async', fn => fn());
+ });
+
+ const makeGetChange = numTries => () => {
+ if (numTries === 1) {
+ return Promise.resolve({_number: 123});
+ } else {
+ numTries--;
+ return Promise.resolve(undefined);
+ }
+ };
+
+ test('succeed', () => {
+ sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(5));
+ return element._waitForChangeReachable(123).then(success => {
+ assert.isTrue(success);
+ });
+ });
+
+ test('fail', () => {
+ sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(6));
+ return element._waitForChangeReachable(123).then(success => {
+ assert.isFalse(success);
+ });
+ });
+ });
+ });
+
+ suite('_send', () => {
+ let cleanup;
+ let payload;
+ let onShowError;
+ let onShowAlert;
+ let getResponseObjectStub;
+
+ setup(() => {
+ cleanup = sinon.stub();
+ element.changeNum = 42;
+ element.latestPatchNum = 12;
+ payload = {foo: 'bar'};
+
+ onShowError = sinon.stub();
+ element.addEventListener('show-error', onShowError);
+ onShowAlert = sinon.stub();
+ element.addEventListener('show-alert', onShowAlert);
+ });
+
+ suite('happy path', () => {
+ let sendStub;
+ setup(() => {
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
+ sendStub = sandbox.stub(element.$.restAPI, 'executeChangeAction')
+ .returns(Promise.resolve({}));
+ getResponseObjectStub = sandbox.stub(element.$.restAPI,
+ 'getResponseObject');
+ sandbox.stub(Gerrit.Nav,
+ 'navigateToChange').returns(Promise.resolve(true));
+ });
+
+ test('change action', done => {
+ element
+ ._send('DELETE', payload, '/endpoint', false, cleanup)
+ .then(() => {
+ assert.isFalse(onShowError.called);
+ assert.isTrue(cleanup.calledOnce);
+ assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint',
+ null, payload));
+ done();
+ });
+ });
+
+ suite('show revert submission dialog', () => {
setup(() => {
- element.change = {
- submission_id: '199',
- current_revision: '2000',
- };
+ element.change.submission_id = '199';
+ element.change.current_revision = '2000';
sandbox.stub(element.$.restAPI, 'getChanges')
.returns(Promise.resolve([
{change_id: '12345678901234', topic: 'T', subject: 'random'},
@@ -923,1047 +1735,198 @@
]));
});
- test('confirm revert dialog shows both options', done => {
- const revertButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="revert"]');
- MockInteractions.tap(revertButton);
- flush(() => {
- const confirmRevertDialog = element.$.confirmRevertDialog;
- const revertSingleChangeLabel = confirmRevertDialog
- .shadowRoot.querySelector('.revertSingleChange');
- const revertSubmissionLabel = confirmRevertDialog.
- shadowRoot.querySelector('.revertSubmission');
- assert(revertSingleChangeLabel.innerText.trim() ===
- 'Revert single change');
- assert(revertSubmissionLabel.innerText.trim() ===
- 'Revert entire submission (2 Changes)');
- let expectedMsg = 'Revert submission 199' + '\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>' + '\n' +
- 'Reverted Changes:' + '\n' +
- '1234567890:random' + '\n' +
- '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
- '\n';
- assert.equal(confirmRevertDialog._message, expectedMsg);
- const radioInputs = confirmRevertDialog.shadowRoot
- .querySelectorAll('input[name="revertOptions"]');
- MockInteractions.tap(radioInputs[0]);
- flush(() => {
- expectedMsg = 'Revert "random commit message"\n\nThis reverts '
- + 'commit 2000.\n\nReason'
- + ' for revert: <INSERT REASONING HERE>\n';
- assert.equal(confirmRevertDialog._message, expectedMsg);
- done();
- });
- });
- });
-
- test('submit fails if message is not edited', done => {
- const revertButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="revert"]');
- const confirmRevertDialog = element.$.confirmRevertDialog;
- MockInteractions.tap(revertButton);
- const fireStub = sandbox.stub(confirmRevertDialog, 'fire');
- flush(() => {
- const confirmButton = element.$.confirmRevertDialog.shadowRoot
- .querySelector('gr-dialog')
- .shadowRoot.querySelector('#confirm');
- MockInteractions.tap(confirmButton);
- flush(() => {
- assert.isTrue(confirmRevertDialog._showErrorMessage);
- assert.isFalse(fireStub.called);
- done();
- });
- });
- });
-
- test('message modification is retained on switching', done => {
- const revertButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="revert"]');
- const confirmRevertDialog = element.$.confirmRevertDialog;
- MockInteractions.tap(revertButton);
- flush(() => {
- const radioInputs = confirmRevertDialog.shadowRoot
- .querySelectorAll('input[name="revertOptions"]');
- const revertSubmissionMsg = 'Revert submission 199' + '\n\n' +
+ test('revert submission shows submissionId', done => {
+ const expectedMsg = 'Revert submission 199' + '\n\n' +
'Reason for revert: <INSERT REASONING HERE>' + '\n' +
'Reverted Changes:' + '\n' +
- '1234567890:random' + '\n' +
- '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
+ '1234567890: random' + '\n' +
+ '23456: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
'\n';
- const singleChangeMsg =
- 'Revert "random commit message"\n\nThis reverts '
- + 'commit 2000.\n\nReason'
- + ' for revert: <INSERT REASONING HERE>\n';
- assert.equal(confirmRevertDialog._message, revertSubmissionMsg);
- const newRevertMsg = revertSubmissionMsg + 'random';
- const newSingleChangeMsg = singleChangeMsg + 'random';
- confirmRevertDialog._message = newRevertMsg;
- MockInteractions.tap(radioInputs[0]);
- flush(() => {
- assert.equal(confirmRevertDialog._message, singleChangeMsg);
- confirmRevertDialog._message = newSingleChangeMsg;
- MockInteractions.tap(radioInputs[1]);
- flush(() => {
- assert.equal(confirmRevertDialog._message, newRevertMsg);
- MockInteractions.tap(radioInputs[0]);
- flush(() => {
- assert.equal(
- confirmRevertDialog._message,
- newSingleChangeMsg
- );
+ const modifiedMsg = expectedMsg + 'abcd';
+ sandbox.stub(element.$.confirmRevertSubmissionDialog,
+ '_modifyRevertSubmissionMsg').returns(modifiedMsg);
+ element.showRevertSubmissionDialog();
+ flush(() => {
+ const msg = element.$.confirmRevertSubmissionDialog.message;
+ assert.equal(msg, modifiedMsg);
+ done();
+ });
+ });
+ });
+
+ suite('single changes revert', () => {
+ let navigateToSearchQueryStub;
+ setup(() => {
+ getResponseObjectStub
+ .returns(Promise.resolve({revert_changes: [
+ {change_id: 12345},
+ ]}));
+ navigateToSearchQueryStub = sandbox.stub(Gerrit.Nav,
+ 'navigateToSearchQuery');
+ });
+
+ test('revert submission single change', done => {
+ element._send('POST', {message: 'Revert submission'},
+ '/revert_submission', false, cleanup).then(res => {
+ element._handleResponse({__key: 'revert_submission'}, {}).
+ then(() => {
+ assert.isTrue(navigateToSearchQueryStub.called);
done();
});
- });
- });
});
});
});
- suite('revert single change', () => {
+ suite('multiple changes revert', () => {
+ let showActionDialogStub;
+ let navigateToSearchQueryStub;
setup(() => {
- element.change = {
- submission_id: '199',
- current_revision: '2000',
- };
- sandbox.stub(element.$.restAPI, 'getChanges')
- .returns(Promise.resolve([
- {change_id: '12345678901234', topic: 'T', subject: 'random'},
- ]));
+ getResponseObjectStub
+ .returns(Promise.resolve({revert_changes: [
+ {change_id: 12345, topic: 'T'},
+ {change_id: 23456, topic: 'T'},
+ ]}));
+ showActionDialogStub = sandbox.stub(element, '_showActionDialog');
+ navigateToSearchQueryStub = sandbox.stub(Gerrit.Nav,
+ 'navigateToSearchQuery');
});
- test('submit fails if message is not edited', done => {
- const revertButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="revert"]');
- const confirmRevertDialog = element.$.confirmRevertDialog;
- MockInteractions.tap(revertButton);
- const fireStub = sandbox.stub(confirmRevertDialog, 'fire');
- flush(() => {
- const confirmButton = element.$.confirmRevertDialog.shadowRoot
- .querySelector('gr-dialog')
- .shadowRoot.querySelector('#confirm');
- MockInteractions.tap(confirmButton);
- flush(() => {
- assert.isTrue(confirmRevertDialog._showErrorMessage);
- assert.isFalse(fireStub.called);
+ test('revert submission multiple change', done => {
+ element._send('POST', {message: 'Revert submission'},
+ '/revert_submission', false, cleanup).then(res => {
+ element._handleResponse({__key: 'revert_submission'}, {}).then(
+ () => {
+ assert.isFalse(showActionDialogStub.called);
+ assert.isTrue(navigateToSearchQueryStub.calledWith(
+ 'topic: T'));
+ done();
+ });
+ });
+ });
+ });
+
+ test('revision action', done => {
+ element
+ ._send('DELETE', payload, '/endpoint', true, cleanup)
+ .then(() => {
+ assert.isFalse(onShowError.called);
+ assert.isTrue(cleanup.calledOnce);
+ assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint',
+ 12, payload));
done();
});
- });
- });
+ });
+ });
- test('confirm revert dialog shows no radio button', done => {
- const revertButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="revert"]');
- MockInteractions.tap(revertButton);
- flush(() => {
- const confirmRevertDialog = element.$.confirmRevertDialog;
- const radioInputs = confirmRevertDialog.shadowRoot
- .querySelectorAll('input[name="revertOptions"]');
- assert.equal(radioInputs.length, 0);
- const msg = 'Revert "random commit message"\n\n'
- + 'This reverts commit 2000.\n\nReason '
- + 'for revert: <INSERT REASONING HERE>\n';
- assert.equal(confirmRevertDialog._message, msg);
- const editedMsg = msg + 'hello';
- confirmRevertDialog._message += 'hello';
- const confirmButton = element.$.confirmRevertDialog.shadowRoot
- .querySelector('gr-dialog')
- .shadowRoot.querySelector('#confirm');
- MockInteractions.tap(confirmButton);
- flush(() => {
- assert.equal(fireActionStub.getCall(0).args[0], '/revert');
- assert.equal(fireActionStub.getCall(0).args[1].__key, 'revert');
- assert.equal(fireActionStub.getCall(0).args[3].message,
- editedMsg);
- done();
+ suite('failure modes', () => {
+ test('non-latest', () => {
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: false}));
+ const sendStub = sandbox.stub(element.$.restAPI,
+ 'executeChangeAction');
+
+ return element._send('DELETE', payload, '/endpoint', true, cleanup)
+ .then(() => {
+ assert.isTrue(onShowAlert.calledOnce);
+ assert.isFalse(onShowError.called);
+ assert.isTrue(cleanup.calledOnce);
+ assert.isFalse(sendStub.called);
});
- });
- });
- });
- });
-
- suite('mark change private', () => {
- setup(() => {
- const privateAction = {
- __key: 'private',
- __type: 'change',
- __primary: false,
- method: 'POST',
- label: 'Mark private',
- title: 'Working...',
- enabled: true,
- };
-
- element.actions = {
- private: privateAction,
- };
-
- element.change.is_private = false;
-
- element.changeNum = '2';
- element.latestPatchNum = '2';
-
- return element.reload();
});
- test('make sure the mark private change button is not outside of the ' +
- 'overflow menu', done => {
- flush(() => {
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="private"]'));
- done();
- });
- });
-
- test('private change', done => {
- flush(() => {
- assert.isOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="private-change"]'));
- element.setActionOverflow('change', 'private', false);
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('[data-action-key="private"]'));
- assert.isNotOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="private-change"]'));
- done();
- });
- });
- });
-
- suite('unmark private change', () => {
- setup(() => {
- const unmarkPrivateAction = {
- __key: 'private.delete',
- __type: 'change',
- __primary: false,
- method: 'POST',
- label: 'Unmark private',
- title: 'Working...',
- enabled: true,
- };
-
- element.actions = {
- 'private.delete': unmarkPrivateAction,
- };
-
- element.change.is_private = true;
-
- element.changeNum = '2';
- element.latestPatchNum = '2';
-
- return element.reload();
- });
-
- test('make sure the unmark private change button is not outside of the ' +
- 'overflow menu', done => {
- flush(() => {
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="private.delete"]'));
- done();
- });
- });
-
- test('unmark the private change', done => {
- flush(() => {
- assert.isOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="private.delete-change"]')
- );
- element.setActionOverflow('change', 'private.delete', false);
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('[data-action-key="private.delete"]'));
- assert.isNotOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="private.delete-change"]')
- );
- done();
- });
- });
- });
-
- suite('delete change', () => {
- let fireActionStub;
- let deleteAction;
-
- setup(() => {
- fireActionStub = sandbox.stub(element, '_fireAction');
- element.change = {
- current_revision: 'abc1234',
- };
- deleteAction = {
- method: 'DELETE',
- label: 'Delete Change',
- title: 'Delete change X_X',
- enabled: true,
- };
- element.actions = {
- '/': deleteAction,
- };
- });
-
- test('does not delete on action', () => {
- element._handleDeleteTap();
- assert.isFalse(fireActionStub.called);
- });
-
- test('shows confirm dialog', () => {
- element._handleDeleteTap();
- assert.isFalse(element.shadowRoot
- .querySelector('#confirmDeleteDialog').hidden);
- MockInteractions.tap(
- element.shadowRoot
- .querySelector('#confirmDeleteDialog')
- .shadowRoot
- .querySelector('gr-button[primary]'));
- flushAsynchronousOperations();
- assert.isTrue(fireActionStub.calledWith('/', deleteAction, false));
- });
-
- test('hides delete confirm on cancel', () => {
- element._handleDeleteTap();
- MockInteractions.tap(
- element.shadowRoot
- .querySelector('#confirmDeleteDialog')
- .shadowRoot
- .querySelector('gr-button:not([primary])'));
- flushAsynchronousOperations();
- assert.isTrue(element.shadowRoot
- .querySelector('#confirmDeleteDialog').hidden);
- assert.isFalse(fireActionStub.called);
- });
- });
-
- suite('ignore change', () => {
- setup(done => {
- sandbox.stub(element, '_fireAction');
-
- const IgnoreAction = {
- __key: 'ignore',
- __type: 'change',
- __primary: false,
- method: 'PUT',
- label: 'Ignore',
- title: 'Working...',
- enabled: true,
- };
-
- element.actions = {
- ignore: IgnoreAction,
- };
-
- element.changeNum = '2';
- element.latestPatchNum = '2';
-
- element.reload().then(() => { flush(done); });
- });
-
- test('make sure the ignore button is not outside of the overflow menu',
- () => {
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="ignore"]'));
- });
-
- test('ignoring change', () => {
- assert.isOk(element.$.moreActions.shadowRoot
- .querySelector('span[data-id="ignore-change"]'));
- element.setActionOverflow('change', 'ignore', false);
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('[data-action-key="ignore"]'));
- assert.isNotOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="ignore-change"]'));
- });
- });
-
- suite('unignore change', () => {
- setup(done => {
- sandbox.stub(element, '_fireAction');
-
- const UnignoreAction = {
- __key: 'unignore',
- __type: 'change',
- __primary: false,
- method: 'PUT',
- label: 'Unignore',
- title: 'Working...',
- enabled: true,
- };
-
- element.actions = {
- unignore: UnignoreAction,
- };
-
- element.changeNum = '2';
- element.latestPatchNum = '2';
-
- element.reload().then(() => { flush(done); });
- });
-
- test('unignore button is not outside of the overflow menu', () => {
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="unignore"]'));
- });
-
- test('unignoring change', () => {
- assert.isOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="unignore-change"]'));
- element.setActionOverflow('change', 'unignore', false);
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('[data-action-key="unignore"]'));
- assert.isNotOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="unignore-change"]'));
- });
- });
-
- suite('reviewed change', () => {
- setup(done => {
- sandbox.stub(element, '_fireAction');
-
- const ReviewedAction = {
- __key: 'reviewed',
- __type: 'change',
- __primary: false,
- method: 'PUT',
- label: 'Mark reviewed',
- title: 'Working...',
- enabled: true,
- };
-
- element.actions = {
- reviewed: ReviewedAction,
- };
-
- element.changeNum = '2';
- element.latestPatchNum = '2';
-
- element.reload().then(() => { flush(done); });
- });
-
- test('make sure the reviewed button is not outside of the overflow menu',
- () => {
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="reviewed"]'));
- });
-
- test('reviewing change', () => {
- assert.isOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="reviewed-change"]'));
- element.setActionOverflow('change', 'reviewed', false);
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('[data-action-key="reviewed"]'));
- assert.isNotOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="reviewed-change"]'));
- });
- });
-
- suite('unreviewed change', () => {
- setup(done => {
- sandbox.stub(element, '_fireAction');
-
- const UnreviewedAction = {
- __key: 'unreviewed',
- __type: 'change',
- __primary: false,
- method: 'PUT',
- label: 'Mark unreviewed',
- title: 'Working...',
- enabled: true,
- };
-
- element.actions = {
- unreviewed: UnreviewedAction,
- };
-
- element.changeNum = '2';
- element.latestPatchNum = '2';
-
- element.reload().then(() => { flush(done); });
- });
-
- test('unreviewed button not outside of the overflow menu', () => {
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="unreviewed"]'));
- });
-
- test('unreviewed change', () => {
- assert.isOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="unreviewed-change"]'));
- element.setActionOverflow('change', 'unreviewed', false);
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('[data-action-key="unreviewed"]'));
- assert.isNotOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="unreviewed-change"]'));
- });
- });
-
- suite('quick approve', () => {
- setup(() => {
- element.change = {
- current_revision: 'abc1234',
- };
- element.change = {
- current_revision: 'abc1234',
- labels: {
- foo: {
- values: {
- '-1': '',
- ' 0': '',
- '+1': '',
- },
- },
- },
- permitted_labels: {
- foo: ['-1', ' 0', '+1'],
- },
- };
- flushAsynchronousOperations();
- });
-
- test('added when can approve', () => {
- const approveButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.isNotNull(approveButton);
- });
-
- test('hide quick approve', () => {
- const approveButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.isNotNull(approveButton);
- assert.isFalse(element._hideQuickApproveAction);
-
- // Assert approve button gets removed from list of buttons.
- element.hideQuickApproveAction();
- flushAsynchronousOperations();
- const approveButtonUpdated =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.isNull(approveButtonUpdated);
- assert.isTrue(element._hideQuickApproveAction);
- });
-
- test('is first in list of secondary actions', () => {
- const approveButton = element.$.secondaryActions
- .querySelector('gr-button');
- assert.equal(approveButton.getAttribute('data-label'), 'foo+1');
- });
-
- test('not added when already approved', () => {
- element.change = {
- current_revision: 'abc1234',
- labels: {
- foo: {
- approved: {},
- values: {},
- },
- },
- permitted_labels: {
- foo: [' 0', '+1'],
- },
- };
- flushAsynchronousOperations();
- const approveButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.isNull(approveButton);
- });
-
- test('not added when label not permitted', () => {
- element.change = {
- current_revision: 'abc1234',
- labels: {
- foo: {values: {}},
- },
- permitted_labels: {
- bar: [],
- },
- };
- flushAsynchronousOperations();
- const approveButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.isNull(approveButton);
- });
-
- test('approves when tapped', () => {
- const fireActionStub = sandbox.stub(element, '_fireAction');
- MockInteractions.tap(
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']'));
- flushAsynchronousOperations();
- assert.isTrue(fireActionStub.called);
- assert.isTrue(fireActionStub.calledWith('/review'));
- const payload = fireActionStub.lastCall.args[3];
- assert.deepEqual(payload.labels, {foo: '+1'});
- });
-
- test('not added when multiple labels are required', () => {
- element.change = {
- current_revision: 'abc1234',
- labels: {
- foo: {values: {}},
- bar: {values: {}},
- },
- permitted_labels: {
- foo: [' 0', '+1'],
- bar: [' 0', '+1', '+2'],
- },
- };
- flushAsynchronousOperations();
- const approveButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.isNull(approveButton);
- });
-
- test('button label for missing approval', () => {
- element.change = {
- current_revision: 'abc1234',
- labels: {
- foo: {
- values: {
- ' 0': '',
- '+1': '',
- },
- },
- bar: {approved: {}, values: {}},
- },
- permitted_labels: {
- foo: [' 0', '+1'],
- bar: [' 0', '+1', '+2'],
- },
- };
- flushAsynchronousOperations();
- const approveButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.equal(approveButton.getAttribute('data-label'), 'foo+1');
- });
-
- test('no quick approve if score is not maximal for a label', () => {
- element.change = {
- current_revision: 'abc1234',
- labels: {
- bar: {
- value: 1,
- values: {
- ' 0': '',
- '+1': '',
- '+2': '',
- },
- },
- },
- permitted_labels: {
- bar: [' 0', '+1'],
- },
- };
- flushAsynchronousOperations();
- const approveButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.isNull(approveButton);
- });
-
- test('approving label with a non-max score', () => {
- element.change = {
- current_revision: 'abc1234',
- labels: {
- bar: {
- value: 1,
- values: {
- ' 0': '',
- '+1': '',
- '+2': '',
- },
- },
- },
- permitted_labels: {
- bar: [' 0', '+1', '+2'],
- },
- };
- flushAsynchronousOperations();
- const approveButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.equal(approveButton.getAttribute('data-label'), 'bar+2');
- });
- });
-
- test('adds download revision action', () => {
- const handler = sandbox.stub();
- element.addEventListener('download-tap', handler);
- assert.ok(element.revisionActions.download);
- element._handleDownloadTap();
- flushAsynchronousOperations();
-
- assert.isTrue(handler.called);
- });
-
- test('changing changeNum or patchNum does not reload', () => {
- const reloadStub = sandbox.stub(element, 'reload');
- element.changeNum = 123;
- assert.isFalse(reloadStub.called);
- element.latestPatchNum = 456;
- assert.isFalse(reloadStub.called);
- });
-
- test('_toSentenceCase', () => {
- assert.equal(element._toSentenceCase('blah blah'), 'Blah blah');
- assert.equal(element._toSentenceCase('BLAH BLAH'), 'Blah blah');
- assert.equal(element._toSentenceCase('b'), 'B');
- assert.equal(element._toSentenceCase(''), '');
- assert.equal(element._toSentenceCase('!@#$%^&*()'), '!@#$%^&*()');
- });
-
- suite('setActionOverflow', () => {
- test('move action from overflow', () => {
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="cherrypick"]'));
- assert.strictEqual(
- element.$.moreActions.items[0].id, 'cherrypick-revision');
- element.setActionOverflow('revision', 'cherrypick', false);
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('[data-action-key="cherrypick"]'));
- assert.notEqual(
- element.$.moreActions.items[0].id, 'cherrypick-revision');
- });
-
- test('move action to overflow', () => {
- assert.isOk(element.shadowRoot
- .querySelector('[data-action-key="submit"]'));
- element.setActionOverflow('revision', 'submit', true);
- flushAsynchronousOperations();
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="submit"]'));
- assert.strictEqual(
- element.$.moreActions.items[3].id, 'submit-revision');
- });
-
- suite('_waitForChangeReachable', () => {
- setup(() => {
- sandbox.stub(element, 'async', fn => fn());
- });
-
- const makeGetChange = numTries => () => {
- if (numTries === 1) {
- return Promise.resolve({_number: 123});
- } else {
- numTries--;
- return Promise.resolve(undefined);
- }
- };
-
- test('succeed', () => {
- sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(5));
- return element._waitForChangeReachable(123).then(success => {
- assert.isTrue(success);
- });
- });
-
- test('fail', () => {
- sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(6));
- return element._waitForChangeReachable(123).then(success => {
- assert.isFalse(success);
- });
- });
- });
- });
-
- suite('_send', () => {
- let cleanup;
- let payload;
- let onShowError;
- let onShowAlert;
- let getResponseObjectStub;
-
- setup(() => {
- cleanup = sinon.stub();
- element.changeNum = 42;
- element.latestPatchNum = 12;
- payload = {foo: 'bar'};
-
- onShowError = sinon.stub();
- element.addEventListener('show-error', onShowError);
- onShowAlert = sinon.stub();
- element.addEventListener('show-alert', onShowAlert);
- });
-
- suite('happy path', () => {
- let sendStub;
- setup(() => {
- sandbox.stub(element, 'fetchChangeUpdates')
- .returns(Promise.resolve({isLatest: true}));
- sendStub = sandbox.stub(element.$.restAPI, 'executeChangeAction')
- .returns(Promise.resolve({}));
- getResponseObjectStub = sandbox.stub(element.$.restAPI,
- 'getResponseObject');
- sandbox.stub(Gerrit.Nav,
- 'navigateToChange').returns(Promise.resolve(true));
- });
-
- test('change action', done => {
- element
- ._send('DELETE', payload, '/endpoint', false, cleanup)
- .then(() => {
- assert.isFalse(onShowError.called);
- assert.isTrue(cleanup.calledOnce);
- assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint',
- null, payload));
- done();
- });
- });
-
- suite('show revert submission dialog', () => {
- setup(() => {
- element.change.submission_id = '199';
- element.change.current_revision = '2000';
- sandbox.stub(element.$.restAPI, 'getChanges')
- .returns(Promise.resolve([
- {change_id: '12345678901234', topic: 'T', subject: 'random'},
- {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
- ]));
- });
-
- test('revert submission shows submissionId', done => {
- const expectedMsg = 'Revert submission 199' + '\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>' + '\n' +
- 'Reverted Changes:' + '\n' +
- '1234567890: random' + '\n' +
- '23456: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
- '\n';
- const modifiedMsg = expectedMsg + 'abcd';
- sandbox.stub(element.$.confirmRevertSubmissionDialog,
- '_modifyRevertSubmissionMsg').returns(modifiedMsg);
- element.showRevertSubmissionDialog();
- flush(() => {
- const msg = element.$.confirmRevertSubmissionDialog.message;
- assert.equal(msg, modifiedMsg);
- done();
+ test('send fails', () => {
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
+ const sendStub = sandbox.stub(element.$.restAPI,
+ 'executeChangeAction',
+ (num, method, patchNum, endpoint, payload, onErr) => {
+ onErr();
+ return Promise.resolve(null);
});
- });
- });
+ const handleErrorStub = sandbox.stub(element, '_handleResponseError');
- suite('single changes revert', () => {
- let navigateToSearchQueryStub;
- setup(() => {
- getResponseObjectStub
- .returns(Promise.resolve({revert_changes: [
- {change_id: 12345},
- ]}));
- navigateToSearchQueryStub = sandbox.stub(Gerrit.Nav,
- 'navigateToSearchQuery');
- });
-
- test('revert submission single change', done => {
- element._send('POST', {message: 'Revert submission'},
- '/revert_submission', false, cleanup).then(res => {
- element._handleResponse({__key: 'revert_submission'}, {}).
- then(() => {
- assert.isTrue(navigateToSearchQueryStub.called);
- done();
- });
+ return element._send('DELETE', payload, '/endpoint', true, cleanup)
+ .then(() => {
+ assert.isFalse(onShowError.called);
+ assert.isTrue(cleanup.called);
+ assert.isTrue(sendStub.calledOnce);
+ assert.isTrue(handleErrorStub.called);
});
- });
- });
-
- suite('multiple changes revert', () => {
- let showActionDialogStub;
- let navigateToSearchQueryStub;
- setup(() => {
- getResponseObjectStub
- .returns(Promise.resolve({revert_changes: [
- {change_id: 12345, topic: 'T'},
- {change_id: 23456, topic: 'T'},
- ]}));
- showActionDialogStub = sandbox.stub(element, '_showActionDialog');
- navigateToSearchQueryStub = sandbox.stub(Gerrit.Nav,
- 'navigateToSearchQuery');
- });
-
- test('revert submission multiple change', done => {
- element._send('POST', {message: 'Revert submission'},
- '/revert_submission', false, cleanup).then(res => {
- element._handleResponse({__key: 'revert_submission'}, {}).then(
- () => {
- assert.isFalse(showActionDialogStub.called);
- assert.isTrue(navigateToSearchQueryStub.calledWith(
- 'topic: T'));
- done();
- });
- });
- });
- });
-
- test('revision action', done => {
- element
- ._send('DELETE', payload, '/endpoint', true, cleanup)
- .then(() => {
- assert.isFalse(onShowError.called);
- assert.isTrue(cleanup.calledOnce);
- assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint',
- 12, payload));
- done();
- });
- });
});
-
- suite('failure modes', () => {
- test('non-latest', () => {
- sandbox.stub(element, 'fetchChangeUpdates')
- .returns(Promise.resolve({isLatest: false}));
- const sendStub = sandbox.stub(element.$.restAPI,
- 'executeChangeAction');
-
- return element._send('DELETE', payload, '/endpoint', true, cleanup)
- .then(() => {
- assert.isTrue(onShowAlert.calledOnce);
- assert.isFalse(onShowError.called);
- assert.isTrue(cleanup.calledOnce);
- assert.isFalse(sendStub.called);
- });
- });
-
- test('send fails', () => {
- sandbox.stub(element, 'fetchChangeUpdates')
- .returns(Promise.resolve({isLatest: true}));
- const sendStub = sandbox.stub(element.$.restAPI,
- 'executeChangeAction',
- (num, method, patchNum, endpoint, payload, onErr) => {
- onErr();
- return Promise.resolve(null);
- });
- const handleErrorStub = sandbox.stub(element, '_handleResponseError');
-
- return element._send('DELETE', payload, '/endpoint', true, cleanup)
- .then(() => {
- assert.isFalse(onShowError.called);
- assert.isTrue(cleanup.called);
- assert.isTrue(sendStub.calledOnce);
- assert.isTrue(handleErrorStub.called);
- });
- });
- });
- });
-
- test('_handleAction reports', () => {
- sandbox.stub(element, '_fireAction');
- const reportStub = sandbox.stub(element.$.reporting, 'reportInteraction');
- element._handleAction('type', 'key');
- assert.isTrue(reportStub.called);
- assert.equal(reportStub.lastCall.args[0], 'type-key');
});
});
- suite('getChangeRevisionActions returns only some actions', () => {
- let element;
- let sandbox;
- let changeRevisionActions;
-
- setup(() => {
- stub('gr-rest-api-interface', {
- getChangeRevisionActions() {
- return Promise.resolve(changeRevisionActions);
- },
- send(method, url, payload) {
- return Promise.reject(new Error('error'));
- },
- getProjectConfig() { return Promise.resolve({}); },
- });
-
- sandbox = sinon.sandbox.create();
- sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
-
- element = fixture('basic');
- // getChangeRevisionActions is not called without
- // set the following properies
- element.change = {};
- element.changeNum = '42';
- element.latestPatchNum = '2';
-
- sandbox.stub(element.$.confirmCherrypick.$.restAPI,
- 'getRepoBranches').returns(Promise.resolve([]));
- sandbox.stub(element.$.confirmMove.$.restAPI,
- 'getRepoBranches').returns(Promise.resolve([]));
- return element.reload();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('confirmSubmitDialog and confirmRebase properties are changed', () => {
- changeRevisionActions = {};
- element.reload();
- assert.strictEqual(element.$.confirmSubmitDialog.action, null);
- assert.strictEqual(element.$.confirmRebase.rebaseOnCurrent, null);
- });
-
- test('_updateRebaseAction sets _parentIsCurrent on no rebase', () => {
- const currentRevisionActions = {
- cherrypick: {
- enabled: true,
- label: 'Cherry Pick',
- method: 'POST',
- title: 'cherrypick',
- },
- };
- element._parentIsCurrent = undefined;
- element._updateRebaseAction(currentRevisionActions);
- assert.isTrue(element._parentIsCurrent);
- });
-
- test('_updateRebaseAction', () => {
- const currentRevisionActions = {
- cherrypick: {
- enabled: true,
- label: 'Cherry Pick',
- method: 'POST',
- title: 'cherrypick',
- },
- rebase: {
- enabled: true,
- label: 'Rebase',
- method: 'POST',
- title: 'Rebase onto tip of branch or parent change',
- },
- };
- element._parentIsCurrent = undefined;
-
- // Rebase enabled should always end up true.
- // When rebase is enabled initially, rebaseOnCurrent should be set to
- // true.
- assert.equal(element._updateRebaseAction(currentRevisionActions),
- currentRevisionActions);
-
- assert.isTrue(currentRevisionActions.rebase.enabled);
- assert.isTrue(currentRevisionActions.rebase.rebaseOnCurrent);
- assert.isFalse(element._parentIsCurrent);
-
- delete currentRevisionActions.rebase.enabled;
-
- // When rebase is not enabled initially, rebaseOnCurrent should be set to
- // false.
- assert.equal(element._updateRebaseAction(currentRevisionActions),
- currentRevisionActions);
-
- assert.isTrue(currentRevisionActions.rebase.enabled);
- assert.isFalse(currentRevisionActions.rebase.rebaseOnCurrent);
- assert.isTrue(element._parentIsCurrent);
- });
+ test('_handleAction reports', () => {
+ sandbox.stub(element, '_fireAction');
+ const reportStub = sandbox.stub(element.$.reporting, 'reportInteraction');
+ element._handleAction('type', 'key');
+ assert.isTrue(reportStub.called);
+ assert.equal(reportStub.lastCall.args[0], 'type-key');
});
});
+
+ suite('getChangeRevisionActions returns only some actions', () => {
+ let element;
+ let sandbox;
+ let changeRevisionActions;
+
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getChangeRevisionActions() {
+ return Promise.resolve(changeRevisionActions);
+ },
+ send(method, url, payload) {
+ return Promise.reject(new Error('error'));
+ },
+ getProjectConfig() { return Promise.resolve({}); },
+ });
+
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
+
+ element = fixture('basic');
+ // getChangeRevisionActions is not called without
+ // set the following properies
+ element.change = {};
+ element.changeNum = '42';
+ element.latestPatchNum = '2';
+
+ sandbox.stub(element.$.confirmCherrypick.$.restAPI,
+ 'getRepoBranches').returns(Promise.resolve([]));
+ sandbox.stub(element.$.confirmMove.$.restAPI,
+ 'getRepoBranches').returns(Promise.resolve([]));
+ return element.reload();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('confirmSubmitDialog and confirmRebase properties are changed', () => {
+ changeRevisionActions = {};
+ element.reload();
+ assert.strictEqual(element.$.confirmSubmitDialog.action, null);
+ assert.strictEqual(element.$.confirmRebase.rebaseOnCurrent, null);
+ });
+
+ test('_computeRebaseOnCurrent', () => {
+ const rebaseAction = {
+ enabled: true,
+ label: 'Rebase',
+ method: 'POST',
+ title: 'Rebase onto tip of branch or parent change',
+ };
+
+ // When rebase is enabled initially, rebaseOnCurrent should be set to
+ // true.
+ assert.isTrue(element._computeRebaseOnCurrent(rebaseAction));
+
+ delete rebaseAction.enabled;
+
+ // When rebase is not enabled initially, rebaseOnCurrent should be set to
+ // false.
+ assert.isFalse(element._computeRebaseOnCurrent(rebaseAction));
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
index 3269dc0..160b912 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
@@ -19,16 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-metadata</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="../../plugins/gr-plugin-host/gr-plugin-host.html">
-<link rel="import" href="gr-change-metadata.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="element">
<template>
@@ -42,139 +36,142 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-metadata integration tests', async () => {
- await readyToTest();
- let sandbox;
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../plugins/gr-plugin-host/gr-plugin-host.js';
+import './gr-change-metadata.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-change-metadata integration tests', () => {
+ let sandbox;
+ let element;
- const sectionSelectors = [
- 'section.assignee',
- 'section.strategy',
- 'section.topic',
- ];
+ const sectionSelectors = [
+ 'section.assignee',
+ 'section.strategy',
+ 'section.topic',
+ ];
- const labels = {
- CI: {
- all: [
- {value: 1, name: 'user 2', _account_id: 1},
- {value: 2, name: 'user '},
- ],
- values: {
- ' 0': 'Don\'t submit as-is',
- '+1': 'No score',
- '+2': 'Looks good to me',
- },
+ const labels = {
+ CI: {
+ all: [
+ {value: 1, name: 'user 2', _account_id: 1},
+ {value: 2, name: 'user '},
+ ],
+ values: {
+ ' 0': 'Don\'t submit as-is',
+ '+1': 'No score',
+ '+2': 'Looks good to me',
},
- };
+ },
+ };
- const getStyle = function(selector, name) {
- return window.getComputedStyle(
- Polymer.dom(element.root).querySelector(selector))[name];
- };
+ const getStyle = function(selector, name) {
+ return window.getComputedStyle(
+ dom(element.root).querySelector(selector))[name];
+ };
- function createElement() {
- const element = fixture('element');
- element.change = {labels, status: 'NEW'};
- element.revision = {};
- return element;
- }
+ function createElement() {
+ const element = fixture('element');
+ element.change = {labels, status: 'NEW'};
+ element.revision = {};
+ return element;
+ }
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getLoggedIn() { return Promise.resolve(false); },
- deleteVote() { return Promise.resolve({ok: true}); },
- });
- });
-
- teardown(() => {
- sandbox.restore();
- Gerrit._testOnly_resetPlugins();
- });
-
- suite('by default', () => {
- setup(done => {
- element = createElement();
- flush(done);
- });
-
- for (const sectionSelector of sectionSelectors) {
- test(sectionSelector + ' does not have display: none', () => {
- assert.notEqual(getStyle(sectionSelector, 'display'), 'none');
- });
- }
- });
-
- suite('with plugin style', () => {
- setup(done => {
- Gerrit._testOnly_resetPlugins();
- const pluginHost = fixture('plugin-host');
- pluginHost.config = {
- plugin: {
- js_resource_paths: [],
- html_resource_paths: [
- new URL('test/plugin.html?' + Math.random(),
- window.location.href).toString(),
- ],
- },
- };
- element = createElement();
- const importSpy = sandbox.spy(element.$.externalStyle, '_import');
- Gerrit.awaitPluginsLoaded().then(() => {
- Promise.all(importSpy.returnValues).then(() => {
- flush(done);
- });
- });
- });
-
- for (const sectionSelector of sectionSelectors) {
- test(sectionSelector + ' may have display: none', () => {
- assert.equal(getStyle(sectionSelector, 'display'), 'none');
- });
- }
- });
-
- suite('label updates', () => {
- let plugin;
-
- setup(() => {
- Gerrit.install(p => plugin = p, '0.1',
- new URL('test/plugin.html?' + Math.random(),
- window.location.href).toString());
- sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
- Gerrit._loadPlugins([]);
- element = createElement();
- });
-
- test('labels changed callback', done => {
- let callCount = 0;
- const labelChangeSpy = sandbox.spy(arg => {
- callCount++;
- if (callCount === 1) {
- assert.deepEqual(arg, labels);
- assert.equal(arg.CI.all.length, 2);
- element.set(['change', 'labels'], {
- CI: {
- all: [
- {value: 1, name: 'user 2', _account_id: 1},
- ],
- values: {
- ' 0': 'Don\'t submit as-is',
- '+1': 'No score',
- '+2': 'Looks good to me',
- },
- },
- });
- } else if (callCount === 2) {
- assert.equal(arg.CI.all.length, 1);
- done();
- }
- });
-
- plugin.changeMetadata().onLabelsChanged(labelChangeSpy);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getLoggedIn() { return Promise.resolve(false); },
+ deleteVote() { return Promise.resolve({ok: true}); },
});
});
+
+ teardown(() => {
+ sandbox.restore();
+ Gerrit._testOnly_resetPlugins();
+ });
+
+ suite('by default', () => {
+ setup(done => {
+ element = createElement();
+ flush(done);
+ });
+
+ for (const sectionSelector of sectionSelectors) {
+ test(sectionSelector + ' does not have display: none', () => {
+ assert.notEqual(getStyle(sectionSelector, 'display'), 'none');
+ });
+ }
+ });
+
+ suite('with plugin style', () => {
+ setup(done => {
+ Gerrit._testOnly_resetPlugins();
+ const pluginHost = fixture('plugin-host');
+ pluginHost.config = {
+ plugin: {
+ js_resource_paths: [],
+ html_resource_paths: [
+ new URL('test/plugin.html?' + Math.random(),
+ window.location.href).toString(),
+ ],
+ },
+ };
+ element = createElement();
+ const importSpy = sandbox.spy(element.$.externalStyle, '_import');
+ Gerrit.awaitPluginsLoaded().then(() => {
+ Promise.all(importSpy.returnValues).then(() => {
+ flush(done);
+ });
+ });
+ });
+
+ for (const sectionSelector of sectionSelectors) {
+ test(sectionSelector + ' may have display: none', () => {
+ assert.equal(getStyle(sectionSelector, 'display'), 'none');
+ });
+ }
+ });
+
+ suite('label updates', () => {
+ let plugin;
+
+ setup(() => {
+ Gerrit.install(p => plugin = p, '0.1',
+ new URL('test/plugin.html?' + Math.random(),
+ window.location.href).toString());
+ sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
+ Gerrit._loadPlugins([]);
+ element = createElement();
+ });
+
+ test('labels changed callback', done => {
+ let callCount = 0;
+ const labelChangeSpy = sandbox.spy(arg => {
+ callCount++;
+ if (callCount === 1) {
+ assert.deepEqual(arg, labels);
+ assert.equal(arg.CI.all.length, 2);
+ element.set(['change', 'labels'], {
+ CI: {
+ all: [
+ {value: 1, name: 'user 2', _account_id: 1},
+ ],
+ values: {
+ ' 0': 'Don\'t submit as-is',
+ '+1': 'No score',
+ '+2': 'Looks good to me',
+ },
+ },
+ });
+ } else if (callCount === 2) {
+ assert.equal(arg.CI.all.length, 1);
+ done();
+ }
+ });
+
+ plugin.changeMetadata().onLabelsChanged(labelChangeSpy);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
deleted file mode 100644
index ad3b621..0000000
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ /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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../../styles/gr-change-metadata-shared-styles.html">
-<link rel="import" href="../../../styles/gr-change-view-integration-shared-styles.html">
-<link rel="import" href="../../../styles/gr-voting-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="../../plugins/gr-external-style/gr-external-style.html">
-<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
-<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-limited-text/gr-limited-text.html">
-<link rel="import" href="../../shared/gr-linked-chip/gr-linked-chip.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-change-requirements/gr-change-requirements.html">
-<link rel="import" href="../gr-commit-info/gr-commit-info.html">
-<link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html">
-<link rel="import" href="../../shared/gr-account-list/gr-account-list.html">
-<script src="../../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
-<script src="../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js"></script>
-
-<dom-module id="gr-change-metadata">
- <template>
- <style include="gr-change-metadata-shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- :host {
- display: table;
- }
- gr-change-requirements {
- --requirements-horizontal-padding: var(--metadata-horizontal-padding);
- }
- gr-account-link {
- max-width: 20ch;
- overflow: hidden;
- text-overflow: ellipsis;
- vertical-align: top;
- white-space: nowrap;
- }
- gr-editable-label {
- max-width: 9em;
- }
- .webLink {
- display: block;
- }
- /* CSS Mixins should be applied last. */
- section.assignee {
- @apply --change-metadata-assignee;
- }
- section.strategy {
- @apply --change-metadata-strategy;
- }
- section.topic {
- @apply --change-metadata-topic;
- }
- gr-account-chip[disabled],
- gr-linked-chip[disabled] {
- opacity: 0;
- pointer-events: none;
- }
- .hashtagChip {
- margin-bottom: var(--spacing-m);
- }
- #externalStyle {
- display: block;
- }
- .parentList.merge {
- list-style-type: decimal;
- padding-left: var(--spacing-l);
- }
- .parentList gr-commit-info {
- display: inline-block;
- }
- .hideDisplay,
- #parentNotCurrentMessage {
- display: none;
- }
- .icon {
- margin: -3px 0;
- }
- .icon.help,
- .icon.notTrusted {
- color: #FFA62F;
- }
- .icon.invalid {
- color: var(--vote-text-color-disliked);
- }
- .icon.trusted {
- color: var(--vote-text-color-recommended);
- }
- .parentList.notCurrent.nonMerge #parentNotCurrentMessage {
- --arrow-color: #ffa62f;
- display: inline-block;
- }
- .separatedSection {
- margin-top: var(--spacing-l);
- padding: var(--spacing-m) 0;
- }
- .hashtag gr-linked-chip,
- .topic gr-linked-chip {
- --linked-chip-text-color: var(--link-color);
- }
- </style>
- <gr-external-style id="externalStyle" name="change-metadata">
- <section>
- <span class="title">Updated</span>
- <span class="value">
- <gr-date-formatter
- has-tooltip
- date-str="[[change.updated]]"></gr-date-formatter>
- </span>
- </section>
- <section>
- <span class="title">Owner</span>
- <span class="value">
- <gr-account-link account="[[change.owner]]"></gr-account-link>
- <template is="dom-if" if="[[_pushCertificateValidation]]">
- <gr-tooltip-content
- has-tooltip
- title$="[[_pushCertificateValidation.message]]">
- <iron-icon
- class$="icon [[_pushCertificateValidation.class]]"
- icon="[[_pushCertificateValidation.icon]]">
- </iron-icon>
- </gr-tooltip-content>
- </template>
- </span>
- </section>
- <section class$="[[_computeShowRoleClass(change, _CHANGE_ROLE.UPLOADER)]]">
- <span class="title">Uploader</span>
- <span class="value">
- <gr-account-link
- account="[[_getNonOwnerRole(change, _CHANGE_ROLE.UPLOADER)]]"
- ></gr-account-link>
- </span>
- </section>
- <section class$="[[_computeShowRoleClass(change, _CHANGE_ROLE.AUTHOR)]]">
- <span class="title">Author</span>
- <span class="value">
- <gr-account-link
- account="[[_getNonOwnerRole(change, _CHANGE_ROLE.AUTHOR)]]"
- ></gr-account-link>
- </span>
- </section>
- <section class$="[[_computeShowRoleClass(change, _CHANGE_ROLE.COMMITTER)]]">
- <span class="title">Committer</span>
- <span class="value">
- <gr-account-link
- account="[[_getNonOwnerRole(change, _CHANGE_ROLE.COMMITTER)]]"
- ></gr-account-link>
- </span>
- </section>
- <section class="assignee">
- <span class="title">Assignee</span>
- <span class="value">
- <gr-account-list
- id="assigneeValue"
- placeholder="Set assignee..."
- max-count="1"
- skip-suggest-on-empty
- accounts="{{_assignee}}"
- readonly="[[_computeAssigneeReadOnly(_mutable, change)]]"
- suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
- </gr-account-list>
- </span>
- </section>
- <section>
- <span class="title">Reviewers</span>
- <span class="value">
- <gr-reviewer-list
- change="{{change}}"
- mutable="[[_mutable]]"
- reviewers-only
- max-reviewers-displayed="3"></gr-reviewer-list>
- </span>
- </section>
- <section>
- <span class="title">CC</span>
- <span class="value">
- <gr-reviewer-list
- change="{{change}}"
- mutable="[[_mutable]]"
- ccs-only
- max-reviewers-displayed="3"></gr-reviewer-list>
- </span>
- </section>
- <template is="dom-if"
- if="[[_computeShowRepoBranchTogether(change.project, change.branch)]]">
- <section>
- <span class="title">Repo / Branch</span>
- <span class="value">
- <a href$="[[_computeProjectUrl(change.project)]]">[[change.project]]</a>
- /
- <a href$="[[_computeBranchUrl(change.project, change.branch)]]">[[change.branch]]</a>
- </span>
- </section>
- </template>
- <template is="dom-if"
- if="[[!_computeShowRepoBranchTogether(change.project, change.branch)]]">
- <section>
- <span class="title">Repo</span>
- <span class="value">
- <a href$="[[_computeProjectUrl(change.project)]]">
- <gr-limited-text limit="40" text="[[change.project]]"></gr-limited-text>
- </a>
- </span>
- </section>
- <section>
- <span class="title">Branch</span>
- <span class="value">
- <a href$="[[_computeBranchUrl(change.project, change.branch)]]">
- <gr-limited-text limit="40" text="[[change.branch]]"></gr-limited-text>
- </a>
- </span>
- </section>
- </template>
- <section>
- <span class="title">[[_computeParentsLabel(_currentParents)]]</span>
- <span class="value">
- <ol class$="[[_computeParentListClass(_currentParents, parentIsCurrent)]]">
- <template is="dom-repeat" items="[[_currentParents]]" as="parent">
- <li>
- <gr-commit-info
- change="[[change]]"
- commit-info="[[parent]]"
- server-config="[[serverConfig]]"></gr-commit-info>
- <gr-tooltip-content
- id="parentNotCurrentMessage"
- has-tooltip
- show-icon
- title$="[[_notCurrentMessage]]"></gr-tooltip-content>
- </li>
- </template>
- </ol>
- </span>
- </section>
- <section class="topic">
- <span class="title">Topic</span>
- <span class="value">
- <template
- is="dom-if"
- if="[[_showTopicChip(change.*, _settingTopic)]]">
- <gr-linked-chip
- text="[[change.topic]]"
- limit="40"
- href="[[_computeTopicUrl(change.topic)]]"
- removable="[[!_topicReadOnly]]"
- on-remove="_handleTopicRemoved"></gr-linked-chip>
- </template>
- <template
- is="dom-if"
- if="[[_showAddTopic(change.*, _settingTopic)]]">
- <gr-editable-label
- class="topicEditableLabel"
- label-text="Add a topic"
- value="[[change.topic]]"
- max-length="1024"
- placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]"
- read-only="[[_topicReadOnly]]"
- on-changed="_handleTopicChanged"></gr-editable-label>
- </template>
- </span>
- </section>
- <template is="dom-if" if="[[_showCherryPickOf(change.*)]]">
- <section>
- <span class="title">Cherry pick of</span>
- <span class="value">
- <a href$="[[_computeCherryPickOfUrl(change.cherry_pick_of_change, change.cherry_pick_of_patch_set, change.project)]]">
- <gr-limited-text
- text="[[change.cherry_pick_of_change]],[[change.cherry_pick_of_patch_set]]"
- limit="40">
- </gr-limited-text>
- </a>
- </span>
- </section>
- </template>
- <section class="strategy" hidden$="[[_computeHideStrategy(change)]]" hidden>
- <span class="title">Strategy</span>
- <span class="value">[[_computeStrategy(change)]]</span>
- </section>
- <section class="hashtag">
- <span class="title">Hashtags</span>
- <span class="value">
- <template is="dom-repeat" items="[[change.hashtags]]">
- <gr-linked-chip
- class="hashtagChip"
- text="[[item]]"
- href="[[_computeHashtagUrl(item)]]"
- removable="[[!_hashtagReadOnly]]"
- on-remove="_handleHashtagRemoved">
- </gr-linked-chip>
- </template>
- <template is="dom-if" if="[[!_hashtagReadOnly]]">
- <gr-editable-label
- uppercase
- label-text="Add a hashtag"
- value="{{_newHashtag}}"
- placeholder="[[_computeHashtagPlaceholder(_hashtagReadOnly)]]"
- read-only="[[_hashtagReadOnly]]"
- on-changed="_handleHashtagChanged"></gr-editable-label>
- </template>
- </span>
- </section>
- <div class="separatedSection">
- <gr-change-requirements
- change="{{change}}"
- account="[[account]]"
- mutable="[[_mutable]]"></gr-change-requirements>
- </div>
- <section id="webLinks" hidden$="[[!_computeWebLinks(commitInfo, serverConfig)]]">
- <span class="title">Links</span>
- <span class="value">
- <template is="dom-repeat"
- items="[[_computeWebLinks(commitInfo, serverConfig)]]" as="link">
- <a href="[[link.url]]" class="webLink" rel="noopener" target="_blank">
- [[link.name]]
- </a>
- </template>
- </span>
- </section>
- <gr-endpoint-decorator name="change-metadata-item">
- <gr-endpoint-param name="labels" value="[[labels]]"></gr-endpoint-param>
- <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
- <gr-endpoint-param name="revision" value="[[revision]]"></gr-endpoint-param>
- </gr-endpoint-decorator>
- </gr-external-style>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-change-metadata.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index ea11514..2e9cdf9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -14,493 +14,524 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../../styles/gr-change-metadata-shared-styles.js';
+import '../../../styles/gr-change-view-integration-shared-styles.js';
+import '../../../styles/gr-voting-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
+import '../../plugins/gr-external-style/gr-external-style.js';
+import '../../shared/gr-account-chip/gr-account-chip.js';
+import '../../shared/gr-account-link/gr-account-link.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-editable-label/gr-editable-label.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-limited-text/gr-limited-text.js';
+import '../../shared/gr-linked-chip/gr-linked-chip.js';
+import '../../shared/gr-tooltip-content/gr-tooltip-content.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-change-requirements/gr-change-requirements.js';
+import '../gr-commit-info/gr-commit-info.js';
+import '../gr-reviewer-list/gr-reviewer-list.js';
+import '../../shared/gr-account-list/gr-account-list.js';
+import '../../../scripts/gr-display-name-utils/gr-display-name-utils.js';
+import '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-change-metadata_html.js';
- const SubmitTypeLabel = {
- FAST_FORWARD_ONLY: 'Fast Forward Only',
- MERGE_IF_NECESSARY: 'Merge if Necessary',
- REBASE_IF_NECESSARY: 'Rebase if Necessary',
- MERGE_ALWAYS: 'Always Merge',
- REBASE_ALWAYS: 'Rebase Always',
- CHERRY_PICK: 'Cherry Pick',
- };
+const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
- const NOT_CURRENT_MESSAGE = 'Not current - rebase possible';
+const SubmitTypeLabel = {
+ FAST_FORWARD_ONLY: 'Fast Forward Only',
+ MERGE_IF_NECESSARY: 'Merge if Necessary',
+ REBASE_IF_NECESSARY: 'Rebase if Necessary',
+ MERGE_ALWAYS: 'Always Merge',
+ REBASE_ALWAYS: 'Rebase Always',
+ CHERRY_PICK: 'Cherry Pick',
+};
+const NOT_CURRENT_MESSAGE = 'Not current - rebase possible';
+
+/**
+ * @enum {string}
+ */
+const CertificateStatus = {
/**
- * @enum {string}
+ * This certificate status is bad.
*/
- const CertificateStatus = {
- /**
- * This certificate status is bad.
- */
- BAD: 'BAD',
- /**
- * This certificate status is OK.
- */
- OK: 'OK',
- /**
- * This certificate status is TRUSTED.
- */
- TRUSTED: 'TRUSTED',
- };
-
+ BAD: 'BAD',
/**
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * This certificate status is OK.
*/
- class GrChangeMetadata extends Polymer.mixinBehaviors( [
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-change-metadata'; }
- /**
- * Fired when the change topic is changed.
- *
- * @event topic-changed
- */
+ OK: 'OK',
+ /**
+ * This certificate status is TRUSTED.
+ */
+ TRUSTED: 'TRUSTED',
+};
- static get properties() {
- return {
+/**
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrChangeMetadata extends mixinBehaviors( [
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-change-metadata'; }
+ /**
+ * Fired when the change topic is changed.
+ *
+ * @event topic-changed
+ */
+
+ static get properties() {
+ return {
+ /** @type {?} */
+ change: Object,
+ labels: {
+ type: Object,
+ notify: true,
+ },
+ account: Object,
/** @type {?} */
- change: Object,
- labels: {
- type: Object,
- notify: true,
+ revision: Object,
+ commitInfo: Object,
+ _mutable: {
+ type: Boolean,
+ computed: '_computeIsMutable(account)',
+ },
+ /** @type {?} */
+ serverConfig: Object,
+ parentIsCurrent: Boolean,
+ _notCurrentMessage: {
+ type: String,
+ value: NOT_CURRENT_MESSAGE,
+ readOnly: true,
+ },
+ _topicReadOnly: {
+ type: Boolean,
+ computed: '_computeTopicReadOnly(_mutable, change)',
+ },
+ _hashtagReadOnly: {
+ type: Boolean,
+ computed: '_computeHashtagReadOnly(_mutable, change)',
+ },
+ /**
+ * @type {Gerrit.PushCertificateValidation}
+ */
+ _pushCertificateValidation: {
+ type: Object,
+ computed: '_computePushCertificateValidation(serverConfig, change)',
+ },
+ _showRequirements: {
+ type: Boolean,
+ computed: '_computeShowRequirements(change)',
+ },
+
+ _assignee: Array,
+ _isWip: {
+ type: Boolean,
+ computed: '_computeIsWip(change)',
+ },
+ _newHashtag: String,
+
+ _settingTopic: {
+ type: Boolean,
+ value: false,
+ },
+
+ _currentParents: {
+ type: Array,
+ computed: '_computeParents(change)',
+ },
+
+ /** @type {?} */
+ _CHANGE_ROLE: {
+ type: Object,
+ readOnly: true,
+ value: {
+ OWNER: 'owner',
+ UPLOADER: 'uploader',
+ AUTHOR: 'author',
+ COMMITTER: 'committer',
},
- account: Object,
- /** @type {?} */
- revision: Object,
- commitInfo: Object,
- _mutable: {
- type: Boolean,
- computed: '_computeIsMutable(account)',
- },
- /** @type {?} */
- serverConfig: Object,
- parentIsCurrent: Boolean,
- _notCurrentMessage: {
- type: String,
- value: NOT_CURRENT_MESSAGE,
- readOnly: true,
- },
- _topicReadOnly: {
- type: Boolean,
- computed: '_computeTopicReadOnly(_mutable, change)',
- },
- _hashtagReadOnly: {
- type: Boolean,
- computed: '_computeHashtagReadOnly(_mutable, change)',
- },
- /**
- * @type {Gerrit.PushCertificateValidation}
- */
- _pushCertificateValidation: {
- type: Object,
- computed: '_computePushCertificateValidation(serverConfig, change)',
- },
- _showRequirements: {
- type: Boolean,
- computed: '_computeShowRequirements(change)',
- },
+ },
+ };
+ }
- _assignee: Array,
- _isWip: {
- type: Boolean,
- computed: '_computeIsWip(change)',
- },
- _newHashtag: String,
+ static get observers() {
+ return [
+ '_changeChanged(change)',
+ '_labelsChanged(change.labels)',
+ '_assigneeChanged(_assignee.*)',
+ ];
+ }
- _settingTopic: {
- type: Boolean,
- value: false,
- },
+ _labelsChanged(labels) {
+ this.labels = Object.assign({}, labels) || null;
+ }
- _currentParents: {
- type: Array,
- computed: '_computeParents(change)',
- },
+ _changeChanged(change) {
+ this._assignee = change.assignee ? [change.assignee] : [];
+ }
- /** @type {?} */
- _CHANGE_ROLE: {
- type: Object,
- readOnly: true,
- value: {
- OWNER: 'owner',
- UPLOADER: 'uploader',
- AUTHOR: 'author',
- COMMITTER: 'committer',
- },
- },
- };
- }
-
- static get observers() {
- return [
- '_changeChanged(change)',
- '_labelsChanged(change.labels)',
- '_assigneeChanged(_assignee.*)',
- ];
- }
-
- _labelsChanged(labels) {
- this.labels = Object.assign({}, labels) || null;
- }
-
- _changeChanged(change) {
- this._assignee = change.assignee ? [change.assignee] : [];
- }
-
- _assigneeChanged(assigneeRecord) {
- if (!this.change) { return; }
- const assignee = assigneeRecord.base;
- if (assignee.length) {
- const acct = assignee[0];
- if (this.change.assignee &&
- acct._account_id === this.change.assignee._account_id) { return; }
- this.set(['change', 'assignee'], acct);
- this.$.restAPI.setAssignee(this.change._number, acct._account_id);
- } else {
- if (!this.change.assignee) { return; }
- this.set(['change', 'assignee'], undefined);
- this.$.restAPI.deleteAssignee(this.change._number);
- }
- }
-
- _computeHideStrategy(change) {
- return !this.changeIsOpen(change);
- }
-
- /**
- * @param {Object} commitInfo
- * @return {?Array} If array is empty, returns null instead so
- * an existential check can be used to hide or show the webLinks
- * section.
- */
- _computeWebLinks(commitInfo, serverConfig) {
- if (!commitInfo) { return null; }
- const weblinks = Gerrit.Nav.getChangeWeblinks(
- this.change ? this.change.repo : '',
- commitInfo.commit,
- {
- weblinks: commitInfo.web_links,
- config: serverConfig,
- });
- return weblinks.length ? weblinks : null;
- }
-
- _computeStrategy(change) {
- return SubmitTypeLabel[change.submit_type];
- }
-
- _computeLabelNames(labels) {
- return Object.keys(labels).sort();
- }
-
- _handleTopicChanged(e, topic) {
- const lastTopic = this.change.topic;
- if (!topic.length) { topic = null; }
- this._settingTopic = true;
- this.$.restAPI.setChangeTopic(this.change._number, topic)
- .then(newTopic => {
- this._settingTopic = false;
- this.set(['change', 'topic'], newTopic);
- if (newTopic !== lastTopic) {
- this.dispatchEvent(new CustomEvent(
- 'topic-changed', {bubbles: true, composed: true}));
- }
- });
- }
-
- _showAddTopic(changeRecord, settingTopic) {
- const hasTopic = !!changeRecord &&
- !!changeRecord.base && !!changeRecord.base.topic;
- return !hasTopic && !settingTopic;
- }
-
- _showTopicChip(changeRecord, settingTopic) {
- const hasTopic = !!changeRecord &&
- !!changeRecord.base && !!changeRecord.base.topic;
- return hasTopic && !settingTopic;
- }
-
- _showCherryPickOf(changeRecord) {
- const hasCherryPickOf = !!changeRecord &&
- !!changeRecord.base && !!changeRecord.base.cherry_pick_of_change &&
- !!changeRecord.base.cherry_pick_of_patch_set;
- return hasCherryPickOf;
- }
-
- _handleHashtagChanged(e) {
- const lastHashtag = this.change.hashtag;
- if (!this._newHashtag.length) { return; }
- const newHashtag = this._newHashtag;
- this._newHashtag = '';
- this.$.restAPI.setChangeHashtag(
- this.change._number, {add: [newHashtag]}).then(newHashtag => {
- this.set(['change', 'hashtags'], newHashtag);
- if (newHashtag !== lastHashtag) {
- this.dispatchEvent(
- new CustomEvent('hashtag-changed', {
- bubbles: true, composed: true}));
- }
- });
- }
-
- _computeTopicReadOnly(mutable, change) {
- return !mutable ||
- !change ||
- !change.actions ||
- !change.actions.topic ||
- !change.actions.topic.enabled;
- }
-
- _computeHashtagReadOnly(mutable, change) {
- return !mutable ||
- !change ||
- !change.actions ||
- !change.actions.hashtags ||
- !change.actions.hashtags.enabled;
- }
-
- _computeAssigneeReadOnly(mutable, change) {
- return !mutable ||
- !change ||
- !change.actions ||
- !change.actions.assignee ||
- !change.actions.assignee.enabled;
- }
-
- _computeTopicPlaceholder(_topicReadOnly) {
- // Action items in Material Design are uppercase -- placeholder label text
- // is sentence case.
- return _topicReadOnly ? 'No topic' : 'ADD TOPIC';
- }
-
- _computeHashtagPlaceholder(_hashtagReadOnly) {
- return _hashtagReadOnly ? '' : HASHTAG_ADD_MESSAGE;
- }
-
- _computeShowRequirements(change) {
- if (change.status !== this.ChangeStatus.NEW) {
- // TODO(maximeg) change this to display the stored
- // requirements, once it is implemented server-side.
- return false;
- }
- const hasRequirements = !!change.requirements &&
- Object.keys(change.requirements).length > 0;
- const hasLabels = !!change.labels &&
- Object.keys(change.labels).length > 0;
- return hasRequirements || hasLabels || !!change.work_in_progress;
- }
-
- /**
- * @return {?Gerrit.PushCertificateValidation} object representing data for
- * the push validation.
- */
- _computePushCertificateValidation(serverConfig, change) {
- if (!change || !serverConfig || !serverConfig.receive ||
- !serverConfig.receive.enable_signed_push) {
- return null;
- }
- const rev = change.revisions[change.current_revision];
- if (!rev.push_certificate || !rev.push_certificate.key) {
- return {
- class: 'help',
- icon: 'gr-icons:help',
- message: 'This patch set was created without a push certificate',
- };
- }
-
- const key = rev.push_certificate.key;
- switch (key.status) {
- case CertificateStatus.BAD:
- return {
- class: 'invalid',
- icon: 'gr-icons:close',
- message: this._problems('Push certificate is invalid', key),
- };
- case CertificateStatus.OK:
- return {
- class: 'notTrusted',
- icon: 'gr-icons:info',
- message: this._problems(
- 'Push certificate is valid, but key is not trusted', key),
- };
- case CertificateStatus.TRUSTED:
- return {
- class: 'trusted',
- icon: 'gr-icons:check',
- message: this._problems(
- 'Push certificate is valid and key is trusted', key),
- };
- default:
- throw new Error(`unknown certificate status: ${key.status}`);
- }
- }
-
- _problems(msg, key) {
- if (!key || !key.problems || key.problems.length === 0) {
- return msg;
- }
-
- return [msg + ':'].concat(key.problems).join('\n');
- }
-
- _computeShowRepoBranchTogether(repo, branch) {
- return !!repo && !!branch && repo.length + branch.length < 40;
- }
-
- _computeProjectUrl(project) {
- return Gerrit.Nav.getUrlForProjectChanges(project);
- }
-
- _computeBranchUrl(project, branch) {
- if (!this.change || !this.change.status) return '';
- return Gerrit.Nav.getUrlForBranch(branch, project,
- this.change.status == this.ChangeStatus.NEW ? 'open' :
- this.change.status.toLowerCase());
- }
-
- _computeCherryPickOfUrl(change, patchset, project) {
- return Gerrit.Nav.getUrlForChangeById(change, project, patchset);
- }
-
- _computeTopicUrl(topic) {
- return Gerrit.Nav.getUrlForTopic(topic);
- }
-
- _computeHashtagUrl(hashtag) {
- return Gerrit.Nav.getUrlForHashtag(hashtag);
- }
-
- _handleTopicRemoved(e) {
- const target = Polymer.dom(e).rootTarget;
- target.disabled = true;
- this.$.restAPI.setChangeTopic(this.change._number, null)
- .then(() => {
- target.disabled = false;
- this.set(['change', 'topic'], '');
- this.dispatchEvent(
- new CustomEvent('topic-changed',
- {bubbles: true, composed: true}));
- })
- .catch(err => {
- target.disabled = false;
- return;
- });
- }
-
- _handleHashtagRemoved(e) {
- e.preventDefault();
- const target = Polymer.dom(e).rootTarget;
- target.disabled = true;
- this.$.restAPI.setChangeHashtag(this.change._number,
- {remove: [target.text]})
- .then(newHashtag => {
- target.disabled = false;
- this.set(['change', 'hashtags'], newHashtag);
- })
- .catch(err => {
- target.disabled = false;
- return;
- });
- }
-
- _computeIsWip(change) {
- return !!change.work_in_progress;
- }
-
- _computeShowRoleClass(change, role) {
- return this._getNonOwnerRole(change, role) ? '' : 'hideDisplay';
- }
-
- /**
- * Get the user with the specified role on the change. Returns null if the
- * user with that role is the same as the owner.
- *
- * @param {!Object} change
- * @param {string} role One of the values from _CHANGE_ROLE
- * @return {Object|null} either an accound or null.
- */
- _getNonOwnerRole(change, role) {
- if (!change || !change.current_revision ||
- !change.revisions[change.current_revision]) {
- return null;
- }
-
- const rev = change.revisions[change.current_revision];
- if (!rev) { return null; }
-
- if (role === this._CHANGE_ROLE.UPLOADER &&
- rev.uploader &&
- change.owner._account_id !== rev.uploader._account_id) {
- return rev.uploader;
- }
-
- if (role === this._CHANGE_ROLE.AUTHOR &&
- rev.commit && rev.commit.author &&
- change.owner.email !== rev.commit.author.email) {
- return rev.commit.author;
- }
-
- if (role === this._CHANGE_ROLE.COMMITTER &&
- rev.commit && rev.commit.committer &&
- change.owner.email !== rev.commit.committer.email) {
- return rev.commit.committer;
- }
-
- return null;
- }
-
- _computeParents(change) {
- if (!change || !change.current_revision ||
- !change.revisions[change.current_revision] ||
- !change.revisions[change.current_revision].commit) {
- return undefined;
- }
- return change.revisions[change.current_revision].commit.parents;
- }
-
- _computeParentsLabel(parents) {
- return parents && parents.length > 1 ? 'Parents' : 'Parent';
- }
-
- _computeParentListClass(parents, parentIsCurrent) {
- // Undefined check for polymer 2
- if (parents === undefined || parentIsCurrent === undefined) {
- return '';
- }
-
- return [
- 'parentList',
- parents && parents.length > 1 ? 'merge' : 'nonMerge',
- parentIsCurrent ? 'current' : 'notCurrent',
- ].join(' ');
- }
-
- _computeIsMutable(account) {
- return !!Object.keys(account).length;
- }
-
- editTopic() {
- if (this._topicReadOnly || this.change.topic) { return; }
- // Cannot use `this.$.ID` syntax because the element exists inside of a
- // dom-if.
- this.shadowRoot.querySelector('.topicEditableLabel').open();
- }
-
- _getReviewerSuggestionsProvider(change) {
- const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI,
- change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY);
- provider.init();
- return provider;
+ _assigneeChanged(assigneeRecord) {
+ if (!this.change) { return; }
+ const assignee = assigneeRecord.base;
+ if (assignee.length) {
+ const acct = assignee[0];
+ if (this.change.assignee &&
+ acct._account_id === this.change.assignee._account_id) { return; }
+ this.set(['change', 'assignee'], acct);
+ this.$.restAPI.setAssignee(this.change._number, acct._account_id);
+ } else {
+ if (!this.change.assignee) { return; }
+ this.set(['change', 'assignee'], undefined);
+ this.$.restAPI.deleteAssignee(this.change._number);
}
}
- customElements.define(GrChangeMetadata.is, GrChangeMetadata);
-})();
+ _computeHideStrategy(change) {
+ return !this.changeIsOpen(change);
+ }
+
+ /**
+ * @param {Object} commitInfo
+ * @return {?Array} If array is empty, returns null instead so
+ * an existential check can be used to hide or show the webLinks
+ * section.
+ */
+ _computeWebLinks(commitInfo, serverConfig) {
+ if (!commitInfo) { return null; }
+ const weblinks = Gerrit.Nav.getChangeWeblinks(
+ this.change ? this.change.repo : '',
+ commitInfo.commit,
+ {
+ weblinks: commitInfo.web_links,
+ config: serverConfig,
+ });
+ return weblinks.length ? weblinks : null;
+ }
+
+ _computeStrategy(change) {
+ return SubmitTypeLabel[change.submit_type];
+ }
+
+ _computeLabelNames(labels) {
+ return Object.keys(labels).sort();
+ }
+
+ _handleTopicChanged(e, topic) {
+ const lastTopic = this.change.topic;
+ if (!topic.length) { topic = null; }
+ this._settingTopic = true;
+ this.$.restAPI.setChangeTopic(this.change._number, topic)
+ .then(newTopic => {
+ this._settingTopic = false;
+ this.set(['change', 'topic'], newTopic);
+ if (newTopic !== lastTopic) {
+ this.dispatchEvent(new CustomEvent(
+ 'topic-changed', {bubbles: true, composed: true}));
+ }
+ });
+ }
+
+ _showAddTopic(changeRecord, settingTopic) {
+ const hasTopic = !!changeRecord &&
+ !!changeRecord.base && !!changeRecord.base.topic;
+ return !hasTopic && !settingTopic;
+ }
+
+ _showTopicChip(changeRecord, settingTopic) {
+ const hasTopic = !!changeRecord &&
+ !!changeRecord.base && !!changeRecord.base.topic;
+ return hasTopic && !settingTopic;
+ }
+
+ _showCherryPickOf(changeRecord) {
+ const hasCherryPickOf = !!changeRecord &&
+ !!changeRecord.base && !!changeRecord.base.cherry_pick_of_change &&
+ !!changeRecord.base.cherry_pick_of_patch_set;
+ return hasCherryPickOf;
+ }
+
+ _handleHashtagChanged(e) {
+ const lastHashtag = this.change.hashtag;
+ if (!this._newHashtag.length) { return; }
+ const newHashtag = this._newHashtag;
+ this._newHashtag = '';
+ this.$.restAPI.setChangeHashtag(
+ this.change._number, {add: [newHashtag]}).then(newHashtag => {
+ this.set(['change', 'hashtags'], newHashtag);
+ if (newHashtag !== lastHashtag) {
+ this.dispatchEvent(
+ new CustomEvent('hashtag-changed', {
+ bubbles: true, composed: true}));
+ }
+ });
+ }
+
+ _computeTopicReadOnly(mutable, change) {
+ return !mutable ||
+ !change ||
+ !change.actions ||
+ !change.actions.topic ||
+ !change.actions.topic.enabled;
+ }
+
+ _computeHashtagReadOnly(mutable, change) {
+ return !mutable ||
+ !change ||
+ !change.actions ||
+ !change.actions.hashtags ||
+ !change.actions.hashtags.enabled;
+ }
+
+ _computeAssigneeReadOnly(mutable, change) {
+ return !mutable ||
+ !change ||
+ !change.actions ||
+ !change.actions.assignee ||
+ !change.actions.assignee.enabled;
+ }
+
+ _computeTopicPlaceholder(_topicReadOnly) {
+ // Action items in Material Design are uppercase -- placeholder label text
+ // is sentence case.
+ return _topicReadOnly ? 'No topic' : 'ADD TOPIC';
+ }
+
+ _computeHashtagPlaceholder(_hashtagReadOnly) {
+ return _hashtagReadOnly ? '' : HASHTAG_ADD_MESSAGE;
+ }
+
+ _computeShowRequirements(change) {
+ if (change.status !== this.ChangeStatus.NEW) {
+ // TODO(maximeg) change this to display the stored
+ // requirements, once it is implemented server-side.
+ return false;
+ }
+ const hasRequirements = !!change.requirements &&
+ Object.keys(change.requirements).length > 0;
+ const hasLabels = !!change.labels &&
+ Object.keys(change.labels).length > 0;
+ return hasRequirements || hasLabels || !!change.work_in_progress;
+ }
+
+ /**
+ * @return {?Gerrit.PushCertificateValidation} object representing data for
+ * the push validation.
+ */
+ _computePushCertificateValidation(serverConfig, change) {
+ if (!change || !serverConfig || !serverConfig.receive ||
+ !serverConfig.receive.enable_signed_push) {
+ return null;
+ }
+ const rev = change.revisions[change.current_revision];
+ if (!rev.push_certificate || !rev.push_certificate.key) {
+ return {
+ class: 'help',
+ icon: 'gr-icons:help',
+ message: 'This patch set was created without a push certificate',
+ };
+ }
+
+ const key = rev.push_certificate.key;
+ switch (key.status) {
+ case CertificateStatus.BAD:
+ return {
+ class: 'invalid',
+ icon: 'gr-icons:close',
+ message: this._problems('Push certificate is invalid', key),
+ };
+ case CertificateStatus.OK:
+ return {
+ class: 'notTrusted',
+ icon: 'gr-icons:info',
+ message: this._problems(
+ 'Push certificate is valid, but key is not trusted', key),
+ };
+ case CertificateStatus.TRUSTED:
+ return {
+ class: 'trusted',
+ icon: 'gr-icons:check',
+ message: this._problems(
+ 'Push certificate is valid and key is trusted', key),
+ };
+ default:
+ throw new Error(`unknown certificate status: ${key.status}`);
+ }
+ }
+
+ _problems(msg, key) {
+ if (!key || !key.problems || key.problems.length === 0) {
+ return msg;
+ }
+
+ return [msg + ':'].concat(key.problems).join('\n');
+ }
+
+ _computeShowRepoBranchTogether(repo, branch) {
+ return !!repo && !!branch && repo.length + branch.length < 40;
+ }
+
+ _computeProjectUrl(project) {
+ return Gerrit.Nav.getUrlForProjectChanges(project);
+ }
+
+ _computeBranchUrl(project, branch) {
+ if (!this.change || !this.change.status) return '';
+ return Gerrit.Nav.getUrlForBranch(branch, project,
+ this.change.status == this.ChangeStatus.NEW ? 'open' :
+ this.change.status.toLowerCase());
+ }
+
+ _computeCherryPickOfUrl(change, patchset, project) {
+ return Gerrit.Nav.getUrlForChangeById(change, project, patchset);
+ }
+
+ _computeTopicUrl(topic) {
+ return Gerrit.Nav.getUrlForTopic(topic);
+ }
+
+ _computeHashtagUrl(hashtag) {
+ return Gerrit.Nav.getUrlForHashtag(hashtag);
+ }
+
+ _handleTopicRemoved(e) {
+ const target = dom(e).rootTarget;
+ target.disabled = true;
+ this.$.restAPI.setChangeTopic(this.change._number, null)
+ .then(() => {
+ target.disabled = false;
+ this.set(['change', 'topic'], '');
+ this.dispatchEvent(
+ new CustomEvent('topic-changed',
+ {bubbles: true, composed: true}));
+ })
+ .catch(err => {
+ target.disabled = false;
+ return;
+ });
+ }
+
+ _handleHashtagRemoved(e) {
+ e.preventDefault();
+ const target = dom(e).rootTarget;
+ target.disabled = true;
+ this.$.restAPI.setChangeHashtag(this.change._number,
+ {remove: [target.text]})
+ .then(newHashtag => {
+ target.disabled = false;
+ this.set(['change', 'hashtags'], newHashtag);
+ })
+ .catch(err => {
+ target.disabled = false;
+ return;
+ });
+ }
+
+ _computeIsWip(change) {
+ return !!change.work_in_progress;
+ }
+
+ _computeShowRoleClass(change, role) {
+ return this._getNonOwnerRole(change, role) ? '' : 'hideDisplay';
+ }
+
+ /**
+ * Get the user with the specified role on the change. Returns null if the
+ * user with that role is the same as the owner.
+ *
+ * @param {!Object} change
+ * @param {string} role One of the values from _CHANGE_ROLE
+ * @return {Object|null} either an accound or null.
+ */
+ _getNonOwnerRole(change, role) {
+ if (!change || !change.current_revision ||
+ !change.revisions[change.current_revision]) {
+ return null;
+ }
+
+ const rev = change.revisions[change.current_revision];
+ if (!rev) { return null; }
+
+ if (role === this._CHANGE_ROLE.UPLOADER &&
+ rev.uploader &&
+ change.owner._account_id !== rev.uploader._account_id) {
+ return rev.uploader;
+ }
+
+ if (role === this._CHANGE_ROLE.AUTHOR &&
+ rev.commit && rev.commit.author &&
+ change.owner.email !== rev.commit.author.email) {
+ return rev.commit.author;
+ }
+
+ if (role === this._CHANGE_ROLE.COMMITTER &&
+ rev.commit && rev.commit.committer &&
+ change.owner.email !== rev.commit.committer.email) {
+ return rev.commit.committer;
+ }
+
+ return null;
+ }
+
+ _computeParents(change) {
+ if (!change || !change.current_revision ||
+ !change.revisions[change.current_revision] ||
+ !change.revisions[change.current_revision].commit) {
+ return undefined;
+ }
+ return change.revisions[change.current_revision].commit.parents;
+ }
+
+ _computeParentsLabel(parents) {
+ return parents && parents.length > 1 ? 'Parents' : 'Parent';
+ }
+
+ _computeParentListClass(parents, parentIsCurrent) {
+ // Undefined check for polymer 2
+ if (parents === undefined || parentIsCurrent === undefined) {
+ return '';
+ }
+
+ return [
+ 'parentList',
+ parents && parents.length > 1 ? 'merge' : 'nonMerge',
+ parentIsCurrent ? 'current' : 'notCurrent',
+ ].join(' ');
+ }
+
+ _computeIsMutable(account) {
+ return !!Object.keys(account).length;
+ }
+
+ editTopic() {
+ if (this._topicReadOnly || this.change.topic) { return; }
+ // Cannot use `this.$.ID` syntax because the element exists inside of a
+ // dom-if.
+ this.shadowRoot.querySelector('.topicEditableLabel').open();
+ }
+
+ _getReviewerSuggestionsProvider(change) {
+ const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI,
+ change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY);
+ provider.init();
+ return provider;
+ }
+}
+
+customElements.define(GrChangeMetadata.is, GrChangeMetadata);
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js
new file mode 100644
index 0000000..786a118
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js
@@ -0,0 +1,256 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="gr-change-metadata-shared-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="shared-styles">
+ :host {
+ display: table;
+ }
+ gr-change-requirements {
+ --requirements-horizontal-padding: var(--metadata-horizontal-padding);
+ }
+ gr-account-link {
+ max-width: 20ch;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: top;
+ white-space: nowrap;
+ }
+ gr-editable-label {
+ max-width: 9em;
+ }
+ .webLink {
+ display: block;
+ }
+ /* CSS Mixins should be applied last. */
+ section.assignee {
+ @apply --change-metadata-assignee;
+ }
+ section.strategy {
+ @apply --change-metadata-strategy;
+ }
+ section.topic {
+ @apply --change-metadata-topic;
+ }
+ gr-account-chip[disabled],
+ gr-linked-chip[disabled] {
+ opacity: 0;
+ pointer-events: none;
+ }
+ .hashtagChip {
+ margin-bottom: var(--spacing-m);
+ }
+ #externalStyle {
+ display: block;
+ }
+ .parentList.merge {
+ list-style-type: decimal;
+ padding-left: var(--spacing-l);
+ }
+ .parentList gr-commit-info {
+ display: inline-block;
+ }
+ .hideDisplay,
+ #parentNotCurrentMessage {
+ display: none;
+ }
+ .icon {
+ margin: -3px 0;
+ }
+ .icon.help,
+ .icon.notTrusted {
+ color: #FFA62F;
+ }
+ .icon.invalid {
+ color: var(--vote-text-color-disliked);
+ }
+ .icon.trusted {
+ color: var(--vote-text-color-recommended);
+ }
+ .parentList.notCurrent.nonMerge #parentNotCurrentMessage {
+ --arrow-color: #ffa62f;
+ display: inline-block;
+ }
+ .separatedSection {
+ margin-top: var(--spacing-l);
+ padding: var(--spacing-m) 0;
+ }
+ .hashtag gr-linked-chip,
+ .topic gr-linked-chip {
+ --linked-chip-text-color: var(--link-color);
+ }
+ </style>
+ <gr-external-style id="externalStyle" name="change-metadata">
+ <section>
+ <span class="title">Updated</span>
+ <span class="value">
+ <gr-date-formatter has-tooltip="" date-str="[[change.updated]]"></gr-date-formatter>
+ </span>
+ </section>
+ <section>
+ <span class="title">Owner</span>
+ <span class="value">
+ <gr-account-link account="[[change.owner]]"></gr-account-link>
+ <template is="dom-if" if="[[_pushCertificateValidation]]">
+ <gr-tooltip-content has-tooltip="" title\$="[[_pushCertificateValidation.message]]">
+ <iron-icon class\$="icon [[_pushCertificateValidation.class]]" icon="[[_pushCertificateValidation.icon]]">
+ </iron-icon>
+ </gr-tooltip-content>
+ </template>
+ </span>
+ </section>
+ <section class\$="[[_computeShowRoleClass(change, _CHANGE_ROLE.UPLOADER)]]">
+ <span class="title">Uploader</span>
+ <span class="value">
+ <gr-account-link account="[[_getNonOwnerRole(change, _CHANGE_ROLE.UPLOADER)]]"></gr-account-link>
+ </span>
+ </section>
+ <section class\$="[[_computeShowRoleClass(change, _CHANGE_ROLE.AUTHOR)]]">
+ <span class="title">Author</span>
+ <span class="value">
+ <gr-account-link account="[[_getNonOwnerRole(change, _CHANGE_ROLE.AUTHOR)]]"></gr-account-link>
+ </span>
+ </section>
+ <section class\$="[[_computeShowRoleClass(change, _CHANGE_ROLE.COMMITTER)]]">
+ <span class="title">Committer</span>
+ <span class="value">
+ <gr-account-link account="[[_getNonOwnerRole(change, _CHANGE_ROLE.COMMITTER)]]"></gr-account-link>
+ </span>
+ </section>
+ <section class="assignee">
+ <span class="title">Assignee</span>
+ <span class="value">
+ <gr-account-list id="assigneeValue" placeholder="Set assignee..." max-count="1" skip-suggest-on-empty="" accounts="{{_assignee}}" readonly="[[_computeAssigneeReadOnly(_mutable, change)]]" suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
+ </gr-account-list>
+ </span>
+ </section>
+ <section>
+ <span class="title">Reviewers</span>
+ <span class="value">
+ <gr-reviewer-list change="{{change}}" mutable="[[_mutable]]" reviewers-only="" max-reviewers-displayed="3"></gr-reviewer-list>
+ </span>
+ </section>
+ <section>
+ <span class="title">CC</span>
+ <span class="value">
+ <gr-reviewer-list change="{{change}}" mutable="[[_mutable]]" ccs-only="" max-reviewers-displayed="3"></gr-reviewer-list>
+ </span>
+ </section>
+ <template is="dom-if" if="[[_computeShowRepoBranchTogether(change.project, change.branch)]]">
+ <section>
+ <span class="title">Repo / Branch</span>
+ <span class="value">
+ <a href\$="[[_computeProjectUrl(change.project)]]">[[change.project]]</a>
+ /
+ <a href\$="[[_computeBranchUrl(change.project, change.branch)]]">[[change.branch]]</a>
+ </span>
+ </section>
+ </template>
+ <template is="dom-if" if="[[!_computeShowRepoBranchTogether(change.project, change.branch)]]">
+ <section>
+ <span class="title">Repo</span>
+ <span class="value">
+ <a href\$="[[_computeProjectUrl(change.project)]]">
+ <gr-limited-text limit="40" text="[[change.project]]"></gr-limited-text>
+ </a>
+ </span>
+ </section>
+ <section>
+ <span class="title">Branch</span>
+ <span class="value">
+ <a href\$="[[_computeBranchUrl(change.project, change.branch)]]">
+ <gr-limited-text limit="40" text="[[change.branch]]"></gr-limited-text>
+ </a>
+ </span>
+ </section>
+ </template>
+ <section>
+ <span class="title">[[_computeParentsLabel(_currentParents)]]</span>
+ <span class="value">
+ <ol class\$="[[_computeParentListClass(_currentParents, parentIsCurrent)]]">
+ <template is="dom-repeat" items="[[_currentParents]]" as="parent">
+ <li>
+ <gr-commit-info change="[[change]]" commit-info="[[parent]]" server-config="[[serverConfig]]"></gr-commit-info>
+ <gr-tooltip-content id="parentNotCurrentMessage" has-tooltip="" show-icon="" title\$="[[_notCurrentMessage]]"></gr-tooltip-content>
+ </li>
+ </template>
+ </ol>
+ </span>
+ </section>
+ <section class="topic">
+ <span class="title">Topic</span>
+ <span class="value">
+ <template is="dom-if" if="[[_showTopicChip(change.*, _settingTopic)]]">
+ <gr-linked-chip text="[[change.topic]]" limit="40" href="[[_computeTopicUrl(change.topic)]]" removable="[[!_topicReadOnly]]" on-remove="_handleTopicRemoved"></gr-linked-chip>
+ </template>
+ <template is="dom-if" if="[[_showAddTopic(change.*, _settingTopic)]]">
+ <gr-editable-label class="topicEditableLabel" label-text="Add a topic" value="[[change.topic]]" max-length="1024" placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]" read-only="[[_topicReadOnly]]" on-changed="_handleTopicChanged"></gr-editable-label>
+ </template>
+ </span>
+ </section>
+ <template is="dom-if" if="[[_showCherryPickOf(change.*)]]">
+ <section>
+ <span class="title">Cherry pick of</span>
+ <span class="value">
+ <a href\$="[[_computeCherryPickOfUrl(change.cherry_pick_of_change, change.cherry_pick_of_patch_set, change.project)]]">
+ <gr-limited-text text="[[change.cherry_pick_of_change]],[[change.cherry_pick_of_patch_set]]" limit="40">
+ </gr-limited-text>
+ </a>
+ </span>
+ </section>
+ </template>
+ <section class="strategy" hidden\$="[[_computeHideStrategy(change)]]" hidden="">
+ <span class="title">Strategy</span>
+ <span class="value">[[_computeStrategy(change)]]</span>
+ </section>
+ <section class="hashtag">
+ <span class="title">Hashtags</span>
+ <span class="value">
+ <template is="dom-repeat" items="[[change.hashtags]]">
+ <gr-linked-chip class="hashtagChip" text="[[item]]" href="[[_computeHashtagUrl(item)]]" removable="[[!_hashtagReadOnly]]" on-remove="_handleHashtagRemoved">
+ </gr-linked-chip>
+ </template>
+ <template is="dom-if" if="[[!_hashtagReadOnly]]">
+ <gr-editable-label uppercase="" label-text="Add a hashtag" value="{{_newHashtag}}" placeholder="[[_computeHashtagPlaceholder(_hashtagReadOnly)]]" read-only="[[_hashtagReadOnly]]" on-changed="_handleHashtagChanged"></gr-editable-label>
+ </template>
+ </span>
+ </section>
+ <div class="separatedSection">
+ <gr-change-requirements change="{{change}}" account="[[account]]" mutable="[[_mutable]]"></gr-change-requirements>
+ </div>
+ <section id="webLinks" hidden\$="[[!_computeWebLinks(commitInfo, serverConfig)]]">
+ <span class="title">Links</span>
+ <span class="value">
+ <template is="dom-repeat" items="[[_computeWebLinks(commitInfo, serverConfig)]]" as="link">
+ <a href="[[link.url]]" class="webLink" rel="noopener" target="_blank">
+ [[link.name]]
+ </a>
+ </template>
+ </span>
+ </section>
+ <gr-endpoint-decorator name="change-metadata-item">
+ <gr-endpoint-param name="labels" value="[[labels]]"></gr-endpoint-param>
+ <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
+ <gr-endpoint-param name="revision" value="[[revision]]"></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </gr-external-style>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index 055f3f0..773e6ec 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -19,16 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-metadata</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="../../core/gr-router/gr-router.html">
-<link rel="import" href="gr-change-metadata.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,733 +30,735 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-metadata tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../core/gr-router/gr-router.js';
+import './gr-change-metadata.js';
+suite('gr-change-metadata tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-endpoint-decorator', {
+ _import: sandbox.stub().returns(Promise.resolve()),
+ });
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getLoggedIn() { return Promise.resolve(false); },
+ });
+
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('computed fields', () => {
+ assert.isFalse(element._computeHideStrategy({status: 'NEW'}));
+ assert.isTrue(element._computeHideStrategy({status: 'MERGED'}));
+ assert.isTrue(element._computeHideStrategy({status: 'ABANDONED'}));
+ assert.equal(element._computeStrategy({submit_type: 'CHERRY_PICK'}),
+ 'Cherry Pick');
+ assert.equal(element._computeStrategy({submit_type: 'REBASE_ALWAYS'}),
+ 'Rebase Always');
+ });
+
+ test('computed fields requirements', () => {
+ assert.isFalse(element._computeShowRequirements({status: 'MERGED'}));
+ assert.isFalse(element._computeShowRequirements({status: 'ABANDONED'}));
+
+ // No labels and no requirements: submit status is useless
+ assert.isFalse(element._computeShowRequirements({
+ status: 'NEW',
+ labels: {},
+ }));
+
+ // Work in Progress: submit status should be present
+ assert.isTrue(element._computeShowRequirements({
+ status: 'NEW',
+ labels: {},
+ work_in_progress: true,
+ }));
+
+ // We have at least one reason to display Submit Status
+ assert.isTrue(element._computeShowRequirements({
+ status: 'NEW',
+ labels: {
+ Verified: {
+ approved: false,
+ },
+ },
+ requirements: [],
+ }));
+ assert.isTrue(element._computeShowRequirements({
+ status: 'NEW',
+ labels: {},
+ requirements: [{
+ fallback_text: 'Resolve all comments',
+ status: 'OK',
+ }],
+ }));
+ });
+
+ test('show strategy for open change', () => {
+ element.change = {status: 'NEW', submit_type: 'CHERRY_PICK', labels: {}};
+ flushAsynchronousOperations();
+ const strategy = element.shadowRoot
+ .querySelector('.strategy');
+ assert.ok(strategy);
+ assert.isFalse(strategy.hasAttribute('hidden'));
+ assert.equal(strategy.children[1].innerHTML, 'Cherry Pick');
+ });
+
+ test('hide strategy for closed change', () => {
+ element.change = {status: 'MERGED', labels: {}};
+ flushAsynchronousOperations();
+ assert.isTrue(element.shadowRoot
+ .querySelector('.strategy').hasAttribute('hidden'));
+ });
+
+ test('weblinks use Gerrit.Nav interface', () => {
+ const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
+ .returns([{name: 'stubb', url: '#s'}]);
+ element.commitInfo = {};
+ element.serverConfig = {};
+ flushAsynchronousOperations();
+ const webLinks = element.$.webLinks;
+ assert.isTrue(weblinksStub.called);
+ assert.isFalse(webLinks.hasAttribute('hidden'));
+ assert.equal(element._computeWebLinks(element.commitInfo).length, 1);
+ });
+
+ test('weblinks hidden when no weblinks', () => {
+ element.commitInfo = {};
+ element.serverConfig = {};
+ flushAsynchronousOperations();
+ const webLinks = element.$.webLinks;
+ assert.isTrue(webLinks.hasAttribute('hidden'));
+ });
+
+ test('weblinks hidden when only gitiles weblink', () => {
+ element.commitInfo = {web_links: [{name: 'gitiles', url: '#'}]};
+ element.serverConfig = {};
+ flushAsynchronousOperations();
+ const webLinks = element.$.webLinks;
+ assert.isTrue(webLinks.hasAttribute('hidden'));
+ assert.equal(element._computeWebLinks(element.commitInfo), null);
+ });
+
+ test('weblinks hidden when sole weblink is set as primary', () => {
+ const browser = 'browser';
+ element.commitInfo = {web_links: [{name: browser, url: '#'}]};
+ element.serverConfig = {
+ gerrit: {
+ primary_weblink_name: browser,
+ },
+ };
+ flushAsynchronousOperations();
+ const webLinks = element.$.webLinks;
+ assert.isTrue(webLinks.hasAttribute('hidden'));
+ });
+
+ test('weblinks are visible when other weblinks', () => {
+ const router = document.createElement('gr-router');
+ sandbox.stub(Gerrit.Nav, '_generateWeblinks',
+ router._generateWeblinks.bind(router));
+
+ element.commitInfo = {web_links: [{name: 'test', url: '#'}]};
+ flushAsynchronousOperations();
+ const webLinks = element.$.webLinks;
+ assert.isFalse(webLinks.hasAttribute('hidden'));
+ assert.equal(element._computeWebLinks(element.commitInfo).length, 1);
+ // With two non-gitiles weblinks, there are two returned.
+ element.commitInfo = {
+ web_links: [{name: 'test', url: '#'}, {name: 'test2', url: '#'}]};
+ assert.equal(element._computeWebLinks(element.commitInfo).length, 2);
+ });
+
+ test('weblinks are visible when gitiles and other weblinks', () => {
+ const router = document.createElement('gr-router');
+ sandbox.stub(Gerrit.Nav, '_generateWeblinks',
+ router._generateWeblinks.bind(router));
+
+ element.commitInfo = {
+ web_links: [{name: 'test', url: '#'}, {name: 'gitiles', url: '#'}]};
+ flushAsynchronousOperations();
+ const webLinks = element.$.webLinks;
+ assert.isFalse(webLinks.hasAttribute('hidden'));
+ // Only the non-gitiles weblink is returned.
+ assert.equal(element._computeWebLinks(element.commitInfo).length, 1);
+ });
+
+ suite('_getNonOwnerRole', () => {
+ let change;
setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-endpoint-decorator', {
- _import: sandbox.stub().returns(Promise.resolve()),
- });
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getLoggedIn() { return Promise.resolve(false); },
- });
-
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('computed fields', () => {
- assert.isFalse(element._computeHideStrategy({status: 'NEW'}));
- assert.isTrue(element._computeHideStrategy({status: 'MERGED'}));
- assert.isTrue(element._computeHideStrategy({status: 'ABANDONED'}));
- assert.equal(element._computeStrategy({submit_type: 'CHERRY_PICK'}),
- 'Cherry Pick');
- assert.equal(element._computeStrategy({submit_type: 'REBASE_ALWAYS'}),
- 'Rebase Always');
- });
-
- test('computed fields requirements', () => {
- assert.isFalse(element._computeShowRequirements({status: 'MERGED'}));
- assert.isFalse(element._computeShowRequirements({status: 'ABANDONED'}));
-
- // No labels and no requirements: submit status is useless
- assert.isFalse(element._computeShowRequirements({
- status: 'NEW',
- labels: {},
- }));
-
- // Work in Progress: submit status should be present
- assert.isTrue(element._computeShowRequirements({
- status: 'NEW',
- labels: {},
- work_in_progress: true,
- }));
-
- // We have at least one reason to display Submit Status
- assert.isTrue(element._computeShowRequirements({
- status: 'NEW',
- labels: {
- Verified: {
- approved: false,
- },
- },
- requirements: [],
- }));
- assert.isTrue(element._computeShowRequirements({
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'OK',
- }],
- }));
- });
-
- test('show strategy for open change', () => {
- element.change = {status: 'NEW', submit_type: 'CHERRY_PICK', labels: {}};
- flushAsynchronousOperations();
- const strategy = element.shadowRoot
- .querySelector('.strategy');
- assert.ok(strategy);
- assert.isFalse(strategy.hasAttribute('hidden'));
- assert.equal(strategy.children[1].innerHTML, 'Cherry Pick');
- });
-
- test('hide strategy for closed change', () => {
- element.change = {status: 'MERGED', labels: {}};
- flushAsynchronousOperations();
- assert.isTrue(element.shadowRoot
- .querySelector('.strategy').hasAttribute('hidden'));
- });
-
- test('weblinks use Gerrit.Nav interface', () => {
- const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
- .returns([{name: 'stubb', url: '#s'}]);
- element.commitInfo = {};
- element.serverConfig = {};
- flushAsynchronousOperations();
- const webLinks = element.$.webLinks;
- assert.isTrue(weblinksStub.called);
- assert.isFalse(webLinks.hasAttribute('hidden'));
- assert.equal(element._computeWebLinks(element.commitInfo).length, 1);
- });
-
- test('weblinks hidden when no weblinks', () => {
- element.commitInfo = {};
- element.serverConfig = {};
- flushAsynchronousOperations();
- const webLinks = element.$.webLinks;
- assert.isTrue(webLinks.hasAttribute('hidden'));
- });
-
- test('weblinks hidden when only gitiles weblink', () => {
- element.commitInfo = {web_links: [{name: 'gitiles', url: '#'}]};
- element.serverConfig = {};
- flushAsynchronousOperations();
- const webLinks = element.$.webLinks;
- assert.isTrue(webLinks.hasAttribute('hidden'));
- assert.equal(element._computeWebLinks(element.commitInfo), null);
- });
-
- test('weblinks hidden when sole weblink is set as primary', () => {
- const browser = 'browser';
- element.commitInfo = {web_links: [{name: browser, url: '#'}]};
- element.serverConfig = {
- gerrit: {
- primary_weblink_name: browser,
- },
- };
- flushAsynchronousOperations();
- const webLinks = element.$.webLinks;
- assert.isTrue(webLinks.hasAttribute('hidden'));
- });
-
- test('weblinks are visible when other weblinks', () => {
- const router = document.createElement('gr-router');
- sandbox.stub(Gerrit.Nav, '_generateWeblinks',
- router._generateWeblinks.bind(router));
-
- element.commitInfo = {web_links: [{name: 'test', url: '#'}]};
- flushAsynchronousOperations();
- const webLinks = element.$.webLinks;
- assert.isFalse(webLinks.hasAttribute('hidden'));
- assert.equal(element._computeWebLinks(element.commitInfo).length, 1);
- // With two non-gitiles weblinks, there are two returned.
- element.commitInfo = {
- web_links: [{name: 'test', url: '#'}, {name: 'test2', url: '#'}]};
- assert.equal(element._computeWebLinks(element.commitInfo).length, 2);
- });
-
- test('weblinks are visible when gitiles and other weblinks', () => {
- const router = document.createElement('gr-router');
- sandbox.stub(Gerrit.Nav, '_generateWeblinks',
- router._generateWeblinks.bind(router));
-
- element.commitInfo = {
- web_links: [{name: 'test', url: '#'}, {name: 'gitiles', url: '#'}]};
- flushAsynchronousOperations();
- const webLinks = element.$.webLinks;
- assert.isFalse(webLinks.hasAttribute('hidden'));
- // Only the non-gitiles weblink is returned.
- assert.equal(element._computeWebLinks(element.commitInfo).length, 1);
- });
-
- suite('_getNonOwnerRole', () => {
- let change;
-
- setup(() => {
- change = {
- owner: {
- email: 'abc@def',
- _account_id: 1019328,
- },
- revisions: {
- rev1: {
- _number: 1,
- uploader: {
- email: 'ghi@def',
- _account_id: 1011123,
- },
- commit: {
- author: {email: 'jkl@def'},
- committer: {email: 'ghi@def'},
- },
- },
- },
- current_revision: 'rev1',
- };
- });
-
- suite('role=uploader', () => {
- test('_getNonOwnerRole for uploader', () => {
- assert.deepEqual(
- element._getNonOwnerRole(change, element._CHANGE_ROLE.UPLOADER),
- {email: 'ghi@def', _account_id: 1011123});
- });
-
- test('_getNonOwnerRole that it does not return uploader', () => {
- // Set the uploader email to be the same as the owner.
- change.revisions.rev1.uploader._account_id = 1019328;
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.UPLOADER));
- });
-
- test('_getNonOwnerRole null for uploader with no current rev', () => {
- delete change.current_revision;
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.UPLOADER));
- });
-
- test('_computeShowRoleClass show uploader', () => {
- assert.equal(element._computeShowRoleClass(
- change, element._CHANGE_ROLE.UPLOADER), '');
- });
-
- test('_computeShowRoleClass hide uploader', () => {
- // Set the uploader email to be the same as the owner.
- change.revisions.rev1.uploader._account_id = 1019328;
- assert.equal(element._computeShowRoleClass(change,
- element._CHANGE_ROLE.UPLOADER), 'hideDisplay');
- });
- });
-
- suite('role=committer', () => {
- test('_getNonOwnerRole for committer', () => {
- assert.deepEqual(
- element._getNonOwnerRole(change, element._CHANGE_ROLE.COMMITTER),
- {email: 'ghi@def'});
- });
-
- test('_getNonOwnerRole that it does not return committer', () => {
- // Set the committer email to be the same as the owner.
- change.revisions.rev1.commit.committer.email = 'abc@def';
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.COMMITTER));
- });
-
- test('_getNonOwnerRole null for committer with no current rev', () => {
- delete change.current_revision;
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.COMMITTER));
- });
-
- test('_getNonOwnerRole null for committer with no commit', () => {
- delete change.revisions.rev1.commit;
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.COMMITTER));
- });
-
- test('_getNonOwnerRole null for committer with no committer', () => {
- delete change.revisions.rev1.commit.committer;
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.COMMITTER));
- });
- });
-
- suite('role=author', () => {
- test('_getNonOwnerRole for author', () => {
- assert.deepEqual(
- element._getNonOwnerRole(change, element._CHANGE_ROLE.AUTHOR),
- {email: 'jkl@def'});
- });
-
- test('_getNonOwnerRole that it does not return author', () => {
- // Set the author email to be the same as the owner.
- change.revisions.rev1.commit.author.email = 'abc@def';
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.AUTHOR));
- });
-
- test('_getNonOwnerRole null for author with no current rev', () => {
- delete change.current_revision;
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.AUTHOR));
- });
-
- test('_getNonOwnerRole null for author with no commit', () => {
- delete change.revisions.rev1.commit;
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.AUTHOR));
- });
-
- test('_getNonOwnerRole null for author with no author', () => {
- delete change.revisions.rev1.commit.author;
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.AUTHOR));
- });
- });
- });
-
- test('Push Certificate Validation test BAD', () => {
- const serverConfig = {
- receive: {
- enable_signed_push: true,
- },
- };
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ change = {
owner: {
+ email: 'abc@def',
_account_id: 1019328,
},
revisions: {
rev1: {
_number: 1,
- push_certificate: {
- key: {
- status: 'BAD',
- problems: [
- 'No public keys found for key ID E5E20E52',
- ],
- },
+ uploader: {
+ email: 'ghi@def',
+ _account_id: 1011123,
+ },
+ commit: {
+ author: {email: 'jkl@def'},
+ committer: {email: 'ghi@def'},
},
},
},
current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- mergeable: true,
};
- const result =
- element._computePushCertificateValidation(serverConfig, change);
- assert.equal(result.message,
- 'Push certificate is invalid:\n' +
- 'No public keys found for key ID E5E20E52');
- assert.equal(result.icon, 'gr-icons:close');
- assert.equal(result.class, 'invalid');
});
- test('Push Certificate Validation test TRUSTED', () => {
- const serverConfig = {
- receive: {
- enable_signed_push: true,
- },
- };
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- owner: {
- _account_id: 1019328,
- },
- revisions: {
- rev1: {
- _number: 1,
- push_certificate: {
- key: {
- status: 'TRUSTED',
- },
- },
- },
- },
- current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- mergeable: true,
- };
- const result =
- element._computePushCertificateValidation(serverConfig, change);
- assert.equal(result.message,
- 'Push certificate is valid and key is trusted');
- assert.equal(result.icon, 'gr-icons:check');
- assert.equal(result.class, 'trusted');
- });
-
- test('Push Certificate Validation is missing test', () => {
- const serverConfig = {
- receive: {
- enable_signed_push: true,
- },
- };
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- owner: {
- _account_id: 1019328,
- },
- revisions: {
- rev1: {
- _number: 1,
- },
- },
- current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- mergeable: true,
- };
- const result =
- element._computePushCertificateValidation(serverConfig, change);
- assert.equal(result.message,
- 'This patch set was created without a push certificate');
- assert.equal(result.icon, 'gr-icons:help');
- assert.equal(result.class, 'help');
- });
-
- test('_computeParents', () => {
- const parents = [{commit: '123', subject: 'abc'}];
- assert.isUndefined(element._computeParents(
- {revisions: {456: {commit: {parents}}}}));
- assert.isUndefined(element._computeParents(
- {current_revision: '789', revisions: {456: {commit: {parents}}}}));
- assert.equal(element._computeParents(
- {current_revision: '456', revisions: {456: {commit: {parents}}}}),
- parents);
- });
-
- test('_computeParentsLabel', () => {
- const parent = {commit: 'abc123', subject: 'My parent commit'};
- assert.equal(element._computeParentsLabel([parent]), 'Parent');
- assert.equal(element._computeParentsLabel([parent, parent]),
- 'Parents');
- });
-
- test('_computeParentListClass', () => {
- const parent = {commit: 'abc123', subject: 'My parent commit'};
- assert.equal(element._computeParentListClass([parent], true),
- 'parentList nonMerge current');
- assert.equal(element._computeParentListClass([parent], false),
- 'parentList nonMerge notCurrent');
- assert.equal(element._computeParentListClass([parent, parent], false),
- 'parentList merge notCurrent');
- assert.equal(element._computeParentListClass([parent, parent], true),
- 'parentList merge current');
- });
-
- test('_showAddTopic', () => {
- assert.isTrue(element._showAddTopic(null, false));
- assert.isTrue(element._showAddTopic({base: {topic: null}}, false));
- assert.isFalse(element._showAddTopic({base: {topic: null}}, true));
- assert.isFalse(element._showAddTopic({base: {topic: 'foo'}}, true));
- assert.isFalse(element._showAddTopic({base: {topic: 'foo'}}, false));
- });
-
- test('_showTopicChip', () => {
- assert.isFalse(element._showTopicChip(null, false));
- assert.isFalse(element._showTopicChip({base: {topic: null}}, false));
- assert.isFalse(element._showTopicChip({base: {topic: null}}, true));
- assert.isFalse(element._showTopicChip({base: {topic: 'foo'}}, true));
- assert.isTrue(element._showTopicChip({base: {topic: 'foo'}}, false));
- });
-
- test('_showCherryPickOf', () => {
- assert.isFalse(element._showCherryPickOf(null));
- assert.isFalse(element._showCherryPickOf({
- base: {
- cherry_pick_of_change: null,
- cherry_pick_of_patch_set: null,
- },
- }));
- assert.isTrue(element._showCherryPickOf({
- base: {
- cherry_pick_of_change: 123,
- cherry_pick_of_patch_set: 1,
- },
- }));
- });
-
- suite('Topic removal', () => {
- let change;
- setup(() => {
- change = {
- _number: 'the number',
- actions: {
- topic: {enabled: false},
- },
- change_id: 'the id',
- topic: 'the topic',
- status: 'NEW',
- submit_type: 'CHERRY_PICK',
- labels: {
- test: {
- all: [{_account_id: 1, name: 'bojack', value: 1}],
- default_value: 0,
- values: [],
- },
- },
- removable_reviewers: [],
- };
+ suite('role=uploader', () => {
+ test('_getNonOwnerRole for uploader', () => {
+ assert.deepEqual(
+ element._getNonOwnerRole(change, element._CHANGE_ROLE.UPLOADER),
+ {email: 'ghi@def', _account_id: 1011123});
});
- test('_computeTopicReadOnly', () => {
- let mutable = false;
- assert.isTrue(element._computeTopicReadOnly(mutable, change));
- mutable = true;
- assert.isTrue(element._computeTopicReadOnly(mutable, change));
- change.actions.topic.enabled = true;
- assert.isFalse(element._computeTopicReadOnly(mutable, change));
- mutable = false;
- assert.isTrue(element._computeTopicReadOnly(mutable, change));
+ test('_getNonOwnerRole that it does not return uploader', () => {
+ // Set the uploader email to be the same as the owner.
+ change.revisions.rev1.uploader._account_id = 1019328;
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.UPLOADER));
});
- test('topic read only hides delete button', () => {
- element.account = {};
- element.change = change;
- flushAsynchronousOperations();
- const button = element.shadowRoot
- .querySelector('gr-linked-chip').shadowRoot
- .querySelector('gr-button');
- assert.isTrue(button.hasAttribute('hidden'));
+ test('_getNonOwnerRole null for uploader with no current rev', () => {
+ delete change.current_revision;
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.UPLOADER));
});
- test('topic not read only does not hide delete button', () => {
- element.account = {test: true};
- change.actions.topic.enabled = true;
- element.change = change;
- flushAsynchronousOperations();
- const button = element.shadowRoot
- .querySelector('gr-linked-chip').shadowRoot
- .querySelector('gr-button');
- assert.isFalse(button.hasAttribute('hidden'));
+ test('_computeShowRoleClass show uploader', () => {
+ assert.equal(element._computeShowRoleClass(
+ change, element._CHANGE_ROLE.UPLOADER), '');
+ });
+
+ test('_computeShowRoleClass hide uploader', () => {
+ // Set the uploader email to be the same as the owner.
+ change.revisions.rev1.uploader._account_id = 1019328;
+ assert.equal(element._computeShowRoleClass(change,
+ element._CHANGE_ROLE.UPLOADER), 'hideDisplay');
});
});
- suite('Hashtag removal', () => {
- let change;
- setup(() => {
- change = {
- _number: 'the number',
- actions: {
- hashtags: {enabled: false},
- },
- change_id: 'the id',
- hashtags: ['test-hashtag'],
- status: 'NEW',
- submit_type: 'CHERRY_PICK',
- labels: {
- test: {
- all: [{_account_id: 1, name: 'bojack', value: 1}],
- default_value: 0,
- values: [],
- },
- },
- removable_reviewers: [],
- };
+ suite('role=committer', () => {
+ test('_getNonOwnerRole for committer', () => {
+ assert.deepEqual(
+ element._getNonOwnerRole(change, element._CHANGE_ROLE.COMMITTER),
+ {email: 'ghi@def'});
});
- test('_computeHashtagReadOnly', () => {
- flushAsynchronousOperations();
- let mutable = false;
- assert.isTrue(element._computeHashtagReadOnly(mutable, change));
- mutable = true;
- assert.isTrue(element._computeHashtagReadOnly(mutable, change));
- change.actions.hashtags.enabled = true;
- assert.isFalse(element._computeHashtagReadOnly(mutable, change));
- mutable = false;
- assert.isTrue(element._computeHashtagReadOnly(mutable, change));
+ test('_getNonOwnerRole that it does not return committer', () => {
+ // Set the committer email to be the same as the owner.
+ change.revisions.rev1.commit.committer.email = 'abc@def';
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.COMMITTER));
});
- test('hashtag read only hides delete button', () => {
- flushAsynchronousOperations();
- element.account = {};
- element.change = change;
- flushAsynchronousOperations();
- const button = element.shadowRoot
- .querySelector('gr-linked-chip').shadowRoot
- .querySelector('gr-button');
- assert.isTrue(button.hasAttribute('hidden'));
+ test('_getNonOwnerRole null for committer with no current rev', () => {
+ delete change.current_revision;
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.COMMITTER));
});
- test('hashtag not read only does not hide delete button', () => {
- flushAsynchronousOperations();
- element.account = {test: true};
- change.actions.hashtags.enabled = true;
- element.change = change;
- flushAsynchronousOperations();
- const button = element.shadowRoot
- .querySelector('gr-linked-chip').shadowRoot
- .querySelector('gr-button');
- assert.isFalse(button.hasAttribute('hidden'));
+ test('_getNonOwnerRole null for committer with no commit', () => {
+ delete change.revisions.rev1.commit;
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.COMMITTER));
+ });
+
+ test('_getNonOwnerRole null for committer with no committer', () => {
+ delete change.revisions.rev1.commit.committer;
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.COMMITTER));
});
});
- suite('remove reviewer votes', () => {
- setup(() => {
- sandbox.stub(element, '_computeTopicReadOnly').returns(true);
- element.change = {
- _number: 42,
- change_id: 'the id',
- actions: [],
- topic: 'the topic',
- status: 'NEW',
- submit_type: 'CHERRY_PICK',
- labels: {
- test: {
- all: [{_account_id: 1, name: 'bojack', value: 1}],
- default_value: 0,
- values: [],
- },
- },
- removable_reviewers: [],
- };
- flushAsynchronousOperations();
+ suite('role=author', () => {
+ test('_getNonOwnerRole for author', () => {
+ assert.deepEqual(
+ element._getNonOwnerRole(change, element._CHANGE_ROLE.AUTHOR),
+ {email: 'jkl@def'});
});
- suite('assignee field', () => {
- const dummyAccount = {
- _account_id: 1,
- name: 'bojack',
- };
- const change = {
- actions: {
- assignee: {enabled: false},
- },
- assignee: dummyAccount,
- };
- let deleteStub;
- let setStub;
-
- setup(() => {
- deleteStub = sandbox.stub(element.$.restAPI, 'deleteAssignee');
- setStub = sandbox.stub(element.$.restAPI, 'setAssignee');
- });
-
- test('changing change recomputes _assignee', () => {
- assert.isFalse(!!element._assignee.length);
- const change = element.change;
- change.assignee = dummyAccount;
- element._changeChanged(change);
- assert.deepEqual(element._assignee[0], dummyAccount);
- });
-
- test('modifying _assignee calls API', () => {
- assert.isFalse(!!element._assignee.length);
- element.set('_assignee', [dummyAccount]);
- assert.isTrue(setStub.calledOnce);
- assert.deepEqual(element.change.assignee, dummyAccount);
- element.set('_assignee', [dummyAccount]);
- assert.isTrue(setStub.calledOnce);
- element.set('_assignee', []);
- assert.isTrue(deleteStub.calledOnce);
- assert.equal(element.change.assignee, undefined);
- element.set('_assignee', []);
- assert.isTrue(deleteStub.calledOnce);
- });
-
- test('_computeAssigneeReadOnly', () => {
- let mutable = false;
- assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
- mutable = true;
- assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
- change.actions.assignee.enabled = true;
- assert.isFalse(element._computeAssigneeReadOnly(mutable, change));
- mutable = false;
- assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
- });
+ test('_getNonOwnerRole that it does not return author', () => {
+ // Set the author email to be the same as the owner.
+ change.revisions.rev1.commit.author.email = 'abc@def';
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.AUTHOR));
});
- test('changing topic', () => {
- const newTopic = 'the new topic';
- sandbox.stub(element.$.restAPI, 'setChangeTopic').returns(
- Promise.resolve(newTopic));
- element._handleTopicChanged({}, newTopic);
- const topicChangedSpy = sandbox.spy();
- element.addEventListener('topic-changed', topicChangedSpy);
- assert.isTrue(element.$.restAPI.setChangeTopic.calledWith(
- 42, newTopic));
- return element.$.restAPI.setChangeTopic.lastCall.returnValue
- .then(() => {
- assert.equal(element.change.topic, newTopic);
- assert.isTrue(topicChangedSpy.called);
- });
+ test('_getNonOwnerRole null for author with no current rev', () => {
+ delete change.current_revision;
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.AUTHOR));
});
- test('topic removal', () => {
- sandbox.stub(element.$.restAPI, 'setChangeTopic').returns(
- Promise.resolve());
- const chip = element.shadowRoot
- .querySelector('gr-linked-chip');
- const remove = chip.$.remove;
- const topicChangedSpy = sandbox.spy();
- element.addEventListener('topic-changed', topicChangedSpy);
- MockInteractions.tap(remove);
- assert.isTrue(chip.disabled);
- assert.isTrue(element.$.restAPI.setChangeTopic.calledWith(
- 42, null));
- return element.$.restAPI.setChangeTopic.lastCall.returnValue
- .then(() => {
- assert.isFalse(chip.disabled);
- assert.equal(element.change.topic, '');
- assert.isTrue(topicChangedSpy.called);
- });
+ test('_getNonOwnerRole null for author with no commit', () => {
+ delete change.revisions.rev1.commit;
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.AUTHOR));
});
- test('changing hashtag', () => {
- flushAsynchronousOperations();
- element._newHashtag = 'new hashtag';
- const newHashtag = ['new hashtag'];
- sandbox.stub(element.$.restAPI, 'setChangeHashtag').returns(
- Promise.resolve(newHashtag));
- element._handleHashtagChanged({}, 'new hashtag');
- assert.isTrue(element.$.restAPI.setChangeHashtag.calledWith(
- 42, {add: ['new hashtag']}));
- return element.$.restAPI.setChangeHashtag.lastCall.returnValue
- .then(() => {
- assert.equal(element.change.hashtags, newHashtag);
- });
- });
- });
-
- test('editTopic', () => {
- element.account = {test: true};
- element.change = {actions: {topic: {enabled: true}}};
- flushAsynchronousOperations();
-
- const label = element.shadowRoot
- .querySelector('.topicEditableLabel');
- assert.ok(label);
- sandbox.stub(label, 'open');
- element.editTopic();
- flushAsynchronousOperations();
-
- assert.isTrue(label.open.called);
- });
-
- suite('plugin endpoints', () => {
- test('endpoint params', done => {
- element.change = {labels: {}};
- element.revision = {};
- let hookEl;
- let plugin;
- Gerrit.install(
- p => {
- plugin = p;
- plugin.hook('change-metadata-item').getLastAttached()
- .then(el => hookEl = el);
- },
- '0.1',
- 'http://some/plugins/url.html');
- Gerrit._loadPlugins([]);
- flush(() => {
- assert.strictEqual(hookEl.plugin, plugin);
- assert.strictEqual(hookEl.change, element.change);
- assert.strictEqual(hookEl.revision, element.revision);
- done();
- });
+ test('_getNonOwnerRole null for author with no author', () => {
+ delete change.revisions.rev1.commit.author;
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.AUTHOR));
});
});
});
+
+ test('Push Certificate Validation test BAD', () => {
+ const serverConfig = {
+ receive: {
+ enable_signed_push: true,
+ },
+ };
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ owner: {
+ _account_id: 1019328,
+ },
+ revisions: {
+ rev1: {
+ _number: 1,
+ push_certificate: {
+ key: {
+ status: 'BAD',
+ problems: [
+ 'No public keys found for key ID E5E20E52',
+ ],
+ },
+ },
+ },
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ mergeable: true,
+ };
+ const result =
+ element._computePushCertificateValidation(serverConfig, change);
+ assert.equal(result.message,
+ 'Push certificate is invalid:\n' +
+ 'No public keys found for key ID E5E20E52');
+ assert.equal(result.icon, 'gr-icons:close');
+ assert.equal(result.class, 'invalid');
+ });
+
+ test('Push Certificate Validation test TRUSTED', () => {
+ const serverConfig = {
+ receive: {
+ enable_signed_push: true,
+ },
+ };
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ owner: {
+ _account_id: 1019328,
+ },
+ revisions: {
+ rev1: {
+ _number: 1,
+ push_certificate: {
+ key: {
+ status: 'TRUSTED',
+ },
+ },
+ },
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ mergeable: true,
+ };
+ const result =
+ element._computePushCertificateValidation(serverConfig, change);
+ assert.equal(result.message,
+ 'Push certificate is valid and key is trusted');
+ assert.equal(result.icon, 'gr-icons:check');
+ assert.equal(result.class, 'trusted');
+ });
+
+ test('Push Certificate Validation is missing test', () => {
+ const serverConfig = {
+ receive: {
+ enable_signed_push: true,
+ },
+ };
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ owner: {
+ _account_id: 1019328,
+ },
+ revisions: {
+ rev1: {
+ _number: 1,
+ },
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ mergeable: true,
+ };
+ const result =
+ element._computePushCertificateValidation(serverConfig, change);
+ assert.equal(result.message,
+ 'This patch set was created without a push certificate');
+ assert.equal(result.icon, 'gr-icons:help');
+ assert.equal(result.class, 'help');
+ });
+
+ test('_computeParents', () => {
+ const parents = [{commit: '123', subject: 'abc'}];
+ assert.isUndefined(element._computeParents(
+ {revisions: {456: {commit: {parents}}}}));
+ assert.isUndefined(element._computeParents(
+ {current_revision: '789', revisions: {456: {commit: {parents}}}}));
+ assert.equal(element._computeParents(
+ {current_revision: '456', revisions: {456: {commit: {parents}}}}),
+ parents);
+ });
+
+ test('_computeParentsLabel', () => {
+ const parent = {commit: 'abc123', subject: 'My parent commit'};
+ assert.equal(element._computeParentsLabel([parent]), 'Parent');
+ assert.equal(element._computeParentsLabel([parent, parent]),
+ 'Parents');
+ });
+
+ test('_computeParentListClass', () => {
+ const parent = {commit: 'abc123', subject: 'My parent commit'};
+ assert.equal(element._computeParentListClass([parent], true),
+ 'parentList nonMerge current');
+ assert.equal(element._computeParentListClass([parent], false),
+ 'parentList nonMerge notCurrent');
+ assert.equal(element._computeParentListClass([parent, parent], false),
+ 'parentList merge notCurrent');
+ assert.equal(element._computeParentListClass([parent, parent], true),
+ 'parentList merge current');
+ });
+
+ test('_showAddTopic', () => {
+ assert.isTrue(element._showAddTopic(null, false));
+ assert.isTrue(element._showAddTopic({base: {topic: null}}, false));
+ assert.isFalse(element._showAddTopic({base: {topic: null}}, true));
+ assert.isFalse(element._showAddTopic({base: {topic: 'foo'}}, true));
+ assert.isFalse(element._showAddTopic({base: {topic: 'foo'}}, false));
+ });
+
+ test('_showTopicChip', () => {
+ assert.isFalse(element._showTopicChip(null, false));
+ assert.isFalse(element._showTopicChip({base: {topic: null}}, false));
+ assert.isFalse(element._showTopicChip({base: {topic: null}}, true));
+ assert.isFalse(element._showTopicChip({base: {topic: 'foo'}}, true));
+ assert.isTrue(element._showTopicChip({base: {topic: 'foo'}}, false));
+ });
+
+ test('_showCherryPickOf', () => {
+ assert.isFalse(element._showCherryPickOf(null));
+ assert.isFalse(element._showCherryPickOf({
+ base: {
+ cherry_pick_of_change: null,
+ cherry_pick_of_patch_set: null,
+ },
+ }));
+ assert.isTrue(element._showCherryPickOf({
+ base: {
+ cherry_pick_of_change: 123,
+ cherry_pick_of_patch_set: 1,
+ },
+ }));
+ });
+
+ suite('Topic removal', () => {
+ let change;
+ setup(() => {
+ change = {
+ _number: 'the number',
+ actions: {
+ topic: {enabled: false},
+ },
+ change_id: 'the id',
+ topic: 'the topic',
+ status: 'NEW',
+ submit_type: 'CHERRY_PICK',
+ labels: {
+ test: {
+ all: [{_account_id: 1, name: 'bojack', value: 1}],
+ default_value: 0,
+ values: [],
+ },
+ },
+ removable_reviewers: [],
+ };
+ });
+
+ test('_computeTopicReadOnly', () => {
+ let mutable = false;
+ assert.isTrue(element._computeTopicReadOnly(mutable, change));
+ mutable = true;
+ assert.isTrue(element._computeTopicReadOnly(mutable, change));
+ change.actions.topic.enabled = true;
+ assert.isFalse(element._computeTopicReadOnly(mutable, change));
+ mutable = false;
+ assert.isTrue(element._computeTopicReadOnly(mutable, change));
+ });
+
+ test('topic read only hides delete button', () => {
+ element.account = {};
+ element.change = change;
+ flushAsynchronousOperations();
+ const button = element.shadowRoot
+ .querySelector('gr-linked-chip').shadowRoot
+ .querySelector('gr-button');
+ assert.isTrue(button.hasAttribute('hidden'));
+ });
+
+ test('topic not read only does not hide delete button', () => {
+ element.account = {test: true};
+ change.actions.topic.enabled = true;
+ element.change = change;
+ flushAsynchronousOperations();
+ const button = element.shadowRoot
+ .querySelector('gr-linked-chip').shadowRoot
+ .querySelector('gr-button');
+ assert.isFalse(button.hasAttribute('hidden'));
+ });
+ });
+
+ suite('Hashtag removal', () => {
+ let change;
+ setup(() => {
+ change = {
+ _number: 'the number',
+ actions: {
+ hashtags: {enabled: false},
+ },
+ change_id: 'the id',
+ hashtags: ['test-hashtag'],
+ status: 'NEW',
+ submit_type: 'CHERRY_PICK',
+ labels: {
+ test: {
+ all: [{_account_id: 1, name: 'bojack', value: 1}],
+ default_value: 0,
+ values: [],
+ },
+ },
+ removable_reviewers: [],
+ };
+ });
+
+ test('_computeHashtagReadOnly', () => {
+ flushAsynchronousOperations();
+ let mutable = false;
+ assert.isTrue(element._computeHashtagReadOnly(mutable, change));
+ mutable = true;
+ assert.isTrue(element._computeHashtagReadOnly(mutable, change));
+ change.actions.hashtags.enabled = true;
+ assert.isFalse(element._computeHashtagReadOnly(mutable, change));
+ mutable = false;
+ assert.isTrue(element._computeHashtagReadOnly(mutable, change));
+ });
+
+ test('hashtag read only hides delete button', () => {
+ flushAsynchronousOperations();
+ element.account = {};
+ element.change = change;
+ flushAsynchronousOperations();
+ const button = element.shadowRoot
+ .querySelector('gr-linked-chip').shadowRoot
+ .querySelector('gr-button');
+ assert.isTrue(button.hasAttribute('hidden'));
+ });
+
+ test('hashtag not read only does not hide delete button', () => {
+ flushAsynchronousOperations();
+ element.account = {test: true};
+ change.actions.hashtags.enabled = true;
+ element.change = change;
+ flushAsynchronousOperations();
+ const button = element.shadowRoot
+ .querySelector('gr-linked-chip').shadowRoot
+ .querySelector('gr-button');
+ assert.isFalse(button.hasAttribute('hidden'));
+ });
+ });
+
+ suite('remove reviewer votes', () => {
+ setup(() => {
+ sandbox.stub(element, '_computeTopicReadOnly').returns(true);
+ element.change = {
+ _number: 42,
+ change_id: 'the id',
+ actions: [],
+ topic: 'the topic',
+ status: 'NEW',
+ submit_type: 'CHERRY_PICK',
+ labels: {
+ test: {
+ all: [{_account_id: 1, name: 'bojack', value: 1}],
+ default_value: 0,
+ values: [],
+ },
+ },
+ removable_reviewers: [],
+ };
+ flushAsynchronousOperations();
+ });
+
+ suite('assignee field', () => {
+ const dummyAccount = {
+ _account_id: 1,
+ name: 'bojack',
+ };
+ const change = {
+ actions: {
+ assignee: {enabled: false},
+ },
+ assignee: dummyAccount,
+ };
+ let deleteStub;
+ let setStub;
+
+ setup(() => {
+ deleteStub = sandbox.stub(element.$.restAPI, 'deleteAssignee');
+ setStub = sandbox.stub(element.$.restAPI, 'setAssignee');
+ });
+
+ test('changing change recomputes _assignee', () => {
+ assert.isFalse(!!element._assignee.length);
+ const change = element.change;
+ change.assignee = dummyAccount;
+ element._changeChanged(change);
+ assert.deepEqual(element._assignee[0], dummyAccount);
+ });
+
+ test('modifying _assignee calls API', () => {
+ assert.isFalse(!!element._assignee.length);
+ element.set('_assignee', [dummyAccount]);
+ assert.isTrue(setStub.calledOnce);
+ assert.deepEqual(element.change.assignee, dummyAccount);
+ element.set('_assignee', [dummyAccount]);
+ assert.isTrue(setStub.calledOnce);
+ element.set('_assignee', []);
+ assert.isTrue(deleteStub.calledOnce);
+ assert.equal(element.change.assignee, undefined);
+ element.set('_assignee', []);
+ assert.isTrue(deleteStub.calledOnce);
+ });
+
+ test('_computeAssigneeReadOnly', () => {
+ let mutable = false;
+ assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
+ mutable = true;
+ assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
+ change.actions.assignee.enabled = true;
+ assert.isFalse(element._computeAssigneeReadOnly(mutable, change));
+ mutable = false;
+ assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
+ });
+ });
+
+ test('changing topic', () => {
+ const newTopic = 'the new topic';
+ sandbox.stub(element.$.restAPI, 'setChangeTopic').returns(
+ Promise.resolve(newTopic));
+ element._handleTopicChanged({}, newTopic);
+ const topicChangedSpy = sandbox.spy();
+ element.addEventListener('topic-changed', topicChangedSpy);
+ assert.isTrue(element.$.restAPI.setChangeTopic.calledWith(
+ 42, newTopic));
+ return element.$.restAPI.setChangeTopic.lastCall.returnValue
+ .then(() => {
+ assert.equal(element.change.topic, newTopic);
+ assert.isTrue(topicChangedSpy.called);
+ });
+ });
+
+ test('topic removal', () => {
+ sandbox.stub(element.$.restAPI, 'setChangeTopic').returns(
+ Promise.resolve());
+ const chip = element.shadowRoot
+ .querySelector('gr-linked-chip');
+ const remove = chip.$.remove;
+ const topicChangedSpy = sandbox.spy();
+ element.addEventListener('topic-changed', topicChangedSpy);
+ MockInteractions.tap(remove);
+ assert.isTrue(chip.disabled);
+ assert.isTrue(element.$.restAPI.setChangeTopic.calledWith(
+ 42, null));
+ return element.$.restAPI.setChangeTopic.lastCall.returnValue
+ .then(() => {
+ assert.isFalse(chip.disabled);
+ assert.equal(element.change.topic, '');
+ assert.isTrue(topicChangedSpy.called);
+ });
+ });
+
+ test('changing hashtag', () => {
+ flushAsynchronousOperations();
+ element._newHashtag = 'new hashtag';
+ const newHashtag = ['new hashtag'];
+ sandbox.stub(element.$.restAPI, 'setChangeHashtag').returns(
+ Promise.resolve(newHashtag));
+ element._handleHashtagChanged({}, 'new hashtag');
+ assert.isTrue(element.$.restAPI.setChangeHashtag.calledWith(
+ 42, {add: ['new hashtag']}));
+ return element.$.restAPI.setChangeHashtag.lastCall.returnValue
+ .then(() => {
+ assert.equal(element.change.hashtags, newHashtag);
+ });
+ });
+ });
+
+ test('editTopic', () => {
+ element.account = {test: true};
+ element.change = {actions: {topic: {enabled: true}}};
+ flushAsynchronousOperations();
+
+ const label = element.shadowRoot
+ .querySelector('.topicEditableLabel');
+ assert.ok(label);
+ sandbox.stub(label, 'open');
+ element.editTopic();
+ flushAsynchronousOperations();
+
+ assert.isTrue(label.open.called);
+ });
+
+ suite('plugin endpoints', () => {
+ test('endpoint params', done => {
+ element.change = {labels: {}};
+ element.revision = {};
+ let hookEl;
+ let plugin;
+ Gerrit.install(
+ p => {
+ plugin = p;
+ plugin.hook('change-metadata-item').getLastAttached()
+ .then(el => hookEl = el);
+ },
+ '0.1',
+ 'http://some/plugins/url.html');
+ Gerrit._loadPlugins([]);
+ flush(() => {
+ assert.strictEqual(hookEl.plugin, plugin);
+ assert.strictEqual(hookEl.change, element.change);
+ assert.strictEqual(hookEl.revision, element.revision);
+ done();
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html
deleted file mode 100644
index 47ff7f7..0000000
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html
+++ /dev/null
@@ -1,169 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-label/gr-label.html">
-<link rel="import" href="../../shared/gr-label-info/gr-label-info.html">
-<link rel="import" href="../../shared/gr-limited-text/gr-limited-text.html">
-
-<dom-module id="gr-change-requirements">
- <template strip-whitespace>
- <style include="shared-styles">
- :host {
- display: table;
- width: 100%;
- }
- .status {
- color: #FFA62F;
- display: inline-block;
- text-align: center;
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-mono);
- line-height: var(--line-height-mono);
- }
- .approved.status {
- color: var(--vote-text-color-recommended);
- }
- .rejected.status {
- color: var(--vote-text-color-disliked);
- }
- iron-icon {
- color: inherit;
- }
- .status iron-icon {
- vertical-align: top;
- }
- section {
- display: table-row;
- }
- .show-hide {
- float: right;
- }
- .title {
- min-width: 10em;
- padding: var(--spacing-s) var(--spacing-m) 0 var(--requirements-horizontal-padding);
- }
- .value {
- padding: var(--spacing-s) 0 0 0;
- }
- .title,
- .value {
- display: table-cell;
- vertical-align: top;
- }
- .hidden {
- display: none;
- }
- .showHide {
- cursor: pointer;
- }
- .showHide .title {
- padding-bottom: var(--spacing-m);
- padding-top: var(--spacing-l);
- }
- .showHide .value {
- padding-top: 0;
- vertical-align: middle;
- }
- .showHide iron-icon {
- color: var(--deemphasized-text-color);
- float: right;
- }
- .spacer {
- height: var(--spacing-m);
- }
- </style>
- <template
- is="dom-repeat"
- items="[[_requirements]]">
- <section>
- <div class="title requirement">
- <span class$="status [[item.style]]">
- <iron-icon class="icon" icon="[[_computeRequirementIcon(item.satisfied)]]"></iron-icon>
- </span>
- <gr-limited-text class="name" limit="40" text="[[item.fallback_text]]"></gr-limited-text>
- </div>
- </section>
- </template>
- <template
- is="dom-repeat"
- items="[[_requiredLabels]]">
- <section>
- <div class="title">
- <span class$="status [[item.style]]">
- <iron-icon class="icon" icon="[[item.icon]]"></iron-icon>
- </span>
- <gr-limited-text class="name" limit="40" text="[[item.label]]"></gr-limited-text>
- </div>
- <div class="value">
- <gr-label-info
- change="{{change}}"
- account="[[account]]"
- mutable="[[mutable]]"
- label="[[item.label]]"
- label-info="[[item.labelInfo]]"></gr-label-info>
- </div>
- </section>
- </template>
- <section class="spacer"></section>
- <section class$="spacer [[_computeShowOptional(_optionalLabels.*)]]"></section>
- <section
- show-bottom-border$="[[_showOptionalLabels]]"
- on-click="_handleShowHide"
- class$="showHide [[_computeShowOptional(_optionalLabels.*)]]">
- <div class="title">Other labels</div>
- <div class="value">
- <iron-icon
- id="showHide"
- icon="[[_computeShowHideIcon(_showOptionalLabels)]]">
- </iron-icon>
- </label>
- </div>
- </section>
- <template
- is="dom-repeat"
- items="[[_optionalLabels]]">
- <section class$="optional [[_computeSectionClass(_showOptionalLabels)]]">
- <div class="title">
- <span class$="status [[item.style]]">
- <template is="dom-if" if="[[item.icon]]">
- <iron-icon class="icon" icon="[[item.icon]]"></iron-icon>
- </template>
- <template is="dom-if" if="[[!item.icon]]">
- <span>[[_computeLabelValue(item.labelInfo.value)]]</span>
- </template>
- </span>
- <gr-limited-text class="name" limit="40" text="[[item.label]]"></gr-limited-text>
- </div>
- <div class="value">
- <gr-label-info
- change="{{change}}"
- account="[[account]]"
- mutable="[[mutable]]"
- label="[[item.label]]"
- label-info="[[item.labelInfo]]"></gr-label-info>
- </div>
- </section>
- </template>
- <section class$="spacer [[_computeShowOptional(_optionalLabels.*)]] [[_computeSectionClass(_showOptionalLabels)]]"></section>
- </template>
- <script src="gr-change-requirements.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
index a413c6f..9dd5acf 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
@@ -14,147 +14,160 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
- */
- class GrChangeRequirements extends Polymer.mixinBehaviors( [
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-change-requirements'; }
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-label/gr-label.js';
+import '../../shared/gr-label-info/gr-label-info.js';
+import '../../shared/gr-limited-text/gr-limited-text.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-change-requirements_html.js';
- static get properties() {
- return {
- /** @type {?} */
- change: Object,
- account: Object,
- mutable: Boolean,
- _requirements: {
- type: Array,
- computed: '_computeRequirements(change)',
- },
- _requiredLabels: {
- type: Array,
- value: () => [],
- },
- _optionalLabels: {
- type: Array,
- value: () => [],
- },
- _showWip: {
- type: Boolean,
- computed: '_computeShowWip(change)',
- },
- _showOptionalLabels: {
- type: Boolean,
- value: true,
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrChangeRequirements extends mixinBehaviors( [
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- static get observers() {
- return [
- '_computeLabels(change.labels.*)',
- ];
- }
+ static get is() { return 'gr-change-requirements'; }
- _computeShowWip(change) {
- return change.work_in_progress;
- }
+ static get properties() {
+ return {
+ /** @type {?} */
+ change: Object,
+ account: Object,
+ mutable: Boolean,
+ _requirements: {
+ type: Array,
+ computed: '_computeRequirements(change)',
+ },
+ _requiredLabels: {
+ type: Array,
+ value: () => [],
+ },
+ _optionalLabels: {
+ type: Array,
+ value: () => [],
+ },
+ _showWip: {
+ type: Boolean,
+ computed: '_computeShowWip(change)',
+ },
+ _showOptionalLabels: {
+ type: Boolean,
+ value: true,
+ },
+ };
+ }
- _computeRequirements(change) {
- const _requirements = [];
+ static get observers() {
+ return [
+ '_computeLabels(change.labels.*)',
+ ];
+ }
- if (change.requirements) {
- for (const requirement of change.requirements) {
- requirement.satisfied = requirement.status === 'OK';
- requirement.style =
- this._computeRequirementClass(requirement.satisfied);
- _requirements.push(requirement);
- }
- }
- if (change.work_in_progress) {
- _requirements.push({
- fallback_text: 'Work-in-progress',
- tooltip: 'Change must not be in \'Work in Progress\' state.',
- });
- }
+ _computeShowWip(change) {
+ return change.work_in_progress;
+ }
- return _requirements;
- }
+ _computeRequirements(change) {
+ const _requirements = [];
- _computeRequirementClass(requirementStatus) {
- return requirementStatus ? 'approved' : '';
- }
-
- _computeRequirementIcon(requirementStatus) {
- return requirementStatus ? 'gr-icons:check' : 'gr-icons:hourglass';
- }
-
- _computeLabels(labelsRecord) {
- const labels = labelsRecord.base;
- this._optionalLabels = [];
- this._requiredLabels = [];
-
- for (const label in labels) {
- if (!labels.hasOwnProperty(label)) { continue; }
-
- const labelInfo = labels[label];
- const icon = this._computeLabelIcon(labelInfo);
- const style = this._computeLabelClass(labelInfo);
- const path = labelInfo.optional ? '_optionalLabels' : '_requiredLabels';
-
- this.push(path, {label, icon, style, labelInfo});
+ if (change.requirements) {
+ for (const requirement of change.requirements) {
+ requirement.satisfied = requirement.status === 'OK';
+ requirement.style =
+ this._computeRequirementClass(requirement.satisfied);
+ _requirements.push(requirement);
}
}
-
- /**
- * @param {Object} labelInfo
- * @return {string} The icon name, or undefined if no icon should
- * be used.
- */
- _computeLabelIcon(labelInfo) {
- if (labelInfo.approved) { return 'gr-icons:check'; }
- if (labelInfo.rejected) { return 'gr-icons:close'; }
- return 'gr-icons:hourglass';
+ if (change.work_in_progress) {
+ _requirements.push({
+ fallback_text: 'Work-in-progress',
+ tooltip: 'Change must not be in \'Work in Progress\' state.',
+ });
}
- /**
- * @param {Object} labelInfo
- */
- _computeLabelClass(labelInfo) {
- if (labelInfo.approved) { return 'approved'; }
- if (labelInfo.rejected) { return 'rejected'; }
- return '';
- }
+ return _requirements;
+ }
- _computeShowOptional(optionalFieldsRecord) {
- return optionalFieldsRecord.base.length ? '' : 'hidden';
- }
+ _computeRequirementClass(requirementStatus) {
+ return requirementStatus ? 'approved' : '';
+ }
- _computeLabelValue(value) {
- return (value > 0 ? '+' : '') + value;
- }
+ _computeRequirementIcon(requirementStatus) {
+ return requirementStatus ? 'gr-icons:check' : 'gr-icons:hourglass';
+ }
- _computeShowHideIcon(showOptionalLabels) {
- return showOptionalLabels ?
- 'gr-icons:expand-less' :
- 'gr-icons:expand-more';
- }
+ _computeLabels(labelsRecord) {
+ const labels = labelsRecord.base;
+ this._optionalLabels = [];
+ this._requiredLabels = [];
- _computeSectionClass(show) {
- return show ? '' : 'hidden';
- }
+ for (const label in labels) {
+ if (!labels.hasOwnProperty(label)) { continue; }
- _handleShowHide(e) {
- this._showOptionalLabels = !this._showOptionalLabels;
+ const labelInfo = labels[label];
+ const icon = this._computeLabelIcon(labelInfo);
+ const style = this._computeLabelClass(labelInfo);
+ const path = labelInfo.optional ? '_optionalLabels' : '_requiredLabels';
+
+ this.push(path, {label, icon, style, labelInfo});
}
}
- customElements.define(GrChangeRequirements.is, GrChangeRequirements);
-})();
+ /**
+ * @param {Object} labelInfo
+ * @return {string} The icon name, or undefined if no icon should
+ * be used.
+ */
+ _computeLabelIcon(labelInfo) {
+ if (labelInfo.approved) { return 'gr-icons:check'; }
+ if (labelInfo.rejected) { return 'gr-icons:close'; }
+ return 'gr-icons:hourglass';
+ }
+
+ /**
+ * @param {Object} labelInfo
+ */
+ _computeLabelClass(labelInfo) {
+ if (labelInfo.approved) { return 'approved'; }
+ if (labelInfo.rejected) { return 'rejected'; }
+ return '';
+ }
+
+ _computeShowOptional(optionalFieldsRecord) {
+ return optionalFieldsRecord.base.length ? '' : 'hidden';
+ }
+
+ _computeLabelValue(value) {
+ return (value > 0 ? '+' : '') + value;
+ }
+
+ _computeShowHideIcon(showOptionalLabels) {
+ return showOptionalLabels ?
+ 'gr-icons:expand-less' :
+ 'gr-icons:expand-more';
+ }
+
+ _computeSectionClass(show) {
+ return show ? '' : 'hidden';
+ }
+
+ _handleShowHide(e) {
+ this._showOptionalLabels = !this._showOptionalLabels;
+ }
+}
+
+customElements.define(GrChangeRequirements.is, GrChangeRequirements);
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js
new file mode 100644
index 0000000..311cfe4
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js
@@ -0,0 +1,137 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: table;
+ width: 100%;
+ }
+ .status {
+ color: #FFA62F;
+ display: inline-block;
+ text-align: center;
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-mono);
+ line-height: var(--line-height-mono);
+ }
+ .approved.status {
+ color: var(--vote-text-color-recommended);
+ }
+ .rejected.status {
+ color: var(--vote-text-color-disliked);
+ }
+ iron-icon {
+ color: inherit;
+ }
+ .status iron-icon {
+ vertical-align: top;
+ }
+ section {
+ display: table-row;
+ }
+ .show-hide {
+ float: right;
+ }
+ .title {
+ min-width: 10em;
+ padding: var(--spacing-s) var(--spacing-m) 0 var(--requirements-horizontal-padding);
+ }
+ .value {
+ padding: var(--spacing-s) 0 0 0;
+ }
+ .title,
+ .value {
+ display: table-cell;
+ vertical-align: top;
+ }
+ .hidden {
+ display: none;
+ }
+ .showHide {
+ cursor: pointer;
+ }
+ .showHide .title {
+ padding-bottom: var(--spacing-m);
+ padding-top: var(--spacing-l);
+ }
+ .showHide .value {
+ padding-top: 0;
+ vertical-align: middle;
+ }
+ .showHide iron-icon {
+ color: var(--deemphasized-text-color);
+ float: right;
+ }
+ .spacer {
+ height: var(--spacing-m);
+ }
+ </style>
+ <template is="dom-repeat" items="[[_requirements]]">
+ <section>
+ <div class="title requirement">
+ <span class\$="status [[item.style]]">
+ <iron-icon class="icon" icon="[[_computeRequirementIcon(item.satisfied)]]"></iron-icon>
+ </span>
+ <gr-limited-text class="name" limit="40" text="[[item.fallback_text]]"></gr-limited-text>
+ </div>
+ </section>
+ </template>
+ <template is="dom-repeat" items="[[_requiredLabels]]">
+ <section>
+ <div class="title">
+ <span class\$="status [[item.style]]">
+ <iron-icon class="icon" icon="[[item.icon]]"></iron-icon>
+ </span>
+ <gr-limited-text class="name" limit="40" text="[[item.label]]"></gr-limited-text>
+ </div>
+ <div class="value">
+ <gr-label-info change="{{change}}" account="[[account]]" mutable="[[mutable]]" label="[[item.label]]" label-info="[[item.labelInfo]]"></gr-label-info>
+ </div>
+ </section>
+ </template>
+ <section class="spacer"></section>
+ <section class\$="spacer [[_computeShowOptional(_optionalLabels.*)]]"></section>
+ <section show-bottom-border\$="[[_showOptionalLabels]]" on-click="_handleShowHide" class\$="showHide [[_computeShowOptional(_optionalLabels.*)]]">
+ <div class="title">Other labels</div>
+ <div class="value">
+ <iron-icon id="showHide" icon="[[_computeShowHideIcon(_showOptionalLabels)]]">
+ </iron-icon>
+
+ </div>
+ </section>
+ <template is="dom-repeat" items="[[_optionalLabels]]">
+ <section class\$="optional [[_computeSectionClass(_showOptionalLabels)]]">
+ <div class="title">
+ <span class\$="status [[item.style]]">
+ <template is="dom-if" if="[[item.icon]]">
+ <iron-icon class="icon" icon="[[item.icon]]"></iron-icon>
+ </template>
+ <template is="dom-if" if="[[!item.icon]]">
+ <span>[[_computeLabelValue(item.labelInfo.value)]]</span>
+ </template>
+ </span>
+ <gr-limited-text class="name" limit="40" text="[[item.label]]"></gr-limited-text>
+ </div>
+ <div class="value">
+ <gr-label-info change="{{change}}" account="[[account]]" mutable="[[mutable]]" label="[[item.label]]" label-info="[[item.labelInfo]]"></gr-label-info>
+ </div>
+ </section>
+ </template>
+ <section class\$="spacer [[_computeShowOptional(_optionalLabels.*)]] [[_computeSectionClass(_showOptionalLabels)]]"></section>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
index 10466db..3a4ef25 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-requirements</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-change-requirements.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,204 +30,205 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-metadata tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-change-requirements.js';
+suite('gr-change-metadata tests', () => {
+ let element;
- setup(() => {
- element = fixture('basic');
- });
-
- test('requirements computed fields', () => {
- assert.isTrue(element._computeShowWip({work_in_progress: true}));
- assert.isFalse(element._computeShowWip({work_in_progress: false}));
-
- assert.equal(element._computeRequirementClass(true), 'approved');
- assert.equal(element._computeRequirementClass(false), '');
-
- assert.equal(element._computeRequirementIcon(true), 'gr-icons:check');
- assert.equal(element._computeRequirementIcon(false),
- 'gr-icons:hourglass');
- });
-
- test('label computed fields', () => {
- assert.equal(element._computeLabelIcon({approved: []}), 'gr-icons:check');
- assert.equal(element._computeLabelIcon({rejected: []}), 'gr-icons:close');
- assert.equal(element._computeLabelIcon({}), 'gr-icons:hourglass');
-
- assert.equal(element._computeLabelClass({approved: []}), 'approved');
- assert.equal(element._computeLabelClass({rejected: []}), 'rejected');
- assert.equal(element._computeLabelClass({}), '');
- assert.equal(element._computeLabelClass({value: 0}), '');
-
- assert.equal(element._computeLabelValue(1), '+1');
- assert.equal(element._computeLabelValue(-1), '-1');
- assert.equal(element._computeLabelValue(0), '0');
- });
-
- test('_computeLabels', () => {
- assert.equal(element._optionalLabels.length, 0);
- assert.equal(element._requiredLabels.length, 0);
- element._computeLabels({base: {
- test: {
- all: [{_account_id: 1, name: 'bojack', value: 1}],
- default_value: 0,
- values: [],
- value: 1,
- },
- opt_test: {
- all: [{_account_id: 1, name: 'bojack', value: 1}],
- default_value: 0,
- values: [],
- optional: true,
- },
- }});
- assert.equal(element._optionalLabels.length, 1);
- assert.equal(element._requiredLabels.length, 1);
-
- assert.equal(element._optionalLabels[0].label, 'opt_test');
- assert.equal(element._optionalLabels[0].icon, 'gr-icons:hourglass');
- assert.equal(element._optionalLabels[0].style, '');
- assert.ok(element._optionalLabels[0].labelInfo);
- });
-
- test('optional show/hide', () => {
- element._optionalLabels = [{label: 'test'}];
- flushAsynchronousOperations();
-
- assert.ok(element.shadowRoot
- .querySelector('section.optional'));
- MockInteractions.tap(element.shadowRoot
- .querySelector('.showHide'));
- flushAsynchronousOperations();
-
- assert.isFalse(element._showOptionalLabels);
- assert.isTrue(isHidden(element.shadowRoot
- .querySelector('section.optional')));
- });
-
- test('properly converts satisfied labels', () => {
- element.change = {
- status: 'NEW',
- labels: {
- Verified: {
- approved: [],
- },
- },
- requirements: [],
- };
- flushAsynchronousOperations();
-
- assert.ok(element.shadowRoot
- .querySelector('.approved'));
- assert.ok(element.shadowRoot
- .querySelector('.name'));
- assert.equal(element.shadowRoot
- .querySelector('.name').text, 'Verified');
- });
-
- test('properly converts unsatisfied labels', () => {
- element.change = {
- status: 'NEW',
- labels: {
- Verified: {
- approved: false,
- },
- },
- };
- flushAsynchronousOperations();
-
- const name = element.shadowRoot
- .querySelector('.name');
- assert.ok(name);
- assert.isFalse(name.hasAttribute('hidden'));
- assert.equal(name.text, 'Verified');
- });
-
- test('properly displays Work In Progress', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [],
- work_in_progress: true,
- };
- flushAsynchronousOperations();
-
- const changeIsWip = element.shadowRoot
- .querySelector('.title');
- assert.ok(changeIsWip);
- });
-
- test('properly displays a satisfied requirement', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'OK',
- }],
- };
- flushAsynchronousOperations();
-
- const requirement = element.shadowRoot
- .querySelector('.requirement');
- assert.ok(requirement);
- assert.isFalse(requirement.hasAttribute('hidden'));
- assert.ok(requirement.querySelector('.approved'));
- assert.equal(requirement.querySelector('.name').text,
- 'Resolve all comments');
- });
-
- test('satisfied class is applied with OK', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'OK',
- }],
- };
- flushAsynchronousOperations();
-
- const requirement = element.shadowRoot
- .querySelector('.requirement');
- assert.ok(requirement);
- assert.ok(requirement.querySelector('.approved'));
- });
-
- test('satisfied class is not applied with NOT_READY', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'NOT_READY',
- }],
- };
- flushAsynchronousOperations();
-
- const requirement = element.shadowRoot
- .querySelector('.requirement');
- assert.ok(requirement);
- assert.strictEqual(requirement.querySelector('.approved'), null);
- });
-
- test('satisfied class is not applied with RULE_ERROR', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'RULE_ERROR',
- }],
- };
- flushAsynchronousOperations();
-
- const requirement = element.shadowRoot
- .querySelector('.requirement');
- assert.ok(requirement);
- assert.strictEqual(requirement.querySelector('.approved'), null);
- });
+ setup(() => {
+ element = fixture('basic');
});
+
+ test('requirements computed fields', () => {
+ assert.isTrue(element._computeShowWip({work_in_progress: true}));
+ assert.isFalse(element._computeShowWip({work_in_progress: false}));
+
+ assert.equal(element._computeRequirementClass(true), 'approved');
+ assert.equal(element._computeRequirementClass(false), '');
+
+ assert.equal(element._computeRequirementIcon(true), 'gr-icons:check');
+ assert.equal(element._computeRequirementIcon(false),
+ 'gr-icons:hourglass');
+ });
+
+ test('label computed fields', () => {
+ assert.equal(element._computeLabelIcon({approved: []}), 'gr-icons:check');
+ assert.equal(element._computeLabelIcon({rejected: []}), 'gr-icons:close');
+ assert.equal(element._computeLabelIcon({}), 'gr-icons:hourglass');
+
+ assert.equal(element._computeLabelClass({approved: []}), 'approved');
+ assert.equal(element._computeLabelClass({rejected: []}), 'rejected');
+ assert.equal(element._computeLabelClass({}), '');
+ assert.equal(element._computeLabelClass({value: 0}), '');
+
+ assert.equal(element._computeLabelValue(1), '+1');
+ assert.equal(element._computeLabelValue(-1), '-1');
+ assert.equal(element._computeLabelValue(0), '0');
+ });
+
+ test('_computeLabels', () => {
+ assert.equal(element._optionalLabels.length, 0);
+ assert.equal(element._requiredLabels.length, 0);
+ element._computeLabels({base: {
+ test: {
+ all: [{_account_id: 1, name: 'bojack', value: 1}],
+ default_value: 0,
+ values: [],
+ value: 1,
+ },
+ opt_test: {
+ all: [{_account_id: 1, name: 'bojack', value: 1}],
+ default_value: 0,
+ values: [],
+ optional: true,
+ },
+ }});
+ assert.equal(element._optionalLabels.length, 1);
+ assert.equal(element._requiredLabels.length, 1);
+
+ assert.equal(element._optionalLabels[0].label, 'opt_test');
+ assert.equal(element._optionalLabels[0].icon, 'gr-icons:hourglass');
+ assert.equal(element._optionalLabels[0].style, '');
+ assert.ok(element._optionalLabels[0].labelInfo);
+ });
+
+ test('optional show/hide', () => {
+ element._optionalLabels = [{label: 'test'}];
+ flushAsynchronousOperations();
+
+ assert.ok(element.shadowRoot
+ .querySelector('section.optional'));
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.showHide'));
+ flushAsynchronousOperations();
+
+ assert.isFalse(element._showOptionalLabels);
+ assert.isTrue(isHidden(element.shadowRoot
+ .querySelector('section.optional')));
+ });
+
+ test('properly converts satisfied labels', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {
+ Verified: {
+ approved: [],
+ },
+ },
+ requirements: [],
+ };
+ flushAsynchronousOperations();
+
+ assert.ok(element.shadowRoot
+ .querySelector('.approved'));
+ assert.ok(element.shadowRoot
+ .querySelector('.name'));
+ assert.equal(element.shadowRoot
+ .querySelector('.name').text, 'Verified');
+ });
+
+ test('properly converts unsatisfied labels', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {
+ Verified: {
+ approved: false,
+ },
+ },
+ };
+ flushAsynchronousOperations();
+
+ const name = element.shadowRoot
+ .querySelector('.name');
+ assert.ok(name);
+ assert.isFalse(name.hasAttribute('hidden'));
+ assert.equal(name.text, 'Verified');
+ });
+
+ test('properly displays Work In Progress', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {},
+ requirements: [],
+ work_in_progress: true,
+ };
+ flushAsynchronousOperations();
+
+ const changeIsWip = element.shadowRoot
+ .querySelector('.title');
+ assert.ok(changeIsWip);
+ });
+
+ test('properly displays a satisfied requirement', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {},
+ requirements: [{
+ fallback_text: 'Resolve all comments',
+ status: 'OK',
+ }],
+ };
+ flushAsynchronousOperations();
+
+ const requirement = element.shadowRoot
+ .querySelector('.requirement');
+ assert.ok(requirement);
+ assert.isFalse(requirement.hasAttribute('hidden'));
+ assert.ok(requirement.querySelector('.approved'));
+ assert.equal(requirement.querySelector('.name').text,
+ 'Resolve all comments');
+ });
+
+ test('satisfied class is applied with OK', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {},
+ requirements: [{
+ fallback_text: 'Resolve all comments',
+ status: 'OK',
+ }],
+ };
+ flushAsynchronousOperations();
+
+ const requirement = element.shadowRoot
+ .querySelector('.requirement');
+ assert.ok(requirement);
+ assert.ok(requirement.querySelector('.approved'));
+ });
+
+ test('satisfied class is not applied with NOT_READY', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {},
+ requirements: [{
+ fallback_text: 'Resolve all comments',
+ status: 'NOT_READY',
+ }],
+ };
+ flushAsynchronousOperations();
+
+ const requirement = element.shadowRoot
+ .querySelector('.requirement');
+ assert.ok(requirement);
+ assert.strictEqual(requirement.querySelector('.approved'), null);
+ });
+
+ test('satisfied class is not applied with RULE_ERROR', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {},
+ requirements: [{
+ fallback_text: 'Resolve all comments',
+ status: 'RULE_ERROR',
+ }],
+ };
+ flushAsynchronousOperations();
+
+ const requirement = element.shadowRoot
+ .querySelector('.requirement');
+ assert.ok(requirement);
+ assert.strictEqual(requirement.querySelector('.approved'), null);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
deleted file mode 100644
index 9a53342..0000000
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ /dev/null
@@ -1,743 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="/bower_components/paper-tabs/paper-tabs.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
-<link rel="import" href="../../edit/gr-edit-constants.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
-<link rel="import" href="../../shared/gr-change-status/gr-change-status.html">
-<link rel="import" href="../../shared/gr-count-string-formatter/gr-count-string-formatter.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-editable-content/gr-editable-content.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../../shared/revision-info/revision-info.html">
-<link rel="import" href="../gr-change-actions/gr-change-actions.html">
-<link rel="import" href="../gr-change-metadata/gr-change-metadata.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../gr-commit-info/gr-commit-info.html">
-<link rel="import" href="../gr-download-dialog/gr-download-dialog.html">
-<link rel="import" href="../gr-file-list-header/gr-file-list-header.html">
-<link rel="import" href="../gr-file-list/gr-file-list.html">
-<link rel="import" href="../gr-included-in-dialog/gr-included-in-dialog.html">
-<link rel="import" href="../gr-messages-list/gr-messages-list.html">
-<link rel="import" href="../gr-related-changes-list/gr-related-changes-list.html">
-<link rel="import" href="../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html">
-<link rel="import" href="../gr-reply-dialog/gr-reply-dialog.html">
-<link rel="import" href="../gr-thread-list/gr-thread-list.html">
-<link rel="import" href="../gr-upload-help-dialog/gr-upload-help-dialog.html">
-
-<dom-module id="gr-change-view">
- <template>
- <style include="shared-styles">
- .container:not(.loading) {
- background-color: var(--background-color-tertiary);
- }
- .container.loading {
- color: var(--deemphasized-text-color);
- padding: var(--spacing-l);
- }
- .header {
- align-items: center;
- background-color: var(--background-color-primary);
- border-bottom: 1px solid var(--border-color);
- display: flex;
- padding: var(--spacing-s) var(--spacing-l);
- z-index: 99; /* Less than gr-overlay's backdrop */
- }
- .header.editMode {
- background-color: var(--edit-mode-background-color);
- }
- .header .download {
- margin-right: var(--spacing-l);
- }
- gr-change-status {
- display: initial;
- margin-left: var(--spacing-s);
- }
- gr-change-status:first-child {
- margin-left: 0;
- }
- .headerTitle {
- align-items: center;
- display: flex;
- flex: 1;
- }
- .headerSubject {
- font-family: var(--header-font-family);
- font-size: var(--font-size-h3);
- font-weight: var(--font-weight-h3);
- line-height: var(--line-height-h3);
- margin-left: var(--spacing-l);
- }
- .changeNumberColon {
- color: transparent;
- }
- .changeCopyClipboard {
- margin-left: var(--spacing-s);
- }
- #replyBtn {
- margin-bottom: var(--spacing-l);
- }
- gr-change-star {
- margin-right: var(--spacing-xs);
- margin-left: var(--spacing-s);
- --gr-change-star-size: var(--line-height-normal);
- }
- gr-reply-dialog {
- width: 60em;
- }
- .changeStatus {
- text-transform: capitalize;
- }
- /* Strong specificity here is needed due to
- https://github.com/Polymer/polymer/issues/2531 */
- .container .changeInfo {
- display: flex;
- background-color: var(--background-color-secondary);
- }
- section {
- background-color: var(--view-background-color);
- box-shadow: var(--elevation-level-1);
- }
- .changeId {
- color: var(--deemphasized-text-color);
- font-family: var(--font-family);
- margin-top: var(--spacing-l);
- }
- .changeMetadata {
- /* Limit meta section to half of the screen at max */
- max-width: 50%;
- }
- .commitMessage {
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-mono);
- line-height: var(--line-height-mono);
- margin-right: var(--spacing-l);
- margin-bottom: var(--spacing-l);
- /* Account for border and padding and rounding errors. */
- max-width: calc(72ch + 2px + 2*var(--spacing-m) + 0.4px);
- }
- .commitMessage gr-linked-text {
- word-break: break-word;
- }
- #commitMessageEditor {
- /* Account for border and padding and rounding errors. */
- min-width: calc(72ch + 2px + 2*var(--spacing-m) + 0.4px);
- }
- .editCommitMessage {
- margin-top: var(--spacing-l);
-
- --gr-button: {
- padding: 5px 0px;
- }
- }
- .changeStatuses,
- .commitActions,
- .statusText {
- align-items: center;
- display: flex;
- }
- .changeStatuses {
- flex-wrap: wrap;
- }
- .mainChangeInfo {
- display: flex;
- flex: 1;
- flex-direction: column;
- min-width: 0;
- }
- #commitAndRelated {
- align-content: flex-start;
- display: flex;
- flex: 1;
- overflow-x: hidden;
- }
- .relatedChanges {
- flex: 1 1 auto;
- overflow: hidden;
- padding: var(--spacing-l) 0;
- }
- .mobile {
- display: none;
- }
- .warning {
- color: var(--error-text-color);
- }
- hr {
- border: 0;
- border-top: 1px solid var(--border-color);
- height: 0;
- margin-bottom: var(--spacing-l);
- }
- #relatedChanges.collapsed {
- margin-bottom: var(--spacing-l);
- max-height: var(--relation-chain-max-height, 2em);
- overflow: hidden;
- }
- .commitContainer {
- display: flex;
- flex-direction: column;
- flex-shrink: 0;
- margin: var(--spacing-l) 0;
- padding: 0 var(--spacing-l);
- }
- .collapseToggleContainer {
- display: flex;
- margin-bottom: 8px;
- }
- #relatedChangesToggle {
- display: none;
- }
- #relatedChangesToggle.showToggle {
- display: flex;
- }
- .collapseToggleContainer gr-button {
- display: block;
- }
- #relatedChangesToggle {
- margin-left: var(--spacing-l);
- padding-top: var(--related-change-btn-top-padding, 0);
- }
- .showOnEdit {
- display: none;
- }
- .scrollable {
- overflow: auto;
- }
- .text {
- white-space: pre;
- }
- gr-commit-info {
- display: inline-block;
- }
- paper-tabs {
- background-color: var(--background-color-tertiary);
- margin-top: var(--spacing-m);
- height: calc(var(--line-height-h3) + var(--spacing-m));
- --paper-tabs-selection-bar-color: var(--link-color);
- }
- paper-tab {
- box-sizing: border-box;
- max-width: 12em;
- --paper-tab-ink: var(--link-color);
- }
- gr-thread-list,
- gr-messages-list {
- display: block;
- }
- gr-thread-list {
- min-height: 250px;
- }
- #includedInOverlay {
- width: 65em;
- }
- #uploadHelpOverlay {
- width: 50em;
- }
- #metadata {
- --metadata-horizontal-padding: var(--spacing-l);
- padding-top: var(--spacing-l);
- width: 100%;
- }
- /* NOTE: If you update this breakpoint, also update the
- BREAKPOINT_RELATED_MED in the JS */
- @media screen and (max-width: 75em) {
- .relatedChanges {
- padding: 0;
- }
- #relatedChanges {
- padding-top: var(--spacing-l);
- }
- #commitAndRelated {
- flex-direction: column;
- flex-wrap: nowrap;
- }
- #commitMessageEditor {
- min-width: 0;
- }
- .commitMessage {
- margin-right: 0;
- }
- .mainChangeInfo {
- padding-right: 0;
- }
- }
- /* NOTE: If you update this breakpoint, also update the
- BREAKPOINT_RELATED_SMALL in the JS */
- @media screen and (max-width: 50em) {
- .mobile {
- display: block;
- }
- .header {
- align-items: flex-start;
- flex-direction: column;
- flex: 1;
- padding: var(--spacing-s) var(--spacing-l);
- }
- gr-change-star {
- vertical-align: middle;
- }
- .headerTitle {
- flex-wrap: wrap;
- font-family: var(--header-font-family);
- font-size: var(--font-size-h3);
- font-weight: var(--font-weight-h3);
- line-height: var(--line-height-h3);
- }
- .desktop {
- display: none;
- }
- .reply {
- display: block;
- margin-right: 0;
- /* px because don't have the same font size */
- margin-bottom: 6px;
- }
- .changeInfo-column:not(:last-of-type) {
- margin-right: 0;
- padding-right: 0;
- }
- .changeInfo,
- #commitAndRelated {
- flex-direction: column;
- flex-wrap: nowrap;
- }
- .commitContainer {
- margin: 0;
- padding: var(--spacing-l);
- }
- .changeMetadata {
- margin-top: var(--spacing-xs);
- max-width: none;
- }
- #metadata,
- .mainChangeInfo {
- padding: 0;
- }
- .commitActions {
- display: block;
- margin-top: var(--spacing-l);
- width: 100%;
- }
- .commitMessage {
- flex: initial;
- margin: 0;
- }
- /* Change actions are the only thing thant need to remain visible due
- to the fact that they may have the currently visible overlay open. */
- #mainContent.overlayOpen .hideOnMobileOverlay {
- display: none;
- }
- gr-reply-dialog {
- height: 100vh;
- min-width: initial;
- width: 100vw;
- }
- #replyOverlay {
- z-index: var(--reply-overlay-z-index);
- }
- }
- .patch-set-dropdown {
- margin: var(--spacing-m) 0 0 var(--spacing-m);
- }
- .show-robot-comments {
- margin: var(--spacing-m);
- }
- </style>
- <div class="container loading" hidden$="[[!_loading]]">Loading...</div>
- <div
- id="mainContent"
- class="container"
- on-show-checks-table="_handleShowTab"
- hidden$="{{_loading}}">
- <section class="changeInfoSection">
- <div class$="[[_computeHeaderClass(_editMode)]]">
- <div class="headerTitle">
- <div class="changeStatuses">
- <template is="dom-repeat" items="[[_changeStatuses]]" as="status">
- <gr-change-status
- max-width="100"
- status="[[status]]"></gr-change-status>
- </template>
- </div>
- <div class="statusText">
- <template
- is="dom-if"
- if="[[_computeShowCommitInfo(_changeStatus, _change.current_revision)]]">
- <span class="text"> as </span>
- <gr-commit-info
- change="[[_change]]"
- commit-info="[[_computeMergedCommitInfo(_change.current_revision, _change.revisions)]]"
- server-config="[[_serverConfig]]"></gr-commit-info>
- </template>
- </div>
- <gr-change-star
- id="changeStar"
- change="{{_change}}"
- on-toggle-star="_handleToggleStar"
- hidden$="[[!_loggedIn]]"></gr-change-star>
-
- <a aria-label$="[[_computeChangePermalinkAriaLabel(_change._number)]]"
- href$="[[_computeChangeUrl(_change)]]">[[_change._number]]</a>
- <span class="changeNumberColon">: </span>
- <span class="headerSubject">[[_change.subject]]</span>
- <gr-copy-clipboard
- class="changeCopyClipboard"
- hide-input
- text="[[_computeCopyTextForTitle(_change)]]">
- </gr-copy-clipboard>
- </div><!-- end headerTitle -->
- <div class="commitActions" hidden$="[[!_loggedIn]]">
- <gr-change-actions
- id="actions"
- change="[[_change]]"
- disable-edit="[[disableEdit]]"
- has-parent="[[hasParent]]"
- actions="[[_change.actions]]"
- revision-actions="{{_currentRevisionActions}}"
- change-num="[[_changeNum]]"
- change-status="[[_change.status]]"
- commit-num="[[_commitInfo.commit]]"
- latest-patch-num="[[computeLatestPatchNum(_allPatchSets)]]"
- commit-message="[[_latestCommitMessage]]"
- edit-patchset-loaded="[[hasEditPatchsetLoaded(_patchRange.*)]]"
- edit-mode="[[_editMode]]"
- edit-based-on-current-patch-set="[[hasEditBasedOnCurrentPatchSet(_allPatchSets)]]"
- private-by-default="[[_projectConfig.private_by_default]]"
- on-reload-change="_handleReloadChange"
- on-edit-tap="_handleEditTap"
- on-stop-edit-tap="_handleStopEditTap"
- on-download-tap="_handleOpenDownloadDialog"></gr-change-actions>
- </div><!-- end commit actions -->
- </div><!-- end header -->
- <div class="changeInfo">
- <div class="changeInfo-column changeMetadata hideOnMobileOverlay">
- <gr-change-metadata
- id="metadata"
- change="{{_change}}"
- account="[[_account]]"
- revision="[[_selectedRevision]]"
- commit-info="[[_commitInfo]]"
- server-config="[[_serverConfig]]"
- parent-is-current="[[_parentIsCurrent]]"
- on-show-reply-dialog="_handleShowReplyDialog">
- </gr-change-metadata>
- </div>
- <div id="mainChangeInfo" class="changeInfo-column mainChangeInfo">
- <div id="commitAndRelated" class="hideOnMobileOverlay">
- <div class="commitContainer">
- <div>
- <gr-button
- id="replyBtn"
- class="reply"
- title="[[createTitle(Shortcut.OPEN_REPLY_DIALOG,
- ShortcutSection.ACTIONS)]]"
- hidden$="[[!_loggedIn]]"
- primary
- disabled="[[_replyDisabled]]"
- on-click="_handleReplyTap">[[_replyButtonLabel]]</gr-button>
- </div>
- <div
- id="commitMessage"
- class="commitMessage">
- <gr-editable-content id="commitMessageEditor"
- editing="[[_editingCommitMessage]]"
- content="{{_latestCommitMessage}}"
- storage-key="[[_computeCommitMessageKey(_change._number, _change.current_revision)]]"
- remove-zero-width-space
- collapsed$="[[_computeCommitMessageCollapsed(_commitCollapsed, _commitCollapsible)]]">
- <gr-linked-text pre
- content="[[_latestCommitMessage]]"
- config="[[_projectConfig.commentlinks]]"
- remove-zero-width-space></gr-linked-text>
- </gr-editable-content>
- <gr-button link
- class="editCommitMessage"
- on-click="_handleEditCommitMessage"
- hidden$="[[_hideEditCommitMessage]]">Edit</gr-button>
- <div class="changeId" hidden$="[[!_changeIdCommitMessageError]]">
- <hr>
- Change-Id:
- <span
- class$="[[_computeChangeIdClass(_changeIdCommitMessageError)]]"
- title$="[[_computeTitleAttributeWarning(_changeIdCommitMessageError)]]">
- [[_change.change_id]]
- </span>
- </div>
- </div>
- <div
- id="commitCollapseToggle"
- class="collapseToggleContainer"
- hidden$="[[!_commitCollapsible]]">
- <gr-button
- link
- id="commitCollapseToggleButton"
- class="collapseToggleButton"
- on-click="_toggleCommitCollapsed">
- [[_computeCollapseText(_commitCollapsed)]]
- </gr-button>
- </div>
- <gr-endpoint-decorator name="commit-container">
- <gr-endpoint-param name="change" value="[[_change]]">
- </gr-endpoint-param>
- <gr-endpoint-param name="revision" value="[[_selectedRevision]]">
- </gr-endpoint-param>
- </gr-endpoint-decorator>
- </div>
- <div class="relatedChanges">
- <gr-related-changes-list id="relatedChanges"
- class$="[[_computeRelatedChangesClass(_relatedChangesCollapsed)]]"
- change="[[_change]]"
- mergeable="[[_mergeable]]"
- has-parent="{{hasParent}}"
- on-update="_updateRelatedChangeMaxHeight"
- patch-num="[[computeLatestPatchNum(_allPatchSets)]]"
- on-new-section-loaded="_computeShowRelatedToggle">
- </gr-related-changes-list>
- <div
- id="relatedChangesToggle"
- class="collapseToggleContainer">
- <gr-button
- link
- id="relatedChangesToggleButton"
- class="collapseToggleButton"
- on-click="_toggleRelatedChangesCollapsed">
- [[_computeCollapseText(_relatedChangesCollapsed)]]
- </gr-button>
- </div>
- </div>
- </div>
- </div>
- </div>
- </section>
-
- <paper-tabs id="primaryTabs" on-selected-changed="_handleFileTabChange">
- <paper-tab data-name$="[[_files_tab_name]]">Files</paper-tab>
- <template is="dom-repeat" items="[[_dynamicTabHeaderEndpoints]]"
- as="tabHeader">
- <paper-tab data-name$="[[tabHeader]]">
- <gr-endpoint-decorator name$="[[tabHeader]]">
- <gr-endpoint-param name="change" value="[[_change]]">
- </gr-endpoint-param>
- <gr-endpoint-param name="revision" value="[[_selectedRevision]]">
- </gr-endpoint-param>
- </gr-endpoint-decorator>
- </paper-tab>
- </template>
- <paper-tab data-name$="[[_findings_tab_name]]">
- Findings
- </paper-tab>
- </paper-tabs>
-
- <section class="patchInfo">
- <div hidden$="[[!_findIfTabMatches(_currentTabName, _files_tab_name)]]">
- <gr-file-list-header
- id="fileListHeader"
- account="[[_account]]"
- all-patch-sets="[[_allPatchSets]]"
- change="[[_change]]"
- change-num="[[_changeNum]]"
- revision-info="[[_revisionInfo]]"
- change-comments="[[_changeComments]]"
- commit-info="[[_commitInfo]]"
- change-url="[[_computeChangeUrl(_change)]]"
- edit-mode="[[_editMode]]"
- logged-in="[[_loggedIn]]"
- server-config="[[_serverConfig]]"
- shown-file-count="[[_shownFileCount]]"
- diff-prefs="[[_diffPrefs]]"
- diff-view-mode="{{viewState.diffMode}}"
- patch-num="{{_patchRange.patchNum}}"
- base-patch-num="{{_patchRange.basePatchNum}}"
- files-expanded="[[_filesExpanded]]"
- diff-prefs-disabled="[[_diffPrefsDisabled]]"
- on-open-diff-prefs="_handleOpenDiffPrefs"
- on-open-download-dialog="_handleOpenDownloadDialog"
- on-open-upload-help-dialog="_handleOpenUploadHelpDialog"
- on-open-included-in-dialog="_handleOpenIncludedInDialog"
- on-expand-diffs="_expandAllDiffs"
- on-collapse-diffs="_collapseAllDiffs">
- </gr-file-list-header>
- <gr-file-list
- id="fileList"
- class="hideOnMobileOverlay"
- diff-prefs="{{_diffPrefs}}"
- change="[[_change]]"
- change-num="[[_changeNum]]"
- patch-range="{{_patchRange}}"
- change-comments="[[_changeComments]]"
- drafts="[[_diffDrafts]]"
- revisions="[[_change.revisions]]"
- project-config="[[_projectConfig]]"
- selected-index="{{viewState.selectedFileIndex}}"
- diff-view-mode="[[viewState.diffMode]]"
- edit-mode="[[_editMode]]"
- num-files-shown="{{_numFilesShown}}"
- files-expanded="{{_filesExpanded}}"
- file-list-increment="{{_numFilesShown}}"
- on-files-shown-changed="_setShownFiles"
- on-file-action-tap="_handleFileActionTap"
- on-reload-drafts="_reloadDraftsWithCallback">
- </gr-file-list>
- </div>
-
- <template is="dom-if" if="[[_findIfTabMatches(_currentTabName, _findings_tab_name)]]">
- <gr-dropdown-list
- class="patch-set-dropdown"
- items="[[_robotCommentsPatchSetDropdownItems]]"
- on-value-change="_handleRobotCommentPatchSetChanged"
- value="[[_currentRobotCommentsPatchSet]]">
- </gr-dropdown-list>
- <gr-thread-list
- threads="[[_robotCommentThreads]]"
- change="[[_change]]"
- change-num="[[_changeNum]]"
- logged-in="[[_loggedIn]]"
- tab="[[_findings_tab_name]]"
- hide-toggle-buttons
- on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list>
- <template is="dom-if" if="[[_showRobotCommentsButton]]">
- <gr-button class="show-robot-comments" on-click="_toggleShowRobotComments">
- [[_computeShowText(_showAllRobotComments)]]
- </gr-button>
- </template>
- </template>
-
- <template is="dom-if" if="[[_findIfTabMatches(_currentTabName, _selectedTabPluginHeader)]]">
- <gr-endpoint-decorator name$="[[_selectedTabPluginEndpoint]]">
- <gr-endpoint-param name="change" value="[[_change]]">
- </gr-endpoint-param>
- <gr-endpoint-param name="revision" value="[[_selectedRevision]]">
- </gr-endpoint-param>
- </gr-endpoint-decorator>
- </template>
- </section>
-
- <gr-endpoint-decorator name="change-view-integration">
- <gr-endpoint-param name="change" value="[[_change]]">
- </gr-endpoint-param>
- <gr-endpoint-param name="revision" value="[[_selectedRevision]]">
- </gr-endpoint-param>
- </gr-endpoint-decorator>
-
- <paper-tabs
- id="commentTabs"
- on-selected-changed="_handleCommentTabChange">
- <paper-tab class="changeLog">Change Log</paper-tab>
- <paper-tab
- class="commentThreads">
- <gr-tooltip-content
- has-tooltip
- title$="[[_computeTotalCommentCounts(_change.unresolved_comment_count, _changeComments)]]">
- <span>Comment Threads</span></gr-tooltip-content>
- </paper-tab>
- </paper-tabs>
- <section class="changeLog">
- <template is="dom-if" if="[[_isSelectedView(_currentView,
- _commentTabs.CHANGE_LOG)]]">
- <gr-messages-list
- class="hideOnMobileOverlay"
- change-num="[[_changeNum]]"
- labels="[[_change.labels]]"
- messages="[[_change.messages]]"
- reviewer-updates="[[_change.reviewer_updates]]"
- change-comments="[[_changeComments]]"
- project-name="[[_change.project]]"
- show-reply-buttons="[[_loggedIn]]"
- on-message-anchor-tap="_handleMessageAnchorTap"
- on-reply="_handleMessageReply"></gr-messages-list>
- </template>
- <template is="dom-if" if="[[_isSelectedView(_currentView,
- _commentTabs.COMMENT_THREADS)]]">
- <gr-thread-list
- threads="[[_commentThreads]]"
- change="[[_change]]"
- change-num="[[_changeNum]]"
- logged-in="[[_loggedIn]]"
- only-show-robot-comments-with-human-reply
- on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list>
- </template>
- </section>
- </div>
-
- <gr-apply-fix-dialog
- id="applyFixDialog"
- prefs="[[_diffPrefs]]"
- change="[[_change]]"
- change-num="[[_changeNum]]"></gr-apply-fix-dialog>
- <gr-overlay id="downloadOverlay" with-backdrop>
- <gr-download-dialog
- id="downloadDialog"
- change="[[_change]]"
- patch-num="[[_patchRange.patchNum]]"
- config="[[_serverConfig.download]]"
- on-close="_handleDownloadDialogClose"></gr-download-dialog>
- </gr-overlay>
- <gr-overlay id="uploadHelpOverlay" with-backdrop>
- <gr-upload-help-dialog
- revision="[[_currentRevision]]"
- target-branch="[[_change.branch]]"
- on-close="_handleCloseUploadHelpDialog"></gr-upload-help-dialog>
- </gr-overlay>
- <gr-overlay id="includedInOverlay" with-backdrop>
- <gr-included-in-dialog
- id="includedInDialog"
- change-num="[[_changeNum]]"
- on-close="_handleIncludedInDialogClose"></gr-included-in-dialog>
- </gr-overlay>
- <gr-overlay id="replyOverlay"
- class="scrollable"
- no-cancel-on-outside-click
- no-cancel-on-esc-key
- with-backdrop>
- <gr-reply-dialog id="replyDialog"
- change="{{_change}}"
- patch-num="[[computeLatestPatchNum(_allPatchSets)]]"
- permitted-labels="[[_change.permitted_labels]]"
- draft-comment-threads="[[_draftCommentThreads]]"
- project-config="[[_projectConfig]]"
- can-be-started="[[_canStartReview]]"
- on-send="_handleReplySent"
- on-cancel="_handleReplyCancel"
- on-autogrow="_handleReplyAutogrow"
- on-send-disabled-changed="_resetReplyOverlayFocusStops"
- hidden$="[[!_loggedIn]]">
- </gr-reply-dialog>
- </gr-overlay>
- <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- <gr-comment-api id="commentAPI"></gr-comment-api>
- <gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="gr-change-view.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 58505ff..d62ea04 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,2064 +14,2122 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const CHANGE_ID_ERROR = {
- MISMATCH: 'mismatch',
- MISSING: 'missing',
- };
- const CHANGE_ID_REGEX_PATTERN = /^Change-Id\:\s(I[0-9a-f]{8,40})/gm;
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '@polymer/paper-tabs/paper-tabs.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../diff/gr-comment-api/gr-comment-api.js';
+import '../../edit/gr-edit-constants.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
+import '../../shared/gr-account-link/gr-account-link.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-change-star/gr-change-star.js';
+import '../../shared/gr-change-status/gr-change-status.js';
+import '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-editable-content/gr-editable-content.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import '../../shared/gr-linked-text/gr-linked-text.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-tooltip-content/gr-tooltip-content.js';
+import '../../shared/revision-info/revision-info.js';
+import '../gr-change-actions/gr-change-actions.js';
+import '../gr-change-metadata/gr-change-metadata.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../gr-commit-info/gr-commit-info.js';
+import '../gr-download-dialog/gr-download-dialog.js';
+import '../gr-file-list-header/gr-file-list-header.js';
+import '../gr-file-list/gr-file-list.js';
+import '../gr-included-in-dialog/gr-included-in-dialog.js';
+import '../gr-messages-list/gr-messages-list.js';
+import '../gr-related-changes-list/gr-related-changes-list.js';
+import '../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js';
+import '../gr-reply-dialog/gr-reply-dialog.js';
+import '../gr-thread-list/gr-thread-list.js';
+import '../gr-upload-help-dialog/gr-upload-help-dialog.js';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {beforeNextRender} from '@polymer/polymer/lib/utils/render-status.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-change-view_html.js';
- const MIN_LINES_FOR_COMMIT_COLLAPSE = 30;
- const DEFAULT_NUM_FILES_SHOWN = 200;
+const CHANGE_ID_ERROR = {
+ MISMATCH: 'mismatch',
+ MISSING: 'missing',
+};
+const CHANGE_ID_REGEX_PATTERN = /^Change-Id\:\s(I[0-9a-f]{8,40})/gm;
- const REVIEWERS_REGEX = /^(R|CC)=/gm;
- const MIN_CHECK_INTERVAL_SECS = 0;
+const MIN_LINES_FOR_COMMIT_COLLAPSE = 30;
+const DEFAULT_NUM_FILES_SHOWN = 200;
- // These are the same as the breakpoint set in CSS. Make sure both are changed
- // together.
- const BREAKPOINT_RELATED_SMALL = '50em';
- const BREAKPOINT_RELATED_MED = '75em';
+const REVIEWERS_REGEX = /^(R|CC)=/gm;
+const MIN_CHECK_INTERVAL_SECS = 0;
- // In the event that the related changes medium width calculation is too close
- // to zero, provide some height.
- const MINIMUM_RELATED_MAX_HEIGHT = 100;
+// These are the same as the breakpoint set in CSS. Make sure both are changed
+// together.
+const BREAKPOINT_RELATED_SMALL = '50em';
+const BREAKPOINT_RELATED_MED = '75em';
- const SMALL_RELATED_HEIGHT = 400;
+// In the event that the related changes medium width calculation is too close
+// to zero, provide some height.
+const MINIMUM_RELATED_MAX_HEIGHT = 100;
- const REPLY_REFIT_DEBOUNCE_INTERVAL_MS = 500;
+const SMALL_RELATED_HEIGHT = 400;
- const TRAILING_WHITESPACE_REGEX = /[ \t]+$/gm;
+const REPLY_REFIT_DEBOUNCE_INTERVAL_MS = 500;
- const MSG_PREFIX = '#message-';
+const TRAILING_WHITESPACE_REGEX = /[ \t]+$/gm;
- const ReloadToastMessage = {
- NEWER_REVISION: 'A newer patch set has been uploaded',
- RESTORED: 'This change has been restored',
- ABANDONED: 'This change has been abandoned',
- MERGED: 'This change has been merged',
- NEW_MESSAGE: 'There are new messages on this change',
- };
+const MSG_PREFIX = '#message-';
- const DiffViewMode = {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- };
+const ReloadToastMessage = {
+ NEWER_REVISION: 'A newer patch set has been uploaded',
+ RESTORED: 'This change has been restored',
+ ABANDONED: 'This change has been abandoned',
+ MERGED: 'This change has been merged',
+ NEW_MESSAGE: 'There are new messages on this change',
+};
- const CommentTabs = {
- CHANGE_LOG: 0,
- COMMENT_THREADS: 1,
- ROBOT_COMMENTS: 2,
- };
+const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+};
- const CHANGE_DATA_TIMING_LABEL = 'ChangeDataLoaded';
- const CHANGE_RELOAD_TIMING_LABEL = 'ChangeReloaded';
- const SEND_REPLY_TIMING_LABEL = 'SendReply';
- // Making the tab names more unique in case a plugin adds one with same name
- const FILES_TAB_NAME = '__gerrit_internal_files';
- const FINDINGS_TAB_NAME = '__gerrit_internal_findings';
- const ROBOT_COMMENTS_LIMIT = 10;
+const CommentTabs = {
+ CHANGE_LOG: 0,
+ COMMENT_THREADS: 1,
+ ROBOT_COMMENTS: 2,
+};
+
+const CHANGE_DATA_TIMING_LABEL = 'ChangeDataLoaded';
+const CHANGE_RELOAD_TIMING_LABEL = 'ChangeReloaded';
+const SEND_REPLY_TIMING_LABEL = 'SendReply';
+// Making the tab names more unique in case a plugin adds one with same name
+const FILES_TAB_NAME = '__gerrit_internal_files';
+const FINDINGS_TAB_NAME = '__gerrit_internal_findings';
+const ROBOT_COMMENTS_LIMIT = 10;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrChangeView extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-change-view'; }
+ /**
+ * Fired when the title of the page should change.
+ *
+ * @event title-change
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * Fired if an error occurs when fetching the change data.
+ *
+ * @event page-error
*/
- class GrChangeView extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-change-view'; }
+
+ /**
+ * Fired if being logged in is required.
+ *
+ * @event show-auth-required
+ */
+
+ static get properties() {
+ return {
/**
- * Fired when the title of the page should change.
- *
- * @event title-change
+ * URL params passed from the router.
*/
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
+ /** @type {?} */
+ viewState: {
+ type: Object,
+ notify: true,
+ value() { return {}; },
+ observer: '_viewStateChanged',
+ },
+ backPage: String,
+ hasParent: Boolean,
+ keyEventTarget: {
+ type: Object,
+ value() { return document.body; },
+ },
+ disableEdit: {
+ type: Boolean,
+ value: false,
+ },
+ disableDiffPrefs: {
+ type: Boolean,
+ value: false,
+ },
+ _diffPrefsDisabled: {
+ type: Boolean,
+ computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)',
+ },
+ _commentThreads: Array,
+ // TODO(taoalpha): Consider replacing diffDrafts
+ // with _draftCommentThreads everywhere, currently only
+ // replaced in reply-dialoig
+ _draftCommentThreads: {
+ type: Array,
+ },
+ _robotCommentThreads: {
+ type: Array,
+ computed: '_computeRobotCommentThreads(_commentThreads,'
+ + ' _currentRobotCommentsPatchSet, _showAllRobotComments)',
+ },
+ /** @type {?} */
+ _serverConfig: {
+ type: Object,
+ observer: '_startUpdateCheckTimer',
+ },
+ _diffPrefs: Object,
+ _numFilesShown: {
+ type: Number,
+ value: DEFAULT_NUM_FILES_SHOWN,
+ observer: '_numFilesShownChanged',
+ },
+ _account: {
+ type: Object,
+ value: {},
+ },
+ _prefs: Object,
+ /** @type {?} */
+ _changeComments: Object,
+ _canStartReview: {
+ type: Boolean,
+ computed: '_computeCanStartReview(_change)',
+ },
+ _comments: Object,
+ /** @type {?} */
+ _change: {
+ type: Object,
+ observer: '_changeChanged',
+ },
+ _revisionInfo: {
+ type: Object,
+ computed: '_getRevisionInfo(_change)',
+ },
+ /** @type {?} */
+ _commitInfo: Object,
+ _currentRevision: {
+ type: Object,
+ computed: '_computeCurrentRevision(_change.current_revision, ' +
+ '_change.revisions)',
+ observer: '_handleCurrentRevisionUpdate',
+ },
+ _files: Object,
+ _changeNum: String,
+ _diffDrafts: {
+ type: Object,
+ value() { return {}; },
+ },
+ _editingCommitMessage: {
+ type: Boolean,
+ value: false,
+ },
+ _hideEditCommitMessage: {
+ type: Boolean,
+ computed: '_computeHideEditCommitMessage(_loggedIn, ' +
+ '_editingCommitMessage, _change, _editMode, _commitCollapsed, ' +
+ '_commitCollapsible)',
+ },
+ _diffAgainst: String,
+ /** @type {?string} */
+ _latestCommitMessage: {
+ type: String,
+ value: '',
+ },
+ _commentTabs: {
+ type: Object,
+ value: CommentTabs,
+ },
+ _lineHeight: Number,
+ _changeIdCommitMessageError: {
+ type: String,
+ computed:
+ '_computeChangeIdCommitMessageError(_latestCommitMessage, _change)',
+ },
+ /** @type {?} */
+ _patchRange: {
+ type: Object,
+ },
+ _filesExpanded: String,
+ _basePatchNum: String,
+ _selectedRevision: Object,
+ _currentRevisionActions: Object,
+ _allPatchSets: {
+ type: Array,
+ computed: 'computeAllPatchSets(_change, _change.revisions.*)',
+ },
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+ _loading: Boolean,
+ /** @type {?} */
+ _projectConfig: Object,
+ _replyButtonLabel: {
+ type: String,
+ value: 'Reply',
+ computed: '_computeReplyButtonLabel(_diffDrafts.*, _canStartReview)',
+ },
+ _selectedPatchSet: String,
+ _shownFileCount: Number,
+ _initialLoadComplete: {
+ type: Boolean,
+ value: false,
+ },
+ _replyDisabled: {
+ type: Boolean,
+ value: true,
+ computed: '_computeReplyDisabled(_serverConfig)',
+ },
+ _changeStatus: {
+ type: String,
+ computed: 'changeStatusString(_change)',
+ },
+ _changeStatuses: {
+ type: String,
+ computed:
+ '_computeChangeStatusChips(_change, _mergeable, _submitEnabled)',
+ },
+ /** If false, then the "Show more" button was used to expand. */
+ _commitCollapsed: {
+ type: Boolean,
+ value: true,
+ },
+ /** Is the "Show more/less" button visible? */
+ _commitCollapsible: {
+ type: Boolean,
+ computed: '_computeCommitCollapsible(_latestCommitMessage)',
+ },
+ _relatedChangesCollapsed: {
+ type: Boolean,
+ value: true,
+ },
+ /** @type {?number} */
+ _updateCheckTimerHandle: Number,
+ _editMode: {
+ type: Boolean,
+ computed: '_computeEditMode(_patchRange.*, params.*)',
+ },
+ _showRelatedToggle: {
+ type: Boolean,
+ value: false,
+ observer: '_updateToggleContainerClass',
+ },
+ _parentIsCurrent: {
+ type: Boolean,
+ computed: '_isParentCurrent(_currentRevisionActions)',
+ },
+ _submitEnabled: {
+ type: Boolean,
+ computed: '_isSubmitEnabled(_currentRevisionActions)',
+ },
- /**
- * Fired if an error occurs when fetching the change data.
- *
- * @event page-error
- */
+ /** @type {?} */
+ _mergeable: {
+ type: Boolean,
+ value: undefined,
+ },
+ _currentView: {
+ type: Number,
+ value: CommentTabs.CHANGE_LOG,
+ },
+ _showFileTabContent: {
+ type: Boolean,
+ value: true,
+ },
+ /** @type {Array<string>} */
+ _dynamicTabHeaderEndpoints: {
+ type: Array,
+ },
+ /** @type {Array<string>} */
+ _dynamicTabContentEndpoints: {
+ type: Array,
+ },
+ // The dynamic content of the plugin added tab
+ _selectedTabPluginEndpoint: {
+ type: String,
+ },
+ // The dynamic heading of the plugin added tab
+ _selectedTabPluginHeader: {
+ type: String,
+ },
+ _robotCommentsPatchSetDropdownItems: {
+ type: Array,
+ value() { return []; },
+ computed: '_computeRobotCommentsPatchSetDropdownItems(_change, ' +
+ '_commentThreads)',
+ },
+ _currentRobotCommentsPatchSet: {
+ type: Number,
+ },
+ _files_tab_name: {
+ type: String,
+ value: FILES_TAB_NAME,
+ },
+ _findings_tab_name: {
+ type: String,
+ value: FINDINGS_TAB_NAME,
+ },
+ _currentTabName: {
+ type: String,
+ value: FILES_TAB_NAME,
+ },
+ _showAllRobotComments: {
+ type: Boolean,
+ value: false,
+ },
+ _showRobotCommentsButton: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
- /**
- * Fired if being logged in is required.
- *
- * @event show-auth-required
- */
+ static get observers() {
+ return [
+ '_labelsChanged(_change.labels.*)',
+ '_paramsAndChangeChanged(params, _change)',
+ '_patchNumChanged(_patchRange.patchNum)',
+ ];
+ }
- static get properties() {
- return {
- /**
- * URL params passed from the router.
- */
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
- /** @type {?} */
- viewState: {
- type: Object,
- notify: true,
- value() { return {}; },
- observer: '_viewStateChanged',
- },
- backPage: String,
- hasParent: Boolean,
- keyEventTarget: {
- type: Object,
- value() { return document.body; },
- },
- disableEdit: {
- type: Boolean,
- value: false,
- },
- disableDiffPrefs: {
- type: Boolean,
- value: false,
- },
- _diffPrefsDisabled: {
- type: Boolean,
- computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)',
- },
- _commentThreads: Array,
- // TODO(taoalpha): Consider replacing diffDrafts
- // with _draftCommentThreads everywhere, currently only
- // replaced in reply-dialoig
- _draftCommentThreads: {
- type: Array,
- },
- _robotCommentThreads: {
- type: Array,
- computed: '_computeRobotCommentThreads(_commentThreads,'
- + ' _currentRobotCommentsPatchSet, _showAllRobotComments)',
- },
- /** @type {?} */
- _serverConfig: {
- type: Object,
- observer: '_startUpdateCheckTimer',
- },
- _diffPrefs: Object,
- _numFilesShown: {
- type: Number,
- value: DEFAULT_NUM_FILES_SHOWN,
- observer: '_numFilesShownChanged',
- },
- _account: {
- type: Object,
- value: {},
- },
- _prefs: Object,
- /** @type {?} */
- _changeComments: Object,
- _canStartReview: {
- type: Boolean,
- computed: '_computeCanStartReview(_change)',
- },
- _comments: Object,
- /** @type {?} */
- _change: {
- type: Object,
- observer: '_changeChanged',
- },
- _revisionInfo: {
- type: Object,
- computed: '_getRevisionInfo(_change)',
- },
- /** @type {?} */
- _commitInfo: Object,
- _currentRevision: {
- type: Object,
- computed: '_computeCurrentRevision(_change.current_revision, ' +
- '_change.revisions)',
- observer: '_handleCurrentRevisionUpdate',
- },
- _files: Object,
- _changeNum: String,
- _diffDrafts: {
- type: Object,
- value() { return {}; },
- },
- _editingCommitMessage: {
- type: Boolean,
- value: false,
- },
- _hideEditCommitMessage: {
- type: Boolean,
- computed: '_computeHideEditCommitMessage(_loggedIn, ' +
- '_editingCommitMessage, _change, _editMode, _commitCollapsed, ' +
- '_commitCollapsible)',
- },
- _diffAgainst: String,
- /** @type {?string} */
- _latestCommitMessage: {
- type: String,
- value: '',
- },
- _commentTabs: {
- type: Object,
- value: CommentTabs,
- },
- _lineHeight: Number,
- _changeIdCommitMessageError: {
- type: String,
- computed:
- '_computeChangeIdCommitMessageError(_latestCommitMessage, _change)',
- },
- /** @type {?} */
- _patchRange: {
- type: Object,
- },
- _filesExpanded: String,
- _basePatchNum: String,
- _selectedRevision: Object,
- _currentRevisionActions: Object,
- _allPatchSets: {
- type: Array,
- computed: 'computeAllPatchSets(_change, _change.revisions.*)',
- },
- _loggedIn: {
- type: Boolean,
- value: false,
- },
- _loading: Boolean,
- /** @type {?} */
- _projectConfig: Object,
- _rebaseOnCurrent: Boolean,
- _replyButtonLabel: {
- type: String,
- value: 'Reply',
- computed: '_computeReplyButtonLabel(_diffDrafts.*, _canStartReview)',
- },
- _selectedPatchSet: String,
- _shownFileCount: Number,
- _initialLoadComplete: {
- type: Boolean,
- value: false,
- },
- _replyDisabled: {
- type: Boolean,
- value: true,
- computed: '_computeReplyDisabled(_serverConfig)',
- },
- _changeStatus: {
- type: String,
- computed: 'changeStatusString(_change)',
- },
- _changeStatuses: {
- type: String,
- computed:
- '_computeChangeStatusChips(_change, _mergeable, _submitEnabled)',
- },
- /** If false, then the "Show more" button was used to expand. */
- _commitCollapsed: {
- type: Boolean,
- value: true,
- },
- /** Is the "Show more/less" button visible? */
- _commitCollapsible: {
- type: Boolean,
- computed: '_computeCommitCollapsible(_latestCommitMessage)',
- },
- _relatedChangesCollapsed: {
- type: Boolean,
- value: true,
- },
- /** @type {?number} */
- _updateCheckTimerHandle: Number,
- _editMode: {
- type: Boolean,
- computed: '_computeEditMode(_patchRange.*, params.*)',
- },
- _showRelatedToggle: {
- type: Boolean,
- value: false,
- observer: '_updateToggleContainerClass',
- },
- _parentIsCurrent: Boolean,
- _submitEnabled: {
- type: Boolean,
- computed: '_isSubmitEnabled(_currentRevisionActions)',
- },
+ keyboardShortcuts() {
+ return {
+ [this.Shortcut.SEND_REPLY]: null, // DOC_ONLY binding
+ [this.Shortcut.EMOJI_DROPDOWN]: null, // DOC_ONLY binding
+ [this.Shortcut.REFRESH_CHANGE]: '_handleRefreshChange',
+ [this.Shortcut.OPEN_REPLY_DIALOG]: '_handleOpenReplyDialog',
+ [this.Shortcut.OPEN_DOWNLOAD_DIALOG]:
+ '_handleOpenDownloadDialogShortcut',
+ [this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
+ [this.Shortcut.TOGGLE_CHANGE_STAR]: '_handleToggleChangeStar',
+ [this.Shortcut.UP_TO_DASHBOARD]: '_handleUpToDashboard',
+ [this.Shortcut.EXPAND_ALL_MESSAGES]: '_handleExpandAllMessages',
+ [this.Shortcut.COLLAPSE_ALL_MESSAGES]: '_handleCollapseAllMessages',
+ [this.Shortcut.OPEN_DIFF_PREFS]: '_handleOpenDiffPrefsShortcut',
+ [this.Shortcut.EDIT_TOPIC]: '_handleEditTopic',
+ };
+ }
- /** @type {?} */
- _mergeable: {
- type: Boolean,
- value: undefined,
- },
- _currentView: {
- type: Number,
- value: CommentTabs.CHANGE_LOG,
- },
- _showFileTabContent: {
- type: Boolean,
- value: true,
- },
- /** @type {Array<string>} */
- _dynamicTabHeaderEndpoints: {
- type: Array,
- },
- /** @type {Array<string>} */
- _dynamicTabContentEndpoints: {
- type: Array,
- },
- // The dynamic content of the plugin added tab
- _selectedTabPluginEndpoint: {
- type: String,
- },
- // The dynamic heading of the plugin added tab
- _selectedTabPluginHeader: {
- type: String,
- },
- _robotCommentsPatchSetDropdownItems: {
- type: Array,
- value() { return []; },
- computed: '_computeRobotCommentsPatchSetDropdownItems(_change, ' +
- '_commentThreads)',
- },
- _currentRobotCommentsPatchSet: {
- type: Number,
- },
- _files_tab_name: {
- type: String,
- value: FILES_TAB_NAME,
- },
- _findings_tab_name: {
- type: String,
- value: FINDINGS_TAB_NAME,
- },
- _currentTabName: {
- type: String,
- value: FILES_TAB_NAME,
- },
- _showAllRobotComments: {
- type: Boolean,
- value: false,
- },
- _showRobotCommentsButton: {
- type: Boolean,
- value: false,
- },
- };
- }
+ /** @override */
+ created() {
+ super.created();
- static get observers() {
- return [
- '_labelsChanged(_change.labels.*)',
- '_paramsAndChangeChanged(params, _change)',
- '_patchNumChanged(_patchRange.patchNum)',
- ];
- }
+ this.addEventListener('topic-changed',
+ () => this._handleTopicChanged());
- keyboardShortcuts() {
- return {
- [this.Shortcut.SEND_REPLY]: null, // DOC_ONLY binding
- [this.Shortcut.EMOJI_DROPDOWN]: null, // DOC_ONLY binding
- [this.Shortcut.REFRESH_CHANGE]: '_handleRefreshChange',
- [this.Shortcut.OPEN_REPLY_DIALOG]: '_handleOpenReplyDialog',
- [this.Shortcut.OPEN_DOWNLOAD_DIALOG]:
- '_handleOpenDownloadDialogShortcut',
- [this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
- [this.Shortcut.TOGGLE_CHANGE_STAR]: '_handleToggleChangeStar',
- [this.Shortcut.UP_TO_DASHBOARD]: '_handleUpToDashboard',
- [this.Shortcut.EXPAND_ALL_MESSAGES]: '_handleExpandAllMessages',
- [this.Shortcut.COLLAPSE_ALL_MESSAGES]: '_handleCollapseAllMessages',
- [this.Shortcut.OPEN_DIFF_PREFS]: '_handleOpenDiffPrefsShortcut',
- [this.Shortcut.EDIT_TOPIC]: '_handleEditTopic',
- };
- }
-
- /** @override */
- created() {
- super.created();
-
- this.addEventListener('topic-changed',
- () => this._handleTopicChanged());
-
- this.addEventListener(
- // When an overlay is opened in a mobile viewport, the overlay has a full
- // screen view. When it has a full screen view, we do not want the
- // background to be scrollable. This will eliminate background scroll by
- // hiding most of the contents on the screen upon opening, and showing
- // again upon closing.
- 'fullscreen-overlay-opened',
- () => this._handleHideBackgroundContent());
-
- this.addEventListener('fullscreen-overlay-closed',
- () => this._handleShowBackgroundContent());
-
- this.addEventListener('diff-comments-modified',
- () => this._handleReloadCommentThreads());
- }
-
- /** @override */
- attached() {
- super.attached();
- this._getServerConfig().then(config => {
- this._serverConfig = config;
- });
-
- this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- if (loggedIn) {
- this.$.restAPI.getAccount().then(acct => {
- this._account = acct;
- });
- }
- this._setDiffViewMode();
- });
-
- Gerrit.awaitPluginsLoaded()
- .then(() => {
- this._dynamicTabHeaderEndpoints =
- Gerrit._endpoints.getDynamicEndpoints('change-view-tab-header');
- this._dynamicTabContentEndpoints =
- Gerrit._endpoints.getDynamicEndpoints('change-view-tab-content');
- if (this._dynamicTabContentEndpoints.length !==
- this._dynamicTabHeaderEndpoints.length) {
- console.warn('Different number of tab headers and tab content.');
- }
- })
- .then(() => this._setPrimaryTab());
-
- this.addEventListener('comment-save', this._handleCommentSave.bind(this));
- this.addEventListener('comment-refresh', this._reloadDrafts.bind(this));
- this.addEventListener('comment-discard',
- this._handleCommentDiscard.bind(this));
- this.addEventListener('change-message-deleted',
- () => this._reload());
- this.addEventListener('editable-content-save',
- this._handleCommitMessageSave.bind(this));
- this.addEventListener('editable-content-cancel',
- this._handleCommitMessageCancel.bind(this));
- this.addEventListener('open-fix-preview',
- this._onOpenFixPreview.bind(this));
- this.addEventListener('close-fix-preview',
- this._onCloseFixPreview.bind(this));
- this.listen(window, 'scroll', '_handleScroll');
- this.listen(document, 'visibilitychange', '_handleVisibilityChange');
- }
-
- /** @override */
- detached() {
- super.detached();
- this.unlisten(window, 'scroll', '_handleScroll');
- this.unlisten(document, 'visibilitychange', '_handleVisibilityChange');
-
- if (this._updateCheckTimerHandle) {
- this._cancelUpdateCheckTimer();
- }
- }
-
- get messagesList() {
- return this.shadowRoot.querySelector('gr-messages-list');
- }
-
- get threadList() {
- return this.shadowRoot.querySelector('gr-thread-list');
- }
-
- /**
- * @param {boolean=} opt_reset
- */
- _setDiffViewMode(opt_reset) {
- if (!opt_reset && this.viewState.diffViewMode) { return; }
-
- return this._getPreferences()
- .then( prefs => {
- if (!this.viewState.diffMode) {
- this.set('viewState.diffMode', prefs.default_diff_view);
- }
- })
- .then(() => {
- if (!this.viewState.diffMode) {
- this.set('viewState.diffMode', 'SIDE_BY_SIDE');
- }
- });
- }
-
- _onOpenFixPreview(e) {
- this.$.applyFixDialog.open(e);
- }
-
- _onCloseFixPreview(e) {
- this._reload();
- }
-
- _handleToggleDiffMode(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- if (this.viewState.diffMode === DiffViewMode.SIDE_BY_SIDE) {
- this.$.fileListHeader.setDiffViewMode(DiffViewMode.UNIFIED);
- } else {
- this.$.fileListHeader.setDiffViewMode(DiffViewMode.SIDE_BY_SIDE);
- }
- }
-
- _handleCommentTabChange() {
- this._currentView = this.$.commentTabs.selected;
- const type = Object.keys(CommentTabs).find(key => CommentTabs[key] ===
- this._currentView);
- this.$.reporting.reportInteraction('comment-tab-changed', {tabName:
- type});
- }
-
- _isSelectedView(currentView, view) {
- return currentView === view;
- }
-
- _findIfTabMatches(currentTab, tab) {
- return currentTab === tab;
- }
-
- _handleFileTabChange(e) {
- const selectedIndex = e.target.selected;
- const tabs = e.target.querySelectorAll('paper-tab');
- this._currentTabName = tabs[selectedIndex] &&
- tabs[selectedIndex].dataset.name;
- const source = e && e.type ? e.type : '';
- const pluginIndex = (this._dynamicTabHeaderEndpoints || []).indexOf(
- this._currentTabName);
- if (pluginIndex !== -1) {
- this._selectedTabPluginEndpoint = this._dynamicTabContentEndpoints[
- pluginIndex];
- this._selectedTabPluginHeader = this._dynamicTabHeaderEndpoints[
- pluginIndex];
- } else {
- this._selectedTabPluginEndpoint = '';
- this._selectedTabPluginHeader = '';
- }
- this.$.reporting.reportInteraction('tab-changed',
- {tabName: this._currentTabName, source});
- }
-
- _handleShowTab(e) {
- const primaryTabs = this.shadowRoot.querySelector('#primaryTabs');
- const tabs = primaryTabs.querySelectorAll('paper-tab');
- let idx = -1;
- tabs.forEach((tab, index) => {
- if (tab.dataset.name === e.detail.tab) idx = index;
- });
- if (idx === -1) {
- console.error(e.detail.tab + ' tab not found');
- return;
- }
- primaryTabs.selected = idx;
- primaryTabs.scrollIntoView();
- this.$.reporting.reportInteraction('show-tab', {tabName: e.detail.tab});
- }
-
- _handleEditCommitMessage(e) {
- this._editingCommitMessage = true;
- this.$.commitMessageEditor.focusTextarea();
- }
-
- _handleCommitMessageSave(e) {
- // Trim trailing whitespace from each line.
- const message = e.detail.content.replace(TRAILING_WHITESPACE_REGEX, '');
-
- this.$.jsAPI.handleCommitMessage(this._change, message);
-
- this.$.commitMessageEditor.disabled = true;
- this.$.restAPI.putChangeCommitMessage(
- this._changeNum, message).then(resp => {
- this.$.commitMessageEditor.disabled = false;
- if (!resp.ok) { return; }
-
- this._latestCommitMessage = this._prepareCommitMsgForLinkify(
- message);
- this._editingCommitMessage = false;
- this._reloadWindow();
- })
- .catch(err => {
- this.$.commitMessageEditor.disabled = false;
- });
- }
-
- _reloadWindow() {
- window.location.reload();
- }
-
- _handleCommitMessageCancel(e) {
- this._editingCommitMessage = false;
- }
-
- _computeChangeStatusChips(change, mergeable, submitEnabled) {
- // Polymer 2: check for undefined
- if ([
- change,
- mergeable,
- ].some(arg => arg === undefined)) {
- // To keep consistent with Polymer 1, we are returning undefined
- // if not all dependencies are defined
- return undefined;
- }
-
- // Show no chips until mergeability is loaded.
- if (mergeable === null) {
- return [];
- }
-
- const options = {
- includeDerived: true,
- mergeable: !!mergeable,
- submitEnabled: !!submitEnabled,
- };
- return this.changeStatuses(change, options);
- }
-
- _computeHideEditCommitMessage(
- loggedIn, editing, change, editMode, collapsed, collapsible) {
- if (!loggedIn || editing ||
- (change && change.status === this.ChangeStatus.MERGED) ||
- editMode ||
- (collapsed && collapsible)) {
- return true;
- }
-
- return false;
- }
-
- _robotCommentCountPerPatchSet(threads) {
- return threads.reduce((robotCommentCountMap, thread) => {
- const comments = thread.comments;
- const robotCommentsCount = comments.reduce((acc, comment) =>
- (comment.robot_id ? acc + 1 : acc), 0);
- robotCommentCountMap[comments[0].patch_set] =
- (robotCommentCountMap[comments[0].patch_set] || 0) +
- robotCommentsCount;
- return robotCommentCountMap;
- }, {});
- }
-
- _computeText(patch, commentThreads) {
- const commentCount = this._robotCommentCountPerPatchSet(commentThreads);
- const commentCnt = commentCount[patch._number] || 0;
- if (commentCnt === 0) return `Patchset ${patch._number}`;
- const findingsText = commentCnt === 1 ? 'finding' : 'findings';
- return `Patchset ${patch._number}`
- + ` (${commentCnt} ${findingsText})`;
- }
-
- _computeRobotCommentsPatchSetDropdownItems(change, commentThreads) {
- if (!change || !commentThreads || !change.revisions) return [];
-
- return Object.values(change.revisions)
- .filter(patch => patch._number !== 'edit')
- .map(patch => {
- return {
- text: this._computeText(patch, commentThreads),
- value: patch._number,
- };
- })
- .sort((a, b) => b.value - a.value);
- }
-
- _handleCurrentRevisionUpdate(currentRevision) {
- this._currentRobotCommentsPatchSet = currentRevision._number;
- }
-
- _handleRobotCommentPatchSetChanged(e) {
- const patchSet = parseInt(e.detail.value);
- if (patchSet === this._currentRobotCommentsPatchSet) return;
- this._currentRobotCommentsPatchSet = patchSet;
- }
-
- _computeShowText(showAllRobotComments) {
- return showAllRobotComments ? 'Show Less' : 'Show more';
- }
-
- _toggleShowRobotComments() {
- this._showAllRobotComments = !this._showAllRobotComments;
- }
-
- _computeRobotCommentThreads(commentThreads, currentRobotCommentsPatchSet,
- showAllRobotComments) {
- if (!commentThreads || !currentRobotCommentsPatchSet) return [];
- const threads = commentThreads.filter(thread => {
- const comments = thread.comments || [];
- return comments.length && comments[0].robot_id && (comments[0].patch_set
- === currentRobotCommentsPatchSet);
- });
- this._showRobotCommentsButton = threads.length > ROBOT_COMMENTS_LIMIT;
- return threads.slice(0, showAllRobotComments ? undefined :
- ROBOT_COMMENTS_LIMIT);
- }
-
- _handleReloadCommentThreads() {
- // Get any new drafts that have been saved in the diff view and show
- // in the comment thread view.
- this._reloadDrafts().then(() => {
- this._commentThreads = this._changeComments.getAllThreadsForChange()
- .map(c => Object.assign({}, c));
- Polymer.dom.flush();
- });
- }
-
- _handleReloadDiffComments(e) {
- // Keeps the file list counts updated.
- this._reloadDrafts().then(() => {
- // Get any new drafts that have been saved in the thread view and show
- // in the diff view.
- this.$.fileList.reloadCommentsForThreadWithRootId(e.detail.rootId,
- e.detail.path);
- Polymer.dom.flush();
- });
- }
-
- _computeTotalCommentCounts(unresolvedCount, changeComments) {
- if (!changeComments) return undefined;
- const draftCount = changeComments.computeDraftCount();
- const unresolvedString = GrCountStringFormatter.computeString(
- unresolvedCount, 'unresolved');
- const draftString = GrCountStringFormatter.computePluralString(
- draftCount, 'draft');
-
- return unresolvedString +
- // Add a comma and space if both unresolved and draft comments exist.
- (unresolvedString && draftString ? ', ' : '') +
- draftString;
- }
-
- _handleCommentSave(e) {
- const draft = e.detail.comment;
- if (!draft.__draft) { return; }
-
- draft.patch_set = draft.patch_set || this._patchRange.patchNum;
-
- // The use of path-based notification helpers (set, push) can’t be used
- // because the paths could contain dots in them. A new object must be
- // created to satisfy Polymer’s dirty checking.
- // https://github.com/Polymer/polymer/issues/3127
- const diffDrafts = Object.assign({}, this._diffDrafts);
- if (!diffDrafts[draft.path]) {
- diffDrafts[draft.path] = [draft];
- this._diffDrafts = diffDrafts;
- return;
- }
- for (let i = 0; i < this._diffDrafts[draft.path].length; i++) {
- if (this._diffDrafts[draft.path][i].id === draft.id) {
- diffDrafts[draft.path][i] = draft;
- this._diffDrafts = diffDrafts;
- return;
- }
- }
- diffDrafts[draft.path].push(draft);
- diffDrafts[draft.path].sort((c1, c2) =>
- // No line number means that it’s a file comment. Sort it above the
- // others.
- (c1.line || -1) - (c2.line || -1)
- );
- this._diffDrafts = diffDrafts;
- }
-
- _handleCommentDiscard(e) {
- const draft = e.detail.comment;
- if (!draft.__draft) { return; }
-
- if (!this._diffDrafts[draft.path]) {
- return;
- }
- let index = -1;
- for (let i = 0; i < this._diffDrafts[draft.path].length; i++) {
- if (this._diffDrafts[draft.path][i].id === draft.id) {
- index = i;
- break;
- }
- }
- if (index === -1) {
- // It may be a draft that hasn’t been added to _diffDrafts since it was
- // never saved.
- return;
- }
-
- draft.patch_set = draft.patch_set || this._patchRange.patchNum;
-
- // The use of path-based notification helpers (set, push) can’t be used
- // because the paths could contain dots in them. A new object must be
- // created to satisfy Polymer’s dirty checking.
- // https://github.com/Polymer/polymer/issues/3127
- const diffDrafts = Object.assign({}, this._diffDrafts);
- diffDrafts[draft.path].splice(index, 1);
- if (diffDrafts[draft.path].length === 0) {
- delete diffDrafts[draft.path];
- }
- this._diffDrafts = diffDrafts;
- }
-
- _handleReplyTap(e) {
- e.preventDefault();
- this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
- }
-
- _handleOpenDiffPrefs() {
- this.$.fileList.openDiffPrefs();
- }
-
- _handleOpenIncludedInDialog() {
- this.$.includedInDialog.loadData().then(() => {
- Polymer.dom.flush();
- this.$.includedInOverlay.refit();
- });
- this.$.includedInOverlay.open();
- }
-
- _handleIncludedInDialogClose(e) {
- this.$.includedInOverlay.close();
- }
-
- _handleOpenDownloadDialog() {
- this.$.downloadOverlay.open().then(() => {
- this.$.downloadOverlay
- .setFocusStops(this.$.downloadDialog.getFocusStops());
- this.$.downloadDialog.focus();
- });
- }
-
- _handleDownloadDialogClose(e) {
- this.$.downloadOverlay.close();
- }
-
- _handleOpenUploadHelpDialog(e) {
- this.$.uploadHelpOverlay.open();
- }
-
- _handleCloseUploadHelpDialog(e) {
- this.$.uploadHelpOverlay.close();
- }
-
- _handleMessageReply(e) {
- const msg = e.detail.message.message;
- const quoteStr = msg.split('\n').map(
- line => '> ' + line)
- .join('\n') + '\n\n';
- this.$.replyDialog.quote = quoteStr;
- this._openReplyDialog(this.$.replyDialog.FocusTarget.BODY);
- }
-
- _handleHideBackgroundContent() {
- this.$.mainContent.classList.add('overlayOpen');
- }
-
- _handleShowBackgroundContent() {
- this.$.mainContent.classList.remove('overlayOpen');
- }
-
- _handleReplySent(e) {
- this.addEventListener('change-details-loaded',
- () => {
- this.$.reporting.timeEnd(SEND_REPLY_TIMING_LABEL);
- }, {once: true});
- this.$.replyOverlay.close();
- this._reload();
- }
-
- _handleReplyCancel(e) {
- this.$.replyOverlay.close();
- }
-
- _handleReplyAutogrow(e) {
- // If the textarea resizes, we need to re-fit the overlay.
- this.debounce('reply-overlay-refit', () => {
- this.$.replyOverlay.refit();
- }, REPLY_REFIT_DEBOUNCE_INTERVAL_MS);
- }
-
- _handleShowReplyDialog(e) {
- let target = this.$.replyDialog.FocusTarget.REVIEWERS;
- if (e.detail.value && e.detail.value.ccsOnly) {
- target = this.$.replyDialog.FocusTarget.CCS;
- }
- this._openReplyDialog(target);
- }
-
- _handleScroll() {
- this.debounce('scroll', () => {
- this.viewState.scrollTop = document.body.scrollTop;
- }, 150);
- }
-
- _setShownFiles(e) {
- this._shownFileCount = e.detail.length;
- }
-
- _expandAllDiffs() {
- this.$.fileList.expandAllDiffs();
- }
-
- _collapseAllDiffs() {
- this.$.fileList.collapseAllDiffs();
- }
-
- _paramsChanged(value) {
- this._currentView = CommentTabs.CHANGE_LOG;
- this._setPrimaryTab();
- if (value.view !== Gerrit.Nav.View.CHANGE) {
- this._initialLoadComplete = false;
- return;
- }
-
- if (value.changeNum && value.project) {
- this.$.restAPI.setInProjectLookup(value.changeNum, value.project);
- }
-
- const patchChanged = this._patchRange &&
- (value.patchNum !== undefined && value.basePatchNum !== undefined) &&
- (this._patchRange.patchNum !== value.patchNum ||
- this._patchRange.basePatchNum !== value.basePatchNum);
+ this.addEventListener(
+ // When an overlay is opened in a mobile viewport, the overlay has a full
+ // screen view. When it has a full screen view, we do not want the
+ // background to be scrollable. This will eliminate background scroll by
+ // hiding most of the contents on the screen upon opening, and showing
+ // again upon closing.
+ 'fullscreen-overlay-opened',
+ () => this._handleHideBackgroundContent());
- if (this._changeNum !== value.changeNum) {
- this._initialLoadComplete = false;
- }
+ this.addEventListener('fullscreen-overlay-closed',
+ () => this._handleShowBackgroundContent());
- const patchRange = {
- patchNum: value.patchNum,
- basePatchNum: value.basePatchNum || 'PARENT',
- };
+ this.addEventListener('diff-comments-modified',
+ () => this._handleReloadCommentThreads());
+ }
- this.$.fileList.collapseAllDiffs();
- this._patchRange = patchRange;
+ /** @override */
+ attached() {
+ super.attached();
+ this._getServerConfig().then(config => {
+ this._serverConfig = config;
+ });
- // If the change has already been loaded and the parameter change is only
- // in the patch range, then don't do a full reload.
- if (this._initialLoadComplete && patchChanged) {
- if (patchRange.patchNum == null) {
- patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets);
- }
- this._reloadPatchNumDependentResources().then(() => {
- this._sendShowChangeEvent();
+ this._getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ if (loggedIn) {
+ this.$.restAPI.getAccount().then(acct => {
+ this._account = acct;
});
- return;
}
+ this._setDiffViewMode();
+ });
- this._changeNum = value.changeNum;
- this.$.relatedChanges.clear();
-
- this._reload(true).then(() => {
- this._performPostLoadTasks();
- });
- }
-
- _sendShowChangeEvent() {
- this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, {
- change: this._change,
- patchNum: this._patchRange.patchNum,
- info: {mergeable: this._mergeable},
- });
- }
-
- _setPrimaryTab() {
- // Selected has to be set after the paper-tabs are visible, because
- // the selected underline depends on calculations made by the browser.
- // paper-tabs depends on iron-resizable-behavior, which only fires on
- // attached() without using RenderStatus.beforeNextRender. Not changing
- // this when migrating from Polymer 1 to 2 was probably an oversight by
- // the paper component maintainers.
- // https://polymer-library.polymer-project.org/2.0/docs/upgrade#attach-time-attached-connectedcallback
- // By calling _onTabSizingChanged() we are reaching into the private API
- // of paper-tabs, but we believe this workaround is acceptable for the
- // time being.
- Polymer.RenderStatus.beforeNextRender(this, () => {
- this.$.commentTabs.selected = 0;
- this.$.commentTabs._onTabSizingChanged();
- const primaryTabs = this.shadowRoot.querySelector('#primaryTabs');
- if (primaryTabs) {
- primaryTabs.selected = 0;
- primaryTabs._onTabSizingChanged();
- }
- });
- }
-
- _performPostLoadTasks() {
- this._maybeShowReplyDialog();
- this._maybeShowRevertDialog();
-
- this._sendShowChangeEvent();
-
- this.async(() => {
- if (this.viewState.scrollTop) {
- document.documentElement.scrollTop =
- document.body.scrollTop = this.viewState.scrollTop;
- } else {
- this._maybeScrollToMessage(window.location.hash);
- }
- this._initialLoadComplete = true;
- });
- }
-
- _paramsAndChangeChanged(value, change) {
- // Polymer 2: check for undefined
- if ([value, change].some(arg => arg === undefined)) {
- return;
- }
-
- // If the change number or patch range is different, then reset the
- // selected file index.
- const patchRangeState = this.viewState.patchRange;
- if (this.viewState.changeNum !== this._changeNum ||
- patchRangeState.basePatchNum !== this._patchRange.basePatchNum ||
- patchRangeState.patchNum !== this._patchRange.patchNum) {
- this._resetFileListViewState();
- }
- }
-
- _viewStateChanged(viewState) {
- this._numFilesShown = viewState.numFilesShown ?
- viewState.numFilesShown : DEFAULT_NUM_FILES_SHOWN;
- }
-
- _numFilesShownChanged(numFilesShown) {
- this.viewState.numFilesShown = numFilesShown;
- }
-
- _handleMessageAnchorTap(e) {
- const hash = MSG_PREFIX + e.detail.id;
- const url = Gerrit.Nav.getUrlForChange(this._change,
- this._patchRange.patchNum, this._patchRange.basePatchNum,
- this._editMode, hash);
- history.replaceState(null, '', url);
- }
-
- _maybeScrollToMessage(hash) {
- if (hash.startsWith(MSG_PREFIX)) {
- this.messagesList.scrollToMessage(hash.substr(MSG_PREFIX.length));
- }
- }
-
- _getLocationSearch() {
- // Not inlining to make it easier to test.
- return window.location.search;
- }
-
- _getUrlParameter(param) {
- const pageURL = this._getLocationSearch().substring(1);
- const vars = pageURL.split('&');
- for (let i = 0; i < vars.length; i++) {
- const name = vars[i].split('=');
- if (name[0] == param) {
- return name[0];
- }
- }
- return null;
- }
-
- _maybeShowRevertDialog() {
- Gerrit.awaitPluginsLoaded()
- .then(this._getLoggedIn.bind(this))
- .then(loggedIn => {
- if (!loggedIn || !this._change ||
- this._change.status !== this.ChangeStatus.MERGED) {
- // Do not display dialog if not logged-in or the change is not
- // merged.
- return;
- }
- if (this._getUrlParameter('revert')) {
- this.$.actions.showRevertDialog();
- }
- });
- }
-
- _maybeShowReplyDialog() {
- this._getLoggedIn().then(loggedIn => {
- if (!loggedIn) { return; }
-
- if (this.viewState.showReplyDialog) {
- this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
- // TODO(kaspern@): Find a better signal for when to call center.
- this.async(() => { this.$.replyOverlay.center(); }, 100);
- this.async(() => { this.$.replyOverlay.center(); }, 1000);
- this.set('viewState.showReplyDialog', false);
- }
- });
- }
-
- _resetFileListViewState() {
- this.set('viewState.selectedFileIndex', 0);
- this.set('viewState.scrollTop', 0);
- if (!!this.viewState.changeNum &&
- this.viewState.changeNum !== this._changeNum) {
- // Reset the diff mode to null when navigating from one change to
- // another, so that the user's preference is restored.
- this._setDiffViewMode(true);
- this.set('_numFilesShown', DEFAULT_NUM_FILES_SHOWN);
- }
- this.set('viewState.changeNum', this._changeNum);
- this.set('viewState.patchRange', this._patchRange);
- }
-
- _changeChanged(change) {
- if (!change || !this._patchRange || !this._allPatchSets) { return; }
-
- // We get the parent first so we keep the original value for basePatchNum
- // and not the updated value.
- const parent = this._getBasePatchNum(change, this._patchRange);
-
- this.set('_patchRange.patchNum', this._patchRange.patchNum ||
- this.computeLatestPatchNum(this._allPatchSets));
-
- this.set('_patchRange.basePatchNum', parent);
-
- const title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
- this.fire('title-change', {title});
- }
-
- /**
- * Gets base patch number, if it is a parent try and decide from
- * preference whether to default to `auto merge`, `Parent 1` or `PARENT`.
- *
- * @param {Object} change
- * @param {Object} patchRange
- * @return {number|string}
- */
- _getBasePatchNum(change, patchRange) {
- if (patchRange.basePatchNum &&
- patchRange.basePatchNum !== 'PARENT') {
- return patchRange.basePatchNum;
- }
-
- const revisionInfo = this._getRevisionInfo(change);
- if (!revisionInfo) return 'PARENT';
-
- const parentCounts = revisionInfo.getParentCountMap();
- // check that there is at least 2 parents otherwise fall back to 1,
- // which means there is only one parent.
- const parentCount = parentCounts.hasOwnProperty(1) ?
- parentCounts[1] : 1;
-
- const preferFirst = this._prefs &&
- this._prefs.default_base_for_merges === 'FIRST_PARENT';
-
- if (parentCount > 1 && preferFirst && !patchRange.patchNum) {
- return -1;
- }
-
- return 'PARENT';
- }
-
- _computeChangeUrl(change) {
- return Gerrit.Nav.getUrlForChange(change);
- }
-
- _computeShowCommitInfo(changeStatus, current_revision) {
- return changeStatus === 'Merged' && current_revision;
- }
-
- _computeMergedCommitInfo(current_revision, revisions) {
- const rev = revisions[current_revision];
- if (!rev || !rev.commit) { return {}; }
- // CommitInfo.commit is optional. Set commit in all cases to avoid error
- // in <gr-commit-info>. @see Issue 5337
- if (!rev.commit.commit) { rev.commit.commit = current_revision; }
- return rev.commit;
- }
-
- _computeChangeIdClass(displayChangeId) {
- return displayChangeId === CHANGE_ID_ERROR.MISMATCH ? 'warning' : '';
- }
-
- _computeTitleAttributeWarning(displayChangeId) {
- if (displayChangeId === CHANGE_ID_ERROR.MISMATCH) {
- return 'Change-Id mismatch';
- } else if (displayChangeId === CHANGE_ID_ERROR.MISSING) {
- return 'No Change-Id in commit message';
- }
- }
-
- _computeChangeIdCommitMessageError(commitMessage, change) {
- // Polymer 2: check for undefined
- if ([commitMessage, change].some(arg => arg === undefined)) {
- return undefined;
- }
-
- if (!commitMessage) { return CHANGE_ID_ERROR.MISSING; }
-
- // Find the last match in the commit message:
- let changeId;
- let changeIdArr;
-
- while (changeIdArr = CHANGE_ID_REGEX_PATTERN.exec(commitMessage)) {
- changeId = changeIdArr[1];
- }
-
- if (changeId) {
- // A change-id is detected in the commit message.
-
- if (changeId === change.change_id) {
- // The change-id found matches the real change-id.
- return null;
- }
- // The change-id found does not match the change-id.
- return CHANGE_ID_ERROR.MISMATCH;
- }
- // There is no change-id in the commit message.
- return CHANGE_ID_ERROR.MISSING;
- }
-
- _computeLabelNames(labels) {
- return Object.keys(labels).sort();
- }
-
- _computeLabelValues(labelName, labels) {
- const result = [];
- const t = labels[labelName];
- if (!t) { return result; }
- const approvals = t.all || [];
- for (const label of approvals) {
- if (label.value && label.value != labels[labelName].default_value) {
- let labelClassName;
- let labelValPrefix = '';
- if (label.value > 0) {
- labelValPrefix = '+';
- labelClassName = 'approved';
- } else if (label.value < 0) {
- labelClassName = 'notApproved';
+ Gerrit.awaitPluginsLoaded()
+ .then(() => {
+ this._dynamicTabHeaderEndpoints =
+ Gerrit._endpoints.getDynamicEndpoints('change-view-tab-header');
+ this._dynamicTabContentEndpoints =
+ Gerrit._endpoints.getDynamicEndpoints('change-view-tab-content');
+ if (this._dynamicTabContentEndpoints.length !==
+ this._dynamicTabHeaderEndpoints.length) {
+ console.warn('Different number of tab headers and tab content.');
}
- result.push({
- value: labelValPrefix + label.value,
- className: labelClassName,
- account: label,
- });
- }
- }
- return result;
- }
+ })
+ .then(() => this._setPrimaryTab());
- _computeReplyButtonLabel(changeRecord, canStartReview) {
- // Polymer 2: check for undefined
- if ([changeRecord, canStartReview].some(arg => arg === undefined)) {
- return 'Reply';
- }
+ this.addEventListener('comment-save', this._handleCommentSave.bind(this));
+ this.addEventListener('comment-refresh', this._reloadDrafts.bind(this));
+ this.addEventListener('comment-discard',
+ this._handleCommentDiscard.bind(this));
+ this.addEventListener('change-message-deleted',
+ () => this._reload());
+ this.addEventListener('editable-content-save',
+ this._handleCommitMessageSave.bind(this));
+ this.addEventListener('editable-content-cancel',
+ this._handleCommitMessageCancel.bind(this));
+ this.addEventListener('open-fix-preview',
+ this._onOpenFixPreview.bind(this));
+ this.addEventListener('close-fix-preview',
+ this._onCloseFixPreview.bind(this));
+ this.listen(window, 'scroll', '_handleScroll');
+ this.listen(document, 'visibilitychange', '_handleVisibilityChange');
+ }
- if (canStartReview) {
- return 'Start review';
- }
+ /** @override */
+ detached() {
+ super.detached();
+ this.unlisten(window, 'scroll', '_handleScroll');
+ this.unlisten(document, 'visibilitychange', '_handleVisibilityChange');
- const drafts = (changeRecord && changeRecord.base) || {};
- const draftCount = Object.keys(drafts)
- .reduce((count, file) => count + drafts[file].length, 0);
-
- let label = 'Reply';
- if (draftCount > 0) {
- label += ' (' + draftCount + ')';
- }
- return label;
- }
-
- _handleOpenReplyDialog(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) {
- return;
- }
- this._getLoggedIn().then(isLoggedIn => {
- if (!isLoggedIn) {
- this.fire('show-auth-required');
- return;
- }
-
- e.preventDefault();
- this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
- });
- }
-
- _handleOpenDownloadDialogShortcut(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.downloadOverlay.open();
- }
-
- _handleEditTopic(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.metadata.editTopic();
- }
-
- _handleRefreshChange(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
- e.preventDefault();
- Gerrit.Nav.navigateToChange(this._change);
- }
-
- _handleToggleChangeStar(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.changeStar.toggleStar();
- }
-
- _handleUpToDashboard(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this._determinePageBack();
- }
-
- _handleExpandAllMessages(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.messagesList.handleExpandCollapse(true);
- }
-
- _handleCollapseAllMessages(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.messagesList.handleExpandCollapse(false);
- }
-
- _handleOpenDiffPrefsShortcut(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- if (this._diffPrefsDisabled) { return; }
-
- e.preventDefault();
- this.$.fileList.openDiffPrefs();
- }
-
- _determinePageBack() {
- // Default backPage to root if user came to change view page
- // via an email link, etc.
- Gerrit.Nav.navigateToRelativeUrl(this.backPage ||
- Gerrit.Nav.getUrlForRoot());
- }
-
- _handleLabelRemoved(splices, path) {
- for (const splice of splices) {
- for (const removed of splice.removed) {
- const changePath = path.split('.');
- const labelPath = changePath.splice(0, changePath.length - 2);
- const labelDict = this.get(labelPath);
- if (labelDict.approved &&
- labelDict.approved._account_id === removed._account_id) {
- this._reload();
- return;
- }
- }
- }
- }
-
- _labelsChanged(changeRecord) {
- if (!changeRecord) { return; }
- if (changeRecord.value && changeRecord.value.indexSplices) {
- this._handleLabelRemoved(changeRecord.value.indexSplices,
- changeRecord.path);
- }
- this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.LABEL_CHANGE, {
- change: this._change,
- });
- }
-
- /**
- * @param {string=} opt_section
- */
- _openReplyDialog(opt_section) {
- this.$.replyOverlay.open().finally(() => {
- // the following code should be executed no matter open succeed or not
- this._resetReplyOverlayFocusStops();
- this.$.replyDialog.open(opt_section);
- Polymer.dom.flush();
- this.$.replyOverlay.center();
- });
- }
-
- _handleReloadChange(e) {
- return this._reload().then(() => {
- // If the change was rebased or submitted, we need to reload the page
- // with the latest patch.
- const action = e.detail.action;
- if (action === 'rebase' || action === 'submit') {
- Gerrit.Nav.navigateToChange(this._change);
- }
- });
- }
-
- _handleGetChangeDetailError(response) {
- this.fire('page-error', {response});
- }
-
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- _getServerConfig() {
- return this.$.restAPI.getConfig();
- }
-
- _getProjectConfig() {
- if (!this._change) return;
- return this.$.restAPI.getProjectConfig(this._change.project).then(
- config => {
- this._projectConfig = config;
- });
- }
-
- _getPreferences() {
- return this.$.restAPI.getPreferences();
- }
-
- _prepareCommitMsgForLinkify(msg) {
- // TODO(wyatta) switch linkify sequence, see issue 5526.
- // This is a zero-with space. It is added to prevent the linkify library
- // from including R= or CC= as part of the email address.
- return msg.replace(REVIEWERS_REGEX, '$1=\u200B');
- }
-
- /**
- * Utility function to make the necessary modifications to a change in the
- * case an edit exists.
- *
- * @param {!Object} change
- * @param {?Object} edit
- */
- _processEdit(change, edit) {
- if (!edit) { return; }
- change.revisions[edit.commit.commit] = {
- _number: this.EDIT_NAME,
- basePatchNum: edit.base_patch_set_number,
- commit: edit.commit,
- fetch: edit.fetch,
- };
- // If the edit is based on the most recent patchset, load it by
- // default, unless another patch set to load was specified in the URL.
- if (!this._patchRange.patchNum &&
- change.current_revision === edit.base_revision) {
- change.current_revision = edit.commit.commit;
- this.set('_patchRange.patchNum', this.EDIT_NAME);
- // Because edits are fibbed as revisions and added to the revisions
- // array, and revision actions are always derived from the 'latest'
- // patch set, we must copy over actions from the patch set base.
- // Context: Issue 7243
- change.revisions[edit.commit.commit].actions =
- change.revisions[edit.base_revision].actions;
- }
- }
-
- _getChangeDetail() {
- const detailCompletes = this.$.restAPI.getChangeDetail(
- this._changeNum, this._handleGetChangeDetailError.bind(this));
- const editCompletes = this._getEdit();
- const prefCompletes = this._getPreferences();
-
- return Promise.all([detailCompletes, editCompletes, prefCompletes])
- .then(([change, edit, prefs]) => {
- this._prefs = prefs;
-
- if (!change) {
- return '';
- }
- this._processEdit(change, edit);
- // Issue 4190: Coalesce missing topics to null.
- if (!change.topic) { change.topic = null; }
- if (!change.reviewer_updates) {
- change.reviewer_updates = null;
- }
- const latestRevisionSha = this._getLatestRevisionSHA(change);
- const currentRevision = change.revisions[latestRevisionSha];
- if (currentRevision.commit && currentRevision.commit.message) {
- this._latestCommitMessage = this._prepareCommitMsgForLinkify(
- currentRevision.commit.message);
- } else {
- this._latestCommitMessage = null;
- }
-
- const lineHeight = getComputedStyle(this).lineHeight;
-
- // Slice returns a number as a string, convert to an int.
- this._lineHeight =
- parseInt(lineHeight.slice(0, lineHeight.length - 2), 10);
-
- this._change = change;
- if (!this._patchRange || !this._patchRange.patchNum ||
- this.patchNumEquals(this._patchRange.patchNum,
- currentRevision._number)) {
- // CommitInfo.commit is optional, and may need patching.
- if (!currentRevision.commit.commit) {
- currentRevision.commit.commit = latestRevisionSha;
- }
- this._commitInfo = currentRevision.commit;
- this._selectedRevision = currentRevision;
- // TODO: Fetch and process files.
- } else {
- this._selectedRevision =
- Object.values(this._change.revisions).find(
- revision => {
- // edit patchset is a special one
- const thePatchNum = this._patchRange.patchNum;
- if (thePatchNum === 'edit') {
- return revision._number === thePatchNum;
- }
- return revision._number === parseInt(thePatchNum, 10);
- });
- }
- });
- }
-
- _isSubmitEnabled(revisionActions) {
- return !!(revisionActions && revisionActions.submit &&
- revisionActions.submit.enabled);
- }
-
- _getEdit() {
- return this.$.restAPI.getChangeEdit(this._changeNum, true);
- }
-
- _getLatestCommitMessage() {
- return this.$.restAPI.getChangeCommitInfo(this._changeNum,
- this.computeLatestPatchNum(this._allPatchSets)).then(commitInfo => {
- if (!commitInfo) return Promise.resolve();
- this._latestCommitMessage =
- this._prepareCommitMsgForLinkify(commitInfo.message);
- });
- }
-
- _getLatestRevisionSHA(change) {
- if (change.current_revision) {
- return change.current_revision;
- }
- // current_revision may not be present in the case where the latest rev is
- // a draft and the user doesn’t have permission to view that rev.
- let latestRev = null;
- let latestPatchNum = -1;
- for (const rev in change.revisions) {
- if (!change.revisions.hasOwnProperty(rev)) { continue; }
-
- if (change.revisions[rev]._number > latestPatchNum) {
- latestRev = rev;
- latestPatchNum = change.revisions[rev]._number;
- }
- }
- return latestRev;
- }
-
- _getCommitInfo() {
- return this.$.restAPI.getChangeCommitInfo(
- this._changeNum, this._patchRange.patchNum).then(
- commitInfo => {
- this._commitInfo = commitInfo;
- });
- }
-
- _reloadDraftsWithCallback(e) {
- return this._reloadDrafts().then(() => e.detail.resolve());
- }
-
- /**
- * Fetches a new changeComment object, and data for all types of comments
- * (comments, robot comments, draft comments) is requested.
- */
- _reloadComments() {
- return this.$.commentAPI.loadAll(this._changeNum)
- .then(comments => this._recomputeComments(comments));
- }
-
- /**
- * Fetches a new changeComment object, but only updated data for drafts is
- * requested.
- *
- * TODO(taoalpha): clean up this and _reloadComments, as single comment
- * can be a thread so it does not make sense to only update drafts
- * without updating threads
- */
- _reloadDrafts() {
- return this.$.commentAPI.reloadDrafts(this._changeNum)
- .then(comments => this._recomputeComments(comments));
- }
-
- _recomputeComments(comments) {
- this._changeComments = comments;
- this._diffDrafts = Object.assign({}, this._changeComments.drafts);
- this._commentThreads = this._changeComments.getAllThreadsForChange()
- .map(c => Object.assign({}, c));
- this._draftCommentThreads = this._commentThreads
- .filter(c => c.comments[c.comments.length - 1].__draft);
- }
-
- /**
- * Reload the change.
- *
- * @param {boolean=} opt_isLocationChange Reloads the related changes
- * when true and ends reporting events that started on location change.
- * @return {Promise} A promise that resolves when the core data has loaded.
- * Some non-core data loading may still be in-flight when the core data
- * promise resolves.
- */
- _reload(opt_isLocationChange) {
- this._loading = true;
- this._relatedChangesCollapsed = true;
- this.$.reporting.time(CHANGE_RELOAD_TIMING_LABEL);
- this.$.reporting.time(CHANGE_DATA_TIMING_LABEL);
-
- // Array to house all promises related to data requests.
- const allDataPromises = [];
-
- // Resolves when the change detail and the edit patch set (if available)
- // are loaded.
- const detailCompletes = this._getChangeDetail();
- allDataPromises.push(detailCompletes);
-
- // Resolves when the loading flag is set to false, meaning that some
- // change content may start appearing.
- const loadingFlagSet = detailCompletes
- .then(() => {
- this._loading = false;
- this.dispatchEvent(new CustomEvent('change-details-loaded',
- {bubbles: true, composed: true}));
- })
- .then(() => {
- this.$.reporting.timeEnd(CHANGE_RELOAD_TIMING_LABEL);
- if (opt_isLocationChange) {
- this.$.reporting.changeDisplayed();
- }
- });
-
- // Resolves when the project config has loaded.
- const projectConfigLoaded = detailCompletes
- .then(() => this._getProjectConfig());
- allDataPromises.push(projectConfigLoaded);
-
- // Resolves when change comments have loaded (comments, drafts and robot
- // comments).
- const commentsLoaded = this._reloadComments();
- allDataPromises.push(commentsLoaded);
-
- let coreDataPromise;
-
- // If the patch number is specified
- if (this._patchRange && this._patchRange.patchNum) {
- // Because a specific patchset is specified, reload the resources that
- // are keyed by patch number or patch range.
- const patchResourcesLoaded = this._reloadPatchNumDependentResources();
- allDataPromises.push(patchResourcesLoaded);
-
- // Promise resolves when the change detail and patch dependent resources
- // have loaded.
- const detailAndPatchResourcesLoaded =
- Promise.all([patchResourcesLoaded, loadingFlagSet]);
-
- // Promise resolves when mergeability information has loaded.
- const mergeabilityLoaded = detailAndPatchResourcesLoaded
- .then(() => this._getMergeability());
- allDataPromises.push(mergeabilityLoaded);
-
- // Promise resovles when the change actions have loaded.
- const actionsLoaded = detailAndPatchResourcesLoaded
- .then(() => this.$.actions.reload());
- allDataPromises.push(actionsLoaded);
-
- // The core data is loaded when both mergeability and actions are known.
- coreDataPromise = Promise.all([mergeabilityLoaded, actionsLoaded]);
- } else {
- // Resolves when the file list has loaded.
- const fileListReload = loadingFlagSet
- .then(() => this.$.fileList.reload());
- allDataPromises.push(fileListReload);
-
- const latestCommitMessageLoaded = loadingFlagSet.then(() => {
- // If the latest commit message is known, there is nothing to do.
- if (this._latestCommitMessage) { return Promise.resolve(); }
- return this._getLatestCommitMessage();
- });
- allDataPromises.push(latestCommitMessageLoaded);
-
- // Promise resolves when mergeability information has loaded.
- const mergeabilityLoaded = loadingFlagSet
- .then(() => this._getMergeability());
- allDataPromises.push(mergeabilityLoaded);
-
- // Core data is loaded when mergeability has been loaded.
- coreDataPromise = mergeabilityLoaded;
- }
-
- if (opt_isLocationChange) {
- const relatedChangesLoaded = coreDataPromise
- .then(() => this.$.relatedChanges.reload());
- allDataPromises.push(relatedChangesLoaded);
- }
-
- Promise.all(allDataPromises).then(() => {
- this.$.reporting.timeEnd(CHANGE_DATA_TIMING_LABEL);
- if (opt_isLocationChange) {
- this.$.reporting.changeFullyLoaded();
- }
- });
-
- return coreDataPromise;
- }
-
- /**
- * Kicks off requests for resources that rely on the patch range
- * (`this._patchRange`) being defined.
- */
- _reloadPatchNumDependentResources() {
- return Promise.all([
- this._getCommitInfo(),
- this.$.fileList.reload(),
- ]);
- }
-
- _getMergeability() {
- if (!this._change) {
- this._mergeable = null;
- return Promise.resolve();
- }
- // If the change is closed, it is not mergeable. Note: already merged
- // changes are obviously not mergeable, but the mergeability API will not
- // answer for abandoned changes.
- if (this._change.status === this.ChangeStatus.MERGED ||
- this._change.status === this.ChangeStatus.ABANDONED) {
- this._mergeable = false;
- return Promise.resolve();
- }
-
- this._mergeable = null;
- return this.$.restAPI.getMergeable(this._changeNum).then(m => {
- this._mergeable = m.mergeable;
- });
- }
-
- _computeCanStartReview(change) {
- return !!(change.actions && change.actions.ready &&
- change.actions.ready.enabled);
- }
-
- _computeReplyDisabled() { return false; }
-
- _computeChangePermalinkAriaLabel(changeNum) {
- return 'Change ' + changeNum;
- }
-
- _computeCommitMessageCollapsed(collapsed, collapsible) {
- return collapsible && collapsed;
- }
-
- _computeRelatedChangesClass(collapsed) {
- return collapsed ? 'collapsed' : '';
- }
-
- _computeCollapseText(collapsed) {
- // Symbols are up and down triangles.
- return collapsed ? '\u25bc Show more' : '\u25b2 Show less';
- }
-
- /**
- * Returns the text to be copied when
- * click the copy icon next to change subject
- *
- * @param {!Object} change
- */
- _computeCopyTextForTitle(change) {
- return `${change._number}: ${change.subject}` +
- ` | https://${location.host}${this._computeChangeUrl(change)}`;
- }
-
- _toggleCommitCollapsed() {
- this._commitCollapsed = !this._commitCollapsed;
- if (this._commitCollapsed) {
- window.scrollTo(0, 0);
- }
- }
-
- _toggleRelatedChangesCollapsed() {
- this._relatedChangesCollapsed = !this._relatedChangesCollapsed;
- if (this._relatedChangesCollapsed) {
- window.scrollTo(0, 0);
- }
- }
-
- _computeCommitCollapsible(commitMessage) {
- if (!commitMessage) { return false; }
- return commitMessage.split('\n').length >= MIN_LINES_FOR_COMMIT_COLLAPSE;
- }
-
- _getOffsetHeight(element) {
- return element.offsetHeight;
- }
-
- _getScrollHeight(element) {
- return element.scrollHeight;
- }
-
- /**
- * Get the line height of an element to the nearest integer.
- */
- _getLineHeight(element) {
- const lineHeightStr = getComputedStyle(element).lineHeight;
- return Math.round(lineHeightStr.slice(0, lineHeightStr.length - 2));
- }
-
- /**
- * New max height for the related changes section, shorter than the existing
- * change info height.
- */
- _updateRelatedChangeMaxHeight() {
- // Takes into account approximate height for the expand button and
- // bottom margin.
- const EXTRA_HEIGHT = 30;
- let newHeight;
-
- if (window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_SMALL})`)
- .matches) {
- // In a small (mobile) view, give the relation chain some space.
- newHeight = SMALL_RELATED_HEIGHT;
- } else if (window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_MED})`)
- .matches) {
- // Since related changes are below the commit message, but still next to
- // metadata, the height should be the height of the metadata minus the
- // height of the commit message to reduce jank. However, if that doesn't
- // result in enough space, instead use the MINIMUM_RELATED_MAX_HEIGHT.
- // Note: extraHeight is to take into account margin/padding.
- const medRelatedHeight = Math.max(
- this._getOffsetHeight(this.$.mainChangeInfo) -
- this._getOffsetHeight(this.$.commitMessage) - 2 * EXTRA_HEIGHT,
- MINIMUM_RELATED_MAX_HEIGHT);
- newHeight = medRelatedHeight;
- } else {
- if (this._commitCollapsible) {
- // Make sure the content is lined up if both areas have buttons. If
- // the commit message is not collapsed, instead use the change info
- // height.
- newHeight = this._getOffsetHeight(this.$.commitMessage);
- } else {
- newHeight = this._getOffsetHeight(this.$.commitAndRelated) -
- EXTRA_HEIGHT;
- }
- }
- const stylesToUpdate = {};
-
- // Get the line height of related changes, and convert it to the nearest
- // integer.
- const lineHeight = this._getLineHeight(this.$.relatedChanges);
-
- // Figure out a new height that is divisible by the rounded line height.
- const remainder = newHeight % lineHeight;
- newHeight = newHeight - remainder;
-
- stylesToUpdate['--relation-chain-max-height'] = newHeight + 'px';
-
- // Update the max-height of the relation chain to this new height.
- if (this._commitCollapsible) {
- stylesToUpdate['--related-change-btn-top-padding'] = remainder + 'px';
- }
-
- this.updateStyles(stylesToUpdate);
- }
-
- _computeShowRelatedToggle() {
- // Make sure the max height has been applied, since there is now content
- // to populate.
- if (!util.getComputedStyleValue('--relation-chain-max-height', this)) {
- this._updateRelatedChangeMaxHeight();
- }
- // Prevents showMore from showing when click on related change, since the
- // line height would be positive, but related changes height is 0.
- if (!this._getScrollHeight(this.$.relatedChanges)) {
- return this._showRelatedToggle = false;
- }
-
- if (this._getScrollHeight(this.$.relatedChanges) >
- (this._getOffsetHeight(this.$.relatedChanges) +
- this._getLineHeight(this.$.relatedChanges))) {
- return this._showRelatedToggle = true;
- }
- this._showRelatedToggle = false;
- }
-
- _updateToggleContainerClass(showRelatedToggle) {
- if (showRelatedToggle) {
- this.$.relatedChangesToggle.classList.add('showToggle');
- } else {
- this.$.relatedChangesToggle.classList.remove('showToggle');
- }
- }
-
- _startUpdateCheckTimer() {
- if (!this._serverConfig ||
- !this._serverConfig.change ||
- this._serverConfig.change.update_delay === undefined ||
- this._serverConfig.change.update_delay <= MIN_CHECK_INTERVAL_SECS) {
- return;
- }
-
- this._updateCheckTimerHandle = this.async(() => {
- this.fetchChangeUpdates(this._change, this.$.restAPI).then(result => {
- let toastMessage = null;
- if (!result.isLatest) {
- toastMessage = ReloadToastMessage.NEWER_REVISION;
- } else if (result.newStatus === this.ChangeStatus.MERGED) {
- toastMessage = ReloadToastMessage.MERGED;
- } else if (result.newStatus === this.ChangeStatus.ABANDONED) {
- toastMessage = ReloadToastMessage.ABANDONED;
- } else if (result.newStatus === this.ChangeStatus.NEW) {
- toastMessage = ReloadToastMessage.RESTORED;
- } else if (result.newMessages) {
- toastMessage = ReloadToastMessage.NEW_MESSAGE;
- }
-
- if (!toastMessage) {
- this._startUpdateCheckTimer();
- return;
- }
-
- this._cancelUpdateCheckTimer();
- this.fire('show-alert', {
- message: toastMessage,
- // Persist this alert.
- dismissOnNavigation: true,
- action: 'Reload',
- callback: function() {
- // Load the current change without any patch range.
- Gerrit.Nav.navigateToChange(this._change);
- }.bind(this),
- });
- });
- }, this._serverConfig.change.update_delay * 1000);
- }
-
- _cancelUpdateCheckTimer() {
- if (this._updateCheckTimerHandle) {
- this.cancelAsync(this._updateCheckTimerHandle);
- }
- this._updateCheckTimerHandle = null;
- }
-
- _handleVisibilityChange() {
- if (document.hidden && this._updateCheckTimerHandle) {
- this._cancelUpdateCheckTimer();
- } else if (!this._updateCheckTimerHandle) {
- this._startUpdateCheckTimer();
- }
- }
-
- _handleTopicChanged() {
- this.$.relatedChanges.reload();
- }
-
- _computeHeaderClass(editMode) {
- const classes = ['header'];
- if (editMode) { classes.push('editMode'); }
- return classes.join(' ');
- }
-
- _computeEditMode(patchRangeRecord, paramsRecord) {
- if ([patchRangeRecord, paramsRecord].some(arg => arg === undefined)) {
- return undefined;
- }
-
- if (paramsRecord.base && paramsRecord.base.edit) { return true; }
-
- const patchRange = patchRangeRecord.base || {};
- return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME);
- }
-
- _handleFileActionTap(e) {
- e.preventDefault();
- const controls = this.$.fileListHeader.$.editControls;
- const path = e.detail.path;
- switch (e.detail.action) {
- case GrEditConstants.Actions.DELETE.id:
- controls.openDeleteDialog(path);
- break;
- case GrEditConstants.Actions.OPEN.id:
- Gerrit.Nav.navigateToRelativeUrl(
- Gerrit.Nav.getEditUrlForDiff(this._change, path,
- this._patchRange.patchNum));
- break;
- case GrEditConstants.Actions.RENAME.id:
- controls.openRenameDialog(path);
- break;
- case GrEditConstants.Actions.RESTORE.id:
- controls.openRestoreDialog(path);
- break;
- }
- }
-
- _computeCommitMessageKey(number, revision) {
- return `c${number}_rev${revision}`;
- }
-
- _patchNumChanged(patchNumStr) {
- if (!this._selectedRevision) {
- return;
- }
-
- let patchNum = parseInt(patchNumStr, 10);
- if (patchNumStr === 'edit') {
- patchNum = patchNumStr;
- }
-
- if (patchNum === this._selectedRevision._number) {
- return;
- }
- this._selectedRevision = Object.values(this._change.revisions).find(
- revision => revision._number === patchNum);
- }
-
- /**
- * If an edit exists already, load it. Otherwise, toggle edit mode via the
- * navigation API.
- */
- _handleEditTap() {
- const editInfo = Object.values(this._change.revisions).find(info =>
- info._number === this.EDIT_NAME);
-
- if (editInfo) {
- Gerrit.Nav.navigateToChange(this._change, this.EDIT_NAME);
- return;
- }
-
- // Avoid putting patch set in the URL unless a non-latest patch set is
- // selected.
- let patchNum;
- if (!this.patchNumEquals(this._patchRange.patchNum,
- this.computeLatestPatchNum(this._allPatchSets))) {
- patchNum = this._patchRange.patchNum;
- }
- Gerrit.Nav.navigateToChange(this._change, patchNum, null, true);
- }
-
- _handleStopEditTap() {
- Gerrit.Nav.navigateToChange(this._change, this._patchRange.patchNum);
- }
-
- _resetReplyOverlayFocusStops() {
- this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops());
- }
-
- _handleToggleStar(e) {
- this.$.restAPI.saveChangeStarred(e.detail.change._number,
- e.detail.starred);
- }
-
- _getRevisionInfo(change) {
- return new Gerrit.RevisionInfo(change);
- }
-
- _computeCurrentRevision(currentRevision, revisions) {
- return currentRevision && revisions && revisions[currentRevision];
- }
-
- _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) {
- return disableDiffPrefs || !loggedIn;
+ if (this._updateCheckTimerHandle) {
+ this._cancelUpdateCheckTimer();
}
}
- customElements.define(GrChangeView.is, GrChangeView);
-})();
+ get messagesList() {
+ return this.shadowRoot.querySelector('gr-messages-list');
+ }
+
+ get threadList() {
+ return this.shadowRoot.querySelector('gr-thread-list');
+ }
+
+ /**
+ * @param {boolean=} opt_reset
+ */
+ _setDiffViewMode(opt_reset) {
+ if (!opt_reset && this.viewState.diffViewMode) { return; }
+
+ return this._getPreferences()
+ .then( prefs => {
+ if (!this.viewState.diffMode) {
+ this.set('viewState.diffMode', prefs.default_diff_view);
+ }
+ })
+ .then(() => {
+ if (!this.viewState.diffMode) {
+ this.set('viewState.diffMode', 'SIDE_BY_SIDE');
+ }
+ });
+ }
+
+ _onOpenFixPreview(e) {
+ this.$.applyFixDialog.open(e);
+ }
+
+ _onCloseFixPreview(e) {
+ this._reload();
+ }
+
+ _handleToggleDiffMode(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ if (this.viewState.diffMode === DiffViewMode.SIDE_BY_SIDE) {
+ this.$.fileListHeader.setDiffViewMode(DiffViewMode.UNIFIED);
+ } else {
+ this.$.fileListHeader.setDiffViewMode(DiffViewMode.SIDE_BY_SIDE);
+ }
+ }
+
+ _handleCommentTabChange() {
+ this._currentView = this.$.commentTabs.selected;
+ const type = Object.keys(CommentTabs).find(key => CommentTabs[key] ===
+ this._currentView);
+ this.$.reporting.reportInteraction('comment-tab-changed', {tabName:
+ type});
+ }
+
+ _isSelectedView(currentView, view) {
+ return currentView === view;
+ }
+
+ _findIfTabMatches(currentTab, tab) {
+ return currentTab === tab;
+ }
+
+ _handleFileTabChange(e) {
+ const selectedIndex = e.target.selected;
+ const tabs = e.target.querySelectorAll('paper-tab');
+ this._currentTabName = tabs[selectedIndex] &&
+ tabs[selectedIndex].dataset.name;
+ const source = e && e.type ? e.type : '';
+ const pluginIndex = (this._dynamicTabHeaderEndpoints || []).indexOf(
+ this._currentTabName);
+ if (pluginIndex !== -1) {
+ this._selectedTabPluginEndpoint = this._dynamicTabContentEndpoints[
+ pluginIndex];
+ this._selectedTabPluginHeader = this._dynamicTabHeaderEndpoints[
+ pluginIndex];
+ } else {
+ this._selectedTabPluginEndpoint = '';
+ this._selectedTabPluginHeader = '';
+ }
+ this.$.reporting.reportInteraction('tab-changed',
+ {tabName: this._currentTabName, source});
+ }
+
+ _handleShowTab(e) {
+ const primaryTabs = this.shadowRoot.querySelector('#primaryTabs');
+ const tabs = primaryTabs.querySelectorAll('paper-tab');
+ let idx = -1;
+ tabs.forEach((tab, index) => {
+ if (tab.dataset.name === e.detail.tab) idx = index;
+ });
+ if (idx === -1) {
+ console.error(e.detail.tab + ' tab not found');
+ return;
+ }
+ primaryTabs.selected = idx;
+ primaryTabs.scrollIntoView();
+ this.$.reporting.reportInteraction('show-tab', {tabName: e.detail.tab});
+ }
+
+ _handleEditCommitMessage(e) {
+ this._editingCommitMessage = true;
+ this.$.commitMessageEditor.focusTextarea();
+ }
+
+ _handleCommitMessageSave(e) {
+ // Trim trailing whitespace from each line.
+ const message = e.detail.content.replace(TRAILING_WHITESPACE_REGEX, '');
+
+ this.$.jsAPI.handleCommitMessage(this._change, message);
+
+ this.$.commitMessageEditor.disabled = true;
+ this.$.restAPI.putChangeCommitMessage(
+ this._changeNum, message).then(resp => {
+ this.$.commitMessageEditor.disabled = false;
+ if (!resp.ok) { return; }
+
+ this._latestCommitMessage = this._prepareCommitMsgForLinkify(
+ message);
+ this._editingCommitMessage = false;
+ this._reloadWindow();
+ })
+ .catch(err => {
+ this.$.commitMessageEditor.disabled = false;
+ });
+ }
+
+ _reloadWindow() {
+ window.location.reload();
+ }
+
+ _handleCommitMessageCancel(e) {
+ this._editingCommitMessage = false;
+ }
+
+ _computeChangeStatusChips(change, mergeable, submitEnabled) {
+ // Polymer 2: check for undefined
+ if ([
+ change,
+ mergeable,
+ ].some(arg => arg === undefined)) {
+ // To keep consistent with Polymer 1, we are returning undefined
+ // if not all dependencies are defined
+ return undefined;
+ }
+
+ // Show no chips until mergeability is loaded.
+ if (mergeable === null) {
+ return [];
+ }
+
+ const options = {
+ includeDerived: true,
+ mergeable: !!mergeable,
+ submitEnabled: !!submitEnabled,
+ };
+ return this.changeStatuses(change, options);
+ }
+
+ _computeHideEditCommitMessage(
+ loggedIn, editing, change, editMode, collapsed, collapsible) {
+ if (!loggedIn || editing ||
+ (change && change.status === this.ChangeStatus.MERGED) ||
+ editMode ||
+ (collapsed && collapsible)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ _robotCommentCountPerPatchSet(threads) {
+ return threads.reduce((robotCommentCountMap, thread) => {
+ const comments = thread.comments;
+ const robotCommentsCount = comments.reduce((acc, comment) =>
+ (comment.robot_id ? acc + 1 : acc), 0);
+ robotCommentCountMap[comments[0].patch_set] =
+ (robotCommentCountMap[comments[0].patch_set] || 0) +
+ robotCommentsCount;
+ return robotCommentCountMap;
+ }, {});
+ }
+
+ _computeText(patch, commentThreads) {
+ const commentCount = this._robotCommentCountPerPatchSet(commentThreads);
+ const commentCnt = commentCount[patch._number] || 0;
+ if (commentCnt === 0) return `Patchset ${patch._number}`;
+ const findingsText = commentCnt === 1 ? 'finding' : 'findings';
+ return `Patchset ${patch._number}`
+ + ` (${commentCnt} ${findingsText})`;
+ }
+
+ _computeRobotCommentsPatchSetDropdownItems(change, commentThreads) {
+ if (!change || !commentThreads || !change.revisions) return [];
+
+ return Object.values(change.revisions)
+ .filter(patch => patch._number !== 'edit')
+ .map(patch => {
+ return {
+ text: this._computeText(patch, commentThreads),
+ value: patch._number,
+ };
+ })
+ .sort((a, b) => b.value - a.value);
+ }
+
+ _handleCurrentRevisionUpdate(currentRevision) {
+ this._currentRobotCommentsPatchSet = currentRevision._number;
+ }
+
+ _handleRobotCommentPatchSetChanged(e) {
+ const patchSet = parseInt(e.detail.value);
+ if (patchSet === this._currentRobotCommentsPatchSet) return;
+ this._currentRobotCommentsPatchSet = patchSet;
+ }
+
+ _computeShowText(showAllRobotComments) {
+ return showAllRobotComments ? 'Show Less' : 'Show more';
+ }
+
+ _toggleShowRobotComments() {
+ this._showAllRobotComments = !this._showAllRobotComments;
+ }
+
+ _computeRobotCommentThreads(commentThreads, currentRobotCommentsPatchSet,
+ showAllRobotComments) {
+ if (!commentThreads || !currentRobotCommentsPatchSet) return [];
+ const threads = commentThreads.filter(thread => {
+ const comments = thread.comments || [];
+ return comments.length && comments[0].robot_id && (comments[0].patch_set
+ === currentRobotCommentsPatchSet);
+ });
+ this._showRobotCommentsButton = threads.length > ROBOT_COMMENTS_LIMIT;
+ return threads.slice(0, showAllRobotComments ? undefined :
+ ROBOT_COMMENTS_LIMIT);
+ }
+
+ _handleReloadCommentThreads() {
+ // Get any new drafts that have been saved in the diff view and show
+ // in the comment thread view.
+ this._reloadDrafts().then(() => {
+ this._commentThreads = this._changeComments.getAllThreadsForChange()
+ .map(c => Object.assign({}, c));
+ flush();
+ });
+ }
+
+ _handleReloadDiffComments(e) {
+ // Keeps the file list counts updated.
+ this._reloadDrafts().then(() => {
+ // Get any new drafts that have been saved in the thread view and show
+ // in the diff view.
+ this.$.fileList.reloadCommentsForThreadWithRootId(e.detail.rootId,
+ e.detail.path);
+ flush();
+ });
+ }
+
+ _computeTotalCommentCounts(unresolvedCount, changeComments) {
+ if (!changeComments) return undefined;
+ const draftCount = changeComments.computeDraftCount();
+ const unresolvedString = GrCountStringFormatter.computeString(
+ unresolvedCount, 'unresolved');
+ const draftString = GrCountStringFormatter.computePluralString(
+ draftCount, 'draft');
+
+ return unresolvedString +
+ // Add a comma and space if both unresolved and draft comments exist.
+ (unresolvedString && draftString ? ', ' : '') +
+ draftString;
+ }
+
+ _handleCommentSave(e) {
+ const draft = e.detail.comment;
+ if (!draft.__draft) { return; }
+
+ draft.patch_set = draft.patch_set || this._patchRange.patchNum;
+
+ // The use of path-based notification helpers (set, push) can’t be used
+ // because the paths could contain dots in them. A new object must be
+ // created to satisfy Polymer’s dirty checking.
+ // https://github.com/Polymer/polymer/issues/3127
+ const diffDrafts = Object.assign({}, this._diffDrafts);
+ if (!diffDrafts[draft.path]) {
+ diffDrafts[draft.path] = [draft];
+ this._diffDrafts = diffDrafts;
+ return;
+ }
+ for (let i = 0; i < this._diffDrafts[draft.path].length; i++) {
+ if (this._diffDrafts[draft.path][i].id === draft.id) {
+ diffDrafts[draft.path][i] = draft;
+ this._diffDrafts = diffDrafts;
+ return;
+ }
+ }
+ diffDrafts[draft.path].push(draft);
+ diffDrafts[draft.path].sort((c1, c2) =>
+ // No line number means that it’s a file comment. Sort it above the
+ // others.
+ (c1.line || -1) - (c2.line || -1)
+ );
+ this._diffDrafts = diffDrafts;
+ }
+
+ _handleCommentDiscard(e) {
+ const draft = e.detail.comment;
+ if (!draft.__draft) { return; }
+
+ if (!this._diffDrafts[draft.path]) {
+ return;
+ }
+ let index = -1;
+ for (let i = 0; i < this._diffDrafts[draft.path].length; i++) {
+ if (this._diffDrafts[draft.path][i].id === draft.id) {
+ index = i;
+ break;
+ }
+ }
+ if (index === -1) {
+ // It may be a draft that hasn’t been added to _diffDrafts since it was
+ // never saved.
+ return;
+ }
+
+ draft.patch_set = draft.patch_set || this._patchRange.patchNum;
+
+ // The use of path-based notification helpers (set, push) can’t be used
+ // because the paths could contain dots in them. A new object must be
+ // created to satisfy Polymer’s dirty checking.
+ // https://github.com/Polymer/polymer/issues/3127
+ const diffDrafts = Object.assign({}, this._diffDrafts);
+ diffDrafts[draft.path].splice(index, 1);
+ if (diffDrafts[draft.path].length === 0) {
+ delete diffDrafts[draft.path];
+ }
+ this._diffDrafts = diffDrafts;
+ }
+
+ _handleReplyTap(e) {
+ e.preventDefault();
+ this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+ }
+
+ _handleOpenDiffPrefs() {
+ this.$.fileList.openDiffPrefs();
+ }
+
+ _handleOpenIncludedInDialog() {
+ this.$.includedInDialog.loadData().then(() => {
+ flush();
+ this.$.includedInOverlay.refit();
+ });
+ this.$.includedInOverlay.open();
+ }
+
+ _handleIncludedInDialogClose(e) {
+ this.$.includedInOverlay.close();
+ }
+
+ _handleOpenDownloadDialog() {
+ this.$.downloadOverlay.open().then(() => {
+ this.$.downloadOverlay
+ .setFocusStops(this.$.downloadDialog.getFocusStops());
+ this.$.downloadDialog.focus();
+ });
+ }
+
+ _handleDownloadDialogClose(e) {
+ this.$.downloadOverlay.close();
+ }
+
+ _handleOpenUploadHelpDialog(e) {
+ this.$.uploadHelpOverlay.open();
+ }
+
+ _handleCloseUploadHelpDialog(e) {
+ this.$.uploadHelpOverlay.close();
+ }
+
+ _handleMessageReply(e) {
+ const msg = e.detail.message.message;
+ const quoteStr = msg.split('\n').map(
+ line => '> ' + line)
+ .join('\n') + '\n\n';
+ this.$.replyDialog.quote = quoteStr;
+ this._openReplyDialog(this.$.replyDialog.FocusTarget.BODY);
+ }
+
+ _handleHideBackgroundContent() {
+ this.$.mainContent.classList.add('overlayOpen');
+ }
+
+ _handleShowBackgroundContent() {
+ this.$.mainContent.classList.remove('overlayOpen');
+ }
+
+ _handleReplySent(e) {
+ this.addEventListener('change-details-loaded',
+ () => {
+ this.$.reporting.timeEnd(SEND_REPLY_TIMING_LABEL);
+ }, {once: true});
+ this.$.replyOverlay.close();
+ this._reload();
+ }
+
+ _handleReplyCancel(e) {
+ this.$.replyOverlay.close();
+ }
+
+ _handleReplyAutogrow(e) {
+ // If the textarea resizes, we need to re-fit the overlay.
+ this.debounce('reply-overlay-refit', () => {
+ this.$.replyOverlay.refit();
+ }, REPLY_REFIT_DEBOUNCE_INTERVAL_MS);
+ }
+
+ _handleShowReplyDialog(e) {
+ let target = this.$.replyDialog.FocusTarget.REVIEWERS;
+ if (e.detail.value && e.detail.value.ccsOnly) {
+ target = this.$.replyDialog.FocusTarget.CCS;
+ }
+ this._openReplyDialog(target);
+ }
+
+ _handleScroll() {
+ this.debounce('scroll', () => {
+ this.viewState.scrollTop = document.body.scrollTop;
+ }, 150);
+ }
+
+ _setShownFiles(e) {
+ this._shownFileCount = e.detail.length;
+ }
+
+ _expandAllDiffs() {
+ this.$.fileList.expandAllDiffs();
+ }
+
+ _collapseAllDiffs() {
+ this.$.fileList.collapseAllDiffs();
+ }
+
+ _paramsChanged(value) {
+ this._currentView = CommentTabs.CHANGE_LOG;
+ this._setPrimaryTab();
+ if (value.view !== Gerrit.Nav.View.CHANGE) {
+ this._initialLoadComplete = false;
+ return;
+ }
+
+ if (value.changeNum && value.project) {
+ this.$.restAPI.setInProjectLookup(value.changeNum, value.project);
+ }
+
+ const patchChanged = this._patchRange &&
+ (value.patchNum !== undefined && value.basePatchNum !== undefined) &&
+ (this._patchRange.patchNum !== value.patchNum ||
+ this._patchRange.basePatchNum !== value.basePatchNum);
+
+ if (this._changeNum !== value.changeNum) {
+ this._initialLoadComplete = false;
+ }
+
+ const patchRange = {
+ patchNum: value.patchNum,
+ basePatchNum: value.basePatchNum || 'PARENT',
+ };
+
+ this.$.fileList.collapseAllDiffs();
+ this._patchRange = patchRange;
+
+ // If the change has already been loaded and the parameter change is only
+ // in the patch range, then don't do a full reload.
+ if (this._initialLoadComplete && patchChanged) {
+ if (patchRange.patchNum == null) {
+ patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets);
+ }
+ this._reloadPatchNumDependentResources().then(() => {
+ this._sendShowChangeEvent();
+ });
+ return;
+ }
+
+ this._changeNum = value.changeNum;
+ this.$.relatedChanges.clear();
+
+ this._reload(true).then(() => {
+ this._performPostLoadTasks();
+ });
+ }
+
+ _sendShowChangeEvent() {
+ this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, {
+ change: this._change,
+ patchNum: this._patchRange.patchNum,
+ info: {mergeable: this._mergeable},
+ });
+ }
+
+ _setPrimaryTab() {
+ // Selected has to be set after the paper-tabs are visible, because
+ // the selected underline depends on calculations made by the browser.
+ // paper-tabs depends on iron-resizable-behavior, which only fires on
+ // attached() without using RenderStatus.beforeNextRender. Not changing
+ // this when migrating from Polymer 1 to 2 was probably an oversight by
+ // the paper component maintainers.
+ // https://polymer-library.polymer-project.org/2.0/docs/upgrade#attach-time-attached-connectedcallback
+ // By calling _onTabSizingChanged() we are reaching into the private API
+ // of paper-tabs, but we believe this workaround is acceptable for the
+ // time being.
+ beforeNextRender(this, () => {
+ this.$.commentTabs.selected = 0;
+ this.$.commentTabs._onTabSizingChanged();
+ const primaryTabs = this.shadowRoot.querySelector('#primaryTabs');
+ if (primaryTabs) {
+ primaryTabs.selected = 0;
+ primaryTabs._onTabSizingChanged();
+ }
+ });
+ }
+
+ _performPostLoadTasks() {
+ this._maybeShowReplyDialog();
+ this._maybeShowRevertDialog();
+
+ this._sendShowChangeEvent();
+
+ this.async(() => {
+ if (this.viewState.scrollTop) {
+ document.documentElement.scrollTop =
+ document.body.scrollTop = this.viewState.scrollTop;
+ } else {
+ this._maybeScrollToMessage(window.location.hash);
+ }
+ this._initialLoadComplete = true;
+ });
+ }
+
+ _paramsAndChangeChanged(value, change) {
+ // Polymer 2: check for undefined
+ if ([value, change].some(arg => arg === undefined)) {
+ return;
+ }
+
+ // If the change number or patch range is different, then reset the
+ // selected file index.
+ const patchRangeState = this.viewState.patchRange;
+ if (this.viewState.changeNum !== this._changeNum ||
+ patchRangeState.basePatchNum !== this._patchRange.basePatchNum ||
+ patchRangeState.patchNum !== this._patchRange.patchNum) {
+ this._resetFileListViewState();
+ }
+ }
+
+ _viewStateChanged(viewState) {
+ this._numFilesShown = viewState.numFilesShown ?
+ viewState.numFilesShown : DEFAULT_NUM_FILES_SHOWN;
+ }
+
+ _numFilesShownChanged(numFilesShown) {
+ this.viewState.numFilesShown = numFilesShown;
+ }
+
+ _handleMessageAnchorTap(e) {
+ const hash = MSG_PREFIX + e.detail.id;
+ const url = Gerrit.Nav.getUrlForChange(this._change,
+ this._patchRange.patchNum, this._patchRange.basePatchNum,
+ this._editMode, hash);
+ history.replaceState(null, '', url);
+ }
+
+ _maybeScrollToMessage(hash) {
+ if (hash.startsWith(MSG_PREFIX)) {
+ this.messagesList.scrollToMessage(hash.substr(MSG_PREFIX.length));
+ }
+ }
+
+ _getLocationSearch() {
+ // Not inlining to make it easier to test.
+ return window.location.search;
+ }
+
+ _getUrlParameter(param) {
+ const pageURL = this._getLocationSearch().substring(1);
+ const vars = pageURL.split('&');
+ for (let i = 0; i < vars.length; i++) {
+ const name = vars[i].split('=');
+ if (name[0] == param) {
+ return name[0];
+ }
+ }
+ return null;
+ }
+
+ _maybeShowRevertDialog() {
+ Gerrit.awaitPluginsLoaded()
+ .then(this._getLoggedIn.bind(this))
+ .then(loggedIn => {
+ if (!loggedIn || !this._change ||
+ this._change.status !== this.ChangeStatus.MERGED) {
+ // Do not display dialog if not logged-in or the change is not
+ // merged.
+ return;
+ }
+ if (this._getUrlParameter('revert')) {
+ this.$.actions.showRevertDialog();
+ }
+ });
+ }
+
+ _maybeShowReplyDialog() {
+ this._getLoggedIn().then(loggedIn => {
+ if (!loggedIn) { return; }
+
+ if (this.viewState.showReplyDialog) {
+ this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+ // TODO(kaspern@): Find a better signal for when to call center.
+ this.async(() => { this.$.replyOverlay.center(); }, 100);
+ this.async(() => { this.$.replyOverlay.center(); }, 1000);
+ this.set('viewState.showReplyDialog', false);
+ }
+ });
+ }
+
+ _resetFileListViewState() {
+ this.set('viewState.selectedFileIndex', 0);
+ this.set('viewState.scrollTop', 0);
+ if (!!this.viewState.changeNum &&
+ this.viewState.changeNum !== this._changeNum) {
+ // Reset the diff mode to null when navigating from one change to
+ // another, so that the user's preference is restored.
+ this._setDiffViewMode(true);
+ this.set('_numFilesShown', DEFAULT_NUM_FILES_SHOWN);
+ }
+ this.set('viewState.changeNum', this._changeNum);
+ this.set('viewState.patchRange', this._patchRange);
+ }
+
+ _changeChanged(change) {
+ if (!change || !this._patchRange || !this._allPatchSets) { return; }
+
+ // We get the parent first so we keep the original value for basePatchNum
+ // and not the updated value.
+ const parent = this._getBasePatchNum(change, this._patchRange);
+
+ this.set('_patchRange.patchNum', this._patchRange.patchNum ||
+ this.computeLatestPatchNum(this._allPatchSets));
+
+ this.set('_patchRange.basePatchNum', parent);
+
+ const title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
+ this.fire('title-change', {title});
+ }
+
+ /**
+ * Gets base patch number, if it is a parent try and decide from
+ * preference whether to default to `auto merge`, `Parent 1` or `PARENT`.
+ *
+ * @param {Object} change
+ * @param {Object} patchRange
+ * @return {number|string}
+ */
+ _getBasePatchNum(change, patchRange) {
+ if (patchRange.basePatchNum &&
+ patchRange.basePatchNum !== 'PARENT') {
+ return patchRange.basePatchNum;
+ }
+
+ const revisionInfo = this._getRevisionInfo(change);
+ if (!revisionInfo) return 'PARENT';
+
+ const parentCounts = revisionInfo.getParentCountMap();
+ // check that there is at least 2 parents otherwise fall back to 1,
+ // which means there is only one parent.
+ const parentCount = parentCounts.hasOwnProperty(1) ?
+ parentCounts[1] : 1;
+
+ const preferFirst = this._prefs &&
+ this._prefs.default_base_for_merges === 'FIRST_PARENT';
+
+ if (parentCount > 1 && preferFirst && !patchRange.patchNum) {
+ return -1;
+ }
+
+ return 'PARENT';
+ }
+
+ _computeChangeUrl(change) {
+ return Gerrit.Nav.getUrlForChange(change);
+ }
+
+ _computeShowCommitInfo(changeStatus, current_revision) {
+ return changeStatus === 'Merged' && current_revision;
+ }
+
+ _computeMergedCommitInfo(current_revision, revisions) {
+ const rev = revisions[current_revision];
+ if (!rev || !rev.commit) { return {}; }
+ // CommitInfo.commit is optional. Set commit in all cases to avoid error
+ // in <gr-commit-info>. @see Issue 5337
+ if (!rev.commit.commit) { rev.commit.commit = current_revision; }
+ return rev.commit;
+ }
+
+ _computeChangeIdClass(displayChangeId) {
+ return displayChangeId === CHANGE_ID_ERROR.MISMATCH ? 'warning' : '';
+ }
+
+ _computeTitleAttributeWarning(displayChangeId) {
+ if (displayChangeId === CHANGE_ID_ERROR.MISMATCH) {
+ return 'Change-Id mismatch';
+ } else if (displayChangeId === CHANGE_ID_ERROR.MISSING) {
+ return 'No Change-Id in commit message';
+ }
+ }
+
+ _computeChangeIdCommitMessageError(commitMessage, change) {
+ // Polymer 2: check for undefined
+ if ([commitMessage, change].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ if (!commitMessage) { return CHANGE_ID_ERROR.MISSING; }
+
+ // Find the last match in the commit message:
+ let changeId;
+ let changeIdArr;
+
+ while (changeIdArr = CHANGE_ID_REGEX_PATTERN.exec(commitMessage)) {
+ changeId = changeIdArr[1];
+ }
+
+ if (changeId) {
+ // A change-id is detected in the commit message.
+
+ if (changeId === change.change_id) {
+ // The change-id found matches the real change-id.
+ return null;
+ }
+ // The change-id found does not match the change-id.
+ return CHANGE_ID_ERROR.MISMATCH;
+ }
+ // There is no change-id in the commit message.
+ return CHANGE_ID_ERROR.MISSING;
+ }
+
+ _computeLabelNames(labels) {
+ return Object.keys(labels).sort();
+ }
+
+ _computeLabelValues(labelName, labels) {
+ const result = [];
+ const t = labels[labelName];
+ if (!t) { return result; }
+ const approvals = t.all || [];
+ for (const label of approvals) {
+ if (label.value && label.value != labels[labelName].default_value) {
+ let labelClassName;
+ let labelValPrefix = '';
+ if (label.value > 0) {
+ labelValPrefix = '+';
+ labelClassName = 'approved';
+ } else if (label.value < 0) {
+ labelClassName = 'notApproved';
+ }
+ result.push({
+ value: labelValPrefix + label.value,
+ className: labelClassName,
+ account: label,
+ });
+ }
+ }
+ return result;
+ }
+
+ _computeReplyButtonLabel(changeRecord, canStartReview) {
+ // Polymer 2: check for undefined
+ if ([changeRecord, canStartReview].some(arg => arg === undefined)) {
+ return 'Reply';
+ }
+
+ if (canStartReview) {
+ return 'Start review';
+ }
+
+ const drafts = (changeRecord && changeRecord.base) || {};
+ const draftCount = Object.keys(drafts)
+ .reduce((count, file) => count + drafts[file].length, 0);
+
+ let label = 'Reply';
+ if (draftCount > 0) {
+ label += ' (' + draftCount + ')';
+ }
+ return label;
+ }
+
+ _handleOpenReplyDialog(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) {
+ return;
+ }
+ this._getLoggedIn().then(isLoggedIn => {
+ if (!isLoggedIn) {
+ this.fire('show-auth-required');
+ return;
+ }
+
+ e.preventDefault();
+ this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+ });
+ }
+
+ _handleOpenDownloadDialogShortcut(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this.$.downloadOverlay.open();
+ }
+
+ _handleEditTopic(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this.$.metadata.editTopic();
+ }
+
+ _handleRefreshChange(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+ e.preventDefault();
+ Gerrit.Nav.navigateToChange(this._change);
+ }
+
+ _handleToggleChangeStar(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this.$.changeStar.toggleStar();
+ }
+
+ _handleUpToDashboard(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this._determinePageBack();
+ }
+
+ _handleExpandAllMessages(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this.messagesList.handleExpandCollapse(true);
+ }
+
+ _handleCollapseAllMessages(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this.messagesList.handleExpandCollapse(false);
+ }
+
+ _handleOpenDiffPrefsShortcut(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ if (this._diffPrefsDisabled) { return; }
+
+ e.preventDefault();
+ this.$.fileList.openDiffPrefs();
+ }
+
+ _determinePageBack() {
+ // Default backPage to root if user came to change view page
+ // via an email link, etc.
+ Gerrit.Nav.navigateToRelativeUrl(this.backPage ||
+ Gerrit.Nav.getUrlForRoot());
+ }
+
+ _handleLabelRemoved(splices, path) {
+ for (const splice of splices) {
+ for (const removed of splice.removed) {
+ const changePath = path.split('.');
+ const labelPath = changePath.splice(0, changePath.length - 2);
+ const labelDict = this.get(labelPath);
+ if (labelDict.approved &&
+ labelDict.approved._account_id === removed._account_id) {
+ this._reload();
+ return;
+ }
+ }
+ }
+ }
+
+ _labelsChanged(changeRecord) {
+ if (!changeRecord) { return; }
+ if (changeRecord.value && changeRecord.value.indexSplices) {
+ this._handleLabelRemoved(changeRecord.value.indexSplices,
+ changeRecord.path);
+ }
+ this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.LABEL_CHANGE, {
+ change: this._change,
+ });
+ }
+
+ /**
+ * @param {string=} opt_section
+ */
+ _openReplyDialog(opt_section) {
+ this.$.replyOverlay.open().finally(() => {
+ // the following code should be executed no matter open succeed or not
+ this._resetReplyOverlayFocusStops();
+ this.$.replyDialog.open(opt_section);
+ flush();
+ this.$.replyOverlay.center();
+ });
+ }
+
+ _handleReloadChange(e) {
+ return this._reload().then(() => {
+ // If the change was rebased or submitted, we need to reload the page
+ // with the latest patch.
+ const action = e.detail.action;
+ if (action === 'rebase' || action === 'submit') {
+ Gerrit.Nav.navigateToChange(this._change);
+ }
+ });
+ }
+
+ _handleGetChangeDetailError(response) {
+ this.fire('page-error', {response});
+ }
+
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _getServerConfig() {
+ return this.$.restAPI.getConfig();
+ }
+
+ _getProjectConfig() {
+ if (!this._change) return;
+ return this.$.restAPI.getProjectConfig(this._change.project).then(
+ config => {
+ this._projectConfig = config;
+ });
+ }
+
+ _getPreferences() {
+ return this.$.restAPI.getPreferences();
+ }
+
+ _prepareCommitMsgForLinkify(msg) {
+ // TODO(wyatta) switch linkify sequence, see issue 5526.
+ // This is a zero-with space. It is added to prevent the linkify library
+ // from including R= or CC= as part of the email address.
+ return msg.replace(REVIEWERS_REGEX, '$1=\u200B');
+ }
+
+ /**
+ * Utility function to make the necessary modifications to a change in the
+ * case an edit exists.
+ *
+ * @param {!Object} change
+ * @param {?Object} edit
+ */
+ _processEdit(change, edit) {
+ if (!edit) { return; }
+ change.revisions[edit.commit.commit] = {
+ _number: this.EDIT_NAME,
+ basePatchNum: edit.base_patch_set_number,
+ commit: edit.commit,
+ fetch: edit.fetch,
+ };
+ // If the edit is based on the most recent patchset, load it by
+ // default, unless another patch set to load was specified in the URL.
+ if (!this._patchRange.patchNum &&
+ change.current_revision === edit.base_revision) {
+ change.current_revision = edit.commit.commit;
+ this.set('_patchRange.patchNum', this.EDIT_NAME);
+ // Because edits are fibbed as revisions and added to the revisions
+ // array, and revision actions are always derived from the 'latest'
+ // patch set, we must copy over actions from the patch set base.
+ // Context: Issue 7243
+ change.revisions[edit.commit.commit].actions =
+ change.revisions[edit.base_revision].actions;
+ }
+ }
+
+ _getChangeDetail() {
+ const detailCompletes = this.$.restAPI.getChangeDetail(
+ this._changeNum, this._handleGetChangeDetailError.bind(this));
+ const editCompletes = this._getEdit();
+ const prefCompletes = this._getPreferences();
+
+ return Promise.all([detailCompletes, editCompletes, prefCompletes])
+ .then(([change, edit, prefs]) => {
+ this._prefs = prefs;
+
+ if (!change) {
+ return '';
+ }
+ this._processEdit(change, edit);
+ // Issue 4190: Coalesce missing topics to null.
+ if (!change.topic) { change.topic = null; }
+ if (!change.reviewer_updates) {
+ change.reviewer_updates = null;
+ }
+ const latestRevisionSha = this._getLatestRevisionSHA(change);
+ const currentRevision = change.revisions[latestRevisionSha];
+ if (currentRevision.commit && currentRevision.commit.message) {
+ this._latestCommitMessage = this._prepareCommitMsgForLinkify(
+ currentRevision.commit.message);
+ } else {
+ this._latestCommitMessage = null;
+ }
+
+ const lineHeight = getComputedStyle(this).lineHeight;
+
+ // Slice returns a number as a string, convert to an int.
+ this._lineHeight =
+ parseInt(lineHeight.slice(0, lineHeight.length - 2), 10);
+
+ this._change = change;
+ if (!this._patchRange || !this._patchRange.patchNum ||
+ this.patchNumEquals(this._patchRange.patchNum,
+ currentRevision._number)) {
+ // CommitInfo.commit is optional, and may need patching.
+ if (!currentRevision.commit.commit) {
+ currentRevision.commit.commit = latestRevisionSha;
+ }
+ this._commitInfo = currentRevision.commit;
+ this._selectedRevision = currentRevision;
+ // TODO: Fetch and process files.
+ } else {
+ this._selectedRevision =
+ Object.values(this._change.revisions).find(
+ revision => {
+ // edit patchset is a special one
+ const thePatchNum = this._patchRange.patchNum;
+ if (thePatchNum === 'edit') {
+ return revision._number === thePatchNum;
+ }
+ return revision._number === parseInt(thePatchNum, 10);
+ });
+ }
+ });
+ }
+
+ _isSubmitEnabled(revisionActions) {
+ return !!(revisionActions && revisionActions.submit &&
+ revisionActions.submit.enabled);
+ }
+
+ _isParentCurrent(revisionActions) {
+ if (revisionActions && revisionActions.rebase) {
+ return !revisionActions.rebase.enabled;
+ } else {
+ return true;
+ }
+ }
+
+ _getEdit() {
+ return this.$.restAPI.getChangeEdit(this._changeNum, true);
+ }
+
+ _getLatestCommitMessage() {
+ return this.$.restAPI.getChangeCommitInfo(this._changeNum,
+ this.computeLatestPatchNum(this._allPatchSets)).then(commitInfo => {
+ if (!commitInfo) return Promise.resolve();
+ this._latestCommitMessage =
+ this._prepareCommitMsgForLinkify(commitInfo.message);
+ });
+ }
+
+ _getLatestRevisionSHA(change) {
+ if (change.current_revision) {
+ return change.current_revision;
+ }
+ // current_revision may not be present in the case where the latest rev is
+ // a draft and the user doesn’t have permission to view that rev.
+ let latestRev = null;
+ let latestPatchNum = -1;
+ for (const rev in change.revisions) {
+ if (!change.revisions.hasOwnProperty(rev)) { continue; }
+
+ if (change.revisions[rev]._number > latestPatchNum) {
+ latestRev = rev;
+ latestPatchNum = change.revisions[rev]._number;
+ }
+ }
+ return latestRev;
+ }
+
+ _getCommitInfo() {
+ return this.$.restAPI.getChangeCommitInfo(
+ this._changeNum, this._patchRange.patchNum).then(
+ commitInfo => {
+ this._commitInfo = commitInfo;
+ });
+ }
+
+ _reloadDraftsWithCallback(e) {
+ return this._reloadDrafts().then(() => e.detail.resolve());
+ }
+
+ /**
+ * Fetches a new changeComment object, and data for all types of comments
+ * (comments, robot comments, draft comments) is requested.
+ */
+ _reloadComments() {
+ return this.$.commentAPI.loadAll(this._changeNum)
+ .then(comments => this._recomputeComments(comments));
+ }
+
+ /**
+ * Fetches a new changeComment object, but only updated data for drafts is
+ * requested.
+ *
+ * TODO(taoalpha): clean up this and _reloadComments, as single comment
+ * can be a thread so it does not make sense to only update drafts
+ * without updating threads
+ */
+ _reloadDrafts() {
+ return this.$.commentAPI.reloadDrafts(this._changeNum)
+ .then(comments => this._recomputeComments(comments));
+ }
+
+ _recomputeComments(comments) {
+ this._changeComments = comments;
+ this._diffDrafts = Object.assign({}, this._changeComments.drafts);
+ this._commentThreads = this._changeComments.getAllThreadsForChange()
+ .map(c => Object.assign({}, c));
+ this._draftCommentThreads = this._commentThreads
+ .filter(c => c.comments[c.comments.length - 1].__draft);
+ }
+
+ /**
+ * Reload the change.
+ *
+ * @param {boolean=} opt_isLocationChange Reloads the related changes
+ * when true and ends reporting events that started on location change.
+ * @return {Promise} A promise that resolves when the core data has loaded.
+ * Some non-core data loading may still be in-flight when the core data
+ * promise resolves.
+ */
+ _reload(opt_isLocationChange) {
+ this._loading = true;
+ this._relatedChangesCollapsed = true;
+ this.$.reporting.time(CHANGE_RELOAD_TIMING_LABEL);
+ this.$.reporting.time(CHANGE_DATA_TIMING_LABEL);
+
+ // Array to house all promises related to data requests.
+ const allDataPromises = [];
+
+ // Resolves when the change detail and the edit patch set (if available)
+ // are loaded.
+ const detailCompletes = this._getChangeDetail();
+ allDataPromises.push(detailCompletes);
+
+ // Resolves when the loading flag is set to false, meaning that some
+ // change content may start appearing.
+ const loadingFlagSet = detailCompletes
+ .then(() => {
+ this._loading = false;
+ this.dispatchEvent(new CustomEvent('change-details-loaded',
+ {bubbles: true, composed: true}));
+ })
+ .then(() => {
+ this.$.reporting.timeEnd(CHANGE_RELOAD_TIMING_LABEL);
+ if (opt_isLocationChange) {
+ this.$.reporting.changeDisplayed();
+ }
+ });
+
+ // Resolves when the project config has loaded.
+ const projectConfigLoaded = detailCompletes
+ .then(() => this._getProjectConfig());
+ allDataPromises.push(projectConfigLoaded);
+
+ // Resolves when change comments have loaded (comments, drafts and robot
+ // comments).
+ const commentsLoaded = this._reloadComments();
+ allDataPromises.push(commentsLoaded);
+
+ let coreDataPromise;
+
+ // If the patch number is specified
+ if (this._patchRange && this._patchRange.patchNum) {
+ // Because a specific patchset is specified, reload the resources that
+ // are keyed by patch number or patch range.
+ const patchResourcesLoaded = this._reloadPatchNumDependentResources();
+ allDataPromises.push(patchResourcesLoaded);
+
+ // Promise resolves when the change detail and patch dependent resources
+ // have loaded.
+ const detailAndPatchResourcesLoaded =
+ Promise.all([patchResourcesLoaded, loadingFlagSet]);
+
+ // Promise resolves when mergeability information has loaded.
+ const mergeabilityLoaded = detailAndPatchResourcesLoaded
+ .then(() => this._getMergeability());
+ allDataPromises.push(mergeabilityLoaded);
+
+ // Promise resovles when the change actions have loaded.
+ const actionsLoaded = detailAndPatchResourcesLoaded
+ .then(() => this.$.actions.reload());
+ allDataPromises.push(actionsLoaded);
+
+ // The core data is loaded when both mergeability and actions are known.
+ coreDataPromise = Promise.all([mergeabilityLoaded, actionsLoaded]);
+ } else {
+ // Resolves when the file list has loaded.
+ const fileListReload = loadingFlagSet
+ .then(() => this.$.fileList.reload());
+ allDataPromises.push(fileListReload);
+
+ const latestCommitMessageLoaded = loadingFlagSet.then(() => {
+ // If the latest commit message is known, there is nothing to do.
+ if (this._latestCommitMessage) { return Promise.resolve(); }
+ return this._getLatestCommitMessage();
+ });
+ allDataPromises.push(latestCommitMessageLoaded);
+
+ // Promise resolves when mergeability information has loaded.
+ const mergeabilityLoaded = loadingFlagSet
+ .then(() => this._getMergeability());
+ allDataPromises.push(mergeabilityLoaded);
+
+ // Core data is loaded when mergeability has been loaded.
+ coreDataPromise = mergeabilityLoaded;
+ }
+
+ if (opt_isLocationChange) {
+ const relatedChangesLoaded = coreDataPromise
+ .then(() => this.$.relatedChanges.reload());
+ allDataPromises.push(relatedChangesLoaded);
+ }
+
+ Promise.all(allDataPromises).then(() => {
+ this.$.reporting.timeEnd(CHANGE_DATA_TIMING_LABEL);
+ if (opt_isLocationChange) {
+ this.$.reporting.changeFullyLoaded();
+ }
+ });
+
+ return coreDataPromise;
+ }
+
+ /**
+ * Kicks off requests for resources that rely on the patch range
+ * (`this._patchRange`) being defined.
+ */
+ _reloadPatchNumDependentResources() {
+ return Promise.all([
+ this._getCommitInfo(),
+ this.$.fileList.reload(),
+ ]);
+ }
+
+ _getMergeability() {
+ if (!this._change) {
+ this._mergeable = null;
+ return Promise.resolve();
+ }
+ // If the change is closed, it is not mergeable. Note: already merged
+ // changes are obviously not mergeable, but the mergeability API will not
+ // answer for abandoned changes.
+ if (this._change.status === this.ChangeStatus.MERGED ||
+ this._change.status === this.ChangeStatus.ABANDONED) {
+ this._mergeable = false;
+ return Promise.resolve();
+ }
+
+ this._mergeable = null;
+ return this.$.restAPI.getMergeable(this._changeNum).then(m => {
+ this._mergeable = m.mergeable;
+ });
+ }
+
+ _computeCanStartReview(change) {
+ return !!(change.actions && change.actions.ready &&
+ change.actions.ready.enabled);
+ }
+
+ _computeReplyDisabled() { return false; }
+
+ _computeChangePermalinkAriaLabel(changeNum) {
+ return 'Change ' + changeNum;
+ }
+
+ _computeCommitMessageCollapsed(collapsed, collapsible) {
+ return collapsible && collapsed;
+ }
+
+ _computeRelatedChangesClass(collapsed) {
+ return collapsed ? 'collapsed' : '';
+ }
+
+ _computeCollapseText(collapsed) {
+ // Symbols are up and down triangles.
+ return collapsed ? '\u25bc Show more' : '\u25b2 Show less';
+ }
+
+ /**
+ * Returns the text to be copied when
+ * click the copy icon next to change subject
+ *
+ * @param {!Object} change
+ */
+ _computeCopyTextForTitle(change) {
+ return `${change._number}: ${change.subject} | ` +
+ `${location.protocol}//${location.host}` +
+ `${this._computeChangeUrl(change)}`;
+ }
+
+ _toggleCommitCollapsed() {
+ this._commitCollapsed = !this._commitCollapsed;
+ if (this._commitCollapsed) {
+ window.scrollTo(0, 0);
+ }
+ }
+
+ _toggleRelatedChangesCollapsed() {
+ this._relatedChangesCollapsed = !this._relatedChangesCollapsed;
+ if (this._relatedChangesCollapsed) {
+ window.scrollTo(0, 0);
+ }
+ }
+
+ _computeCommitCollapsible(commitMessage) {
+ if (!commitMessage) { return false; }
+ return commitMessage.split('\n').length >= MIN_LINES_FOR_COMMIT_COLLAPSE;
+ }
+
+ _getOffsetHeight(element) {
+ return element.offsetHeight;
+ }
+
+ _getScrollHeight(element) {
+ return element.scrollHeight;
+ }
+
+ /**
+ * Get the line height of an element to the nearest integer.
+ */
+ _getLineHeight(element) {
+ const lineHeightStr = getComputedStyle(element).lineHeight;
+ return Math.round(lineHeightStr.slice(0, lineHeightStr.length - 2));
+ }
+
+ /**
+ * New max height for the related changes section, shorter than the existing
+ * change info height.
+ */
+ _updateRelatedChangeMaxHeight() {
+ // Takes into account approximate height for the expand button and
+ // bottom margin.
+ const EXTRA_HEIGHT = 30;
+ let newHeight;
+
+ if (window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_SMALL})`)
+ .matches) {
+ // In a small (mobile) view, give the relation chain some space.
+ newHeight = SMALL_RELATED_HEIGHT;
+ } else if (window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_MED})`)
+ .matches) {
+ // Since related changes are below the commit message, but still next to
+ // metadata, the height should be the height of the metadata minus the
+ // height of the commit message to reduce jank. However, if that doesn't
+ // result in enough space, instead use the MINIMUM_RELATED_MAX_HEIGHT.
+ // Note: extraHeight is to take into account margin/padding.
+ const medRelatedHeight = Math.max(
+ this._getOffsetHeight(this.$.mainChangeInfo) -
+ this._getOffsetHeight(this.$.commitMessage) - 2 * EXTRA_HEIGHT,
+ MINIMUM_RELATED_MAX_HEIGHT);
+ newHeight = medRelatedHeight;
+ } else {
+ if (this._commitCollapsible) {
+ // Make sure the content is lined up if both areas have buttons. If
+ // the commit message is not collapsed, instead use the change info
+ // height.
+ newHeight = this._getOffsetHeight(this.$.commitMessage);
+ } else {
+ newHeight = this._getOffsetHeight(this.$.commitAndRelated) -
+ EXTRA_HEIGHT;
+ }
+ }
+ const stylesToUpdate = {};
+
+ // Get the line height of related changes, and convert it to the nearest
+ // integer.
+ const lineHeight = this._getLineHeight(this.$.relatedChanges);
+
+ // Figure out a new height that is divisible by the rounded line height.
+ const remainder = newHeight % lineHeight;
+ newHeight = newHeight - remainder;
+
+ stylesToUpdate['--relation-chain-max-height'] = newHeight + 'px';
+
+ // Update the max-height of the relation chain to this new height.
+ if (this._commitCollapsible) {
+ stylesToUpdate['--related-change-btn-top-padding'] = remainder + 'px';
+ }
+
+ this.updateStyles(stylesToUpdate);
+ }
+
+ _computeShowRelatedToggle() {
+ // Make sure the max height has been applied, since there is now content
+ // to populate.
+ if (!util.getComputedStyleValue('--relation-chain-max-height', this)) {
+ this._updateRelatedChangeMaxHeight();
+ }
+ // Prevents showMore from showing when click on related change, since the
+ // line height would be positive, but related changes height is 0.
+ if (!this._getScrollHeight(this.$.relatedChanges)) {
+ return this._showRelatedToggle = false;
+ }
+
+ if (this._getScrollHeight(this.$.relatedChanges) >
+ (this._getOffsetHeight(this.$.relatedChanges) +
+ this._getLineHeight(this.$.relatedChanges))) {
+ return this._showRelatedToggle = true;
+ }
+ this._showRelatedToggle = false;
+ }
+
+ _updateToggleContainerClass(showRelatedToggle) {
+ if (showRelatedToggle) {
+ this.$.relatedChangesToggle.classList.add('showToggle');
+ } else {
+ this.$.relatedChangesToggle.classList.remove('showToggle');
+ }
+ }
+
+ _startUpdateCheckTimer() {
+ if (!this._serverConfig ||
+ !this._serverConfig.change ||
+ this._serverConfig.change.update_delay === undefined ||
+ this._serverConfig.change.update_delay <= MIN_CHECK_INTERVAL_SECS) {
+ return;
+ }
+
+ this._updateCheckTimerHandle = this.async(() => {
+ this.fetchChangeUpdates(this._change, this.$.restAPI).then(result => {
+ let toastMessage = null;
+ if (!result.isLatest) {
+ toastMessage = ReloadToastMessage.NEWER_REVISION;
+ } else if (result.newStatus === this.ChangeStatus.MERGED) {
+ toastMessage = ReloadToastMessage.MERGED;
+ } else if (result.newStatus === this.ChangeStatus.ABANDONED) {
+ toastMessage = ReloadToastMessage.ABANDONED;
+ } else if (result.newStatus === this.ChangeStatus.NEW) {
+ toastMessage = ReloadToastMessage.RESTORED;
+ } else if (result.newMessages) {
+ toastMessage = ReloadToastMessage.NEW_MESSAGE;
+ }
+
+ if (!toastMessage) {
+ this._startUpdateCheckTimer();
+ return;
+ }
+
+ this._cancelUpdateCheckTimer();
+ this.fire('show-alert', {
+ message: toastMessage,
+ // Persist this alert.
+ dismissOnNavigation: true,
+ action: 'Reload',
+ callback: function() {
+ // Load the current change without any patch range.
+ Gerrit.Nav.navigateToChange(this._change);
+ }.bind(this),
+ });
+ });
+ }, this._serverConfig.change.update_delay * 1000);
+ }
+
+ _cancelUpdateCheckTimer() {
+ if (this._updateCheckTimerHandle) {
+ this.cancelAsync(this._updateCheckTimerHandle);
+ }
+ this._updateCheckTimerHandle = null;
+ }
+
+ _handleVisibilityChange() {
+ if (document.hidden && this._updateCheckTimerHandle) {
+ this._cancelUpdateCheckTimer();
+ } else if (!this._updateCheckTimerHandle) {
+ this._startUpdateCheckTimer();
+ }
+ }
+
+ _handleTopicChanged() {
+ this.$.relatedChanges.reload();
+ }
+
+ _computeHeaderClass(editMode) {
+ const classes = ['header'];
+ if (editMode) { classes.push('editMode'); }
+ return classes.join(' ');
+ }
+
+ _computeEditMode(patchRangeRecord, paramsRecord) {
+ if ([patchRangeRecord, paramsRecord].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ if (paramsRecord.base && paramsRecord.base.edit) { return true; }
+
+ const patchRange = patchRangeRecord.base || {};
+ return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME);
+ }
+
+ _handleFileActionTap(e) {
+ e.preventDefault();
+ const controls = this.$.fileListHeader.$.editControls;
+ const path = e.detail.path;
+ switch (e.detail.action) {
+ case GrEditConstants.Actions.DELETE.id:
+ controls.openDeleteDialog(path);
+ break;
+ case GrEditConstants.Actions.OPEN.id:
+ Gerrit.Nav.navigateToRelativeUrl(
+ Gerrit.Nav.getEditUrlForDiff(this._change, path,
+ this._patchRange.patchNum));
+ break;
+ case GrEditConstants.Actions.RENAME.id:
+ controls.openRenameDialog(path);
+ break;
+ case GrEditConstants.Actions.RESTORE.id:
+ controls.openRestoreDialog(path);
+ break;
+ }
+ }
+
+ _computeCommitMessageKey(number, revision) {
+ return `c${number}_rev${revision}`;
+ }
+
+ _patchNumChanged(patchNumStr) {
+ if (!this._selectedRevision) {
+ return;
+ }
+
+ let patchNum = parseInt(patchNumStr, 10);
+ if (patchNumStr === 'edit') {
+ patchNum = patchNumStr;
+ }
+
+ if (patchNum === this._selectedRevision._number) {
+ return;
+ }
+ this._selectedRevision = Object.values(this._change.revisions).find(
+ revision => revision._number === patchNum);
+ }
+
+ /**
+ * If an edit exists already, load it. Otherwise, toggle edit mode via the
+ * navigation API.
+ */
+ _handleEditTap() {
+ const editInfo = Object.values(this._change.revisions).find(info =>
+ info._number === this.EDIT_NAME);
+
+ if (editInfo) {
+ Gerrit.Nav.navigateToChange(this._change, this.EDIT_NAME);
+ return;
+ }
+
+ // Avoid putting patch set in the URL unless a non-latest patch set is
+ // selected.
+ let patchNum;
+ if (!this.patchNumEquals(this._patchRange.patchNum,
+ this.computeLatestPatchNum(this._allPatchSets))) {
+ patchNum = this._patchRange.patchNum;
+ }
+ Gerrit.Nav.navigateToChange(this._change, patchNum, null, true);
+ }
+
+ _handleStopEditTap() {
+ Gerrit.Nav.navigateToChange(this._change, this._patchRange.patchNum);
+ }
+
+ _resetReplyOverlayFocusStops() {
+ this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops());
+ }
+
+ _handleToggleStar(e) {
+ this.$.restAPI.saveChangeStarred(e.detail.change._number,
+ e.detail.starred);
+ }
+
+ _getRevisionInfo(change) {
+ return new Gerrit.RevisionInfo(change);
+ }
+
+ _computeCurrentRevision(currentRevision, revisions) {
+ return currentRevision && revisions && revisions[currentRevision];
+ }
+
+ _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) {
+ return disableDiffPrefs || !loggedIn;
+ }
+}
+
+customElements.define(GrChangeView.is, GrChangeView);
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
new file mode 100644
index 0000000..b7fdbb7
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
@@ -0,0 +1,505 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ .container:not(.loading) {
+ background-color: var(--background-color-tertiary);
+ }
+ .container.loading {
+ color: var(--deemphasized-text-color);
+ padding: var(--spacing-l);
+ }
+ .header {
+ align-items: center;
+ background-color: var(--background-color-primary);
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ padding: var(--spacing-s) var(--spacing-l);
+ z-index: 99; /* Less than gr-overlay's backdrop */
+ }
+ .header.editMode {
+ background-color: var(--edit-mode-background-color);
+ }
+ .header .download {
+ margin-right: var(--spacing-l);
+ }
+ gr-change-status {
+ display: initial;
+ margin-left: var(--spacing-s);
+ }
+ gr-change-status:first-child {
+ margin-left: 0;
+ }
+ .headerTitle {
+ align-items: center;
+ display: flex;
+ flex: 1;
+ }
+ .headerSubject {
+ font-family: var(--header-font-family);
+ font-size: var(--font-size-h3);
+ font-weight: var(--font-weight-h3);
+ line-height: var(--line-height-h3);
+ margin-left: var(--spacing-l);
+ }
+ .changeNumberColon {
+ color: transparent;
+ }
+ .changeCopyClipboard {
+ margin-left: var(--spacing-s);
+ }
+ #replyBtn {
+ margin-bottom: var(--spacing-l);
+ }
+ gr-change-star {
+ margin-right: var(--spacing-xs);
+ margin-left: var(--spacing-s);
+ --gr-change-star-size: var(--line-height-normal);
+ }
+ gr-reply-dialog {
+ width: 60em;
+ }
+ .changeStatus {
+ text-transform: capitalize;
+ }
+ /* Strong specificity here is needed due to
+ https://github.com/Polymer/polymer/issues/2531 */
+ .container .changeInfo {
+ display: flex;
+ background-color: var(--background-color-secondary);
+ }
+ section {
+ background-color: var(--view-background-color);
+ box-shadow: var(--elevation-level-1);
+ }
+ .changeId {
+ color: var(--deemphasized-text-color);
+ font-family: var(--font-family);
+ margin-top: var(--spacing-l);
+ }
+ .changeMetadata {
+ /* Limit meta section to half of the screen at max */
+ max-width: 50%;
+ }
+ .commitMessage {
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-mono);
+ line-height: var(--line-height-mono);
+ margin-right: var(--spacing-l);
+ margin-bottom: var(--spacing-l);
+ /* Account for border and padding and rounding errors. */
+ max-width: calc(72ch + 2px + 2*var(--spacing-m) + 0.4px);
+ }
+ .commitMessage gr-linked-text {
+ word-break: break-word;
+ }
+ #commitMessageEditor {
+ /* Account for border and padding and rounding errors. */
+ min-width: calc(72ch + 2px + 2*var(--spacing-m) + 0.4px);
+ }
+ .editCommitMessage {
+ margin-top: var(--spacing-l);
+
+ --gr-button: {
+ padding: 5px 0px;
+ }
+ }
+ .changeStatuses,
+ .commitActions,
+ .statusText {
+ align-items: center;
+ display: flex;
+ }
+ .changeStatuses {
+ flex-wrap: wrap;
+ }
+ .mainChangeInfo {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ min-width: 0;
+ }
+ #commitAndRelated {
+ align-content: flex-start;
+ display: flex;
+ flex: 1;
+ overflow-x: hidden;
+ }
+ .relatedChanges {
+ flex: 1 1 auto;
+ overflow: hidden;
+ padding: var(--spacing-l) 0;
+ }
+ .mobile {
+ display: none;
+ }
+ .warning {
+ color: var(--error-text-color);
+ }
+ hr {
+ border: 0;
+ border-top: 1px solid var(--border-color);
+ height: 0;
+ margin-bottom: var(--spacing-l);
+ }
+ #relatedChanges.collapsed {
+ margin-bottom: var(--spacing-l);
+ max-height: var(--relation-chain-max-height, 2em);
+ overflow: hidden;
+ }
+ .commitContainer {
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+ margin: var(--spacing-l) 0;
+ padding: 0 var(--spacing-l);
+ }
+ .collapseToggleContainer {
+ display: flex;
+ margin-bottom: 8px;
+ }
+ #relatedChangesToggle {
+ display: none;
+ }
+ #relatedChangesToggle.showToggle {
+ display: flex;
+ }
+ .collapseToggleContainer gr-button {
+ display: block;
+ }
+ #relatedChangesToggle {
+ margin-left: var(--spacing-l);
+ padding-top: var(--related-change-btn-top-padding, 0);
+ }
+ .showOnEdit {
+ display: none;
+ }
+ .scrollable {
+ overflow: auto;
+ }
+ .text {
+ white-space: pre;
+ }
+ gr-commit-info {
+ display: inline-block;
+ }
+ paper-tabs {
+ background-color: var(--background-color-tertiary);
+ margin-top: var(--spacing-m);
+ height: calc(var(--line-height-h3) + var(--spacing-m));
+ --paper-tabs-selection-bar-color: var(--link-color);
+ }
+ paper-tab {
+ box-sizing: border-box;
+ max-width: 12em;
+ --paper-tab-ink: var(--link-color);
+ }
+ gr-thread-list,
+ gr-messages-list {
+ display: block;
+ }
+ gr-thread-list {
+ min-height: 250px;
+ }
+ #includedInOverlay {
+ width: 65em;
+ }
+ #uploadHelpOverlay {
+ width: 50em;
+ }
+ #metadata {
+ --metadata-horizontal-padding: var(--spacing-l);
+ padding-top: var(--spacing-l);
+ width: 100%;
+ }
+ /* NOTE: If you update this breakpoint, also update the
+ BREAKPOINT_RELATED_MED in the JS */
+ @media screen and (max-width: 75em) {
+ .relatedChanges {
+ padding: 0;
+ }
+ #relatedChanges {
+ padding-top: var(--spacing-l);
+ }
+ #commitAndRelated {
+ flex-direction: column;
+ flex-wrap: nowrap;
+ }
+ #commitMessageEditor {
+ min-width: 0;
+ }
+ .commitMessage {
+ margin-right: 0;
+ }
+ .mainChangeInfo {
+ padding-right: 0;
+ }
+ }
+ /* NOTE: If you update this breakpoint, also update the
+ BREAKPOINT_RELATED_SMALL in the JS */
+ @media screen and (max-width: 50em) {
+ .mobile {
+ display: block;
+ }
+ .header {
+ align-items: flex-start;
+ flex-direction: column;
+ flex: 1;
+ padding: var(--spacing-s) var(--spacing-l);
+ }
+ gr-change-star {
+ vertical-align: middle;
+ }
+ .headerTitle {
+ flex-wrap: wrap;
+ font-family: var(--header-font-family);
+ font-size: var(--font-size-h3);
+ font-weight: var(--font-weight-h3);
+ line-height: var(--line-height-h3);
+ }
+ .desktop {
+ display: none;
+ }
+ .reply {
+ display: block;
+ margin-right: 0;
+ /* px because don't have the same font size */
+ margin-bottom: 6px;
+ }
+ .changeInfo-column:not(:last-of-type) {
+ margin-right: 0;
+ padding-right: 0;
+ }
+ .changeInfo,
+ #commitAndRelated {
+ flex-direction: column;
+ flex-wrap: nowrap;
+ }
+ .commitContainer {
+ margin: 0;
+ padding: var(--spacing-l);
+ }
+ .changeMetadata {
+ margin-top: var(--spacing-xs);
+ max-width: none;
+ }
+ #metadata,
+ .mainChangeInfo {
+ padding: 0;
+ }
+ .commitActions {
+ display: block;
+ margin-top: var(--spacing-l);
+ width: 100%;
+ }
+ .commitMessage {
+ flex: initial;
+ margin: 0;
+ }
+ /* Change actions are the only thing thant need to remain visible due
+ to the fact that they may have the currently visible overlay open. */
+ #mainContent.overlayOpen .hideOnMobileOverlay {
+ display: none;
+ }
+ gr-reply-dialog {
+ height: 100vh;
+ min-width: initial;
+ width: 100vw;
+ }
+ #replyOverlay {
+ z-index: var(--reply-overlay-z-index);
+ }
+ }
+ .patch-set-dropdown {
+ margin: var(--spacing-m) 0 0 var(--spacing-m);
+ }
+ .show-robot-comments {
+ margin: var(--spacing-m);
+ }
+ </style>
+ <div class="container loading" hidden\$="[[!_loading]]">Loading...</div>
+ <div id="mainContent" class="container" on-show-checks-table="_handleShowTab" hidden\$="{{_loading}}">
+ <section class="changeInfoSection">
+ <div class\$="[[_computeHeaderClass(_editMode)]]">
+ <div class="headerTitle">
+ <div class="changeStatuses">
+ <template is="dom-repeat" items="[[_changeStatuses]]" as="status">
+ <gr-change-status max-width="100" status="[[status]]"></gr-change-status>
+ </template>
+ </div>
+ <div class="statusText">
+ <template is="dom-if" if="[[_computeShowCommitInfo(_changeStatus, _change.current_revision)]]">
+ <span class="text"> as </span>
+ <gr-commit-info change="[[_change]]" commit-info="[[_computeMergedCommitInfo(_change.current_revision, _change.revisions)]]" server-config="[[_serverConfig]]"></gr-commit-info>
+ </template>
+ </div>
+ <gr-change-star id="changeStar" change="{{_change}}" on-toggle-star="_handleToggleStar" hidden\$="[[!_loggedIn]]"></gr-change-star>
+
+ <a aria-label\$="[[_computeChangePermalinkAriaLabel(_change._number)]]" href\$="[[_computeChangeUrl(_change)]]">[[_change._number]]</a>
+ <span class="changeNumberColon">: </span>
+ <span class="headerSubject">[[_change.subject]]</span>
+ <gr-copy-clipboard class="changeCopyClipboard" hide-input="" text="[[_computeCopyTextForTitle(_change)]]">
+ </gr-copy-clipboard>
+ </div><!-- end headerTitle -->
+ <div class="commitActions" hidden\$="[[!_loggedIn]]">
+ <gr-change-actions id="actions" change="[[_change]]" disable-edit="[[disableEdit]]" has-parent="[[hasParent]]" actions="[[_change.actions]]" revision-actions="{{_currentRevisionActions}}" change-num="[[_changeNum]]" change-status="[[_change.status]]" commit-num="[[_commitInfo.commit]]" latest-patch-num="[[computeLatestPatchNum(_allPatchSets)]]" commit-message="[[_latestCommitMessage]]" edit-patchset-loaded="[[hasEditPatchsetLoaded(_patchRange.*)]]" edit-mode="[[_editMode]]" edit-based-on-current-patch-set="[[hasEditBasedOnCurrentPatchSet(_allPatchSets)]]" private-by-default="[[_projectConfig.private_by_default]]" on-reload-change="_handleReloadChange" on-edit-tap="_handleEditTap" on-stop-edit-tap="_handleStopEditTap" on-download-tap="_handleOpenDownloadDialog"></gr-change-actions>
+ </div><!-- end commit actions -->
+ </div><!-- end header -->
+ <div class="changeInfo">
+ <div class="changeInfo-column changeMetadata hideOnMobileOverlay">
+ <gr-change-metadata id="metadata" change="{{_change}}" account="[[_account]]" revision="[[_selectedRevision]]" commit-info="[[_commitInfo]]" server-config="[[_serverConfig]]" parent-is-current="[[_parentIsCurrent]]" on-show-reply-dialog="_handleShowReplyDialog">
+ </gr-change-metadata>
+ </div>
+ <div id="mainChangeInfo" class="changeInfo-column mainChangeInfo">
+ <div id="commitAndRelated" class="hideOnMobileOverlay">
+ <div class="commitContainer">
+ <div>
+ <gr-button id="replyBtn" class="reply" title="[[createTitle(Shortcut.OPEN_REPLY_DIALOG,
+ ShortcutSection.ACTIONS)]]" hidden\$="[[!_loggedIn]]" primary="" disabled="[[_replyDisabled]]" on-click="_handleReplyTap">[[_replyButtonLabel]]</gr-button>
+ </div>
+ <div id="commitMessage" class="commitMessage">
+ <gr-editable-content id="commitMessageEditor" editing="[[_editingCommitMessage]]" content="{{_latestCommitMessage}}" storage-key="[[_computeCommitMessageKey(_change._number, _change.current_revision)]]" remove-zero-width-space="" collapsed\$="[[_computeCommitMessageCollapsed(_commitCollapsed, _commitCollapsible)]]">
+ <gr-linked-text pre="" content="[[_latestCommitMessage]]" config="[[_projectConfig.commentlinks]]" remove-zero-width-space=""></gr-linked-text>
+ </gr-editable-content>
+ <gr-button link="" class="editCommitMessage" on-click="_handleEditCommitMessage" hidden\$="[[_hideEditCommitMessage]]">Edit</gr-button>
+ <div class="changeId" hidden\$="[[!_changeIdCommitMessageError]]">
+ <hr>
+ Change-Id:
+ <span class\$="[[_computeChangeIdClass(_changeIdCommitMessageError)]]" title\$="[[_computeTitleAttributeWarning(_changeIdCommitMessageError)]]">
+ [[_change.change_id]]
+ </span>
+ </div>
+ </div>
+ <div id="commitCollapseToggle" class="collapseToggleContainer" hidden\$="[[!_commitCollapsible]]">
+ <gr-button link="" id="commitCollapseToggleButton" class="collapseToggleButton" on-click="_toggleCommitCollapsed">
+ [[_computeCollapseText(_commitCollapsed)]]
+ </gr-button>
+ </div>
+ <gr-endpoint-decorator name="commit-container">
+ <gr-endpoint-param name="change" value="[[_change]]">
+ </gr-endpoint-param>
+ <gr-endpoint-param name="revision" value="[[_selectedRevision]]">
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </div>
+ <div class="relatedChanges">
+ <gr-related-changes-list id="relatedChanges" class\$="[[_computeRelatedChangesClass(_relatedChangesCollapsed)]]" change="[[_change]]" mergeable="[[_mergeable]]" has-parent="{{hasParent}}" on-update="_updateRelatedChangeMaxHeight" patch-num="[[computeLatestPatchNum(_allPatchSets)]]" on-new-section-loaded="_computeShowRelatedToggle">
+ </gr-related-changes-list>
+ <div id="relatedChangesToggle" class="collapseToggleContainer">
+ <gr-button link="" id="relatedChangesToggleButton" class="collapseToggleButton" on-click="_toggleRelatedChangesCollapsed">
+ [[_computeCollapseText(_relatedChangesCollapsed)]]
+ </gr-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <paper-tabs id="primaryTabs" on-selected-changed="_handleFileTabChange">
+ <paper-tab data-name\$="[[_files_tab_name]]">Files</paper-tab>
+ <template is="dom-repeat" items="[[_dynamicTabHeaderEndpoints]]" as="tabHeader">
+ <paper-tab data-name\$="[[tabHeader]]">
+ <gr-endpoint-decorator name\$="[[tabHeader]]">
+ <gr-endpoint-param name="change" value="[[_change]]">
+ </gr-endpoint-param>
+ <gr-endpoint-param name="revision" value="[[_selectedRevision]]">
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </paper-tab>
+ </template>
+ <paper-tab data-name\$="[[_findings_tab_name]]">
+ Findings
+ </paper-tab>
+ </paper-tabs>
+
+ <section class="patchInfo">
+ <div hidden\$="[[!_findIfTabMatches(_currentTabName, _files_tab_name)]]">
+ <gr-file-list-header id="fileListHeader" account="[[_account]]" all-patch-sets="[[_allPatchSets]]" change="[[_change]]" change-num="[[_changeNum]]" revision-info="[[_revisionInfo]]" change-comments="[[_changeComments]]" commit-info="[[_commitInfo]]" change-url="[[_computeChangeUrl(_change)]]" edit-mode="[[_editMode]]" logged-in="[[_loggedIn]]" server-config="[[_serverConfig]]" shown-file-count="[[_shownFileCount]]" diff-prefs="[[_diffPrefs]]" diff-view-mode="{{viewState.diffMode}}" patch-num="{{_patchRange.patchNum}}" base-patch-num="{{_patchRange.basePatchNum}}" files-expanded="[[_filesExpanded]]" diff-prefs-disabled="[[_diffPrefsDisabled]]" on-open-diff-prefs="_handleOpenDiffPrefs" on-open-download-dialog="_handleOpenDownloadDialog" on-open-upload-help-dialog="_handleOpenUploadHelpDialog" on-open-included-in-dialog="_handleOpenIncludedInDialog" on-expand-diffs="_expandAllDiffs" on-collapse-diffs="_collapseAllDiffs">
+ </gr-file-list-header>
+ <gr-file-list id="fileList" class="hideOnMobileOverlay" diff-prefs="{{_diffPrefs}}" change="[[_change]]" change-num="[[_changeNum]]" patch-range="{{_patchRange}}" change-comments="[[_changeComments]]" drafts="[[_diffDrafts]]" revisions="[[_change.revisions]]" project-config="[[_projectConfig]]" selected-index="{{viewState.selectedFileIndex}}" diff-view-mode="[[viewState.diffMode]]" edit-mode="[[_editMode]]" num-files-shown="{{_numFilesShown}}" files-expanded="{{_filesExpanded}}" file-list-increment="{{_numFilesShown}}" on-files-shown-changed="_setShownFiles" on-file-action-tap="_handleFileActionTap" on-reload-drafts="_reloadDraftsWithCallback">
+ </gr-file-list>
+ </div>
+
+ <template is="dom-if" if="[[_findIfTabMatches(_currentTabName, _findings_tab_name)]]">
+ <gr-dropdown-list class="patch-set-dropdown" items="[[_robotCommentsPatchSetDropdownItems]]" on-value-change="_handleRobotCommentPatchSetChanged" value="[[_currentRobotCommentsPatchSet]]">
+ </gr-dropdown-list>
+ <gr-thread-list threads="[[_robotCommentThreads]]" change="[[_change]]" change-num="[[_changeNum]]" logged-in="[[_loggedIn]]" tab="[[_findings_tab_name]]" hide-toggle-buttons="" on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list>
+ <template is="dom-if" if="[[_showRobotCommentsButton]]">
+ <gr-button class="show-robot-comments" on-click="_toggleShowRobotComments">
+ [[_computeShowText(_showAllRobotComments)]]
+ </gr-button>
+ </template>
+ </template>
+
+ <template is="dom-if" if="[[_findIfTabMatches(_currentTabName, _selectedTabPluginHeader)]]">
+ <gr-endpoint-decorator name\$="[[_selectedTabPluginEndpoint]]">
+ <gr-endpoint-param name="change" value="[[_change]]">
+ </gr-endpoint-param>
+ <gr-endpoint-param name="revision" value="[[_selectedRevision]]">
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </template>
+ </section>
+
+ <gr-endpoint-decorator name="change-view-integration">
+ <gr-endpoint-param name="change" value="[[_change]]">
+ </gr-endpoint-param>
+ <gr-endpoint-param name="revision" value="[[_selectedRevision]]">
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+
+ <paper-tabs id="commentTabs" on-selected-changed="_handleCommentTabChange">
+ <paper-tab class="changeLog">Change Log</paper-tab>
+ <paper-tab class="commentThreads">
+ <gr-tooltip-content has-tooltip="" title\$="[[_computeTotalCommentCounts(_change.unresolved_comment_count, _changeComments)]]">
+ <span>Comment Threads</span></gr-tooltip-content>
+ </paper-tab>
+ </paper-tabs>
+ <section class="changeLog">
+ <template is="dom-if" if="[[_isSelectedView(_currentView,
+ _commentTabs.CHANGE_LOG)]]">
+ <gr-messages-list class="hideOnMobileOverlay" change-num="[[_changeNum]]" labels="[[_change.labels]]" messages="[[_change.messages]]" reviewer-updates="[[_change.reviewer_updates]]" change-comments="[[_changeComments]]" project-name="[[_change.project]]" show-reply-buttons="[[_loggedIn]]" on-message-anchor-tap="_handleMessageAnchorTap" on-reply="_handleMessageReply"></gr-messages-list>
+ </template>
+ <template is="dom-if" if="[[_isSelectedView(_currentView,
+ _commentTabs.COMMENT_THREADS)]]">
+ <gr-thread-list threads="[[_commentThreads]]" change="[[_change]]" change-num="[[_changeNum]]" logged-in="[[_loggedIn]]" only-show-robot-comments-with-human-reply="" on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list>
+ </template>
+ </section>
+ </div>
+
+ <gr-apply-fix-dialog id="applyFixDialog" prefs="[[_diffPrefs]]" change="[[_change]]" change-num="[[_changeNum]]"></gr-apply-fix-dialog>
+ <gr-overlay id="downloadOverlay" with-backdrop="">
+ <gr-download-dialog id="downloadDialog" change="[[_change]]" patch-num="[[_patchRange.patchNum]]" config="[[_serverConfig.download]]" on-close="_handleDownloadDialogClose"></gr-download-dialog>
+ </gr-overlay>
+ <gr-overlay id="uploadHelpOverlay" with-backdrop="">
+ <gr-upload-help-dialog revision="[[_currentRevision]]" target-branch="[[_change.branch]]" on-close="_handleCloseUploadHelpDialog"></gr-upload-help-dialog>
+ </gr-overlay>
+ <gr-overlay id="includedInOverlay" with-backdrop="">
+ <gr-included-in-dialog id="includedInDialog" change-num="[[_changeNum]]" on-close="_handleIncludedInDialogClose"></gr-included-in-dialog>
+ </gr-overlay>
+ <gr-overlay id="replyOverlay" class="scrollable" no-cancel-on-outside-click="" no-cancel-on-esc-key="" with-backdrop="">
+ <gr-reply-dialog id="replyDialog" change="{{_change}}" patch-num="[[computeLatestPatchNum(_allPatchSets)]]" permitted-labels="[[_change.permitted_labels]]" draft-comment-threads="[[_draftCommentThreads]]" project-config="[[_projectConfig]]" can-be-started="[[_canStartReview]]" on-send="_handleReplySent" on-cancel="_handleReplyCancel" on-autogrow="_handleReplyAutogrow" on-send-disabled-changed="_resetReplyOverlayFocusStops" hidden\$="[[!_loggedIn]]">
+ </gr-reply-dialog>
+ </gr-overlay>
+ <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ <gr-comment-api id="commentAPI"></gr-comment-api>
+ <gr-reporting id="reporting"></gr-reporting>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 26bcbf2..24e2fbd 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -19,18 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-view</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="/bower_components/page/page.js"></script>
-
-<link rel="import" href="../../edit/gr-edit-constants.html">
-<link rel="import" href="gr-change-view.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script src="/node_modules/page/page.js"></script>
<test-fixture id="basic">
<template>
@@ -44,887 +37,666 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-view tests', async () => {
- await readyToTest();
- const kb = window.Gerrit.KeyboardShortcutBinder;
- kb.bindShortcut(kb.Shortcut.SEND_REPLY, 'ctrl+enter');
- kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE, 'shift+r');
- kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
- kb.bindShortcut(kb.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
- kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
- kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
- kb.bindShortcut(kb.Shortcut.UP_TO_DASHBOARD, 'u');
- kb.bindShortcut(kb.Shortcut.EXPAND_ALL_MESSAGES, 'x');
- kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
- kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
- kb.bindShortcut(kb.Shortcut.EDIT_TOPIC, 't');
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../edit/gr-edit-constants.js';
+import './gr-change-view.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-change-view tests', () => {
+ const kb = window.Gerrit.KeyboardShortcutBinder;
+ kb.bindShortcut(kb.Shortcut.SEND_REPLY, 'ctrl+enter');
+ kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE, 'shift+r');
+ kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
+ kb.bindShortcut(kb.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
+ kb.bindShortcut(kb.Shortcut.UP_TO_DASHBOARD, 'u');
+ kb.bindShortcut(kb.Shortcut.EXPAND_ALL_MESSAGES, 'x');
+ kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
+ kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
+ kb.bindShortcut(kb.Shortcut.EDIT_TOPIC, 't');
- let element;
- let sandbox;
- let navigateToChangeStub;
- const TEST_SCROLL_TOP_PX = 100;
+ let element;
+ let sandbox;
+ let navigateToChangeStub;
+ const TEST_SCROLL_TOP_PX = 100;
- const CommentTabs = {
- CHANGE_LOG: 0,
- COMMENT_THREADS: 1,
+ const CommentTabs = {
+ CHANGE_LOG: 0,
+ COMMENT_THREADS: 1,
+ };
+ const ROBOT_COMMENTS_LIMIT = 10;
+
+ const THREADS = [
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 2,
+ robot_id: 'rb1',
+ id: 'ecf9fa_fe1a5f62',
+ line: 5,
+ updated: '2018-02-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ },
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 4,
+ id: 'ecf0b9fa_fe1a5f62',
+ line: 5,
+ updated: '2018-02-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ },
+ {
+ id: '503008e2_0ab203ee',
+ path: '/COMMIT_MSG',
+ line: 5,
+ in_reply_to: 'ecf0b9fa_fe1a5f62',
+ updated: '2018-02-13 22:48:48.018000000',
+ message: 'draft',
+ unresolved: false,
+ __draft: true,
+ __draftID: '0.m683trwff68',
+ __editing: false,
+ patch_set: '2',
+ },
+ ],
+ patchNum: 4,
+ path: '/COMMIT_MSG',
+ line: 5,
+ rootId: 'ecf0b9fa_fe1a5f62',
+ start_datetime: '2018-02-08 18:49:18.000000000',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 3,
+ id: 'ecf0b9fa_fe5f62',
+ robot_id: 'rb2',
+ line: 5,
+ updated: '2018-02-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ },
+ {
+ __path: 'test.txt',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 3,
+ id: '09a9fb0a_1484e6cf',
+ side: 'PARENT',
+ updated: '2018-02-13 22:47:19.000000000',
+ message: 'Some comment on another patchset.',
+ unresolved: false,
+ },
+ ],
+ patchNum: 3,
+ path: 'test.txt',
+ rootId: '09a9fb0a_1484e6cf',
+ start_datetime: '2018-02-13 22:47:19.000000000',
+ commentSide: 'PARENT',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 2,
+ id: '8caddf38_44770ec1',
+ line: 4,
+ updated: '2018-02-13 22:48:40.000000000',
+ message: 'Another unresolved comment',
+ unresolved: true,
+ },
+ ],
+ patchNum: 2,
+ path: '/COMMIT_MSG',
+ line: 4,
+ rootId: '8caddf38_44770ec1',
+ start_datetime: '2018-02-13 22:48:40.000000000',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 2,
+ id: 'scaddf38_44770ec1',
+ line: 4,
+ updated: '2018-02-14 22:48:40.000000000',
+ message: 'Yet another unresolved comment',
+ unresolved: true,
+ },
+ ],
+ patchNum: 2,
+ path: '/COMMIT_MSG',
+ line: 4,
+ rootId: 'scaddf38_44770ec1',
+ start_datetime: '2018-02-14 22:48:40.000000000',
+ },
+ {
+ comments: [
+ {
+ id: 'zcf0b9fa_fe1a5f62',
+ path: '/COMMIT_MSG',
+ line: 6,
+ updated: '2018-02-15 22:48:48.018000000',
+ message: 'resolved draft',
+ unresolved: false,
+ __draft: true,
+ __draftID: '0.m683trwff68',
+ __editing: false,
+ patch_set: '2',
+ },
+ ],
+ patchNum: 4,
+ path: '/COMMIT_MSG',
+ line: 6,
+ rootId: 'zcf0b9fa_fe1a5f62',
+ start_datetime: '2018-02-09 18:49:18.000000000',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 4,
+ id: 'rc1',
+ line: 5,
+ updated: '2019-02-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ robot_id: 'rc1',
+ },
+ ],
+ patchNum: 4,
+ path: '/COMMIT_MSG',
+ line: 5,
+ rootId: 'rc1',
+ start_datetime: '2019-02-08 18:49:18.000000000',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 4,
+ id: 'rc2',
+ line: 5,
+ updated: '2019-03-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ robot_id: 'rc2',
+ },
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 4,
+ id: 'c2_1',
+ line: 5,
+ updated: '2019-03-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ },
+ ],
+ patchNum: 4,
+ path: '/COMMIT_MSG',
+ line: 5,
+ rootId: 'rc2',
+ start_datetime: '2019-03-08 18:49:18.000000000',
+ },
+ ];
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-endpoint-decorator', {
+ _import: sandbox.stub().returns(Promise.resolve()),
+ });
+ // Since _endpoints are global, must reset state.
+ Gerrit._endpoints = new GrPluginEndpoints();
+ navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({test: 'config'}); },
+ getAccount() { return Promise.resolve(null); },
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ _fetchSharedCacheURL() { return Promise.resolve({}); },
+ });
+ element = fixture('basic');
+ sandbox.stub(element.$.actions, 'reload').returns(Promise.resolve());
+ Gerrit._loadPlugins([]);
+ Gerrit.install(
+ plugin => {
+ plugin.registerDynamicCustomComponent(
+ 'change-view-tab-header',
+ 'gr-checks-change-view-tab-header-view'
+ );
+ plugin.registerDynamicCustomComponent(
+ 'change-view-tab-content',
+ 'gr-checks-view'
+ );
+ },
+ '0.1',
+ 'http://some/plugins/url.html'
+ );
+ });
+
+ teardown(done => {
+ flush(() => {
+ sandbox.restore();
+ done();
+ });
+ });
+
+ const getCustomCssValue =
+ cssParam => util.getComputedStyleValue(cssParam, element);
+
+ test('_handleMessageAnchorTap', () => {
+ element._changeNum = '1';
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 1,
};
- const ROBOT_COMMENTS_LIMIT = 10;
+ const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForChange');
+ const replaceStateStub = sandbox.stub(history, 'replaceState');
+ element._handleMessageAnchorTap({detail: {id: 'a12345'}});
- const THREADS = [
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 2,
- robot_id: 'rb1',
- id: 'ecf9fa_fe1a5f62',
- line: 5,
- updated: '2018-02-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- },
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'ecf0b9fa_fe1a5f62',
- line: 5,
- updated: '2018-02-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- },
- {
- id: '503008e2_0ab203ee',
- path: '/COMMIT_MSG',
- line: 5,
- in_reply_to: 'ecf0b9fa_fe1a5f62',
- updated: '2018-02-13 22:48:48.018000000',
- message: 'draft',
- unresolved: false,
- __draft: true,
- __draftID: '0.m683trwff68',
- __editing: false,
- patch_set: '2',
- },
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 5,
- rootId: 'ecf0b9fa_fe1a5f62',
- start_datetime: '2018-02-08 18:49:18.000000000',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 3,
- id: 'ecf0b9fa_fe5f62',
- robot_id: 'rb2',
- line: 5,
- updated: '2018-02-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- },
- {
- __path: 'test.txt',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 3,
- id: '09a9fb0a_1484e6cf',
- side: 'PARENT',
- updated: '2018-02-13 22:47:19.000000000',
- message: 'Some comment on another patchset.',
- unresolved: false,
- },
- ],
- patchNum: 3,
- path: 'test.txt',
- rootId: '09a9fb0a_1484e6cf',
- start_datetime: '2018-02-13 22:47:19.000000000',
- commentSide: 'PARENT',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 2,
- id: '8caddf38_44770ec1',
- line: 4,
- updated: '2018-02-13 22:48:40.000000000',
- message: 'Another unresolved comment',
- unresolved: true,
- },
- ],
- patchNum: 2,
- path: '/COMMIT_MSG',
- line: 4,
- rootId: '8caddf38_44770ec1',
- start_datetime: '2018-02-13 22:48:40.000000000',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 2,
- id: 'scaddf38_44770ec1',
- line: 4,
- updated: '2018-02-14 22:48:40.000000000',
- message: 'Yet another unresolved comment',
- unresolved: true,
- },
- ],
- patchNum: 2,
- path: '/COMMIT_MSG',
- line: 4,
- rootId: 'scaddf38_44770ec1',
- start_datetime: '2018-02-14 22:48:40.000000000',
- },
- {
- comments: [
- {
- id: 'zcf0b9fa_fe1a5f62',
- path: '/COMMIT_MSG',
- line: 6,
- updated: '2018-02-15 22:48:48.018000000',
- message: 'resolved draft',
- unresolved: false,
- __draft: true,
- __draftID: '0.m683trwff68',
- __editing: false,
- patch_set: '2',
- },
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 6,
- rootId: 'zcf0b9fa_fe1a5f62',
- start_datetime: '2018-02-09 18:49:18.000000000',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'rc1',
- line: 5,
- updated: '2019-02-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- robot_id: 'rc1',
- },
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 5,
- rootId: 'rc1',
- start_datetime: '2019-02-08 18:49:18.000000000',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'rc2',
- line: 5,
- updated: '2019-03-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- robot_id: 'rc2',
- },
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'c2_1',
- line: 5,
- updated: '2019-03-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- },
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 5,
- rootId: 'rc2',
- start_datetime: '2019-03-08 18:49:18.000000000',
- },
- ];
+ assert.equal(getUrlStub.lastCall.args[4], '#message-a12345');
+ assert.isTrue(replaceStateStub.called);
+ });
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-endpoint-decorator', {
- _import: sandbox.stub().returns(Promise.resolve()),
- });
- // Since _endpoints are global, must reset state.
- Gerrit._endpoints = new GrPluginEndpoints();
- navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({test: 'config'}); },
- getAccount() { return Promise.resolve(null); },
- getDiffComments() { return Promise.resolve({}); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
- _fetchSharedCacheURL() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- sandbox.stub(element.$.actions, 'reload').returns(Promise.resolve());
- Gerrit._loadPlugins([]);
- Gerrit.install(
- plugin => {
- plugin.registerDynamicCustomComponent(
- 'change-view-tab-header',
- 'gr-checks-change-view-tab-header-view'
- );
- plugin.registerDynamicCustomComponent(
- 'change-view-tab-content',
- 'gr-checks-view'
- );
- },
- '0.1',
- 'http://some/plugins/url.html'
- );
+ suite('plugins adding to file tab', () => {
+ setup(done => {
+ // Resolving it here instead of during setup() as other tests depend
+ // on flush() not being called during setup.
+ flush(() => done());
});
- teardown(done => {
+ test('plugin added tab shows up as a dynamic endpoint', () => {
+ assert(element._dynamicTabHeaderEndpoints.includes(
+ 'change-view-tab-header-url'));
+ const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
+ // 3 Tabs are : Files, Plugin, Findings
+ assert.equal(paperTabs.querySelectorAll('paper-tab').length, 3);
+ assert.equal(paperTabs.querySelectorAll('paper-tab')[1].dataset.name,
+ 'change-view-tab-header-url');
+ });
+
+ test('handleShowTab switched tab correctly', done => {
+ const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
+ assert.equal(paperTabs.selected, 0);
+ element._handleShowTab({detail:
+ {tab: 'change-view-tab-header-url'}});
flush(() => {
- sandbox.restore();
+ assert.equal(paperTabs.selected, 1);
done();
});
});
- const getCustomCssValue =
- cssParam => util.getComputedStyleValue(cssParam, element);
+ test('switching tab sets _selectedTabPluginEndpoint', done => {
+ const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
+ MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[1]);
+ flush(() => {
+ assert.equal(element._selectedTabPluginEndpoint,
+ 'change-view-tab-content-url');
+ done();
+ });
+ });
+ });
- test('_handleMessageAnchorTap', () => {
+ suite('keyboard shortcuts', () => {
+ test('t to add topic', () => {
+ const editStub = sandbox.stub(element.$.metadata, 'editTopic');
+ MockInteractions.pressAndReleaseKeyOn(element, 83, null, 't');
+ assert(editStub.called);
+ });
+
+ test('S should toggle the CL star', () => {
+ const starStub = sandbox.stub(element.$.changeStar, 'toggleStar');
+ MockInteractions.pressAndReleaseKeyOn(element, 83, null, 's');
+ assert(starStub.called);
+ });
+
+ test('U should navigate to root if no backPage set', () => {
+ const relativeNavStub = sandbox.stub(Gerrit.Nav,
+ 'navigateToRelativeUrl');
+ MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
+ assert.isTrue(relativeNavStub.called);
+ assert.isTrue(relativeNavStub.lastCall.calledWithExactly(
+ Gerrit.Nav.getUrlForRoot()));
+ });
+
+ test('U should navigate to backPage if set', () => {
+ const relativeNavStub = sandbox.stub(Gerrit.Nav,
+ 'navigateToRelativeUrl');
+ element.backPage = '/dashboard/self';
+ MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
+ assert.isTrue(relativeNavStub.called);
+ assert.isTrue(relativeNavStub.lastCall.calledWithExactly(
+ '/dashboard/self'));
+ });
+
+ test('A fires an error event when not logged in', done => {
+ sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(false));
+ const loggedInErrorSpy = sandbox.spy();
+ element.addEventListener('show-auth-required', loggedInErrorSpy);
+ MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ flush(() => {
+ assert.isFalse(element.$.replyOverlay.opened);
+ assert.isTrue(loggedInErrorSpy.called);
+ done();
+ });
+ });
+
+ test('shift A does not open reply overlay', done => {
+ sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+ MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
+ flush(() => {
+ assert.isFalse(element.$.replyOverlay.opened);
+ done();
+ });
+ });
+
+ test('A toggles overlay when logged in', done => {
+ sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+ sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
+ element._change = {labels: {}};
+ const openSpy = sandbox.spy(element, '_openReplyDialog');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ flush(() => {
+ assert.isTrue(element.$.replyOverlay.opened);
+ element.$.replyOverlay.close();
+ assert.isFalse(element.$.replyOverlay.opened);
+ assert(openSpy.lastCall.calledWithExactly(
+ element.$.replyDialog.FocusTarget.ANY),
+ '_openReplyDialog should have been passed ANY');
+ assert.equal(openSpy.callCount, 1);
+ done();
+ });
+ });
+
+ test('fullscreen-overlay-opened hides content', () => {
+ element._loggedIn = true;
+ element._loading = false;
+ element._change = {
+ owner: {_account_id: 1},
+ labels: {},
+ actions: {
+ abandon: {
+ enabled: true,
+ label: 'Abandon',
+ method: 'POST',
+ title: 'Abandon',
+ },
+ },
+ };
+ sandbox.spy(element, '_handleHideBackgroundContent');
+ element.$.replyDialog.fire('fullscreen-overlay-opened');
+ assert.isTrue(element._handleHideBackgroundContent.called);
+ assert.isTrue(element.$.mainContent.classList.contains('overlayOpen'));
+ assert.equal(getComputedStyle(element.$.actions).display, 'flex');
+ });
+
+ test('fullscreen-overlay-closed shows content', () => {
+ element._loggedIn = true;
+ element._loading = false;
+ element._change = {
+ owner: {_account_id: 1},
+ labels: {},
+ actions: {
+ abandon: {
+ enabled: true,
+ label: 'Abandon',
+ method: 'POST',
+ title: 'Abandon',
+ },
+ },
+ };
+ sandbox.spy(element, '_handleShowBackgroundContent');
+ element.$.replyDialog.fire('fullscreen-overlay-closed');
+ assert.isTrue(element._handleShowBackgroundContent.called);
+ assert.isFalse(element.$.mainContent.classList.contains('overlayOpen'));
+ });
+
+ test('expand all messages when expand-diffs fired', () => {
+ const handleExpand =
+ sandbox.stub(element.$.fileList, 'expandAllDiffs');
+ element.$.fileListHeader.fire('expand-diffs');
+ assert.isTrue(handleExpand.called);
+ });
+
+ test('collapse all messages when collapse-diffs fired', () => {
+ const handleCollapse =
+ sandbox.stub(element.$.fileList, 'collapseAllDiffs');
+ element.$.fileListHeader.fire('collapse-diffs');
+ assert.isTrue(handleCollapse.called);
+ });
+
+ test('X should expand all messages', done => {
+ flush(() => {
+ const handleExpand = sandbox.stub(element.messagesList,
+ 'handleExpandCollapse');
+ MockInteractions.pressAndReleaseKeyOn(element, 88, null, 'x');
+ assert(handleExpand.calledWith(true));
+ done();
+ });
+ });
+
+ test('Z should collapse all messages', done => {
+ flush(() => {
+ const handleExpand = sandbox.stub(element.messagesList,
+ 'handleExpandCollapse');
+ MockInteractions.pressAndReleaseKeyOn(element, 90, null, 'z');
+ assert(handleExpand.calledWith(false));
+ done();
+ });
+ });
+
+ test('shift + R should fetch and navigate to the latest patch set',
+ done => {
+ element._changeNum = '42';
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 1,
+ };
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ _number: 42,
+ revisions: {
+ rev1: {_number: 1, commit: {parents: []}},
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ actions: {},
+ };
+
+ navigateToChangeStub.restore();
+ navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange',
+ (change, patchNum, basePatchNum) => {
+ assert.equal(change, element._change);
+ assert.isUndefined(patchNum);
+ assert.isUndefined(basePatchNum);
+ done();
+ });
+
+ MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
+ });
+
+ test('d should open download overlay', () => {
+ const stub = sandbox.stub(element.$.downloadOverlay, 'open');
+ MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd');
+ assert.isTrue(stub.called);
+ });
+
+ test(', should open diff preferences', () => {
+ const stub = sandbox.stub(
+ element.$.fileList.$.diffPreferencesDialog, 'open');
+ element._loggedIn = false;
+ element.disableDiffPrefs = true;
+ MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
+ assert.isFalse(stub.called);
+
+ element._loggedIn = true;
+ MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
+ assert.isFalse(stub.called);
+
+ element.disableDiffPrefs = false;
+ MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
+ assert.isTrue(stub.called);
+ });
+
+ test('m should toggle diff mode', () => {
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ const setModeStub = sandbox.stub(element.$.fileListHeader,
+ 'setDiffViewMode');
+ const e = {preventDefault: () => {}};
+ flushAsynchronousOperations();
+
+ element.viewState.diffMode = 'SIDE_BY_SIDE';
+ element._handleToggleDiffMode(e);
+ assert.isTrue(setModeStub.calledWith('UNIFIED_DIFF'));
+
+ element.viewState.diffMode = 'UNIFIED_DIFF';
+ element._handleToggleDiffMode(e);
+ assert.isTrue(setModeStub.calledWith('SIDE_BY_SIDE'));
+ });
+ });
+
+ suite('reloading drafts', () => {
+ let reloadStub;
+ const drafts = {
+ 'testfile.txt': [
+ {
+ patch_set: 5,
+ id: 'dd2982f5_c01c9e6a',
+ line: 1,
+ updated: '2017-11-08 18:47:45.000000000',
+ message: 'test',
+ unresolved: true,
+ },
+ ],
+ };
+ setup(() => {
+ // Fake computeDraftCount as its required for ChangeComments,
+ // see gr-comment-api#reloadDrafts.
+ reloadStub = sandbox.stub(element.$.commentAPI, 'reloadDrafts')
+ .returns(Promise.resolve({
+ drafts,
+ getAllThreadsForChange: () => ([]),
+ computeDraftCount: () => 1,
+ }));
+ });
+
+ test('drafts are reloaded when reload-drafts fired', done => {
+ element.$.fileList.fire('reload-drafts', {
+ resolve: () => {
+ assert.isTrue(reloadStub.called);
+ assert.deepEqual(element._diffDrafts, drafts);
+ done();
+ },
+ });
+ });
+
+ test('drafts are reloaded when comment-refresh fired', () => {
+ element.fire('comment-refresh');
+ assert.isTrue(reloadStub.called);
+ });
+ });
+
+ test('diff comments modified', () => {
+ sandbox.spy(element, '_handleReloadCommentThreads');
+ return element._reloadComments().then(() => {
+ element.fire('diff-comments-modified');
+ assert.isTrue(element._handleReloadCommentThreads.called);
+ });
+ });
+
+ test('thread list modified', () => {
+ sandbox.spy(element, '_handleReloadDiffComments');
+ element._currentView = CommentTabs.COMMENT_THREADS;
+ flushAsynchronousOperations();
+
+ return element._reloadComments().then(() => {
+ element.threadList.fire('thread-list-modified');
+ assert.isTrue(element._handleReloadDiffComments.called);
+
+ let draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
+ .returns(1);
+ assert.equal(element._computeTotalCommentCounts(5,
+ element._changeComments), '5 unresolved, 1 draft');
+ assert.equal(element._computeTotalCommentCounts(0,
+ element._changeComments), '1 draft');
+ draftStub.restore();
+ draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
+ .returns(0);
+ assert.equal(element._computeTotalCommentCounts(0,
+ element._changeComments), '');
+ assert.equal(element._computeTotalCommentCounts(1,
+ element._changeComments), '1 unresolved');
+ draftStub.restore();
+ draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
+ .returns(2);
+ assert.equal(element._computeTotalCommentCounts(1,
+ element._changeComments), '1 unresolved, 2 drafts');
+ draftStub.restore();
+ });
+ });
+
+ suite('thread list and change log tabs', () => {
+ setup(() => {
element._changeNum = '1';
element._patchRange = {
basePatchNum: 'PARENT',
patchNum: 1,
};
- const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForChange');
- const replaceStateStub = sandbox.stub(history, 'replaceState');
- element._handleMessageAnchorTap({detail: {id: 'a12345'}});
-
- assert.equal(getUrlStub.lastCall.args[4], '#message-a12345');
- assert.isTrue(replaceStateStub.called);
- });
-
- suite('plugins adding to file tab', () => {
- setup(done => {
- // Resolving it here instead of during setup() as other tests depend
- // on flush() not being called during setup.
- flush(() => done());
- });
-
- test('plugin added tab shows up as a dynamic endpoint', () => {
- assert(element._dynamicTabHeaderEndpoints.includes(
- 'change-view-tab-header-url'));
- const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
- // 3 Tabs are : Files, Plugin, Findings
- assert.equal(paperTabs.querySelectorAll('paper-tab').length, 3);
- assert.equal(paperTabs.querySelectorAll('paper-tab')[1].dataset.name,
- 'change-view-tab-header-url');
- });
-
- test('handleShowTab switched tab correctly', done => {
- const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
- assert.equal(paperTabs.selected, 0);
- element._handleShowTab({detail:
- {tab: 'change-view-tab-header-url'}});
- flush(() => {
- assert.equal(paperTabs.selected, 1);
- done();
- });
- });
-
- test('switching tab sets _selectedTabPluginEndpoint', done => {
- const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
- MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[1]);
- flush(() => {
- assert.equal(element._selectedTabPluginEndpoint,
- 'change-view-tab-content-url');
- done();
- });
- });
- });
-
- suite('keyboard shortcuts', () => {
- test('t to add topic', () => {
- const editStub = sandbox.stub(element.$.metadata, 'editTopic');
- MockInteractions.pressAndReleaseKeyOn(element, 83, null, 't');
- assert(editStub.called);
- });
-
- test('S should toggle the CL star', () => {
- const starStub = sandbox.stub(element.$.changeStar, 'toggleStar');
- MockInteractions.pressAndReleaseKeyOn(element, 83, null, 's');
- assert(starStub.called);
- });
-
- test('U should navigate to root if no backPage set', () => {
- const relativeNavStub = sandbox.stub(Gerrit.Nav,
- 'navigateToRelativeUrl');
- MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
- assert.isTrue(relativeNavStub.called);
- assert.isTrue(relativeNavStub.lastCall.calledWithExactly(
- Gerrit.Nav.getUrlForRoot()));
- });
-
- test('U should navigate to backPage if set', () => {
- const relativeNavStub = sandbox.stub(Gerrit.Nav,
- 'navigateToRelativeUrl');
- element.backPage = '/dashboard/self';
- MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
- assert.isTrue(relativeNavStub.called);
- assert.isTrue(relativeNavStub.lastCall.calledWithExactly(
- '/dashboard/self'));
- });
-
- test('A fires an error event when not logged in', done => {
- sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(false));
- const loggedInErrorSpy = sandbox.spy();
- element.addEventListener('show-auth-required', loggedInErrorSpy);
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
- flush(() => {
- assert.isFalse(element.$.replyOverlay.opened);
- assert.isTrue(loggedInErrorSpy.called);
- done();
- });
- });
-
- test('shift A does not open reply overlay', done => {
- sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
- MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
- flush(() => {
- assert.isFalse(element.$.replyOverlay.opened);
- done();
- });
- });
-
- test('A toggles overlay when logged in', done => {
- sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
- sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates')
- .returns(Promise.resolve({isLatest: true}));
- element._change = {labels: {}};
- const openSpy = sandbox.spy(element, '_openReplyDialog');
-
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
- flush(() => {
- assert.isTrue(element.$.replyOverlay.opened);
- element.$.replyOverlay.close();
- assert.isFalse(element.$.replyOverlay.opened);
- assert(openSpy.lastCall.calledWithExactly(
- element.$.replyDialog.FocusTarget.ANY),
- '_openReplyDialog should have been passed ANY');
- assert.equal(openSpy.callCount, 1);
- done();
- });
- });
-
- test('fullscreen-overlay-opened hides content', () => {
- element._loggedIn = true;
- element._loading = false;
- element._change = {
- owner: {_account_id: 1},
- labels: {},
- actions: {
- abandon: {
- enabled: true,
- label: 'Abandon',
- method: 'POST',
- title: 'Abandon',
- },
- },
- };
- sandbox.spy(element, '_handleHideBackgroundContent');
- element.$.replyDialog.fire('fullscreen-overlay-opened');
- assert.isTrue(element._handleHideBackgroundContent.called);
- assert.isTrue(element.$.mainContent.classList.contains('overlayOpen'));
- assert.equal(getComputedStyle(element.$.actions).display, 'flex');
- });
-
- test('fullscreen-overlay-closed shows content', () => {
- element._loggedIn = true;
- element._loading = false;
- element._change = {
- owner: {_account_id: 1},
- labels: {},
- actions: {
- abandon: {
- enabled: true,
- label: 'Abandon',
- method: 'POST',
- title: 'Abandon',
- },
- },
- };
- sandbox.spy(element, '_handleShowBackgroundContent');
- element.$.replyDialog.fire('fullscreen-overlay-closed');
- assert.isTrue(element._handleShowBackgroundContent.called);
- assert.isFalse(element.$.mainContent.classList.contains('overlayOpen'));
- });
-
- test('expand all messages when expand-diffs fired', () => {
- const handleExpand =
- sandbox.stub(element.$.fileList, 'expandAllDiffs');
- element.$.fileListHeader.fire('expand-diffs');
- assert.isTrue(handleExpand.called);
- });
-
- test('collapse all messages when collapse-diffs fired', () => {
- const handleCollapse =
- sandbox.stub(element.$.fileList, 'collapseAllDiffs');
- element.$.fileListHeader.fire('collapse-diffs');
- assert.isTrue(handleCollapse.called);
- });
-
- test('X should expand all messages', done => {
- flush(() => {
- const handleExpand = sandbox.stub(element.messagesList,
- 'handleExpandCollapse');
- MockInteractions.pressAndReleaseKeyOn(element, 88, null, 'x');
- assert(handleExpand.calledWith(true));
- done();
- });
- });
-
- test('Z should collapse all messages', done => {
- flush(() => {
- const handleExpand = sandbox.stub(element.messagesList,
- 'handleExpandCollapse');
- MockInteractions.pressAndReleaseKeyOn(element, 90, null, 'z');
- assert(handleExpand.calledWith(false));
- done();
- });
- });
-
- test('shift + R should fetch and navigate to the latest patch set',
- done => {
- element._changeNum = '42';
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 1,
- };
- element._change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- _number: 42,
- revisions: {
- rev1: {_number: 1, commit: {parents: []}},
- },
- current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- actions: {},
- };
-
- navigateToChangeStub.restore();
- navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange',
- (change, patchNum, basePatchNum) => {
- assert.equal(change, element._change);
- assert.isUndefined(patchNum);
- assert.isUndefined(basePatchNum);
- done();
- });
-
- MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
- });
-
- test('d should open download overlay', () => {
- const stub = sandbox.stub(element.$.downloadOverlay, 'open');
- MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd');
- assert.isTrue(stub.called);
- });
-
- test(', should open diff preferences', () => {
- const stub = sandbox.stub(
- element.$.fileList.$.diffPreferencesDialog, 'open');
- element._loggedIn = false;
- element.disableDiffPrefs = true;
- MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
- assert.isFalse(stub.called);
-
- element._loggedIn = true;
- MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
- assert.isFalse(stub.called);
-
- element.disableDiffPrefs = false;
- MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
- assert.isTrue(stub.called);
- });
-
- test('m should toggle diff mode', () => {
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- const setModeStub = sandbox.stub(element.$.fileListHeader,
- 'setDiffViewMode');
- const e = {preventDefault: () => {}};
- flushAsynchronousOperations();
-
- element.viewState.diffMode = 'SIDE_BY_SIDE';
- element._handleToggleDiffMode(e);
- assert.isTrue(setModeStub.calledWith('UNIFIED_DIFF'));
-
- element.viewState.diffMode = 'UNIFIED_DIFF';
- element._handleToggleDiffMode(e);
- assert.isTrue(setModeStub.calledWith('SIDE_BY_SIDE'));
- });
- });
-
- suite('reloading drafts', () => {
- let reloadStub;
- const drafts = {
- 'testfile.txt': [
- {
- patch_set: 5,
- id: 'dd2982f5_c01c9e6a',
- line: 1,
- updated: '2017-11-08 18:47:45.000000000',
- message: 'test',
- unresolved: true,
- },
- ],
- };
- setup(() => {
- // Fake computeDraftCount as its required for ChangeComments,
- // see gr-comment-api#reloadDrafts.
- reloadStub = sandbox.stub(element.$.commentAPI, 'reloadDrafts')
- .returns(Promise.resolve({
- drafts,
- getAllThreadsForChange: () => ([]),
- computeDraftCount: () => 1,
- }));
- });
-
- test('drafts are reloaded when reload-drafts fired', done => {
- element.$.fileList.fire('reload-drafts', {
- resolve: () => {
- assert.isTrue(reloadStub.called);
- assert.deepEqual(element._diffDrafts, drafts);
- done();
- },
- });
- });
-
- test('drafts are reloaded when comment-refresh fired', () => {
- element.fire('comment-refresh');
- assert.isTrue(reloadStub.called);
- });
- });
-
- test('diff comments modified', () => {
- sandbox.spy(element, '_handleReloadCommentThreads');
- return element._reloadComments().then(() => {
- element.fire('diff-comments-modified');
- assert.isTrue(element._handleReloadCommentThreads.called);
- });
- });
-
- test('thread list modified', () => {
- sandbox.spy(element, '_handleReloadDiffComments');
- element._currentView = CommentTabs.COMMENT_THREADS;
- flushAsynchronousOperations();
-
- return element._reloadComments().then(() => {
- element.threadList.fire('thread-list-modified');
- assert.isTrue(element._handleReloadDiffComments.called);
-
- let draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
- .returns(1);
- assert.equal(element._computeTotalCommentCounts(5,
- element._changeComments), '5 unresolved, 1 draft');
- assert.equal(element._computeTotalCommentCounts(0,
- element._changeComments), '1 draft');
- draftStub.restore();
- draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
- .returns(0);
- assert.equal(element._computeTotalCommentCounts(0,
- element._changeComments), '');
- assert.equal(element._computeTotalCommentCounts(1,
- element._changeComments), '1 unresolved');
- draftStub.restore();
- draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
- .returns(2);
- assert.equal(element._computeTotalCommentCounts(1,
- element._changeComments), '1 unresolved, 2 drafts');
- draftStub.restore();
- });
- });
-
- suite('thread list and change log tabs', () => {
- setup(() => {
- element._changeNum = '1';
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 1,
- };
- element._change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev2: {_number: 2, commit: {parents: []}},
- rev1: {_number: 1, commit: {parents: []}},
- rev13: {_number: 13, commit: {parents: []}},
- rev3: {_number: 3, commit: {parents: []}},
- },
- current_revision: 'rev3',
- status: 'NEW',
- labels: {
- test: {
- all: [],
- default_value: 0,
- values: [],
- approved: {},
- },
- },
- };
- sandbox.stub(element.$.relatedChanges, 'reload');
- sandbox.stub(element, '_reload').returns(Promise.resolve());
- sandbox.spy(element, '_paramsChanged');
- element.params = {view: 'change', changeNum: '1'};
- });
-
- test('tab switch works correctly', done => {
- assert.isTrue(element._paramsChanged.called);
- assert.equal(element.$.commentTabs.selected, CommentTabs.CHANGE_LOG);
- assert.equal(element._currentView, CommentTabs.CHANGE_LOG);
-
- const commentTab = element.shadowRoot.querySelector(
- 'paper-tab.commentThreads'
- );
- // Switch to comment thread tab
- MockInteractions.tap(commentTab);
- const commentTabs = element.$.commentTabs;
- assert.equal(commentTabs.selected,
- CommentTabs.COMMENT_THREADS);
- assert.equal(element._currentView, CommentTabs.COMMENT_THREADS);
-
- // Switch back to 'Change Log' tab
- element._paramsChanged(element.params);
- flush(() => {
- assert.equal(commentTabs.selected,
- CommentTabs.CHANGE_LOG);
- assert.equal(element._currentView, CommentTabs.CHANGE_LOG);
- done();
- });
- });
- });
-
- suite('Findings comment tab', () => {
- setup(done => {
- element._change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev2: {_number: 2, commit: {parents: []}},
- rev1: {_number: 1, commit: {parents: []}},
- rev13: {_number: 13, commit: {parents: []}},
- rev3: {_number: 3, commit: {parents: []}},
- rev4: {_number: 4, commit: {parents: []}},
- },
- current_revision: 'rev4',
- };
- element._commentThreads = THREADS;
- const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
- MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[2]);
- flush(() => {
- done();
- });
- });
-
- test('robot comments count per patchset', () => {
- const count = element._robotCommentCountPerPatchSet(THREADS);
- const expectedCount = {
- 2: 1,
- 3: 1,
- 4: 2,
- };
- assert.deepEqual(count, expectedCount);
- assert.equal(element._computeText({_number: 2}, THREADS),
- 'Patchset 2 (1 finding)');
- assert.equal(element._computeText({_number: 4}, THREADS),
- 'Patchset 4 (2 findings)');
- assert.equal(element._computeText({_number: 5}, THREADS),
- 'Patchset 5');
- });
-
- test('only robot comments are rendered', () => {
- assert.equal(element._robotCommentThreads.length, 2);
- assert.equal(element._robotCommentThreads[0].comments[0].robot_id,
- 'rc1');
- assert.equal(element._robotCommentThreads[1].comments[0].robot_id,
- 'rc2');
- });
-
- test('changing patchsets resets robot comments', done => {
- element.set('_change.current_revision', 'rev3');
- flush(() => {
- assert.equal(element._robotCommentThreads.length, 1);
- done();
- });
- });
-
- test('Show more button is hidden', () => {
- assert.isNull(element.shadowRoot.querySelector('.show-robot-comments'));
- });
-
- suite('robot comments show more button', () => {
- setup(done => {
- const arr = [];
- for (let i = 0; i <= 30; i++) {
- arr.push(...THREADS);
- }
- element._commentThreads = arr;
- flush(() => {
- done();
- });
- });
-
- test('Show more button is rendered', () => {
- assert.isOk(element.shadowRoot.querySelector('.show-robot-comments'));
- assert.equal(element._robotCommentThreads.length,
- ROBOT_COMMENTS_LIMIT);
- });
-
- test('Clicking show more button renders all comments', done => {
- MockInteractions.tap(element.shadowRoot.querySelector(
- '.show-robot-comments'));
- flush(() => {
- assert.equal(element._robotCommentThreads.length, 62);
- done();
- });
- });
- });
- });
-
- test('reply button is not visible when logged out', () => {
- assert.equal(getComputedStyle(element.$.replyBtn).display, 'none');
- element._loggedIn = true;
- assert.notEqual(getComputedStyle(element.$.replyBtn).display, 'none');
- });
-
- test('download tap calls _handleOpenDownloadDialog', () => {
- sandbox.stub(element, '_handleOpenDownloadDialog');
- element.$.actions.fire('download-tap');
- assert.isTrue(element._handleOpenDownloadDialog.called);
- });
-
- test('fetches the server config on attached', done => {
- flush(() => {
- assert.equal(element._serverConfig.test, 'config');
- done();
- });
- });
-
- test('_changeStatuses', () => {
- sandbox.stub(element, 'changeStatuses').returns(
- ['Merged', 'WIP']);
- element._loading = false;
element._change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
- rev2: {_number: 2},
- rev1: {_number: 1},
- rev13: {_number: 13},
- rev3: {_number: 3},
- },
- current_revision: 'rev3',
- labels: {
- test: {
- all: [],
- default_value: 0,
- values: [],
- approved: {},
- },
- },
- };
- element._mergeable = true;
- const expectedStatuses = ['Merged', 'WIP'];
- assert.deepEqual(element._changeStatuses, expectedStatuses);
- assert.equal(element._changeStatus, expectedStatuses.join(', '));
- flushAsynchronousOperations();
- const statusChips = Polymer.dom(element.root)
- .querySelectorAll('gr-change-status');
- assert.equal(statusChips.length, 2);
- });
-
- test('diff preferences open when open-diff-prefs is fired', () => {
- const overlayOpenStub = sandbox.stub(element.$.fileList,
- 'openDiffPrefs');
- element.$.fileListHeader.fire('open-diff-prefs');
- assert.isTrue(overlayOpenStub.called);
- });
-
- test('_prepareCommitMsgForLinkify', () => {
- let commitMessage = 'R=test@google.com';
- let result = element._prepareCommitMsgForLinkify(commitMessage);
- assert.equal(result, 'R=\u200Btest@google.com');
-
- commitMessage = 'R=test@google.com\nR=test@google.com';
- result = element._prepareCommitMsgForLinkify(commitMessage);
- assert.equal(result, 'R=\u200Btest@google.com\nR=\u200Btest@google.com');
-
- commitMessage = 'CC=test@google.com';
- result = element._prepareCommitMsgForLinkify(commitMessage);
- assert.equal(result, 'CC=\u200Btest@google.com');
- }),
-
- test('_isSubmitEnabled', () => {
- assert.isFalse(element._isSubmitEnabled({}));
- assert.isFalse(element._isSubmitEnabled({submit: {}}));
- assert.isTrue(element._isSubmitEnabled(
- {submit: {enabled: true}}));
- });
-
- test('_reload is called when an approved label is removed', () => {
- const vote = {_account_id: 1, name: 'bojack', value: 1};
- element._changeNum = '42';
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 1,
- };
- element._change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- owner: {email: 'abc@def'},
- revisions: {
rev2: {_number: 2, commit: {parents: []}},
rev1: {_number: 1, commit: {parents: []}},
rev13: {_number: 13, commit: {parents: []}},
@@ -934,1312 +706,1536 @@
status: 'NEW',
labels: {
test: {
- all: [vote],
+ all: [],
default_value: 0,
values: [],
approved: {},
},
},
};
- flushAsynchronousOperations();
- const reloadStub = sandbox.stub(element, '_reload');
- element.splice('_change.labels.test.all', 0, 1);
- assert.isFalse(reloadStub.called);
- element._change.labels.test.all.push(vote);
- element._change.labels.test.all.push(vote);
- element._change.labels.test.approved = vote;
- flushAsynchronousOperations();
- element.splice('_change.labels.test.all', 0, 2);
- assert.isTrue(reloadStub.called);
- assert.isTrue(reloadStub.calledOnce);
- });
-
- test('reply button has updated count when there are drafts', () => {
- const getLabel = element._computeReplyButtonLabel;
-
- assert.equal(getLabel(null, false), 'Reply');
- assert.equal(getLabel(null, true), 'Start review');
-
- const changeRecord = {base: null};
- assert.equal(getLabel(changeRecord, false), 'Reply');
-
- changeRecord.base = {};
- assert.equal(getLabel(changeRecord, false), 'Reply');
-
- changeRecord.base = {
- 'file1.txt': [{}],
- 'file2.txt': [{}, {}],
- };
- assert.equal(getLabel(changeRecord, false), 'Reply (3)');
- });
-
- test('start review button when owner of WIP change', () => {
- assert.equal(
- element._computeReplyButtonLabel(null, true),
- 'Start review');
- });
-
- test('comment events properly update diff drafts', () => {
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 2,
- };
- const draft = {
- __draft: true,
- id: 'id1',
- path: '/foo/bar.txt',
- text: 'hello',
- };
- element._handleCommentSave({detail: {comment: draft}});
- draft.patch_set = 2;
- assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]});
- draft.patch_set = null;
- draft.text = 'hello, there';
- element._handleCommentSave({detail: {comment: draft}});
- draft.patch_set = 2;
- assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]});
- const draft2 = {
- __draft: true,
- id: 'id2',
- path: '/foo/bar.txt',
- text: 'hola',
- };
- element._handleCommentSave({detail: {comment: draft2}});
- draft2.patch_set = 2;
- assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft, draft2]});
- draft.patch_set = null;
- element._handleCommentDiscard({detail: {comment: draft}});
- draft.patch_set = 2;
- assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft2]});
- element._handleCommentDiscard({detail: {comment: draft2}});
- assert.deepEqual(element._diffDrafts, {});
- });
-
- test('change num change', () => {
- element._changeNum = null;
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 2,
- };
- element._change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- labels: {},
- };
- element.viewState.changeNum = null;
- element.viewState.diffMode = 'UNIFIED';
- assert.equal(element.viewState.numFilesShown, 200);
- assert.equal(element._numFilesShown, 200);
- element._numFilesShown = 150;
- flushAsynchronousOperations();
- assert.equal(element.viewState.diffMode, 'UNIFIED');
- assert.equal(element.viewState.numFilesShown, 150);
-
- element._changeNum = '1';
- element.params = {changeNum: '1'};
- element._change.newProp = '1';
- flushAsynchronousOperations();
- assert.equal(element.viewState.diffMode, 'UNIFIED');
- assert.equal(element.viewState.changeNum, '1');
-
- element._changeNum = '2';
- element.params = {changeNum: '2'};
- element._change.newProp = '2';
- flushAsynchronousOperations();
- assert.equal(element.viewState.diffMode, 'UNIFIED');
- assert.equal(element.viewState.changeNum, '2');
- assert.equal(element.viewState.numFilesShown, 200);
- assert.equal(element._numFilesShown, 200);
- });
-
- test('_setDiffViewMode is called with reset when new change is loaded',
- () => {
- sandbox.stub(element, '_setDiffViewMode');
- element.viewState = {changeNum: 1};
- element._changeNum = 2;
- element._resetFileListViewState();
- assert.isTrue(
- element._setDiffViewMode.lastCall.calledWithExactly(true));
- });
-
- test('diffViewMode is propagated from file list header', () => {
- element.viewState = {diffMode: 'UNIFIED'};
- element.$.fileListHeader.diffViewMode = 'SIDE_BY_SIDE';
- assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
- });
-
- test('diffMode defaults to side by side without preferences', done => {
- sandbox.stub(element.$.restAPI, 'getPreferences').returns(
- Promise.resolve({}));
- // No user prefs or diff view mode set.
-
- element._setDiffViewMode().then(() => {
- assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
- done();
- });
- });
-
- test('diffMode defaults to preference when not already set', done => {
- sandbox.stub(element.$.restAPI, 'getPreferences').returns(
- Promise.resolve({default_diff_view: 'UNIFIED'}));
-
- element._setDiffViewMode().then(() => {
- assert.equal(element.viewState.diffMode, 'UNIFIED');
- done();
- });
- });
-
- test('existing diffMode overrides preference', done => {
- element.viewState.diffMode = 'SIDE_BY_SIDE';
- sandbox.stub(element.$.restAPI, 'getPreferences').returns(
- Promise.resolve({default_diff_view: 'UNIFIED'}));
- element._setDiffViewMode().then(() => {
- assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
- done();
- });
- });
-
- test('don’t reload entire page when patchRange changes', () => {
- const reloadStub = sandbox.stub(element, '_reload',
- () => Promise.resolve());
- const reloadPatchDependentStub = sandbox.stub(element,
- '_reloadPatchNumDependentResources',
- () => Promise.resolve());
- const relatedClearSpy = sandbox.spy(element.$.relatedChanges, 'clear');
- const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
-
- const value = {
- view: Gerrit.Nav.View.CHANGE,
- patchNum: '1',
- };
- element._paramsChanged(value);
- assert.isTrue(reloadStub.calledOnce);
- assert.isTrue(relatedClearSpy.calledOnce);
-
- element._initialLoadComplete = true;
-
- value.basePatchNum = '1';
- value.patchNum = '2';
- element._paramsChanged(value);
- assert.isFalse(reloadStub.calledTwice);
- assert.isTrue(reloadPatchDependentStub.calledOnce);
- assert.isTrue(relatedClearSpy.calledOnce);
- assert.isTrue(collapseStub.calledTwice);
- });
-
- test('reload entire page when patchRange doesnt change', () => {
- const reloadStub = sandbox.stub(element, '_reload',
- () => Promise.resolve());
- const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
- const value = {
- view: Gerrit.Nav.View.CHANGE,
- };
- element._paramsChanged(value);
- assert.isTrue(reloadStub.calledOnce);
- element._initialLoadComplete = true;
- element._paramsChanged(value);
- assert.isTrue(reloadStub.calledTwice);
- assert.isTrue(collapseStub.calledTwice);
- });
-
- test('related changes are updated and new patch selected after rebase',
- done => {
- element._changeNum = '42';
- sandbox.stub(element, 'computeLatestPatchNum', () => 1);
- sandbox.stub(element, '_reload',
- () => Promise.resolve());
- const e = {detail: {action: 'rebase'}};
- element._handleReloadChange(e).then(() => {
- assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
- element._change));
- done();
- });
- });
-
- test('related changes are not updated after other action', done => {
- sandbox.stub(element, '_reload', () => Promise.resolve());
- sandbox.stub(element.$.relatedChanges, 'reload');
- const e = {detail: {action: 'abandon'}};
- element._handleReloadChange(e).then(() => {
- assert.isFalse(navigateToChangeStub.called);
- done();
- });
- });
-
- test('_computeMergedCommitInfo', () => {
- const dummyRevs = {
- 1: {commit: {commit: 1}},
- 2: {commit: {}},
- };
- assert.deepEqual(element._computeMergedCommitInfo(0, dummyRevs), {});
- assert.deepEqual(element._computeMergedCommitInfo(1, dummyRevs),
- dummyRevs[1].commit);
-
- // Regression test for issue 5337.
- const commit = element._computeMergedCommitInfo(2, dummyRevs);
- assert.notDeepEqual(commit, dummyRevs[2]);
- assert.deepEqual(commit, {commit: 2});
- });
-
- test('_computeCopyTextForTitle', () => {
- const change = {
- _number: 123,
- subject: 'test subject',
- revisions: {
- rev1: {_number: 1},
- rev3: {_number: 3},
- },
- current_revision: 'rev3',
- };
- sandbox.stub(Gerrit.Nav, 'getUrlForChange')
- .returns('/change/123');
- assert.equal(
- element._computeCopyTextForTitle(change),
- '123: test subject | https://localhost:8081/change/123'
- );
- });
-
- test('get latest revision', () => {
- let change = {
- revisions: {
- rev1: {_number: 1},
- rev3: {_number: 3},
- },
- current_revision: 'rev3',
- };
- assert.equal(element._getLatestRevisionSHA(change), 'rev3');
- change = {
- revisions: {
- rev1: {_number: 1},
- },
- };
- assert.equal(element._getLatestRevisionSHA(change), 'rev1');
- });
-
- test('show commit message edit button', () => {
- const _change = {
- status: element.ChangeStatus.MERGED,
- };
- assert.isTrue(element._computeHideEditCommitMessage(false, false, {}));
- assert.isTrue(element._computeHideEditCommitMessage(true, true, {}));
- assert.isTrue(element._computeHideEditCommitMessage(false, true, {}));
- assert.isFalse(element._computeHideEditCommitMessage(true, false, {}));
- assert.isTrue(element._computeHideEditCommitMessage(true, false,
- _change));
- assert.isTrue(element._computeHideEditCommitMessage(true, false, {},
- true));
- assert.isFalse(element._computeHideEditCommitMessage(true, false, {},
- false));
- });
-
- test('_handleCommitMessageSave trims trailing whitespace', () => {
- const putStub = sandbox.stub(element.$.restAPI, 'putChangeCommitMessage')
- .returns(Promise.resolve({}));
-
- const mockEvent = content => { return {detail: {content}}; };
-
- element._handleCommitMessageSave(mockEvent('test \n test '));
- assert.equal(putStub.lastCall.args[1], 'test\n test');
-
- element._handleCommitMessageSave(mockEvent(' test\ntest'));
- assert.equal(putStub.lastCall.args[1], ' test\ntest');
-
- element._handleCommitMessageSave(mockEvent('\n\n\n\n\n\n\n\n'));
- assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n');
- });
-
- test('_computeChangeIdCommitMessageError', () => {
- let commitMessage =
- 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483';
- let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'};
- assert.equal(
- element._computeChangeIdCommitMessageError(commitMessage, change),
- null);
-
- change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'};
- assert.equal(
- element._computeChangeIdCommitMessageError(commitMessage, change),
- 'mismatch');
-
- commitMessage = 'This is the greatest change.';
- assert.equal(
- element._computeChangeIdCommitMessageError(commitMessage, change),
- 'missing');
- });
-
- test('multiple change Ids in commit message picks last', () => {
- const commitMessage = [
- 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484',
- 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483',
- ].join('\n');
- let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'};
- assert.equal(
- element._computeChangeIdCommitMessageError(commitMessage, change),
- null);
- change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'};
- assert.equal(
- element._computeChangeIdCommitMessageError(commitMessage, change),
- 'mismatch');
- });
-
- test('does not count change Id that starts mid line', () => {
- const commitMessage = [
- 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484',
- 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483',
- ].join(' and ');
- let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'};
- assert.equal(
- element._computeChangeIdCommitMessageError(commitMessage, change),
- null);
- change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'};
- assert.equal(
- element._computeChangeIdCommitMessageError(commitMessage, change),
- 'mismatch');
- });
-
- test('_computeTitleAttributeWarning', () => {
- let changeIdCommitMessageError = 'missing';
- assert.equal(
- element._computeTitleAttributeWarning(changeIdCommitMessageError),
- 'No Change-Id in commit message');
-
- changeIdCommitMessageError = 'mismatch';
- assert.equal(
- element._computeTitleAttributeWarning(changeIdCommitMessageError),
- 'Change-Id mismatch');
- });
-
- test('_computeChangeIdClass', () => {
- let changeIdCommitMessageError = 'missing';
- assert.equal(
- element._computeChangeIdClass(changeIdCommitMessageError), '');
-
- changeIdCommitMessageError = 'mismatch';
- assert.equal(
- element._computeChangeIdClass(changeIdCommitMessageError), 'warning');
- });
-
- test('topic is coalesced to null', done => {
- sandbox.stub(element, '_changeChanged');
- sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
- id: '123456789',
- labels: {},
- current_revision: 'foo',
- revisions: {foo: {commit: {}}},
- }));
-
- element._getChangeDetail().then(() => {
- assert.isNull(element._change.topic);
- done();
- });
- });
-
- test('commit sha is populated from getChangeDetail', done => {
- sandbox.stub(element, '_changeChanged');
- sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
- id: '123456789',
- labels: {},
- current_revision: 'foo',
- revisions: {foo: {commit: {}}},
- }));
-
- element._getChangeDetail().then(() => {
- assert.equal('foo', element._commitInfo.commit);
- done();
- });
- });
-
- test('edit is added to change', () => {
- sandbox.stub(element, '_changeChanged');
- sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
- id: '123456789',
- labels: {},
- current_revision: 'foo',
- revisions: {foo: {commit: {}}},
- }));
- sandbox.stub(element, '_getEdit', () => Promise.resolve({
- base_patch_set_number: 1,
- commit: {commit: 'bar'},
- }));
- element._patchRange = {};
-
- return element._getChangeDetail().then(() => {
- const revs = element._change.revisions;
- assert.equal(Object.keys(revs).length, 2);
- assert.deepEqual(revs['foo'], {commit: {commit: 'foo'}});
- assert.deepEqual(revs['bar'], {
- _number: element.EDIT_NAME,
- basePatchNum: 1,
- commit: {commit: 'bar'},
- fetch: undefined,
- });
- });
- });
-
- test('_getBasePatchNum', () => {
- const _change = {
- _number: 42,
- revisions: {
- '98da160735fb81604b4c40e93c368f380539dd0e': {
- _number: 1,
- commit: {
- parents: [],
- },
- },
- },
- };
- const _patchRange = {
- basePatchNum: 'PARENT',
- };
- assert.equal(element._getBasePatchNum(_change, _patchRange), 'PARENT');
-
- element._prefs = {
- default_base_for_merges: 'FIRST_PARENT',
- };
-
- const _change2 = {
- _number: 42,
- revisions: {
- '98da160735fb81604b4c40e93c368f380539dd0e': {
- _number: 1,
- commit: {
- parents: [
- {
- commit: '6e12bdf1176eb4ab24d8491ba3b6d0704409cde8',
- subject: 'test',
- },
- {
- commit: '22f7db4754b5d9816fc581f3d9a6c0ef8429c841',
- subject: 'test3',
- },
- ],
- },
- },
- },
- };
- assert.equal(element._getBasePatchNum(_change2, _patchRange), -1);
-
- _patchRange.patchNum = 1;
- assert.equal(element._getBasePatchNum(_change2, _patchRange), 'PARENT');
- });
-
- test('_openReplyDialog called with `ANY` when coming from tap event',
- () => {
- const openStub = sandbox.stub(element, '_openReplyDialog');
- element._serverConfig = {};
- MockInteractions.tap(element.$.replyBtn);
- assert(openStub.lastCall.calledWithExactly(
- element.$.replyDialog.FocusTarget.ANY),
- '_openReplyDialog should have been passed ANY');
- assert.equal(openStub.callCount, 1);
- });
-
- test('_openReplyDialog called with `BODY` when coming from message reply' +
- 'event', done => {
- flush(() => {
- const openStub = sandbox.stub(element, '_openReplyDialog');
- element.messagesList.fire('reply',
- {message: {message: 'text'}});
- assert(openStub.lastCall.calledWithExactly(
- element.$.replyDialog.FocusTarget.BODY),
- '_openReplyDialog should have been passed BODY');
- assert.equal(openStub.callCount, 1);
- done();
- });
- });
-
- test('reply dialog focus can be controlled', () => {
- const FocusTarget = element.$.replyDialog.FocusTarget;
- const openStub = sandbox.stub(element, '_openReplyDialog');
-
- const e = {detail: {}};
- element._handleShowReplyDialog(e);
- assert(openStub.lastCall.calledWithExactly(FocusTarget.REVIEWERS),
- '_openReplyDialog should have been passed REVIEWERS');
- assert.equal(openStub.callCount, 1);
-
- e.detail.value = {ccsOnly: true};
- element._handleShowReplyDialog(e);
- assert(openStub.lastCall.calledWithExactly(FocusTarget.CCS),
- '_openReplyDialog should have been passed CCS');
- assert.equal(openStub.callCount, 2);
- });
-
- test('getUrlParameter functionality', () => {
- const locationStub = sandbox.stub(element, '_getLocationSearch');
-
- locationStub.returns('?test');
- assert.equal(element._getUrlParameter('test'), 'test');
- locationStub.returns('?test2=12&test=3');
- assert.equal(element._getUrlParameter('test'), 'test');
- locationStub.returns('');
- assert.isNull(element._getUrlParameter('test'));
- locationStub.returns('?');
- assert.isNull(element._getUrlParameter('test'));
- locationStub.returns('?test2');
- assert.isNull(element._getUrlParameter('test'));
- });
-
- test('revert dialog opened with revert param', done => {
- sandbox.stub(element.$.restAPI, 'getLoggedIn', () => Promise.resolve(true));
- sandbox.stub(Gerrit, 'awaitPluginsLoaded', () => Promise.resolve());
-
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 2,
- };
- element._change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1, commit: {parents: []}},
- rev2: {_number: 2, commit: {parents: []}},
- },
- current_revision: 'rev1',
- status: element.ChangeStatus.MERGED,
- labels: {},
- actions: {},
- };
-
- sandbox.stub(element, '_getUrlParameter',
- param => {
- assert.equal(param, 'revert');
- return param;
- });
-
- sandbox.stub(element.$.actions, 'showRevertDialog',
- done);
-
- element._maybeShowRevertDialog();
- assert.isTrue(Gerrit.awaitPluginsLoaded.called);
- });
-
- suite('scroll related tests', () => {
- test('document scrolling calls function to set scroll height', done => {
- const originalHeight = document.body.scrollHeight;
- const scrollStub = sandbox.stub(element, '_handleScroll',
- () => {
- assert.isTrue(scrollStub.called);
- document.body.style.height = originalHeight + 'px';
- scrollStub.restore();
- done();
- });
- document.body.style.height = '10000px';
- element._handleScroll();
- });
-
- test('scrollTop is set correctly', () => {
- element.viewState = {scrollTop: TEST_SCROLL_TOP_PX};
-
- sandbox.stub(element, '_reload', () => {
- // When element is reloaded, ensure that the history
- // state has the scrollTop set earlier. This will then
- // be reset.
- assert.isTrue(element.viewState.scrollTop == TEST_SCROLL_TOP_PX);
- return Promise.resolve({});
- });
-
- // simulate reloading component, which is done when route
- // changes to match a regex of change view type.
- element._paramsChanged({view: Gerrit.Nav.View.CHANGE});
- });
-
- test('scrollTop is reset when new change is loaded', () => {
- element._resetFileListViewState();
- assert.equal(element.viewState.scrollTop, 0);
- });
- });
-
- suite('reply dialog tests', () => {
- setup(() => {
- sandbox.stub(element.$.replyDialog, '_draftChanged');
- sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates',
- () => Promise.resolve({isLatest: true}));
- element._change = {labels: {}};
- });
-
- test('reply from comment adds quote text', () => {
- const e = {detail: {message: {message: 'quote text'}}};
- element._handleMessageReply(e);
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
- });
-
- test('reply from comment replaces quote text', () => {
- element.$.replyDialog.draft = '> old quote text\n\n some draft text';
- element.$.replyDialog.quote = '> old quote text\n\n';
- const e = {detail: {message: {message: 'quote text'}}};
- element._handleMessageReply(e);
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
- });
-
- test('reply from same comment preserves quote text', () => {
- element.$.replyDialog.draft = '> quote text\n\n some draft text';
- element.$.replyDialog.quote = '> quote text\n\n';
- const e = {detail: {message: {message: 'quote text'}}};
- element._handleMessageReply(e);
- assert.equal(element.$.replyDialog.draft,
- '> quote text\n\n some draft text');
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
- });
-
- test('reply from top of page contains previous draft', () => {
- const div = document.createElement('div');
- element.$.replyDialog.draft = '> quote text\n\n some draft text';
- element.$.replyDialog.quote = '> quote text\n\n';
- const e = {target: div, preventDefault: sandbox.spy()};
- element._handleReplyTap(e);
- assert.equal(element.$.replyDialog.draft,
- '> quote text\n\n some draft text');
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
- });
- });
-
- test('reply button is disabled until server config is loaded', () => {
- assert.isTrue(element._replyDisabled);
- element._serverConfig = {};
- assert.isFalse(element._replyDisabled);
- });
-
- suite('commit message expand/collapse', () => {
- setup(() => {
- sandbox.stub(element, 'fetchChangeUpdates',
- () => Promise.resolve({isLatest: false}));
- });
-
- test('commitCollapseToggle hidden for short commit message', () => {
- element._latestCommitMessage = '';
- assert.isTrue(element.$.commitCollapseToggle.hasAttribute('hidden'));
- });
-
- test('commitCollapseToggle shown for long commit message', () => {
- element._latestCommitMessage = _.times(31, String).join('\n');
- assert.isFalse(element.$.commitCollapseToggle.hasAttribute('hidden'));
- });
-
- test('commitCollapseToggle functions', () => {
- element._latestCommitMessage = _.times(35, String).join('\n');
- assert.isTrue(element._commitCollapsed);
- assert.isTrue(element._commitCollapsible);
- assert.isTrue(
- element.$.commitMessageEditor.hasAttribute('collapsed'));
- MockInteractions.tap(element.$.commitCollapseToggleButton);
- assert.isFalse(element._commitCollapsed);
- assert.isTrue(element._commitCollapsible);
- assert.isFalse(
- element.$.commitMessageEditor.hasAttribute('collapsed'));
- });
- });
-
- suite('related changes expand/collapse', () => {
- let updateHeightSpy;
- setup(() => {
- updateHeightSpy = sandbox.spy(element, '_updateRelatedChangeMaxHeight');
- });
-
- test('relatedChangesToggle shown height greater than changeInfo height',
- () => {
- assert.isFalse(element.$.relatedChangesToggle.classList
- .contains('showToggle'));
- sandbox.stub(element, '_getOffsetHeight', () => 50);
- sandbox.stub(element, '_getScrollHeight', () => 60);
- sandbox.stub(element, '_getLineHeight', () => 5);
- sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
- element.$.relatedChanges.dispatchEvent(
- new CustomEvent('new-section-loaded'));
- assert.isTrue(element.$.relatedChangesToggle.classList
- .contains('showToggle'));
- assert.equal(updateHeightSpy.callCount, 1);
- });
-
- test('relatedChangesToggle hidden height less than changeInfo height',
- () => {
- assert.isFalse(element.$.relatedChangesToggle.classList
- .contains('showToggle'));
- sandbox.stub(element, '_getOffsetHeight', () => 50);
- sandbox.stub(element, '_getScrollHeight', () => 40);
- sandbox.stub(element, '_getLineHeight', () => 5);
- sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
- element.$.relatedChanges.dispatchEvent(
- new CustomEvent('new-section-loaded'));
- assert.isFalse(element.$.relatedChangesToggle.classList
- .contains('showToggle'));
- assert.equal(updateHeightSpy.callCount, 1);
- });
-
- test('relatedChangesToggle functions', () => {
- sandbox.stub(element, '_getOffsetHeight', () => 50);
- sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
- element._relatedChangesLoading = false;
- assert.isTrue(element._relatedChangesCollapsed);
- assert.isTrue(
- element.$.relatedChanges.classList.contains('collapsed'));
- MockInteractions.tap(element.$.relatedChangesToggleButton);
- assert.isFalse(element._relatedChangesCollapsed);
- assert.isFalse(
- element.$.relatedChanges.classList.contains('collapsed'));
- });
-
- test('_updateRelatedChangeMaxHeight without commit toggle', () => {
- sandbox.stub(element, '_getOffsetHeight', () => 50);
- sandbox.stub(element, '_getLineHeight', () => 12);
- sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
-
- // 50 (existing height) - 30 (extra height) = 20 (adjusted height).
- // 20 (max existing height) % 12 (line height) = 6 (remainder).
- // 20 (adjusted height) - 8 (remainder) = 12 (max height to set).
-
- element._updateRelatedChangeMaxHeight();
- assert.equal(getCustomCssValue('--relation-chain-max-height'),
- '12px');
- assert.equal(getCustomCssValue('--related-change-btn-top-padding'),
- '');
- });
-
- test('_updateRelatedChangeMaxHeight with commit toggle', () => {
- element._latestCommitMessage = _.times(31, String).join('\n');
- sandbox.stub(element, '_getOffsetHeight', () => 50);
- sandbox.stub(element, '_getLineHeight', () => 12);
- sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
-
- // 50 (existing height) % 12 (line height) = 2 (remainder).
- // 50 (existing height) - 2 (remainder) = 48 (max height to set).
-
- element._updateRelatedChangeMaxHeight();
- assert.equal(getCustomCssValue('--relation-chain-max-height'),
- '48px');
- assert.equal(getCustomCssValue('--related-change-btn-top-padding'),
- '2px');
- });
-
- test('_updateRelatedChangeMaxHeight in small screen mode', () => {
- element._latestCommitMessage = _.times(31, String).join('\n');
- sandbox.stub(element, '_getOffsetHeight', () => 50);
- sandbox.stub(element, '_getLineHeight', () => 12);
- sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
-
- element._updateRelatedChangeMaxHeight();
-
- // 400 (new height) % 12 (line height) = 4 (remainder).
- // 400 (new height) - 4 (remainder) = 396.
-
- assert.equal(getCustomCssValue('--relation-chain-max-height'),
- '396px');
- });
-
- test('_updateRelatedChangeMaxHeight in medium screen mode', () => {
- element._latestCommitMessage = _.times(31, String).join('\n');
- sandbox.stub(element, '_getOffsetHeight', () => 50);
- sandbox.stub(element, '_getLineHeight', () => 12);
- sandbox.stub(window, 'matchMedia', () => {
- if (window.matchMedia.lastCall.args[0] === '(max-width: 75em)') {
- return {matches: true};
- } else {
- return {matches: false};
- }
- });
-
- // 100 (new height) % 12 (line height) = 4 (remainder).
- // 100 (new height) - 4 (remainder) = 96.
- element._updateRelatedChangeMaxHeight();
- assert.equal(getCustomCssValue('--relation-chain-max-height'),
- '96px');
- });
-
- suite('update checks', () => {
- setup(() => {
- sandbox.spy(element, '_startUpdateCheckTimer');
- sandbox.stub(element, 'async', f => {
- // Only fire the async callback one time.
- if (element.async.callCount > 1) { return; }
- f.call(element);
- });
- });
-
- test('_startUpdateCheckTimer negative delay', () => {
- sandbox.stub(element, 'fetchChangeUpdates');
-
- element._serverConfig = {change: {update_delay: -1}};
-
- assert.isTrue(element._startUpdateCheckTimer.called);
- assert.isFalse(element.fetchChangeUpdates.called);
- });
-
- test('_startUpdateCheckTimer up-to-date', () => {
- sandbox.stub(element, 'fetchChangeUpdates',
- () => Promise.resolve({isLatest: true}));
-
- element._serverConfig = {change: {update_delay: 12345}};
-
- assert.isTrue(element._startUpdateCheckTimer.called);
- assert.isTrue(element.fetchChangeUpdates.called);
- assert.equal(element.async.lastCall.args[1], 12345 * 1000);
- });
-
- test('_startUpdateCheckTimer out-of-date shows an alert', done => {
- sandbox.stub(element, 'fetchChangeUpdates',
- () => Promise.resolve({isLatest: false}));
- element.addEventListener('show-alert', e => {
- assert.equal(e.detail.message,
- 'A newer patch set has been uploaded');
- done();
- });
- element._serverConfig = {change: {update_delay: 12345}};
- });
-
- test('_startUpdateCheckTimer new status shows an alert', done => {
- sandbox.stub(element, 'fetchChangeUpdates')
- .returns(Promise.resolve({
- isLatest: true,
- newStatus: element.ChangeStatus.MERGED,
- }));
- element.addEventListener('show-alert', e => {
- assert.equal(e.detail.message, 'This change has been merged');
- done();
- });
- element._serverConfig = {change: {update_delay: 12345}};
- });
-
- test('_startUpdateCheckTimer new messages shows an alert', done => {
- sandbox.stub(element, 'fetchChangeUpdates')
- .returns(Promise.resolve({
- isLatest: true,
- newMessages: true,
- }));
- element.addEventListener('show-alert', e => {
- assert.equal(e.detail.message,
- 'There are new messages on this change');
- done();
- });
- element._serverConfig = {change: {update_delay: 12345}};
- });
- });
-
- test('canStartReview computation', () => {
- const change1 = {};
- const change2 = {
- actions: {
- ready: {
- enabled: true,
- },
- },
- };
- const change3 = {
- actions: {
- ready: {
- label: 'Ready for Review',
- },
- },
- };
- assert.isFalse(element._computeCanStartReview(change1));
- assert.isTrue(element._computeCanStartReview(change2));
- assert.isFalse(element._computeCanStartReview(change3));
- });
- });
-
- test('header class computation', () => {
- assert.equal(element._computeHeaderClass(), 'header');
- assert.equal(element._computeHeaderClass(true), 'header editMode');
- });
-
- test('_maybeScrollToMessage', done => {
- flush(() => {
- const scrollStub = sandbox.stub(element.messagesList,
- 'scrollToMessage');
-
- element._maybeScrollToMessage('');
- assert.isFalse(scrollStub.called);
- element._maybeScrollToMessage('message');
- assert.isFalse(scrollStub.called);
- element._maybeScrollToMessage('#message-TEST');
- assert.isTrue(scrollStub.called);
- assert.equal(scrollStub.lastCall.args[0], 'TEST');
- done();
- });
- });
-
- test('topic update reloads related changes', () => {
- sandbox.stub(element.$.relatedChanges, 'reload');
- element.dispatchEvent(new CustomEvent('topic-changed'));
- assert.isTrue(element.$.relatedChanges.reload.calledOnce);
- });
-
- test('_computeEditMode', () => {
- const callCompute = (range, params) =>
- element._computeEditMode({base: range}, {base: params});
- assert.isFalse(callCompute({}, {}));
- assert.isTrue(callCompute({}, {edit: true}));
- assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1}, {}));
- assert.isFalse(callCompute({basePatchNum: 'edit', patchNum: 1}, {}));
- assert.isTrue(callCompute({basePatchNum: 1, patchNum: 'edit'}, {}));
- });
-
- test('_processEdit', () => {
- element._patchRange = {};
- const change = {
- current_revision: 'foo',
- revisions: {foo: {commit: {}, actions: {cherrypick: {enabled: true}}}},
- };
- let mockChange;
-
- // With no edit, mockChange should be unmodified.
- element._processEdit(mockChange = _.cloneDeep(change), null);
- assert.deepEqual(mockChange, change);
-
- // When edit is not based on the latest PS, current_revision should be
- // unmodified.
- const edit = {
- base_patch_set_number: 1,
- commit: {commit: 'bar'},
- fetch: true,
- };
- element._processEdit(mockChange = _.cloneDeep(change), edit);
- assert.notDeepEqual(mockChange, change);
- assert.equal(mockChange.revisions.bar._number, element.EDIT_NAME);
- assert.equal(mockChange.current_revision, change.current_revision);
- assert.deepEqual(mockChange.revisions.bar.commit, {commit: 'bar'});
- assert.notOk(mockChange.revisions.bar.actions);
-
- edit.base_revision = 'foo';
- element._processEdit(mockChange = _.cloneDeep(change), edit);
- assert.notDeepEqual(mockChange, change);
- assert.equal(mockChange.current_revision, 'bar');
- assert.deepEqual(mockChange.revisions.bar.actions,
- mockChange.revisions.foo.actions);
-
- // If _patchRange.patchNum is defined, do not load edit.
- element._patchRange.patchNum = 'baz';
- change.current_revision = 'baz';
- element._processEdit(mockChange = _.cloneDeep(change), edit);
- assert.equal(element._patchRange.patchNum, 'baz');
- assert.notOk(mockChange.revisions.bar.actions);
- });
-
- test('file-action-tap handling', () => {
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 1,
- };
- const fileList = element.$.fileList;
- const Actions = GrEditConstants.Actions;
- const controls = element.$.fileListHeader.$.editControls;
- sandbox.stub(controls, 'openDeleteDialog');
- sandbox.stub(controls, 'openRenameDialog');
- sandbox.stub(controls, 'openRestoreDialog');
- sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff');
- sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
-
- // Delete
- fileList.dispatchEvent(new CustomEvent('file-action-tap', {
- detail: {action: Actions.DELETE.id, path: 'foo'},
- bubbles: true,
- composed: true,
- }));
- flushAsynchronousOperations();
-
- assert.isTrue(controls.openDeleteDialog.called);
- assert.equal(controls.openDeleteDialog.lastCall.args[0], 'foo');
-
- // Restore
- fileList.dispatchEvent(new CustomEvent('file-action-tap', {
- detail: {action: Actions.RESTORE.id, path: 'foo'},
- bubbles: true,
- composed: true,
- }));
- flushAsynchronousOperations();
-
- assert.isTrue(controls.openRestoreDialog.called);
- assert.equal(controls.openRestoreDialog.lastCall.args[0], 'foo');
-
- // Rename
- fileList.dispatchEvent(new CustomEvent('file-action-tap', {
- detail: {action: Actions.RENAME.id, path: 'foo'},
- bubbles: true,
- composed: true,
- }));
- flushAsynchronousOperations();
-
- assert.isTrue(controls.openRenameDialog.called);
- assert.equal(controls.openRenameDialog.lastCall.args[0], 'foo');
-
- // Open
- fileList.dispatchEvent(new CustomEvent('file-action-tap', {
- detail: {action: Actions.OPEN.id, path: 'foo'},
- bubbles: true,
- composed: true,
- }));
- flushAsynchronousOperations();
-
- assert.isTrue(Gerrit.Nav.getEditUrlForDiff.called);
- assert.equal(Gerrit.Nav.getEditUrlForDiff.lastCall.args[1], 'foo');
- assert.equal(Gerrit.Nav.getEditUrlForDiff.lastCall.args[2], '1');
- assert.isTrue(Gerrit.Nav.navigateToRelativeUrl.called);
- });
-
- test('_selectedRevision updates when patchNum is changed', () => {
- const revision1 = {_number: 1, commit: {parents: []}};
- const revision2 = {_number: 2, commit: {parents: []}};
- sandbox.stub(element.$.restAPI, 'getChangeDetail').returns(
- Promise.resolve({
- revisions: {
- aaa: revision1,
- bbb: revision2,
- },
- labels: {},
- actions: {},
- current_revision: 'bbb',
- change_id: 'loremipsumdolorsitamet',
- }));
- sandbox.stub(element, '_getEdit').returns(Promise.resolve());
- sandbox.stub(element, '_getPreferences').returns(Promise.resolve({}));
- element._patchRange = {patchNum: '2'};
- return element._getChangeDetail().then(() => {
- assert.strictEqual(element._selectedRevision, revision2);
-
- element.set('_patchRange.patchNum', '1');
- assert.strictEqual(element._selectedRevision, revision1);
- });
- });
-
- test('_selectedRevision is assigned when patchNum is edit', () => {
- const revision1 = {_number: 1, commit: {parents: []}};
- const revision2 = {_number: 2, commit: {parents: []}};
- const revision3 = {_number: 'edit', commit: {parents: []}};
- sandbox.stub(element.$.restAPI, 'getChangeDetail').returns(
- Promise.resolve({
- revisions: {
- aaa: revision1,
- bbb: revision2,
- ccc: revision3,
- },
- labels: {},
- actions: {},
- current_revision: 'ccc',
- change_id: 'loremipsumdolorsitamet',
- }));
- sandbox.stub(element, '_getEdit').returns(Promise.resolve());
- sandbox.stub(element, '_getPreferences').returns(Promise.resolve({}));
- element._patchRange = {patchNum: 'edit'};
- return element._getChangeDetail().then(() => {
- assert.strictEqual(element._selectedRevision, revision3);
- });
- });
-
- test('_sendShowChangeEvent', () => {
- element._change = {labels: {}};
- element._patchRange = {patchNum: 4};
- element._mergeable = true;
- const showStub = sandbox.stub(element.$.jsAPI, 'handleEvent');
- element._sendShowChangeEvent();
- assert.isTrue(showStub.calledOnce);
- assert.equal(
- showStub.lastCall.args[0], element.$.jsAPI.EventType.SHOW_CHANGE);
- assert.deepEqual(showStub.lastCall.args[1], {
- change: {labels: {}},
- patchNum: 4,
- info: {mergeable: true},
- });
- });
-
- suite('_handleEditTap', () => {
- let fireEdit;
-
- setup(() => {
- fireEdit = () => {
- element.$.actions.dispatchEvent(new CustomEvent('edit-tap'));
- };
- navigateToChangeStub.restore();
-
- element._change = {revisions: {rev1: {_number: 1}}};
- });
-
- test('edit exists in revisions', done => {
- sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
- assert.equal(args.length, 2);
- assert.equal(args[1], element.EDIT_NAME); // patchNum
- done();
- });
-
- element.set('_change.revisions.rev2', {_number: element.EDIT_NAME});
- flushAsynchronousOperations();
-
- fireEdit();
- });
-
- test('no edit exists in revisions, non-latest patchset', done => {
- sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
- assert.equal(args.length, 4);
- assert.equal(args[1], 1); // patchNum
- assert.equal(args[3], true); // opt_isEdit
- done();
- });
-
- element.set('_change.revisions.rev2', {_number: 2});
- element._patchRange = {patchNum: 1};
- flushAsynchronousOperations();
-
- fireEdit();
- });
-
- test('no edit exists in revisions, latest patchset', done => {
- sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
- assert.equal(args.length, 4);
- // No patch should be specified when patchNum == latest.
- assert.isNotOk(args[1]); // patchNum
- assert.equal(args[3], true); // opt_isEdit
- done();
- });
-
- element.set('_change.revisions.rev2', {_number: 2});
- element._patchRange = {patchNum: 2};
- flushAsynchronousOperations();
-
- fireEdit();
- });
- });
-
- test('_handleStopEditTap', done => {
- sandbox.stub(element.$.metadata, '_computeLabelNames');
- navigateToChangeStub.restore();
- sandbox.stub(element, 'computeLatestPatchNum').returns(1);
- sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
- assert.equal(args.length, 2);
- assert.equal(args[1], 1); // patchNum
- done();
- });
-
- element._patchRange = {patchNum: 1};
- element.$.actions.dispatchEvent(new CustomEvent('stop-edit-tap',
- {bubbles: false}));
- });
-
- suite('plugin endpoints', () => {
- test('endpoint params', done => {
- element._change = {labels: {}};
- element._selectedRevision = {};
- let hookEl;
- let plugin;
- Gerrit.install(
- p => {
- plugin = p;
- plugin.hook('change-view-integration').getLastAttached()
- .then(
- el => hookEl = el);
- },
- '0.1',
- 'http://some/plugins/url.html');
- flush(() => {
- assert.strictEqual(hookEl.plugin, plugin);
- assert.strictEqual(hookEl.change, element._change);
- assert.strictEqual(hookEl.revision, element._selectedRevision);
- done();
- });
- });
- });
-
- suite('_getMergeability', () => {
- let getMergeableStub;
-
- setup(() => {
- element._change = {labels: {}};
- getMergeableStub = sandbox.stub(element.$.restAPI, 'getMergeable')
- .returns(Promise.resolve({mergeable: true}));
- });
-
- test('merged change', () => {
- element._mergeable = null;
- element._change.status = element.ChangeStatus.MERGED;
- return element._getMergeability().then(() => {
- assert.isFalse(element._mergeable);
- assert.isFalse(getMergeableStub.called);
- });
- });
-
- test('abandoned change', () => {
- element._mergeable = null;
- element._change.status = element.ChangeStatus.ABANDONED;
- return element._getMergeability().then(() => {
- assert.isFalse(element._mergeable);
- assert.isFalse(getMergeableStub.called);
- });
- });
-
- test('open change', () => {
- element._mergeable = null;
- return element._getMergeability().then(() => {
- assert.isTrue(element._mergeable);
- assert.isTrue(getMergeableStub.called);
- });
- });
- });
-
- test('_paramsChanged sets in projectLookup', () => {
sandbox.stub(element.$.relatedChanges, 'reload');
sandbox.stub(element, '_reload').returns(Promise.resolve());
- const setStub = sandbox.stub(element.$.restAPI, 'setInProjectLookup');
- element._paramsChanged({
- view: Gerrit.Nav.View.CHANGE,
- changeNum: 101,
- project: 'test-project',
- });
- assert.isTrue(setStub.calledOnce);
- assert.isTrue(setStub.calledWith(101, 'test-project'));
+ sandbox.spy(element, '_paramsChanged');
+ element.params = {view: 'change', changeNum: '1'};
});
- test('_handleToggleStar called when star is tapped', () => {
+ test('tab switch works correctly', done => {
+ assert.isTrue(element._paramsChanged.called);
+ assert.equal(element.$.commentTabs.selected, CommentTabs.CHANGE_LOG);
+ assert.equal(element._currentView, CommentTabs.CHANGE_LOG);
+
+ const commentTab = element.shadowRoot.querySelector(
+ 'paper-tab.commentThreads'
+ );
+ // Switch to comment thread tab
+ MockInteractions.tap(commentTab);
+ const commentTabs = element.$.commentTabs;
+ assert.equal(commentTabs.selected,
+ CommentTabs.COMMENT_THREADS);
+ assert.equal(element._currentView, CommentTabs.COMMENT_THREADS);
+
+ // Switch back to 'Change Log' tab
+ element._paramsChanged(element.params);
+ flush(() => {
+ assert.equal(commentTabs.selected,
+ CommentTabs.CHANGE_LOG);
+ assert.equal(element._currentView, CommentTabs.CHANGE_LOG);
+ done();
+ });
+ });
+ });
+
+ suite('Findings comment tab', () => {
+ setup(done => {
element._change = {
- owner: {_account_id: 1},
- starred: false,
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev2: {_number: 2, commit: {parents: []}},
+ rev1: {_number: 1, commit: {parents: []}},
+ rev13: {_number: 13, commit: {parents: []}},
+ rev3: {_number: 3, commit: {parents: []}},
+ rev4: {_number: 4, commit: {parents: []}},
+ },
+ current_revision: 'rev4',
};
- element._loggedIn = true;
- const stub = sandbox.stub(element, '_handleToggleStar');
- flushAsynchronousOperations();
-
- MockInteractions.tap(element.$.changeStar.shadowRoot
- .querySelector('button'));
- assert.isTrue(stub.called);
+ element._commentThreads = THREADS;
+ const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
+ MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[2]);
+ flush(() => {
+ done();
+ });
});
- suite('gr-reporting tests', () => {
- setup(() => {
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 1,
- };
- sandbox.stub(element, '_getChangeDetail').returns(Promise.resolve());
- sandbox.stub(element, '_getProjectConfig').returns(Promise.resolve());
- sandbox.stub(element, '_reloadComments').returns(Promise.resolve());
- sandbox.stub(element, '_getMergeability').returns(Promise.resolve());
- sandbox.stub(element, '_getLatestCommitMessage')
- .returns(Promise.resolve());
- });
+ test('robot comments count per patchset', () => {
+ const count = element._robotCommentCountPerPatchSet(THREADS);
+ const expectedCount = {
+ 2: 1,
+ 3: 1,
+ 4: 2,
+ };
+ assert.deepEqual(count, expectedCount);
+ assert.equal(element._computeText({_number: 2}, THREADS),
+ 'Patchset 2 (1 finding)');
+ assert.equal(element._computeText({_number: 4}, THREADS),
+ 'Patchset 4 (2 findings)');
+ assert.equal(element._computeText({_number: 5}, THREADS),
+ 'Patchset 5');
+ });
- test('don\'t report changedDisplayed on reply', done => {
- const changeDisplayStub =
- sandbox.stub(element.$.reporting, 'changeDisplayed');
- const changeFullyLoadedStub =
- sandbox.stub(element.$.reporting, 'changeFullyLoaded');
- element._handleReplySent();
+ test('only robot comments are rendered', () => {
+ assert.equal(element._robotCommentThreads.length, 2);
+ assert.equal(element._robotCommentThreads[0].comments[0].robot_id,
+ 'rc1');
+ assert.equal(element._robotCommentThreads[1].comments[0].robot_id,
+ 'rc2');
+ });
+
+ test('changing patchsets resets robot comments', done => {
+ element.set('_change.current_revision', 'rev3');
+ flush(() => {
+ assert.equal(element._robotCommentThreads.length, 1);
+ done();
+ });
+ });
+
+ test('Show more button is hidden', () => {
+ assert.isNull(element.shadowRoot.querySelector('.show-robot-comments'));
+ });
+
+ suite('robot comments show more button', () => {
+ setup(done => {
+ const arr = [];
+ for (let i = 0; i <= 30; i++) {
+ arr.push(...THREADS);
+ }
+ element._commentThreads = arr;
flush(() => {
- assert.isFalse(changeDisplayStub.called);
- assert.isFalse(changeFullyLoadedStub.called);
done();
});
});
- test('report changedDisplayed on _paramsChanged', done => {
- const changeDisplayStub =
- sandbox.stub(element.$.reporting, 'changeDisplayed');
- const changeFullyLoadedStub =
- sandbox.stub(element.$.reporting, 'changeFullyLoaded');
- element._paramsChanged({
- view: Gerrit.Nav.View.CHANGE,
- changeNum: 101,
- project: 'test-project',
- });
+ test('Show more button is rendered', () => {
+ assert.isOk(element.shadowRoot.querySelector('.show-robot-comments'));
+ assert.equal(element._robotCommentThreads.length,
+ ROBOT_COMMENTS_LIMIT);
+ });
+
+ test('Clicking show more button renders all comments', done => {
+ MockInteractions.tap(element.shadowRoot.querySelector(
+ '.show-robot-comments'));
flush(() => {
- assert.isTrue(changeDisplayStub.called);
- assert.isTrue(changeFullyLoadedStub.called);
+ assert.equal(element._robotCommentThreads.length, 62);
done();
});
});
});
});
+
+ test('reply button is not visible when logged out', () => {
+ assert.equal(getComputedStyle(element.$.replyBtn).display, 'none');
+ element._loggedIn = true;
+ assert.notEqual(getComputedStyle(element.$.replyBtn).display, 'none');
+ });
+
+ test('download tap calls _handleOpenDownloadDialog', () => {
+ sandbox.stub(element, '_handleOpenDownloadDialog');
+ element.$.actions.fire('download-tap');
+ assert.isTrue(element._handleOpenDownloadDialog.called);
+ });
+
+ test('fetches the server config on attached', done => {
+ flush(() => {
+ assert.equal(element._serverConfig.test, 'config');
+ done();
+ });
+ });
+
+ test('_changeStatuses', () => {
+ sandbox.stub(element, 'changeStatuses').returns(
+ ['Merged', 'WIP']);
+ element._loading = false;
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev2: {_number: 2},
+ rev1: {_number: 1},
+ rev13: {_number: 13},
+ rev3: {_number: 3},
+ },
+ current_revision: 'rev3',
+ labels: {
+ test: {
+ all: [],
+ default_value: 0,
+ values: [],
+ approved: {},
+ },
+ },
+ };
+ element._mergeable = true;
+ const expectedStatuses = ['Merged', 'WIP'];
+ assert.deepEqual(element._changeStatuses, expectedStatuses);
+ assert.equal(element._changeStatus, expectedStatuses.join(', '));
+ flushAsynchronousOperations();
+ const statusChips = dom(element.root)
+ .querySelectorAll('gr-change-status');
+ assert.equal(statusChips.length, 2);
+ });
+
+ test('diff preferences open when open-diff-prefs is fired', () => {
+ const overlayOpenStub = sandbox.stub(element.$.fileList,
+ 'openDiffPrefs');
+ element.$.fileListHeader.fire('open-diff-prefs');
+ assert.isTrue(overlayOpenStub.called);
+ });
+
+ test('_prepareCommitMsgForLinkify', () => {
+ let commitMessage = 'R=test@google.com';
+ let result = element._prepareCommitMsgForLinkify(commitMessage);
+ assert.equal(result, 'R=\u200Btest@google.com');
+
+ commitMessage = 'R=test@google.com\nR=test@google.com';
+ result = element._prepareCommitMsgForLinkify(commitMessage);
+ assert.equal(result, 'R=\u200Btest@google.com\nR=\u200Btest@google.com');
+
+ commitMessage = 'CC=test@google.com';
+ result = element._prepareCommitMsgForLinkify(commitMessage);
+ assert.equal(result, 'CC=\u200Btest@google.com');
+ }),
+
+ test('_isSubmitEnabled', () => {
+ assert.isFalse(element._isSubmitEnabled({}));
+ assert.isFalse(element._isSubmitEnabled({submit: {}}));
+ assert.isTrue(element._isSubmitEnabled(
+ {submit: {enabled: true}}));
+ });
+
+ test('_reload is called when an approved label is removed', () => {
+ const vote = {_account_id: 1, name: 'bojack', value: 1};
+ element._changeNum = '42';
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 1,
+ };
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ owner: {email: 'abc@def'},
+ revisions: {
+ rev2: {_number: 2, commit: {parents: []}},
+ rev1: {_number: 1, commit: {parents: []}},
+ rev13: {_number: 13, commit: {parents: []}},
+ rev3: {_number: 3, commit: {parents: []}},
+ },
+ current_revision: 'rev3',
+ status: 'NEW',
+ labels: {
+ test: {
+ all: [vote],
+ default_value: 0,
+ values: [],
+ approved: {},
+ },
+ },
+ };
+ flushAsynchronousOperations();
+ const reloadStub = sandbox.stub(element, '_reload');
+ element.splice('_change.labels.test.all', 0, 1);
+ assert.isFalse(reloadStub.called);
+ element._change.labels.test.all.push(vote);
+ element._change.labels.test.all.push(vote);
+ element._change.labels.test.approved = vote;
+ flushAsynchronousOperations();
+ element.splice('_change.labels.test.all', 0, 2);
+ assert.isTrue(reloadStub.called);
+ assert.isTrue(reloadStub.calledOnce);
+ });
+
+ test('reply button has updated count when there are drafts', () => {
+ const getLabel = element._computeReplyButtonLabel;
+
+ assert.equal(getLabel(null, false), 'Reply');
+ assert.equal(getLabel(null, true), 'Start review');
+
+ const changeRecord = {base: null};
+ assert.equal(getLabel(changeRecord, false), 'Reply');
+
+ changeRecord.base = {};
+ assert.equal(getLabel(changeRecord, false), 'Reply');
+
+ changeRecord.base = {
+ 'file1.txt': [{}],
+ 'file2.txt': [{}, {}],
+ };
+ assert.equal(getLabel(changeRecord, false), 'Reply (3)');
+ });
+
+ test('start review button when owner of WIP change', () => {
+ assert.equal(
+ element._computeReplyButtonLabel(null, true),
+ 'Start review');
+ });
+
+ test('comment events properly update diff drafts', () => {
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 2,
+ };
+ const draft = {
+ __draft: true,
+ id: 'id1',
+ path: '/foo/bar.txt',
+ text: 'hello',
+ };
+ element._handleCommentSave({detail: {comment: draft}});
+ draft.patch_set = 2;
+ assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]});
+ draft.patch_set = null;
+ draft.text = 'hello, there';
+ element._handleCommentSave({detail: {comment: draft}});
+ draft.patch_set = 2;
+ assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]});
+ const draft2 = {
+ __draft: true,
+ id: 'id2',
+ path: '/foo/bar.txt',
+ text: 'hola',
+ };
+ element._handleCommentSave({detail: {comment: draft2}});
+ draft2.patch_set = 2;
+ assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft, draft2]});
+ draft.patch_set = null;
+ element._handleCommentDiscard({detail: {comment: draft}});
+ draft.patch_set = 2;
+ assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft2]});
+ element._handleCommentDiscard({detail: {comment: draft2}});
+ assert.deepEqual(element._diffDrafts, {});
+ });
+
+ test('change num change', () => {
+ element._changeNum = null;
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 2,
+ };
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ labels: {},
+ };
+ element.viewState.changeNum = null;
+ element.viewState.diffMode = 'UNIFIED';
+ assert.equal(element.viewState.numFilesShown, 200);
+ assert.equal(element._numFilesShown, 200);
+ element._numFilesShown = 150;
+ flushAsynchronousOperations();
+ assert.equal(element.viewState.diffMode, 'UNIFIED');
+ assert.equal(element.viewState.numFilesShown, 150);
+
+ element._changeNum = '1';
+ element.params = {changeNum: '1'};
+ element._change.newProp = '1';
+ flushAsynchronousOperations();
+ assert.equal(element.viewState.diffMode, 'UNIFIED');
+ assert.equal(element.viewState.changeNum, '1');
+
+ element._changeNum = '2';
+ element.params = {changeNum: '2'};
+ element._change.newProp = '2';
+ flushAsynchronousOperations();
+ assert.equal(element.viewState.diffMode, 'UNIFIED');
+ assert.equal(element.viewState.changeNum, '2');
+ assert.equal(element.viewState.numFilesShown, 200);
+ assert.equal(element._numFilesShown, 200);
+ });
+
+ test('_setDiffViewMode is called with reset when new change is loaded',
+ () => {
+ sandbox.stub(element, '_setDiffViewMode');
+ element.viewState = {changeNum: 1};
+ element._changeNum = 2;
+ element._resetFileListViewState();
+ assert.isTrue(
+ element._setDiffViewMode.lastCall.calledWithExactly(true));
+ });
+
+ test('diffViewMode is propagated from file list header', () => {
+ element.viewState = {diffMode: 'UNIFIED'};
+ element.$.fileListHeader.diffViewMode = 'SIDE_BY_SIDE';
+ assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
+ });
+
+ test('diffMode defaults to side by side without preferences', done => {
+ sandbox.stub(element.$.restAPI, 'getPreferences').returns(
+ Promise.resolve({}));
+ // No user prefs or diff view mode set.
+
+ element._setDiffViewMode().then(() => {
+ assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
+ done();
+ });
+ });
+
+ test('diffMode defaults to preference when not already set', done => {
+ sandbox.stub(element.$.restAPI, 'getPreferences').returns(
+ Promise.resolve({default_diff_view: 'UNIFIED'}));
+
+ element._setDiffViewMode().then(() => {
+ assert.equal(element.viewState.diffMode, 'UNIFIED');
+ done();
+ });
+ });
+
+ test('existing diffMode overrides preference', done => {
+ element.viewState.diffMode = 'SIDE_BY_SIDE';
+ sandbox.stub(element.$.restAPI, 'getPreferences').returns(
+ Promise.resolve({default_diff_view: 'UNIFIED'}));
+ element._setDiffViewMode().then(() => {
+ assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
+ done();
+ });
+ });
+
+ test('don’t reload entire page when patchRange changes', () => {
+ const reloadStub = sandbox.stub(element, '_reload',
+ () => Promise.resolve());
+ const reloadPatchDependentStub = sandbox.stub(element,
+ '_reloadPatchNumDependentResources',
+ () => Promise.resolve());
+ const relatedClearSpy = sandbox.spy(element.$.relatedChanges, 'clear');
+ const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
+
+ const value = {
+ view: Gerrit.Nav.View.CHANGE,
+ patchNum: '1',
+ };
+ element._paramsChanged(value);
+ assert.isTrue(reloadStub.calledOnce);
+ assert.isTrue(relatedClearSpy.calledOnce);
+
+ element._initialLoadComplete = true;
+
+ value.basePatchNum = '1';
+ value.patchNum = '2';
+ element._paramsChanged(value);
+ assert.isFalse(reloadStub.calledTwice);
+ assert.isTrue(reloadPatchDependentStub.calledOnce);
+ assert.isTrue(relatedClearSpy.calledOnce);
+ assert.isTrue(collapseStub.calledTwice);
+ });
+
+ test('reload entire page when patchRange doesnt change', () => {
+ const reloadStub = sandbox.stub(element, '_reload',
+ () => Promise.resolve());
+ const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
+ const value = {
+ view: Gerrit.Nav.View.CHANGE,
+ };
+ element._paramsChanged(value);
+ assert.isTrue(reloadStub.calledOnce);
+ element._initialLoadComplete = true;
+ element._paramsChanged(value);
+ assert.isTrue(reloadStub.calledTwice);
+ assert.isTrue(collapseStub.calledTwice);
+ });
+
+ test('related changes are updated and new patch selected after rebase',
+ done => {
+ element._changeNum = '42';
+ sandbox.stub(element, 'computeLatestPatchNum', () => 1);
+ sandbox.stub(element, '_reload',
+ () => Promise.resolve());
+ const e = {detail: {action: 'rebase'}};
+ element._handleReloadChange(e).then(() => {
+ assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
+ element._change));
+ done();
+ });
+ });
+
+ test('related changes are not updated after other action', done => {
+ sandbox.stub(element, '_reload', () => Promise.resolve());
+ sandbox.stub(element.$.relatedChanges, 'reload');
+ const e = {detail: {action: 'abandon'}};
+ element._handleReloadChange(e).then(() => {
+ assert.isFalse(navigateToChangeStub.called);
+ done();
+ });
+ });
+
+ test('_computeMergedCommitInfo', () => {
+ const dummyRevs = {
+ 1: {commit: {commit: 1}},
+ 2: {commit: {}},
+ };
+ assert.deepEqual(element._computeMergedCommitInfo(0, dummyRevs), {});
+ assert.deepEqual(element._computeMergedCommitInfo(1, dummyRevs),
+ dummyRevs[1].commit);
+
+ // Regression test for issue 5337.
+ const commit = element._computeMergedCommitInfo(2, dummyRevs);
+ assert.notDeepEqual(commit, dummyRevs[2]);
+ assert.deepEqual(commit, {commit: 2});
+ });
+
+ test('_computeCopyTextForTitle', () => {
+ const change = {
+ _number: 123,
+ subject: 'test subject',
+ revisions: {
+ rev1: {_number: 1},
+ rev3: {_number: 3},
+ },
+ current_revision: 'rev3',
+ };
+ sandbox.stub(Gerrit.Nav, 'getUrlForChange')
+ .returns('/change/123');
+ assert.equal(
+ element._computeCopyTextForTitle(change),
+ `123: test subject | http://${location.host}/change/123`
+ );
+ });
+
+ test('get latest revision', () => {
+ let change = {
+ revisions: {
+ rev1: {_number: 1},
+ rev3: {_number: 3},
+ },
+ current_revision: 'rev3',
+ };
+ assert.equal(element._getLatestRevisionSHA(change), 'rev3');
+ change = {
+ revisions: {
+ rev1: {_number: 1},
+ },
+ };
+ assert.equal(element._getLatestRevisionSHA(change), 'rev1');
+ });
+
+ test('show commit message edit button', () => {
+ const _change = {
+ status: element.ChangeStatus.MERGED,
+ };
+ assert.isTrue(element._computeHideEditCommitMessage(false, false, {}));
+ assert.isTrue(element._computeHideEditCommitMessage(true, true, {}));
+ assert.isTrue(element._computeHideEditCommitMessage(false, true, {}));
+ assert.isFalse(element._computeHideEditCommitMessage(true, false, {}));
+ assert.isTrue(element._computeHideEditCommitMessage(true, false,
+ _change));
+ assert.isTrue(element._computeHideEditCommitMessage(true, false, {},
+ true));
+ assert.isFalse(element._computeHideEditCommitMessage(true, false, {},
+ false));
+ });
+
+ test('_handleCommitMessageSave trims trailing whitespace', () => {
+ const putStub = sandbox.stub(element.$.restAPI, 'putChangeCommitMessage')
+ .returns(Promise.resolve({}));
+
+ const mockEvent = content => { return {detail: {content}}; };
+
+ element._handleCommitMessageSave(mockEvent('test \n test '));
+ assert.equal(putStub.lastCall.args[1], 'test\n test');
+
+ element._handleCommitMessageSave(mockEvent(' test\ntest'));
+ assert.equal(putStub.lastCall.args[1], ' test\ntest');
+
+ element._handleCommitMessageSave(mockEvent('\n\n\n\n\n\n\n\n'));
+ assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n');
+ });
+
+ test('_computeChangeIdCommitMessageError', () => {
+ let commitMessage =
+ 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483';
+ let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'};
+ assert.equal(
+ element._computeChangeIdCommitMessageError(commitMessage, change),
+ null);
+
+ change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'};
+ assert.equal(
+ element._computeChangeIdCommitMessageError(commitMessage, change),
+ 'mismatch');
+
+ commitMessage = 'This is the greatest change.';
+ assert.equal(
+ element._computeChangeIdCommitMessageError(commitMessage, change),
+ 'missing');
+ });
+
+ test('multiple change Ids in commit message picks last', () => {
+ const commitMessage = [
+ 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484',
+ 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483',
+ ].join('\n');
+ let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'};
+ assert.equal(
+ element._computeChangeIdCommitMessageError(commitMessage, change),
+ null);
+ change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'};
+ assert.equal(
+ element._computeChangeIdCommitMessageError(commitMessage, change),
+ 'mismatch');
+ });
+
+ test('does not count change Id that starts mid line', () => {
+ const commitMessage = [
+ 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484',
+ 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483',
+ ].join(' and ');
+ let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'};
+ assert.equal(
+ element._computeChangeIdCommitMessageError(commitMessage, change),
+ null);
+ change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'};
+ assert.equal(
+ element._computeChangeIdCommitMessageError(commitMessage, change),
+ 'mismatch');
+ });
+
+ test('_computeTitleAttributeWarning', () => {
+ let changeIdCommitMessageError = 'missing';
+ assert.equal(
+ element._computeTitleAttributeWarning(changeIdCommitMessageError),
+ 'No Change-Id in commit message');
+
+ changeIdCommitMessageError = 'mismatch';
+ assert.equal(
+ element._computeTitleAttributeWarning(changeIdCommitMessageError),
+ 'Change-Id mismatch');
+ });
+
+ test('_computeChangeIdClass', () => {
+ let changeIdCommitMessageError = 'missing';
+ assert.equal(
+ element._computeChangeIdClass(changeIdCommitMessageError), '');
+
+ changeIdCommitMessageError = 'mismatch';
+ assert.equal(
+ element._computeChangeIdClass(changeIdCommitMessageError), 'warning');
+ });
+
+ test('topic is coalesced to null', done => {
+ sandbox.stub(element, '_changeChanged');
+ sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
+ id: '123456789',
+ labels: {},
+ current_revision: 'foo',
+ revisions: {foo: {commit: {}}},
+ }));
+
+ element._getChangeDetail().then(() => {
+ assert.isNull(element._change.topic);
+ done();
+ });
+ });
+
+ test('commit sha is populated from getChangeDetail', done => {
+ sandbox.stub(element, '_changeChanged');
+ sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
+ id: '123456789',
+ labels: {},
+ current_revision: 'foo',
+ revisions: {foo: {commit: {}}},
+ }));
+
+ element._getChangeDetail().then(() => {
+ assert.equal('foo', element._commitInfo.commit);
+ done();
+ });
+ });
+
+ test('edit is added to change', () => {
+ sandbox.stub(element, '_changeChanged');
+ sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
+ id: '123456789',
+ labels: {},
+ current_revision: 'foo',
+ revisions: {foo: {commit: {}}},
+ }));
+ sandbox.stub(element, '_getEdit', () => Promise.resolve({
+ base_patch_set_number: 1,
+ commit: {commit: 'bar'},
+ }));
+ element._patchRange = {};
+
+ return element._getChangeDetail().then(() => {
+ const revs = element._change.revisions;
+ assert.equal(Object.keys(revs).length, 2);
+ assert.deepEqual(revs['foo'], {commit: {commit: 'foo'}});
+ assert.deepEqual(revs['bar'], {
+ _number: element.EDIT_NAME,
+ basePatchNum: 1,
+ commit: {commit: 'bar'},
+ fetch: undefined,
+ });
+ });
+ });
+
+ test('_getBasePatchNum', () => {
+ const _change = {
+ _number: 42,
+ revisions: {
+ '98da160735fb81604b4c40e93c368f380539dd0e': {
+ _number: 1,
+ commit: {
+ parents: [],
+ },
+ },
+ },
+ };
+ const _patchRange = {
+ basePatchNum: 'PARENT',
+ };
+ assert.equal(element._getBasePatchNum(_change, _patchRange), 'PARENT');
+
+ element._prefs = {
+ default_base_for_merges: 'FIRST_PARENT',
+ };
+
+ const _change2 = {
+ _number: 42,
+ revisions: {
+ '98da160735fb81604b4c40e93c368f380539dd0e': {
+ _number: 1,
+ commit: {
+ parents: [
+ {
+ commit: '6e12bdf1176eb4ab24d8491ba3b6d0704409cde8',
+ subject: 'test',
+ },
+ {
+ commit: '22f7db4754b5d9816fc581f3d9a6c0ef8429c841',
+ subject: 'test3',
+ },
+ ],
+ },
+ },
+ },
+ };
+ assert.equal(element._getBasePatchNum(_change2, _patchRange), -1);
+
+ _patchRange.patchNum = 1;
+ assert.equal(element._getBasePatchNum(_change2, _patchRange), 'PARENT');
+ });
+
+ test('_openReplyDialog called with `ANY` when coming from tap event',
+ () => {
+ const openStub = sandbox.stub(element, '_openReplyDialog');
+ element._serverConfig = {};
+ MockInteractions.tap(element.$.replyBtn);
+ assert(openStub.lastCall.calledWithExactly(
+ element.$.replyDialog.FocusTarget.ANY),
+ '_openReplyDialog should have been passed ANY');
+ assert.equal(openStub.callCount, 1);
+ });
+
+ test('_openReplyDialog called with `BODY` when coming from message reply' +
+ 'event', done => {
+ flush(() => {
+ const openStub = sandbox.stub(element, '_openReplyDialog');
+ element.messagesList.fire('reply',
+ {message: {message: 'text'}});
+ assert(openStub.lastCall.calledWithExactly(
+ element.$.replyDialog.FocusTarget.BODY),
+ '_openReplyDialog should have been passed BODY');
+ assert.equal(openStub.callCount, 1);
+ done();
+ });
+ });
+
+ test('reply dialog focus can be controlled', () => {
+ const FocusTarget = element.$.replyDialog.FocusTarget;
+ const openStub = sandbox.stub(element, '_openReplyDialog');
+
+ const e = {detail: {}};
+ element._handleShowReplyDialog(e);
+ assert(openStub.lastCall.calledWithExactly(FocusTarget.REVIEWERS),
+ '_openReplyDialog should have been passed REVIEWERS');
+ assert.equal(openStub.callCount, 1);
+
+ e.detail.value = {ccsOnly: true};
+ element._handleShowReplyDialog(e);
+ assert(openStub.lastCall.calledWithExactly(FocusTarget.CCS),
+ '_openReplyDialog should have been passed CCS');
+ assert.equal(openStub.callCount, 2);
+ });
+
+ test('getUrlParameter functionality', () => {
+ const locationStub = sandbox.stub(element, '_getLocationSearch');
+
+ locationStub.returns('?test');
+ assert.equal(element._getUrlParameter('test'), 'test');
+ locationStub.returns('?test2=12&test=3');
+ assert.equal(element._getUrlParameter('test'), 'test');
+ locationStub.returns('');
+ assert.isNull(element._getUrlParameter('test'));
+ locationStub.returns('?');
+ assert.isNull(element._getUrlParameter('test'));
+ locationStub.returns('?test2');
+ assert.isNull(element._getUrlParameter('test'));
+ });
+
+ test('revert dialog opened with revert param', done => {
+ sandbox.stub(element.$.restAPI, 'getLoggedIn', () => Promise.resolve(true));
+ sandbox.stub(Gerrit, 'awaitPluginsLoaded', () => Promise.resolve());
+
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 2,
+ };
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1, commit: {parents: []}},
+ rev2: {_number: 2, commit: {parents: []}},
+ },
+ current_revision: 'rev1',
+ status: element.ChangeStatus.MERGED,
+ labels: {},
+ actions: {},
+ };
+
+ sandbox.stub(element, '_getUrlParameter',
+ param => {
+ assert.equal(param, 'revert');
+ return param;
+ });
+
+ sandbox.stub(element.$.actions, 'showRevertDialog',
+ done);
+
+ element._maybeShowRevertDialog();
+ assert.isTrue(Gerrit.awaitPluginsLoaded.called);
+ });
+
+ suite('scroll related tests', () => {
+ test('document scrolling calls function to set scroll height', done => {
+ const originalHeight = document.body.scrollHeight;
+ const scrollStub = sandbox.stub(element, '_handleScroll',
+ () => {
+ assert.isTrue(scrollStub.called);
+ document.body.style.height = originalHeight + 'px';
+ scrollStub.restore();
+ done();
+ });
+ document.body.style.height = '10000px';
+ element._handleScroll();
+ });
+
+ test('scrollTop is set correctly', () => {
+ element.viewState = {scrollTop: TEST_SCROLL_TOP_PX};
+
+ sandbox.stub(element, '_reload', () => {
+ // When element is reloaded, ensure that the history
+ // state has the scrollTop set earlier. This will then
+ // be reset.
+ assert.isTrue(element.viewState.scrollTop == TEST_SCROLL_TOP_PX);
+ return Promise.resolve({});
+ });
+
+ // simulate reloading component, which is done when route
+ // changes to match a regex of change view type.
+ element._paramsChanged({view: Gerrit.Nav.View.CHANGE});
+ });
+
+ test('scrollTop is reset when new change is loaded', () => {
+ element._resetFileListViewState();
+ assert.equal(element.viewState.scrollTop, 0);
+ });
+ });
+
+ suite('reply dialog tests', () => {
+ setup(() => {
+ sandbox.stub(element.$.replyDialog, '_draftChanged');
+ sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates',
+ () => Promise.resolve({isLatest: true}));
+ element._change = {labels: {}};
+ });
+
+ test('reply from comment adds quote text', () => {
+ const e = {detail: {message: {message: 'quote text'}}};
+ element._handleMessageReply(e);
+ assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
+ });
+
+ test('reply from comment replaces quote text', () => {
+ element.$.replyDialog.draft = '> old quote text\n\n some draft text';
+ element.$.replyDialog.quote = '> old quote text\n\n';
+ const e = {detail: {message: {message: 'quote text'}}};
+ element._handleMessageReply(e);
+ assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
+ });
+
+ test('reply from same comment preserves quote text', () => {
+ element.$.replyDialog.draft = '> quote text\n\n some draft text';
+ element.$.replyDialog.quote = '> quote text\n\n';
+ const e = {detail: {message: {message: 'quote text'}}};
+ element._handleMessageReply(e);
+ assert.equal(element.$.replyDialog.draft,
+ '> quote text\n\n some draft text');
+ assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
+ });
+
+ test('reply from top of page contains previous draft', () => {
+ const div = document.createElement('div');
+ element.$.replyDialog.draft = '> quote text\n\n some draft text';
+ element.$.replyDialog.quote = '> quote text\n\n';
+ const e = {target: div, preventDefault: sandbox.spy()};
+ element._handleReplyTap(e);
+ assert.equal(element.$.replyDialog.draft,
+ '> quote text\n\n some draft text');
+ assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
+ });
+ });
+
+ test('reply button is disabled until server config is loaded', () => {
+ assert.isTrue(element._replyDisabled);
+ element._serverConfig = {};
+ assert.isFalse(element._replyDisabled);
+ });
+
+ suite('commit message expand/collapse', () => {
+ setup(() => {
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => Promise.resolve({isLatest: false}));
+ });
+
+ test('commitCollapseToggle hidden for short commit message', () => {
+ element._latestCommitMessage = '';
+ assert.isTrue(element.$.commitCollapseToggle.hasAttribute('hidden'));
+ });
+
+ test('commitCollapseToggle shown for long commit message', () => {
+ element._latestCommitMessage = _.times(31, String).join('\n');
+ assert.isFalse(element.$.commitCollapseToggle.hasAttribute('hidden'));
+ });
+
+ test('commitCollapseToggle functions', () => {
+ element._latestCommitMessage = _.times(35, String).join('\n');
+ assert.isTrue(element._commitCollapsed);
+ assert.isTrue(element._commitCollapsible);
+ assert.isTrue(
+ element.$.commitMessageEditor.hasAttribute('collapsed'));
+ MockInteractions.tap(element.$.commitCollapseToggleButton);
+ assert.isFalse(element._commitCollapsed);
+ assert.isTrue(element._commitCollapsible);
+ assert.isFalse(
+ element.$.commitMessageEditor.hasAttribute('collapsed'));
+ });
+ });
+
+ suite('related changes expand/collapse', () => {
+ let updateHeightSpy;
+ setup(() => {
+ updateHeightSpy = sandbox.spy(element, '_updateRelatedChangeMaxHeight');
+ });
+
+ test('relatedChangesToggle shown height greater than changeInfo height',
+ () => {
+ assert.isFalse(element.$.relatedChangesToggle.classList
+ .contains('showToggle'));
+ sandbox.stub(element, '_getOffsetHeight', () => 50);
+ sandbox.stub(element, '_getScrollHeight', () => 60);
+ sandbox.stub(element, '_getLineHeight', () => 5);
+ sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
+ element.$.relatedChanges.dispatchEvent(
+ new CustomEvent('new-section-loaded'));
+ assert.isTrue(element.$.relatedChangesToggle.classList
+ .contains('showToggle'));
+ assert.equal(updateHeightSpy.callCount, 1);
+ });
+
+ test('relatedChangesToggle hidden height less than changeInfo height',
+ () => {
+ assert.isFalse(element.$.relatedChangesToggle.classList
+ .contains('showToggle'));
+ sandbox.stub(element, '_getOffsetHeight', () => 50);
+ sandbox.stub(element, '_getScrollHeight', () => 40);
+ sandbox.stub(element, '_getLineHeight', () => 5);
+ sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
+ element.$.relatedChanges.dispatchEvent(
+ new CustomEvent('new-section-loaded'));
+ assert.isFalse(element.$.relatedChangesToggle.classList
+ .contains('showToggle'));
+ assert.equal(updateHeightSpy.callCount, 1);
+ });
+
+ test('relatedChangesToggle functions', () => {
+ sandbox.stub(element, '_getOffsetHeight', () => 50);
+ sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
+ element._relatedChangesLoading = false;
+ assert.isTrue(element._relatedChangesCollapsed);
+ assert.isTrue(
+ element.$.relatedChanges.classList.contains('collapsed'));
+ MockInteractions.tap(element.$.relatedChangesToggleButton);
+ assert.isFalse(element._relatedChangesCollapsed);
+ assert.isFalse(
+ element.$.relatedChanges.classList.contains('collapsed'));
+ });
+
+ test('_updateRelatedChangeMaxHeight without commit toggle', () => {
+ sandbox.stub(element, '_getOffsetHeight', () => 50);
+ sandbox.stub(element, '_getLineHeight', () => 12);
+ sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
+
+ // 50 (existing height) - 30 (extra height) = 20 (adjusted height).
+ // 20 (max existing height) % 12 (line height) = 6 (remainder).
+ // 20 (adjusted height) - 8 (remainder) = 12 (max height to set).
+
+ element._updateRelatedChangeMaxHeight();
+ assert.equal(getCustomCssValue('--relation-chain-max-height'),
+ '12px');
+ assert.equal(getCustomCssValue('--related-change-btn-top-padding'),
+ '');
+ });
+
+ test('_updateRelatedChangeMaxHeight with commit toggle', () => {
+ element._latestCommitMessage = _.times(31, String).join('\n');
+ sandbox.stub(element, '_getOffsetHeight', () => 50);
+ sandbox.stub(element, '_getLineHeight', () => 12);
+ sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
+
+ // 50 (existing height) % 12 (line height) = 2 (remainder).
+ // 50 (existing height) - 2 (remainder) = 48 (max height to set).
+
+ element._updateRelatedChangeMaxHeight();
+ assert.equal(getCustomCssValue('--relation-chain-max-height'),
+ '48px');
+ assert.equal(getCustomCssValue('--related-change-btn-top-padding'),
+ '2px');
+ });
+
+ test('_updateRelatedChangeMaxHeight in small screen mode', () => {
+ element._latestCommitMessage = _.times(31, String).join('\n');
+ sandbox.stub(element, '_getOffsetHeight', () => 50);
+ sandbox.stub(element, '_getLineHeight', () => 12);
+ sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
+
+ element._updateRelatedChangeMaxHeight();
+
+ // 400 (new height) % 12 (line height) = 4 (remainder).
+ // 400 (new height) - 4 (remainder) = 396.
+
+ assert.equal(getCustomCssValue('--relation-chain-max-height'),
+ '396px');
+ });
+
+ test('_updateRelatedChangeMaxHeight in medium screen mode', () => {
+ element._latestCommitMessage = _.times(31, String).join('\n');
+ sandbox.stub(element, '_getOffsetHeight', () => 50);
+ sandbox.stub(element, '_getLineHeight', () => 12);
+ sandbox.stub(window, 'matchMedia', () => {
+ if (window.matchMedia.lastCall.args[0] === '(max-width: 75em)') {
+ return {matches: true};
+ } else {
+ return {matches: false};
+ }
+ });
+
+ // 100 (new height) % 12 (line height) = 4 (remainder).
+ // 100 (new height) - 4 (remainder) = 96.
+ element._updateRelatedChangeMaxHeight();
+ assert.equal(getCustomCssValue('--relation-chain-max-height'),
+ '96px');
+ });
+
+ suite('update checks', () => {
+ setup(() => {
+ sandbox.spy(element, '_startUpdateCheckTimer');
+ sandbox.stub(element, 'async', f => {
+ // Only fire the async callback one time.
+ if (element.async.callCount > 1) { return; }
+ f.call(element);
+ });
+ });
+
+ test('_startUpdateCheckTimer negative delay', () => {
+ sandbox.stub(element, 'fetchChangeUpdates');
+
+ element._serverConfig = {change: {update_delay: -1}};
+
+ assert.isTrue(element._startUpdateCheckTimer.called);
+ assert.isFalse(element.fetchChangeUpdates.called);
+ });
+
+ test('_startUpdateCheckTimer up-to-date', () => {
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => Promise.resolve({isLatest: true}));
+
+ element._serverConfig = {change: {update_delay: 12345}};
+
+ assert.isTrue(element._startUpdateCheckTimer.called);
+ assert.isTrue(element.fetchChangeUpdates.called);
+ assert.equal(element.async.lastCall.args[1], 12345 * 1000);
+ });
+
+ test('_startUpdateCheckTimer out-of-date shows an alert', done => {
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => Promise.resolve({isLatest: false}));
+ element.addEventListener('show-alert', e => {
+ assert.equal(e.detail.message,
+ 'A newer patch set has been uploaded');
+ done();
+ });
+ element._serverConfig = {change: {update_delay: 12345}};
+ });
+
+ test('_startUpdateCheckTimer new status shows an alert', done => {
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({
+ isLatest: true,
+ newStatus: element.ChangeStatus.MERGED,
+ }));
+ element.addEventListener('show-alert', e => {
+ assert.equal(e.detail.message, 'This change has been merged');
+ done();
+ });
+ element._serverConfig = {change: {update_delay: 12345}};
+ });
+
+ test('_startUpdateCheckTimer new messages shows an alert', done => {
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({
+ isLatest: true,
+ newMessages: true,
+ }));
+ element.addEventListener('show-alert', e => {
+ assert.equal(e.detail.message,
+ 'There are new messages on this change');
+ done();
+ });
+ element._serverConfig = {change: {update_delay: 12345}};
+ });
+ });
+
+ test('canStartReview computation', () => {
+ const change1 = {};
+ const change2 = {
+ actions: {
+ ready: {
+ enabled: true,
+ },
+ },
+ };
+ const change3 = {
+ actions: {
+ ready: {
+ label: 'Ready for Review',
+ },
+ },
+ };
+ assert.isFalse(element._computeCanStartReview(change1));
+ assert.isTrue(element._computeCanStartReview(change2));
+ assert.isFalse(element._computeCanStartReview(change3));
+ });
+ });
+
+ test('header class computation', () => {
+ assert.equal(element._computeHeaderClass(), 'header');
+ assert.equal(element._computeHeaderClass(true), 'header editMode');
+ });
+
+ test('_maybeScrollToMessage', done => {
+ flush(() => {
+ const scrollStub = sandbox.stub(element.messagesList,
+ 'scrollToMessage');
+
+ element._maybeScrollToMessage('');
+ assert.isFalse(scrollStub.called);
+ element._maybeScrollToMessage('message');
+ assert.isFalse(scrollStub.called);
+ element._maybeScrollToMessage('#message-TEST');
+ assert.isTrue(scrollStub.called);
+ assert.equal(scrollStub.lastCall.args[0], 'TEST');
+ done();
+ });
+ });
+
+ test('topic update reloads related changes', () => {
+ sandbox.stub(element.$.relatedChanges, 'reload');
+ element.dispatchEvent(new CustomEvent('topic-changed'));
+ assert.isTrue(element.$.relatedChanges.reload.calledOnce);
+ });
+
+ test('_computeEditMode', () => {
+ const callCompute = (range, params) =>
+ element._computeEditMode({base: range}, {base: params});
+ assert.isFalse(callCompute({}, {}));
+ assert.isTrue(callCompute({}, {edit: true}));
+ assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1}, {}));
+ assert.isFalse(callCompute({basePatchNum: 'edit', patchNum: 1}, {}));
+ assert.isTrue(callCompute({basePatchNum: 1, patchNum: 'edit'}, {}));
+ });
+
+ test('_processEdit', () => {
+ element._patchRange = {};
+ const change = {
+ current_revision: 'foo',
+ revisions: {foo: {commit: {}, actions: {cherrypick: {enabled: true}}}},
+ };
+ let mockChange;
+
+ // With no edit, mockChange should be unmodified.
+ element._processEdit(mockChange = _.cloneDeep(change), null);
+ assert.deepEqual(mockChange, change);
+
+ // When edit is not based on the latest PS, current_revision should be
+ // unmodified.
+ const edit = {
+ base_patch_set_number: 1,
+ commit: {commit: 'bar'},
+ fetch: true,
+ };
+ element._processEdit(mockChange = _.cloneDeep(change), edit);
+ assert.notDeepEqual(mockChange, change);
+ assert.equal(mockChange.revisions.bar._number, element.EDIT_NAME);
+ assert.equal(mockChange.current_revision, change.current_revision);
+ assert.deepEqual(mockChange.revisions.bar.commit, {commit: 'bar'});
+ assert.notOk(mockChange.revisions.bar.actions);
+
+ edit.base_revision = 'foo';
+ element._processEdit(mockChange = _.cloneDeep(change), edit);
+ assert.notDeepEqual(mockChange, change);
+ assert.equal(mockChange.current_revision, 'bar');
+ assert.deepEqual(mockChange.revisions.bar.actions,
+ mockChange.revisions.foo.actions);
+
+ // If _patchRange.patchNum is defined, do not load edit.
+ element._patchRange.patchNum = 'baz';
+ change.current_revision = 'baz';
+ element._processEdit(mockChange = _.cloneDeep(change), edit);
+ assert.equal(element._patchRange.patchNum, 'baz');
+ assert.notOk(mockChange.revisions.bar.actions);
+ });
+
+ test('file-action-tap handling', () => {
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 1,
+ };
+ const fileList = element.$.fileList;
+ const Actions = GrEditConstants.Actions;
+ const controls = element.$.fileListHeader.$.editControls;
+ sandbox.stub(controls, 'openDeleteDialog');
+ sandbox.stub(controls, 'openRenameDialog');
+ sandbox.stub(controls, 'openRestoreDialog');
+ sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff');
+ sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
+
+ // Delete
+ fileList.dispatchEvent(new CustomEvent('file-action-tap', {
+ detail: {action: Actions.DELETE.id, path: 'foo'},
+ bubbles: true,
+ composed: true,
+ }));
+ flushAsynchronousOperations();
+
+ assert.isTrue(controls.openDeleteDialog.called);
+ assert.equal(controls.openDeleteDialog.lastCall.args[0], 'foo');
+
+ // Restore
+ fileList.dispatchEvent(new CustomEvent('file-action-tap', {
+ detail: {action: Actions.RESTORE.id, path: 'foo'},
+ bubbles: true,
+ composed: true,
+ }));
+ flushAsynchronousOperations();
+
+ assert.isTrue(controls.openRestoreDialog.called);
+ assert.equal(controls.openRestoreDialog.lastCall.args[0], 'foo');
+
+ // Rename
+ fileList.dispatchEvent(new CustomEvent('file-action-tap', {
+ detail: {action: Actions.RENAME.id, path: 'foo'},
+ bubbles: true,
+ composed: true,
+ }));
+ flushAsynchronousOperations();
+
+ assert.isTrue(controls.openRenameDialog.called);
+ assert.equal(controls.openRenameDialog.lastCall.args[0], 'foo');
+
+ // Open
+ fileList.dispatchEvent(new CustomEvent('file-action-tap', {
+ detail: {action: Actions.OPEN.id, path: 'foo'},
+ bubbles: true,
+ composed: true,
+ }));
+ flushAsynchronousOperations();
+
+ assert.isTrue(Gerrit.Nav.getEditUrlForDiff.called);
+ assert.equal(Gerrit.Nav.getEditUrlForDiff.lastCall.args[1], 'foo');
+ assert.equal(Gerrit.Nav.getEditUrlForDiff.lastCall.args[2], '1');
+ assert.isTrue(Gerrit.Nav.navigateToRelativeUrl.called);
+ });
+
+ test('_selectedRevision updates when patchNum is changed', () => {
+ const revision1 = {_number: 1, commit: {parents: []}};
+ const revision2 = {_number: 2, commit: {parents: []}};
+ sandbox.stub(element.$.restAPI, 'getChangeDetail').returns(
+ Promise.resolve({
+ revisions: {
+ aaa: revision1,
+ bbb: revision2,
+ },
+ labels: {},
+ actions: {},
+ current_revision: 'bbb',
+ change_id: 'loremipsumdolorsitamet',
+ }));
+ sandbox.stub(element, '_getEdit').returns(Promise.resolve());
+ sandbox.stub(element, '_getPreferences').returns(Promise.resolve({}));
+ element._patchRange = {patchNum: '2'};
+ return element._getChangeDetail().then(() => {
+ assert.strictEqual(element._selectedRevision, revision2);
+
+ element.set('_patchRange.patchNum', '1');
+ assert.strictEqual(element._selectedRevision, revision1);
+ });
+ });
+
+ test('_selectedRevision is assigned when patchNum is edit', () => {
+ const revision1 = {_number: 1, commit: {parents: []}};
+ const revision2 = {_number: 2, commit: {parents: []}};
+ const revision3 = {_number: 'edit', commit: {parents: []}};
+ sandbox.stub(element.$.restAPI, 'getChangeDetail').returns(
+ Promise.resolve({
+ revisions: {
+ aaa: revision1,
+ bbb: revision2,
+ ccc: revision3,
+ },
+ labels: {},
+ actions: {},
+ current_revision: 'ccc',
+ change_id: 'loremipsumdolorsitamet',
+ }));
+ sandbox.stub(element, '_getEdit').returns(Promise.resolve());
+ sandbox.stub(element, '_getPreferences').returns(Promise.resolve({}));
+ element._patchRange = {patchNum: 'edit'};
+ return element._getChangeDetail().then(() => {
+ assert.strictEqual(element._selectedRevision, revision3);
+ });
+ });
+
+ test('_sendShowChangeEvent', () => {
+ element._change = {labels: {}};
+ element._patchRange = {patchNum: 4};
+ element._mergeable = true;
+ const showStub = sandbox.stub(element.$.jsAPI, 'handleEvent');
+ element._sendShowChangeEvent();
+ assert.isTrue(showStub.calledOnce);
+ assert.equal(
+ showStub.lastCall.args[0], element.$.jsAPI.EventType.SHOW_CHANGE);
+ assert.deepEqual(showStub.lastCall.args[1], {
+ change: {labels: {}},
+ patchNum: 4,
+ info: {mergeable: true},
+ });
+ });
+
+ suite('_handleEditTap', () => {
+ let fireEdit;
+
+ setup(() => {
+ fireEdit = () => {
+ element.$.actions.dispatchEvent(new CustomEvent('edit-tap'));
+ };
+ navigateToChangeStub.restore();
+
+ element._change = {revisions: {rev1: {_number: 1}}};
+ });
+
+ test('edit exists in revisions', done => {
+ sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
+ assert.equal(args.length, 2);
+ assert.equal(args[1], element.EDIT_NAME); // patchNum
+ done();
+ });
+
+ element.set('_change.revisions.rev2', {_number: element.EDIT_NAME});
+ flushAsynchronousOperations();
+
+ fireEdit();
+ });
+
+ test('no edit exists in revisions, non-latest patchset', done => {
+ sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
+ assert.equal(args.length, 4);
+ assert.equal(args[1], 1); // patchNum
+ assert.equal(args[3], true); // opt_isEdit
+ done();
+ });
+
+ element.set('_change.revisions.rev2', {_number: 2});
+ element._patchRange = {patchNum: 1};
+ flushAsynchronousOperations();
+
+ fireEdit();
+ });
+
+ test('no edit exists in revisions, latest patchset', done => {
+ sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
+ assert.equal(args.length, 4);
+ // No patch should be specified when patchNum == latest.
+ assert.isNotOk(args[1]); // patchNum
+ assert.equal(args[3], true); // opt_isEdit
+ done();
+ });
+
+ element.set('_change.revisions.rev2', {_number: 2});
+ element._patchRange = {patchNum: 2};
+ flushAsynchronousOperations();
+
+ fireEdit();
+ });
+ });
+
+ test('_handleStopEditTap', done => {
+ sandbox.stub(element.$.metadata, '_computeLabelNames');
+ navigateToChangeStub.restore();
+ sandbox.stub(element, 'computeLatestPatchNum').returns(1);
+ sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
+ assert.equal(args.length, 2);
+ assert.equal(args[1], 1); // patchNum
+ done();
+ });
+
+ element._patchRange = {patchNum: 1};
+ element.$.actions.dispatchEvent(new CustomEvent('stop-edit-tap',
+ {bubbles: false}));
+ });
+
+ suite('plugin endpoints', () => {
+ test('endpoint params', done => {
+ element._change = {labels: {}};
+ element._selectedRevision = {};
+ let hookEl;
+ let plugin;
+ Gerrit.install(
+ p => {
+ plugin = p;
+ plugin.hook('change-view-integration').getLastAttached()
+ .then(
+ el => hookEl = el);
+ },
+ '0.1',
+ 'http://some/plugins/url.html');
+ flush(() => {
+ assert.strictEqual(hookEl.plugin, plugin);
+ assert.strictEqual(hookEl.change, element._change);
+ assert.strictEqual(hookEl.revision, element._selectedRevision);
+ done();
+ });
+ });
+ });
+
+ suite('_getMergeability', () => {
+ let getMergeableStub;
+
+ setup(() => {
+ element._change = {labels: {}};
+ getMergeableStub = sandbox.stub(element.$.restAPI, 'getMergeable')
+ .returns(Promise.resolve({mergeable: true}));
+ });
+
+ test('merged change', () => {
+ element._mergeable = null;
+ element._change.status = element.ChangeStatus.MERGED;
+ return element._getMergeability().then(() => {
+ assert.isFalse(element._mergeable);
+ assert.isFalse(getMergeableStub.called);
+ });
+ });
+
+ test('abandoned change', () => {
+ element._mergeable = null;
+ element._change.status = element.ChangeStatus.ABANDONED;
+ return element._getMergeability().then(() => {
+ assert.isFalse(element._mergeable);
+ assert.isFalse(getMergeableStub.called);
+ });
+ });
+
+ test('open change', () => {
+ element._mergeable = null;
+ return element._getMergeability().then(() => {
+ assert.isTrue(element._mergeable);
+ assert.isTrue(getMergeableStub.called);
+ });
+ });
+ });
+
+ test('_paramsChanged sets in projectLookup', () => {
+ sandbox.stub(element.$.relatedChanges, 'reload');
+ sandbox.stub(element, '_reload').returns(Promise.resolve());
+ const setStub = sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+ element._paramsChanged({
+ view: Gerrit.Nav.View.CHANGE,
+ changeNum: 101,
+ project: 'test-project',
+ });
+ assert.isTrue(setStub.calledOnce);
+ assert.isTrue(setStub.calledWith(101, 'test-project'));
+ });
+
+ test('_handleToggleStar called when star is tapped', () => {
+ element._change = {
+ owner: {_account_id: 1},
+ starred: false,
+ };
+ element._loggedIn = true;
+ const stub = sandbox.stub(element, '_handleToggleStar');
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(element.$.changeStar.shadowRoot
+ .querySelector('button'));
+ assert.isTrue(stub.called);
+ });
+
+ suite('gr-reporting tests', () => {
+ setup(() => {
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 1,
+ };
+ sandbox.stub(element, '_getChangeDetail').returns(Promise.resolve());
+ sandbox.stub(element, '_getProjectConfig').returns(Promise.resolve());
+ sandbox.stub(element, '_reloadComments').returns(Promise.resolve());
+ sandbox.stub(element, '_getMergeability').returns(Promise.resolve());
+ sandbox.stub(element, '_getLatestCommitMessage')
+ .returns(Promise.resolve());
+ });
+
+ test('don\'t report changedDisplayed on reply', done => {
+ const changeDisplayStub =
+ sandbox.stub(element.$.reporting, 'changeDisplayed');
+ const changeFullyLoadedStub =
+ sandbox.stub(element.$.reporting, 'changeFullyLoaded');
+ element._handleReplySent();
+ flush(() => {
+ assert.isFalse(changeDisplayStub.called);
+ assert.isFalse(changeFullyLoadedStub.called);
+ done();
+ });
+ });
+
+ test('report changedDisplayed on _paramsChanged', done => {
+ const changeDisplayStub =
+ sandbox.stub(element.$.reporting, 'changeDisplayed');
+ const changeFullyLoadedStub =
+ sandbox.stub(element.$.reporting, 'changeFullyLoaded');
+ element._paramsChanged({
+ view: Gerrit.Nav.View.CHANGE,
+ changeNum: 101,
+ project: 'test-project',
+ });
+ flush(() => {
+ assert.isTrue(changeDisplayStub.called);
+ assert.isTrue(changeFullyLoadedStub.called);
+ done();
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
deleted file mode 100644
index 52c0c89..0000000
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ /dev/null
@@ -1,90 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<!--
- The custom CSS property `--gr-formatted-text-prose-max-width` controls the max
- width of formatted text blocks that are not code.
--->
-
-<dom-module id="gr-comment-list">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- word-wrap: break-word;
- }
- .file {
- padding: var(--spacing-s) 0;
- }
- .container {
- display: flex;
- padding: var(--spacing-s) 0;
- }
- .lineNum {
- margin-right: var(--spacing-s);
- min-width: 135px;
- text-align: right;
- }
- .message {
- flex: 1;
- --gr-formatted-text-prose-max-width: 80ch;
- }
- @media screen and (max-width: 50em) {
- .container {
- flex-direction: column;
- }
- .lineNum {
- margin-right: 0;
- min-width: initial;
- text-align: left;
- }
- }
- </style>
- <template is="dom-repeat" items="[[_computeFilesFromComments(comments)]]" as="file">
- <div class="file"><a class="fileLink" href="[[_computeDiffURL(file, changeNum, comments)]]">[[computeDisplayPath(file)]]</a></div>
- <template is="dom-repeat"
- items="[[_computeCommentsForFile(comments, file)]]" as="comment">
- <div class="container">
- <a class="lineNum"
- href$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]">
- <span hidden$="[[!comment.line]]">
- <span>[[_computePatchDisplayName(comment)]]</span>
- Line <span>[[comment.line]]</span>
- </span>
- <span hidden$="[[comment.line]]">
- File comment:
- </span>
- </a>
- <gr-formatted-text
- class="message"
- no-trailing-margin
- content="[[comment.message]]"
- config="[[projectConfig.commentlinks]]"></gr-formatted-text>
- </div>
- </template>
- </template>
- </template>
- <script src="gr-comment-list.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index 16c55cd..2649733 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,79 +14,100 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+/*
+ The custom CSS property `--gr-formatted-text-prose-max-width` controls the max
+ width of formatted text blocks that are not code.
+*/
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.PathListMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrCommentList extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.PathListBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-comment-list'; }
+import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../scripts/bundled-polymer.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-formatted-text/gr-formatted-text.js';
+import '../../../styles/shared-styles.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-comment-list_html.js';
- static get properties() {
- return {
- changeNum: Number,
- comments: Object,
- patchNum: Number,
- projectName: String,
- /** @type {?} */
- projectConfig: Object,
- };
- }
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.PathListMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrCommentList extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.PathListBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _computeFilesFromComments(comments) {
- const arr = Object.keys(comments || {});
- return arr.sort(this.specialFilePathCompare);
- }
+ static get is() { return 'gr-comment-list'; }
- _isOnParent(comment) {
- return comment.side === 'PARENT';
- }
-
- _computeDiffURL(filePath, changeNum, allComments) {
- if ([filePath, changeNum, allComments].some(arg => arg === undefined)) {
- return;
- }
- const fileComments = this._computeCommentsForFile(allComments, filePath);
- // This can happen for files that don't exist anymore in the current ps.
- if (fileComments.length === 0) return;
- return Gerrit.Nav.getUrlForDiffById(changeNum, this.projectName,
- filePath, fileComments[0].patch_set);
- }
-
- _computeDiffLineURL(filePath, changeNum, patchNum, comment) {
- const basePatchNum = comment.hasOwnProperty('parent') ?
- -comment.parent : null;
- return Gerrit.Nav.getUrlForDiffById(changeNum, this.projectName,
- filePath, patchNum, basePatchNum, comment.line,
- this._isOnParent(comment));
- }
-
- _computeCommentsForFile(comments, filePath) {
- // Changes are not picked up by the dom-repeat due to the array instance
- // identity not changing even when it has elements added/removed from it.
- return (comments[filePath] || []).slice();
- }
-
- _computePatchDisplayName(comment) {
- if (this._isOnParent(comment)) {
- return 'Base, ';
- }
- if (comment.patch_set != this.patchNum) {
- return `PS${comment.patch_set}, `;
- }
- return '';
- }
+ static get properties() {
+ return {
+ changeNum: Number,
+ comments: Object,
+ patchNum: Number,
+ projectName: String,
+ /** @type {?} */
+ projectConfig: Object,
+ };
}
- customElements.define(GrCommentList.is, GrCommentList);
-})();
+ _computeFilesFromComments(comments) {
+ const arr = Object.keys(comments || {});
+ return arr.sort(this.specialFilePathCompare);
+ }
+
+ _isOnParent(comment) {
+ return comment.side === 'PARENT';
+ }
+
+ _computeDiffURL(filePath, changeNum, allComments) {
+ if ([filePath, changeNum, allComments].some(arg => arg === undefined)) {
+ return;
+ }
+ const fileComments = this._computeCommentsForFile(allComments, filePath);
+ // This can happen for files that don't exist anymore in the current ps.
+ if (fileComments.length === 0) return;
+ return Gerrit.Nav.getUrlForDiffById(changeNum, this.projectName,
+ filePath, fileComments[0].patch_set);
+ }
+
+ _computeDiffLineURL(filePath, changeNum, patchNum, comment) {
+ const basePatchNum = comment.hasOwnProperty('parent') ?
+ -comment.parent : null;
+ return Gerrit.Nav.getUrlForDiffById(changeNum, this.projectName,
+ filePath, patchNum, basePatchNum, comment.line,
+ this._isOnParent(comment));
+ }
+
+ _computeCommentsForFile(comments, filePath) {
+ // Changes are not picked up by the dom-repeat due to the array instance
+ // identity not changing even when it has elements added/removed from it.
+ return (comments[filePath] || []).slice();
+ }
+
+ _computePatchDisplayName(comment) {
+ if (this._isOnParent(comment)) {
+ return 'Base, ';
+ }
+ if (comment.patch_set != this.patchNum) {
+ return `PS${comment.patch_set}, `;
+ }
+ return '';
+ }
+}
+
+customElements.define(GrCommentList.is, GrCommentList);
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js
new file mode 100644
index 0000000..d50ba6a
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js
@@ -0,0 +1,69 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ word-wrap: break-word;
+ }
+ .file {
+ padding: var(--spacing-s) 0;
+ }
+ .container {
+ display: flex;
+ padding: var(--spacing-s) 0;
+ }
+ .lineNum {
+ margin-right: var(--spacing-s);
+ min-width: 135px;
+ text-align: right;
+ }
+ .message {
+ flex: 1;
+ --gr-formatted-text-prose-max-width: 80ch;
+ }
+ @media screen and (max-width: 50em) {
+ .container {
+ flex-direction: column;
+ }
+ .lineNum {
+ margin-right: 0;
+ min-width: initial;
+ text-align: left;
+ }
+ }
+ </style>
+ <template is="dom-repeat" items="[[_computeFilesFromComments(comments)]]" as="file">
+ <div class="file"><a class="fileLink" href="[[_computeDiffURL(file, changeNum, comments)]]">[[computeDisplayPath(file)]]</a></div>
+ <template is="dom-repeat" items="[[_computeCommentsForFile(comments, file)]]" as="comment">
+ <div class="container">
+ <a class="lineNum" href\$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]">
+ <span hidden\$="[[!comment.line]]">
+ <span>[[_computePatchDisplayName(comment)]]</span>
+ Line <span>[[comment.line]]</span>
+ </span>
+ <span hidden\$="[[comment.line]]">
+ File comment:
+ </span>
+ </a>
+ <gr-formatted-text class="message" no-trailing-margin="" content="[[comment.message]]" config="[[projectConfig.commentlinks]]"></gr-formatted-text>
+ </div>
+ </template>
+ </template>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
index a91ec0e..c064679 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-comment-list</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-comment-list.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,98 +30,100 @@
</template>
</test-fixture>
-<script>
- suite('gr-comment-list tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-comment-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-comment-list tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- sandbox.stub(Gerrit.Nav, 'mapCommentlinks', x => x);
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('_computeFilesFromComments w/ special file path sorting', () => {
- const comments = {
- 'file_b.html': [],
- 'file_c.css': [],
- 'file_a.js': [],
- 'test.cc': [],
- 'test.h': [],
- };
- const expected = [
- 'file_a.js',
- 'file_b.html',
- 'file_c.css',
- 'test.h',
- 'test.cc',
- ];
- const actual = element._computeFilesFromComments(comments);
- assert.deepEqual(actual, expected);
-
- assert.deepEqual(element._computeFilesFromComments(null), []);
- });
-
- test('_computePatchDisplayName', () => {
- const comment = {line: 123, side: 'REVISION', patch_set: 10};
-
- element.patchNum = 10;
- assert.equal(element._computePatchDisplayName(comment), '');
-
- element.patchNum = 9;
- assert.equal(element._computePatchDisplayName(comment), 'PS10, ');
-
- comment.side = 'PARENT';
- assert.equal(element._computePatchDisplayName(comment), 'Base, ');
- });
-
- test('config commentlinks propagate to formatted text', () => {
- element.comments = {
- 'test.h': [{
- author: {name: 'foo'},
- patch_set: 4,
- line: 10,
- updated: '2017-10-30 20:48:40.000000000',
- message: 'Ideadbeefdeadbeef',
- unresolved: true,
- }],
- };
- element.projectConfig = {
- commentlinks: {foo: {link: '#/q/$1', match: '(I[0-9a-f]{8,40})'}},
- };
- flushAsynchronousOperations();
- const formattedText = Polymer.dom(element.root).querySelector(
- 'gr-formatted-text.message');
- assert.isOk(formattedText.config);
- assert.deepEqual(formattedText.config,
- element.projectConfig.commentlinks);
- });
-
- test('_computeDiffLineURL', () => {
- const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
- element.projectName = 'proj';
- element.changeNum = 123;
-
- const comment = {line: 456};
- element._computeDiffLineURL('foo.cc', 123, 4, comment);
- assert.isTrue(getUrlStub.calledOnce);
- assert.deepEqual(getUrlStub.lastCall.args,
- [123, 'proj', 'foo.cc', 4, null, 456, false]);
-
- comment.side = 'PARENT';
- element._computeDiffLineURL('foo.cc', 123, 4, comment);
- assert.isTrue(getUrlStub.calledTwice);
- assert.deepEqual(getUrlStub.lastCall.args,
- [123, 'proj', 'foo.cc', 4, null, 456, true]);
-
- comment.parent = 12;
- element._computeDiffLineURL('foo.cc', 123, 4, comment);
- assert.isTrue(getUrlStub.calledThrice);
- assert.deepEqual(getUrlStub.lastCall.args,
- [123, 'proj', 'foo.cc', 4, -12, 456, true]);
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(Gerrit.Nav, 'mapCommentlinks', x => x);
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('_computeFilesFromComments w/ special file path sorting', () => {
+ const comments = {
+ 'file_b.html': [],
+ 'file_c.css': [],
+ 'file_a.js': [],
+ 'test.cc': [],
+ 'test.h': [],
+ };
+ const expected = [
+ 'file_a.js',
+ 'file_b.html',
+ 'file_c.css',
+ 'test.h',
+ 'test.cc',
+ ];
+ const actual = element._computeFilesFromComments(comments);
+ assert.deepEqual(actual, expected);
+
+ assert.deepEqual(element._computeFilesFromComments(null), []);
+ });
+
+ test('_computePatchDisplayName', () => {
+ const comment = {line: 123, side: 'REVISION', patch_set: 10};
+
+ element.patchNum = 10;
+ assert.equal(element._computePatchDisplayName(comment), '');
+
+ element.patchNum = 9;
+ assert.equal(element._computePatchDisplayName(comment), 'PS10, ');
+
+ comment.side = 'PARENT';
+ assert.equal(element._computePatchDisplayName(comment), 'Base, ');
+ });
+
+ test('config commentlinks propagate to formatted text', () => {
+ element.comments = {
+ 'test.h': [{
+ author: {name: 'foo'},
+ patch_set: 4,
+ line: 10,
+ updated: '2017-10-30 20:48:40.000000000',
+ message: 'Ideadbeefdeadbeef',
+ unresolved: true,
+ }],
+ };
+ element.projectConfig = {
+ commentlinks: {foo: {link: '#/q/$1', match: '(I[0-9a-f]{8,40})'}},
+ };
+ flushAsynchronousOperations();
+ const formattedText = dom(element.root).querySelector(
+ 'gr-formatted-text.message');
+ assert.isOk(formattedText.config);
+ assert.deepEqual(formattedText.config,
+ element.projectConfig.commentlinks);
+ });
+
+ test('_computeDiffLineURL', () => {
+ const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
+ element.projectName = 'proj';
+ element.changeNum = 123;
+
+ const comment = {line: 456};
+ element._computeDiffLineURL('foo.cc', 123, 4, comment);
+ assert.isTrue(getUrlStub.calledOnce);
+ assert.deepEqual(getUrlStub.lastCall.args,
+ [123, 'proj', 'foo.cc', 4, null, 456, false]);
+
+ comment.side = 'PARENT';
+ element._computeDiffLineURL('foo.cc', 123, 4, comment);
+ assert.isTrue(getUrlStub.calledTwice);
+ assert.deepEqual(getUrlStub.lastCall.args,
+ [123, 'proj', 'foo.cc', 4, null, 456, true]);
+
+ comment.parent = 12;
+ element._computeDiffLineURL('foo.cc', 123, 4, comment);
+ assert.isTrue(getUrlStub.calledThrice);
+ assert.deepEqual(getUrlStub.lastCall.args,
+ [123, 'proj', 'foo.cc', 4, -12, 456, true]);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html
deleted file mode 100644
index 902bf41..0000000
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html
+++ /dev/null
@@ -1,47 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
-
-<dom-module id="gr-commit-info">
- <template>
- <style include="shared-styles">
- .container {
- align-items: center;
- display: flex;
- }
- </style>
- <div class="container">
- <template is="dom-if" if="[[_showWebLink]]">
- <a target="_blank" rel="noopener"
- href$="[[_webLink]]">[[_computeShortHash(commitInfo)]]</a>
- </template>
- <template is="dom-if" if="[[!_showWebLink]]">
- [[_computeShortHash(commitInfo)]]
- </template>
- <gr-copy-clipboard
- has-tooltip
- button-title="Copy full SHA to clipboard"
- hide-input
- text="[[commitInfo.commit]]">
- </gr-copy-clipboard>
- </div>
- </template>
- <script src="gr-commit-info.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
index a339865..79a3692 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
@@ -14,68 +14,75 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrCommitInfo extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-commit-info'; }
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-commit-info_html.js';
- static get properties() {
- return {
- change: Object,
- /** @type {?} */
- commitInfo: Object,
- serverConfig: Object,
- _showWebLink: {
- type: Boolean,
- computed: '_computeShowWebLink(change, commitInfo, serverConfig)',
- },
- _webLink: {
- type: String,
- computed: '_computeWebLink(change, commitInfo, serverConfig)',
- },
- };
- }
+/** @extends Polymer.Element */
+class GrCommitInfo extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _getWeblink(change, commitInfo, config) {
- return Gerrit.Nav.getPatchSetWeblink(
- change.project,
- commitInfo.commit,
- {
- weblinks: commitInfo.web_links,
- config,
- });
- }
+ static get is() { return 'gr-commit-info'; }
- _computeShowWebLink(change, commitInfo, serverConfig) {
- // Polymer 2: check for undefined
- if ([change, commitInfo, serverConfig].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const weblink = this._getWeblink(change, commitInfo, serverConfig);
- return !!weblink && !!weblink.url;
- }
-
- _computeWebLink(change, commitInfo, serverConfig) {
- // Polymer 2: check for undefined
- if ([change, commitInfo, serverConfig].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const {url} = this._getWeblink(change, commitInfo, serverConfig) || {};
- return url;
- }
-
- _computeShortHash(commitInfo) {
- const {name} =
- this._getWeblink(this.change, commitInfo, this.serverConfig) || {};
- return name;
- }
+ static get properties() {
+ return {
+ change: Object,
+ /** @type {?} */
+ commitInfo: Object,
+ serverConfig: Object,
+ _showWebLink: {
+ type: Boolean,
+ computed: '_computeShowWebLink(change, commitInfo, serverConfig)',
+ },
+ _webLink: {
+ type: String,
+ computed: '_computeWebLink(change, commitInfo, serverConfig)',
+ },
+ };
}
- customElements.define(GrCommitInfo.is, GrCommitInfo);
-})();
+ _getWeblink(change, commitInfo, config) {
+ return Gerrit.Nav.getPatchSetWeblink(
+ change.project,
+ commitInfo.commit,
+ {
+ weblinks: commitInfo.web_links,
+ config,
+ });
+ }
+
+ _computeShowWebLink(change, commitInfo, serverConfig) {
+ // Polymer 2: check for undefined
+ if ([change, commitInfo, serverConfig].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const weblink = this._getWeblink(change, commitInfo, serverConfig);
+ return !!weblink && !!weblink.url;
+ }
+
+ _computeWebLink(change, commitInfo, serverConfig) {
+ // Polymer 2: check for undefined
+ if ([change, commitInfo, serverConfig].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const {url} = this._getWeblink(change, commitInfo, serverConfig) || {};
+ return url;
+ }
+
+ _computeShortHash(commitInfo) {
+ const {name} =
+ this._getWeblink(this.change, commitInfo, this.serverConfig) || {};
+ return name;
+ }
+}
+
+customElements.define(GrCommitInfo.is, GrCommitInfo);
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.js b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.js
new file mode 100644
index 0000000..ffd36f4
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.js
@@ -0,0 +1,36 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ .container {
+ align-items: center;
+ display: flex;
+ }
+ </style>
+ <div class="container">
+ <template is="dom-if" if="[[_showWebLink]]">
+ <a target="_blank" rel="noopener" href\$="[[_webLink]]">[[_computeShortHash(commitInfo)]]</a>
+ </template>
+ <template is="dom-if" if="[[!_showWebLink]]">
+ [[_computeShortHash(commitInfo)]]
+ </template>
+ <gr-copy-clipboard has-tooltip="" button-title="Copy full SHA to clipboard" hide-input="" text="[[commitInfo.commit]]">
+ </gr-copy-clipboard>
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
index b063561..74d4cca 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
@@ -19,16 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-commit-info</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="../../core/gr-router/gr-router.html">
-<link rel="import" href="gr-commit-info.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,105 +30,107 @@
</template>
</test-fixture>
-<script>
- suite('gr-commit-info tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../core/gr-router/gr-router.js';
+import './gr-commit-info.js';
+suite('gr-commit-info tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('weblinks use Gerrit.Nav interface', () => {
- const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
- .returns([{name: 'stubb', url: '#s'}]);
- element.change = {};
- element.commitInfo = {};
- element.serverConfig = {};
- assert.isTrue(weblinksStub.called);
- });
-
- test('no web link when unavailable', () => {
- element.commitInfo = {};
- element.serverConfig = {};
- element.change = {labels: [], project: ''};
-
- assert.isNotOk(element._computeShowWebLink(element.change,
- element.commitInfo, element.serverConfig));
- });
-
- test('use web link when available', () => {
- const router = document.createElement('gr-router');
- sandbox.stub(Gerrit.Nav, '_generateWeblinks',
- router._generateWeblinks.bind(router));
-
- element.change = {labels: [], project: ''};
- element.commitInfo =
- {commit: 'commitsha', web_links: [{name: 'gitweb', url: 'link-url'}]};
- element.serverConfig = {};
-
- assert.isOk(element._computeShowWebLink(element.change,
- element.commitInfo, element.serverConfig));
- assert.equal(element._computeWebLink(element.change, element.commitInfo,
- element.serverConfig), 'link-url');
- });
-
- test('does not relativize web links that begin with scheme', () => {
- const router = document.createElement('gr-router');
- sandbox.stub(Gerrit.Nav, '_generateWeblinks',
- router._generateWeblinks.bind(router));
-
- element.change = {labels: [], project: ''};
- element.commitInfo = {
- commit: 'commitsha',
- web_links: [{name: 'gitweb', url: 'https://link-url'}],
- };
- element.serverConfig = {};
-
- assert.isOk(element._computeShowWebLink(element.change,
- element.commitInfo, element.serverConfig));
- assert.equal(element._computeWebLink(element.change, element.commitInfo,
- element.serverConfig), 'https://link-url');
- });
-
- test('ignore web links that are neither gitweb nor gitiles', () => {
- const router = document.createElement('gr-router');
- sandbox.stub(Gerrit.Nav, '_generateWeblinks',
- router._generateWeblinks.bind(router));
-
- element.change = {project: 'project-name'};
- element.commitInfo = {
- commit: 'commit-sha',
- web_links: [
- {
- name: 'ignore',
- url: 'ignore',
- },
- {
- name: 'gitiles',
- url: 'https://link-url',
- },
- ],
- };
- element.serverConfig = {};
-
- assert.isOk(element._computeShowWebLink(element.change,
- element.commitInfo, element.serverConfig));
- assert.equal(element._computeWebLink(element.change, element.commitInfo,
- element.serverConfig), 'https://link-url');
-
- // Remove gitiles link.
- element.commitInfo.web_links.splice(1, 1);
- assert.isNotOk(element._computeShowWebLink(element.change,
- element.commitInfo, element.serverConfig));
- assert.isNotOk(element._computeWebLink(element.change, element.commitInfo,
- element.serverConfig));
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('weblinks use Gerrit.Nav interface', () => {
+ const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
+ .returns([{name: 'stubb', url: '#s'}]);
+ element.change = {};
+ element.commitInfo = {};
+ element.serverConfig = {};
+ assert.isTrue(weblinksStub.called);
+ });
+
+ test('no web link when unavailable', () => {
+ element.commitInfo = {};
+ element.serverConfig = {};
+ element.change = {labels: [], project: ''};
+
+ assert.isNotOk(element._computeShowWebLink(element.change,
+ element.commitInfo, element.serverConfig));
+ });
+
+ test('use web link when available', () => {
+ const router = document.createElement('gr-router');
+ sandbox.stub(Gerrit.Nav, '_generateWeblinks',
+ router._generateWeblinks.bind(router));
+
+ element.change = {labels: [], project: ''};
+ element.commitInfo =
+ {commit: 'commitsha', web_links: [{name: 'gitweb', url: 'link-url'}]};
+ element.serverConfig = {};
+
+ assert.isOk(element._computeShowWebLink(element.change,
+ element.commitInfo, element.serverConfig));
+ assert.equal(element._computeWebLink(element.change, element.commitInfo,
+ element.serverConfig), 'link-url');
+ });
+
+ test('does not relativize web links that begin with scheme', () => {
+ const router = document.createElement('gr-router');
+ sandbox.stub(Gerrit.Nav, '_generateWeblinks',
+ router._generateWeblinks.bind(router));
+
+ element.change = {labels: [], project: ''};
+ element.commitInfo = {
+ commit: 'commitsha',
+ web_links: [{name: 'gitweb', url: 'https://link-url'}],
+ };
+ element.serverConfig = {};
+
+ assert.isOk(element._computeShowWebLink(element.change,
+ element.commitInfo, element.serverConfig));
+ assert.equal(element._computeWebLink(element.change, element.commitInfo,
+ element.serverConfig), 'https://link-url');
+ });
+
+ test('ignore web links that are neither gitweb nor gitiles', () => {
+ const router = document.createElement('gr-router');
+ sandbox.stub(Gerrit.Nav, '_generateWeblinks',
+ router._generateWeblinks.bind(router));
+
+ element.change = {project: 'project-name'};
+ element.commitInfo = {
+ commit: 'commit-sha',
+ web_links: [
+ {
+ name: 'ignore',
+ url: 'ignore',
+ },
+ {
+ name: 'gitiles',
+ url: 'https://link-url',
+ },
+ ],
+ };
+ element.serverConfig = {};
+
+ assert.isOk(element._computeShowWebLink(element.change,
+ element.commitInfo, element.serverConfig));
+ assert.equal(element._computeWebLink(element.change, element.commitInfo,
+ element.serverConfig), 'https://link-url');
+
+ // Remove gitiles link.
+ element.commitInfo.web_links.splice(1, 1);
+ assert.isNotOk(element._computeShowWebLink(element.change,
+ element.commitInfo, element.serverConfig));
+ assert.isNotOk(element._computeWebLink(element.change, element.commitInfo,
+ element.serverConfig));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
deleted file mode 100644
index 9e7857c4..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
+++ /dev/null
@@ -1,68 +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.
--->
-
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-confirm-abandon-dialog">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- }
- :host([disabled]) {
- opacity: .5;
- pointer-events: none;
- }
- .main {
- display: flex;
- flex-direction: column;
- width: 100%;
- }
- 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="Abandon"
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap">
- <div class="header" slot="header">Abandon Change</div>
- <div class="main" slot="main">
- <label for="messageInput">Abandon Message</label>
- <iron-autogrow-textarea
- id="messageInput"
- class="message"
- autocomplete="on"
- placeholder="<Insert reasoning here>"
- bind-value="{{message}}"></iron-autogrow-textarea>
- </div>
- </gr-dialog>
- </template>
- <script src="gr-confirm-abandon-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
index 555c605..d950988 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
@@ -14,69 +14,79 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../../styles/shared-styles.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-confirm-abandon-dialog_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrConfirmAbandonDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-abandon-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
*/
- class GrConfirmAbandonDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-confirm-abandon-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
- message: String,
- };
- }
-
- get keyBindings() {
- return {
- 'ctrl+enter meta+enter': '_handleEnterKey',
- };
- }
-
- resetFocus() {
- this.$.messageInput.textarea.focus();
- }
-
- _handleEnterKey(e) {
- this._confirm();
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this._confirm();
- }
-
- _confirm() {
- this.fire('confirm', {reason: this.message}, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', null, {bubbles: false});
- }
+ static get properties() {
+ return {
+ message: String,
+ };
}
- customElements.define(GrConfirmAbandonDialog.is, GrConfirmAbandonDialog);
-})();
+ get keyBindings() {
+ return {
+ 'ctrl+enter meta+enter': '_handleEnterKey',
+ };
+ }
+
+ resetFocus() {
+ this.$.messageInput.textarea.focus();
+ }
+
+ _handleEnterKey(e) {
+ this._confirm();
+ }
+
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this._confirm();
+ }
+
+ _confirm() {
+ this.fire('confirm', {reason: this.message}, {bubbles: false});
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', null, {bubbles: false});
+ }
+}
+
+customElements.define(GrConfirmAbandonDialog.is, GrConfirmAbandonDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.js
new file mode 100644
index 0000000..e8b530b
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.js
@@ -0,0 +1,52 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ }
+ :host([disabled]) {
+ opacity: .5;
+ pointer-events: none;
+ }
+ .main {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ }
+ 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="Abandon" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap">
+ <div class="header" slot="header">Abandon Change</div>
+ <div class="main" slot="main">
+ <label for="messageInput">Abandon Message</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/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
index 3786174..c9bea1c 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-confirm-abandon-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-confirm-abandon-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,46 +30,47 @@
</template>
</test-fixture>
-<script>
- suite('gr-confirm-abandon-dialog tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-confirm-abandon-dialog.js';
+suite('gr-confirm-abandon-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('_handleConfirmTap', () => {
- const confirmHandler = sandbox.stub();
- element.addEventListener('confirm', confirmHandler);
- sandbox.spy(element, '_handleConfirmTap');
- sandbox.spy(element, '_confirm');
- element.shadowRoot
- .querySelector('gr-dialog').fire('confirm');
- assert.isTrue(confirmHandler.called);
- assert.isTrue(confirmHandler.calledOnce);
- assert.isTrue(element._handleConfirmTap.called);
- assert.isTrue(element._confirm.called);
- assert.isTrue(element._confirm.called);
- assert.isTrue(element._confirm.calledOnce);
- });
-
- test('_handleCancelTap', () => {
- const cancelHandler = sandbox.stub();
- element.addEventListener('cancel', cancelHandler);
- sandbox.spy(element, '_handleCancelTap');
- element.shadowRoot
- .querySelector('gr-dialog').fire('cancel');
- assert.isTrue(cancelHandler.called);
- assert.isTrue(cancelHandler.calledOnce);
- assert.isTrue(element._handleCancelTap.called);
- assert.isTrue(element._handleCancelTap.calledOnce);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_handleConfirmTap', () => {
+ const confirmHandler = sandbox.stub();
+ element.addEventListener('confirm', confirmHandler);
+ sandbox.spy(element, '_handleConfirmTap');
+ sandbox.spy(element, '_confirm');
+ element.shadowRoot
+ .querySelector('gr-dialog').fire('confirm');
+ assert.isTrue(confirmHandler.called);
+ assert.isTrue(confirmHandler.calledOnce);
+ assert.isTrue(element._handleConfirmTap.called);
+ assert.isTrue(element._confirm.called);
+ assert.isTrue(element._confirm.called);
+ assert.isTrue(element._confirm.calledOnce);
+ });
+
+ test('_handleCancelTap', () => {
+ const cancelHandler = sandbox.stub();
+ element.addEventListener('cancel', cancelHandler);
+ sandbox.spy(element, '_handleCancelTap');
+ element.shadowRoot
+ .querySelector('gr-dialog').fire('cancel');
+ assert.isTrue(cancelHandler.called);
+ assert.isTrue(cancelHandler.calledOnce);
+ assert.isTrue(element._handleCancelTap.called);
+ assert.isTrue(element._handleCancelTap.calledOnce);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html
deleted file mode 100644
index b9e9155..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html
+++ /dev/null
@@ -1,52 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-
-<dom-module id="gr-confirm-cherrypick-conflict-dialog">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- }
- :host([disabled]) {
- opacity: .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>
- </template>
- <script src="gr-confirm-cherrypick-conflict-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
index 35e9afb..9c5ddf4 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
@@ -14,45 +14,54 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-confirm-cherrypick-conflict-dialog_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrConfirmCherrypickConflictDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-cherrypick-conflict-dialog'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
*/
- class GrConfirmCherrypickConflictDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-confirm-cherrypick-conflict-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
+ /**
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
+ */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('confirm', null, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', null, {bubbles: false});
- }
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('confirm', null, {bubbles: false});
}
- customElements.define(GrConfirmCherrypickConflictDialog.is,
- GrConfirmCherrypickConflictDialog);
-})();
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', null, {bubbles: false});
+ }
+}
+
+customElements.define(GrConfirmCherrypickConflictDialog.is,
+ GrConfirmCherrypickConflictDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.js
new file mode 100644
index 0000000..c03c246
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.js
@@ -0,0 +1,42 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ }
+ :host([disabled]) {
+ opacity: .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.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
index 7c9896a..d78be11 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-confirm-cherrypick-conflict-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-confirm-cherrypick-conflict-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,41 +30,42 @@
</template>
</test-fixture>
-<script>
- suite('gr-confirm-cherrypick-conflict-dialog tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-confirm-cherrypick-conflict-dialog.js';
+suite('gr-confirm-cherrypick-conflict-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('_handleConfirmTap', () => {
- const confirmHandler = sandbox.stub();
- element.addEventListener('confirm', confirmHandler);
- sandbox.spy(element, '_handleConfirmTap');
- element.shadowRoot
- .querySelector('gr-dialog').fire('confirm');
- assert.isTrue(confirmHandler.called);
- assert.isTrue(confirmHandler.calledOnce);
- assert.isTrue(element._handleConfirmTap.called);
- assert.isTrue(element._handleConfirmTap.calledOnce);
- });
-
- test('_handleCancelTap', () => {
- const cancelHandler = sandbox.stub();
- element.addEventListener('cancel', cancelHandler);
- sandbox.spy(element, '_handleCancelTap');
- element.shadowRoot
- .querySelector('gr-dialog').fire('cancel');
- assert.isTrue(cancelHandler.called);
- assert.isTrue(cancelHandler.calledOnce);
- assert.isTrue(element._handleCancelTap.called);
- assert.isTrue(element._handleCancelTap.calledOnce);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('_handleConfirmTap', () => {
+ const confirmHandler = sandbox.stub();
+ element.addEventListener('confirm', confirmHandler);
+ sandbox.spy(element, '_handleConfirmTap');
+ element.shadowRoot
+ .querySelector('gr-dialog').fire('confirm');
+ assert.isTrue(confirmHandler.called);
+ assert.isTrue(confirmHandler.calledOnce);
+ assert.isTrue(element._handleConfirmTap.called);
+ assert.isTrue(element._handleConfirmTap.calledOnce);
+ });
+
+ test('_handleCancelTap', () => {
+ const cancelHandler = sandbox.stub();
+ element.addEventListener('cancel', cancelHandler);
+ sandbox.spy(element, '_handleCancelTap');
+ element.shadowRoot
+ .querySelector('gr-dialog').fire('cancel');
+ assert.isTrue(cancelHandler.called);
+ assert.isTrue(cancelHandler.calledOnce);
+ assert.isTrue(element._handleCancelTap.called);
+ assert.isTrue(element._handleCancelTap.calledOnce);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
deleted file mode 100644
index cab9fd6..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
+++ /dev/null
@@ -1,101 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-confirm-cherrypick-dialog">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- }
- :host([disabled]) {
- opacity: .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%;
- }
- 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="Cherry Pick"
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap">
- <div class="header" slot="header">Cherry Pick Change to Another Branch</div>
- <div class="main" slot="main">
- <label for="branchInput">
- Cherry Pick to branch
- </label>
- <gr-autocomplete
- id="branchInput"
- text="{{branch}}"
- query="[[_query]]"
- placeholder="Destination branch">
- </gr-autocomplete>
- <label for="baseInput">
- Provide base commit sha1 for cherry-pick
- </label>
- <iron-input
- maxlength="40"
- placeholder="(optional)"
- bind-value="{{baseCommit}}">
- <input
- is="iron-input"
- id="baseCommitInput"
- maxlength="40"
- placeholder="(optional)"
- bind-value="{{baseCommit}}">
- </iron-input>
- <label for="messageInput">
- Cherry Pick Commit 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>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-confirm-cherrypick-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
index 2b10a97..7405a30 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
@@ -14,115 +14,128 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const SUGGESTIONS_LIMIT = 15;
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-confirm-cherrypick-dialog_html.js';
+
+const SUGGESTIONS_LIMIT = 15;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrConfirmCherrypickDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-cherrypick-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
*/
- class GrConfirmCherrypickDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-confirm-cherrypick-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
- branch: String,
- baseCommit: String,
- changeStatus: String,
- commitMessage: String,
- commitNum: String,
- message: String,
- project: String,
- _query: {
- type: Function,
- value() {
- return this._getProjectBranchesSuggestions.bind(this);
- },
+ static get properties() {
+ return {
+ branch: String,
+ baseCommit: String,
+ changeStatus: String,
+ commitMessage: String,
+ commitNum: String,
+ message: String,
+ project: String,
+ _query: {
+ type: Function,
+ value() {
+ return this._getProjectBranchesSuggestions.bind(this);
},
- };
- }
-
- static get observers() {
- return [
- '_computeMessage(changeStatus, commitNum, commitMessage)',
- ];
- }
-
- _computeMessage(changeStatus, commitNum, commitMessage) {
- // Polymer 2: check for undefined
- if ([
- changeStatus,
- commitNum,
- commitMessage,
- ].some(arg => arg === undefined)) {
- return;
- }
-
- let newMessage = commitMessage;
-
- if (changeStatus === 'MERGED') {
- newMessage += '(cherry picked from commit ' + commitNum + ')';
- }
- this.message = newMessage;
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('confirm', null, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', null, {bubbles: false});
- }
-
- resetFocus() {
- this.$.branchInput.focus();
- }
-
- _getProjectBranchesSuggestions(input) {
- if (input.startsWith('refs/heads/')) {
- input = input.substring('refs/heads/'.length);
- }
- return this.$.restAPI.getRepoBranches(
- input, this.project, SUGGESTIONS_LIMIT).then(response => {
- const branches = [];
- let branch;
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- if (response[key].ref.startsWith('refs/heads/')) {
- branch = response[key].ref.substring('refs/heads/'.length);
- } else {
- branch = response[key].ref;
- }
- branches.push({
- name: branch,
- });
- }
- return branches;
- });
- }
+ },
+ };
}
- customElements.define(GrConfirmCherrypickDialog.is,
- GrConfirmCherrypickDialog);
-})();
+ static get observers() {
+ return [
+ '_computeMessage(changeStatus, commitNum, commitMessage)',
+ ];
+ }
+
+ _computeMessage(changeStatus, commitNum, commitMessage) {
+ // Polymer 2: check for undefined
+ if ([
+ changeStatus,
+ commitNum,
+ commitMessage,
+ ].some(arg => arg === undefined)) {
+ return;
+ }
+
+ let newMessage = commitMessage;
+
+ if (changeStatus === 'MERGED') {
+ newMessage += '(cherry picked from commit ' + commitNum + ')';
+ }
+ this.message = newMessage;
+ }
+
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('confirm', null, {bubbles: false});
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', null, {bubbles: false});
+ }
+
+ resetFocus() {
+ this.$.branchInput.focus();
+ }
+
+ _getProjectBranchesSuggestions(input) {
+ if (input.startsWith('refs/heads/')) {
+ input = input.substring('refs/heads/'.length);
+ }
+ return this.$.restAPI.getRepoBranches(
+ input, this.project, SUGGESTIONS_LIMIT).then(response => {
+ const branches = [];
+ let branch;
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ if (response[key].ref.startsWith('refs/heads/')) {
+ branch = response[key].ref.substring('refs/heads/'.length);
+ } else {
+ branch = response[key].ref;
+ }
+ branches.push({
+ name: branch,
+ });
+ }
+ return branches;
+ });
+ }
+}
+
+customElements.define(GrConfirmCherrypickDialog.is,
+ GrConfirmCherrypickDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js
new file mode 100644
index 0000000..ee6e55d
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js
@@ -0,0 +1,69 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ }
+ :host([disabled]) {
+ opacity: .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%;
+ }
+ 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="Cherry Pick" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap">
+ <div class="header" slot="header">Cherry Pick Change to Another Branch</div>
+ <div class="main" slot="main">
+ <label for="branchInput">
+ Cherry Pick to branch
+ </label>
+ <gr-autocomplete id="branchInput" text="{{branch}}" query="[[_query]]" placeholder="Destination branch">
+ </gr-autocomplete>
+ <label for="baseInput">
+ Provide base commit sha1 for cherry-pick
+ </label>
+ <iron-input maxlength="40" placeholder="(optional)" bind-value="{{baseCommit}}">
+ <input is="iron-input" id="baseCommitInput" maxlength="40" placeholder="(optional)" bind-value="{{baseCommit}}">
+ </iron-input>
+ <label for="messageInput">
+ Cherry Pick Commit 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>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
index 12e6252..780b2ee 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-confirm-cherrypick-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-confirm-cherrypick-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,85 +30,86 @@
</template>
</test-fixture>
-<script>
- suite('gr-confirm-cherrypick-dialog tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-confirm-cherrypick-dialog.js';
+suite('gr-confirm-cherrypick-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getRepoBranches(input) {
- if (input.startsWith('test')) {
- return Promise.resolve([
- {
- ref: 'refs/heads/test-branch',
- revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
- can_delete: true,
- },
- ]);
- } else {
- return Promise.resolve({});
- }
- },
- });
- element = fixture('basic');
- element.project = 'test-project';
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getRepoBranches(input) {
+ if (input.startsWith('test')) {
+ return Promise.resolve([
+ {
+ ref: 'refs/heads/test-branch',
+ revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
+ can_delete: true,
+ },
+ ]);
+ } else {
+ return Promise.resolve({});
+ }
+ },
});
+ element = fixture('basic');
+ element.project = 'test-project';
+ });
- teardown(() => { sandbox.restore(); });
+ teardown(() => { sandbox.restore(); });
- test('with merged change', () => {
- element.changeStatus = 'MERGED';
- element.commitMessage = 'message\n';
- element.commitNum = '123';
- element.branch = 'master';
- flushAsynchronousOperations();
- const expectedMessage = 'message\n(cherry picked from commit 123)';
- assert.equal(element.message, expectedMessage);
- });
+ test('with merged change', () => {
+ element.changeStatus = 'MERGED';
+ element.commitMessage = 'message\n';
+ element.commitNum = '123';
+ element.branch = 'master';
+ flushAsynchronousOperations();
+ const expectedMessage = 'message\n(cherry picked from commit 123)';
+ assert.equal(element.message, expectedMessage);
+ });
- test('with unmerged change', () => {
- element.changeStatus = 'OPEN';
- element.commitMessage = 'message\n';
- element.commitNum = '123';
- element.branch = 'master';
- flushAsynchronousOperations();
- const expectedMessage = 'message\n';
- assert.equal(element.message, expectedMessage);
- });
+ test('with unmerged change', () => {
+ element.changeStatus = 'OPEN';
+ element.commitMessage = 'message\n';
+ element.commitNum = '123';
+ element.branch = 'master';
+ flushAsynchronousOperations();
+ const expectedMessage = 'message\n';
+ assert.equal(element.message, expectedMessage);
+ });
- test('with updated commit message', () => {
- element.changeStatus = 'OPEN';
- element.commitMessage = 'message\n';
- element.commitNum = '123';
- element.branch = 'master';
- const myNewMessage = 'updated commit message';
- element.message = myNewMessage;
- flushAsynchronousOperations();
- assert.equal(element.message, myNewMessage);
- });
+ test('with updated commit message', () => {
+ element.changeStatus = 'OPEN';
+ element.commitMessage = 'message\n';
+ element.commitNum = '123';
+ element.branch = 'master';
+ const myNewMessage = 'updated commit message';
+ element.message = myNewMessage;
+ flushAsynchronousOperations();
+ assert.equal(element.message, myNewMessage);
+ });
- test('_getProjectBranchesSuggestions empty', done => {
- element._getProjectBranchesSuggestions('nonexistent').then(branches => {
- assert.equal(branches.length, 0);
- done();
- });
- });
-
- test('resetFocus', () => {
- const focusStub = sandbox.stub(element.$.branchInput, 'focus');
- element.resetFocus();
- assert.isTrue(focusStub.called);
- });
-
- test('_getProjectBranchesSuggestions non-empty', done => {
- element._getProjectBranchesSuggestions('test-branch').then(branches => {
- assert.equal(branches.length, 1);
- assert.equal(branches[0].name, 'test-branch');
- done();
- });
+ test('_getProjectBranchesSuggestions empty', done => {
+ element._getProjectBranchesSuggestions('nonexistent').then(branches => {
+ assert.equal(branches.length, 0);
+ done();
});
});
+
+ test('resetFocus', () => {
+ const focusStub = sandbox.stub(element.$.branchInput, 'focus');
+ element.resetFocus();
+ assert.isTrue(focusStub.called);
+ });
+
+ test('_getProjectBranchesSuggestions non-empty', done => {
+ element._getProjectBranchesSuggestions('test-branch').then(branches => {
+ assert.equal(branches.length, 1);
+ assert.equal(branches[0].name, 'test-branch');
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
deleted file mode 100644
index f65ec03..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
+++ /dev/null
@@ -1,90 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-confirm-move-dialog">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- width: 30em;
- }
- :host([disabled]) {
- opacity: .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>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-confirm-move-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
index 8932af7..8316951 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
@@ -14,90 +14,102 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
- const SUGGESTIONS_LIMIT = 15;
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-confirm-move-dialog_html.js';
+
+const SUGGESTIONS_LIMIT = 15;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrConfirmMoveDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-move-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
*/
- class GrConfirmMoveDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-confirm-move-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
- branch: String,
- message: String,
- project: String,
- _query: {
- type: Function,
- value() {
- return this._getProjectBranchesSuggestions.bind(this);
- },
+ static get properties() {
+ return {
+ branch: String,
+ message: String,
+ project: String,
+ _query: {
+ type: Function,
+ value() {
+ return this._getProjectBranchesSuggestions.bind(this);
},
- };
- }
-
- get keyBindings() {
- return {
- 'ctrl+enter meta+enter': '_handleConfirmTap',
- };
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('confirm', null, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', null, {bubbles: false});
- }
-
- _getProjectBranchesSuggestions(input) {
- if (input.startsWith('refs/heads/')) {
- input = input.substring('refs/heads/'.length);
- }
- return this.$.restAPI.getRepoBranches(
- input, this.project, SUGGESTIONS_LIMIT).then(response => {
- const branches = [];
- let branch;
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- if (response[key].ref.startsWith('refs/heads/')) {
- branch = response[key].ref.substring('refs/heads/'.length);
- } else {
- branch = response[key].ref;
- }
- branches.push({
- name: branch,
- });
- }
- return branches;
- });
- }
+ },
+ };
}
- customElements.define(GrConfirmMoveDialog.is, GrConfirmMoveDialog);
-})();
+ get keyBindings() {
+ return {
+ 'ctrl+enter meta+enter': '_handleConfirmTap',
+ };
+ }
+
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('confirm', null, {bubbles: false});
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', null, {bubbles: false});
+ }
+
+ _getProjectBranchesSuggestions(input) {
+ if (input.startsWith('refs/heads/')) {
+ input = input.substring('refs/heads/'.length);
+ }
+ return this.$.restAPI.getRepoBranches(
+ input, this.project, SUGGESTIONS_LIMIT).then(response => {
+ const branches = [];
+ let branch;
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ if (response[key].ref.startsWith('refs/heads/')) {
+ branch = response[key].ref.substring('refs/heads/'.length);
+ } else {
+ branch = response[key].ref;
+ }
+ branches.push({
+ name: branch,
+ });
+ }
+ return branches;
+ });
+ }
+}
+
+customElements.define(GrConfirmMoveDialog.is, GrConfirmMoveDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.js
new file mode 100644
index 0000000..b8f3336
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.js
@@ -0,0 +1,67 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ width: 30em;
+ }
+ :host([disabled]) {
+ opacity: .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>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html
index 465ce73..25b110ad 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-confirm-move-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-confirm-move-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,52 +30,53 @@
</template>
</test-fixture>
-<script>
- suite('gr-confirm-move-dialog tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-confirm-move-dialog.js';
+suite('gr-confirm-move-dialog tests', () => {
+ let element;
- setup(() => {
- stub('gr-rest-api-interface', {
- getRepoBranches(input) {
- if (input.startsWith('test')) {
- return Promise.resolve([
- {
- ref: 'refs/heads/test-branch',
- revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
- can_delete: true,
- },
- ]);
- } else {
- return Promise.resolve({});
- }
- },
- });
- element = fixture('basic');
- element.project = 'test-project';
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getRepoBranches(input) {
+ if (input.startsWith('test')) {
+ return Promise.resolve([
+ {
+ ref: 'refs/heads/test-branch',
+ revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
+ can_delete: true,
+ },
+ ]);
+ } else {
+ return Promise.resolve({});
+ }
+ },
});
+ element = fixture('basic');
+ element.project = 'test-project';
+ });
- test('with updated commit message', () => {
- element.branch = 'master';
- const myNewMessage = 'updated commit message';
- element.message = myNewMessage;
- flushAsynchronousOperations();
- assert.equal(element.message, myNewMessage);
- });
+ test('with updated commit message', () => {
+ element.branch = 'master';
+ const myNewMessage = 'updated commit message';
+ element.message = myNewMessage;
+ flushAsynchronousOperations();
+ assert.equal(element.message, myNewMessage);
+ });
- test('_getProjectBranchesSuggestions empty', done => {
- element._getProjectBranchesSuggestions('nonexistent').then(branches => {
- assert.equal(branches.length, 0);
- done();
- });
- });
-
- test('_getProjectBranchesSuggestions non-empty', done => {
- element._getProjectBranchesSuggestions('test-branch').then(branches => {
- assert.equal(branches.length, 1);
- assert.equal(branches[0].name, 'test-branch');
- done();
- });
+ test('_getProjectBranchesSuggestions empty', done => {
+ element._getProjectBranchesSuggestions('nonexistent').then(branches => {
+ assert.equal(branches.length, 0);
+ done();
});
});
+
+ test('_getProjectBranchesSuggestions non-empty', done => {
+ element._getProjectBranchesSuggestions('test-branch').then(branches => {
+ assert.equal(branches.length, 1);
+ assert.equal(branches[0].name, 'test-branch');
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
deleted file mode 100644
index cf2721a..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
+++ /dev/null
@@ -1,119 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-confirm-rebase-dialog">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- width: 30em;
- }
- :host([disabled]) {
- opacity: .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"
- on-click="_handleRebaseOnParent">
- <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)]]"
- on-click="_handleRebaseOnTip">
- <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>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-confirm-rebase-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
index 607f587..e451034 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
@@ -14,157 +14,166 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrConfirmRebaseDialog extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-confirm-rebase-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-confirm-rebase-dialog_html.js';
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
+/** @extends Polymer.Element */
+class GrConfirmRebaseDialog extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- static get properties() {
- return {
- branch: String,
- changeNumber: Number,
- hasParent: Boolean,
- rebaseOnCurrent: Boolean,
- _text: String,
- _query: {
- type: Function,
- value() {
- return this._getChangeSuggestions.bind(this);
- },
+ static get is() { return 'gr-confirm-rebase-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
+
+ /**
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
+ */
+
+ static get properties() {
+ return {
+ branch: String,
+ changeNumber: Number,
+ hasParent: Boolean,
+ rebaseOnCurrent: Boolean,
+ _text: String,
+ _query: {
+ type: Function,
+ value() {
+ return this._getChangeSuggestions.bind(this);
},
- _recentChanges: Array,
- };
- }
-
- static get observers() {
- return [
- '_updateSelectedOption(rebaseOnCurrent, hasParent)',
- ];
- }
-
- // This is called by gr-change-actions every time the rebase dialog is
- // re-opened. Unlike other autocompletes that make a request with each
- // updated input, this one gets all recent changes once and then filters
- // them by the input. The query is re-run each time the dialog is opened
- // in case there are new/updated changes in the generic query since the
- // last time it was run.
- fetchRecentChanges() {
- return this.$.restAPI.getChanges(null, `is:open -age:90d`)
- .then(response => {
- const changes = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- changes.push({
- name: `${response[key]._number}: ${response[key].subject}`,
- value: response[key]._number,
- });
- }
- this._recentChanges = changes;
- return this._recentChanges;
- });
- }
-
- _getRecentChanges() {
- if (this._recentChanges) {
- return Promise.resolve(this._recentChanges);
- }
- return this.fetchRecentChanges();
- }
-
- _getChangeSuggestions(input) {
- return this._getRecentChanges().then(changes =>
- this._filterChanges(input, changes));
- }
-
- _filterChanges(input, changes) {
- return changes.filter(change => change.name.includes(input) &&
- change.value !== this.changeNumber);
- }
-
- _displayParentOption(rebaseOnCurrent, hasParent) {
- return hasParent && rebaseOnCurrent;
- }
-
- _displayParentUpToDateMsg(rebaseOnCurrent, hasParent) {
- return hasParent && !rebaseOnCurrent;
- }
-
- _displayTipOption(rebaseOnCurrent, hasParent) {
- return !(!rebaseOnCurrent && !hasParent);
- }
-
- /**
- * There is a subtle but important difference between setting the base to an
- * empty string and omitting it entirely from the payload. An empty string
- * implies that the parent should be cleared and the change should be
- * 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) { return null; }
- if (this.$.rebaseOnTipInput.checked) { return ''; }
- // Change numbers will have their description appended by the
- // autocomplete.
- return this._text.split(':')[0];
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.dispatchEvent(new CustomEvent('confirm',
- {detail: {base: this._getSelectedBase()}}));
- this._text = '';
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.dispatchEvent(new CustomEvent('cancel'));
- this._text = '';
- }
-
- _handleRebaseOnOther() {
- this.$.parentInput.focus();
- }
-
- _handleEnterChangeNumberClick() {
- this.$.rebaseOnOtherInput.checked = true;
- }
-
- /**
- * Sets the default radio button based on the state of the app and
- * the corresponding value to be submitted.
- */
- _updateSelectedOption(rebaseOnCurrent, hasParent) {
- // Polymer 2: check for undefined
- if ([rebaseOnCurrent, hasParent].some(arg => arg === undefined)) {
- return;
- }
-
- if (this._displayParentOption(rebaseOnCurrent, hasParent)) {
- this.$.rebaseOnParentInput.checked = true;
- } else if (this._displayTipOption(rebaseOnCurrent, hasParent)) {
- this.$.rebaseOnTipInput.checked = true;
- } else {
- this.$.rebaseOnOtherInput.checked = true;
- }
- }
+ },
+ _recentChanges: Array,
+ };
}
- customElements.define(GrConfirmRebaseDialog.is, GrConfirmRebaseDialog);
-})();
+ static get observers() {
+ return [
+ '_updateSelectedOption(rebaseOnCurrent, hasParent)',
+ ];
+ }
+
+ // This is called by gr-change-actions every time the rebase dialog is
+ // re-opened. Unlike other autocompletes that make a request with each
+ // updated input, this one gets all recent changes once and then filters
+ // them by the input. The query is re-run each time the dialog is opened
+ // in case there are new/updated changes in the generic query since the
+ // last time it was run.
+ fetchRecentChanges() {
+ return this.$.restAPI.getChanges(null, `is:open -age:90d`)
+ .then(response => {
+ const changes = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ changes.push({
+ name: `${response[key]._number}: ${response[key].subject}`,
+ value: response[key]._number,
+ });
+ }
+ this._recentChanges = changes;
+ return this._recentChanges;
+ });
+ }
+
+ _getRecentChanges() {
+ if (this._recentChanges) {
+ return Promise.resolve(this._recentChanges);
+ }
+ return this.fetchRecentChanges();
+ }
+
+ _getChangeSuggestions(input) {
+ return this._getRecentChanges().then(changes =>
+ this._filterChanges(input, changes));
+ }
+
+ _filterChanges(input, changes) {
+ return changes.filter(change => change.name.includes(input) &&
+ change.value !== this.changeNumber);
+ }
+
+ _displayParentOption(rebaseOnCurrent, hasParent) {
+ return hasParent && rebaseOnCurrent;
+ }
+
+ _displayParentUpToDateMsg(rebaseOnCurrent, hasParent) {
+ return hasParent && !rebaseOnCurrent;
+ }
+
+ _displayTipOption(rebaseOnCurrent, hasParent) {
+ return !(!rebaseOnCurrent && !hasParent);
+ }
+
+ /**
+ * There is a subtle but important difference between setting the base to an
+ * empty string and omitting it entirely from the payload. An empty string
+ * implies that the parent should be cleared and the change should be
+ * 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) { return null; }
+ if (this.$.rebaseOnTipInput.checked) { return ''; }
+ // Change numbers will have their description appended by the
+ // autocomplete.
+ return this._text.split(':')[0];
+ }
+
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(new CustomEvent('confirm',
+ {detail: {base: this._getSelectedBase()}}));
+ this._text = '';
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(new CustomEvent('cancel'));
+ this._text = '';
+ }
+
+ _handleRebaseOnOther() {
+ this.$.parentInput.focus();
+ }
+
+ _handleEnterChangeNumberClick() {
+ this.$.rebaseOnOtherInput.checked = true;
+ }
+
+ /**
+ * Sets the default radio button based on the state of the app and
+ * the corresponding value to be submitted.
+ */
+ _updateSelectedOption(rebaseOnCurrent, hasParent) {
+ // Polymer 2: check for undefined
+ if ([rebaseOnCurrent, hasParent].some(arg => arg === undefined)) {
+ return;
+ }
+
+ if (this._displayParentOption(rebaseOnCurrent, hasParent)) {
+ this.$.rebaseOnParentInput.checked = true;
+ } else if (this._displayTipOption(rebaseOnCurrent, hasParent)) {
+ this.$.rebaseOnTipInput.checked = true;
+ } else {
+ this.$.rebaseOnOtherInput.checked = true;
+ }
+ }
+}
+
+customElements.define(GrConfirmRebaseDialog.is, GrConfirmRebaseDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.js
new file mode 100644
index 0000000..20872bc
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.js
@@ -0,0 +1,86 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ width: 30em;
+ }
+ :host([disabled]) {
+ opacity: .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" on-click="_handleRebaseOnParent">
+ <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)]]" on-click="_handleRebaseOnTip">
+ <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>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
index fe42cac..5cf455f 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-confirm-rebase-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-confirm-rebase-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,167 +30,168 @@
</template>
</test-fixture>
-<script>
- suite('gr-confirm-rebase-dialog tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-confirm-rebase-dialog.js';
+suite('gr-confirm-rebase-dialog tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('controls with parent and rebase on current available', () => {
+ element.rebaseOnCurrent = true;
+ element.hasParent = true;
+ flushAsynchronousOperations();
+ 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'));
+ });
+
+ test('controls with parent rebase on current not available', () => {
+ element.rebaseOnCurrent = false;
+ element.hasParent = true;
+ flushAsynchronousOperations();
+ 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'));
+ });
+
+ test('controls without parent and rebase on current available', () => {
+ element.rebaseOnCurrent = true;
+ element.hasParent = false;
+ flushAsynchronousOperations();
+ 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'));
+ });
+
+ test('controls without parent rebase on current not available', () => {
+ element.rebaseOnCurrent = false;
+ element.hasParent = false;
+ flushAsynchronousOperations();
+ 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'));
+ });
+
+ test('input cleared on cancel or submit', () => {
+ element._text = '123';
+ element.$.confirmDialog.fire('confirm');
+ assert.equal(element._text, '');
+
+ element._text = '123';
+ element.$.confirmDialog.fire('cancel');
+ 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');
+ });
+
+ suite('parent suggestions', () => {
+ let recentChanges;
setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
+ recentChanges = [
+ {
+ name: '123: my first awesome change',
+ value: 123,
+ },
+ {
+ name: '124: my second awesome change',
+ value: 124,
+ },
+ {
+ name: '245: my third awesome change',
+ value: 245,
+ },
+ ];
+
+ sandbox.stub(element.$.restAPI, 'getChanges').returns(Promise.resolve(
+ [
+ {
+ _number: 123,
+ subject: 'my first awesome change',
+ },
+ {
+ _number: 124,
+ subject: 'my second awesome change',
+ },
+ {
+ _number: 245,
+ subject: 'my third awesome change',
+ },
+ ]
+ ));
});
- teardown(() => {
- sandbox.restore();
+ test('_getRecentChanges', () => {
+ sandbox.spy(element, '_getRecentChanges');
+ return element._getRecentChanges()
+ .then(() => {
+ assert.deepEqual(element._recentChanges, recentChanges);
+ assert.equal(element.$.restAPI.getChanges.callCount, 1);
+ // When called a second time, should not re-request recent changes.
+ element._getRecentChanges();
+ })
+ .then(() => {
+ assert.equal(element._getRecentChanges.callCount, 2);
+ assert.equal(element.$.restAPI.getChanges.callCount, 1);
+ });
});
- test('controls with parent and rebase on current available', () => {
- element.rebaseOnCurrent = true;
- element.hasParent = true;
- flushAsynchronousOperations();
- 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'));
+ 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);
+
+ element.changeNumber = 123;
+ 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('controls with parent rebase on current not available', () => {
- element.rebaseOnCurrent = false;
- element.hasParent = true;
- flushAsynchronousOperations();
- 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'));
- });
-
- test('controls without parent and rebase on current available', () => {
- element.rebaseOnCurrent = true;
- element.hasParent = false;
- flushAsynchronousOperations();
- 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'));
- });
-
- test('controls without parent rebase on current not available', () => {
- element.rebaseOnCurrent = false;
- element.hasParent = false;
- flushAsynchronousOperations();
- 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'));
- });
-
- test('input cleared on cancel or submit', () => {
- element._text = '123';
- element.$.confirmDialog.fire('confirm');
- assert.equal(element._text, '');
-
- element._text = '123';
- element.$.confirmDialog.fire('cancel');
- 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');
- });
-
- suite('parent suggestions', () => {
- let recentChanges;
- setup(() => {
- recentChanges = [
- {
- name: '123: my first awesome change',
- value: 123,
- },
- {
- name: '124: my second awesome change',
- value: 124,
- },
- {
- name: '245: my third awesome change',
- value: 245,
- },
- ];
-
- sandbox.stub(element.$.restAPI, 'getChanges').returns(Promise.resolve(
- [
- {
- _number: 123,
- subject: 'my first awesome change',
- },
- {
- _number: 124,
- subject: 'my second awesome change',
- },
- {
- _number: 245,
- subject: 'my third awesome change',
- },
- ]
- ));
- });
-
- test('_getRecentChanges', () => {
- sandbox.spy(element, '_getRecentChanges');
- return element._getRecentChanges()
- .then(() => {
- assert.deepEqual(element._recentChanges, recentChanges);
- assert.equal(element.$.restAPI.getChanges.callCount, 1);
- // When called a second time, should not re-request recent changes.
- element._getRecentChanges();
- })
- .then(() => {
- assert.equal(element._getRecentChanges.callCount, 2);
- assert.equal(element.$.restAPI.getChanges.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);
-
- element.changeNumber = 123;
- 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', () => {
- sandbox.spy(element, '_getRecentChanges');
- element.$.parentInput.noDebounce = true;
- MockInteractions.pressAndReleaseKeyOn(
- element.$.parentInput.$.input,
- 13,
- null,
- 'enter');
- element._text = '1';
- assert.isTrue(element._getRecentChanges.calledOnce);
- element._text = '12';
- assert.isTrue(element._getRecentChanges.calledTwice);
- });
+ test('input text change triggers function', () => {
+ sandbox.spy(element, '_getRecentChanges');
+ element.$.parentInput.noDebounce = true;
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.parentInput.$.input,
+ 13,
+ null,
+ 'enter');
+ element._text = '1';
+ assert.isTrue(element._getRecentChanges.calledOnce);
+ element._text = '12';
+ assert.isTrue(element._getRecentChanges.calledTwice);
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
deleted file mode 100644
index 144cf20..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
+++ /dev/null
@@ -1,109 +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.
--->
-
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-
-<dom-module id="gr-confirm-revert-dialog">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- }
- :host([disabled]) {
- opacity: .5;
- pointer-events: none;
- }
- label {
- cursor: pointer;
- display: block;
- width: 100%;
- }
- .revertSubmissionLayout {
- display: flex;
- }
- .label {
- margin-left: var(--spacing-m);
- margin-bottom: 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);
- }
- </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>
- </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>
- <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
- </template>
- <script src="gr-confirm-revert-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
index 05660bf..9bc0f8d 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -14,184 +14,196 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
- const ERR_COMMIT_NOT_FOUND =
- 'Unable to find the commit hash of this change.';
- const CHANGE_SUBJECT_LIMIT = 50;
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../../styles/shared-styles.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-confirm-revert-dialog_html.js';
- // TODO(dhruvsri): clean up repeated definitions after moving to js modules
- const REVERT_TYPES = {
- REVERT_SINGLE_CHANGE: 1,
- REVERT_SUBMISSION: 2,
- };
+const ERR_COMMIT_NOT_FOUND =
+ 'Unable to find the commit hash of this change.';
+const CHANGE_SUBJECT_LIMIT = 50;
+
+// TODO(dhruvsri): clean up repeated definitions after moving to js modules
+const REVERT_TYPES = {
+ REVERT_SINGLE_CHANGE: 1,
+ REVERT_SUBMISSION: 2,
+};
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrConfirmRevertDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-revert-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
*/
- class GrConfirmRevertDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-confirm-revert-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
- /* The revert message updated by the user
- The default value is set by the dialog */
- _message: String,
- _revertType: {
- type: Number,
- value: REVERT_TYPES.REVERT_SINGLE_CHANGE,
- },
- _showRevertSubmission: {
- type: Boolean,
- value: false,
- },
- _changesCount: Number,
- _showErrorMessage: {
- type: Boolean,
- value: 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 */
- _originalRevertMessages: {
- type: Array,
- value() { return []; },
- },
- // Store the actual messages that the user has edited
- _revertMessages: {
- type: Array,
- value() { return []; },
- },
- };
- }
-
- _computeIfSingleRevert(revertType) {
- return revertType === REVERT_TYPES.REVERT_SINGLE_CHANGE;
- }
-
- _computeIfRevertSubmission(revertType) {
- return revertType === REVERT_TYPES.REVERT_SUBMISSION;
- }
-
- _modifyRevertMsg(change, commitMessage, message) {
- return this.$.jsAPI.modifyRevertMsg(change,
- message, commitMessage);
- }
-
- populate(change, commitMessage, changes) {
- this._changesCount = changes.length;
- // The option to revert a single change is always available
- this._populateRevertSingleChangeMessage(
- change, commitMessage, change.current_revision);
- this._populateRevertSubmissionMessage(change, changes, commitMessage);
- }
-
- _populateRevertSingleChangeMessage(change, commitMessage, commitHash) {
- // Figure out what the revert title should be.
- const originalTitle = (commitMessage || '').split('\n')[0];
- const revertTitle = `Revert "${originalTitle}"`;
- if (!commitHash) {
- this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND});
- return;
- }
- const revertCommitText = `This reverts commit ${commitHash}.`;
-
- this._message = `${revertTitle}\n\n${revertCommitText}\n\n` +
- `Reason for revert: <INSERT REASONING HERE>\n`;
- // This is to give plugins a chance to update message
- this._message = this._modifyRevertMsg(change, commitMessage,
- this._message);
- this._revertType = REVERT_TYPES.REVERT_SINGLE_CHANGE;
- this._showRevertSubmission = false;
- this._revertMessages[this._revertType] = this._message;
- this._originalRevertMessages[this._revertType] = this._message;
- }
-
- _getTrimmedChangeSubject(subject) {
- if (!subject) return '';
- if (subject.length < CHANGE_SUBJECT_LIMIT) return subject;
- return subject.substring(0, CHANGE_SUBJECT_LIMIT) + '...';
- }
-
- _modifyRevertSubmissionMsg(change, msg, commitMessage) {
- return this.$.jsAPI.modifyRevertSubmissionMsg(change, msg,
- commitMessage);
- }
-
- _populateRevertSubmissionMessage(change, changes, commitMessage) {
- // Follow the same convention of the revert
- const commitHash = change.current_revision;
- if (!commitHash) {
- this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND});
- return;
- }
- if (!changes || changes.length <= 1) return;
- const submissionId = change.submission_id;
- const revertTitle = 'Revert submission ' + submissionId;
- this._message = revertTitle + '\n\n' + 'Reason for revert: <INSERT ' +
- 'REASONING HERE>\n';
- this._message += 'Reverted Changes:\n';
- changes.forEach(change => {
- this._message += change.change_id.substring(0, 10) + ':'
- + this._getTrimmedChangeSubject(change.subject) + '\n';
- });
- this._message = this._modifyRevertSubmissionMsg(change, this._message,
- commitMessage);
- this._revertType = REVERT_TYPES.REVERT_SUBMISSION;
- this._revertMessages[this._revertType] = this._message;
- this._originalRevertMessages[this._revertType] = this._message;
- this._showRevertSubmission = true;
- }
-
- _handleRevertSingleChangeClicked() {
- this._showErrorMessage = false;
- this._revertMessages[REVERT_TYPES.REVERT_SUBMISSION] = this._message;
- this._message = this._revertMessages[REVERT_TYPES.REVERT_SINGLE_CHANGE];
- this._revertType = REVERT_TYPES.REVERT_SINGLE_CHANGE;
- }
-
- _handleRevertSubmissionClicked() {
- this._showErrorMessage = false;
- this._revertType = REVERT_TYPES.REVERT_SUBMISSION;
- this._revertMessages[REVERT_TYPES.REVERT_SINGLE_CHANGE] = this._message;
- this._message = this._revertMessages[REVERT_TYPES.REVERT_SUBMISSION];
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- if (this._message === this._originalRevertMessages[this._revertType]) {
- this._showErrorMessage = true;
- return;
- }
- this.fire('confirm', {revertType: this._revertType,
- message: this._message}, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', {revertType: this._revertType},
- {bubbles: false});
- }
+ static get properties() {
+ return {
+ /* The revert message updated by the user
+ The default value is set by the dialog */
+ _message: String,
+ _revertType: {
+ type: Number,
+ value: REVERT_TYPES.REVERT_SINGLE_CHANGE,
+ },
+ _showRevertSubmission: {
+ type: Boolean,
+ value: false,
+ },
+ _changesCount: Number,
+ _showErrorMessage: {
+ type: Boolean,
+ value: 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 */
+ _originalRevertMessages: {
+ type: Array,
+ value() { return []; },
+ },
+ // Store the actual messages that the user has edited
+ _revertMessages: {
+ type: Array,
+ value() { return []; },
+ },
+ };
}
- customElements.define(GrConfirmRevertDialog.is, GrConfirmRevertDialog);
-})();
+ _computeIfSingleRevert(revertType) {
+ return revertType === REVERT_TYPES.REVERT_SINGLE_CHANGE;
+ }
+
+ _computeIfRevertSubmission(revertType) {
+ return revertType === REVERT_TYPES.REVERT_SUBMISSION;
+ }
+
+ _modifyRevertMsg(change, commitMessage, message) {
+ return this.$.jsAPI.modifyRevertMsg(change,
+ message, commitMessage);
+ }
+
+ populate(change, commitMessage, changes) {
+ this._changesCount = changes.length;
+ // The option to revert a single change is always available
+ this._populateRevertSingleChangeMessage(
+ change, commitMessage, change.current_revision);
+ this._populateRevertSubmissionMessage(change, changes, commitMessage);
+ }
+
+ _populateRevertSingleChangeMessage(change, commitMessage, commitHash) {
+ // Figure out what the revert title should be.
+ const originalTitle = (commitMessage || '').split('\n')[0];
+ const revertTitle = `Revert "${originalTitle}"`;
+ if (!commitHash) {
+ this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND});
+ return;
+ }
+ const revertCommitText = `This reverts commit ${commitHash}.`;
+
+ this._message = `${revertTitle}\n\n${revertCommitText}\n\n` +
+ `Reason for revert: <INSERT REASONING HERE>\n`;
+ // This is to give plugins a chance to update message
+ this._message = this._modifyRevertMsg(change, commitMessage,
+ this._message);
+ this._revertType = REVERT_TYPES.REVERT_SINGLE_CHANGE;
+ this._showRevertSubmission = false;
+ this._revertMessages[this._revertType] = this._message;
+ this._originalRevertMessages[this._revertType] = this._message;
+ }
+
+ _getTrimmedChangeSubject(subject) {
+ if (!subject) return '';
+ if (subject.length < CHANGE_SUBJECT_LIMIT) return subject;
+ return subject.substring(0, CHANGE_SUBJECT_LIMIT) + '...';
+ }
+
+ _modifyRevertSubmissionMsg(change, msg, commitMessage) {
+ return this.$.jsAPI.modifyRevertSubmissionMsg(change, msg,
+ commitMessage);
+ }
+
+ _populateRevertSubmissionMessage(change, changes, commitMessage) {
+ // Follow the same convention of the revert
+ const commitHash = change.current_revision;
+ if (!commitHash) {
+ this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND});
+ return;
+ }
+ if (!changes || changes.length <= 1) return;
+ const submissionId = change.submission_id;
+ const revertTitle = 'Revert submission ' + submissionId;
+ this._message = revertTitle + '\n\n' + 'Reason for revert: <INSERT ' +
+ 'REASONING HERE>\n';
+ this._message += 'Reverted Changes:\n';
+ changes.forEach(change => {
+ this._message += change.change_id.substring(0, 10) + ':'
+ + this._getTrimmedChangeSubject(change.subject) + '\n';
+ });
+ this._message = this._modifyRevertSubmissionMsg(change, this._message,
+ commitMessage);
+ this._revertType = REVERT_TYPES.REVERT_SUBMISSION;
+ this._revertMessages[this._revertType] = this._message;
+ this._originalRevertMessages[this._revertType] = this._message;
+ this._showRevertSubmission = true;
+ }
+
+ _handleRevertSingleChangeClicked() {
+ this._showErrorMessage = false;
+ this._revertMessages[REVERT_TYPES.REVERT_SUBMISSION] = this._message;
+ this._message = this._revertMessages[REVERT_TYPES.REVERT_SINGLE_CHANGE];
+ this._revertType = REVERT_TYPES.REVERT_SINGLE_CHANGE;
+ }
+
+ _handleRevertSubmissionClicked() {
+ this._showErrorMessage = false;
+ this._revertType = REVERT_TYPES.REVERT_SUBMISSION;
+ this._revertMessages[REVERT_TYPES.REVERT_SINGLE_CHANGE] = this._message;
+ this._message = this._revertMessages[REVERT_TYPES.REVERT_SUBMISSION];
+ }
+
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (this._message === this._originalRevertMessages[this._revertType]) {
+ this._showErrorMessage = true;
+ return;
+ }
+ this.fire('confirm', {revertType: this._revertType,
+ message: this._message}, {bubbles: false});
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', {revertType: this._revertType},
+ {bubbles: false});
+ }
+}
+
+customElements.define(GrConfirmRevertDialog.is, GrConfirmRevertDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.js
new file mode 100644
index 0000000..3f293cf
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.js
@@ -0,0 +1,81 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ }
+ :host([disabled]) {
+ opacity: .5;
+ pointer-events: none;
+ }
+ label {
+ cursor: pointer;
+ display: block;
+ width: 100%;
+ }
+ .revertSubmissionLayout {
+ display: flex;
+ }
+ .label {
+ margin-left: var(--spacing-m);
+ margin-bottom: 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);
+ }
+ </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>
+ <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
index 29886dd..fd984ec 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-confirm-revert-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-confirm-revert-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,70 +30,71 @@
</template>
</test-fixture>
-<script>
- suite('gr-confirm-revert-dialog tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-confirm-revert-dialog.js';
+suite('gr-confirm-revert-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox =sinon.sandbox.create();
- });
-
- teardown(() => sandbox.restore());
-
- test('no match', () => {
- assert.isNotOk(element._message);
- const alertStub = sandbox.stub();
- element.addEventListener('show-alert', alertStub);
- element._populateRevertSingleChangeMessage({},
- 'not a commitHash in sight', undefined);
- assert.isTrue(alertStub.calledOnce);
- });
-
- test('single line', () => {
- assert.isNotOk(element._message);
- element._populateRevertSingleChangeMessage({},
- 'one line commit\n\nChange-Id: abcdefg\n',
- 'abcd123');
- const expected = 'Revert "one line commit"\n\n' +
- 'This reverts commit abcd123.\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element._message, expected);
- });
-
- test('multi line', () => {
- assert.isNotOk(element._message);
- element._populateRevertSingleChangeMessage({},
- 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n',
- 'abcd123');
- const expected = 'Revert "many lines"\n\n' +
- 'This reverts commit abcd123.\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element._message, expected);
- });
-
- test('issue above change id', () => {
- assert.isNotOk(element._message);
- element._populateRevertSingleChangeMessage({},
- 'much lines\nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n',
- 'abcd123');
- const expected = 'Revert "much lines"\n\n' +
- 'This reverts commit abcd123.\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element._message, expected);
- });
-
- test('revert a revert', () => {
- assert.isNotOk(element._message);
- element._populateRevertSingleChangeMessage({},
- 'Revert "one line commit"\n\nChange-Id: abcdefg\n',
- 'abcd123');
- const expected = '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);
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox =sinon.sandbox.create();
});
+
+ teardown(() => sandbox.restore());
+
+ test('no match', () => {
+ assert.isNotOk(element._message);
+ const alertStub = sandbox.stub();
+ element.addEventListener('show-alert', alertStub);
+ element._populateRevertSingleChangeMessage({},
+ 'not a commitHash in sight', undefined);
+ assert.isTrue(alertStub.calledOnce);
+ });
+
+ test('single line', () => {
+ assert.isNotOk(element._message);
+ element._populateRevertSingleChangeMessage({},
+ 'one line commit\n\nChange-Id: abcdefg\n',
+ 'abcd123');
+ const expected = 'Revert "one line commit"\n\n' +
+ 'This reverts commit abcd123.\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ assert.equal(element._message, expected);
+ });
+
+ test('multi line', () => {
+ assert.isNotOk(element._message);
+ element._populateRevertSingleChangeMessage({},
+ 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n',
+ 'abcd123');
+ const expected = 'Revert "many lines"\n\n' +
+ 'This reverts commit abcd123.\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ assert.equal(element._message, expected);
+ });
+
+ test('issue above change id', () => {
+ assert.isNotOk(element._message);
+ element._populateRevertSingleChangeMessage({},
+ 'much lines\nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n',
+ 'abcd123');
+ const expected = 'Revert "much lines"\n\n' +
+ 'This reverts commit abcd123.\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ assert.equal(element._message, expected);
+ });
+
+ test('revert a revert', () => {
+ assert.isNotOk(element._message);
+ element._populateRevertSingleChangeMessage({},
+ 'Revert "one line commit"\n\nChange-Id: abcdefg\n',
+ 'abcd123');
+ const expected = '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);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.html
deleted file mode 100644
index f2cfef8..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.html
+++ /dev/null
@@ -1,68 +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.
--->
-
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-
-<dom-module id="gr-confirm-revert-submission-dialog">
- <template>
- <!-- TODO(taoalpha): move all shared styles to a style module. -->
- <style include="shared-styles">
- :host {
- display: block;
- }
- :host([disabled]) {
- opacity: .5;
- pointer-events: none;
- }
- 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="Revert Submission"
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap">
- <div class="header" slot="header">Revert Submission</div>
- <div class="main" slot="main">
- <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>
- </div>
- </gr-dialog>
- <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
- </template>
- <script src="gr-confirm-revert-submission-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js
index ae8dfa5..3cae44a 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js
@@ -14,87 +14,98 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
- const ERR_COMMIT_NOT_FOUND =
- 'Unable to find the commit hash of this change.';
- const CHANGE_SUBJECT_LIMIT = 50;
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-confirm-revert-submission-dialog_html.js';
+
+const ERR_COMMIT_NOT_FOUND =
+ 'Unable to find the commit hash of this change.';
+const CHANGE_SUBJECT_LIMIT = 50;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrConfirmRevertSubmissionDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-revert-submission-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
*/
- class GrConfirmRevertSubmissionDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-confirm-revert-submission-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
- message: String,
- commitMessage: String,
- };
- }
-
- _getTrimmedChangeSubject(subject) {
- if (!subject) return '';
- if (subject.length < CHANGE_SUBJECT_LIMIT) return subject;
- return subject.substring(0, CHANGE_SUBJECT_LIMIT) + '...';
- }
-
- _modifyRevertSubmissionMsg(change) {
- return this.$.jsAPI.modifyRevertSubmissionMsg(change,
- this.message, this.commitMessage);
- }
-
- _populateRevertSubmissionMessage(message, change, changes) {
- // Follow the same convention of the revert
- const commitHash = change.current_revision;
- if (!commitHash) {
- this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND});
- return;
- }
- const submissionId = change.submission_id;
- const revertTitle = 'Revert submission ' + submissionId;
- this.changes = changes;
- this.message = revertTitle + '\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n';
- this.message += 'Reverted Changes:\n';
- changes = changes || [];
- changes.forEach(change => {
- this.message += change.change_id.substring(0, 10) + ': ' +
- this._getTrimmedChangeSubject(change.subject) + '\n';
- });
- this.message = this._modifyRevertSubmissionMsg(change);
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('confirm', null, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', null, {bubbles: false});
- }
+ static get properties() {
+ return {
+ message: String,
+ commitMessage: String,
+ };
}
- customElements.define(GrConfirmRevertSubmissionDialog.is,
- GrConfirmRevertSubmissionDialog);
-})();
+ _getTrimmedChangeSubject(subject) {
+ if (!subject) return '';
+ if (subject.length < CHANGE_SUBJECT_LIMIT) return subject;
+ return subject.substring(0, CHANGE_SUBJECT_LIMIT) + '...';
+ }
+
+ _modifyRevertSubmissionMsg(change) {
+ return this.$.jsAPI.modifyRevertSubmissionMsg(change,
+ this.message, this.commitMessage);
+ }
+
+ _populateRevertSubmissionMessage(message, change, changes) {
+ // Follow the same convention of the revert
+ const commitHash = change.current_revision;
+ if (!commitHash) {
+ this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND});
+ return;
+ }
+ const submissionId = change.submission_id;
+ const revertTitle = 'Revert submission ' + submissionId;
+ this.changes = changes;
+ this.message = revertTitle + '\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ this.message += 'Reverted Changes:\n';
+ changes = changes || [];
+ changes.forEach(change => {
+ this.message += change.change_id.substring(0, 10) + ': ' +
+ this._getTrimmedChangeSubject(change.subject) + '\n';
+ });
+ this.message = this._modifyRevertSubmissionMsg(change);
+ }
+
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('confirm', null, {bubbles: false});
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', null, {bubbles: false});
+ }
+}
+
+customElements.define(GrConfirmRevertSubmissionDialog.is,
+ GrConfirmRevertSubmissionDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_html.js
new file mode 100644
index 0000000..a68920c
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_html.js
@@ -0,0 +1,51 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <!-- TODO(taoalpha): move all shared styles to a style module. -->
+ <style include="shared-styles">
+ :host {
+ display: block;
+ }
+ :host([disabled]) {
+ opacity: .5;
+ pointer-events: none;
+ }
+ 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="Revert Submission" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap">
+ <div class="header" slot="header">Revert Submission</div>
+ <div class="main" slot="main">
+ <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>
+ </div>
+ </gr-dialog>
+ <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html
index 2513986..cc35d72 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-confirm-revert-submission-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-confirm-revert-submission-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,67 +31,68 @@
</template>
</test-fixture>
-<script>
- suite('gr-confirm-revert-submission-dialog tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-confirm-revert-submission-dialog.js';
+suite('gr-confirm-revert-submission-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox =sinon.sandbox.create();
- });
-
- teardown(() => sandbox.restore());
-
- test('no match', () => {
- assert.isNotOk(element.message);
- const alertStub = sandbox.stub();
- element.addEventListener('show-alert', alertStub);
- element._populateRevertSubmissionMessage(
- 'not a commitHash in sight'
- );
- assert.isTrue(alertStub.calledOnce);
- });
-
- test('single line', () => {
- assert.isNotOk(element.message);
- element._populateRevertSubmissionMessage(
- 'one line commit\n\nChange-Id: abcdefg\n',
- 'abcd123');
- const expected = 'Revert submission\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element.message, expected);
- });
-
- test('multi line', () => {
- assert.isNotOk(element.message);
- element._populateRevertSubmissionMessage(
- 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n',
- 'abcd123');
- const expected = 'Revert submission\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element.message, expected);
- });
-
- test('issue above change id', () => {
- assert.isNotOk(element.message);
- element._populateRevertSubmissionMessage(
- 'test \nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n',
- 'abcd123');
- const expected = 'Revert submission\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element.message, expected);
- });
-
- test('revert a revert', () => {
- assert.isNotOk(element.message);
- element._populateRevertSubmissionMessage(
- 'Revert "one line commit"\n\nChange-Id: abcdefg\n',
- 'abcd123');
- const expected = 'Revert submission\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element.message, expected);
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox =sinon.sandbox.create();
});
+
+ teardown(() => sandbox.restore());
+
+ test('no match', () => {
+ assert.isNotOk(element.message);
+ const alertStub = sandbox.stub();
+ element.addEventListener('show-alert', alertStub);
+ element._populateRevertSubmissionMessage(
+ 'not a commitHash in sight'
+ );
+ assert.isTrue(alertStub.calledOnce);
+ });
+
+ test('single line', () => {
+ assert.isNotOk(element.message);
+ element._populateRevertSubmissionMessage(
+ 'one line commit\n\nChange-Id: abcdefg\n',
+ 'abcd123');
+ const expected = 'Revert submission\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ assert.equal(element.message, expected);
+ });
+
+ test('multi line', () => {
+ assert.isNotOk(element.message);
+ element._populateRevertSubmissionMessage(
+ 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n',
+ 'abcd123');
+ const expected = 'Revert submission\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ assert.equal(element.message, expected);
+ });
+
+ test('issue above change id', () => {
+ assert.isNotOk(element.message);
+ element._populateRevertSubmissionMessage(
+ 'test \nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n',
+ 'abcd123');
+ const expected = 'Revert submission\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ assert.equal(element.message, expected);
+ });
+
+ test('revert a revert', () => {
+ assert.isNotOk(element.message);
+ element._populateRevertSubmissionMessage(
+ 'Revert "one line commit"\n\nChange-Id: abcdefg\n',
+ 'abcd123');
+ const expected = 'Revert submission\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ assert.equal(element.message, expected);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html
deleted file mode 100644
index a845ed4..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html
+++ /dev/null
@@ -1,84 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
-
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-confirm-submit-dialog">
- <template>
- <style include="shared-styles">
- #dialog {
- min-width: 40em;
- }
- p {
- margin-bottom: var(--spacing-l);
- }
- .warningBeforeSubmit {
- color: var(--error-text-color);
- vertical-align: top;
- margin-right: var(--spacing-s);
- }
- @media screen and (max-width: 50em) {
- #dialog {
- min-width: inherit;
- width: 100%;
- }
- }
- </style>
- <gr-dialog
- id="dialog"
- confirm-label="Continue"
- confirm-on-enter
- on-cancel="_handleCancelTap"
- on-confirm="_handleConfirmTap">
- <div class="header" slot="header">
- [[action.label]]
- </div>
- <div class="main" slot="main">
- <gr-endpoint-decorator name="confirm-submit-change">
- <p>Ready to submit “<strong>[[change.subject]]</strong>”?</p>
- <template is="dom-if" if="[[change.is_private]]">
- <p>
- <iron-icon icon="gr-icons:error" class="warningBeforeSubmit"></iron-icon>
- <strong>Heads Up!</strong>
- Submitting this private change will also make it public.
- </p>
- </template>
- <template is="dom-if" if="[[change.unresolved_comment_count]]">
- <p>
- <iron-icon icon="gr-icons:error" class="warningBeforeSubmit"></iron-icon>
- [[_computeUnresolvedCommentsWarning(change)]]
- </p>
- </template>
- <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
- <gr-endpoint-param name="action" value="[[action]]"></gr-endpoint-param>
- </gr-endpoint-decorator>
- </div>
- </gr-dialog>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-confirm-submit-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
index aa26681..037d53d 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
@@ -14,67 +14,80 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrConfirmSubmitDialog extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-confirm-submit-dialog'; }
+import '@polymer/iron-icon/iron-icon.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
+import '../../../styles/shared-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-confirm-submit-dialog_html.js';
+
+/** @extends Polymer.Element */
+class GrConfirmSubmitDialog extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-submit-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
+
+ /**
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
+ */
+
+ static get properties() {
+ return {
/**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
+ * @type {{
+ * is_private: boolean,
+ * subject: string,
+ * }}
*/
+ change: Object,
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
/**
* @type {{
- * is_private: boolean,
- * subject: string,
+ * label: string,
* }}
*/
- change: Object,
-
- /**
- * @type {{
- * label: string,
- * }}
- */
- action: Object,
- };
- }
-
- resetFocus(e) {
- this.$.dialog.resetFocus();
- }
-
- _computeUnresolvedCommentsWarning(change) {
- const unresolvedCount = change.unresolved_comment_count;
- const plural = unresolvedCount > 1 ? 's' : '';
- return `Heads Up! ${unresolvedCount} unresolved comment${plural}.`;
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.dispatchEvent(new CustomEvent('confirm', {bubbles: false}));
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.dispatchEvent(new CustomEvent('cancel', {bubbles: false}));
- }
+ action: Object,
+ };
}
- customElements.define(GrConfirmSubmitDialog.is, GrConfirmSubmitDialog);
-})();
+ resetFocus(e) {
+ this.$.dialog.resetFocus();
+ }
+
+ _computeUnresolvedCommentsWarning(change) {
+ const unresolvedCount = change.unresolved_comment_count;
+ const plural = unresolvedCount > 1 ? 's' : '';
+ return `Heads Up! ${unresolvedCount} unresolved comment${plural}.`;
+ }
+
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(new CustomEvent('confirm', {bubbles: false}));
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(new CustomEvent('cancel', {bubbles: false}));
+ }
+}
+
+customElements.define(GrConfirmSubmitDialog.is, GrConfirmSubmitDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.js
new file mode 100644
index 0000000..03e0f17
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.js
@@ -0,0 +1,65 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ #dialog {
+ min-width: 40em;
+ }
+ p {
+ margin-bottom: var(--spacing-l);
+ }
+ .warningBeforeSubmit {
+ color: var(--error-text-color);
+ vertical-align: top;
+ margin-right: var(--spacing-s);
+ }
+ @media screen and (max-width: 50em) {
+ #dialog {
+ min-width: inherit;
+ width: 100%;
+ }
+ }
+ </style>
+ <gr-dialog id="dialog" confirm-label="Continue" confirm-on-enter="" on-cancel="_handleCancelTap" on-confirm="_handleConfirmTap">
+ <div class="header" slot="header">
+ [[action.label]]
+ </div>
+ <div class="main" slot="main">
+ <gr-endpoint-decorator name="confirm-submit-change">
+ <p>Ready to submit “<strong>[[change.subject]]</strong>”?</p>
+ <template is="dom-if" if="[[change.is_private]]">
+ <p>
+ <iron-icon icon="gr-icons:error" class="warningBeforeSubmit"></iron-icon>
+ <strong>Heads Up!</strong>
+ Submitting this private change will also make it public.
+ </p>
+ </template>
+ <template is="dom-if" if="[[change.unresolved_comment_count]]">
+ <p>
+ <iron-icon icon="gr-icons:error" class="warningBeforeSubmit"></iron-icon>
+ [[_computeUnresolvedCommentsWarning(change)]]
+ </p>
+ </template>
+ <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
+ <gr-endpoint-param name="action" value="[[action]]"></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </div>
+ </gr-dialog>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
index a5dffa8..dfe1e6e 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
@@ -19,17 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-confirm-submit-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="/bower_components/page/page.js"></script>
-
-<link rel="import" href="gr-confirm-submit-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script src="/node_modules/page/page.js"></script>
<test-fixture id="basic">
<template>
@@ -37,43 +31,44 @@
</template>
</test-fixture>
-<script>
- suite('gr-file-list-header tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-confirm-submit-dialog.js';
+suite('gr-file-list-header tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('display', () => {
- element.action = {label: 'my-label'};
- element.change = {subject: 'my-subject'};
- flushAsynchronousOperations();
- const header = element.shadowRoot
- .querySelector('.header');
- assert.equal(header.textContent.trim(), 'my-label');
-
- const message = element.shadowRoot
- .querySelector('.main p');
- assert.notEqual(message.textContent.length, 0);
- assert.notEqual(message.textContent.indexOf('my-subject'), -1);
- });
-
- test('_computeUnresolvedCommentsWarning', () => {
- const change = {unresolved_comment_count: 1};
- assert.equal(element._computeUnresolvedCommentsWarning(change),
- 'Heads Up! 1 unresolved comment.');
-
- const change2 = {unresolved_comment_count: 2};
- assert.equal(element._computeUnresolvedCommentsWarning(change2),
- 'Heads Up! 2 unresolved comments.');
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('display', () => {
+ element.action = {label: 'my-label'};
+ element.change = {subject: 'my-subject'};
+ flushAsynchronousOperations();
+ const header = element.shadowRoot
+ .querySelector('.header');
+ assert.equal(header.textContent.trim(), 'my-label');
+
+ const message = element.shadowRoot
+ .querySelector('.main p');
+ assert.notEqual(message.textContent.length, 0);
+ assert.notEqual(message.textContent.indexOf('my-subject'), -1);
+ });
+
+ test('_computeUnresolvedCommentsWarning', () => {
+ const change = {unresolved_comment_count: 1};
+ assert.equal(element._computeUnresolvedCommentsWarning(change),
+ 'Heads Up! 1 unresolved comment.');
+
+ const change2 = {unresolved_comment_count: 2};
+ assert.equal(element._computeUnresolvedCommentsWarning(change2),
+ 'Heads Up! 2 unresolved comments.');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
deleted file mode 100644
index 4ddc876..0000000
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
+++ /dev/null
@@ -1,126 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-download-commands/gr-download-commands.html">
-
-<dom-module id="gr-download-dialog">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- padding: var(--spacing-m) 0;
- }
- section {
- display: flex;
- padding: var(--spacing-m) var(--spacing-xl);
- }
- .flexContainer {
- display: flex;
- justify-content: space-between;
- padding-top: var(--spacing-m);
- }
- .footer {
- justify-content: flex-end;
- }
- .closeButtonContainer {
- align-items: flex-end;
- display: flex;
- flex: 0;
- justify-content: flex-end;
- }
- .patchFiles,
- .archivesContainer {
- padding-bottom: var(--spacing-m);
- }
- .patchFiles {
- margin-right: var(--spacing-xxl);
- }
- .patchFiles a,
- .archives a {
- display: inline-block;
- margin-right: var(--spacing-l);
- }
- .patchFiles a:last-of-type,
- .archives a:last-of-type {
- margin-right: 0;
- }
- .title {
- flex: 1;
- font-weight: var(--font-weight-bold);
- }
- .hidden {
- display: none;
- }
- </style>
- <section>
- <h3 class="title">
- Patch set [[patchNum]] of [[_computePatchSetQuantity(change.revisions)]]
- </h3>
- </section>
- <section class$="[[_computeShowDownloadCommands(_schemes)]]">
- <gr-download-commands
- id="downloadCommands"
- commands="[[_computeDownloadCommands(change, patchNum, _selectedScheme)]]"
- schemes="[[_schemes]]"
- selected-scheme="{{_selectedScheme}}"></gr-download-commands>
- </section>
- <section class="flexContainer">
- <div class="patchFiles" hidden="[[_computeHidePatchFile(change, patchNum)]]" hidden>
- <label>Patch file</label>
- <div>
- <a
- id="download"
- href$="[[_computeDownloadLink(change, patchNum)]]"
- download>
- [[_computeDownloadFilename(change, patchNum)]]
- </a>
- <a
- href$="[[_computeZipDownloadLink(change, patchNum)]]"
- download>
- [[_computeZipDownloadFilename(change, patchNum)]]
- </a>
- </div>
- </div>
- <div class="archivesContainer" hidden$="[[!config.archives.length]]" hidden>
- <label>Archive</label>
- <div id="archives" class="archives">
- <template is="dom-repeat" items="[[config.archives]]" as="format">
- <a
- href$="[[_computeArchiveDownloadLink(change, patchNum, format)]]"
- download>
- [[format]]
- </a>
- </template>
- </div>
- </div>
- </section>
- <section class="footer">
- <span class="closeButtonContainer">
- <gr-button id="closeButton"
- link
- on-click="_handleCloseTap">Close</gr-button>
- </span>
- </section>
- </template>
- <script src="gr-download-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
index 17c6f50..1b6e521 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
@@ -14,214 +14,225 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-download-commands/gr-download-commands.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-download-dialog_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrDownloadDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-download-dialog'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * Fired when the user presses the close button.
+ *
+ * @event close
*/
- class GrDownloadDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-download-dialog'; }
- /**
- * Fired when the user presses the close button.
- *
- * @event close
- */
- static get properties() {
- return {
- /** @type {{ revisions: Array }} */
- change: Object,
- patchNum: String,
- /** @type {?} */
- config: Object,
+ static get properties() {
+ return {
+ /** @type {{ revisions: Array }} */
+ change: Object,
+ patchNum: String,
+ /** @type {?} */
+ config: Object,
- _schemes: {
- type: Array,
- value() { return []; },
- computed: '_computeSchemes(change, patchNum)',
- observer: '_schemesChanged',
- },
- _selectedScheme: String,
- };
- }
+ _schemes: {
+ type: Array,
+ value() { return []; },
+ computed: '_computeSchemes(change, patchNum)',
+ observer: '_schemesChanged',
+ },
+ _selectedScheme: String,
+ };
+ }
- /** @override */
- ready() {
- super.ready();
- this._ensureAttribute('role', 'dialog');
- }
+ /** @override */
+ ready() {
+ super.ready();
+ this._ensureAttribute('role', 'dialog');
+ }
- focus() {
- if (this._schemes.length) {
- this.$.downloadCommands.focusOnCopy();
- } else {
- this.$.download.focus();
- }
- }
-
- getFocusStops() {
- const links = this.shadowRoot
- .querySelector('#archives').querySelectorAll('a');
- return {
- start: this.$.closeButton,
- end: links[links.length - 1],
- };
- }
-
- _computeDownloadCommands(change, patchNum, _selectedScheme) {
- let commandObj;
- if (!change) return [];
- for (const rev of Object.values(change.revisions || {})) {
- if (this.patchNumEquals(rev._number, patchNum) &&
- rev && rev.fetch && rev.fetch.hasOwnProperty(_selectedScheme)) {
- commandObj = rev.fetch[_selectedScheme].commands;
- break;
- }
- }
- const commands = [];
- for (const title in commandObj) {
- if (!commandObj || !commandObj.hasOwnProperty(title)) { continue; }
- commands.push({
- title,
- command: commandObj[title],
- });
- }
- return commands;
- }
-
- /**
- * @param {!Object} change
- * @param {number|string} patchNum
- *
- * @return {string}
- */
- _computeZipDownloadLink(change, patchNum) {
- return this._computeDownloadLink(change, patchNum, true);
- }
-
- /**
- * @param {!Object} change
- * @param {number|string} patchNum
- *
- * @return {string}
- */
- _computeZipDownloadFilename(change, patchNum) {
- return this._computeDownloadFilename(change, patchNum, true);
- }
-
- /**
- * @param {!Object} change
- * @param {number|string} patchNum
- * @param {boolean=} opt_zip
- *
- * @return {string} Not sure why there was a mismatch
- */
- _computeDownloadLink(change, patchNum, opt_zip) {
- // Polymer 2: check for undefined
- if ([change, patchNum].some(arg => arg === undefined)) {
- return '';
- }
- return this.changeBaseURL(change.project, change._number, patchNum) +
- '/patch?' + (opt_zip ? 'zip' : 'download');
- }
-
- /**
- * @param {!Object} change
- * @param {number|string} patchNum
- * @param {boolean=} opt_zip
- *
- * @return {string}
- */
- _computeDownloadFilename(change, patchNum, opt_zip) {
- // Polymer 2: check for undefined
- if ([change, patchNum].some(arg => arg === undefined)) {
- return '';
- }
-
- let shortRev = '';
- for (const rev in change.revisions) {
- if (this.patchNumEquals(change.revisions[rev]._number, patchNum)) {
- shortRev = rev.substr(0, 7);
- break;
- }
- }
- return shortRev + '.diff.' + (opt_zip ? 'zip' : 'base64');
- }
-
- _computeHidePatchFile(change, patchNum) {
- // Polymer 2: check for undefined
- if ([change, patchNum].some(arg => arg === undefined)) {
- return false;
- }
- for (const rev of Object.values(change.revisions || {})) {
- if (this.patchNumEquals(rev._number, patchNum)) {
- const parentLength = rev.commit && rev.commit.parents ?
- rev.commit.parents.length : 0;
- return parentLength == 0;
- }
- }
- return false;
- }
-
- _computeArchiveDownloadLink(change, patchNum, format) {
- // Polymer 2: check for undefined
- if ([change, patchNum, format].some(arg => arg === undefined)) {
- return '';
- }
- return this.changeBaseURL(change.project, change._number, patchNum) +
- '/archive?format=' + format;
- }
-
- _computeSchemes(change, patchNum) {
- // Polymer 2: check for undefined
- if ([change, patchNum].some(arg => arg === undefined)) {
- return [];
- }
-
- for (const rev of Object.values(change.revisions || {})) {
- if (this.patchNumEquals(rev._number, patchNum)) {
- const fetch = rev.fetch;
- if (fetch) {
- return Object.keys(fetch).sort();
- }
- break;
- }
- }
- return [];
- }
-
- _computePatchSetQuantity(revisions) {
- if (!revisions) { return 0; }
- return Object.keys(revisions).length;
- }
-
- _handleCloseTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('close', null, {bubbles: false});
- }
-
- _schemesChanged(schemes) {
- if (schemes.length === 0) { return; }
- if (!schemes.includes(this._selectedScheme)) {
- this._selectedScheme = schemes.sort()[0];
- }
- }
-
- _computeShowDownloadCommands(schemes) {
- return schemes.length ? '' : 'hidden';
+ focus() {
+ if (this._schemes.length) {
+ this.$.downloadCommands.focusOnCopy();
+ } else {
+ this.$.download.focus();
}
}
- customElements.define(GrDownloadDialog.is, GrDownloadDialog);
-})();
+ getFocusStops() {
+ const links = this.shadowRoot
+ .querySelector('#archives').querySelectorAll('a');
+ return {
+ start: this.$.closeButton,
+ end: links[links.length - 1],
+ };
+ }
+
+ _computeDownloadCommands(change, patchNum, _selectedScheme) {
+ let commandObj;
+ if (!change) return [];
+ for (const rev of Object.values(change.revisions || {})) {
+ if (this.patchNumEquals(rev._number, patchNum) &&
+ rev && rev.fetch && rev.fetch.hasOwnProperty(_selectedScheme)) {
+ commandObj = rev.fetch[_selectedScheme].commands;
+ break;
+ }
+ }
+ const commands = [];
+ for (const title in commandObj) {
+ if (!commandObj || !commandObj.hasOwnProperty(title)) { continue; }
+ commands.push({
+ title,
+ command: commandObj[title],
+ });
+ }
+ return commands;
+ }
+
+ /**
+ * @param {!Object} change
+ * @param {number|string} patchNum
+ *
+ * @return {string}
+ */
+ _computeZipDownloadLink(change, patchNum) {
+ return this._computeDownloadLink(change, patchNum, true);
+ }
+
+ /**
+ * @param {!Object} change
+ * @param {number|string} patchNum
+ *
+ * @return {string}
+ */
+ _computeZipDownloadFilename(change, patchNum) {
+ return this._computeDownloadFilename(change, patchNum, true);
+ }
+
+ /**
+ * @param {!Object} change
+ * @param {number|string} patchNum
+ * @param {boolean=} opt_zip
+ *
+ * @return {string} Not sure why there was a mismatch
+ */
+ _computeDownloadLink(change, patchNum, opt_zip) {
+ // Polymer 2: check for undefined
+ if ([change, patchNum].some(arg => arg === undefined)) {
+ return '';
+ }
+ return this.changeBaseURL(change.project, change._number, patchNum) +
+ '/patch?' + (opt_zip ? 'zip' : 'download');
+ }
+
+ /**
+ * @param {!Object} change
+ * @param {number|string} patchNum
+ * @param {boolean=} opt_zip
+ *
+ * @return {string}
+ */
+ _computeDownloadFilename(change, patchNum, opt_zip) {
+ // Polymer 2: check for undefined
+ if ([change, patchNum].some(arg => arg === undefined)) {
+ return '';
+ }
+
+ let shortRev = '';
+ for (const rev in change.revisions) {
+ if (this.patchNumEquals(change.revisions[rev]._number, patchNum)) {
+ shortRev = rev.substr(0, 7);
+ break;
+ }
+ }
+ return shortRev + '.diff.' + (opt_zip ? 'zip' : 'base64');
+ }
+
+ _computeHidePatchFile(change, patchNum) {
+ // Polymer 2: check for undefined
+ if ([change, patchNum].some(arg => arg === undefined)) {
+ return false;
+ }
+ for (const rev of Object.values(change.revisions || {})) {
+ if (this.patchNumEquals(rev._number, patchNum)) {
+ const parentLength = rev.commit && rev.commit.parents ?
+ rev.commit.parents.length : 0;
+ return parentLength == 0;
+ }
+ }
+ return false;
+ }
+
+ _computeArchiveDownloadLink(change, patchNum, format) {
+ // Polymer 2: check for undefined
+ if ([change, patchNum, format].some(arg => arg === undefined)) {
+ return '';
+ }
+ return this.changeBaseURL(change.project, change._number, patchNum) +
+ '/archive?format=' + format;
+ }
+
+ _computeSchemes(change, patchNum) {
+ // Polymer 2: check for undefined
+ if ([change, patchNum].some(arg => arg === undefined)) {
+ return [];
+ }
+
+ for (const rev of Object.values(change.revisions || {})) {
+ if (this.patchNumEquals(rev._number, patchNum)) {
+ const fetch = rev.fetch;
+ if (fetch) {
+ return Object.keys(fetch).sort();
+ }
+ break;
+ }
+ }
+ return [];
+ }
+
+ _computePatchSetQuantity(revisions) {
+ if (!revisions) { return 0; }
+ return Object.keys(revisions).length;
+ }
+
+ _handleCloseTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('close', null, {bubbles: false});
+ }
+
+ _schemesChanged(schemes) {
+ if (schemes.length === 0) { return; }
+ if (!schemes.includes(this._selectedScheme)) {
+ this._selectedScheme = schemes.sort()[0];
+ }
+ }
+
+ _computeShowDownloadCommands(schemes) {
+ return schemes.length ? '' : 'hidden';
+ }
+}
+
+customElements.define(GrDownloadDialog.is, GrDownloadDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js
new file mode 100644
index 0000000..324f9f4
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js
@@ -0,0 +1,103 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ padding: var(--spacing-m) 0;
+ }
+ section {
+ display: flex;
+ padding: var(--spacing-m) var(--spacing-xl);
+ }
+ .flexContainer {
+ display: flex;
+ justify-content: space-between;
+ padding-top: var(--spacing-m);
+ }
+ .footer {
+ justify-content: flex-end;
+ }
+ .closeButtonContainer {
+ align-items: flex-end;
+ display: flex;
+ flex: 0;
+ justify-content: flex-end;
+ }
+ .patchFiles,
+ .archivesContainer {
+ padding-bottom: var(--spacing-m);
+ }
+ .patchFiles {
+ margin-right: var(--spacing-xxl);
+ }
+ .patchFiles a,
+ .archives a {
+ display: inline-block;
+ margin-right: var(--spacing-l);
+ }
+ .patchFiles a:last-of-type,
+ .archives a:last-of-type {
+ margin-right: 0;
+ }
+ .title {
+ flex: 1;
+ font-weight: var(--font-weight-bold);
+ }
+ .hidden {
+ display: none;
+ }
+ </style>
+ <section>
+ <h3 class="title">
+ Patch set [[patchNum]] of [[_computePatchSetQuantity(change.revisions)]]
+ </h3>
+ </section>
+ <section class\$="[[_computeShowDownloadCommands(_schemes)]]">
+ <gr-download-commands id="downloadCommands" commands="[[_computeDownloadCommands(change, patchNum, _selectedScheme)]]" schemes="[[_schemes]]" selected-scheme="{{_selectedScheme}}"></gr-download-commands>
+ </section>
+ <section class="flexContainer">
+ <div class="patchFiles" hidden="[[_computeHidePatchFile(change, patchNum)]]">
+ <label>Patch file</label>
+ <div>
+ <a id="download" href\$="[[_computeDownloadLink(change, patchNum)]]" download="">
+ [[_computeDownloadFilename(change, patchNum)]]
+ </a>
+ <a href\$="[[_computeZipDownloadLink(change, patchNum)]]" download="">
+ [[_computeZipDownloadFilename(change, patchNum)]]
+ </a>
+ </div>
+ </div>
+ <div class="archivesContainer" hidden\$="[[!config.archives.length]]" hidden="">
+ <label>Archive</label>
+ <div id="archives" class="archives">
+ <template is="dom-repeat" items="[[config.archives]]" as="format">
+ <a href\$="[[_computeArchiveDownloadLink(change, patchNum, format)]]" download="">
+ [[format]]
+ </a>
+ </template>
+ </div>
+ </div>
+ </section>
+ <section class="footer">
+ <span class="closeButtonContainer">
+ <gr-button id="closeButton" link="" on-click="_handleCloseTap">Close</gr-button>
+ </span>
+ </section>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
index a3755ba..c2f4963 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-download-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-download-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -41,184 +36,186 @@
</template>
</test-fixture>
-<script>
- function getChangeObject() {
- return {
- current_revision: '34685798fe548b6d17d1e8e5edc43a26d055cc72',
- revisions: {
- '34685798fe548b6d17d1e8e5edc43a26d055cc72': {
- _number: 1,
- commit: {
- parents: [],
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-download-dialog.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+function getChangeObject() {
+ return {
+ current_revision: '34685798fe548b6d17d1e8e5edc43a26d055cc72',
+ revisions: {
+ '34685798fe548b6d17d1e8e5edc43a26d055cc72': {
+ _number: 1,
+ commit: {
+ parents: [],
+ },
+ fetch: {
+ repo: {
+ commands: {
+ repo: 'repo download test-project 5/1',
+ },
},
- fetch: {
- repo: {
- commands: {
- repo: 'repo download test-project 5/1',
- },
+ ssh: {
+ commands: {
+ 'Checkout':
+ 'git fetch ' +
+ 'ssh://andybons@localhost:29418/test-project ' +
+ 'refs/changes/05/5/1 && git checkout FETCH_HEAD',
+ 'Cherry Pick':
+ 'git fetch ' +
+ 'ssh://andybons@localhost:29418/test-project ' +
+ 'refs/changes/05/5/1 && git cherry-pick FETCH_HEAD',
+ 'Format Patch':
+ 'git fetch ' +
+ 'ssh://andybons@localhost:29418/test-project ' +
+ 'refs/changes/05/5/1 ' +
+ '&& git format-patch -1 --stdout FETCH_HEAD',
+ 'Pull':
+ 'git pull ' +
+ 'ssh://andybons@localhost:29418/test-project ' +
+ 'refs/changes/05/5/1',
},
- ssh: {
- commands: {
- 'Checkout':
- 'git fetch ' +
- 'ssh://andybons@localhost:29418/test-project ' +
- 'refs/changes/05/5/1 && git checkout FETCH_HEAD',
- 'Cherry Pick':
- 'git fetch ' +
- 'ssh://andybons@localhost:29418/test-project ' +
- 'refs/changes/05/5/1 && git cherry-pick FETCH_HEAD',
- 'Format Patch':
- 'git fetch ' +
- 'ssh://andybons@localhost:29418/test-project ' +
- 'refs/changes/05/5/1 ' +
- '&& git format-patch -1 --stdout FETCH_HEAD',
- 'Pull':
- 'git pull ' +
- 'ssh://andybons@localhost:29418/test-project ' +
- 'refs/changes/05/5/1',
- },
- },
- http: {
- commands: {
- 'Checkout':
- 'git fetch ' +
- 'http://andybons@localhost:8080/a/test-project ' +
- 'refs/changes/05/5/1 && git checkout FETCH_HEAD',
- 'Cherry Pick':
- 'git fetch ' +
- 'http://andybons@localhost:8080/a/test-project ' +
- 'refs/changes/05/5/1 && git cherry-pick FETCH_HEAD',
- 'Format Patch':
- 'git fetch ' +
- 'http://andybons@localhost:8080/a/test-project ' +
- 'refs/changes/05/5/1 && ' +
- 'git format-patch -1 --stdout FETCH_HEAD',
- 'Pull':
- 'git pull ' +
- 'http://andybons@localhost:8080/a/test-project ' +
- 'refs/changes/05/5/1',
- },
+ },
+ http: {
+ commands: {
+ 'Checkout':
+ 'git fetch ' +
+ 'http://andybons@localhost:8080/a/test-project ' +
+ 'refs/changes/05/5/1 && git checkout FETCH_HEAD',
+ 'Cherry Pick':
+ 'git fetch ' +
+ 'http://andybons@localhost:8080/a/test-project ' +
+ 'refs/changes/05/5/1 && git cherry-pick FETCH_HEAD',
+ 'Format Patch':
+ 'git fetch ' +
+ 'http://andybons@localhost:8080/a/test-project ' +
+ 'refs/changes/05/5/1 && ' +
+ 'git format-patch -1 --stdout FETCH_HEAD',
+ 'Pull':
+ 'git pull ' +
+ 'http://andybons@localhost:8080/a/test-project ' +
+ 'refs/changes/05/5/1',
},
},
},
},
- };
- }
+ },
+ };
+}
- function getChangeObjectNoFetch() {
- return {
- current_revision: '34685798fe548b6d17d1e8e5edc43a26d055cc72',
- revisions: {
- '34685798fe548b6d17d1e8e5edc43a26d055cc72': {
- _number: 1,
- commit: {
- parents: [],
- },
- fetch: {},
+function getChangeObjectNoFetch() {
+ return {
+ current_revision: '34685798fe548b6d17d1e8e5edc43a26d055cc72',
+ revisions: {
+ '34685798fe548b6d17d1e8e5edc43a26d055cc72': {
+ _number: 1,
+ commit: {
+ parents: [],
},
+ fetch: {},
},
+ },
+ };
+}
+
+suite('gr-download-dialog', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+
+ element = fixture('basic');
+ element.patchNum = '1';
+ element.config = {
+ schemes: {
+ 'anonymous http': {},
+ 'http': {},
+ 'repo': {},
+ 'ssh': {},
+ },
+ archives: ['tgz', 'tar', 'tbz2', 'txz'],
};
- }
- suite('gr-download-dialog', async () => {
- await readyToTest();
- let element;
- let sandbox;
+ flushAsynchronousOperations();
+ });
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('anchors use download attribute', () => {
+ const anchors = Array.from(
+ dom(element.root).querySelectorAll('a'));
+ assert.isTrue(!anchors.some(a => !a.hasAttribute('download')));
+ });
+
+ suite('gr-download-dialog tests with no fetch options', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
-
- element = fixture('basic');
- element.patchNum = '1';
- element.config = {
- schemes: {
- 'anonymous http': {},
- 'http': {},
- 'repo': {},
- 'ssh': {},
- },
- archives: ['tgz', 'tar', 'tbz2', 'txz'],
- };
-
+ element.change = getChangeObjectNoFetch();
flushAsynchronousOperations();
});
- teardown(() => {
- sandbox.restore();
- });
-
- test('anchors use download attribute', () => {
- const anchors = Array.from(
- Polymer.dom(element.root).querySelectorAll('a'));
- assert.isTrue(!anchors.some(a => !a.hasAttribute('download')));
- });
-
- suite('gr-download-dialog tests with no fetch options', () => {
- setup(() => {
- element.change = getChangeObjectNoFetch();
- flushAsynchronousOperations();
- });
-
- test('focuses on first download link if no copy links', () => {
- const focusStub = sandbox.stub(element.$.download, 'focus');
- element.focus();
- assert.isTrue(focusStub.called);
- focusStub.restore();
- });
- });
-
- suite('gr-download-dialog with fetch options', () => {
- setup(() => {
- element.change = getChangeObject();
- flushAsynchronousOperations();
- });
-
- test('focuses on first copy link', () => {
- const focusStub = sinon.stub(element.$.downloadCommands, 'focusOnCopy');
- element.focus();
- flushAsynchronousOperations();
- assert.isTrue(focusStub.called);
- focusStub.restore();
- });
-
- test('computed fields', () => {
- assert.equal(element._computeArchiveDownloadLink(
- {project: 'test/project', _number: 123}, 2, 'tgz'),
- '/changes/test%2Fproject~123/revisions/2/archive?format=tgz');
- });
-
- test('close event', done => {
- element.addEventListener('close', () => {
- done();
- });
- MockInteractions.tap(element.shadowRoot
- .querySelector('.closeButtonContainer gr-button'));
- });
- });
-
- test('_computeShowDownloadCommands', () => {
- assert.equal(element._computeShowDownloadCommands([]), 'hidden');
- assert.equal(element._computeShowDownloadCommands(['test']), '');
- });
-
- test('_computeHidePatchFile', () => {
- const patchNum = '1';
-
- const change1 = {
- revisions: {
- r1: {_number: 1, commit: {parents: []}},
- },
- };
- assert.isTrue(element._computeHidePatchFile(change1, patchNum));
-
- const change2 = {
- revisions: {
- r1: {_number: 1, commit: {parents: [
- {commit: 'p1'},
- ]}},
- },
- };
- assert.isFalse(element._computeHidePatchFile(change2, patchNum));
+ test('focuses on first download link if no copy links', () => {
+ const focusStub = sandbox.stub(element.$.download, 'focus');
+ element.focus();
+ assert.isTrue(focusStub.called);
+ focusStub.restore();
});
});
+
+ suite('gr-download-dialog with fetch options', () => {
+ setup(() => {
+ element.change = getChangeObject();
+ flushAsynchronousOperations();
+ });
+
+ test('focuses on first copy link', () => {
+ const focusStub = sinon.stub(element.$.downloadCommands, 'focusOnCopy');
+ element.focus();
+ flushAsynchronousOperations();
+ assert.isTrue(focusStub.called);
+ focusStub.restore();
+ });
+
+ test('computed fields', () => {
+ assert.equal(element._computeArchiveDownloadLink(
+ {project: 'test/project', _number: 123}, 2, 'tgz'),
+ '/changes/test%2Fproject~123/revisions/2/archive?format=tgz');
+ });
+
+ test('close event', done => {
+ element.addEventListener('close', () => {
+ done();
+ });
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.closeButtonContainer gr-button'));
+ });
+ });
+
+ test('_computeShowDownloadCommands', () => {
+ assert.equal(element._computeShowDownloadCommands([]), 'hidden');
+ assert.equal(element._computeShowDownloadCommands(['test']), '');
+ });
+
+ test('_computeHidePatchFile', () => {
+ const patchNum = '1';
+
+ const change1 = {
+ revisions: {
+ r1: {_number: 1, commit: {parents: []}},
+ },
+ };
+ assert.isTrue(element._computeHidePatchFile(change1, patchNum));
+
+ const change2 = {
+ revisions: {
+ r1: {_number: 1, commit: {parents: [
+ {commit: 'p1'},
+ ]}},
+ },
+ };
+ assert.isFalse(element._computeHidePatchFile(change2, patchNum));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-constants.html b/polygerrit-ui/app/elements/change/gr-file-list-constants.html
deleted file mode 100644
index 8bdcf7a..0000000
--- a/polygerrit-ui/app/elements/change/gr-file-list-constants.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<script>
- (function(window) {
- 'use strict';
-
- const GrFileListConstants = window.GrFileListConstants || {};
-
- GrFileListConstants.FilesExpandedState = {
- ALL: 'all',
- NONE: 'none',
- SOME: 'some',
- };
-
- window.GrFileListConstants = GrFileListConstants;
- })(window);
-</script>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-constants.js b/polygerrit-ui/app/elements/change/gr-file-list-constants.js
new file mode 100644
index 0000000..0f93b52
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-file-list-constants.js
@@ -0,0 +1,29 @@
+/**
+ * @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.
+ */
+(function(window) {
+ 'use strict';
+
+ const GrFileListConstants = window.GrFileListConstants || {};
+
+ GrFileListConstants.FilesExpandedState = {
+ ALL: 'all',
+ NONE: 'none',
+ SOME: 'some',
+ };
+
+ window.GrFileListConstants = GrFileListConstants;
+})(window);
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
deleted file mode 100644
index 79f6c50..0000000
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
+++ /dev/null
@@ -1,273 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../diff/gr-diff-mode-selector/gr-diff-mode-selector.html">
-<link rel="import" href="../../diff/gr-patch-range-select/gr-patch-range-select.html">
-<link rel="import" href="../../edit/gr-edit-controls/gr-edit-controls.html">
-<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
-<link rel="import" href="../../shared/gr-linked-chip/gr-linked-chip.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../gr-file-list-constants.html">
-<link rel="import" href="../gr-commit-info/gr-commit-info.html">
-
-<dom-module id="gr-file-list-header">
- <template>
- <style include="shared-styles">
- .prefsButton {
- float: right;
- }
- .collapseToggleButton {
- text-decoration: none;
- }
- .patchInfoOldPatchSet.patchInfo-header {
- background-color: var(--emphasis-color);
- }
- .patchInfo-header {
- align-items: center;
- border-top: 1px solid var(--border-color);
- display: flex;
- padding: var(--spacing-s) var(--spacing-l);
- }
- .patchInfo-left {
- align-items: baseline;
- display: flex;
- }
- .patchInfoContent {
- align-items: center;
- display: flex;
- flex-wrap: wrap;
- }
- .patchInfo-header .container.latestPatchContainer {
- display: none;
- }
- .patchInfoOldPatchSet .container.latestPatchContainer {
- display: initial;
- }
- .latestPatchContainer a {
- text-decoration: none;
- }
- gr-editable-label.descriptionLabel {
- max-width: 100%;
- }
- .mobile {
- display: none;
- }
- .patchInfo-header .container {
- align-items: center;
- display: flex;
- }
- .downloadContainer,
- .uploadContainer,
- .includedInContainer {
- margin-right: 16px;
- }
- .includedInContainer.hide,
- .uploadContainer.hide {
- display: none;
- }
- .rightControls {
- align-self: flex-end;
- margin: auto 0 auto auto;
- align-items: center;
- display: flex;
- flex-wrap: wrap;
- font-weight: var(--font-weight-normal);
- justify-content: flex-end;
- }
- #collapseBtn,
- .expanded #expandBtn,
- .fileViewActions{
- display: none;
- }
- .expanded #expandBtn {
- display: none;
- }
- gr-linked-chip {
- --linked-chip-text-color: var(--primary-text-color);
- }
- .expanded #collapseBtn,
- .openFile .fileViewActions {
- align-items: center;
- display: flex;
- }
- .rightControls gr-button,
- gr-patch-range-select {
- margin: 0 -4px;
- }
- .fileViewActions gr-button {
- margin: 0;
- --gr-button: {
- padding: 2px 4px;
- }
- }
- .editMode .hideOnEdit {
- display: none;
- }
- .showOnEdit {
- display: none;
- }
- .editMode .showOnEdit {
- display: initial;
- }
- .editMode .showOnEdit.flexContainer {
- align-items: center;
- display: flex;
- }
- .label {
- font-weight: var(--font-weight-bold);
- margin-right: 24px;
- }
- gr-commit-info,
- gr-edit-controls {
- margin-right: -5px;
- }
- .fileViewActionsLabel {
- margin-right: var(--spacing-xs);
- }
- @media screen and (max-width: 50em) {
- .patchInfo-header .desktop {
- display: none;
- }
- }
- </style>
- <div class$="patchInfo-header [[_computeEditModeClass(editMode)]] [[_computePatchInfoClass(patchNum, allPatchSets)]]">
- <div class="patchInfo-left">
- <div class="patchInfoContent">
- <gr-patch-range-select
- id="rangeSelect"
- change-comments="[[changeComments]]"
- change-num="[[changeNum]]"
- patch-num="[[patchNum]]"
- base-patch-num="[[basePatchNum]]"
- available-patches="[[allPatchSets]]"
- revisions="[[change.revisions]]"
- revision-info="[[revisionInfo]]"
- on-patch-range-change="_handlePatchChange">
- </gr-patch-range-select>
- <span class="separator"></span>
- <gr-commit-info
- change="[[change]]"
- server-config="[[serverConfig]]"
- commit-info="[[commitInfo]]"></gr-commit-info>
- <span class="container latestPatchContainer">
- <span class="separator"></span>
- <a href$="[[changeUrl]]">Go to latest patch set</a>
- </span>
- <span class="container descriptionContainer hideOnEdit">
- <span class="separator"></span>
- <template
- is="dom-if"
- if="[[_patchsetDescription]]">
- <gr-linked-chip
- id="descriptionChip"
- text="[[_patchsetDescription]]"
- removable="[[!_descriptionReadOnly]]"
- on-remove="_handleDescriptionRemoved"></gr-linked-chip>
- </template>
- <template
- is="dom-if"
- if="[[!_patchsetDescription]]">
- <gr-editable-label
- id="descriptionLabel"
- uppercase
- class="descriptionLabel"
- label-text="Add patchset description"
- value="[[_patchsetDescription]]"
- placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]"
- read-only="[[_descriptionReadOnly]]"
- on-changed="_handleDescriptionChanged"></gr-editable-label>
- </template>
- </span>
- </div>
- </div>
- <div class$="rightControls [[_computeExpandedClass(filesExpanded)]]">
- <span class="showOnEdit flexContainer">
- <gr-edit-controls
- id="editControls"
- patch-num="[[patchNum]]"
- change="[[change]]"></gr-edit-controls>
- <span class="separator"></span>
- </span>
- <span class$="[[_computeUploadHelpContainerClass(change, account)]]">
- <gr-button link
- class="upload"
- on-click="_handleUploadTap">Update Change</gr-button>
- </span>
- <span class="downloadContainer desktop">
- <gr-button link
- class="download"
- title="[[createTitle(Shortcut.OPEN_DOWNLOAD_DIALOG,
- ShortcutSection.ACTIONS)]]"
- on-click="_handleDownloadTap">Download</gr-button>
- </span>
- <span class$="includedInContainer [[_hideIncludedIn(change)]] desktop">
- <gr-button link
- class="includedIn"
- on-click="_handleIncludedInTap">Included In</gr-button>
- </span>
- <template is="dom-if"
- if="[[_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]">
- <gr-button
- id="expandBtn"
- link
- title="[[createTitle(Shortcut.EXPAND_ALL_DIFF_CONTEXT,
- ShortcutSection.DIFFS)]]"
- on-click="_expandAllDiffs">Expand All</gr-button>
- <gr-button
- id="collapseBtn"
- link
- on-click="_collapseAllDiffs">Collapse All</gr-button>
- </template>
- <template is="dom-if"
- if="[[!_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]">
- <div class="warning">
- Bulk actions disabled because there are too many files.
- </div>
- </template>
- <div class="fileViewActions">
- <span class="separator"></span>
- <span class="fileViewActionsLabel">Diff view:</span>
- <gr-diff-mode-selector
- id="modeSelect"
- mode="{{diffViewMode}}"
- save-on-change="[[!diffPrefsDisabled]]"></gr-diff-mode-selector>
- <span id="diffPrefsContainer"
- class="hideOnEdit"
- hidden$="[[_computePrefsButtonHidden(diffPrefs, diffPrefsDisabled)]]"
- hidden>
- <gr-button
- link
- has-tooltip
- title="Diff preferences"
- class="prefsButton desktop"
- on-click="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button>
- </span>
- </div>
- </div>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-file-list-header.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
index 34e1cfa..eef436b 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
@@ -14,264 +14,286 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- // Maximum length for patch set descriptions.
- const PATCH_DESC_MAX_LENGTH = 500;
- const MERGED_STATUS = 'MERGED';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../diff/gr-diff-mode-selector/gr-diff-mode-selector.js';
+import '../../diff/gr-patch-range-select/gr-patch-range-select.js';
+import '../../edit/gr-edit-controls/gr-edit-controls.js';
+import '../../shared/gr-editable-label/gr-editable-label.js';
+import '../../shared/gr-linked-chip/gr-linked-chip.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../gr-file-list-constants.js';
+import '../gr-commit-info/gr-commit-info.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-file-list-header_html.js';
+
+// Maximum length for patch set descriptions.
+const PATCH_DESC_MAX_LENGTH = 500;
+const MERGED_STATUS = 'MERGED';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrFileListHeader extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-file-list-header'; }
+ /**
+ * @event expand-diffs
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * @event collapse-diffs
*/
- class GrFileListHeader extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-file-list-header'; }
- /**
- * @event expand-diffs
- */
- /**
- * @event collapse-diffs
- */
+ /**
+ * @event open-diff-prefs
+ */
- /**
- * @event open-diff-prefs
- */
+ /**
+ * @event open-included-in-dialog
+ */
- /**
- * @event open-included-in-dialog
- */
+ /**
+ * @event open-download-dialog
+ */
- /**
- * @event open-download-dialog
- */
+ /**
+ * @event open-upload-help-dialog
+ */
- /**
- * @event open-upload-help-dialog
- */
+ static get properties() {
+ return {
+ account: Object,
+ allPatchSets: Array,
+ /** @type {?} */
+ change: Object,
+ changeNum: String,
+ changeUrl: String,
+ changeComments: Object,
+ commitInfo: Object,
+ editMode: Boolean,
+ loggedIn: Boolean,
+ serverConfig: Object,
+ shownFileCount: Number,
+ diffPrefs: Object,
+ diffPrefsDisabled: Boolean,
+ diffViewMode: {
+ type: String,
+ notify: true,
+ },
+ patchNum: String,
+ basePatchNum: String,
+ filesExpanded: String,
+ // Caps the number of files that can be shown and have the 'show diffs' /
+ // 'hide diffs' buttons still be functional.
+ _maxFilesForBulkActions: {
+ type: Number,
+ readOnly: true,
+ value: 225,
+ },
+ _patchsetDescription: {
+ type: String,
+ value: '',
+ },
+ _descriptionReadOnly: {
+ type: Boolean,
+ computed: '_computeDescriptionReadOnly(loggedIn, change, account)',
+ },
+ revisionInfo: Object,
+ };
+ }
- static get properties() {
- return {
- account: Object,
- allPatchSets: Array,
- /** @type {?} */
- change: Object,
- changeNum: String,
- changeUrl: String,
- changeComments: Object,
- commitInfo: Object,
- editMode: Boolean,
- loggedIn: Boolean,
- serverConfig: Object,
- shownFileCount: Number,
- diffPrefs: Object,
- diffPrefsDisabled: Boolean,
- diffViewMode: {
- type: String,
- notify: true,
- },
- patchNum: String,
- basePatchNum: String,
- filesExpanded: String,
- // Caps the number of files that can be shown and have the 'show diffs' /
- // 'hide diffs' buttons still be functional.
- _maxFilesForBulkActions: {
- type: Number,
- readOnly: true,
- value: 225,
- },
- _patchsetDescription: {
- type: String,
- value: '',
- },
- _descriptionReadOnly: {
- type: Boolean,
- computed: '_computeDescriptionReadOnly(loggedIn, change, account)',
- },
- revisionInfo: Object,
- };
+ static get observers() {
+ return [
+ '_computePatchSetDescription(change, patchNum)',
+ ];
+ }
+
+ setDiffViewMode(mode) {
+ this.$.modeSelect.setMode(mode);
+ }
+
+ _expandAllDiffs() {
+ this._expanded = true;
+ this.fire('expand-diffs');
+ }
+
+ _collapseAllDiffs() {
+ this._expanded = false;
+ this.fire('collapse-diffs');
+ }
+
+ _computeExpandedClass(filesExpanded) {
+ const classes = [];
+ if (filesExpanded === GrFileListConstants.FilesExpandedState.ALL) {
+ classes.push('expanded');
+ }
+ if (filesExpanded === GrFileListConstants.FilesExpandedState.SOME ||
+ filesExpanded === GrFileListConstants.FilesExpandedState.ALL) {
+ classes.push('openFile');
+ }
+ return classes.join(' ');
+ }
+
+ _computeDescriptionPlaceholder(readOnly) {
+ return (readOnly ? 'No' : 'Add') + ' patchset description';
+ }
+
+ _computeDescriptionReadOnly(loggedIn, change, account) {
+ // Polymer 2: check for undefined
+ if ([loggedIn, change, account].some(arg => arg === undefined)) {
+ return undefined;
}
- static get observers() {
- return [
- '_computePatchSetDescription(change, patchNum)',
- ];
+ return !(loggedIn && (account._account_id === change.owner._account_id));
+ }
+
+ _computePatchSetDescription(change, patchNum) {
+ // Polymer 2: check for undefined
+ if ([change, patchNum].some(arg => arg === undefined)) {
+ return;
}
- setDiffViewMode(mode) {
- this.$.modeSelect.setMode(mode);
- }
+ const rev = this.getRevisionByPatchNum(change.revisions, patchNum);
+ this._patchsetDescription = (rev && rev.description) ?
+ rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
+ }
- _expandAllDiffs() {
- this._expanded = true;
- this.fire('expand-diffs');
- }
+ _handleDescriptionRemoved(e) {
+ return this._updateDescription('', e);
+ }
- _collapseAllDiffs() {
- this._expanded = false;
- this.fire('collapse-diffs');
- }
-
- _computeExpandedClass(filesExpanded) {
- const classes = [];
- if (filesExpanded === GrFileListConstants.FilesExpandedState.ALL) {
- classes.push('expanded');
+ /**
+ * @param {!Object} revisions The revisions object keyed by revision hashes
+ * @param {?Object} patchSet A revision already fetched from {revisions}
+ * @return {string|undefined} the SHA hash corresponding to the revision.
+ */
+ _getPatchsetHash(revisions, patchSet) {
+ for (const rev in revisions) {
+ if (revisions.hasOwnProperty(rev) &&
+ revisions[rev] === patchSet) {
+ return rev;
}
- if (filesExpanded === GrFileListConstants.FilesExpandedState.SOME ||
- filesExpanded === GrFileListConstants.FilesExpandedState.ALL) {
- classes.push('openFile');
- }
- return classes.join(' ');
- }
-
- _computeDescriptionPlaceholder(readOnly) {
- return (readOnly ? 'No' : 'Add') + ' patchset description';
- }
-
- _computeDescriptionReadOnly(loggedIn, change, account) {
- // Polymer 2: check for undefined
- if ([loggedIn, change, account].some(arg => arg === undefined)) {
- return undefined;
- }
-
- return !(loggedIn && (account._account_id === change.owner._account_id));
- }
-
- _computePatchSetDescription(change, patchNum) {
- // Polymer 2: check for undefined
- if ([change, patchNum].some(arg => arg === undefined)) {
- return;
- }
-
- const rev = this.getRevisionByPatchNum(change.revisions, patchNum);
- this._patchsetDescription = (rev && rev.description) ?
- rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
- }
-
- _handleDescriptionRemoved(e) {
- return this._updateDescription('', e);
- }
-
- /**
- * @param {!Object} revisions The revisions object keyed by revision hashes
- * @param {?Object} patchSet A revision already fetched from {revisions}
- * @return {string|undefined} the SHA hash corresponding to the revision.
- */
- _getPatchsetHash(revisions, patchSet) {
- for (const rev in revisions) {
- if (revisions.hasOwnProperty(rev) &&
- revisions[rev] === patchSet) {
- return rev;
- }
- }
- }
-
- _handleDescriptionChanged(e) {
- const desc = e.detail.trim();
- this._updateDescription(desc, e);
- }
-
- /**
- * Update the patchset description with the rest API.
- *
- * @param {string} desc
- * @param {?(Event|Node)} e
- * @return {!Promise}
- */
- _updateDescription(desc, e) {
- const target = Polymer.dom(e).rootTarget;
- if (target) { target.disabled = true; }
- const rev = this.getRevisionByPatchNum(this.change.revisions,
- this.patchNum);
- const sha = this._getPatchsetHash(this.change.revisions, rev);
- return this.$.restAPI.setDescription(this.changeNum, this.patchNum, desc)
- .then(res => {
- if (res.ok) {
- if (target) { target.disabled = false; }
- this.set(['change', 'revisions', sha, 'description'], desc);
- this._patchsetDescription = desc;
- }
- })
- .catch(err => {
- if (target) { target.disabled = false; }
- return;
- });
- }
-
- _computePrefsButtonHidden(prefs, diffPrefsDisabled) {
- return diffPrefsDisabled || !prefs;
- }
-
- _fileListActionsVisible(shownFileCount, maxFilesForBulkActions) {
- return shownFileCount <= maxFilesForBulkActions;
- }
-
- _handlePatchChange(e) {
- const {basePatchNum, patchNum} = e.detail;
- if (this.patchNumEquals(basePatchNum, this.basePatchNum) &&
- this.patchNumEquals(patchNum, this.patchNum)) { return; }
- Gerrit.Nav.navigateToChange(this.change, patchNum, basePatchNum);
- }
-
- _handlePrefsTap(e) {
- e.preventDefault();
- this.fire('open-diff-prefs');
- }
-
- _handleIncludedInTap(e) {
- e.preventDefault();
- this.fire('open-included-in-dialog');
- }
-
- _handleDownloadTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('open-download-dialog', {bubbles: false}));
- }
-
- _computeEditModeClass(editMode) {
- return editMode ? 'editMode' : '';
- }
-
- _computePatchInfoClass(patchNum, allPatchSets) {
- const latestNum = this.computeLatestPatchNum(allPatchSets);
- if (this.patchNumEquals(patchNum, latestNum)) {
- return '';
- }
- return 'patchInfoOldPatchSet';
- }
-
- _hideIncludedIn(change) {
- return change && change.status === MERGED_STATUS ? '' : 'hide';
- }
-
- _handleUploadTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('open-upload-help-dialog', {bubbles: false}));
- }
-
- _computeUploadHelpContainerClass(change, account) {
- const changeIsMerged = change && change.status === MERGED_STATUS;
- const ownerId = change && change.owner && change.owner._account_id ?
- change.owner._account_id : null;
- const userId = account && account._account_id;
- const userIsOwner = ownerId && userId && ownerId === userId;
- const hideContainer = !userIsOwner || changeIsMerged;
- return 'uploadContainer desktop' + (hideContainer ? ' hide' : '');
}
}
- customElements.define(GrFileListHeader.is, GrFileListHeader);
-})();
+ _handleDescriptionChanged(e) {
+ const desc = e.detail.trim();
+ this._updateDescription(desc, e);
+ }
+
+ /**
+ * Update the patchset description with the rest API.
+ *
+ * @param {string} desc
+ * @param {?(Event|Node)} e
+ * @return {!Promise}
+ */
+ _updateDescription(desc, e) {
+ const target = dom(e).rootTarget;
+ if (target) { target.disabled = true; }
+ const rev = this.getRevisionByPatchNum(this.change.revisions,
+ this.patchNum);
+ const sha = this._getPatchsetHash(this.change.revisions, rev);
+ return this.$.restAPI.setDescription(this.changeNum, this.patchNum, desc)
+ .then(res => {
+ if (res.ok) {
+ if (target) { target.disabled = false; }
+ this.set(['change', 'revisions', sha, 'description'], desc);
+ this._patchsetDescription = desc;
+ }
+ })
+ .catch(err => {
+ if (target) { target.disabled = false; }
+ return;
+ });
+ }
+
+ _computePrefsButtonHidden(prefs, diffPrefsDisabled) {
+ return diffPrefsDisabled || !prefs;
+ }
+
+ _fileListActionsVisible(shownFileCount, maxFilesForBulkActions) {
+ return shownFileCount <= maxFilesForBulkActions;
+ }
+
+ _handlePatchChange(e) {
+ const {basePatchNum, patchNum} = e.detail;
+ if (this.patchNumEquals(basePatchNum, this.basePatchNum) &&
+ this.patchNumEquals(patchNum, this.patchNum)) { return; }
+ Gerrit.Nav.navigateToChange(this.change, patchNum, basePatchNum);
+ }
+
+ _handlePrefsTap(e) {
+ e.preventDefault();
+ this.fire('open-diff-prefs');
+ }
+
+ _handleIncludedInTap(e) {
+ e.preventDefault();
+ this.fire('open-included-in-dialog');
+ }
+
+ _handleDownloadTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(
+ new CustomEvent('open-download-dialog', {bubbles: false}));
+ }
+
+ _computeEditModeClass(editMode) {
+ return editMode ? 'editMode' : '';
+ }
+
+ _computePatchInfoClass(patchNum, allPatchSets) {
+ const latestNum = this.computeLatestPatchNum(allPatchSets);
+ if (this.patchNumEquals(patchNum, latestNum)) {
+ return '';
+ }
+ return 'patchInfoOldPatchSet';
+ }
+
+ _hideIncludedIn(change) {
+ return change && change.status === MERGED_STATUS ? '' : 'hide';
+ }
+
+ _handleUploadTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(
+ new CustomEvent('open-upload-help-dialog', {bubbles: false}));
+ }
+
+ _computeUploadHelpContainerClass(change, account) {
+ const changeIsMerged = change && change.status === MERGED_STATUS;
+ const ownerId = change && change.owner && change.owner._account_id ?
+ change.owner._account_id : null;
+ const userId = account && account._account_id;
+ const userIsOwner = ownerId && userId && ownerId === userId;
+ const hideContainer = !userIsOwner || changeIsMerged;
+ return 'uploadContainer desktop' + (hideContainer ? ' hide' : '');
+ }
+}
+
+customElements.define(GrFileListHeader.is, GrFileListHeader);
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js
new file mode 100644
index 0000000..28cd645
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js
@@ -0,0 +1,196 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ .prefsButton {
+ float: right;
+ }
+ .collapseToggleButton {
+ text-decoration: none;
+ }
+ .patchInfoOldPatchSet.patchInfo-header {
+ background-color: var(--emphasis-color);
+ }
+ .patchInfo-header {
+ align-items: center;
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ padding: var(--spacing-s) var(--spacing-l);
+ }
+ .patchInfo-left {
+ align-items: baseline;
+ display: flex;
+ }
+ .patchInfoContent {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ }
+ .patchInfo-header .container.latestPatchContainer {
+ display: none;
+ }
+ .patchInfoOldPatchSet .container.latestPatchContainer {
+ display: initial;
+ }
+ .latestPatchContainer a {
+ text-decoration: none;
+ }
+ gr-editable-label.descriptionLabel {
+ max-width: 100%;
+ }
+ .mobile {
+ display: none;
+ }
+ .patchInfo-header .container {
+ align-items: center;
+ display: flex;
+ }
+ .downloadContainer,
+ .uploadContainer,
+ .includedInContainer {
+ margin-right: 16px;
+ }
+ .includedInContainer.hide,
+ .uploadContainer.hide {
+ display: none;
+ }
+ .rightControls {
+ align-self: flex-end;
+ margin: auto 0 auto auto;
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ font-weight: var(--font-weight-normal);
+ justify-content: flex-end;
+ }
+ #collapseBtn,
+ .expanded #expandBtn,
+ .fileViewActions{
+ display: none;
+ }
+ .expanded #expandBtn {
+ display: none;
+ }
+ gr-linked-chip {
+ --linked-chip-text-color: var(--primary-text-color);
+ }
+ .expanded #collapseBtn,
+ .openFile .fileViewActions {
+ align-items: center;
+ display: flex;
+ }
+ .rightControls gr-button,
+ gr-patch-range-select {
+ margin: 0 -4px;
+ }
+ .fileViewActions gr-button {
+ margin: 0;
+ --gr-button: {
+ padding: 2px 4px;
+ }
+ }
+ .editMode .hideOnEdit {
+ display: none;
+ }
+ .showOnEdit {
+ display: none;
+ }
+ .editMode .showOnEdit {
+ display: initial;
+ }
+ .editMode .showOnEdit.flexContainer {
+ align-items: center;
+ display: flex;
+ }
+ .label {
+ font-weight: var(--font-weight-bold);
+ margin-right: 24px;
+ }
+ gr-commit-info,
+ gr-edit-controls {
+ margin-right: -5px;
+ }
+ .fileViewActionsLabel {
+ margin-right: var(--spacing-xs);
+ }
+ @media screen and (max-width: 50em) {
+ .patchInfo-header .desktop {
+ display: none;
+ }
+ }
+ </style>
+ <div class\$="patchInfo-header [[_computeEditModeClass(editMode)]] [[_computePatchInfoClass(patchNum, allPatchSets)]]">
+ <div class="patchInfo-left">
+ <div class="patchInfoContent">
+ <gr-patch-range-select id="rangeSelect" change-comments="[[changeComments]]" change-num="[[changeNum]]" patch-num="[[patchNum]]" base-patch-num="[[basePatchNum]]" available-patches="[[allPatchSets]]" revisions="[[change.revisions]]" revision-info="[[revisionInfo]]" on-patch-range-change="_handlePatchChange">
+ </gr-patch-range-select>
+ <span class="separator"></span>
+ <gr-commit-info change="[[change]]" server-config="[[serverConfig]]" commit-info="[[commitInfo]]"></gr-commit-info>
+ <span class="container latestPatchContainer">
+ <span class="separator"></span>
+ <a href\$="[[changeUrl]]">Go to latest patch set</a>
+ </span>
+ <span class="container descriptionContainer hideOnEdit">
+ <span class="separator"></span>
+ <template is="dom-if" if="[[_patchsetDescription]]">
+ <gr-linked-chip id="descriptionChip" text="[[_patchsetDescription]]" removable="[[!_descriptionReadOnly]]" on-remove="_handleDescriptionRemoved"></gr-linked-chip>
+ </template>
+ <template is="dom-if" if="[[!_patchsetDescription]]">
+ <gr-editable-label id="descriptionLabel" uppercase="" class="descriptionLabel" label-text="Add patchset description" value="[[_patchsetDescription]]" placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]" read-only="[[_descriptionReadOnly]]" on-changed="_handleDescriptionChanged"></gr-editable-label>
+ </template>
+ </span>
+ </div>
+ </div>
+ <div class\$="rightControls [[_computeExpandedClass(filesExpanded)]]">
+ <span class="showOnEdit flexContainer">
+ <gr-edit-controls id="editControls" patch-num="[[patchNum]]" change="[[change]]"></gr-edit-controls>
+ <span class="separator"></span>
+ </span>
+ <span class\$="[[_computeUploadHelpContainerClass(change, account)]]">
+ <gr-button link="" class="upload" on-click="_handleUploadTap">Update Change</gr-button>
+ </span>
+ <span class="downloadContainer desktop">
+ <gr-button link="" class="download" title="[[createTitle(Shortcut.OPEN_DOWNLOAD_DIALOG,
+ ShortcutSection.ACTIONS)]]" on-click="_handleDownloadTap">Download</gr-button>
+ </span>
+ <span class\$="includedInContainer [[_hideIncludedIn(change)]] desktop">
+ <gr-button link="" class="includedIn" on-click="_handleIncludedInTap">Included In</gr-button>
+ </span>
+ <template is="dom-if" if="[[_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]">
+ <gr-button id="expandBtn" link="" title="[[createTitle(Shortcut.EXPAND_ALL_DIFF_CONTEXT,
+ ShortcutSection.DIFFS)]]" on-click="_expandAllDiffs">Expand All</gr-button>
+ <gr-button id="collapseBtn" link="" on-click="_collapseAllDiffs">Collapse All</gr-button>
+ </template>
+ <template is="dom-if" if="[[!_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]">
+ <div class="warning">
+ Bulk actions disabled because there are too many files.
+ </div>
+ </template>
+ <div class="fileViewActions">
+ <span class="separator"></span>
+ <span class="fileViewActionsLabel">Diff view:</span>
+ <gr-diff-mode-selector id="modeSelect" mode="{{diffViewMode}}" save-on-change="[[!diffPrefsDisabled]]"></gr-diff-mode-selector>
+ <span id="diffPrefsContainer" class="hideOnEdit" hidden\$="[[_computePrefsButtonHidden(diffPrefs, diffPrefsDisabled)]]" hidden="">
+ <gr-button link="" has-tooltip="" title="Diff preferences" class="prefsButton desktop" on-click="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button>
+ </span>
+ </div>
+ </div>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
index f8a4e87..745d25e 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
@@ -19,17 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-file-list-header</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="/bower_components/page/page.js"></script>
-
-<link rel="import" href="gr-file-list-header.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script src="/node_modules/page/page.js"></script>
<test-fixture id="basic">
<template>
@@ -43,281 +37,283 @@
</template>
</test-fixture>
-<script>
- suite('gr-file-list-header tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-file-list-header.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-file-list-header tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({test: 'config'}); },
- getAccount() { return Promise.resolve(null); },
- _fetchSharedCacheURL() { return Promise.resolve({}); },
- });
- element = fixture('basic');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({test: 'config'}); },
+ getAccount() { return Promise.resolve(null); },
+ _fetchSharedCacheURL() { return Promise.resolve({}); },
});
+ element = fixture('basic');
+ });
- teardown(done => {
- flush(() => {
- sandbox.restore();
- done();
- });
- });
-
- test('Diff preferences hidden when no prefs or diffPrefsDisabled', () => {
- element.diffPrefsDisabled = true;
- flushAsynchronousOperations();
- assert.isTrue(element.$.diffPrefsContainer.hidden);
-
- element.diffPrefsDisabled = false;
- flushAsynchronousOperations();
- assert.isTrue(element.$.diffPrefsContainer.hidden);
-
- element.diffPrefsDisabled = true;
- element.diffPrefs = {font_size: '12'};
- flushAsynchronousOperations();
- assert.isTrue(element.$.diffPrefsContainer.hidden);
-
- element.diffPrefsDisabled = false;
- flushAsynchronousOperations();
- assert.isFalse(element.$.diffPrefsContainer.hidden);
- });
-
- test('_computeDescriptionReadOnly', () => {
- assert.equal(element._computeDescriptionReadOnly(false,
- {owner: {_account_id: 1}}, {_account_id: 1}), true);
- assert.equal(element._computeDescriptionReadOnly(true,
- {owner: {_account_id: 0}}, {_account_id: 1}), true);
- assert.equal(element._computeDescriptionReadOnly(true,
- {owner: {_account_id: 1}}, {_account_id: 1}), false);
- });
-
- test('_computeDescriptionPlaceholder', () => {
- assert.equal(element._computeDescriptionPlaceholder(true),
- 'No patchset description');
- assert.equal(element._computeDescriptionPlaceholder(false),
- 'Add patchset description');
- });
-
- test('description editing', () => {
- const putDescStub = sandbox.stub(element.$.restAPI, 'setDescription')
- .returns(Promise.resolve({ok: true}));
-
- element.changeNum = '42';
- element.basePatchNum = 'PARENT';
- element.patchNum = 1;
-
- element.change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1, description: 'test', commit: {commit: 'rev1'}},
- },
- current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- actions: {},
- owner: {_account_id: 1},
- };
- element.account = {_account_id: 1};
- element.loggedIn = true;
-
- flushAsynchronousOperations();
-
- // The element has a description, so the account chip should be visible
- // and the description label should not exist.
- const chip = Polymer.dom(element.root).querySelector('#descriptionChip');
- let label = Polymer.dom(element.root).querySelector('#descriptionLabel');
-
- assert.equal(chip.text, 'test');
- assert.isNotOk(label);
-
- // Simulate tapping the remove button, but call function directly so that
- // can determine what happens after the promise is resolved.
- return element._handleDescriptionRemoved()
- .then(() => {
- // The API stub should be called with an empty string for the new
- // description.
- assert.equal(putDescStub.lastCall.args[2], '');
- assert.equal(element.change.revisions.rev1.description, '');
-
- flushAsynchronousOperations();
- // The editable label should now be visible and the chip hidden.
- label = Polymer.dom(element.root).querySelector('#descriptionLabel');
- assert.isOk(label);
- assert.equal(getComputedStyle(chip).display, 'none');
- assert.notEqual(getComputedStyle(label).display, 'none');
- assert.isFalse(label.readOnly);
- // Edit the label to have a new value of test2, and save.
- label.editing = true;
- label._inputText = 'test2';
- label._save();
- flushAsynchronousOperations();
- // The API stub should be called with an `test2` for the new
- // description.
- assert.equal(putDescStub.callCount, 2);
- assert.equal(putDescStub.lastCall.args[2], 'test2');
- })
- .then(() => {
- flushAsynchronousOperations();
- // The chip should be visible again, and the label hidden.
- assert.equal(element.change.revisions.rev1.description, 'test2');
- assert.equal(getComputedStyle(label).display, 'none');
- assert.notEqual(getComputedStyle(chip).display, 'none');
- });
- });
-
- test('expandAllDiffs called when expand button clicked', () => {
- element.shownFileCount = 1;
- flushAsynchronousOperations();
- sandbox.stub(element, '_expandAllDiffs');
- MockInteractions.tap(Polymer.dom(element.root).querySelector(
- '#expandBtn'));
- assert.isTrue(element._expandAllDiffs.called);
- });
-
- test('collapseAllDiffs called when expand button clicked', () => {
- element.shownFileCount = 1;
- flushAsynchronousOperations();
- sandbox.stub(element, '_collapseAllDiffs');
- MockInteractions.tap(Polymer.dom(element.root).querySelector(
- '#collapseBtn'));
- assert.isTrue(element._collapseAllDiffs.called);
- });
-
- test('show/hide diffs disabled for large amounts of files', done => {
- const computeSpy = sandbox.spy(element, '_fileListActionsVisible');
- element._files = [];
- element.changeNum = '42';
- element.basePatchNum = 'PARENT';
- element.patchNum = '2';
- element.shownFileCount = 1;
- flush(() => {
- assert.isTrue(computeSpy.lastCall.returnValue);
- _.times(element._maxFilesForBulkActions + 1, () => {
- element.shownFileCount = element.shownFileCount + 1;
- });
- assert.isFalse(computeSpy.lastCall.returnValue);
- done();
- });
- });
-
- test('fileViewActions are properly hidden', () => {
- const actions = element.shadowRoot
- .querySelector('.fileViewActions');
- assert.equal(getComputedStyle(actions).display, 'none');
- element.filesExpanded = GrFileListConstants.FilesExpandedState.SOME;
- flushAsynchronousOperations();
- assert.notEqual(getComputedStyle(actions).display, 'none');
- element.filesExpanded = GrFileListConstants.FilesExpandedState.ALL;
- flushAsynchronousOperations();
- assert.notEqual(getComputedStyle(actions).display, 'none');
- element.filesExpanded = GrFileListConstants.FilesExpandedState.NONE;
- flushAsynchronousOperations();
- assert.equal(getComputedStyle(actions).display, 'none');
- });
-
- test('expand/collapse buttons are toggled correctly', () => {
- element.shownFileCount = 10;
- flushAsynchronousOperations();
- const expandBtn = element.shadowRoot.querySelector('#expandBtn');
- const collapseBtn = element.shadowRoot.querySelector('#collapseBtn');
- assert.notEqual(getComputedStyle(expandBtn).display, 'none');
- assert.equal(getComputedStyle(collapseBtn).display, 'none');
- element.filesExpanded = GrFileListConstants.FilesExpandedState.SOME;
- flushAsynchronousOperations();
- assert.notEqual(getComputedStyle(expandBtn).display, 'none');
- assert.equal(getComputedStyle(collapseBtn).display, 'none');
- element.filesExpanded = GrFileListConstants.FilesExpandedState.ALL;
- flushAsynchronousOperations();
- assert.equal(getComputedStyle(expandBtn).display, 'none');
- assert.notEqual(getComputedStyle(collapseBtn).display, 'none');
- element.filesExpanded = GrFileListConstants.FilesExpandedState.NONE;
- flushAsynchronousOperations();
- assert.notEqual(getComputedStyle(expandBtn).display, 'none');
- assert.equal(getComputedStyle(collapseBtn).display, 'none');
- });
-
- test('navigateToChange called when range select changes', () => {
- const navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
- element.change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev2: {_number: 2},
- rev1: {_number: 1},
- rev13: {_number: 13},
- rev3: {_number: 3},
- },
- status: 'NEW',
- labels: {},
- };
- element.basePatchNum = 1;
- element.patchNum = 2;
-
- element._handlePatchChange({detail: {basePatchNum: 1, patchNum: 3}});
- assert.equal(navigateToChangeStub.callCount, 1);
- assert.isTrue(navigateToChangeStub.lastCall
- .calledWithExactly(element.change, 3, 1));
- });
-
- test('class is applied to file list on old patch set', () => {
- const allPatchSets = [{num: 4}, {num: 2}, {num: 1}];
- assert.equal(element._computePatchInfoClass('1', allPatchSets),
- 'patchInfoOldPatchSet');
- assert.equal(element._computePatchInfoClass('2', allPatchSets),
- 'patchInfoOldPatchSet');
- assert.equal(element._computePatchInfoClass('4', allPatchSets), '');
- });
-
- suite('editMode behavior', () => {
- setup(() => {
- element.diffPrefsDisabled = false;
- element.diffPrefs = {};
- });
-
- const isVisible = el => {
- assert.ok(el);
- return getComputedStyle(el).getPropertyValue('display') !== 'none';
- };
-
- test('patch specific elements', () => {
- element.editMode = true;
- sandbox.stub(element, 'computeLatestPatchNum').returns('2');
- flushAsynchronousOperations();
-
- assert.isFalse(isVisible(element.$.diffPrefsContainer));
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.descriptionContainer')));
-
- element.editMode = false;
- flushAsynchronousOperations();
-
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.descriptionContainer')));
- assert.isTrue(isVisible(element.$.diffPrefsContainer));
- });
-
- test('edit-controls visibility', () => {
- element.editMode = true;
- flushAsynchronousOperations();
- assert.isTrue(isVisible(element.$.editControls.parentElement));
-
- element.editMode = false;
- flushAsynchronousOperations();
- assert.isFalse(isVisible(element.$.editControls.parentElement));
- });
-
- test('_computeUploadHelpContainerClass', () => {
- // Only show the upload helper button when an unmerged change is viewed
- // by its owner.
- const accountA = {_account_id: 1};
- const accountB = {_account_id: 2};
- assert.notInclude(element._computeUploadHelpContainerClass(
- {owner: accountA}, accountA), 'hide');
- assert.include(element._computeUploadHelpContainerClass(
- {owner: accountA}, accountB), 'hide');
- });
+ teardown(done => {
+ flush(() => {
+ sandbox.restore();
+ done();
});
});
+
+ test('Diff preferences hidden when no prefs or diffPrefsDisabled', () => {
+ element.diffPrefsDisabled = true;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element.diffPrefsDisabled = false;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element.diffPrefsDisabled = true;
+ element.diffPrefs = {font_size: '12'};
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element.diffPrefsDisabled = false;
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.diffPrefsContainer.hidden);
+ });
+
+ test('_computeDescriptionReadOnly', () => {
+ assert.equal(element._computeDescriptionReadOnly(false,
+ {owner: {_account_id: 1}}, {_account_id: 1}), true);
+ assert.equal(element._computeDescriptionReadOnly(true,
+ {owner: {_account_id: 0}}, {_account_id: 1}), true);
+ assert.equal(element._computeDescriptionReadOnly(true,
+ {owner: {_account_id: 1}}, {_account_id: 1}), false);
+ });
+
+ test('_computeDescriptionPlaceholder', () => {
+ assert.equal(element._computeDescriptionPlaceholder(true),
+ 'No patchset description');
+ assert.equal(element._computeDescriptionPlaceholder(false),
+ 'Add patchset description');
+ });
+
+ test('description editing', () => {
+ const putDescStub = sandbox.stub(element.$.restAPI, 'setDescription')
+ .returns(Promise.resolve({ok: true}));
+
+ element.changeNum = '42';
+ element.basePatchNum = 'PARENT';
+ element.patchNum = 1;
+
+ element.change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1, description: 'test', commit: {commit: 'rev1'}},
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ actions: {},
+ owner: {_account_id: 1},
+ };
+ element.account = {_account_id: 1};
+ element.loggedIn = true;
+
+ flushAsynchronousOperations();
+
+ // The element has a description, so the account chip should be visible
+ // and the description label should not exist.
+ const chip = dom(element.root).querySelector('#descriptionChip');
+ let label = dom(element.root).querySelector('#descriptionLabel');
+
+ assert.equal(chip.text, 'test');
+ assert.isNotOk(label);
+
+ // Simulate tapping the remove button, but call function directly so that
+ // can determine what happens after the promise is resolved.
+ return element._handleDescriptionRemoved()
+ .then(() => {
+ // The API stub should be called with an empty string for the new
+ // description.
+ assert.equal(putDescStub.lastCall.args[2], '');
+ assert.equal(element.change.revisions.rev1.description, '');
+
+ flushAsynchronousOperations();
+ // The editable label should now be visible and the chip hidden.
+ label = dom(element.root).querySelector('#descriptionLabel');
+ assert.isOk(label);
+ assert.equal(getComputedStyle(chip).display, 'none');
+ assert.notEqual(getComputedStyle(label).display, 'none');
+ assert.isFalse(label.readOnly);
+ // Edit the label to have a new value of test2, and save.
+ label.editing = true;
+ label._inputText = 'test2';
+ label._save();
+ flushAsynchronousOperations();
+ // The API stub should be called with an `test2` for the new
+ // description.
+ assert.equal(putDescStub.callCount, 2);
+ assert.equal(putDescStub.lastCall.args[2], 'test2');
+ })
+ .then(() => {
+ flushAsynchronousOperations();
+ // The chip should be visible again, and the label hidden.
+ assert.equal(element.change.revisions.rev1.description, 'test2');
+ assert.equal(getComputedStyle(label).display, 'none');
+ assert.notEqual(getComputedStyle(chip).display, 'none');
+ });
+ });
+
+ test('expandAllDiffs called when expand button clicked', () => {
+ element.shownFileCount = 1;
+ flushAsynchronousOperations();
+ sandbox.stub(element, '_expandAllDiffs');
+ MockInteractions.tap(dom(element.root).querySelector(
+ '#expandBtn'));
+ assert.isTrue(element._expandAllDiffs.called);
+ });
+
+ test('collapseAllDiffs called when expand button clicked', () => {
+ element.shownFileCount = 1;
+ flushAsynchronousOperations();
+ sandbox.stub(element, '_collapseAllDiffs');
+ MockInteractions.tap(dom(element.root).querySelector(
+ '#collapseBtn'));
+ assert.isTrue(element._collapseAllDiffs.called);
+ });
+
+ test('show/hide diffs disabled for large amounts of files', done => {
+ const computeSpy = sandbox.spy(element, '_fileListActionsVisible');
+ element._files = [];
+ element.changeNum = '42';
+ element.basePatchNum = 'PARENT';
+ element.patchNum = '2';
+ element.shownFileCount = 1;
+ flush(() => {
+ assert.isTrue(computeSpy.lastCall.returnValue);
+ _.times(element._maxFilesForBulkActions + 1, () => {
+ element.shownFileCount = element.shownFileCount + 1;
+ });
+ assert.isFalse(computeSpy.lastCall.returnValue);
+ done();
+ });
+ });
+
+ test('fileViewActions are properly hidden', () => {
+ const actions = element.shadowRoot
+ .querySelector('.fileViewActions');
+ assert.equal(getComputedStyle(actions).display, 'none');
+ element.filesExpanded = GrFileListConstants.FilesExpandedState.SOME;
+ flushAsynchronousOperations();
+ assert.notEqual(getComputedStyle(actions).display, 'none');
+ element.filesExpanded = GrFileListConstants.FilesExpandedState.ALL;
+ flushAsynchronousOperations();
+ assert.notEqual(getComputedStyle(actions).display, 'none');
+ element.filesExpanded = GrFileListConstants.FilesExpandedState.NONE;
+ flushAsynchronousOperations();
+ assert.equal(getComputedStyle(actions).display, 'none');
+ });
+
+ test('expand/collapse buttons are toggled correctly', () => {
+ element.shownFileCount = 10;
+ flushAsynchronousOperations();
+ const expandBtn = element.shadowRoot.querySelector('#expandBtn');
+ const collapseBtn = element.shadowRoot.querySelector('#collapseBtn');
+ assert.notEqual(getComputedStyle(expandBtn).display, 'none');
+ assert.equal(getComputedStyle(collapseBtn).display, 'none');
+ element.filesExpanded = GrFileListConstants.FilesExpandedState.SOME;
+ flushAsynchronousOperations();
+ assert.notEqual(getComputedStyle(expandBtn).display, 'none');
+ assert.equal(getComputedStyle(collapseBtn).display, 'none');
+ element.filesExpanded = GrFileListConstants.FilesExpandedState.ALL;
+ flushAsynchronousOperations();
+ assert.equal(getComputedStyle(expandBtn).display, 'none');
+ assert.notEqual(getComputedStyle(collapseBtn).display, 'none');
+ element.filesExpanded = GrFileListConstants.FilesExpandedState.NONE;
+ flushAsynchronousOperations();
+ assert.notEqual(getComputedStyle(expandBtn).display, 'none');
+ assert.equal(getComputedStyle(collapseBtn).display, 'none');
+ });
+
+ test('navigateToChange called when range select changes', () => {
+ const navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ element.change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev2: {_number: 2},
+ rev1: {_number: 1},
+ rev13: {_number: 13},
+ rev3: {_number: 3},
+ },
+ status: 'NEW',
+ labels: {},
+ };
+ element.basePatchNum = 1;
+ element.patchNum = 2;
+
+ element._handlePatchChange({detail: {basePatchNum: 1, patchNum: 3}});
+ assert.equal(navigateToChangeStub.callCount, 1);
+ assert.isTrue(navigateToChangeStub.lastCall
+ .calledWithExactly(element.change, 3, 1));
+ });
+
+ test('class is applied to file list on old patch set', () => {
+ const allPatchSets = [{num: 4}, {num: 2}, {num: 1}];
+ assert.equal(element._computePatchInfoClass('1', allPatchSets),
+ 'patchInfoOldPatchSet');
+ assert.equal(element._computePatchInfoClass('2', allPatchSets),
+ 'patchInfoOldPatchSet');
+ assert.equal(element._computePatchInfoClass('4', allPatchSets), '');
+ });
+
+ suite('editMode behavior', () => {
+ setup(() => {
+ element.diffPrefsDisabled = false;
+ element.diffPrefs = {};
+ });
+
+ const isVisible = el => {
+ assert.ok(el);
+ return getComputedStyle(el).getPropertyValue('display') !== 'none';
+ };
+
+ test('patch specific elements', () => {
+ element.editMode = true;
+ sandbox.stub(element, 'computeLatestPatchNum').returns('2');
+ flushAsynchronousOperations();
+
+ assert.isFalse(isVisible(element.$.diffPrefsContainer));
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.descriptionContainer')));
+
+ element.editMode = false;
+ flushAsynchronousOperations();
+
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.descriptionContainer')));
+ assert.isTrue(isVisible(element.$.diffPrefsContainer));
+ });
+
+ test('edit-controls visibility', () => {
+ element.editMode = true;
+ flushAsynchronousOperations();
+ assert.isTrue(isVisible(element.$.editControls.parentElement));
+
+ element.editMode = false;
+ flushAsynchronousOperations();
+ assert.isFalse(isVisible(element.$.editControls.parentElement));
+ });
+
+ test('_computeUploadHelpContainerClass', () => {
+ // Only show the upload helper button when an unmerged change is viewed
+ // by its owner.
+ const accountA = {_account_id: 1};
+ const accountB = {_account_id: 2};
+ assert.notInclude(element._computeUploadHelpContainerClass(
+ {owner: accountA}, accountA), 'hide');
+ assert.include(element._computeUploadHelpContainerClass(
+ {owner: accountA}, accountB), 'hide');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
deleted file mode 100644
index 289e3f4..0000000
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ /dev/null
@@ -1,554 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/async-foreach-behavior/async-foreach-behavior.html">
-<link rel="import" href="../../../behaviors/dom-util-behavior/dom-util-behavior.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../diff/gr-diff-cursor/gr-diff-cursor.html">
-<link rel="import" href="../../diff/gr-diff-host/gr-diff-host.html">
-<link rel="import" href="../../diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html">
-<link rel="import" href="../../edit/gr-edit-file-controls/gr-edit-file-controls.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-<link rel="import" href="../../shared/gr-count-string-formatter/gr-count-string-formatter.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
-<link rel="import" href="../gr-file-list-constants.html">
-
-<dom-module id="gr-file-list">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- }
- .row {
- align-items: center;
- border-top: 1px solid var(--border-color);
- display: flex;
- min-height: calc(var(--line-height-normal) + 2*var(--spacing-s));
- padding: var(--spacing-xs) var(--spacing-l) var(--spacing-xs) calc(var(--spacing-l) - .35rem);
- }
- :host(.loading) .row {
- opacity: .5;
- };
- :host(.editMode) .hideOnEdit {
- display: none;
- }
- .showOnEdit {
- display: none;
- }
- :host(.editMode) .showOnEdit {
- display: initial;
- }
- .invisible {
- visibility: hidden;
- }
- .header-row {
- background-color: var(--background-color-secondary);
- }
- .controlRow {
- align-items: center;
- display: flex;
- height: 2.25em;
- justify-content: center;
- }
- .controlRow.invisible,
- .show-hide.invisible {
- display: none;
- }
- .reviewed,
- .status {
- align-items: center;
- display: inline-flex;
- }
- .reviewed,
- .status {
- display: inline-block;
- text-align: left;
- width: 1.5em;
- }
- .file-row {
- cursor: pointer;
- }
- .file-row.expanded {
- border-bottom: 1px solid var(--border-color);
- position: -webkit-sticky;
- position: sticky;
- top: 0;
- /* Has to visible above the diff view, and by default has a lower
- z-index. setting to 1 places it directly above. */
- z-index: 1;
- }
- .file-row:hover {
- background-color: var(--hover-background-color);
- }
- .file-row.selected {
- background-color: var(--selection-background-color);
- }
- .file-row.expanded,
- .file-row.expanded:hover {
- background-color: var(--expanded-background-color);
- }
- .path {
- cursor: pointer;
- flex: 1;
- /* Wrap it into multiple lines if too long. */
- white-space: normal;
- word-break: break-word;
- }
- .oldPath {
- color: var(--deemphasized-text-color);
- }
- .header-stats {
- text-align: center;
- min-width: 7.5em;
- }
- .stats {
- text-align: right;
- min-width: 7.5em;
- }
- .comments {
- padding-left: var(--spacing-l);
- min-width: 7.5em;
- }
- .row:not(.header-row) .stats,
- .total-stats {
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-mono);
- line-height: var(--line-height-mono);
- display: flex;
- }
- .sizeBars {
- margin-left: var(--spacing-m);
- min-width: 7em;
- text-align: center;
- }
- .sizeBars.hide {
- display: none;
- }
- .added,
- .removed {
- display: inline-block;
- min-width: 3.5em;
- }
- .added {
- color: var(--vote-text-color-recommended);
- }
- .removed {
- color: var(--vote-text-color-disliked);
- text-align: left;
- min-width: 4em;
- padding-left: var(--spacing-s);
- }
- .drafts {
- color: #C62828;
- font-weight: var(--font-weight-bold);
- }
- .show-hide {
- margin-left: var(--spacing-s);
- width: 1.9em;
- }
- .fileListButton {
- margin: var(--spacing-m);
- }
- .totalChanges {
- justify-content: flex-end;
- text-align: right;
- }
- .warning {
- color: var(--deemphasized-text-color);
- }
- input.show-hide {
- display: none;
- }
- label.show-hide {
- cursor: pointer;
- display: block;
- min-width: 2em;
- }
- gr-diff {
- display: block;
- overflow-x: auto;
- }
- .truncatedFileName {
- display: none;
- }
- .mobile {
- display: none;
- }
- .reviewed {
- margin-left: var(--spacing-xxl);
- width: 15em;
- }
- .reviewed label {
- color: var(--link-color);
- opacity: 0;
- justify-content: flex-end;
- width: 100%;
- }
- .reviewed label:hover {
- cursor: pointer;
- opacity: 100;
- }
- .row:focus {
- outline: none;
- }
- .row:hover .reviewed label,
- .row:focus .reviewed label,
- .row.expanded .reviewed label {
- opacity: 100;
- }
- .reviewed input {
- display: none;
- }
- .reviewedLabel {
- color: var(--deemphasized-text-color);
- margin-right: var(--spacing-l);
- opacity: 0;
- }
- .reviewedLabel.isReviewed {
- display: initial;
- opacity: 100;
- }
- .editFileControls {
- width: 7em;
- }
- .markReviewed,
- .pathLink {
- display: inline-block;
- margin: -2px 0;
- padding: var(--spacing-s) 0;
- text-decoration: none;
- }
- .pathLink:hover {
- text-decoration: underline;
- }
-
- /** copy on file path **/
- .pathLink gr-copy-clipboard,
- .oldPath gr-copy-clipboard {
- display: inline-block;
- visibility: hidden;
- vertical-align: bottom;
- text-decoration: none;
- --gr-button: {
- padding: 0px;
- }
- }
- .pathLink:hover gr-copy-clipboard,
- .oldPath:hover gr-copy-clipboard {
- visibility: visible;
- }
-
- /** small screen breakpoint: 768px */
- @media screen and (max-width: 55em) {
- .desktop {
- display: none;
- }
- .mobile {
- display: block;
- }
- .row.selected {
- background-color: var(--view-background-color);
- }
- .stats {
- display: none;
- }
- .reviewed,
- .status {
- justify-content: flex-start;
- }
- .reviewed {
- display: none;
- }
- .comments {
- min-width: initial;
- }
- .expanded .fullFileName,
- .truncatedFileName {
- display: inline;
- }
- .expanded .truncatedFileName,
- .fullFileName {
- display: none;
- }
- }
- </style>
- <div
- id="container"
- on-click="_handleFileListClick">
- <div class="header-row row">
- <div class="status"></div>
- <div class="path">File</div>
- <div class="comments">Comments</div>
- <div class="sizeBars">Size</div>
- <div class="header-stats">Delta</div>
- <template is="dom-if" if="[[_showDynamicColumns]]">
- <template is="dom-repeat" items="[[_dynamicHeaderEndpoints]]" as="headerEndpoint">
- <gr-endpoint-decorator name$="[[headerEndpoint]]">
- </gr-endpoint-decorator>
- </template>
- </template>
- <!-- Empty div here exists to keep spacing in sync with file rows. -->
- <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]"></div>
- <div class="editFileControls showOnEdit"></div>
- <div class="show-hide"></div>
- </div>
-
- <template is="dom-repeat"
- items="[[_shownFiles]]"
- id="files"
- as="file"
- initial-count="[[fileListIncrement]]"
- target-framerate="1">
- [[_reportRenderedRow(index)]]
- <div class="stickyArea">
- <div class$="file-row row [[_computePathClass(file.__path, _expandedFilePaths.*)]]"
- data-path$="[[file.__path]]" tabindex="-1">
- <div class$="[[_computeClass('status', file.__path)]]"
- tabindex="0"
- title$="[[_computeFileStatusLabel(file.status)]]"
- aria-label$="[[_computeFileStatusLabel(file.status)]]">
- [[_computeFileStatus(file.status)]]
- </div>
- <!-- TODO: Remove data-url as it appears its not used -->
- <span
- data-url="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]"
- class="path">
- <a class="pathLink" href$="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]">
- <span title$="[[computeDisplayPath(file.__path)]]"
- class="fullFileName">
- [[computeDisplayPath(file.__path)]]
- </span>
- <span title$="[[computeDisplayPath(file.__path)]]"
- class="truncatedFileName">
- [[computeTruncatedPath(file.__path)]]
- </span>
- <gr-copy-clipboard
- hide-input
- text="[[file.__path]]"></gr-copy-clipboard>
- </a>
- <template is="dom-if" if="[[file.old_path]]">
- <div class="oldPath" title$="[[file.old_path]]">
- [[file.old_path]]
- <gr-copy-clipboard
- hide-input
- text="[[file.old_path]]"></gr-copy-clipboard>
- </div>
- </template>
- </span>
- <div class="comments desktop">
- <span class="drafts">
- [[_computeDraftsString(changeComments, patchRange, file.__path)]]
- </span>
- [[_computeCommentsString(changeComments, patchRange, file.__path)]]
- </div>
- <div class="comments mobile">
- <span class="drafts">
- [[_computeDraftsStringMobile(changeComments, patchRange,
- file.__path)]]
- </span>
- [[_computeCommentsStringMobile(changeComments, patchRange,
- file.__path)]]
- </div>
- <div class$="[[_computeSizeBarsClass(_showSizeBars, file.__path)]]">
- <svg width="61" height="8">
- <rect
- x$="[[_computeBarAdditionX(file, _sizeBarLayout)]]"
- y="0"
- height="8"
- fill="#388E3C"
- width$="[[_computeBarAdditionWidth(file, _sizeBarLayout)]]" />
- <rect
- x$="[[_computeBarDeletionX(_sizeBarLayout)]]"
- y="0"
- height="8"
- fill="#D32F2F"
- width$="[[_computeBarDeletionWidth(file, _sizeBarLayout)]]" />
- </svg>
- </div>
- <div class$="[[_computeClass('stats', file.__path)]]">
- <span
- class="added"
- tabindex="0"
- aria-label$="[[file.lines_inserted]] lines added"
- hidden$=[[file.binary]]>
- +[[file.lines_inserted]]
- </span>
- <span
- class="removed"
- tabindex="0"
- aria-label$="[[file.lines_deleted]] lines removed"
- hidden$=[[file.binary]]>
- -[[file.lines_deleted]]
- </span>
- <span class$="[[_computeBinaryClass(file.size_delta)]]"
- hidden$=[[!file.binary]]>
- [[_formatBytes(file.size_delta)]]
- [[_formatPercentage(file.size, file.size_delta)]]
- </span>
- </div>
- <template is="dom-if" if="[[_showDynamicColumns]]">
- <template is="dom-repeat" items="[[_dynamicContentEndpoints]]" as="contentEndpoint">
- <div class$="[[_computeClass('', file.__path)]]">
- <gr-endpoint-decorator name="[[contentEndpoint]]">
- <gr-endpoint-param name="changeNum" value="[[changeNum]]">
- </gr-endpoint-param>
- <gr-endpoint-param name="patchRange" value="[[patchRange]]">
- </gr-endpoint-param>
- <gr-endpoint-param name="path" value="[[file.__path]]">
- </gr-endpoint-param>
- </gr-endpoint-decorator>
- </div>
- </template>
- </template>
- <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]" hidden>
- <span class$="reviewedLabel [[_computeReviewedClass(file.isReviewed)]]">Reviewed</span>
- <label>
- <input class="reviewed" type="checkbox" checked="[[file.isReviewed]]">
- <span class="markReviewed" title$="[[_reviewedTitle(file.isReviewed)]]">[[_computeReviewedText(file.isReviewed)]]</span>
- </label>
- </div>
- <div class="editFileControls showOnEdit">
- <template is="dom-if" if="[[editMode]]">
- <gr-edit-file-controls
- class$="[[_computeClass('', file.__path)]]"
- file-path="[[file.__path]]"></gr-edit-file-controls>
- </template>
- </div>
- <div class="show-hide">
- <label class="show-hide" data-path$="[[file.__path]]"
- data-expand=true>
- <input type="checkbox" class="show-hide"
- checked$="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]"
- data-path$="[[file.__path]]" data-expand=true>
- <iron-icon
- id="icon"
- icon="[[_computeShowHideIcon(file.__path, _expandedFilePaths.*)]]">
- </iron-icon>
- </label>
- </div>
- </div>
- <template is="dom-if"
- if="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]">
- <gr-diff-host
- no-auto-render
- show-load-failure
- display-line="[[_displayLine]]"
- hidden="[[!_isFileExpanded(file.__path, _expandedFilePaths.*)]]"
- change-num="[[changeNum]]"
- patch-range="[[patchRange]]"
- path="[[file.__path]]"
- prefs="[[diffPrefs]]"
- project-name="[[change.project]]"
- on-line-selected="_onLineSelected"
- no-render-on-prefs-change
- view-mode="[[diffViewMode]]"></gr-diff-host>
- </template>
- </div>
- </template>
- </div>
- <div
- class="row totalChanges"
- hidden$="[[_hideChangeTotals]]">
- <div class="total-stats">
- <span
- class="added"
- tabindex="0"
- aria-label$="[[_patchChange.inserted]] lines added">
- +[[_patchChange.inserted]]
- </span>
- <span
- class="removed"
- tabindex="0"
- aria-label$="[[_patchChange.deleted]] lines removed">
- -[[_patchChange.deleted]]
- </span>
- </div>
- <template is="dom-if" if="[[_showDynamicColumns]]">
- <template is="dom-repeat" items="[[_dynamicSummaryEndpoints]]" as="summaryEndpoint">
- <gr-endpoint-decorator name="[[summaryEndpoint]]">
- </gr-endpoint-decorator>
- </template>
- </template>
- <!-- Empty div here exists to keep spacing in sync with file rows. -->
- <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]"></div>
- <div class="editFileControls showOnEdit"></div>
- <div class="show-hide"></div>
- </div>
- <div
- class="row totalChanges"
- hidden$="[[_hideBinaryChangeTotals]]">
- <div class="total-stats">
- <span class="added" aria-label="Total lines added">
- [[_formatBytes(_patchChange.size_delta_inserted)]]
- [[_formatPercentage(_patchChange.total_size,
- _patchChange.size_delta_inserted)]]
- </span>
- <span class="removed" aria-label="Total lines removed">
- [[_formatBytes(_patchChange.size_delta_deleted)]]
- [[_formatPercentage(_patchChange.total_size,
- _patchChange.size_delta_deleted)]]
- </span>
- </div>
- </div>
- <div class$="row controlRow [[_computeFileListControlClass(numFilesShown, _files)]]">
- <gr-button
- class="fileListButton"
- id="incrementButton"
- link on-click="_incrementNumFilesShown">
- [[_computeIncrementText(numFilesShown, _files)]]
- </gr-button>
- <gr-tooltip-content
- has-tooltip="[[_computeWarnShowAll(_files)]]"
- show-icon="[[_computeWarnShowAll(_files)]]"
- title$="[[_computeShowAllWarning(_files)]]">
- <gr-button
- class="fileListButton"
- id="showAllButton"
- link on-click="_showAllFiles">
- [[_computeShowAllText(_files)]]
- </gr-button><!--
- --></gr-tooltip-content>
- </div>
- <gr-diff-preferences-dialog
- id="diffPreferencesDialog"
- diff-prefs="{{diffPrefs}}"
- on-reload-diff-preference="_handleReloadingDiffPreference">
- </gr-diff-preferences-dialog>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- <gr-storage id="storage"></gr-storage>
- <gr-diff-cursor id="diffCursor"></gr-diff-cursor>
- <gr-cursor-manager
- id="fileCursor"
- scroll-behavior="keep-visible"
- focus-on-move
- cursor-target-class="selected"></gr-cursor-manager>
- <gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="gr-file-list.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index d2f11c2..6b8e971 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,1359 +14,1416 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- // Maximum length for patch set descriptions.
- const PATCH_DESC_MAX_LENGTH = 500;
- const WARN_SHOW_ALL_THRESHOLD = 1000;
- const LOADING_DEBOUNCE_INTERVAL = 100;
+import '../../../behaviors/async-foreach-behavior/async-foreach-behavior.js';
+import '../../../behaviors/dom-util-behavior/dom-util-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../diff/gr-diff-cursor/gr-diff-cursor.js';
+import '../../diff/gr-diff-host/gr-diff-host.js';
+import '../../diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js';
+import '../../edit/gr-edit-file-controls/gr-edit-file-controls.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-cursor-manager/gr-cursor-manager.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-linked-text/gr-linked-text.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.js';
+import '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
+import '../../shared/gr-tooltip-content/gr-tooltip-content.js';
+import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
+import '../gr-file-list-constants.js';
+import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-file-list_html.js';
- const SIZE_BAR_MAX_WIDTH = 61;
- const SIZE_BAR_GAP_WIDTH = 1;
- const SIZE_BAR_MIN_WIDTH = 1.5;
+// Maximum length for patch set descriptions.
+const PATCH_DESC_MAX_LENGTH = 500;
+const WARN_SHOW_ALL_THRESHOLD = 1000;
+const LOADING_DEBOUNCE_INTERVAL = 100;
- const RENDER_TIMING_LABEL = 'FileListRenderTime';
- const RENDER_AVG_TIMING_LABEL = 'FileListRenderTimePerFile';
- const EXPAND_ALL_TIMING_LABEL = 'ExpandAllDiffs';
- const EXPAND_ALL_AVG_TIMING_LABEL = 'ExpandAllPerDiff';
+const SIZE_BAR_MAX_WIDTH = 61;
+const SIZE_BAR_GAP_WIDTH = 1;
+const SIZE_BAR_MIN_WIDTH = 1.5;
- const FileStatus = {
- A: 'Added',
- C: 'Copied',
- D: 'Deleted',
- M: 'Modified',
- R: 'Renamed',
- W: 'Rewritten',
- U: 'Unchanged',
- };
+const RENDER_TIMING_LABEL = 'FileListRenderTime';
+const RENDER_AVG_TIMING_LABEL = 'FileListRenderTimePerFile';
+const EXPAND_ALL_TIMING_LABEL = 'ExpandAllDiffs';
+const EXPAND_ALL_AVG_TIMING_LABEL = 'ExpandAllPerDiff';
+
+const FileStatus = {
+ A: 'Added',
+ C: 'Copied',
+ D: 'Deleted',
+ M: 'Modified',
+ R: 'Renamed',
+ W: 'Rewritten',
+ U: 'Unchanged',
+};
+
+/**
+ * @appliesMixin Gerrit.AsyncForeachMixin
+ * @appliesMixin Gerrit.DomUtilMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.PathListMixin
+ * @extends Polymer.Element
+ */
+class GrFileList extends mixinBehaviors( [
+ Gerrit.AsyncForeachBehavior,
+ Gerrit.DomUtilBehavior,
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.PathListBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-file-list'; }
+ /**
+ * Fired when a draft refresh should get triggered
+ *
+ * @event reload-drafts
+ */
+
+ static get properties() {
+ return {
+ /** @type {?} */
+ patchRange: Object,
+ patchNum: String,
+ changeNum: String,
+ /** @type {?} */
+ changeComments: Object,
+ drafts: Object,
+ revisions: Array,
+ projectConfig: Object,
+ selectedIndex: {
+ type: Number,
+ notify: true,
+ },
+ keyEventTarget: {
+ type: Object,
+ value() { return document.body; },
+ },
+ /** @type {?} */
+ change: Object,
+ diffViewMode: {
+ type: String,
+ notify: true,
+ observer: '_updateDiffPreferences',
+ },
+ editMode: {
+ type: Boolean,
+ observer: '_editModeChanged',
+ },
+ filesExpanded: {
+ type: String,
+ value: GrFileListConstants.FilesExpandedState.NONE,
+ notify: true,
+ },
+ _filesByPath: Object,
+ _files: {
+ type: Array,
+ observer: '_filesChanged',
+ value() { return []; },
+ },
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+ _reviewed: {
+ type: Array,
+ value() { return []; },
+ },
+ diffPrefs: {
+ type: Object,
+ notify: true,
+ observer: '_updateDiffPreferences',
+ },
+ /** @type {?} */
+ _userPrefs: Object,
+ _showInlineDiffs: Boolean,
+ numFilesShown: {
+ type: Number,
+ notify: true,
+ },
+ /** @type {?} */
+ _patchChange: {
+ type: Object,
+ computed: '_calculatePatchChange(_files)',
+ },
+ fileListIncrement: Number,
+ _hideChangeTotals: {
+ type: Boolean,
+ computed: '_shouldHideChangeTotals(_patchChange)',
+ },
+ _hideBinaryChangeTotals: {
+ type: Boolean,
+ computed: '_shouldHideBinaryChangeTotals(_patchChange)',
+ },
+
+ _shownFiles: {
+ type: Array,
+ computed: '_computeFilesShown(numFilesShown, _files)',
+ },
+
+ /**
+ * The amount of files added to the shown files list the last time it was
+ * updated. This is used for reporting the average render time.
+ */
+ _reportinShownFilesIncrement: Number,
+
+ _expandedFilePaths: {
+ type: Array,
+ value() { return []; },
+ },
+ _displayLine: Boolean,
+ _loading: {
+ type: Boolean,
+ observer: '_loadingChanged',
+ },
+ /** @type {Gerrit.LayoutStats|undefined} */
+ _sizeBarLayout: {
+ type: Object,
+ computed: '_computeSizeBarLayout(_shownFiles.*)',
+ },
+
+ _showSizeBars: {
+ type: Boolean,
+ value: true,
+ computed: '_computeShowSizeBars(_userPrefs)',
+ },
+
+ /** @type {Function} */
+ _cancelForEachDiff: Function,
+
+ _showDynamicColumns: {
+ type: Boolean,
+ computed: '_computeShowDynamicColumns(_dynamicHeaderEndpoints, ' +
+ '_dynamicContentEndpoints, _dynamicSummaryEndpoints)',
+ },
+ /** @type {Array<string>} */
+ _dynamicHeaderEndpoints: {
+ type: Array,
+ },
+ /** @type {Array<string>} */
+ _dynamicContentEndpoints: {
+ type: Array,
+ },
+ /** @type {Array<string>} */
+ _dynamicSummaryEndpoints: {
+ type: Array,
+ },
+ };
+ }
+
+ static get observers() {
+ return [
+ '_expandedPathsChanged(_expandedFilePaths.splices)',
+ '_computeFiles(_filesByPath, changeComments, patchRange, _reviewed, ' +
+ '_loading)',
+ ];
+ }
+
+ get keyBindings() {
+ return {
+ esc: '_handleEscKey',
+ };
+ }
+
+ keyboardShortcuts() {
+ return {
+ [this.Shortcut.LEFT_PANE]: '_handleLeftPane',
+ [this.Shortcut.RIGHT_PANE]: '_handleRightPane',
+ [this.Shortcut.TOGGLE_INLINE_DIFF]: '_handleToggleInlineDiff',
+ [this.Shortcut.TOGGLE_ALL_INLINE_DIFFS]: '_handleToggleAllInlineDiffs',
+ [this.Shortcut.CURSOR_NEXT_FILE]: '_handleCursorNext',
+ [this.Shortcut.CURSOR_PREV_FILE]: '_handleCursorPrev',
+ [this.Shortcut.NEXT_LINE]: '_handleCursorNext',
+ [this.Shortcut.PREV_LINE]: '_handleCursorPrev',
+ [this.Shortcut.NEW_COMMENT]: '_handleNewComment',
+ [this.Shortcut.OPEN_LAST_FILE]: '_handleOpenLastFile',
+ [this.Shortcut.OPEN_FIRST_FILE]: '_handleOpenFirstFile',
+ [this.Shortcut.OPEN_FILE]: '_handleOpenFile',
+ [this.Shortcut.NEXT_CHUNK]: '_handleNextChunk',
+ [this.Shortcut.PREV_CHUNK]: '_handlePrevChunk',
+ [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
+ [this.Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane',
+
+ // Final two are actually handled by gr-comment-thread.
+ [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
+ [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
+ };
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('keydown',
+ e => this._scopedKeydownHandler(e));
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ Gerrit.awaitPluginsLoaded().then(() => {
+ this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints(
+ 'change-view-file-list-header');
+ this._dynamicContentEndpoints = Gerrit._endpoints.getDynamicEndpoints(
+ 'change-view-file-list-content');
+ this._dynamicSummaryEndpoints = Gerrit._endpoints.getDynamicEndpoints(
+ 'change-view-file-list-summary');
+
+ if (this._dynamicHeaderEndpoints.length !==
+ this._dynamicContentEndpoints.length) {
+ console.warn(
+ 'Different number of dynamic file-list header and content.');
+ }
+ if (this._dynamicHeaderEndpoints.length !==
+ this._dynamicSummaryEndpoints.length) {
+ console.warn(
+ 'Different number of dynamic file-list headers and summary.');
+ }
+ });
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this._cancelDiffs();
+ }
/**
- * @appliesMixin Gerrit.AsyncForeachMixin
- * @appliesMixin Gerrit.DomUtilMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.PathListMixin
- * @extends Polymer.Element
+ * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
+ * events must be scoped to a component level (e.g. `enter`) in order to not
+ * override native browser functionality.
+ *
+ * Context: Issue 7277
*/
- class GrFileList extends Polymer.mixinBehaviors( [
- Gerrit.AsyncForeachBehavior,
- Gerrit.DomUtilBehavior,
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.PathListBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-file-list'; }
- /**
- * Fired when a draft refresh should get triggered
- *
- * @event reload-drafts
- */
-
- static get properties() {
- return {
- /** @type {?} */
- patchRange: Object,
- patchNum: String,
- changeNum: String,
- /** @type {?} */
- changeComments: Object,
- drafts: Object,
- revisions: Array,
- projectConfig: Object,
- selectedIndex: {
- type: Number,
- notify: true,
- },
- keyEventTarget: {
- type: Object,
- value() { return document.body; },
- },
- /** @type {?} */
- change: Object,
- diffViewMode: {
- type: String,
- notify: true,
- observer: '_updateDiffPreferences',
- },
- editMode: {
- type: Boolean,
- observer: '_editModeChanged',
- },
- filesExpanded: {
- type: String,
- value: GrFileListConstants.FilesExpandedState.NONE,
- notify: true,
- },
- _filesByPath: Object,
- _files: {
- type: Array,
- observer: '_filesChanged',
- value() { return []; },
- },
- _loggedIn: {
- type: Boolean,
- value: false,
- },
- _reviewed: {
- type: Array,
- value() { return []; },
- },
- diffPrefs: {
- type: Object,
- notify: true,
- observer: '_updateDiffPreferences',
- },
- /** @type {?} */
- _userPrefs: Object,
- _showInlineDiffs: Boolean,
- numFilesShown: {
- type: Number,
- notify: true,
- },
- /** @type {?} */
- _patchChange: {
- type: Object,
- computed: '_calculatePatchChange(_files)',
- },
- fileListIncrement: Number,
- _hideChangeTotals: {
- type: Boolean,
- computed: '_shouldHideChangeTotals(_patchChange)',
- },
- _hideBinaryChangeTotals: {
- type: Boolean,
- computed: '_shouldHideBinaryChangeTotals(_patchChange)',
- },
-
- _shownFiles: {
- type: Array,
- computed: '_computeFilesShown(numFilesShown, _files)',
- },
-
- /**
- * The amount of files added to the shown files list the last time it was
- * updated. This is used for reporting the average render time.
- */
- _reportinShownFilesIncrement: Number,
-
- _expandedFilePaths: {
- type: Array,
- value() { return []; },
- },
- _displayLine: Boolean,
- _loading: {
- type: Boolean,
- observer: '_loadingChanged',
- },
- /** @type {Gerrit.LayoutStats|undefined} */
- _sizeBarLayout: {
- type: Object,
- computed: '_computeSizeBarLayout(_shownFiles.*)',
- },
-
- _showSizeBars: {
- type: Boolean,
- value: true,
- computed: '_computeShowSizeBars(_userPrefs)',
- },
-
- /** @type {Function} */
- _cancelForEachDiff: Function,
-
- _showDynamicColumns: {
- type: Boolean,
- computed: '_computeShowDynamicColumns(_dynamicHeaderEndpoints, ' +
- '_dynamicContentEndpoints, _dynamicSummaryEndpoints)',
- },
- /** @type {Array<string>} */
- _dynamicHeaderEndpoints: {
- type: Array,
- },
- /** @type {Array<string>} */
- _dynamicContentEndpoints: {
- type: Array,
- },
- /** @type {Array<string>} */
- _dynamicSummaryEndpoints: {
- type: Array,
- },
- };
- }
-
- static get observers() {
- return [
- '_expandedPathsChanged(_expandedFilePaths.splices)',
- '_computeFiles(_filesByPath, changeComments, patchRange, _reviewed, ' +
- '_loading)',
- ];
- }
-
- get keyBindings() {
- return {
- esc: '_handleEscKey',
- };
- }
-
- keyboardShortcuts() {
- return {
- [this.Shortcut.LEFT_PANE]: '_handleLeftPane',
- [this.Shortcut.RIGHT_PANE]: '_handleRightPane',
- [this.Shortcut.TOGGLE_INLINE_DIFF]: '_handleToggleInlineDiff',
- [this.Shortcut.TOGGLE_ALL_INLINE_DIFFS]: '_handleToggleAllInlineDiffs',
- [this.Shortcut.CURSOR_NEXT_FILE]: '_handleCursorNext',
- [this.Shortcut.CURSOR_PREV_FILE]: '_handleCursorPrev',
- [this.Shortcut.NEXT_LINE]: '_handleCursorNext',
- [this.Shortcut.PREV_LINE]: '_handleCursorPrev',
- [this.Shortcut.NEW_COMMENT]: '_handleNewComment',
- [this.Shortcut.OPEN_LAST_FILE]: '_handleOpenLastFile',
- [this.Shortcut.OPEN_FIRST_FILE]: '_handleOpenFirstFile',
- [this.Shortcut.OPEN_FILE]: '_handleOpenFile',
- [this.Shortcut.NEXT_CHUNK]: '_handleNextChunk',
- [this.Shortcut.PREV_CHUNK]: '_handlePrevChunk',
- [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
- [this.Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane',
-
- // Final two are actually handled by gr-comment-thread.
- [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
- [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
- };
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('keydown',
- e => this._scopedKeydownHandler(e));
- }
-
- /** @override */
- attached() {
- super.attached();
- Gerrit.awaitPluginsLoaded().then(() => {
- this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints(
- 'change-view-file-list-header');
- this._dynamicContentEndpoints = Gerrit._endpoints.getDynamicEndpoints(
- 'change-view-file-list-content');
- this._dynamicSummaryEndpoints = Gerrit._endpoints.getDynamicEndpoints(
- 'change-view-file-list-summary');
-
- if (this._dynamicHeaderEndpoints.length !==
- this._dynamicContentEndpoints.length) {
- console.warn(
- 'Different number of dynamic file-list header and content.');
- }
- if (this._dynamicHeaderEndpoints.length !==
- this._dynamicSummaryEndpoints.length) {
- console.warn(
- 'Different number of dynamic file-list headers and summary.');
- }
- });
- }
-
- /** @override */
- detached() {
- super.detached();
- this._cancelDiffs();
- }
-
- /**
- * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
- * events must be scoped to a component level (e.g. `enter`) in order to not
- * override native browser functionality.
- *
- * Context: Issue 7277
- */
- _scopedKeydownHandler(e) {
- if (e.keyCode === 13) {
- // Enter.
- this._handleOpenFile(e);
- }
- }
-
- reload() {
- if (!this.changeNum || !this.patchRange.patchNum) {
- return Promise.resolve();
- }
-
- this._loading = true;
-
- this.collapseAllDiffs();
- const promises = [];
-
- promises.push(this._getFiles().then(filesByPath => {
- this._filesByPath = filesByPath;
- }));
- promises.push(this._getLoggedIn()
- .then(loggedIn => this._loggedIn = loggedIn)
- .then(loggedIn => {
- if (!loggedIn) { return; }
-
- return this._getReviewedFiles().then(reviewed => {
- this._reviewed = reviewed;
- });
- }));
-
- promises.push(this._getDiffPreferences().then(prefs => {
- this.diffPrefs = prefs;
- }));
-
- promises.push(this._getPreferences().then(prefs => {
- this._userPrefs = prefs;
- }));
-
- return Promise.all(promises).then(() => {
- this._loading = false;
- this._detectChromiteButler();
- this.$.reporting.fileListDisplayed();
- });
- }
-
- _detectChromiteButler() {
- const hasButler = !!document.getElementById('butler-suggested-owners');
- if (hasButler) {
- this.$.reporting.reportExtension('butler');
- }
- }
-
- get diffs() {
- const diffs = Polymer.dom(this.root).querySelectorAll('gr-diff-host');
- // It is possible that a bogus diff element is hanging around invisibly
- // from earlier with a different patch set choice and associated with a
- // different entry in the files array. So filter on visible items only.
- return Array.from(diffs).filter(
- el => !!el && !!el.style && el.style.display !== 'none');
- }
-
- openDiffPrefs() {
- this.$.diffPreferencesDialog.open();
- }
-
- _calculatePatchChange(files) {
- const magicFilesExcluded = files.filter(files =>
- !this.isMagicPath(files.__path)
- );
-
- return magicFilesExcluded.reduce((acc, obj) => {
- const inserted = obj.lines_inserted ? obj.lines_inserted : 0;
- const deleted = obj.lines_deleted ? obj.lines_deleted : 0;
- const total_size = (obj.size && obj.binary) ? obj.size : 0;
- const size_delta_inserted =
- obj.binary && obj.size_delta > 0 ? obj.size_delta : 0;
- const size_delta_deleted =
- obj.binary && obj.size_delta < 0 ? obj.size_delta : 0;
-
- return {
- inserted: acc.inserted + inserted,
- deleted: acc.deleted + deleted,
- size_delta_inserted: acc.size_delta_inserted + size_delta_inserted,
- size_delta_deleted: acc.size_delta_deleted + size_delta_deleted,
- total_size: acc.total_size + total_size,
- };
- }, {inserted: 0, deleted: 0, size_delta_inserted: 0,
- size_delta_deleted: 0, total_size: 0});
- }
-
- _getDiffPreferences() {
- return this.$.restAPI.getDiffPreferences();
- }
-
- _getPreferences() {
- return this.$.restAPI.getPreferences();
- }
-
- _togglePathExpanded(path) {
- // Is the path in the list of expanded diffs? IF so remove it, otherwise
- // add it to the list.
- const pathIndex = this._expandedFilePaths.indexOf(path);
- if (pathIndex === -1) {
- this.push('_expandedFilePaths', path);
- } else {
- this.splice('_expandedFilePaths', pathIndex, 1);
- }
- }
-
- _togglePathExpandedByIndex(index) {
- this._togglePathExpanded(this._files[index].__path);
- }
-
- _updateDiffPreferences() {
- if (!this.diffs.length) { return; }
- // Re-render all expanded diffs sequentially.
- this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
- this._renderInOrder(this._expandedFilePaths, this.diffs,
- this._expandedFilePaths.length);
- }
-
- _forEachDiff(fn) {
- const diffs = this.diffs;
- for (let i = 0; i < diffs.length; i++) {
- fn(diffs[i]);
- }
- }
-
- expandAllDiffs() {
- this._showInlineDiffs = true;
-
- // Find the list of paths that are in the file list, but not in the
- // expanded list.
- const newPaths = [];
- let path;
- for (let i = 0; i < this._shownFiles.length; i++) {
- path = this._shownFiles[i].__path;
- if (!this._expandedFilePaths.includes(path)) {
- newPaths.push(path);
- }
- }
-
- this.splice(...['_expandedFilePaths', 0, 0].concat(newPaths));
- }
-
- collapseAllDiffs() {
- this._showInlineDiffs = false;
- this._expandedFilePaths = [];
- this.filesExpanded = this._computeExpandedFiles(
- this._expandedFilePaths.length, this._files.length);
- this.$.diffCursor.handleDiffUpdate();
- }
-
- /**
- * Computes a string with the number of comments and unresolved comments.
- *
- * @param {!Object} changeComments
- * @param {!Object} patchRange
- * @param {string} path
- * @return {string}
- */
- _computeCommentsString(changeComments, patchRange, path) {
- const unresolvedCount =
- changeComments.computeUnresolvedNum(patchRange.basePatchNum, path) +
- changeComments.computeUnresolvedNum(patchRange.patchNum, path);
- const commentCount =
- changeComments.computeCommentCount(patchRange.basePatchNum, path) +
- changeComments.computeCommentCount(patchRange.patchNum, path);
- const commentString = GrCountStringFormatter.computePluralString(
- commentCount, 'comment');
- const unresolvedString = GrCountStringFormatter.computeString(
- unresolvedCount, 'unresolved');
-
- return commentString +
- // Add a space if both comments and unresolved
- (commentString && unresolvedString ? ' ' : '') +
- // Add parentheses around unresolved if it exists.
- (unresolvedString ? `(${unresolvedString})` : '');
- }
-
- /**
- * Computes a string with the number of drafts.
- *
- * @param {!Object} changeComments
- * @param {!Object} patchRange
- * @param {string} path
- * @return {string}
- */
- _computeDraftsString(changeComments, patchRange, path) {
- const draftCount =
- changeComments.computeDraftCount(patchRange.basePatchNum, path) +
- changeComments.computeDraftCount(patchRange.patchNum, path);
- return GrCountStringFormatter.computePluralString(draftCount, 'draft');
- }
-
- /**
- * Computes a shortened string with the number of drafts.
- *
- * @param {!Object} changeComments
- * @param {!Object} patchRange
- * @param {string} path
- * @return {string}
- */
- _computeDraftsStringMobile(changeComments, patchRange, path) {
- const draftCount =
- changeComments.computeDraftCount(patchRange.basePatchNum, path) +
- changeComments.computeDraftCount(patchRange.patchNum, path);
- return GrCountStringFormatter.computeShortString(draftCount, 'd');
- }
-
- /**
- * Computes a shortened string with the number of comments.
- *
- * @param {!Object} changeComments
- * @param {!Object} patchRange
- * @param {string} path
- * @return {string}
- */
- _computeCommentsStringMobile(changeComments, patchRange, path) {
- const commentCount =
- changeComments.computeCommentCount(patchRange.basePatchNum, path) +
- changeComments.computeCommentCount(patchRange.patchNum, path);
- return GrCountStringFormatter.computeShortString(commentCount, 'c');
- }
-
- /**
- * @param {string} path
- * @param {boolean=} opt_reviewed
- */
- _reviewFile(path, opt_reviewed) {
- if (this.editMode) { return; }
- const index = this._files.findIndex(file => file.__path === path);
- const reviewed = opt_reviewed || !this._files[index].isReviewed;
-
- this.set(['_files', index, 'isReviewed'], reviewed);
- if (index < this._shownFiles.length) {
- this.notifyPath(`_shownFiles.${index}.isReviewed`);
- }
-
- this._saveReviewedState(path, reviewed);
- }
-
- _saveReviewedState(path, reviewed) {
- return this.$.restAPI.saveFileReviewed(this.changeNum,
- this.patchRange.patchNum, path, reviewed);
- }
-
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- _getReviewedFiles() {
- if (this.editMode) { return Promise.resolve([]); }
- return this.$.restAPI.getReviewedFiles(this.changeNum,
- this.patchRange.patchNum);
- }
-
- _getFiles() {
- return this.$.restAPI.getChangeOrEditFiles(
- this.changeNum, this.patchRange);
- }
-
- /**
- * The closure compiler doesn't realize this.specialFilePathCompare is
- * valid.
- *
- * @suppress {checkTypes}
- */
- _normalizeChangeFilesResponse(response) {
- if (!response) { return []; }
- const paths = Object.keys(response).sort(this.specialFilePathCompare);
- const files = [];
- for (let i = 0; i < paths.length; i++) {
- const info = response[paths[i]];
- info.__path = paths[i];
- info.lines_inserted = info.lines_inserted || 0;
- info.lines_deleted = info.lines_deleted || 0;
- files.push(info);
- }
- return files;
- }
-
- /**
- * Handle all events from the file list dom-repeat so event handleers don't
- * have to get registered for potentially very long lists.
- */
- _handleFileListClick(e) {
- // Traverse upwards to find the row element if the target is not the row.
- let row = e.target;
- while (!row.classList.contains('row') && row.parentElement) {
- row = row.parentElement;
- }
-
- const path = row.dataset.path;
- // Handle checkbox mark as reviewed.
- if (e.target.classList.contains('markReviewed')) {
- e.preventDefault();
- return this._reviewFile(path);
- }
-
- // If a path cannot be interpreted from the click target (meaning it's not
- // somewhere in the row, e.g. diff content) or if the user clicked the
- // link, defer to the native behavior.
- if (!path || this.descendedFromClass(e.target, 'pathLink')) { return; }
-
- // Disregard the event if the click target is in the edit controls.
- if (this.descendedFromClass(e.target, 'editFileControls')) { return; }
-
- e.preventDefault();
- this._togglePathExpanded(path);
- }
-
- _handleLeftPane(e) {
- if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
- return;
- }
-
- e.preventDefault();
- this.$.diffCursor.moveLeft();
- }
-
- _handleRightPane(e) {
- if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
- return;
- }
-
- e.preventDefault();
- this.$.diffCursor.moveRight();
- }
-
- _handleToggleInlineDiff(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e) ||
- this.$.fileCursor.index === -1) { return; }
-
- e.preventDefault();
- this._togglePathExpandedByIndex(this.$.fileCursor.index);
- }
-
- _handleToggleAllInlineDiffs(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- this._toggleInlineDiffs();
- }
-
- _handleCursorNext(e) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
- return;
- }
-
- if (this._showInlineDiffs) {
- e.preventDefault();
- this.$.diffCursor.moveDown();
- this._displayLine = true;
- } else {
- // Down key
- if (this.getKeyboardEvent(e).keyCode === 40) { return; }
- e.preventDefault();
- this.$.fileCursor.next();
- this.selectedIndex = this.$.fileCursor.index;
- }
- }
-
- _handleCursorPrev(e) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
- return;
- }
-
- if (this._showInlineDiffs) {
- e.preventDefault();
- this.$.diffCursor.moveUp();
- this._displayLine = true;
- } else {
- // Up key
- if (this.getKeyboardEvent(e).keyCode === 38) { return; }
- e.preventDefault();
- this.$.fileCursor.previous();
- this.selectedIndex = this.$.fileCursor.index;
- }
- }
-
- _handleNewComment(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
- e.preventDefault();
- this.$.diffCursor.createCommentInPlace();
- }
-
- _handleOpenLastFile(e) {
- // Check for meta key to avoid overriding native chrome shortcut.
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.getKeyboardEvent(e).metaKey) { return; }
-
- e.preventDefault();
- this._openSelectedFile(this._files.length - 1);
- }
-
- _handleOpenFirstFile(e) {
- // Check for meta key to avoid overriding native chrome shortcut.
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.getKeyboardEvent(e).metaKey) { return; }
-
- e.preventDefault();
- this._openSelectedFile(0);
- }
-
- _handleOpenFile(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
- e.preventDefault();
-
- if (this._showInlineDiffs) {
- this._openCursorFile();
- return;
- }
-
- this._openSelectedFile();
- }
-
- _handleNextChunk(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- (this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) ||
- this._noDiffsExpanded()) {
- return;
- }
-
- e.preventDefault();
- if (this.isModifierPressed(e, 'shiftKey')) {
- this.$.diffCursor.moveToNextCommentThread();
- } else {
- this.$.diffCursor.moveToNextChunk();
- }
- }
-
- _handlePrevChunk(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- (this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) ||
- this._noDiffsExpanded()) {
- return;
- }
-
- e.preventDefault();
- if (this.isModifierPressed(e, 'shiftKey')) {
- this.$.diffCursor.moveToPreviousCommentThread();
- } else {
- this.$.diffCursor.moveToPreviousChunk();
- }
- }
-
- _handleToggleFileReviewed(e) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
- return;
- }
-
- e.preventDefault();
- if (!this._files[this.$.fileCursor.index]) { return; }
- this._reviewFile(this._files[this.$.fileCursor.index].__path);
- }
-
- _handleToggleLeftPane(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- this._forEachDiff(diff => {
- diff.toggleLeftDiff();
- });
- }
-
- _toggleInlineDiffs() {
- if (this._showInlineDiffs) {
- this.collapseAllDiffs();
- } else {
- this.expandAllDiffs();
- }
- }
-
- _openCursorFile() {
- const diff = this.$.diffCursor.getTargetDiffElement();
- Gerrit.Nav.navigateToDiff(this.change, diff.path,
- diff.patchRange.patchNum, this.patchRange.basePatchNum);
- }
-
- /**
- * @param {number=} opt_index
- */
- _openSelectedFile(opt_index) {
- if (opt_index != null) {
- this.$.fileCursor.setCursorAtIndex(opt_index);
- }
- if (!this._files[this.$.fileCursor.index]) { return; }
- Gerrit.Nav.navigateToDiff(this.change,
- this._files[this.$.fileCursor.index].__path, this.patchRange.patchNum,
- this.patchRange.basePatchNum);
- }
-
- _addDraftAtTarget() {
- const diff = this.$.diffCursor.getTargetDiffElement();
- const target = this.$.diffCursor.getTargetLineElement();
- if (diff && target) {
- diff.addDraftAtLine(target);
- }
- }
-
- _shouldHideChangeTotals(_patchChange) {
- return _patchChange.inserted === 0 && _patchChange.deleted === 0;
- }
-
- _shouldHideBinaryChangeTotals(_patchChange) {
- return _patchChange.size_delta_inserted === 0 &&
- _patchChange.size_delta_deleted === 0;
- }
-
- _computeFileStatus(status) {
- return status || 'M';
- }
-
- _computeDiffURL(change, patchRange, path, editMode) {
- // Polymer 2: check for undefined
- if ([change, patchRange, path, editMode]
- .some(arg => arg === undefined)) {
- return;
- }
- // TODO(kaspern): Fix editing for commit messages and merge lists.
- if (editMode && path !== this.COMMIT_MESSAGE_PATH &&
- path !== this.MERGE_LIST_PATH) {
- return Gerrit.Nav.getEditUrlForDiff(change, path, patchRange.patchNum,
- patchRange.basePatchNum);
- }
- return Gerrit.Nav.getUrlForDiff(change, path, patchRange.patchNum,
- patchRange.basePatchNum);
- }
-
- _formatBytes(bytes) {
- if (bytes == 0) return '+/-0 B';
- const bits = 1024;
- const decimals = 1;
- const sizes =
- ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
- const exponent = Math.floor(Math.log(Math.abs(bytes)) / Math.log(bits));
- const prepend = bytes > 0 ? '+' : '';
- return prepend + parseFloat((bytes / Math.pow(bits, exponent))
- .toFixed(decimals)) + ' ' + sizes[exponent];
- }
-
- _formatPercentage(size, delta) {
- const oldSize = size - delta;
-
- if (oldSize === 0) { return ''; }
-
- const percentage = Math.round(Math.abs(delta * 100 / oldSize));
- return '(' + (delta > 0 ? '+' : '-') + percentage + '%)';
- }
-
- _computeBinaryClass(delta) {
- if (delta === 0) { return; }
- return delta >= 0 ? 'added' : 'removed';
- }
-
- /**
- * @param {string} baseClass
- * @param {string} path
- */
- _computeClass(baseClass, path) {
- const classes = [];
- if (baseClass) {
- classes.push(baseClass);
- }
- if (path === this.COMMIT_MESSAGE_PATH || path === this.MERGE_LIST_PATH) {
- classes.push('invisible');
- }
- return classes.join(' ');
- }
-
- _computePathClass(path, expandedFilesRecord) {
- return this._isFileExpanded(path, expandedFilesRecord) ? 'expanded' : '';
- }
-
- _computeShowHideIcon(path, expandedFilesRecord) {
- return this._isFileExpanded(path, expandedFilesRecord) ?
- 'gr-icons:expand-less' : 'gr-icons:expand-more';
- }
-
- _computeFiles(filesByPath, changeComments, patchRange, reviewed, loading) {
- // Polymer 2: check for undefined
- if ([
- filesByPath,
- changeComments,
- patchRange,
- reviewed,
- loading,
- ].some(arg => arg === undefined)) {
- return;
- }
-
- // Await all promises resolving from reload. @See Issue 9057
- if (loading || !changeComments) { return; }
-
- const commentedPaths = changeComments.getPaths(patchRange);
- const files = Object.assign({}, filesByPath);
- Object.keys(commentedPaths).forEach(commentedPath => {
- if (files.hasOwnProperty(commentedPath)) { return; }
- files[commentedPath] = {status: 'U'};
- });
- const reviewedSet = new Set(reviewed || []);
- for (const filePath in files) {
- if (!files.hasOwnProperty(filePath)) { continue; }
- files[filePath].isReviewed = reviewedSet.has(filePath);
- }
-
- this._files = this._normalizeChangeFilesResponse(files);
- }
-
- _computeFilesShown(numFilesShown, files) {
- // Polymer 2: check for undefined
- if ([numFilesShown, files].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const previousNumFilesShown = this._shownFiles ?
- this._shownFiles.length : 0;
-
- const filesShown = files.slice(0, numFilesShown);
- this.fire('files-shown-changed', {length: filesShown.length});
-
- // Start the timer for the rendering work hwere because this is where the
- // _shownFiles property is being set, and _shownFiles is used in the
- // dom-repeat binding.
- this.$.reporting.time(RENDER_TIMING_LABEL);
-
- // How many more files are being shown (if it's an increase).
- this._reportinShownFilesIncrement =
- Math.max(0, filesShown.length - previousNumFilesShown);
-
- return filesShown;
- }
-
- _updateDiffCursor() {
- // Overwrite the cursor's list of diffs:
- this.$.diffCursor.splice(
- ...['diffs', 0, this.$.diffCursor.diffs.length].concat(this.diffs));
- }
-
- _filesChanged() {
- if (this._files && this._files.length > 0) {
- Polymer.dom.flush();
- const files = Array.from(
- Polymer.dom(this.root).querySelectorAll('.file-row'));
- this.$.fileCursor.stops = files;
- this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true);
- }
- }
-
- _incrementNumFilesShown() {
- this.numFilesShown += this.fileListIncrement;
- }
-
- _computeFileListControlClass(numFilesShown, files) {
- return numFilesShown >= files.length ? 'invisible' : '';
- }
-
- _computeIncrementText(numFilesShown, files) {
- if (!files) { return ''; }
- const text =
- Math.min(this.fileListIncrement, files.length - numFilesShown);
- return 'Show ' + text + ' more';
- }
-
- _computeShowAllText(files) {
- if (!files) { return ''; }
- return 'Show all ' + files.length + ' files';
- }
-
- _computeWarnShowAll(files) {
- return files.length > WARN_SHOW_ALL_THRESHOLD;
- }
-
- _computeShowAllWarning(files) {
- if (!this._computeWarnShowAll(files)) { return ''; }
- return 'Warning: showing all ' + files.length +
- ' files may take several seconds.';
- }
-
- _showAllFiles() {
- this.numFilesShown = this._files.length;
- }
-
- _computePatchSetDescription(revisions, patchNum) {
- // Polymer 2: check for undefined
- if ([revisions, patchNum].some(arg => arg === undefined)) {
- return '';
- }
-
- const rev = this.getRevisionByPatchNum(revisions, patchNum);
- return (rev && rev.description) ?
- rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
- }
-
- /**
- * Get a descriptive label for use in the status indicator's tooltip and
- * ARIA label.
- *
- * @param {string} status
- * @return {string}
- */
- _computeFileStatusLabel(status) {
- const statusCode = this._computeFileStatus(status);
- return FileStatus.hasOwnProperty(statusCode) ?
- FileStatus[statusCode] : 'Status Unknown';
- }
-
- _isFileExpanded(path, expandedFilesRecord) {
- return expandedFilesRecord.base.includes(path);
- }
-
- _onLineSelected(e, detail) {
- this.$.diffCursor.moveToLineNumber(detail.number, detail.side,
- detail.path);
- }
-
- _computeExpandedFiles(expandedCount, totalCount) {
- if (expandedCount === 0) {
- return GrFileListConstants.FilesExpandedState.NONE;
- } else if (expandedCount === totalCount) {
- return GrFileListConstants.FilesExpandedState.ALL;
- }
- return GrFileListConstants.FilesExpandedState.SOME;
- }
-
- /**
- * Handle splices to the list of expanded file paths. If there are any new
- * entries in the expanded list, then render each diff corresponding in
- * order by waiting for the previous diff to finish before starting the next
- * one.
- *
- * @param {!Array} record The splice record in the expanded paths list.
- */
- _expandedPathsChanged(record) {
- // Clear content for any diffs that are not open so if they get re-opened
- // the stale content does not flash before it is cleared and reloaded.
- const collapsedDiffs = this.diffs.filter(diff =>
- this._expandedFilePaths.indexOf(diff.path) === -1);
- this._clearCollapsedDiffs(collapsedDiffs);
-
- if (!record) { return; } // Happens after "Collapse all" clicked.
-
- this.filesExpanded = this._computeExpandedFiles(
- this._expandedFilePaths.length, this._files.length);
-
- // Find the paths introduced by the new index splices:
- const newPaths = record.indexSplices
- .map(splice => splice.object.slice(
- splice.index, splice.index + splice.addedCount))
- .reduce((acc, paths) => acc.concat(paths), []);
-
- // Required so that the newly created diff view is included in this.diffs.
- Polymer.dom.flush();
-
- this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
-
- if (newPaths.length) {
- this._renderInOrder(newPaths, this.diffs, newPaths.length);
- }
-
- this._updateDiffCursor();
- this.$.diffCursor.handleDiffUpdate();
- }
-
- _clearCollapsedDiffs(collapsedDiffs) {
- for (const diff of collapsedDiffs) {
- diff.cancel();
- diff.clearDiffContent();
- }
- }
-
- /**
- * Given an array of paths and a NodeList of diff elements, render the diff
- * for each path in order, awaiting the previous render to complete before
- * continung.
- *
- * @param {!Array<string>} paths
- * @param {!NodeList<!Object>} diffElements (GrDiffHostElement)
- * @param {number} initialCount The total number of paths in the pass. This
- * is used to generate log messages.
- * @return {!Promise}
- */
- _renderInOrder(paths, diffElements, initialCount) {
- let iter = 0;
-
- return (new Promise(resolve => {
- this.fire('reload-drafts', {resolve});
- })).then(() => this.asyncForeach(paths, (path, cancel) => {
- this._cancelForEachDiff = cancel;
-
- iter++;
- console.log('Expanding diff', iter, 'of', initialCount, ':',
- path);
- const diffElem = this._findDiffByPath(path, diffElements);
- if (!diffElem) {
- console.warn(`Did not find <gr-diff-host> element for ${path}`);
- return Promise.resolve();
- }
- diffElem.comments = this.changeComments.getCommentsBySideForPath(
- path, this.patchRange, this.projectConfig);
- const promises = [diffElem.reload()];
- if (this._loggedIn && !this.diffPrefs.manual_review) {
- promises.push(this._reviewFile(path, true));
- }
- return Promise.all(promises);
- }).then(() => {
- this._cancelForEachDiff = null;
- this._nextRenderParams = null;
- console.log('Finished expanding', initialCount, 'diff(s)');
- this.$.reporting.timeEndWithAverage(EXPAND_ALL_TIMING_LABEL,
- EXPAND_ALL_AVG_TIMING_LABEL, initialCount);
- this.$.diffCursor.handleDiffUpdate();
- }));
- }
-
- /** Cancel the rendering work of every diff in the list */
- _cancelDiffs() {
- if (this._cancelForEachDiff) { this._cancelForEachDiff(); }
- this._forEachDiff(d => d.cancel());
- }
-
- /**
- * In the given NodeList of diff elements, find the diff for the given path.
- *
- * @param {string} path
- * @param {!NodeList<!Object>} diffElements (GrDiffElement)
- * @return {!Object|undefined} (GrDiffElement)
- */
- _findDiffByPath(path, diffElements) {
- for (let i = 0; i < diffElements.length; i++) {
- if (diffElements[i].path === path) {
- return diffElements[i];
- }
- }
- }
-
- /**
- * Reset the comments of a modified thread
- *
- * @param {string} rootId
- * @param {string} path
- */
- reloadCommentsForThreadWithRootId(rootId, path) {
- // Don't bother continuing if we already know that the path that contains
- // the updated comment thread is not expanded.
- if (!this._expandedFilePaths.includes(path)) { return; }
- const diff = this.diffs.find(d => d.path === path);
-
- const threadEl = diff.getThreadEls().find(t => t.rootId === rootId);
- if (!threadEl) { return; }
-
- const newComments = this.changeComments.getCommentsForThread(rootId);
-
- // If newComments is null, it means that a single draft was
- // removed from a thread in the thread view, and the thread should
- // no longer exist. Remove the existing thread element in the diff
- // view.
- if (!newComments) {
- threadEl.fireRemoveSelf();
- return;
- }
-
- // Comments are not returned with the commentSide attribute from
- // the api, but it's necessary to be stored on the diff's
- // comments due to use in the _handleCommentUpdate function.
- // The comment thread already has a side associated with it, so
- // set the comment's side to match.
- threadEl.comments = newComments.map(c => Object.assign(
- c, {__commentSide: threadEl.commentSide}
- ));
- Polymer.dom.flush();
- return;
- }
-
- _handleEscKey(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
- e.preventDefault();
- this._displayLine = false;
- }
-
- /**
- * Update the loading class for the file list rows. The update is inside a
- * debouncer so that the file list doesn't flash gray when the API requests
- * are reasonably fast.
- *
- * @param {boolean} loading
- */
- _loadingChanged(loading) {
- this.debounce('loading-change', () => {
- // Only show set the loading if there have been files loaded to show. In
- // this way, the gray loading style is not shown on initial loads.
- this.classList.toggle('loading', loading && !!this._files.length);
- }, LOADING_DEBOUNCE_INTERVAL);
- }
-
- _editModeChanged(editMode) {
- this.classList.toggle('editMode', editMode);
- }
-
- _computeReviewedClass(isReviewed) {
- return isReviewed ? 'isReviewed' : '';
- }
-
- _computeReviewedText(isReviewed) {
- return isReviewed ? 'MARK UNREVIEWED' : 'MARK REVIEWED';
- }
-
- /**
- * Given a file path, return whether that path should have visible size bars
- * and be included in the size bars calculation.
- *
- * @param {string} path
- * @return {boolean}
- */
- _showBarsForPath(path) {
- return path !== this.COMMIT_MESSAGE_PATH && path !== this.MERGE_LIST_PATH;
- }
-
- /**
- * Compute size bar layout values from the file list.
- *
- * @return {Gerrit.LayoutStats|undefined}
- *
- */
- _computeSizeBarLayout(shownFilesRecord) {
- if (!shownFilesRecord || !shownFilesRecord.base) { return undefined; }
- const stats = {
- maxInserted: 0,
- maxDeleted: 0,
- maxAdditionWidth: 0,
- maxDeletionWidth: 0,
- deletionOffset: 0,
- };
- shownFilesRecord.base
- .filter(f => this._showBarsForPath(f.__path))
- .forEach(f => {
- if (f.lines_inserted) {
- stats.maxInserted = Math.max(stats.maxInserted, f.lines_inserted);
- }
- if (f.lines_deleted) {
- stats.maxDeleted = Math.max(stats.maxDeleted, f.lines_deleted);
- }
- });
- const ratio = stats.maxInserted / (stats.maxInserted + stats.maxDeleted);
- if (!isNaN(ratio)) {
- stats.maxAdditionWidth =
- (SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH) * ratio;
- stats.maxDeletionWidth =
- SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH - stats.maxAdditionWidth;
- stats.deletionOffset = stats.maxAdditionWidth + SIZE_BAR_GAP_WIDTH;
- }
- return stats;
- }
-
- /**
- * Get the width of the addition bar for a file.
- *
- * @param {Object} file
- * @param {Gerrit.LayoutStats} stats
- * @return {number}
- */
- _computeBarAdditionWidth(file, stats) {
- if (stats.maxInserted === 0 ||
- !file.lines_inserted ||
- !this._showBarsForPath(file.__path)) {
- return 0;
- }
- const width =
- stats.maxAdditionWidth * file.lines_inserted / stats.maxInserted;
- return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width);
- }
-
- /**
- * Get the x-offset of the addition bar for a file.
- *
- * @param {Object} file
- * @param {Gerrit.LayoutStats} stats
- * @return {number}
- */
- _computeBarAdditionX(file, stats) {
- return stats.maxAdditionWidth -
- this._computeBarAdditionWidth(file, stats);
- }
-
- /**
- * Get the width of the deletion bar for a file.
- *
- * @param {Object} file
- * @param {Gerrit.LayoutStats} stats
- * @return {number}
- */
- _computeBarDeletionWidth(file, stats) {
- if (stats.maxDeleted === 0 ||
- !file.lines_deleted ||
- !this._showBarsForPath(file.__path)) {
- return 0;
- }
- const width =
- stats.maxDeletionWidth * file.lines_deleted / stats.maxDeleted;
- return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width);
- }
-
- /**
- * Get the x-offset of the deletion bar for a file.
- *
- * @param {Gerrit.LayoutStats} stats
- *
- * @return {number}
- */
- _computeBarDeletionX(stats) {
- return stats.deletionOffset;
- }
-
- _computeShowSizeBars(userPrefs) {
- return !!userPrefs.size_bar_in_change_table;
- }
-
- _computeSizeBarsClass(showSizeBars, path) {
- let hideClass = '';
- if (!showSizeBars) {
- hideClass = 'hide';
- } else if (!this._showBarsForPath(path)) {
- hideClass = 'invisible';
- }
- return `sizeBars desktop ${hideClass}`;
- }
-
- /**
- * Shows registered dynamic columns iff the 'header', 'content' and
- * 'summary' endpoints are regiestered the exact same number of times.
- * Ideally, there should be a better way to enforce the expectation of the
- * dependencies between dynamic endpoints.
- */
- _computeShowDynamicColumns(
- headerEndpoints, contentEndpoints, summaryEndpoints) {
- return headerEndpoints && contentEndpoints && summaryEndpoints &&
- headerEndpoints.length === contentEndpoints.length &&
- headerEndpoints.length === summaryEndpoints.length;
- }
-
- /**
- * Returns true if none of the inline diffs have been expanded.
- *
- * @return {boolean}
- */
- _noDiffsExpanded() {
- return this.filesExpanded === GrFileListConstants.FilesExpandedState.NONE;
- }
-
- /**
- * Method to call via binding when each file list row is rendered. This
- * allows approximate detection of when the dom-repeat has completed
- * rendering.
- *
- * @param {number} index The index of the row being rendered.
- * @return {string} an empty string.
- */
- _reportRenderedRow(index) {
- if (index === this._shownFiles.length - 1) {
- this.async(() => {
- this.$.reporting.timeEndWithAverage(RENDER_TIMING_LABEL,
- RENDER_AVG_TIMING_LABEL, this._reportinShownFilesIncrement);
- }, 1);
- }
- return '';
- }
-
- _reviewedTitle(reviewed) {
- if (reviewed) {
- return 'Mark as not reviewed (shortcut: r)';
- }
-
- return 'Mark as reviewed (shortcut: r)';
- }
-
- _handleReloadingDiffPreference() {
- this._getDiffPreferences().then(prefs => {
- this.diffPrefs = prefs;
- });
+ _scopedKeydownHandler(e) {
+ if (e.keyCode === 13) {
+ // Enter.
+ this._handleOpenFile(e);
}
}
- customElements.define(GrFileList.is, GrFileList);
-})();
+ reload() {
+ if (!this.changeNum || !this.patchRange.patchNum) {
+ return Promise.resolve();
+ }
+
+ this._loading = true;
+
+ this.collapseAllDiffs();
+ const promises = [];
+
+ promises.push(this._getFiles().then(filesByPath => {
+ this._filesByPath = filesByPath;
+ }));
+ promises.push(this._getLoggedIn()
+ .then(loggedIn => this._loggedIn = loggedIn)
+ .then(loggedIn => {
+ if (!loggedIn) { return; }
+
+ return this._getReviewedFiles().then(reviewed => {
+ this._reviewed = reviewed;
+ });
+ }));
+
+ promises.push(this._getDiffPreferences().then(prefs => {
+ this.diffPrefs = prefs;
+ }));
+
+ promises.push(this._getPreferences().then(prefs => {
+ this._userPrefs = prefs;
+ }));
+
+ return Promise.all(promises).then(() => {
+ this._loading = false;
+ this._detectChromiteButler();
+ this.$.reporting.fileListDisplayed();
+ });
+ }
+
+ _detectChromiteButler() {
+ const hasButler = !!document.getElementById('butler-suggested-owners');
+ if (hasButler) {
+ this.$.reporting.reportExtension('butler');
+ }
+ }
+
+ get diffs() {
+ const diffs = dom(this.root).querySelectorAll('gr-diff-host');
+ // It is possible that a bogus diff element is hanging around invisibly
+ // from earlier with a different patch set choice and associated with a
+ // different entry in the files array. So filter on visible items only.
+ return Array.from(diffs).filter(
+ el => !!el && !!el.style && el.style.display !== 'none');
+ }
+
+ openDiffPrefs() {
+ this.$.diffPreferencesDialog.open();
+ }
+
+ _calculatePatchChange(files) {
+ const magicFilesExcluded = files.filter(files =>
+ !this.isMagicPath(files.__path)
+ );
+
+ return magicFilesExcluded.reduce((acc, obj) => {
+ const inserted = obj.lines_inserted ? obj.lines_inserted : 0;
+ const deleted = obj.lines_deleted ? obj.lines_deleted : 0;
+ const total_size = (obj.size && obj.binary) ? obj.size : 0;
+ const size_delta_inserted =
+ obj.binary && obj.size_delta > 0 ? obj.size_delta : 0;
+ const size_delta_deleted =
+ obj.binary && obj.size_delta < 0 ? obj.size_delta : 0;
+
+ return {
+ inserted: acc.inserted + inserted,
+ deleted: acc.deleted + deleted,
+ size_delta_inserted: acc.size_delta_inserted + size_delta_inserted,
+ size_delta_deleted: acc.size_delta_deleted + size_delta_deleted,
+ total_size: acc.total_size + total_size,
+ };
+ }, {inserted: 0, deleted: 0, size_delta_inserted: 0,
+ size_delta_deleted: 0, total_size: 0});
+ }
+
+ _getDiffPreferences() {
+ return this.$.restAPI.getDiffPreferences();
+ }
+
+ _getPreferences() {
+ return this.$.restAPI.getPreferences();
+ }
+
+ _togglePathExpanded(path) {
+ // Is the path in the list of expanded diffs? IF so remove it, otherwise
+ // add it to the list.
+ const pathIndex = this._expandedFilePaths.indexOf(path);
+ if (pathIndex === -1) {
+ this.push('_expandedFilePaths', path);
+ } else {
+ this.splice('_expandedFilePaths', pathIndex, 1);
+ }
+ }
+
+ _togglePathExpandedByIndex(index) {
+ this._togglePathExpanded(this._files[index].__path);
+ }
+
+ _updateDiffPreferences() {
+ if (!this.diffs.length) { return; }
+ // Re-render all expanded diffs sequentially.
+ this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
+ this._renderInOrder(this._expandedFilePaths, this.diffs,
+ this._expandedFilePaths.length);
+ }
+
+ _forEachDiff(fn) {
+ const diffs = this.diffs;
+ for (let i = 0; i < diffs.length; i++) {
+ fn(diffs[i]);
+ }
+ }
+
+ expandAllDiffs() {
+ this._showInlineDiffs = true;
+
+ // Find the list of paths that are in the file list, but not in the
+ // expanded list.
+ const newPaths = [];
+ let path;
+ for (let i = 0; i < this._shownFiles.length; i++) {
+ path = this._shownFiles[i].__path;
+ if (!this._expandedFilePaths.includes(path)) {
+ newPaths.push(path);
+ }
+ }
+
+ this.splice(...['_expandedFilePaths', 0, 0].concat(newPaths));
+ }
+
+ collapseAllDiffs() {
+ this._showInlineDiffs = false;
+ this._expandedFilePaths = [];
+ this.filesExpanded = this._computeExpandedFiles(
+ this._expandedFilePaths.length, this._files.length);
+ this.$.diffCursor.handleDiffUpdate();
+ }
+
+ /**
+ * Computes a string with the number of comments and unresolved comments.
+ *
+ * @param {!Object} changeComments
+ * @param {!Object} patchRange
+ * @param {string} path
+ * @return {string}
+ */
+ _computeCommentsString(changeComments, patchRange, path) {
+ const unresolvedCount =
+ changeComments.computeUnresolvedNum({
+ patchNum: patchRange.basePatchNum,
+ path,
+ }) +
+ changeComments.computeUnresolvedNum({
+ patchNum: patchRange.patchNum,
+ path,
+ });
+ const commentCount =
+ changeComments.computeCommentCount({
+ patchNum: patchRange.basePatchNum,
+ path,
+ }) +
+ changeComments.computeCommentCount({
+ patchNum: patchRange.patchNum,
+ path,
+ });
+ const commentString = GrCountStringFormatter.computePluralString(
+ commentCount, 'comment');
+ const unresolvedString = GrCountStringFormatter.computeString(
+ unresolvedCount, 'unresolved');
+
+ return commentString +
+ // Add a space if both comments and unresolved
+ (commentString && unresolvedString ? ' ' : '') +
+ // Add parentheses around unresolved if it exists.
+ (unresolvedString ? `(${unresolvedString})` : '');
+ }
+
+ /**
+ * Computes a string with the number of drafts.
+ *
+ * @param {!Object} changeComments
+ * @param {!Object} patchRange
+ * @param {string} path
+ * @return {string}
+ */
+ _computeDraftsString(changeComments, patchRange, path) {
+ const draftCount =
+ changeComments.computeDraftCount({
+ patchNum: patchRange.basePatchNum,
+ path,
+ }) +
+ changeComments.computeDraftCount({
+ patchNum: patchRange.patchNum,
+ path,
+ });
+ return GrCountStringFormatter.computePluralString(draftCount, 'draft');
+ }
+
+ /**
+ * Computes a shortened string with the number of drafts.
+ *
+ * @param {!Object} changeComments
+ * @param {!Object} patchRange
+ * @param {string} path
+ * @return {string}
+ */
+ _computeDraftsStringMobile(changeComments, patchRange, path) {
+ const draftCount =
+ changeComments.computeDraftCount({
+ patchNum: patchRange.basePatchNum,
+ path,
+ }) +
+ changeComments.computeDraftCount({
+ patchNum: patchRange.patchNum,
+ path,
+ });
+ return GrCountStringFormatter.computeShortString(draftCount, 'd');
+ }
+
+ /**
+ * Computes a shortened string with the number of comments.
+ *
+ * @param {!Object} changeComments
+ * @param {!Object} patchRange
+ * @param {string} path
+ * @return {string}
+ */
+ _computeCommentsStringMobile(changeComments, patchRange, path) {
+ const commentCount =
+ changeComments.computeCommentCount({
+ patchNum: patchRange.basePatchNum,
+ path,
+ }) +
+ changeComments.computeCommentCount({
+ patchNum: patchRange.patchNum,
+ path,
+ });
+ return GrCountStringFormatter.computeShortString(commentCount, 'c');
+ }
+
+ /**
+ * @param {string} path
+ * @param {boolean=} opt_reviewed
+ */
+ _reviewFile(path, opt_reviewed) {
+ if (this.editMode) { return; }
+ const index = this._files.findIndex(file => file.__path === path);
+ const reviewed = opt_reviewed || !this._files[index].isReviewed;
+
+ this.set(['_files', index, 'isReviewed'], reviewed);
+ if (index < this._shownFiles.length) {
+ this.notifyPath(`_shownFiles.${index}.isReviewed`);
+ }
+
+ this._saveReviewedState(path, reviewed);
+ }
+
+ _saveReviewedState(path, reviewed) {
+ return this.$.restAPI.saveFileReviewed(this.changeNum,
+ this.patchRange.patchNum, path, reviewed);
+ }
+
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _getReviewedFiles() {
+ if (this.editMode) { return Promise.resolve([]); }
+ return this.$.restAPI.getReviewedFiles(this.changeNum,
+ this.patchRange.patchNum);
+ }
+
+ _getFiles() {
+ return this.$.restAPI.getChangeOrEditFiles(
+ this.changeNum, this.patchRange);
+ }
+
+ /**
+ * The closure compiler doesn't realize this.specialFilePathCompare is
+ * valid.
+ *
+ * @suppress {checkTypes}
+ */
+ _normalizeChangeFilesResponse(response) {
+ if (!response) { return []; }
+ const paths = Object.keys(response).sort(this.specialFilePathCompare);
+ const files = [];
+ for (let i = 0; i < paths.length; i++) {
+ const info = response[paths[i]];
+ info.__path = paths[i];
+ info.lines_inserted = info.lines_inserted || 0;
+ info.lines_deleted = info.lines_deleted || 0;
+ files.push(info);
+ }
+ return files;
+ }
+
+ /**
+ * Handle all events from the file list dom-repeat so event handleers don't
+ * have to get registered for potentially very long lists.
+ */
+ _handleFileListClick(e) {
+ // Traverse upwards to find the row element if the target is not the row.
+ let row = e.target;
+ while (!row.classList.contains('row') && row.parentElement) {
+ row = row.parentElement;
+ }
+
+ const path = row.dataset.path;
+ // Handle checkbox mark as reviewed.
+ if (e.target.classList.contains('markReviewed')) {
+ e.preventDefault();
+ return this._reviewFile(path);
+ }
+
+ // If a path cannot be interpreted from the click target (meaning it's not
+ // somewhere in the row, e.g. diff content) or if the user clicked the
+ // link, defer to the native behavior.
+ if (!path || this.descendedFromClass(e.target, 'pathLink')) { return; }
+
+ // Disregard the event if the click target is in the edit controls.
+ if (this.descendedFromClass(e.target, 'editFileControls')) { return; }
+
+ e.preventDefault();
+ this._togglePathExpanded(path);
+ }
+
+ _handleLeftPane(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
+ return;
+ }
+
+ e.preventDefault();
+ this.$.diffCursor.moveLeft();
+ }
+
+ _handleRightPane(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
+ return;
+ }
+
+ e.preventDefault();
+ this.$.diffCursor.moveRight();
+ }
+
+ _handleToggleInlineDiff(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e) ||
+ this.$.fileCursor.index === -1) { return; }
+
+ e.preventDefault();
+ this._togglePathExpandedByIndex(this.$.fileCursor.index);
+ }
+
+ _handleToggleAllInlineDiffs(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ this._toggleInlineDiffs();
+ }
+
+ _handleCursorNext(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+
+ if (this._showInlineDiffs) {
+ e.preventDefault();
+ this.$.diffCursor.moveDown();
+ this._displayLine = true;
+ } else {
+ // Down key
+ if (this.getKeyboardEvent(e).keyCode === 40) { return; }
+ e.preventDefault();
+ this.$.fileCursor.next();
+ this.selectedIndex = this.$.fileCursor.index;
+ }
+ }
+
+ _handleCursorPrev(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+
+ if (this._showInlineDiffs) {
+ e.preventDefault();
+ this.$.diffCursor.moveUp();
+ this._displayLine = true;
+ } else {
+ // Up key
+ if (this.getKeyboardEvent(e).keyCode === 38) { return; }
+ e.preventDefault();
+ this.$.fileCursor.previous();
+ this.selectedIndex = this.$.fileCursor.index;
+ }
+ }
+
+ _handleNewComment(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+ e.preventDefault();
+ this.$.diffCursor.createCommentInPlace();
+ }
+
+ _handleOpenLastFile(e) {
+ // Check for meta key to avoid overriding native chrome shortcut.
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.getKeyboardEvent(e).metaKey) { return; }
+
+ e.preventDefault();
+ this._openSelectedFile(this._files.length - 1);
+ }
+
+ _handleOpenFirstFile(e) {
+ // Check for meta key to avoid overriding native chrome shortcut.
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.getKeyboardEvent(e).metaKey) { return; }
+
+ e.preventDefault();
+ this._openSelectedFile(0);
+ }
+
+ _handleOpenFile(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+ e.preventDefault();
+
+ if (this._showInlineDiffs) {
+ this._openCursorFile();
+ return;
+ }
+
+ this._openSelectedFile();
+ }
+
+ _handleNextChunk(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ (this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) ||
+ this._noDiffsExpanded()) {
+ return;
+ }
+
+ e.preventDefault();
+ if (this.isModifierPressed(e, 'shiftKey')) {
+ this.$.diffCursor.moveToNextCommentThread();
+ } else {
+ this.$.diffCursor.moveToNextChunk();
+ }
+ }
+
+ _handlePrevChunk(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ (this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) ||
+ this._noDiffsExpanded()) {
+ return;
+ }
+
+ e.preventDefault();
+ if (this.isModifierPressed(e, 'shiftKey')) {
+ this.$.diffCursor.moveToPreviousCommentThread();
+ } else {
+ this.$.diffCursor.moveToPreviousChunk();
+ }
+ }
+
+ _handleToggleFileReviewed(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+
+ e.preventDefault();
+ if (!this._files[this.$.fileCursor.index]) { return; }
+ this._reviewFile(this._files[this.$.fileCursor.index].__path);
+ }
+
+ _handleToggleLeftPane(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ this._forEachDiff(diff => {
+ diff.toggleLeftDiff();
+ });
+ }
+
+ _toggleInlineDiffs() {
+ if (this._showInlineDiffs) {
+ this.collapseAllDiffs();
+ } else {
+ this.expandAllDiffs();
+ }
+ }
+
+ _openCursorFile() {
+ const diff = this.$.diffCursor.getTargetDiffElement();
+ Gerrit.Nav.navigateToDiff(this.change, diff.path,
+ diff.patchRange.patchNum, this.patchRange.basePatchNum);
+ }
+
+ /**
+ * @param {number=} opt_index
+ */
+ _openSelectedFile(opt_index) {
+ if (opt_index != null) {
+ this.$.fileCursor.setCursorAtIndex(opt_index);
+ }
+ if (!this._files[this.$.fileCursor.index]) { return; }
+ Gerrit.Nav.navigateToDiff(this.change,
+ this._files[this.$.fileCursor.index].__path, this.patchRange.patchNum,
+ this.patchRange.basePatchNum);
+ }
+
+ _addDraftAtTarget() {
+ const diff = this.$.diffCursor.getTargetDiffElement();
+ const target = this.$.diffCursor.getTargetLineElement();
+ if (diff && target) {
+ diff.addDraftAtLine(target);
+ }
+ }
+
+ _shouldHideChangeTotals(_patchChange) {
+ return _patchChange.inserted === 0 && _patchChange.deleted === 0;
+ }
+
+ _shouldHideBinaryChangeTotals(_patchChange) {
+ return _patchChange.size_delta_inserted === 0 &&
+ _patchChange.size_delta_deleted === 0;
+ }
+
+ _computeFileStatus(status) {
+ return status || 'M';
+ }
+
+ _computeDiffURL(change, patchRange, path, editMode) {
+ // Polymer 2: check for undefined
+ if ([change, patchRange, path, editMode]
+ .some(arg => arg === undefined)) {
+ return;
+ }
+ if (editMode && path !== this.MERGE_LIST_PATH) {
+ return Gerrit.Nav.getEditUrlForDiff(change, path, patchRange.patchNum,
+ patchRange.basePatchNum);
+ }
+ return Gerrit.Nav.getUrlForDiff(change, path, patchRange.patchNum,
+ patchRange.basePatchNum);
+ }
+
+ _formatBytes(bytes) {
+ if (bytes == 0) return '+/-0 B';
+ const bits = 1024;
+ const decimals = 1;
+ const sizes =
+ ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
+ const exponent = Math.floor(Math.log(Math.abs(bytes)) / Math.log(bits));
+ const prepend = bytes > 0 ? '+' : '';
+ return prepend + parseFloat((bytes / Math.pow(bits, exponent))
+ .toFixed(decimals)) + ' ' + sizes[exponent];
+ }
+
+ _formatPercentage(size, delta) {
+ const oldSize = size - delta;
+
+ if (oldSize === 0) { return ''; }
+
+ const percentage = Math.round(Math.abs(delta * 100 / oldSize));
+ return '(' + (delta > 0 ? '+' : '-') + percentage + '%)';
+ }
+
+ _computeBinaryClass(delta) {
+ if (delta === 0) { return; }
+ return delta >= 0 ? 'added' : 'removed';
+ }
+
+ /**
+ * @param {string} baseClass
+ * @param {string} path
+ */
+ _computeClass(baseClass, path) {
+ const classes = [];
+ if (baseClass) {
+ classes.push(baseClass);
+ }
+ if (path === this.COMMIT_MESSAGE_PATH || path === this.MERGE_LIST_PATH) {
+ classes.push('invisible');
+ }
+ return classes.join(' ');
+ }
+
+ _computePathClass(path, expandedFilesRecord) {
+ return this._isFileExpanded(path, expandedFilesRecord) ? 'expanded' : '';
+ }
+
+ _computeShowHideIcon(path, expandedFilesRecord) {
+ return this._isFileExpanded(path, expandedFilesRecord) ?
+ 'gr-icons:expand-less' : 'gr-icons:expand-more';
+ }
+
+ _computeFiles(filesByPath, changeComments, patchRange, reviewed, loading) {
+ // Polymer 2: check for undefined
+ if ([
+ filesByPath,
+ changeComments,
+ patchRange,
+ reviewed,
+ loading,
+ ].some(arg => arg === undefined)) {
+ return;
+ }
+
+ // Await all promises resolving from reload. @See Issue 9057
+ if (loading || !changeComments) { return; }
+
+ const commentedPaths = changeComments.getPaths(patchRange);
+ const files = Object.assign({}, filesByPath);
+ Object.keys(commentedPaths).forEach(commentedPath => {
+ if (files.hasOwnProperty(commentedPath)) { return; }
+ files[commentedPath] = {status: 'U'};
+ });
+ const reviewedSet = new Set(reviewed || []);
+ for (const filePath in files) {
+ if (!files.hasOwnProperty(filePath)) { continue; }
+ files[filePath].isReviewed = reviewedSet.has(filePath);
+ }
+
+ this._files = this._normalizeChangeFilesResponse(files);
+ }
+
+ _computeFilesShown(numFilesShown, files) {
+ // Polymer 2: check for undefined
+ if ([numFilesShown, files].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const previousNumFilesShown = this._shownFiles ?
+ this._shownFiles.length : 0;
+
+ const filesShown = files.slice(0, numFilesShown);
+ this.fire('files-shown-changed', {length: filesShown.length});
+
+ // Start the timer for the rendering work hwere because this is where the
+ // _shownFiles property is being set, and _shownFiles is used in the
+ // dom-repeat binding.
+ this.$.reporting.time(RENDER_TIMING_LABEL);
+
+ // How many more files are being shown (if it's an increase).
+ this._reportinShownFilesIncrement =
+ Math.max(0, filesShown.length - previousNumFilesShown);
+
+ return filesShown;
+ }
+
+ _updateDiffCursor() {
+ // Overwrite the cursor's list of diffs:
+ this.$.diffCursor.splice(
+ ...['diffs', 0, this.$.diffCursor.diffs.length].concat(this.diffs));
+ }
+
+ _filesChanged() {
+ if (this._files && this._files.length > 0) {
+ flush();
+ const files = Array.from(
+ dom(this.root).querySelectorAll('.file-row'));
+ this.$.fileCursor.stops = files;
+ this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true);
+ }
+ }
+
+ _incrementNumFilesShown() {
+ this.numFilesShown += this.fileListIncrement;
+ }
+
+ _computeFileListControlClass(numFilesShown, files) {
+ return numFilesShown >= files.length ? 'invisible' : '';
+ }
+
+ _computeIncrementText(numFilesShown, files) {
+ if (!files) { return ''; }
+ const text =
+ Math.min(this.fileListIncrement, files.length - numFilesShown);
+ return 'Show ' + text + ' more';
+ }
+
+ _computeShowAllText(files) {
+ if (!files) { return ''; }
+ return 'Show all ' + files.length + ' files';
+ }
+
+ _computeWarnShowAll(files) {
+ return files.length > WARN_SHOW_ALL_THRESHOLD;
+ }
+
+ _computeShowAllWarning(files) {
+ if (!this._computeWarnShowAll(files)) { return ''; }
+ return 'Warning: showing all ' + files.length +
+ ' files may take several seconds.';
+ }
+
+ _showAllFiles() {
+ this.numFilesShown = this._files.length;
+ }
+
+ _computePatchSetDescription(revisions, patchNum) {
+ // Polymer 2: check for undefined
+ if ([revisions, patchNum].some(arg => arg === undefined)) {
+ return '';
+ }
+
+ const rev = this.getRevisionByPatchNum(revisions, patchNum);
+ return (rev && rev.description) ?
+ rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
+ }
+
+ /**
+ * Get a descriptive label for use in the status indicator's tooltip and
+ * ARIA label.
+ *
+ * @param {string} status
+ * @return {string}
+ */
+ _computeFileStatusLabel(status) {
+ const statusCode = this._computeFileStatus(status);
+ return FileStatus.hasOwnProperty(statusCode) ?
+ FileStatus[statusCode] : 'Status Unknown';
+ }
+
+ _isFileExpanded(path, expandedFilesRecord) {
+ return expandedFilesRecord.base.includes(path);
+ }
+
+ _onLineSelected(e, detail) {
+ this.$.diffCursor.moveToLineNumber(detail.number, detail.side,
+ detail.path);
+ }
+
+ _computeExpandedFiles(expandedCount, totalCount) {
+ if (expandedCount === 0) {
+ return GrFileListConstants.FilesExpandedState.NONE;
+ } else if (expandedCount === totalCount) {
+ return GrFileListConstants.FilesExpandedState.ALL;
+ }
+ return GrFileListConstants.FilesExpandedState.SOME;
+ }
+
+ /**
+ * Handle splices to the list of expanded file paths. If there are any new
+ * entries in the expanded list, then render each diff corresponding in
+ * order by waiting for the previous diff to finish before starting the next
+ * one.
+ *
+ * @param {!Array} record The splice record in the expanded paths list.
+ */
+ _expandedPathsChanged(record) {
+ // Clear content for any diffs that are not open so if they get re-opened
+ // the stale content does not flash before it is cleared and reloaded.
+ const collapsedDiffs = this.diffs.filter(diff =>
+ this._expandedFilePaths.indexOf(diff.path) === -1);
+ this._clearCollapsedDiffs(collapsedDiffs);
+
+ if (!record) { return; } // Happens after "Collapse all" clicked.
+
+ this.filesExpanded = this._computeExpandedFiles(
+ this._expandedFilePaths.length, this._files.length);
+
+ // Find the paths introduced by the new index splices:
+ const newPaths = record.indexSplices
+ .map(splice => splice.object.slice(
+ splice.index, splice.index + splice.addedCount))
+ .reduce((acc, paths) => acc.concat(paths), []);
+
+ // Required so that the newly created diff view is included in this.diffs.
+ flush();
+
+ this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
+
+ if (newPaths.length) {
+ this._renderInOrder(newPaths, this.diffs, newPaths.length);
+ }
+
+ this._updateDiffCursor();
+ this.$.diffCursor.handleDiffUpdate();
+ }
+
+ _clearCollapsedDiffs(collapsedDiffs) {
+ for (const diff of collapsedDiffs) {
+ diff.cancel();
+ diff.clearDiffContent();
+ }
+ }
+
+ /**
+ * Given an array of paths and a NodeList of diff elements, render the diff
+ * for each path in order, awaiting the previous render to complete before
+ * continung.
+ *
+ * @param {!Array<string>} paths
+ * @param {!NodeList<!Object>} diffElements (GrDiffHostElement)
+ * @param {number} initialCount The total number of paths in the pass. This
+ * is used to generate log messages.
+ * @return {!Promise}
+ */
+ _renderInOrder(paths, diffElements, initialCount) {
+ let iter = 0;
+
+ return (new Promise(resolve => {
+ this.fire('reload-drafts', {resolve});
+ })).then(() => this.asyncForeach(paths, (path, cancel) => {
+ this._cancelForEachDiff = cancel;
+
+ iter++;
+ console.log('Expanding diff', iter, 'of', initialCount, ':',
+ path);
+ const diffElem = this._findDiffByPath(path, diffElements);
+ if (!diffElem) {
+ console.warn(`Did not find <gr-diff-host> element for ${path}`);
+ return Promise.resolve();
+ }
+ diffElem.comments = this.changeComments.getCommentsBySideForPath(
+ path, this.patchRange, this.projectConfig);
+ const promises = [diffElem.reload()];
+ if (this._loggedIn && !this.diffPrefs.manual_review) {
+ promises.push(this._reviewFile(path, true));
+ }
+ return Promise.all(promises);
+ }).then(() => {
+ this._cancelForEachDiff = null;
+ this._nextRenderParams = null;
+ console.log('Finished expanding', initialCount, 'diff(s)');
+ this.$.reporting.timeEndWithAverage(EXPAND_ALL_TIMING_LABEL,
+ EXPAND_ALL_AVG_TIMING_LABEL, initialCount);
+ this.$.diffCursor.handleDiffUpdate();
+ }));
+ }
+
+ /** Cancel the rendering work of every diff in the list */
+ _cancelDiffs() {
+ if (this._cancelForEachDiff) { this._cancelForEachDiff(); }
+ this._forEachDiff(d => d.cancel());
+ }
+
+ /**
+ * In the given NodeList of diff elements, find the diff for the given path.
+ *
+ * @param {string} path
+ * @param {!NodeList<!Object>} diffElements (GrDiffElement)
+ * @return {!Object|undefined} (GrDiffElement)
+ */
+ _findDiffByPath(path, diffElements) {
+ for (let i = 0; i < diffElements.length; i++) {
+ if (diffElements[i].path === path) {
+ return diffElements[i];
+ }
+ }
+ }
+
+ /**
+ * Reset the comments of a modified thread
+ *
+ * @param {string} rootId
+ * @param {string} path
+ */
+ reloadCommentsForThreadWithRootId(rootId, path) {
+ // Don't bother continuing if we already know that the path that contains
+ // the updated comment thread is not expanded.
+ if (!this._expandedFilePaths.includes(path)) { return; }
+ const diff = this.diffs.find(d => d.path === path);
+
+ const threadEl = diff.getThreadEls().find(t => t.rootId === rootId);
+ if (!threadEl) { return; }
+
+ const newComments = this.changeComments.getCommentsForThread(rootId);
+
+ // If newComments is null, it means that a single draft was
+ // removed from a thread in the thread view, and the thread should
+ // no longer exist. Remove the existing thread element in the diff
+ // view.
+ if (!newComments) {
+ threadEl.fireRemoveSelf();
+ return;
+ }
+
+ // Comments are not returned with the commentSide attribute from
+ // the api, but it's necessary to be stored on the diff's
+ // comments due to use in the _handleCommentUpdate function.
+ // The comment thread already has a side associated with it, so
+ // set the comment's side to match.
+ threadEl.comments = newComments.map(c => Object.assign(
+ c, {__commentSide: threadEl.commentSide}
+ ));
+ flush();
+ return;
+ }
+
+ _handleEscKey(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+ e.preventDefault();
+ this._displayLine = false;
+ }
+
+ /**
+ * Update the loading class for the file list rows. The update is inside a
+ * debouncer so that the file list doesn't flash gray when the API requests
+ * are reasonably fast.
+ *
+ * @param {boolean} loading
+ */
+ _loadingChanged(loading) {
+ this.debounce('loading-change', () => {
+ // Only show set the loading if there have been files loaded to show. In
+ // this way, the gray loading style is not shown on initial loads.
+ this.classList.toggle('loading', loading && !!this._files.length);
+ }, LOADING_DEBOUNCE_INTERVAL);
+ }
+
+ _editModeChanged(editMode) {
+ this.classList.toggle('editMode', editMode);
+ }
+
+ _computeReviewedClass(isReviewed) {
+ return isReviewed ? 'isReviewed' : '';
+ }
+
+ _computeReviewedText(isReviewed) {
+ return isReviewed ? 'MARK UNREVIEWED' : 'MARK REVIEWED';
+ }
+
+ /**
+ * Given a file path, return whether that path should have visible size bars
+ * and be included in the size bars calculation.
+ *
+ * @param {string} path
+ * @return {boolean}
+ */
+ _showBarsForPath(path) {
+ return path !== this.COMMIT_MESSAGE_PATH && path !== this.MERGE_LIST_PATH;
+ }
+
+ /**
+ * Compute size bar layout values from the file list.
+ *
+ * @return {Gerrit.LayoutStats|undefined}
+ *
+ */
+ _computeSizeBarLayout(shownFilesRecord) {
+ if (!shownFilesRecord || !shownFilesRecord.base) { return undefined; }
+ const stats = {
+ maxInserted: 0,
+ maxDeleted: 0,
+ maxAdditionWidth: 0,
+ maxDeletionWidth: 0,
+ deletionOffset: 0,
+ };
+ shownFilesRecord.base
+ .filter(f => this._showBarsForPath(f.__path))
+ .forEach(f => {
+ if (f.lines_inserted) {
+ stats.maxInserted = Math.max(stats.maxInserted, f.lines_inserted);
+ }
+ if (f.lines_deleted) {
+ stats.maxDeleted = Math.max(stats.maxDeleted, f.lines_deleted);
+ }
+ });
+ const ratio = stats.maxInserted / (stats.maxInserted + stats.maxDeleted);
+ if (!isNaN(ratio)) {
+ stats.maxAdditionWidth =
+ (SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH) * ratio;
+ stats.maxDeletionWidth =
+ SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH - stats.maxAdditionWidth;
+ stats.deletionOffset = stats.maxAdditionWidth + SIZE_BAR_GAP_WIDTH;
+ }
+ return stats;
+ }
+
+ /**
+ * Get the width of the addition bar for a file.
+ *
+ * @param {Object} file
+ * @param {Gerrit.LayoutStats} stats
+ * @return {number}
+ */
+ _computeBarAdditionWidth(file, stats) {
+ if (stats.maxInserted === 0 ||
+ !file.lines_inserted ||
+ !this._showBarsForPath(file.__path)) {
+ return 0;
+ }
+ const width =
+ stats.maxAdditionWidth * file.lines_inserted / stats.maxInserted;
+ return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width);
+ }
+
+ /**
+ * Get the x-offset of the addition bar for a file.
+ *
+ * @param {Object} file
+ * @param {Gerrit.LayoutStats} stats
+ * @return {number}
+ */
+ _computeBarAdditionX(file, stats) {
+ return stats.maxAdditionWidth -
+ this._computeBarAdditionWidth(file, stats);
+ }
+
+ /**
+ * Get the width of the deletion bar for a file.
+ *
+ * @param {Object} file
+ * @param {Gerrit.LayoutStats} stats
+ * @return {number}
+ */
+ _computeBarDeletionWidth(file, stats) {
+ if (stats.maxDeleted === 0 ||
+ !file.lines_deleted ||
+ !this._showBarsForPath(file.__path)) {
+ return 0;
+ }
+ const width =
+ stats.maxDeletionWidth * file.lines_deleted / stats.maxDeleted;
+ return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width);
+ }
+
+ /**
+ * Get the x-offset of the deletion bar for a file.
+ *
+ * @param {Gerrit.LayoutStats} stats
+ *
+ * @return {number}
+ */
+ _computeBarDeletionX(stats) {
+ return stats.deletionOffset;
+ }
+
+ _computeShowSizeBars(userPrefs) {
+ return !!userPrefs.size_bar_in_change_table;
+ }
+
+ _computeSizeBarsClass(showSizeBars, path) {
+ let hideClass = '';
+ if (!showSizeBars) {
+ hideClass = 'hide';
+ } else if (!this._showBarsForPath(path)) {
+ hideClass = 'invisible';
+ }
+ return `sizeBars desktop ${hideClass}`;
+ }
+
+ /**
+ * Shows registered dynamic columns iff the 'header', 'content' and
+ * 'summary' endpoints are regiestered the exact same number of times.
+ * Ideally, there should be a better way to enforce the expectation of the
+ * dependencies between dynamic endpoints.
+ */
+ _computeShowDynamicColumns(
+ headerEndpoints, contentEndpoints, summaryEndpoints) {
+ return headerEndpoints && contentEndpoints && summaryEndpoints &&
+ headerEndpoints.length === contentEndpoints.length &&
+ headerEndpoints.length === summaryEndpoints.length;
+ }
+
+ /**
+ * Returns true if none of the inline diffs have been expanded.
+ *
+ * @return {boolean}
+ */
+ _noDiffsExpanded() {
+ return this.filesExpanded === GrFileListConstants.FilesExpandedState.NONE;
+ }
+
+ /**
+ * Method to call via binding when each file list row is rendered. This
+ * allows approximate detection of when the dom-repeat has completed
+ * rendering.
+ *
+ * @param {number} index The index of the row being rendered.
+ * @return {string} an empty string.
+ */
+ _reportRenderedRow(index) {
+ if (index === this._shownFiles.length - 1) {
+ this.async(() => {
+ this.$.reporting.timeEndWithAverage(RENDER_TIMING_LABEL,
+ RENDER_AVG_TIMING_LABEL, this._reportinShownFilesIncrement);
+ }, 1);
+ }
+ return '';
+ }
+
+ _reviewedTitle(reviewed) {
+ if (reviewed) {
+ return 'Mark as not reviewed (shortcut: r)';
+ }
+
+ return 'Mark as reviewed (shortcut: r)';
+ }
+
+ _handleReloadingDiffPreference() {
+ this._getDiffPreferences().then(prefs => {
+ this.diffPrefs = prefs;
+ });
+ }
+}
+
+customElements.define(GrFileList.is, GrFileList);
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
new file mode 100644
index 0000000..9652156
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
@@ -0,0 +1,444 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ }
+ .row {
+ align-items: center;
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ min-height: calc(var(--line-height-normal) + 2*var(--spacing-s));
+ padding: var(--spacing-xs) var(--spacing-l) var(--spacing-xs) calc(var(--spacing-l) - .35rem);
+ }
+ :host(.loading) .row {
+ opacity: .5;
+ };
+ :host(.editMode) .hideOnEdit {
+ display: none;
+ }
+ .showOnEdit {
+ display: none;
+ }
+ :host(.editMode) .showOnEdit {
+ display: initial;
+ }
+ .invisible {
+ visibility: hidden;
+ }
+ .header-row {
+ background-color: var(--background-color-secondary);
+ }
+ .controlRow {
+ align-items: center;
+ display: flex;
+ height: 2.25em;
+ justify-content: center;
+ }
+ .controlRow.invisible,
+ .show-hide.invisible {
+ display: none;
+ }
+ .reviewed,
+ .status {
+ align-items: center;
+ display: inline-flex;
+ }
+ .reviewed,
+ .status {
+ display: inline-block;
+ text-align: left;
+ width: 1.5em;
+ }
+ .file-row {
+ cursor: pointer;
+ }
+ .file-row.expanded {
+ border-bottom: 1px solid var(--border-color);
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0;
+ /* Has to visible above the diff view, and by default has a lower
+ z-index. setting to 1 places it directly above. */
+ z-index: 1;
+ }
+ .file-row:hover {
+ background-color: var(--hover-background-color);
+ }
+ .file-row.selected {
+ background-color: var(--selection-background-color);
+ }
+ .file-row.expanded,
+ .file-row.expanded:hover {
+ background-color: var(--expanded-background-color);
+ }
+ .path {
+ cursor: pointer;
+ flex: 1;
+ /* Wrap it into multiple lines if too long. */
+ white-space: normal;
+ word-break: break-word;
+ }
+ .oldPath {
+ color: var(--deemphasized-text-color);
+ }
+ .header-stats {
+ text-align: center;
+ min-width: 7.5em;
+ }
+ .stats {
+ text-align: right;
+ min-width: 7.5em;
+ }
+ .comments {
+ padding-left: var(--spacing-l);
+ min-width: 7.5em;
+ }
+ .row:not(.header-row) .stats,
+ .total-stats {
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-mono);
+ line-height: var(--line-height-mono);
+ display: flex;
+ }
+ .sizeBars {
+ margin-left: var(--spacing-m);
+ min-width: 7em;
+ text-align: center;
+ }
+ .sizeBars.hide {
+ display: none;
+ }
+ .added,
+ .removed {
+ display: inline-block;
+ min-width: 3.5em;
+ }
+ .added {
+ color: var(--vote-text-color-recommended);
+ }
+ .removed {
+ color: var(--vote-text-color-disliked);
+ text-align: left;
+ min-width: 4em;
+ padding-left: var(--spacing-s);
+ }
+ .drafts {
+ color: #C62828;
+ font-weight: var(--font-weight-bold);
+ }
+ .show-hide {
+ margin-left: var(--spacing-s);
+ width: 1.9em;
+ }
+ .fileListButton {
+ margin: var(--spacing-m);
+ }
+ .totalChanges {
+ justify-content: flex-end;
+ text-align: right;
+ }
+ .warning {
+ color: var(--deemphasized-text-color);
+ }
+ input.show-hide {
+ display: none;
+ }
+ label.show-hide {
+ cursor: pointer;
+ display: block;
+ min-width: 2em;
+ }
+ gr-diff {
+ display: block;
+ overflow-x: auto;
+ }
+ .truncatedFileName {
+ display: none;
+ }
+ .mobile {
+ display: none;
+ }
+ .reviewed {
+ margin-left: var(--spacing-xxl);
+ width: 15em;
+ }
+ .reviewed label {
+ color: var(--link-color);
+ opacity: 0;
+ justify-content: flex-end;
+ width: 100%;
+ }
+ .reviewed label:hover {
+ cursor: pointer;
+ opacity: 100;
+ }
+ .row:focus {
+ outline: none;
+ }
+ .row:hover .reviewed label,
+ .row:focus .reviewed label,
+ .row.expanded .reviewed label {
+ opacity: 100;
+ }
+ .reviewed input {
+ display: none;
+ }
+ .reviewedLabel {
+ color: var(--deemphasized-text-color);
+ margin-right: var(--spacing-l);
+ opacity: 0;
+ }
+ .reviewedLabel.isReviewed {
+ display: initial;
+ opacity: 100;
+ }
+ .editFileControls {
+ width: 7em;
+ }
+ .markReviewed,
+ .pathLink {
+ display: inline-block;
+ margin: -2px 0;
+ padding: var(--spacing-s) 0;
+ text-decoration: none;
+ }
+ .pathLink:hover {
+ text-decoration: underline;
+ }
+
+ /** copy on file path **/
+ .pathLink gr-copy-clipboard,
+ .oldPath gr-copy-clipboard {
+ display: inline-block;
+ visibility: hidden;
+ vertical-align: bottom;
+ text-decoration: none;
+ --gr-button: {
+ padding: 0px;
+ }
+ }
+ .pathLink:hover gr-copy-clipboard,
+ .oldPath:hover gr-copy-clipboard {
+ visibility: visible;
+ }
+
+ /** small screen breakpoint: 768px */
+ @media screen and (max-width: 55em) {
+ .desktop {
+ display: none;
+ }
+ .mobile {
+ display: block;
+ }
+ .row.selected {
+ background-color: var(--view-background-color);
+ }
+ .stats {
+ display: none;
+ }
+ .reviewed,
+ .status {
+ justify-content: flex-start;
+ }
+ .reviewed {
+ display: none;
+ }
+ .comments {
+ min-width: initial;
+ }
+ .expanded .fullFileName,
+ .truncatedFileName {
+ display: inline;
+ }
+ .expanded .truncatedFileName,
+ .fullFileName {
+ display: none;
+ }
+ }
+ </style>
+ <div id="container" on-click="_handleFileListClick">
+ <div class="header-row row">
+ <div class="status"></div>
+ <div class="path">File</div>
+ <div class="comments">Comments</div>
+ <div class="sizeBars">Size</div>
+ <div class="header-stats">Delta</div>
+ <template is="dom-if" if="[[_showDynamicColumns]]">
+ <template is="dom-repeat" items="[[_dynamicHeaderEndpoints]]" as="headerEndpoint">
+ <gr-endpoint-decorator name\$="[[headerEndpoint]]">
+ </gr-endpoint-decorator>
+ </template>
+ </template>
+ <!-- Empty div here exists to keep spacing in sync with file rows. -->
+ <div class="reviewed hideOnEdit" hidden\$="[[!_loggedIn]]"></div>
+ <div class="editFileControls showOnEdit"></div>
+ <div class="show-hide"></div>
+ </div>
+
+ <template is="dom-repeat" items="[[_shownFiles]]" id="files" as="file" initial-count="[[fileListIncrement]]" target-framerate="1">
+ [[_reportRenderedRow(index)]]
+ <div class="stickyArea">
+ <div class\$="file-row row [[_computePathClass(file.__path, _expandedFilePaths.*)]]" data-path\$="[[file.__path]]" tabindex="-1">
+ <div class\$="[[_computeClass('status', file.__path)]]" tabindex="0" title\$="[[_computeFileStatusLabel(file.status)]]" aria-label\$="[[_computeFileStatusLabel(file.status)]]">
+ [[_computeFileStatus(file.status)]]
+ </div>
+ <!-- TODO: Remove data-url as it appears its not used -->
+ <span data-url="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]" class="path">
+ <a class="pathLink" href\$="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]">
+ <span title\$="[[computeDisplayPath(file.__path)]]" class="fullFileName">
+ [[computeDisplayPath(file.__path)]]
+ </span>
+ <span title\$="[[computeDisplayPath(file.__path)]]" class="truncatedFileName">
+ [[computeTruncatedPath(file.__path)]]
+ </span>
+ <gr-copy-clipboard hide-input="" text="[[file.__path]]"></gr-copy-clipboard>
+ </a>
+ <template is="dom-if" if="[[file.old_path]]">
+ <div class="oldPath" title\$="[[file.old_path]]">
+ [[file.old_path]]
+ <gr-copy-clipboard hide-input="" text="[[file.old_path]]"></gr-copy-clipboard>
+ </div>
+ </template>
+ </span>
+ <div class="comments desktop">
+ <span class="drafts">
+ [[_computeDraftsString(changeComments, patchRange, file.__path)]]
+ </span>
+ [[_computeCommentsString(changeComments, patchRange, file.__path)]]
+ </div>
+ <div class="comments mobile">
+ <span class="drafts">
+ [[_computeDraftsStringMobile(changeComments, patchRange,
+ file.__path)]]
+ </span>
+ [[_computeCommentsStringMobile(changeComments, patchRange,
+ file.__path)]]
+ </div>
+ <div class\$="[[_computeSizeBarsClass(_showSizeBars, file.__path)]]">
+ <svg width="61" height="8">
+ <rect x\$="[[_computeBarAdditionX(file, _sizeBarLayout)]]" y="0" height="8" fill="#388E3C" width\$="[[_computeBarAdditionWidth(file, _sizeBarLayout)]]"></rect>
+ <rect x\$="[[_computeBarDeletionX(_sizeBarLayout)]]" y="0" height="8" fill="#D32F2F" width\$="[[_computeBarDeletionWidth(file, _sizeBarLayout)]]"></rect>
+ </svg>
+ </div>
+ <div class\$="[[_computeClass('stats', file.__path)]]">
+ <span class="added" tabindex="0" aria-label\$="[[file.lines_inserted]] lines added" hidden\$="[[file.binary]]">
+ +[[file.lines_inserted]]
+ </span>
+ <span class="removed" tabindex="0" aria-label\$="[[file.lines_deleted]] lines removed" hidden\$="[[file.binary]]">
+ -[[file.lines_deleted]]
+ </span>
+ <span class\$="[[_computeBinaryClass(file.size_delta)]]" hidden\$="[[!file.binary]]">
+ [[_formatBytes(file.size_delta)]]
+ [[_formatPercentage(file.size, file.size_delta)]]
+ </span>
+ </div>
+ <template is="dom-if" if="[[_showDynamicColumns]]">
+ <template is="dom-repeat" items="[[_dynamicContentEndpoints]]" as="contentEndpoint">
+ <div class\$="[[_computeClass('', file.__path)]]">
+ <gr-endpoint-decorator name="[[contentEndpoint]]">
+ <gr-endpoint-param name="changeNum" value="[[changeNum]]">
+ </gr-endpoint-param>
+ <gr-endpoint-param name="patchRange" value="[[patchRange]]">
+ </gr-endpoint-param>
+ <gr-endpoint-param name="path" value="[[file.__path]]">
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </div>
+ </template>
+ </template>
+ <div class="reviewed hideOnEdit" hidden\$="[[!_loggedIn]]" hidden="">
+ <span class\$="reviewedLabel [[_computeReviewedClass(file.isReviewed)]]">Reviewed</span>
+ <label>
+ <input class="reviewed" type="checkbox" checked="[[file.isReviewed]]">
+ <span class="markReviewed" title\$="[[_reviewedTitle(file.isReviewed)]]">[[_computeReviewedText(file.isReviewed)]]</span>
+ </label>
+ </div>
+ <div class="editFileControls showOnEdit">
+ <template is="dom-if" if="[[editMode]]">
+ <gr-edit-file-controls class\$="[[_computeClass('', file.__path)]]" file-path="[[file.__path]]"></gr-edit-file-controls>
+ </template>
+ </div>
+ <div class="show-hide">
+ <label class="show-hide" data-path\$="[[file.__path]]" data-expand="true">
+ <input type="checkbox" class="show-hide" checked\$="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]" data-path\$="[[file.__path]]" data-expand="true">
+ <iron-icon id="icon" icon="[[_computeShowHideIcon(file.__path, _expandedFilePaths.*)]]">
+ </iron-icon>
+ </label>
+ </div>
+ </div>
+ <template is="dom-if" if="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]">
+ <gr-diff-host no-auto-render="" show-load-failure="" display-line="[[_displayLine]]" hidden="[[!_isFileExpanded(file.__path, _expandedFilePaths.*)]]" change-num="[[changeNum]]" patch-range="[[patchRange]]" path="[[file.__path]]" prefs="[[diffPrefs]]" project-name="[[change.project]]" on-line-selected="_onLineSelected" no-render-on-prefs-change="" view-mode="[[diffViewMode]]"></gr-diff-host>
+ </template>
+ </div>
+ </template>
+ </div>
+ <div class="row totalChanges" hidden\$="[[_hideChangeTotals]]">
+ <div class="total-stats">
+ <span class="added" tabindex="0" aria-label\$="[[_patchChange.inserted]] lines added">
+ +[[_patchChange.inserted]]
+ </span>
+ <span class="removed" tabindex="0" aria-label\$="[[_patchChange.deleted]] lines removed">
+ -[[_patchChange.deleted]]
+ </span>
+ </div>
+ <template is="dom-if" if="[[_showDynamicColumns]]">
+ <template is="dom-repeat" items="[[_dynamicSummaryEndpoints]]" as="summaryEndpoint">
+ <gr-endpoint-decorator name="[[summaryEndpoint]]">
+ </gr-endpoint-decorator>
+ </template>
+ </template>
+ <!-- Empty div here exists to keep spacing in sync with file rows. -->
+ <div class="reviewed hideOnEdit" hidden\$="[[!_loggedIn]]"></div>
+ <div class="editFileControls showOnEdit"></div>
+ <div class="show-hide"></div>
+ </div>
+ <div class="row totalChanges" hidden\$="[[_hideBinaryChangeTotals]]">
+ <div class="total-stats">
+ <span class="added" aria-label="Total lines added">
+ [[_formatBytes(_patchChange.size_delta_inserted)]]
+ [[_formatPercentage(_patchChange.total_size,
+ _patchChange.size_delta_inserted)]]
+ </span>
+ <span class="removed" aria-label="Total lines removed">
+ [[_formatBytes(_patchChange.size_delta_deleted)]]
+ [[_formatPercentage(_patchChange.total_size,
+ _patchChange.size_delta_deleted)]]
+ </span>
+ </div>
+ </div>
+ <div class\$="row controlRow [[_computeFileListControlClass(numFilesShown, _files)]]">
+ <gr-button class="fileListButton" id="incrementButton" link="" on-click="_incrementNumFilesShown">
+ [[_computeIncrementText(numFilesShown, _files)]]
+ </gr-button>
+ <gr-tooltip-content has-tooltip="[[_computeWarnShowAll(_files)]]" show-icon="[[_computeWarnShowAll(_files)]]" title\$="[[_computeShowAllWarning(_files)]]">
+ <gr-button class="fileListButton" id="showAllButton" link="" on-click="_showAllFiles">
+ [[_computeShowAllText(_files)]]
+ </gr-button><!--
+ --></gr-tooltip-content>
+ </div>
+ <gr-diff-preferences-dialog id="diffPreferencesDialog" diff-prefs="{{diffPrefs}}" on-reload-diff-preference="_handleReloadingDiffPreference">
+ </gr-diff-preferences-dialog>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ <gr-storage id="storage"></gr-storage>
+ <gr-diff-cursor id="diffCursor"></gr-diff-cursor>
+ <gr-cursor-manager id="fileCursor" scroll-behavior="keep-visible" focus-on-move="" cursor-target-class="selected"></gr-cursor-manager>
+ <gr-reporting id="reporting"></gr-reporting>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index af80ca8..96db120 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -19,21 +19,12 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-file-list</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<script src="/components/web-component-tester/data/a11ySuite.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="/bower_components/page/page.js"></script>
-<link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
-<link rel="import" href="gr-file-list.html">
-
-<script>void(0);</script>
+<script src="/node_modules/page/page.js"></script>
<dom-module id="comment-api-mock">
<template>
@@ -42,8 +33,7 @@
on-reload-drafts="_reloadDraftsWithCallback"></gr-file-list>
<gr-comment-api id="commentAPI"></gr-comment-api>
</template>
- <script src="../../diff/gr-comment-api/gr-comment-api-mock_test.js"></script>
-</dom-module>
+ </dom-module>
<test-fixture id="basic">
<template>
@@ -51,1389 +41,1823 @@
</template>
</test-fixture>
-<script>
- suite('gr-file-list tests', async () => {
- await readyToTest();
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../diff/gr-comment-api/gr-comment-api.js';
+import '../../../scripts/util.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import './gr-file-list.js';
+import '../../diff/gr-comment-api/gr-comment-api-mock_test.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-file-list tests', () => {
+ const kb = window.Gerrit.KeyboardShortcutBinder;
+ kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
+ kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
+ kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
+ kb.bindShortcut(kb.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
+ kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
+ kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
+ kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
+ kb.bindShortcut(kb.Shortcut.OPEN_LAST_FILE, '[');
+ kb.bindShortcut(kb.Shortcut.OPEN_FIRST_FILE, ']');
+ kb.bindShortcut(kb.Shortcut.OPEN_FILE, 'o');
+ kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
+ kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
- const kb = window.Gerrit.KeyboardShortcutBinder;
- kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
- kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
- kb.bindShortcut(kb.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
- kb.bindShortcut(kb.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
- kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
- kb.bindShortcut(kb.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
- kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
- kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
- kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
- kb.bindShortcut(kb.Shortcut.OPEN_LAST_FILE, '[');
- kb.bindShortcut(kb.Shortcut.OPEN_FIRST_FILE, ']');
- kb.bindShortcut(kb.Shortcut.OPEN_FILE, 'o');
- kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
- kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
- kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
- kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
+ let element;
+ let commentApiWrapper;
+ let sandbox;
+ let saveStub;
+ let loadCommentSpy;
- let element;
- let commentApiWrapper;
- let sandbox;
- let saveStub;
- let loadCommentSpy;
-
- suite('basic tests', () => {
- setup(done => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- getPreferences() { return Promise.resolve({}); },
- getDiffPreferences() { return Promise.resolve({}); },
- getDiffComments() { return Promise.resolve({}); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
- getAccountCapabilities() { return Promise.resolve({}); },
- });
- stub('gr-date-formatter', {
- _loadTimeFormat() { return Promise.resolve(''); },
- });
- stub('gr-diff-host', {
- reload() { return Promise.resolve(); },
- });
-
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = fixture('basic');
- element = commentApiWrapper.$.fileList;
- loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
-
- // Stub methods on the changeComments object after changeComments has
- // been initialized.
- commentApiWrapper.loadComments().then(() => {
- sandbox.stub(element.changeComments, 'getPaths').returns({});
- sandbox.stub(element.changeComments, 'getCommentsBySideForPath')
- .returns({meta: {}, left: [], right: []});
- done();
- });
- element._loading = false;
- element.diffPrefs = {};
- element.numFilesShown = 200;
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
- saveStub = sandbox.stub(element, '_saveReviewedState',
- () => Promise.resolve());
+ suite('basic tests', () => {
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
+ getPreferences() { return Promise.resolve({}); },
+ getDiffPreferences() { return Promise.resolve({}); },
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ getAccountCapabilities() { return Promise.resolve({}); },
+ });
+ stub('gr-date-formatter', {
+ _loadTimeFormat() { return Promise.resolve(''); },
+ });
+ stub('gr-diff-host', {
+ reload() { return Promise.resolve(); },
});
- teardown(() => {
- sandbox.restore();
+ // Element must be wrapped in an element with direct access to the
+ // comment API.
+ commentApiWrapper = fixture('basic');
+ element = commentApiWrapper.$.fileList;
+ loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
+
+ // Stub methods on the changeComments object after changeComments has
+ // been initialized.
+ commentApiWrapper.loadComments().then(() => {
+ sandbox.stub(element.changeComments, 'getPaths').returns({});
+ sandbox.stub(element.changeComments, 'getCommentsBySideForPath')
+ .returns({meta: {}, left: [], right: []});
+ done();
});
+ element._loading = false;
+ element.diffPrefs = {};
+ element.numFilesShown = 200;
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ saveStub = sandbox.stub(element, '_saveReviewedState',
+ () => Promise.resolve());
+ });
- test('correct number of files are shown', () => {
- element.fileListIncrement = 300;
- element._filesByPath = _.range(500)
- .reduce((_filesByPath, i) => {
- _filesByPath['/file' + i] = {lines_inserted: 9};
- return _filesByPath;
- }, {});
+ teardown(() => {
+ sandbox.restore();
+ });
- flushAsynchronousOperations();
- assert.equal(
- Polymer.dom(element.root).querySelectorAll('.file-row').length,
- element.numFilesShown);
- const controlRow = element.shadowRoot
- .querySelector('.controlRow');
- assert.isFalse(controlRow.classList.contains('invisible'));
- assert.equal(element.$.incrementButton.textContent.trim(),
- 'Show 300 more');
- assert.equal(element.$.showAllButton.textContent.trim(),
- 'Show all 500 files');
+ test('correct number of files are shown', () => {
+ element.fileListIncrement = 300;
+ element._filesByPath = _.range(500)
+ .reduce((_filesByPath, i) => {
+ _filesByPath['/file' + i] = {lines_inserted: 9};
+ return _filesByPath;
+ }, {});
- MockInteractions.tap(element.$.showAllButton);
- flushAsynchronousOperations();
+ flushAsynchronousOperations();
+ assert.equal(
+ dom(element.root).querySelectorAll('.file-row').length,
+ element.numFilesShown);
+ const controlRow = element.shadowRoot
+ .querySelector('.controlRow');
+ assert.isFalse(controlRow.classList.contains('invisible'));
+ assert.equal(element.$.incrementButton.textContent.trim(),
+ 'Show 300 more');
+ assert.equal(element.$.showAllButton.textContent.trim(),
+ 'Show all 500 files');
- assert.equal(element.numFilesShown, 500);
- assert.equal(element._shownFiles.length, 500);
- assert.isTrue(controlRow.classList.contains('invisible'));
+ MockInteractions.tap(element.$.showAllButton);
+ flushAsynchronousOperations();
+
+ assert.equal(element.numFilesShown, 500);
+ assert.equal(element._shownFiles.length, 500);
+ assert.isTrue(controlRow.classList.contains('invisible'));
+ });
+
+ test('rendering each row calls the _reportRenderedRow method', () => {
+ const renderedStub = sandbox.stub(element, '_reportRenderedRow');
+ element._filesByPath = _.range(10)
+ .reduce((_filesByPath, i) => {
+ _filesByPath['/file' + i] = {lines_inserted: 9};
+ return _filesByPath;
+ }, {});
+ flushAsynchronousOperations();
+ assert.equal(
+ dom(element.root).querySelectorAll('.file-row').length, 10);
+ assert.equal(renderedStub.callCount, 10);
+ });
+
+ test('calculate totals for patch number', () => {
+ element._filesByPath = {
+ '/COMMIT_MSG': {
+ lines_inserted: 9,
+ },
+ '/MERGE_LIST': {
+ lines_inserted: 9,
+ },
+ 'file_added_in_rev2.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ size_delta: 10,
+ size: 100,
+ },
+ 'myfile.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ size_delta: 10,
+ size: 100,
+ },
+ };
+
+ assert.deepEqual(element._patchChange, {
+ inserted: 2,
+ deleted: 2,
+ size_delta_inserted: 0,
+ size_delta_deleted: 0,
+ total_size: 0,
});
+ assert.isTrue(element._hideBinaryChangeTotals);
+ assert.isFalse(element._hideChangeTotals);
- test('rendering each row calls the _reportRenderedRow method', () => {
- const renderedStub = sandbox.stub(element, '_reportRenderedRow');
- element._filesByPath = _.range(10)
- .reduce((_filesByPath, i) => {
- _filesByPath['/file' + i] = {lines_inserted: 9};
- return _filesByPath;
- }, {});
- flushAsynchronousOperations();
- assert.equal(
- Polymer.dom(element.root).querySelectorAll('.file-row').length, 10);
- assert.equal(renderedStub.callCount, 10);
+ // Test with a commit message that isn't the first file.
+ element._filesByPath = {
+ 'file_added_in_rev2.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ },
+ '/COMMIT_MSG': {
+ lines_inserted: 9,
+ },
+ '/MERGE_LIST': {
+ lines_inserted: 9,
+ },
+ 'myfile.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ },
+ };
+
+ assert.deepEqual(element._patchChange, {
+ inserted: 2,
+ deleted: 2,
+ size_delta_inserted: 0,
+ size_delta_deleted: 0,
+ total_size: 0,
});
+ assert.isTrue(element._hideBinaryChangeTotals);
+ assert.isFalse(element._hideChangeTotals);
- test('calculate totals for patch number', () => {
- element._filesByPath = {
- '/COMMIT_MSG': {
- lines_inserted: 9,
- },
- '/MERGE_LIST': {
- lines_inserted: 9,
- },
- 'file_added_in_rev2.txt': {
- lines_inserted: 1,
- lines_deleted: 1,
- size_delta: 10,
- size: 100,
- },
- 'myfile.txt': {
- lines_inserted: 1,
- lines_deleted: 1,
- size_delta: 10,
- size: 100,
- },
- };
+ // Test with no commit message.
+ element._filesByPath = {
+ 'file_added_in_rev2.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ },
+ 'myfile.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ },
+ };
- assert.deepEqual(element._patchChange, {
- inserted: 2,
- deleted: 2,
- size_delta_inserted: 0,
- size_delta_deleted: 0,
- total_size: 0,
- });
- assert.isTrue(element._hideBinaryChangeTotals);
- assert.isFalse(element._hideChangeTotals);
-
- // Test with a commit message that isn't the first file.
- element._filesByPath = {
- 'file_added_in_rev2.txt': {
- lines_inserted: 1,
- lines_deleted: 1,
- },
- '/COMMIT_MSG': {
- lines_inserted: 9,
- },
- '/MERGE_LIST': {
- lines_inserted: 9,
- },
- 'myfile.txt': {
- lines_inserted: 1,
- lines_deleted: 1,
- },
- };
-
- assert.deepEqual(element._patchChange, {
- inserted: 2,
- deleted: 2,
- size_delta_inserted: 0,
- size_delta_deleted: 0,
- total_size: 0,
- });
- assert.isTrue(element._hideBinaryChangeTotals);
- assert.isFalse(element._hideChangeTotals);
-
- // Test with no commit message.
- element._filesByPath = {
- 'file_added_in_rev2.txt': {
- lines_inserted: 1,
- lines_deleted: 1,
- },
- 'myfile.txt': {
- lines_inserted: 1,
- lines_deleted: 1,
- },
- };
-
- assert.deepEqual(element._patchChange, {
- inserted: 2,
- deleted: 2,
- size_delta_inserted: 0,
- size_delta_deleted: 0,
- total_size: 0,
- });
- assert.isTrue(element._hideBinaryChangeTotals);
- assert.isFalse(element._hideChangeTotals);
-
- // Test with files missing either lines_inserted or lines_deleted.
- element._filesByPath = {
- 'file_added_in_rev2.txt': {lines_inserted: 1},
- 'myfile.txt': {lines_deleted: 1},
- };
- assert.deepEqual(element._patchChange, {
- inserted: 1,
- deleted: 1,
- size_delta_inserted: 0,
- size_delta_deleted: 0,
- total_size: 0,
- });
- assert.isTrue(element._hideBinaryChangeTotals);
- assert.isFalse(element._hideChangeTotals);
+ assert.deepEqual(element._patchChange, {
+ inserted: 2,
+ deleted: 2,
+ size_delta_inserted: 0,
+ size_delta_deleted: 0,
+ total_size: 0,
});
+ assert.isTrue(element._hideBinaryChangeTotals);
+ assert.isFalse(element._hideChangeTotals);
- test('binary only files', () => {
- element._filesByPath = {
- '/COMMIT_MSG': {lines_inserted: 9},
- 'file_binary_1': {binary: true, size_delta: 10, size: 100},
- 'file_binary_2': {binary: true, size_delta: -5, size: 120},
- };
- assert.deepEqual(element._patchChange, {
- inserted: 0,
- deleted: 0,
- size_delta_inserted: 10,
- size_delta_deleted: -5,
- total_size: 220,
- });
- assert.isFalse(element._hideBinaryChangeTotals);
- assert.isTrue(element._hideChangeTotals);
+ // Test with files missing either lines_inserted or lines_deleted.
+ element._filesByPath = {
+ 'file_added_in_rev2.txt': {lines_inserted: 1},
+ 'myfile.txt': {lines_deleted: 1},
+ };
+ assert.deepEqual(element._patchChange, {
+ inserted: 1,
+ deleted: 1,
+ size_delta_inserted: 0,
+ size_delta_deleted: 0,
+ total_size: 0,
});
+ assert.isTrue(element._hideBinaryChangeTotals);
+ assert.isFalse(element._hideChangeTotals);
+ });
- test('binary and regular files', () => {
- element._filesByPath = {
- '/COMMIT_MSG': {lines_inserted: 9},
- 'file_binary_1': {binary: true, size_delta: 10, size: 100},
- 'file_binary_2': {binary: true, size_delta: -5, size: 120},
- 'myfile.txt': {lines_deleted: 5, size_delta: -10, size: 100},
- 'myfile2.txt': {lines_inserted: 10},
- };
- assert.deepEqual(element._patchChange, {
- inserted: 10,
- deleted: 5,
- size_delta_inserted: 10,
- size_delta_deleted: -5,
- total_size: 220,
- });
- assert.isFalse(element._hideBinaryChangeTotals);
- assert.isFalse(element._hideChangeTotals);
+ test('binary only files', () => {
+ element._filesByPath = {
+ '/COMMIT_MSG': {lines_inserted: 9},
+ 'file_binary_1': {binary: true, size_delta: 10, size: 100},
+ 'file_binary_2': {binary: true, size_delta: -5, size: 120},
+ };
+ assert.deepEqual(element._patchChange, {
+ inserted: 0,
+ deleted: 0,
+ size_delta_inserted: 10,
+ size_delta_deleted: -5,
+ total_size: 220,
});
+ assert.isFalse(element._hideBinaryChangeTotals);
+ assert.isTrue(element._hideChangeTotals);
+ });
- test('_formatBytes function', () => {
- const table = {
- '64': '+64 B',
- '1023': '+1023 B',
- '1024': '+1 KiB',
- '4096': '+4 KiB',
- '1073741824': '+1 GiB',
- '-64': '-64 B',
- '-1023': '-1023 B',
- '-1024': '-1 KiB',
- '-4096': '-4 KiB',
- '-1073741824': '-1 GiB',
- '0': '+/-0 B',
- };
+ test('binary and regular files', () => {
+ element._filesByPath = {
+ '/COMMIT_MSG': {lines_inserted: 9},
+ 'file_binary_1': {binary: true, size_delta: 10, size: 100},
+ 'file_binary_2': {binary: true, size_delta: -5, size: 120},
+ 'myfile.txt': {lines_deleted: 5, size_delta: -10, size: 100},
+ 'myfile2.txt': {lines_inserted: 10},
+ };
+ assert.deepEqual(element._patchChange, {
+ inserted: 10,
+ deleted: 5,
+ size_delta_inserted: 10,
+ size_delta_deleted: -5,
+ total_size: 220,
+ });
+ assert.isFalse(element._hideBinaryChangeTotals);
+ assert.isFalse(element._hideChangeTotals);
+ });
- for (const bytes in table) {
- if (table.hasOwnProperty(bytes)) {
- assert.equal(element._formatBytes(bytes), table[bytes]);
- }
+ test('_formatBytes function', () => {
+ const table = {
+ '64': '+64 B',
+ '1023': '+1023 B',
+ '1024': '+1 KiB',
+ '4096': '+4 KiB',
+ '1073741824': '+1 GiB',
+ '-64': '-64 B',
+ '-1023': '-1023 B',
+ '-1024': '-1 KiB',
+ '-4096': '-4 KiB',
+ '-1073741824': '-1 GiB',
+ '0': '+/-0 B',
+ };
+
+ for (const bytes in table) {
+ if (table.hasOwnProperty(bytes)) {
+ assert.equal(element._formatBytes(bytes), table[bytes]);
}
- });
+ }
+ });
- test('_formatPercentage function', () => {
- const table = [
- {size: 100,
- delta: 100,
- display: '',
+ test('_formatPercentage function', () => {
+ const table = [
+ {size: 100,
+ delta: 100,
+ display: '',
+ },
+ {size: 195060,
+ delta: 64,
+ display: '(+0%)',
+ },
+ {size: 195060,
+ delta: -64,
+ display: '(-0%)',
+ },
+ {size: 394892,
+ delta: -7128,
+ display: '(-2%)',
+ },
+ {size: 90,
+ delta: -10,
+ display: '(-10%)',
+ },
+ {size: 110,
+ delta: 10,
+ display: '(+10%)',
+ },
+ ];
+
+ for (const item of table) {
+ assert.equal(element._formatPercentage(
+ item.size, item.delta), item.display);
+ }
+ });
+
+ test('comment filtering', () => {
+ element.changeComments._comments = {
+ '/COMMIT_MSG': [
+ {patch_set: 1, message: 'Done', updated: '2017-02-08 16:40:49'},
+ {patch_set: 1, message: 'oh hay', updated: '2017-02-09 16:40:49'},
+ {patch_set: 2, message: 'hello', updated: '2017-02-10 16:40:49'},
+ ],
+ 'myfile.txt': [
+ {patch_set: 1, message: 'good news!', updated: '2017-02-08 16:40:49'},
+ {patch_set: 2, message: 'wat!?', updated: '2017-02-09 16:40:49'},
+ {patch_set: 2, message: 'hi', updated: '2017-02-10 16:40:49'},
+ ],
+ 'unresolved.file': [
+ {
+ patch_set: 2,
+ message: 'wat!?',
+ updated: '2017-02-09 16:40:49',
+ id: '1',
+ unresolved: true,
},
- {size: 195060,
- delta: 64,
- display: '(+0%)',
+ {
+ patch_set: 2,
+ message: 'hi',
+ updated: '2017-02-10 16:40:49',
+ id: '2',
+ in_reply_to: '1',
+ unresolved: false,
},
- {size: 195060,
- delta: -64,
- display: '(-0%)',
+ {
+ patch_set: 2,
+ message: 'good news!',
+ updated: '2017-02-08 16:40:49',
+ id: '3',
+ unresolved: true,
},
- {size: 394892,
- delta: -7128,
- display: '(-2%)',
+ ],
+ };
+ element.changeComments._drafts = {
+ '/COMMIT_MSG': [
+ {
+ patch_set: 1,
+ message: 'hi',
+ updated: '2017-02-15 16:40:49',
+ id: '5',
+ unresolved: true,
},
- {size: 90,
- delta: -10,
- display: '(-10%)',
+ {
+ patch_set: 1,
+ message: 'fyi',
+ updated: '2017-02-15 16:40:49',
+ id: '6',
+ unresolved: false,
},
- {size: 110,
- delta: 10,
- display: '(+10%)',
+ ],
+ 'unresolved.file': [
+ {
+ patch_set: 1,
+ message: 'hi',
+ updated: '2017-02-11 16:40:49',
+ id: '4',
+ unresolved: false,
},
- ];
+ ],
+ };
- for (const item of table) {
- assert.equal(element._formatPercentage(
- item.size, item.delta), item.display);
- }
- });
+ const parentTo1 = {
+ basePatchNum: 'PARENT',
+ patchNum: '1',
+ };
- test('comment filtering', () => {
- element.changeComments._comments = {
- '/COMMIT_MSG': [
- {patch_set: 1, message: 'Done', updated: '2017-02-08 16:40:49'},
- {patch_set: 1, message: 'oh hay', updated: '2017-02-09 16:40:49'},
- {patch_set: 2, message: 'hello', updated: '2017-02-10 16:40:49'},
- ],
- 'myfile.txt': [
- {patch_set: 1, message: 'good news!', updated: '2017-02-08 16:40:49'},
- {patch_set: 2, message: 'wat!?', updated: '2017-02-09 16:40:49'},
- {patch_set: 2, message: 'hi', updated: '2017-02-10 16:40:49'},
- ],
- 'unresolved.file': [
- {
- patch_set: 2,
- message: 'wat!?',
- updated: '2017-02-09 16:40:49',
- id: '1',
- unresolved: true,
- },
- {
- patch_set: 2,
- message: 'hi',
- updated: '2017-02-10 16:40:49',
- id: '2',
- in_reply_to: '1',
- unresolved: false,
- },
- {
- patch_set: 2,
- message: 'good news!',
- updated: '2017-02-08 16:40:49',
- id: '3',
- unresolved: true,
- },
- ],
- };
- element.changeComments._drafts = {
- '/COMMIT_MSG': [
- {
- patch_set: 1,
- message: 'hi',
- updated: '2017-02-15 16:40:49',
- id: '5',
- unresolved: true,
- },
- {
- patch_set: 1,
- message: 'fyi',
- updated: '2017-02-15 16:40:49',
- id: '6',
- unresolved: false,
- },
- ],
- 'unresolved.file': [
- {
- patch_set: 1,
- message: 'hi',
- updated: '2017-02-11 16:40:49',
- id: '4',
- unresolved: false,
- },
- ],
- };
+ const parentTo2 = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
- const parentTo1 = {
- basePatchNum: 'PARENT',
- patchNum: '1',
- };
+ const _1To2 = {
+ basePatchNum: '1',
+ patchNum: '2',
+ };
- const parentTo2 = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
+ assert.equal(
+ element._computeCommentsString(element.changeComments, parentTo1,
+ '/COMMIT_MSG', 'comment'), '2 comments (1 unresolved)');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, _1To2,
+ '/COMMIT_MSG', 'comment'), '3 comments (1 unresolved)');
+ assert.equal(
+ element._computeCommentsStringMobile(element.changeComments, parentTo1
+ , '/COMMIT_MSG'), '2c');
+ assert.equal(
+ element._computeCommentsStringMobile(element.changeComments, _1To2
+ , '/COMMIT_MSG'), '3c');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, parentTo1,
+ 'unresolved.file'), '1 draft');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, _1To2,
+ 'unresolved.file'), '1 draft');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, parentTo1,
+ 'unresolved.file'), '1d');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, _1To2,
+ 'unresolved.file'), '1d');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, parentTo1,
+ 'myfile.txt', 'comment'), '1 comment');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, _1To2,
+ 'myfile.txt', 'comment'), '3 comments');
+ assert.equal(
+ element._computeCommentsStringMobile(
+ element.changeComments,
+ parentTo1,
+ 'myfile.txt'
+ ), '1c');
+ assert.equal(
+ element._computeCommentsStringMobile(element.changeComments, _1To2,
+ 'myfile.txt'), '3c');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, parentTo1,
+ 'myfile.txt'), '');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, _1To2,
+ 'myfile.txt'), '');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, parentTo1,
+ 'myfile.txt'), '');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, _1To2,
+ 'myfile.txt'), '');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, parentTo1,
+ 'file_added_in_rev2.txt', 'comment'), '');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, _1To2,
+ 'file_added_in_rev2.txt', 'comment'), '');
+ assert.equal(
+ element._computeCommentsStringMobile(
+ element.changeComments,
+ parentTo1,
+ 'file_added_in_rev2.txt'
+ ), '');
+ assert.equal(
+ element._computeCommentsStringMobile(element.changeComments, _1To2,
+ 'file_added_in_rev2.txt'), '');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, parentTo1,
+ 'file_added_in_rev2.txt'), '');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, _1To2,
+ 'file_added_in_rev2.txt'), '');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, parentTo1,
+ 'file_added_in_rev2.txt'), '');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, _1To2,
+ 'file_added_in_rev2.txt'), '');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, parentTo2,
+ '/COMMIT_MSG', 'comment'), '1 comment');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, _1To2,
+ '/COMMIT_MSG', 'comment'), '3 comments (1 unresolved)');
+ assert.equal(
+ element._computeCommentsStringMobile(
+ element.changeComments,
+ parentTo2,
+ '/COMMIT_MSG'
+ ), '1c');
+ assert.equal(
+ element._computeCommentsStringMobile(element.changeComments, _1To2,
+ '/COMMIT_MSG'), '3c');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, parentTo1,
+ '/COMMIT_MSG'), '2 drafts');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, _1To2,
+ '/COMMIT_MSG'), '2 drafts');
+ assert.equal(
+ element._computeDraftsStringMobile(
+ element.changeComments,
+ parentTo1,
+ '/COMMIT_MSG'
+ ), '2d');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, _1To2,
+ '/COMMIT_MSG'), '2d');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, parentTo2,
+ 'myfile.txt', 'comment'), '2 comments');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, _1To2,
+ 'myfile.txt', 'comment'), '3 comments');
+ assert.equal(
+ element._computeCommentsStringMobile(
+ element.changeComments,
+ parentTo2,
+ 'myfile.txt'
+ ), '2c');
+ assert.equal(
+ element._computeCommentsStringMobile(element.changeComments, _1To2,
+ 'myfile.txt'), '3c');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, parentTo2,
+ 'myfile.txt'), '');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, _1To2,
+ 'myfile.txt'), '');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, parentTo2,
+ 'file_added_in_rev2.txt', 'comment'), '');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, _1To2,
+ 'file_added_in_rev2.txt', 'comment'), '');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, parentTo2,
+ 'unresolved.file', 'comment'), '3 comments (1 unresolved)');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, _1To2,
+ 'unresolved.file', 'comment'), '3 comments (1 unresolved)');
+ });
- const _1To2 = {
- basePatchNum: '1',
- patchNum: '2',
- };
+ test('_reviewedTitle', () => {
+ assert.equal(
+ element._reviewedTitle(true), 'Mark as not reviewed (shortcut: r)');
- assert.equal(
- element._computeCommentsString(element.changeComments, parentTo1,
- '/COMMIT_MSG', 'comment'), '2 comments (1 unresolved)');
- assert.equal(
- element._computeCommentsString(element.changeComments, _1To2,
- '/COMMIT_MSG', 'comment'), '3 comments (1 unresolved)');
- assert.equal(
- element._computeCommentsStringMobile(element.changeComments, parentTo1
- , '/COMMIT_MSG'), '2c');
- assert.equal(
- element._computeCommentsStringMobile(element.changeComments, _1To2
- , '/COMMIT_MSG'), '3c');
- assert.equal(
- element._computeDraftsString(element.changeComments, parentTo1,
- 'unresolved.file'), '1 draft');
- assert.equal(
- element._computeDraftsString(element.changeComments, _1To2,
- 'unresolved.file'), '1 draft');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, parentTo1,
- 'unresolved.file'), '1d');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, _1To2,
- 'unresolved.file'), '1d');
- assert.equal(
- element._computeCommentsString(element.changeComments, parentTo1,
- 'myfile.txt', 'comment'), '1 comment');
- assert.equal(
- element._computeCommentsString(element.changeComments, _1To2,
- 'myfile.txt', 'comment'), '3 comments');
- assert.equal(
- element._computeCommentsStringMobile(
- element.changeComments,
- parentTo1,
- 'myfile.txt'
- ), '1c');
- assert.equal(
- element._computeCommentsStringMobile(element.changeComments, _1To2,
- 'myfile.txt'), '3c');
- assert.equal(
- element._computeDraftsString(element.changeComments, parentTo1,
- 'myfile.txt'), '');
- assert.equal(
- element._computeDraftsString(element.changeComments, _1To2,
- 'myfile.txt'), '');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, parentTo1,
- 'myfile.txt'), '');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, _1To2,
- 'myfile.txt'), '');
- assert.equal(
- element._computeCommentsString(element.changeComments, parentTo1,
- 'file_added_in_rev2.txt', 'comment'), '');
- assert.equal(
- element._computeCommentsString(element.changeComments, _1To2,
- 'file_added_in_rev2.txt', 'comment'), '');
- assert.equal(
- element._computeCommentsStringMobile(
- element.changeComments,
- parentTo1,
- 'file_added_in_rev2.txt'
- ), '');
- assert.equal(
- element._computeCommentsStringMobile(element.changeComments, _1To2,
- 'file_added_in_rev2.txt'), '');
- assert.equal(
- element._computeDraftsString(element.changeComments, parentTo1,
- 'file_added_in_rev2.txt'), '');
- assert.equal(
- element._computeDraftsString(element.changeComments, _1To2,
- 'file_added_in_rev2.txt'), '');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, parentTo1,
- 'file_added_in_rev2.txt'), '');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, _1To2,
- 'file_added_in_rev2.txt'), '');
- assert.equal(
- element._computeCommentsString(element.changeComments, parentTo2,
- '/COMMIT_MSG', 'comment'), '1 comment');
- assert.equal(
- element._computeCommentsString(element.changeComments, _1To2,
- '/COMMIT_MSG', 'comment'), '3 comments (1 unresolved)');
- assert.equal(
- element._computeCommentsStringMobile(
- element.changeComments,
- parentTo2,
- '/COMMIT_MSG'
- ), '1c');
- assert.equal(
- element._computeCommentsStringMobile(element.changeComments, _1To2,
- '/COMMIT_MSG'), '3c');
- assert.equal(
- element._computeDraftsString(element.changeComments, parentTo1,
- '/COMMIT_MSG'), '2 drafts');
- assert.equal(
- element._computeDraftsString(element.changeComments, _1To2,
- '/COMMIT_MSG'), '2 drafts');
- assert.equal(
- element._computeDraftsStringMobile(
- element.changeComments,
- parentTo1,
- '/COMMIT_MSG'
- ), '2d');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, _1To2,
- '/COMMIT_MSG'), '2d');
- assert.equal(
- element._computeCommentsString(element.changeComments, parentTo2,
- 'myfile.txt', 'comment'), '2 comments');
- assert.equal(
- element._computeCommentsString(element.changeComments, _1To2,
- 'myfile.txt', 'comment'), '3 comments');
- assert.equal(
- element._computeCommentsStringMobile(
- element.changeComments,
- parentTo2,
- 'myfile.txt'
- ), '2c');
- assert.equal(
- element._computeCommentsStringMobile(element.changeComments, _1To2,
- 'myfile.txt'), '3c');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, parentTo2,
- 'myfile.txt'), '');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, _1To2,
- 'myfile.txt'), '');
- assert.equal(
- element._computeCommentsString(element.changeComments, parentTo2,
- 'file_added_in_rev2.txt', 'comment'), '');
- assert.equal(
- element._computeCommentsString(element.changeComments, _1To2,
- 'file_added_in_rev2.txt', 'comment'), '');
- assert.equal(
- element._computeCommentsString(element.changeComments, parentTo2,
- 'unresolved.file', 'comment'), '3 comments (1 unresolved)');
- assert.equal(
- element._computeCommentsString(element.changeComments, _1To2,
- 'unresolved.file', 'comment'), '3 comments (1 unresolved)');
- });
+ assert.equal(
+ element._reviewedTitle(false), 'Mark as reviewed (shortcut: r)');
+ });
- test('_reviewedTitle', () => {
- assert.equal(
- element._reviewedTitle(true), 'Mark as not reviewed (shortcut: r)');
-
- assert.equal(
- element._reviewedTitle(false), 'Mark as reviewed (shortcut: r)');
- });
-
- suite('keyboard shortcuts', () => {
- setup(() => {
- element._filesByPath = {
- '/COMMIT_MSG': {},
- 'file_added_in_rev2.txt': {},
- 'myfile.txt': {},
- };
- element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
- element.change = {_number: 42};
- element.$.fileCursor.setCursorAtIndex(0);
- });
-
- test('toggle left diff via shortcut', () => {
- const toggleLeftDiffStub = sandbox.stub();
- // Property getter cannot be stubbed w/ sandbox due to a bug in Sinon.
- // https://github.com/sinonjs/sinon/issues/781
- const diffsStub = sinon.stub(element, 'diffs', {
- get() {
- return [{toggleLeftDiff: toggleLeftDiffStub}];
- },
- });
- MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
- assert.isTrue(toggleLeftDiffStub.calledOnce);
- diffsStub.restore();
- });
-
- test('keyboard shortcuts', () => {
- flushAsynchronousOperations();
-
- const items = Polymer.dom(element.root).querySelectorAll('.file-row');
- element.$.fileCursor.stops = items;
- element.$.fileCursor.setCursorAtIndex(0);
- assert.equal(items.length, 3);
- assert.isTrue(items[0].classList.contains('selected'));
- assert.isFalse(items[1].classList.contains('selected'));
- assert.isFalse(items[2].classList.contains('selected'));
- // j with a modifier should not move the cursor.
- MockInteractions.pressAndReleaseKeyOn(element, 74, 'shift', 'j');
- assert.equal(element.$.fileCursor.index, 0);
- // down should not move the cursor.
- MockInteractions.pressAndReleaseKeyOn(element, 40, null, 'down');
- assert.equal(element.$.fileCursor.index, 0);
-
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- assert.equal(element.$.fileCursor.index, 1);
- assert.equal(element.selectedIndex, 1);
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
-
- const navStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
- assert.equal(element.$.fileCursor.index, 2);
- assert.equal(element.selectedIndex, 2);
-
- // k with a modifier should not move the cursor.
- MockInteractions.pressAndReleaseKeyOn(element, 75, 'shift', 'k');
- assert.equal(element.$.fileCursor.index, 2);
-
- // up should not move the cursor.
- MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'down');
- assert.equal(element.$.fileCursor.index, 2);
-
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- assert.equal(element.$.fileCursor.index, 1);
- assert.equal(element.selectedIndex, 1);
- MockInteractions.pressAndReleaseKeyOn(element, 79, null, 'o');
-
- assert(navStub.lastCall.calledWith(element.change,
- 'file_added_in_rev2.txt', '2'),
- 'Should navigate to /c/42/2/file_added_in_rev2.txt');
-
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- assert.equal(element.$.fileCursor.index, 0);
- assert.equal(element.selectedIndex, 0);
-
- const createCommentInPlaceStub = sandbox.stub(element.$.diffCursor,
- 'createCommentInPlace');
- MockInteractions.pressAndReleaseKeyOn(element, 67, null, 'c');
- assert.isTrue(createCommentInPlaceStub.called);
- });
-
- test('i key shows/hides selected inline diff', () => {
- const paths = Object.keys(element._filesByPath);
- sandbox.stub(element, '_expandedPathsChanged');
- flushAsynchronousOperations();
- const files = Polymer.dom(element.root).querySelectorAll('.file-row');
- element.$.fileCursor.stops = files;
- element.$.fileCursor.setCursorAtIndex(0);
- assert.equal(element.diffs.length, 0);
- assert.equal(element._expandedFilePaths.length, 0);
-
- MockInteractions.keyUpOn(element, 73, null, 'i');
- flushAsynchronousOperations();
- assert.equal(element.diffs.length, 1);
- assert.equal(element.diffs[0].path, paths[0]);
- assert.equal(element._expandedFilePaths.length, 1);
- assert.equal(element._expandedFilePaths[0], paths[0]);
-
- MockInteractions.keyUpOn(element, 73, null, 'i');
- flushAsynchronousOperations();
- assert.equal(element.diffs.length, 0);
- assert.equal(element._expandedFilePaths.length, 0);
-
- element.$.fileCursor.setCursorAtIndex(1);
- MockInteractions.keyUpOn(element, 73, null, 'i');
- flushAsynchronousOperations();
- assert.equal(element.diffs.length, 1);
- assert.equal(element.diffs[0].path, paths[1]);
- assert.equal(element._expandedFilePaths.length, 1);
- assert.equal(element._expandedFilePaths[0], paths[1]);
-
- MockInteractions.keyUpOn(element, 73, 'shift', 'i');
- flushAsynchronousOperations();
- assert.equal(element.diffs.length, paths.length);
- assert.equal(element._expandedFilePaths.length, paths.length);
- for (const index in element.diffs) {
- if (!element.diffs.hasOwnProperty(index)) { continue; }
- assert.include(element._expandedFilePaths, element.diffs[index].path);
- }
-
- MockInteractions.keyUpOn(element, 73, 'shift', 'i');
- flushAsynchronousOperations();
- assert.equal(element.diffs.length, 0);
- assert.equal(element._expandedFilePaths.length, 0);
- });
-
- test('r key toggles reviewed flag', () => {
- const reducer = (accum, file) => (file.isReviewed ? ++accum : accum);
- const getNumReviewed = () => element._files.reduce(reducer, 0);
- flushAsynchronousOperations();
-
- // Default state should be unreviewed.
- assert.equal(getNumReviewed(), 0);
-
- // Press the review key to toggle it (set the flag).
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
- flushAsynchronousOperations();
- assert.equal(getNumReviewed(), 1);
-
- // Press the review key to toggle it (clear the flag).
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
- assert.equal(getNumReviewed(), 0);
- });
-
- suite('_handleOpenFile', () => {
- let interact;
-
- setup(() => {
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut')
- .returns(false);
- sandbox.stub(element, 'modifierPressed').returns(false);
- const openCursorStub = sandbox.stub(element, '_openCursorFile');
- const openSelectedStub = sandbox.stub(element, '_openSelectedFile');
- const expandStub = sandbox.stub(element, '_togglePathExpanded');
-
- interact = function(opt_payload) {
- openCursorStub.reset();
- openSelectedStub.reset();
- expandStub.reset();
-
- const e = new CustomEvent('fake-keyboard-event', opt_payload);
- sinon.stub(e, 'preventDefault');
- element._handleOpenFile(e);
- assert.isTrue(e.preventDefault.called);
- const result = {};
- if (openCursorStub.called) {
- result.opened_cursor = true;
- }
- if (openSelectedStub.called) {
- result.opened_selected = true;
- }
- if (expandStub.called) {
- result.expanded = true;
- }
- return result;
- };
- });
-
- test('open from selected file', () => {
- element._showInlineDiffs = false;
- assert.deepEqual(interact(), {opened_selected: true});
- });
-
- test('open from diff cursor', () => {
- element._showInlineDiffs = true;
- assert.deepEqual(interact(), {opened_cursor: true});
- });
-
- test('expand when user prefers', () => {
- element._showInlineDiffs = false;
- assert.deepEqual(interact(), {opened_selected: true});
- element._userPrefs = {};
- assert.deepEqual(interact(), {opened_selected: true});
- });
- });
-
- test('shift+left/shift+right', () => {
- const moveLeftStub = sandbox.stub(element.$.diffCursor, 'moveLeft');
- const moveRightStub = sandbox.stub(element.$.diffCursor, 'moveRight');
-
- let noDiffsExpanded = true;
- sandbox.stub(element, '_noDiffsExpanded', () => noDiffsExpanded);
-
- MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'left');
- assert.isFalse(moveLeftStub.called);
- MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'right');
- assert.isFalse(moveRightStub.called);
-
- noDiffsExpanded = false;
-
- MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'left');
- assert.isTrue(moveLeftStub.called);
- MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'right');
- assert.isTrue(moveRightStub.called);
- });
- });
-
- test('computed properties', () => {
- assert.equal(element._computeFileStatus('A'), 'A');
- assert.equal(element._computeFileStatus(undefined), 'M');
- assert.equal(element._computeFileStatus(null), 'M');
-
- assert.equal(element._computeClass('clazz', '/foo/bar/baz'), 'clazz');
- assert.equal(element._computeClass('clazz', '/COMMIT_MSG'),
- 'clazz invisible');
- });
-
- test('file review status', () => {
- element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
+ suite('keyboard shortcuts', () => {
+ setup(() => {
element._filesByPath = {
'/COMMIT_MSG': {},
'file_added_in_rev2.txt': {},
'myfile.txt': {},
};
- element._loggedIn = true;
element.changeNum = '42';
element.patchRange = {
basePatchNum: 'PARENT',
patchNum: '2',
};
+ element.change = {_number: 42};
element.$.fileCursor.setCursorAtIndex(0);
+ });
+ test('toggle left diff via shortcut', () => {
+ const toggleLeftDiffStub = sandbox.stub();
+ // Property getter cannot be stubbed w/ sandbox due to a bug in Sinon.
+ // https://github.com/sinonjs/sinon/issues/781
+ const diffsStub = sinon.stub(element, 'diffs', {
+ get() {
+ return [{toggleLeftDiff: toggleLeftDiffStub}];
+ },
+ });
+ MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
+ assert.isTrue(toggleLeftDiffStub.calledOnce);
+ diffsStub.restore();
+ });
+
+ test('keyboard shortcuts', () => {
flushAsynchronousOperations();
- const fileRows =
- Polymer.dom(element.root).querySelectorAll('.row:not(.header-row)');
- const checkSelector = 'input.reviewed[type="checkbox"]';
- const commitMsg = fileRows[0].querySelector(checkSelector);
- const fileAdded = fileRows[1].querySelector(checkSelector);
- const myFile = fileRows[2].querySelector(checkSelector);
- assert.isTrue(commitMsg.checked);
- assert.isFalse(fileAdded.checked);
- assert.isTrue(myFile.checked);
+ const items = dom(element.root).querySelectorAll('.file-row');
+ element.$.fileCursor.stops = items;
+ element.$.fileCursor.setCursorAtIndex(0);
+ assert.equal(items.length, 3);
+ assert.isTrue(items[0].classList.contains('selected'));
+ assert.isFalse(items[1].classList.contains('selected'));
+ assert.isFalse(items[2].classList.contains('selected'));
+ // j with a modifier should not move the cursor.
+ MockInteractions.pressAndReleaseKeyOn(element, 74, 'shift', 'j');
+ assert.equal(element.$.fileCursor.index, 0);
+ // down should not move the cursor.
+ MockInteractions.pressAndReleaseKeyOn(element, 40, null, 'down');
+ assert.equal(element.$.fileCursor.index, 0);
- const commitReviewLabel = fileRows[0].querySelector('.reviewedLabel');
- const markReviewLabel = commitMsg.nextElementSibling;
- assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
- assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
+ MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
+ assert.equal(element.$.fileCursor.index, 1);
+ assert.equal(element.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- const clickSpy = sandbox.spy(element, '_handleFileListClick');
- MockInteractions.tap(markReviewLabel);
- assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false));
- assert.isFalse(commitReviewLabel.classList.contains('isReviewed'));
- assert.equal(markReviewLabel.textContent, 'MARK REVIEWED');
- assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented);
+ const navStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+ assert.equal(element.$.fileCursor.index, 2);
+ assert.equal(element.selectedIndex, 2);
- MockInteractions.tap(markReviewLabel);
- assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', true));
- assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
- assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
- assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented);
+ // k with a modifier should not move the cursor.
+ MockInteractions.pressAndReleaseKeyOn(element, 75, 'shift', 'k');
+ assert.equal(element.$.fileCursor.index, 2);
+
+ // up should not move the cursor.
+ MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'down');
+ assert.equal(element.$.fileCursor.index, 2);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ assert.equal(element.$.fileCursor.index, 1);
+ assert.equal(element.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 79, null, 'o');
+
+ assert(navStub.lastCall.calledWith(element.change,
+ 'file_added_in_rev2.txt', '2'),
+ 'Should navigate to /c/42/2/file_added_in_rev2.txt');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ assert.equal(element.$.fileCursor.index, 0);
+ assert.equal(element.selectedIndex, 0);
+
+ const createCommentInPlaceStub = sandbox.stub(element.$.diffCursor,
+ 'createCommentInPlace');
+ MockInteractions.pressAndReleaseKeyOn(element, 67, null, 'c');
+ assert.isTrue(createCommentInPlaceStub.called);
});
- test('_computeFileStatusLabel', () => {
- assert.equal(element._computeFileStatusLabel('A'), 'Added');
- assert.equal(element._computeFileStatusLabel('M'), 'Modified');
+ test('i key shows/hides selected inline diff', () => {
+ const paths = Object.keys(element._filesByPath);
+ sandbox.stub(element, '_expandedPathsChanged');
+ flushAsynchronousOperations();
+ const files = dom(element.root).querySelectorAll('.file-row');
+ element.$.fileCursor.stops = files;
+ element.$.fileCursor.setCursorAtIndex(0);
+ assert.equal(element.diffs.length, 0);
+ assert.equal(element._expandedFilePaths.length, 0);
+
+ MockInteractions.keyUpOn(element, 73, null, 'i');
+ flushAsynchronousOperations();
+ assert.equal(element.diffs.length, 1);
+ assert.equal(element.diffs[0].path, paths[0]);
+ assert.equal(element._expandedFilePaths.length, 1);
+ assert.equal(element._expandedFilePaths[0], paths[0]);
+
+ MockInteractions.keyUpOn(element, 73, null, 'i');
+ flushAsynchronousOperations();
+ assert.equal(element.diffs.length, 0);
+ assert.equal(element._expandedFilePaths.length, 0);
+
+ element.$.fileCursor.setCursorAtIndex(1);
+ MockInteractions.keyUpOn(element, 73, null, 'i');
+ flushAsynchronousOperations();
+ assert.equal(element.diffs.length, 1);
+ assert.equal(element.diffs[0].path, paths[1]);
+ assert.equal(element._expandedFilePaths.length, 1);
+ assert.equal(element._expandedFilePaths[0], paths[1]);
+
+ MockInteractions.keyUpOn(element, 73, 'shift', 'i');
+ flushAsynchronousOperations();
+ assert.equal(element.diffs.length, paths.length);
+ assert.equal(element._expandedFilePaths.length, paths.length);
+ for (const index in element.diffs) {
+ if (!element.diffs.hasOwnProperty(index)) { continue; }
+ assert.include(element._expandedFilePaths, element.diffs[index].path);
+ }
+
+ MockInteractions.keyUpOn(element, 73, 'shift', 'i');
+ flushAsynchronousOperations();
+ assert.equal(element.diffs.length, 0);
+ assert.equal(element._expandedFilePaths.length, 0);
});
- test('_handleFileListClick', () => {
- element._filesByPath = {
- '/COMMIT_MSG': {},
- 'f1.txt': {},
- 'f2.txt': {},
- };
- element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
+ test('r key toggles reviewed flag', () => {
+ const reducer = (accum, file) => (file.isReviewed ? ++accum : accum);
+ const getNumReviewed = () => element._files.reduce(reducer, 0);
+ flushAsynchronousOperations();
- const clickSpy = sandbox.spy(element, '_handleFileListClick');
- const reviewStub = sandbox.stub(element, '_reviewFile');
- const toggleExpandSpy = sandbox.spy(element, '_togglePathExpanded');
+ // Default state should be unreviewed.
+ assert.equal(getNumReviewed(), 0);
- const row = Polymer.dom(element.root)
- .querySelector('.row[data-path="f1.txt"]');
+ // Press the review key to toggle it (set the flag).
+ MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ flushAsynchronousOperations();
+ assert.equal(getNumReviewed(), 1);
- // Click on the expand button, resulting in _togglePathExpanded being
- // called and not resulting in a call to _reviewFile.
- row.querySelector('div.show-hide').click();
- assert.isTrue(clickSpy.calledOnce);
- assert.isTrue(toggleExpandSpy.calledOnce);
+ // Press the review key to toggle it (clear the flag).
+ MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ assert.equal(getNumReviewed(), 0);
+ });
+
+ suite('_handleOpenFile', () => {
+ let interact;
+
+ setup(() => {
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut')
+ .returns(false);
+ sandbox.stub(element, 'modifierPressed').returns(false);
+ const openCursorStub = sandbox.stub(element, '_openCursorFile');
+ const openSelectedStub = sandbox.stub(element, '_openSelectedFile');
+ const expandStub = sandbox.stub(element, '_togglePathExpanded');
+
+ interact = function(opt_payload) {
+ openCursorStub.reset();
+ openSelectedStub.reset();
+ expandStub.reset();
+
+ const e = new CustomEvent('fake-keyboard-event', opt_payload);
+ sinon.stub(e, 'preventDefault');
+ element._handleOpenFile(e);
+ assert.isTrue(e.preventDefault.called);
+ const result = {};
+ if (openCursorStub.called) {
+ result.opened_cursor = true;
+ }
+ if (openSelectedStub.called) {
+ result.opened_selected = true;
+ }
+ if (expandStub.called) {
+ result.expanded = true;
+ }
+ return result;
+ };
+ });
+
+ test('open from selected file', () => {
+ element._showInlineDiffs = false;
+ assert.deepEqual(interact(), {opened_selected: true});
+ });
+
+ test('open from diff cursor', () => {
+ element._showInlineDiffs = true;
+ assert.deepEqual(interact(), {opened_cursor: true});
+ });
+
+ test('expand when user prefers', () => {
+ element._showInlineDiffs = false;
+ assert.deepEqual(interact(), {opened_selected: true});
+ element._userPrefs = {};
+ assert.deepEqual(interact(), {opened_selected: true});
+ });
+ });
+
+ test('shift+left/shift+right', () => {
+ const moveLeftStub = sandbox.stub(element.$.diffCursor, 'moveLeft');
+ const moveRightStub = sandbox.stub(element.$.diffCursor, 'moveRight');
+
+ let noDiffsExpanded = true;
+ sandbox.stub(element, '_noDiffsExpanded', () => noDiffsExpanded);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'left');
+ assert.isFalse(moveLeftStub.called);
+ MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'right');
+ assert.isFalse(moveRightStub.called);
+
+ noDiffsExpanded = false;
+
+ MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'left');
+ assert.isTrue(moveLeftStub.called);
+ MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'right');
+ assert.isTrue(moveRightStub.called);
+ });
+ });
+
+ test('computed properties', () => {
+ assert.equal(element._computeFileStatus('A'), 'A');
+ assert.equal(element._computeFileStatus(undefined), 'M');
+ assert.equal(element._computeFileStatus(null), 'M');
+
+ assert.equal(element._computeClass('clazz', '/foo/bar/baz'), 'clazz');
+ assert.equal(element._computeClass('clazz', '/COMMIT_MSG'),
+ 'clazz invisible');
+ });
+
+ test('file review status', () => {
+ element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
+ element._filesByPath = {
+ '/COMMIT_MSG': {},
+ 'file_added_in_rev2.txt': {},
+ 'myfile.txt': {},
+ };
+ element._loggedIn = true;
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ element.$.fileCursor.setCursorAtIndex(0);
+
+ flushAsynchronousOperations();
+ const fileRows =
+ dom(element.root).querySelectorAll('.row:not(.header-row)');
+ const checkSelector = 'input.reviewed[type="checkbox"]';
+ const commitMsg = fileRows[0].querySelector(checkSelector);
+ const fileAdded = fileRows[1].querySelector(checkSelector);
+ const myFile = fileRows[2].querySelector(checkSelector);
+
+ assert.isTrue(commitMsg.checked);
+ assert.isFalse(fileAdded.checked);
+ assert.isTrue(myFile.checked);
+
+ const commitReviewLabel = fileRows[0].querySelector('.reviewedLabel');
+ const markReviewLabel = commitMsg.nextElementSibling;
+ assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
+ assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
+
+ const clickSpy = sandbox.spy(element, '_handleFileListClick');
+ MockInteractions.tap(markReviewLabel);
+ assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false));
+ assert.isFalse(commitReviewLabel.classList.contains('isReviewed'));
+ assert.equal(markReviewLabel.textContent, 'MARK REVIEWED');
+ assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented);
+
+ MockInteractions.tap(markReviewLabel);
+ assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', true));
+ assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
+ assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
+ assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented);
+ });
+
+ test('_computeFileStatusLabel', () => {
+ assert.equal(element._computeFileStatusLabel('A'), 'Added');
+ assert.equal(element._computeFileStatusLabel('M'), 'Modified');
+ });
+
+ test('_handleFileListClick', () => {
+ element._filesByPath = {
+ '/COMMIT_MSG': {},
+ 'f1.txt': {},
+ 'f2.txt': {},
+ };
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+
+ const clickSpy = sandbox.spy(element, '_handleFileListClick');
+ const reviewStub = sandbox.stub(element, '_reviewFile');
+ const toggleExpandSpy = sandbox.spy(element, '_togglePathExpanded');
+
+ const row = dom(element.root)
+ .querySelector('.row[data-path="f1.txt"]');
+
+ // Click on the expand button, resulting in _togglePathExpanded being
+ // called and not resulting in a call to _reviewFile.
+ row.querySelector('div.show-hide').click();
+ assert.isTrue(clickSpy.calledOnce);
+ assert.isTrue(toggleExpandSpy.calledOnce);
+ assert.isFalse(reviewStub.called);
+
+ // Click inside the diff. This should result in no additional calls to
+ // _togglePathExpanded or _reviewFile.
+ dom(element.root).querySelector('gr-diff-host')
+ .click();
+ assert.isTrue(clickSpy.calledTwice);
+ assert.isTrue(toggleExpandSpy.calledOnce);
+ assert.isFalse(reviewStub.called);
+
+ // Click the reviewed checkbox, resulting in a call to _reviewFile, but
+ // no additional call to _togglePathExpanded.
+ row.querySelector('.markReviewed').click();
+ assert.isTrue(clickSpy.calledThrice);
+ assert.isTrue(toggleExpandSpy.calledOnce);
+ assert.isTrue(reviewStub.calledOnce);
+ });
+
+ test('_handleFileListClick editMode', () => {
+ element._filesByPath = {
+ '/COMMIT_MSG': {},
+ 'f1.txt': {},
+ 'f2.txt': {},
+ };
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ element.editMode = true;
+ flushAsynchronousOperations();
+ const clickSpy = sandbox.spy(element, '_handleFileListClick');
+ const toggleExpandSpy = sandbox.spy(element, '_togglePathExpanded');
+
+ // Tap the edit controls. Should be ignored by _handleFileListClick.
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.editFileControls'));
+ assert.isTrue(clickSpy.calledOnce);
+ assert.isFalse(toggleExpandSpy.called);
+ });
+
+ test('patch set from revisions', () => {
+ const expected = [
+ {num: 4, desc: 'test', sha: 'rev4'},
+ {num: 3, desc: 'test', sha: 'rev3'},
+ {num: 2, desc: 'test', sha: 'rev2'},
+ {num: 1, desc: 'test', sha: 'rev1'},
+ ];
+ const patchNums = element.computeAllPatchSets({
+ revisions: {
+ rev3: {_number: 3, description: 'test', date: 3},
+ rev1: {_number: 1, description: 'test', date: 1},
+ rev4: {_number: 4, description: 'test', date: 4},
+ rev2: {_number: 2, description: 'test', date: 2},
+ },
+ });
+ assert.equal(patchNums.length, expected.length);
+ for (let i = 0; i < expected.length; i++) {
+ assert.deepEqual(patchNums[i], expected[i]);
+ }
+ });
+
+ test('checkbox shows/hides diff inline', () => {
+ element._filesByPath = {
+ 'myfile.txt': {},
+ };
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ element.$.fileCursor.setCursorAtIndex(0);
+ sandbox.stub(element, '_expandedPathsChanged');
+ flushAsynchronousOperations();
+ const fileRows =
+ dom(element.root).querySelectorAll('.row:not(.header-row)');
+ // Because the label surrounds the input, the tap event is triggered
+ // there first.
+ const showHideLabel = fileRows[0].querySelector('label.show-hide');
+ const showHideCheck = fileRows[0].querySelector(
+ 'input.show-hide[type="checkbox"]');
+ assert.isNotOk(showHideCheck.checked);
+ MockInteractions.tap(showHideLabel);
+ assert.isOk(showHideCheck.checked);
+ assert.notEqual(element._expandedFilePaths.indexOf('myfile.txt'), -1);
+ });
+
+ test('diff mode correctly toggles the diffs', () => {
+ element._filesByPath = {
+ 'myfile.txt': {},
+ };
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ sandbox.spy(element, '_updateDiffPreferences');
+ element.$.fileCursor.setCursorAtIndex(0);
+ flushAsynchronousOperations();
+
+ // Tap on a file to generate the diff.
+ const row = dom(element.root)
+ .querySelectorAll('.row:not(.header-row) label.show-hide')[0];
+
+ MockInteractions.tap(row);
+ flushAsynchronousOperations();
+ const diffDisplay = element.diffs[0];
+ element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};
+ element.set('diffViewMode', 'UNIFIED_DIFF');
+ assert.equal(diffDisplay.viewMode, 'UNIFIED_DIFF');
+ assert.isTrue(element._updateDiffPreferences.called);
+ });
+
+ test('expanded attribute not set on path when not expanded', () => {
+ element._filesByPath = {
+ '/COMMIT_MSG': {},
+ };
+ assert.isNotOk(element.shadowRoot
+ .querySelector('.expanded'));
+ });
+
+ test('tapping row ignores links', () => {
+ element._filesByPath = {
+ '/COMMIT_MSG': {},
+ };
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ sandbox.stub(element, '_expandedPathsChanged');
+ flushAsynchronousOperations();
+ const commitMsgFile = dom(element.root)
+ .querySelectorAll('.row:not(.header-row) a.pathLink')[0];
+
+ // Remove href attribute so the app doesn't route to a diff view
+ commitMsgFile.removeAttribute('href');
+ const togglePathSpy = sandbox.spy(element, '_togglePathExpanded');
+
+ MockInteractions.tap(commitMsgFile);
+ flushAsynchronousOperations();
+ assert(togglePathSpy.notCalled, 'file is opened as diff view');
+ assert.isNotOk(element.shadowRoot
+ .querySelector('.expanded'));
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('.show-hide')).display,
+ 'none');
+ });
+
+ test('_togglePathExpanded', () => {
+ const path = 'path/to/my/file.txt';
+ element._filesByPath = {[path]: {}};
+ const renderSpy = sandbox.spy(element, '_renderInOrder');
+ const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs');
+
+ assert.equal(element.shadowRoot
+ .querySelector('iron-icon').icon, 'gr-icons:expand-more');
+ assert.equal(element._expandedFilePaths.length, 0);
+ element._togglePathExpanded(path);
+ flushAsynchronousOperations();
+ assert.equal(collapseStub.lastCall.args[0].length, 0);
+ assert.equal(element.shadowRoot
+ .querySelector('iron-icon').icon, 'gr-icons:expand-less');
+
+ assert.equal(renderSpy.callCount, 1);
+ assert.include(element._expandedFilePaths, path);
+ element._togglePathExpanded(path);
+ flushAsynchronousOperations();
+
+ assert.equal(element.shadowRoot
+ .querySelector('iron-icon').icon, 'gr-icons:expand-more');
+ assert.equal(renderSpy.callCount, 1);
+ assert.notInclude(element._expandedFilePaths, path);
+ assert.equal(collapseStub.lastCall.args[0].length, 1);
+ });
+
+ test('expandAllDiffs and collapseAllDiffs', () => {
+ const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs');
+ const cursorUpdateStub = sandbox.stub(element.$.diffCursor,
+ 'handleDiffUpdate');
+
+ const path = 'path/to/my/file.txt';
+ element._filesByPath = {[path]: {}};
+ element.expandAllDiffs();
+ flushAsynchronousOperations();
+ assert.isTrue(element._showInlineDiffs);
+ assert.isTrue(cursorUpdateStub.calledOnce);
+ assert.equal(collapseStub.lastCall.args[0].length, 0);
+
+ element.collapseAllDiffs();
+ flushAsynchronousOperations();
+ assert.equal(element._expandedFilePaths.length, 0);
+ assert.isFalse(element._showInlineDiffs);
+ assert.isTrue(cursorUpdateStub.calledTwice);
+ assert.equal(collapseStub.lastCall.args[0].length, 1);
+ });
+
+ test('_expandedPathsChanged', done => {
+ sandbox.stub(element, '_reviewFile');
+ const path = 'path/to/my/file.txt';
+ const diffs = [{
+ path,
+ style: {},
+ reload() {
+ done();
+ },
+ cancel() {},
+ getCursorStops() { return []; },
+ addEventListener(eventName, callback) {
+ callback(new Event(eventName));
+ },
+ }];
+ sinon.stub(element, 'diffs', {
+ get() { return diffs; },
+ });
+ element.push('_expandedFilePaths', path);
+ });
+
+ test('_clearCollapsedDiffs', () => {
+ const diff = {
+ cancel: sinon.stub(),
+ clearDiffContent: sinon.stub(),
+ };
+ element._clearCollapsedDiffs([diff]);
+ assert.isTrue(diff.cancel.calledOnce);
+ assert.isTrue(diff.clearDiffContent.calledOnce);
+ });
+
+ test('filesExpanded value updates to correct enum', () => {
+ element._filesByPath = {
+ 'foo.bar': {},
+ 'baz.bar': {},
+ };
+ flushAsynchronousOperations();
+ assert.equal(element.filesExpanded,
+ GrFileListConstants.FilesExpandedState.NONE);
+ element.push('_expandedFilePaths', 'baz.bar');
+ flushAsynchronousOperations();
+ assert.equal(element.filesExpanded,
+ GrFileListConstants.FilesExpandedState.SOME);
+ element.push('_expandedFilePaths', 'foo.bar');
+ flushAsynchronousOperations();
+ assert.equal(element.filesExpanded,
+ GrFileListConstants.FilesExpandedState.ALL);
+ element.collapseAllDiffs();
+ flushAsynchronousOperations();
+ assert.equal(element.filesExpanded,
+ GrFileListConstants.FilesExpandedState.NONE);
+ element.expandAllDiffs();
+ flushAsynchronousOperations();
+ assert.equal(element.filesExpanded,
+ GrFileListConstants.FilesExpandedState.ALL);
+ });
+
+ test('_renderInOrder', done => {
+ const reviewStub = sandbox.stub(element, '_reviewFile');
+ let callCount = 0;
+ const diffs = [{
+ path: 'p0',
+ style: {},
+ reload() {
+ assert.equal(callCount++, 2);
+ return Promise.resolve();
+ },
+ }, {
+ path: 'p1',
+ style: {},
+ reload() {
+ assert.equal(callCount++, 1);
+ return Promise.resolve();
+ },
+ }, {
+ path: 'p2',
+ style: {},
+ reload() {
+ assert.equal(callCount++, 0);
+ return Promise.resolve();
+ },
+ }];
+ element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3)
+ .then(() => {
+ assert.isFalse(reviewStub.called);
+ assert.isTrue(loadCommentSpy.called);
+ done();
+ });
+ });
+
+ test('_renderInOrder logged in', done => {
+ element._loggedIn = true;
+ const reviewStub = sandbox.stub(element, '_reviewFile');
+ let callCount = 0;
+ const diffs = [{
+ path: 'p0',
+ style: {},
+ reload() {
+ assert.equal(reviewStub.callCount, 2);
+ assert.equal(callCount++, 2);
+ return Promise.resolve();
+ },
+ }, {
+ path: 'p1',
+ style: {},
+ reload() {
+ assert.equal(reviewStub.callCount, 1);
+ assert.equal(callCount++, 1);
+ return Promise.resolve();
+ },
+ }, {
+ path: 'p2',
+ style: {},
+ reload() {
+ assert.equal(reviewStub.callCount, 0);
+ assert.equal(callCount++, 0);
+ return Promise.resolve();
+ },
+ }];
+ element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3)
+ .then(() => {
+ assert.equal(reviewStub.callCount, 3);
+ done();
+ });
+ });
+
+ test('_renderInOrder respects diffPrefs.manual_review', () => {
+ element._loggedIn = true;
+ element.diffPrefs = {manual_review: true};
+ const reviewStub = sandbox.stub(element, '_reviewFile');
+ const diffs = [{
+ path: 'p',
+ style: {},
+ reload() { return Promise.resolve(); },
+ }];
+
+ return element._renderInOrder(['p'], diffs, 1).then(() => {
assert.isFalse(reviewStub.called);
+ delete element.diffPrefs.manual_review;
+ return element._renderInOrder(['p'], diffs, 1).then(() => {
+ assert.isTrue(reviewStub.called);
+ assert.isTrue(reviewStub.calledWithExactly('p', true));
+ });
+ });
+ });
- // Click inside the diff. This should result in no additional calls to
- // _togglePathExpanded or _reviewFile.
- Polymer.dom(element.root).querySelector('gr-diff-host')
- .click();
- assert.isTrue(clickSpy.calledTwice);
- assert.isTrue(toggleExpandSpy.calledOnce);
- assert.isFalse(reviewStub.called);
+ test('_loadingChanged fired from reload in debouncer', done => {
+ sandbox.stub(element, '_getReviewedFiles').returns(Promise.resolve([]));
+ element.changeNum = 123;
+ element.patchRange = {patchNum: 12};
+ element._filesByPath = {'foo.bar': {}};
- // Click the reviewed checkbox, resulting in a call to _reviewFile, but
- // no additional call to _togglePathExpanded.
- row.querySelector('.markReviewed').click();
- assert.isTrue(clickSpy.calledThrice);
- assert.isTrue(toggleExpandSpy.calledOnce);
- assert.isTrue(reviewStub.calledOnce);
+ element.reload().then(() => {
+ assert.isFalse(element._loading);
+ element.flushDebouncer('loading-change');
+ assert.isFalse(element.classList.contains('loading'));
+ done();
+ });
+ assert.isTrue(element._loading);
+ assert.isFalse(element.classList.contains('loading'));
+ element.flushDebouncer('loading-change');
+ assert.isTrue(element.classList.contains('loading'));
+ });
+
+ test('_loadingChanged does not set class when there are no files', () => {
+ sandbox.stub(element, '_getReviewedFiles').returns(Promise.resolve([]));
+ element.changeNum = 123;
+ element.patchRange = {patchNum: 12};
+ element.reload();
+ assert.isTrue(element._loading);
+ element.flushDebouncer('loading-change');
+ assert.isFalse(element.classList.contains('loading'));
+ });
+ });
+
+ suite('diff url file list', () => {
+ test('diff url', () => {
+ const diffStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiff')
+ .returns('/c/gerrit/+/1/1/index.php');
+ const change = {
+ _number: 1,
+ project: 'gerrit',
+ };
+ const path = 'index.php';
+ const patchRange = {
+ patchNum: 1,
+ };
+ assert.equal(
+ element._computeDiffURL(change, patchRange, path, false),
+ '/c/gerrit/+/1/1/index.php');
+ diffStub.restore();
+ });
+
+ test('diff url commit msg', () => {
+ const diffStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiff')
+ .returns('/c/gerrit/+/1/1//COMMIT_MSG');
+ const change = {
+ _number: 1,
+ project: 'gerrit',
+ };
+ const path = '/COMMIT_MSG';
+ const patchRange = {
+ patchNum: 1,
+ };
+ assert.equal(
+ element._computeDiffURL(change, patchRange, path, false),
+ '/c/gerrit/+/1/1//COMMIT_MSG');
+ diffStub.restore();
+ });
+
+ test('edit url', () => {
+ const editStub = sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff')
+ .returns('/c/gerrit/+/1/edit/index.php,edit');
+ const change = {
+ _number: 1,
+ project: 'gerrit',
+ };
+ const path = 'index.php';
+ const patchRange = {
+ patchNum: 1,
+ };
+ assert.equal(
+ element._computeDiffURL(change, patchRange, path, true),
+ '/c/gerrit/+/1/edit/index.php,edit');
+ editStub.restore();
+ });
+
+ test('edit url commit msg', () => {
+ const editStub = sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff')
+ .returns('/c/gerrit/+/1/edit//COMMIT_MSG,edit');
+ const change = {
+ _number: 1,
+ project: 'gerrit',
+ };
+ const path = '/COMMIT_MSG';
+ const patchRange = {
+ patchNum: 1,
+ };
+ assert.equal(
+ element._computeDiffURL(change, patchRange, path, true),
+ '/c/gerrit/+/1/edit//COMMIT_MSG,edit');
+ editStub.restore();
+ });
+ });
+
+ suite('size bars', () => {
+ test('_computeSizeBarLayout', () => {
+ assert.isUndefined(element._computeSizeBarLayout(null));
+ assert.isUndefined(element._computeSizeBarLayout({}));
+ assert.deepEqual(element._computeSizeBarLayout({base: []}), {
+ maxInserted: 0,
+ maxDeleted: 0,
+ maxAdditionWidth: 0,
+ maxDeletionWidth: 0,
+ deletionOffset: 0,
});
- test('_handleFileListClick editMode', () => {
- element._filesByPath = {
- '/COMMIT_MSG': {},
- 'f1.txt': {},
- 'f2.txt': {},
- };
- element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
+ const files = [
+ {__path: '/COMMIT_MSG', lines_inserted: 10000},
+ {__path: 'foo', lines_inserted: 4, lines_deleted: 10},
+ {__path: 'bar', lines_inserted: 5, lines_deleted: 8},
+ ];
+ const layout = element._computeSizeBarLayout({base: files});
+ assert.equal(layout.maxInserted, 5);
+ assert.equal(layout.maxDeleted, 10);
+ });
+
+ test('_computeBarAdditionWidth', () => {
+ const file = {
+ __path: 'foo/bar.baz',
+ lines_inserted: 5,
+ lines_deleted: 0,
+ };
+ const stats = {
+ maxInserted: 10,
+ maxDeleted: 0,
+ maxAdditionWidth: 60,
+ maxDeletionWidth: 0,
+ deletionOffset: 60,
+ };
+
+ // Uses half the space when file is half the largest addition and there
+ // are no deletions.
+ assert.equal(element._computeBarAdditionWidth(file, stats), 30);
+
+ // If there are no insetions, there is no width.
+ stats.maxInserted = 0;
+ assert.equal(element._computeBarAdditionWidth(file, stats), 0);
+
+ // If the insertions is not present on the file, there is no width.
+ stats.maxInserted = 10;
+ file.lines_inserted = undefined;
+ assert.equal(element._computeBarAdditionWidth(file, stats), 0);
+
+ // If the file is a commit message, returns zero.
+ file.lines_inserted = 5;
+ file.__path = '/COMMIT_MSG';
+ assert.equal(element._computeBarAdditionWidth(file, stats), 0);
+
+ // Width bottoms-out at the minimum width.
+ file.__path = 'stuff.txt';
+ file.lines_inserted = 1;
+ stats.maxInserted = 1000000;
+ assert.equal(element._computeBarAdditionWidth(file, stats), 1.5);
+ });
+
+ test('_computeBarAdditionX', () => {
+ const file = {
+ __path: 'foo/bar.baz',
+ lines_inserted: 5,
+ lines_deleted: 0,
+ };
+ const stats = {
+ maxInserted: 10,
+ maxDeleted: 0,
+ maxAdditionWidth: 60,
+ maxDeletionWidth: 0,
+ deletionOffset: 60,
+ };
+ assert.equal(element._computeBarAdditionX(file, stats), 30);
+ });
+
+ test('_computeBarDeletionWidth', () => {
+ const file = {
+ __path: 'foo/bar.baz',
+ lines_inserted: 0,
+ lines_deleted: 5,
+ };
+ const stats = {
+ maxInserted: 10,
+ maxDeleted: 10,
+ maxAdditionWidth: 30,
+ maxDeletionWidth: 30,
+ deletionOffset: 31,
+ };
+
+ // Uses a quarter the space when file is half the largest deletions and
+ // there are equal additions.
+ assert.equal(element._computeBarDeletionWidth(file, stats), 15);
+
+ // If there are no deletions, there is no width.
+ stats.maxDeleted = 0;
+ assert.equal(element._computeBarDeletionWidth(file, stats), 0);
+
+ // If the deletions is not present on the file, there is no width.
+ stats.maxDeleted = 10;
+ file.lines_deleted = undefined;
+ assert.equal(element._computeBarDeletionWidth(file, stats), 0);
+
+ // If the file is a commit message, returns zero.
+ file.lines_deleted = 5;
+ file.__path = '/COMMIT_MSG';
+ assert.equal(element._computeBarDeletionWidth(file, stats), 0);
+
+ // Width bottoms-out at the minimum width.
+ file.__path = 'stuff.txt';
+ file.lines_deleted = 1;
+ stats.maxDeleted = 1000000;
+ assert.equal(element._computeBarDeletionWidth(file, stats), 1.5);
+ });
+
+ test('_computeSizeBarsClass', () => {
+ assert.equal(element._computeSizeBarsClass(false, 'foo/bar.baz'),
+ 'sizeBars desktop hide');
+ assert.equal(element._computeSizeBarsClass(true, '/COMMIT_MSG'),
+ 'sizeBars desktop invisible');
+ assert.equal(element._computeSizeBarsClass(true, 'foo/bar.baz'),
+ 'sizeBars desktop ');
+ });
+ });
+
+ suite('gr-file-list inline diff tests', () => {
+ let element;
+ let sandbox;
+
+ const commitMsgComments = [
+ {
+ patch_set: 2,
+ id: 'ecf0b9fa_fe1a5f62',
+ line: 20,
+ updated: '2018-02-08 18:49:18.000000000',
+ message: 'another comment',
+ unresolved: true,
+ },
+ {
+ patch_set: 2,
+ id: '503008e2_0ab203ee',
+ line: 10,
+ updated: '2018-02-14 22:07:43.000000000',
+ message: 'a comment',
+ unresolved: true,
+ },
+ {
+ patch_set: 2,
+ id: 'cc788d2c_cb1d728c',
+ line: 20,
+ in_reply_to: 'ecf0b9fa_fe1a5f62',
+ updated: '2018-02-13 22:07:43.000000000',
+ message: 'response',
+ unresolved: true,
+ },
+ ];
+
+ const setupDiff = function(diff) {
+ const mock = document.createElement('mock-diff-response');
+ diff.comments = {
+ left: diff.path === '/COMMIT_MSG' ? commitMsgComments : [],
+ right: [],
+ meta: {
+ changeNum: 1,
+ patchRange: {
+ basePatchNum: 'PARENT',
+ patchNum: 2,
+ },
+ },
+ };
+ diff.prefs = {
+ context: 10,
+ tab_size: 8,
+ font_size: 12,
+ line_length: 100,
+ cursor_blink_rate: 0,
+ line_wrapping: false,
+ intraline_difference: true,
+ show_line_endings: true,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ auto_hide_diff_table_header: true,
+ theme: 'DEFAULT',
+ ignore_whitespace: 'IGNORE_NONE',
+ };
+ diff.diff = mock.diffResponse;
+ diff.$.diff.flushDebouncer('renderDiffTable');
+ };
+
+ const renderAndGetNewDiffs = function(index) {
+ const diffs =
+ dom(element.root).querySelectorAll('gr-diff-host');
+
+ for (let i = index; i < diffs.length; i++) {
+ setupDiff(diffs[i]);
+ }
+
+ element._updateDiffCursor();
+ element.$.diffCursor.handleDiffUpdate();
+ return diffs;
+ };
+
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
+ getPreferences() { return Promise.resolve({}); },
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ });
+ stub('gr-date-formatter', {
+ _loadTimeFormat() { return Promise.resolve(''); },
+ });
+ stub('gr-diff-host', {
+ reload() { return Promise.resolve(); },
+ });
+
+ // Element must be wrapped in an element with direct access to the
+ // comment API.
+ commentApiWrapper = fixture('basic');
+ element = commentApiWrapper.$.fileList;
+ loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
+ element.diffPrefs = {};
+ sandbox.stub(element, '_reviewFile');
+
+ // Stub methods on the changeComments object after changeComments has
+ // been initialized.
+ commentApiWrapper.loadComments().then(() => {
+ sandbox.stub(element.changeComments, 'getPaths').returns({});
+ sandbox.stub(element.changeComments, 'getCommentsBySideForPath')
+ .returns({meta: {}, left: [], right: []});
+ done();
+ });
+ element._loading = false;
+ element.numFilesShown = 75;
+ element.selectedIndex = 0;
+ element._filesByPath = {
+ '/COMMIT_MSG': {lines_inserted: 9},
+ 'file_added_in_rev2.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ size_delta: 10,
+ size: 100,
+ },
+ 'myfile.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ size_delta: 10,
+ size: 100,
+ },
+ };
+ element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
+ element._loggedIn = true;
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ sandbox.stub(window, 'fetch', () => Promise.resolve());
+ flushAsynchronousOperations();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('cursor with individually opened files', () => {
+ MockInteractions.keyUpOn(element, 73, null, 'i');
+ flushAsynchronousOperations();
+ let diffs = renderAndGetNewDiffs(0);
+ const diffStops = diffs[0].getCursorStops();
+
+ // 1 diff should be rendered.
+ assert.equal(diffs.length, 1);
+
+ // No line number is selected.
+ assert.isFalse(diffStops[10].classList.contains('target-row'));
+
+ // Tapping content on a line selects the line number.
+ MockInteractions.tap(dom(
+ diffStops[10]).querySelectorAll('.contentText')[0]);
+ flushAsynchronousOperations();
+ assert.isTrue(diffStops[10].classList.contains('target-row'));
+
+ // Keyboard shortcuts are still moving the file cursor, not the diff
+ // cursor.
+ MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
+ flushAsynchronousOperations();
+ assert.isTrue(diffStops[10].classList.contains('target-row'));
+ assert.isFalse(diffStops[11].classList.contains('target-row'));
+
+ // The file cusor is now at 1.
+ assert.equal(element.$.fileCursor.index, 1);
+ MockInteractions.keyUpOn(element, 73, null, 'i');
+ flushAsynchronousOperations();
+
+ diffs = renderAndGetNewDiffs(1);
+ // Two diffs should be rendered.
+ assert.equal(diffs.length, 2);
+ const diffStopsFirst = diffs[0].getCursorStops();
+ const diffStopsSecond = diffs[1].getCursorStops();
+
+ // The line on the first diff is stil selected
+ assert.isTrue(diffStopsFirst[10].classList.contains('target-row'));
+ assert.isFalse(diffStopsSecond[10].classList.contains('target-row'));
+ });
+
+ test('cursor with toggle all files', () => {
+ MockInteractions.keyUpOn(element, 73, 'shift', 'i');
+ flushAsynchronousOperations();
+
+ const diffs = renderAndGetNewDiffs(0);
+ const diffStops = diffs[0].getCursorStops();
+
+ // 1 diff should be rendered.
+ assert.equal(diffs.length, 3);
+
+ // No line number is selected.
+ assert.isFalse(diffStops[10].classList.contains('target-row'));
+
+ // Tapping content on a line selects the line number.
+ MockInteractions.tap(dom(
+ diffStops[10]).querySelectorAll('.contentText')[0]);
+ flushAsynchronousOperations();
+ assert.isTrue(diffStops[10].classList.contains('target-row'));
+
+ // Keyboard shortcuts are still moving the file cursor, not the diff
+ // cursor.
+ MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
+ flushAsynchronousOperations();
+ assert.isFalse(diffStops[10].classList.contains('target-row'));
+ assert.isTrue(diffStops[11].classList.contains('target-row'));
+
+ // The file cusor is still at 0.
+ assert.equal(element.$.fileCursor.index, 0);
+ });
+
+ suite('n key presses', () => {
+ let nKeySpy;
+ let nextCommentStub;
+ let nextChunkStub;
+ let fileRows;
+
+ setup(() => {
+ sandbox.stub(element, '_renderInOrder').returns(Promise.resolve());
+ nKeySpy = sandbox.spy(element, '_handleNextChunk');
+ nextCommentStub = sandbox.stub(element.$.diffCursor,
+ 'moveToNextCommentThread');
+ nextChunkStub = sandbox.stub(element.$.diffCursor,
+ 'moveToNextChunk');
+ fileRows =
+ dom(element.root).querySelectorAll('.row:not(.header-row)');
+ });
+
+ test('n key with some files expanded and no shift key', () => {
+ MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
+ flushAsynchronousOperations();
+ assert.equal(nextChunkStub.callCount, 1);
+
+ // Handle N key should return before calling diff cursor functions.
+ MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
+ assert.isTrue(nKeySpy.called);
+ assert.isFalse(nextCommentStub.called);
+
+ // This is also called in diffCursor.moveToFirstChunk.
+ assert.equal(nextChunkStub.callCount, 2);
+ assert.equal(element.filesExpanded, 'some');
+ });
+
+ test('n key with some files expanded and shift key', () => {
+ MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
+ flushAsynchronousOperations();
+ assert.equal(nextChunkStub.callCount, 1);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
+ assert.isTrue(nKeySpy.called);
+ assert.isTrue(nextCommentStub.called);
+
+ // This is also called in diffCursor.moveToFirstChunk.
+ assert.equal(nextChunkStub.callCount, 1);
+ assert.equal(element.filesExpanded, 'some');
+ });
+
+ test('n key without all files expanded and shift key', () => {
+ MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i');
+ flushAsynchronousOperations();
+
+ MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
+ assert.isTrue(nKeySpy.called);
+ assert.isFalse(nextCommentStub.called);
+
+ // This is also called in diffCursor.moveToFirstChunk.
+ assert.equal(nextChunkStub.callCount, 2);
+ assert.isTrue(element._showInlineDiffs);
+ });
+
+ test('n key without all files expanded and no shift key', () => {
+ MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i');
+ flushAsynchronousOperations();
+
+ MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
+ assert.isTrue(nKeySpy.called);
+ assert.isTrue(nextCommentStub.called);
+
+ // This is also called in diffCursor.moveToFirstChunk.
+ assert.equal(nextChunkStub.callCount, 1);
+ assert.isTrue(element._showInlineDiffs);
+ });
+ });
+
+ test('_openSelectedFile behavior', () => {
+ const _filesByPath = element._filesByPath;
+ element.set('_filesByPath', {});
+ const navStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+ // Noop when there are no files.
+ element._openSelectedFile();
+ assert.isFalse(navStub.called);
+
+ element.set('_filesByPath', _filesByPath);
+ flushAsynchronousOperations();
+ // Navigates when a file is selected.
+ element._openSelectedFile();
+ assert.isTrue(navStub.called);
+ });
+
+ test('_displayLine', () => {
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut', () => false);
+ sandbox.stub(element, 'modifierPressed', () => false);
+ element._showInlineDiffs = true;
+ const mockEvent = {preventDefault() {}};
+
+ element._displayLine = false;
+ element._handleCursorNext(mockEvent);
+ assert.isTrue(element._displayLine);
+
+ element._displayLine = false;
+ element._handleCursorPrev(mockEvent);
+ assert.isTrue(element._displayLine);
+
+ element._displayLine = true;
+ element._handleEscKey(mockEvent);
+ assert.isFalse(element._displayLine);
+ });
+
+ suite('editMode behavior', () => {
+ test('reviewed checkbox', () => {
+ element._reviewFile.restore();
+ const saveReviewStub = sandbox.stub(element, '_saveReviewedState');
+
+ element.editMode = false;
+ MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ assert.isTrue(saveReviewStub.calledOnce);
+
element.editMode = true;
flushAsynchronousOperations();
- const clickSpy = sandbox.spy(element, '_handleFileListClick');
- const toggleExpandSpy = sandbox.spy(element, '_togglePathExpanded');
- // Tap the edit controls. Should be ignored by _handleFileListClick.
- MockInteractions.tap(element.shadowRoot
- .querySelector('.editFileControls'));
- assert.isTrue(clickSpy.calledOnce);
- assert.isFalse(toggleExpandSpy.called);
+ MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ assert.isTrue(saveReviewStub.calledOnce);
});
- test('patch set from revisions', () => {
- const expected = [
- {num: 4, desc: 'test', sha: 'rev4'},
- {num: 3, desc: 'test', sha: 'rev3'},
- {num: 2, desc: 'test', sha: 'rev2'},
- {num: 1, desc: 'test', sha: 'rev1'},
- ];
- const patchNums = element.computeAllPatchSets({
- revisions: {
- rev3: {_number: 3, description: 'test', date: 3},
- rev1: {_number: 1, description: 'test', date: 1},
- rev4: {_number: 4, description: 'test', date: 4},
- rev2: {_number: 2, description: 'test', date: 2},
- },
+ test('_getReviewedFiles does not call API', () => {
+ const apiSpy = sandbox.spy(element.$.restAPI, 'getReviewedFiles');
+ element.editMode = true;
+ return element._getReviewedFiles().then(files => {
+ assert.equal(files.length, 0);
+ assert.isFalse(apiSpy.called);
});
- assert.equal(patchNums.length, expected.length);
- for (let i = 0; i < expected.length; i++) {
- assert.deepEqual(patchNums[i], expected[i]);
- }
- });
-
- test('checkbox shows/hides diff inline', () => {
- element._filesByPath = {
- 'myfile.txt': {},
- };
- element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
- element.$.fileCursor.setCursorAtIndex(0);
- sandbox.stub(element, '_expandedPathsChanged');
- flushAsynchronousOperations();
- const fileRows =
- Polymer.dom(element.root).querySelectorAll('.row:not(.header-row)');
- // Because the label surrounds the input, the tap event is triggered
- // there first.
- const showHideLabel = fileRows[0].querySelector('label.show-hide');
- const showHideCheck = fileRows[0].querySelector(
- 'input.show-hide[type="checkbox"]');
- assert.isNotOk(showHideCheck.checked);
- MockInteractions.tap(showHideLabel);
- assert.isOk(showHideCheck.checked);
- assert.notEqual(element._expandedFilePaths.indexOf('myfile.txt'), -1);
- });
-
- test('diff mode correctly toggles the diffs', () => {
- element._filesByPath = {
- 'myfile.txt': {},
- };
- element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
- sandbox.spy(element, '_updateDiffPreferences');
- element.$.fileCursor.setCursorAtIndex(0);
- flushAsynchronousOperations();
-
- // Tap on a file to generate the diff.
- const row = Polymer.dom(element.root)
- .querySelectorAll('.row:not(.header-row) label.show-hide')[0];
-
- MockInteractions.tap(row);
- flushAsynchronousOperations();
- const diffDisplay = element.diffs[0];
- element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};
- element.set('diffViewMode', 'UNIFIED_DIFF');
- assert.equal(diffDisplay.viewMode, 'UNIFIED_DIFF');
- assert.isTrue(element._updateDiffPreferences.called);
- });
-
- test('expanded attribute not set on path when not expanded', () => {
- element._filesByPath = {
- '/COMMIT_MSG': {},
- };
- assert.isNotOk(element.shadowRoot
- .querySelector('.expanded'));
- });
-
- test('tapping row ignores links', () => {
- element._filesByPath = {
- '/COMMIT_MSG': {},
- };
- element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
- sandbox.stub(element, '_expandedPathsChanged');
- flushAsynchronousOperations();
- const commitMsgFile = Polymer.dom(element.root)
- .querySelectorAll('.row:not(.header-row) a.pathLink')[0];
-
- // Remove href attribute so the app doesn't route to a diff view
- commitMsgFile.removeAttribute('href');
- const togglePathSpy = sandbox.spy(element, '_togglePathExpanded');
-
- MockInteractions.tap(commitMsgFile);
- flushAsynchronousOperations();
- assert(togglePathSpy.notCalled, 'file is opened as diff view');
- assert.isNotOk(element.shadowRoot
- .querySelector('.expanded'));
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('.show-hide')).display,
- 'none');
- });
-
- test('_togglePathExpanded', () => {
- const path = 'path/to/my/file.txt';
- element._filesByPath = {[path]: {}};
- const renderSpy = sandbox.spy(element, '_renderInOrder');
- const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs');
-
- assert.equal(element.shadowRoot
- .querySelector('iron-icon').icon, 'gr-icons:expand-more');
- assert.equal(element._expandedFilePaths.length, 0);
- element._togglePathExpanded(path);
- flushAsynchronousOperations();
- assert.equal(collapseStub.lastCall.args[0].length, 0);
- assert.equal(element.shadowRoot
- .querySelector('iron-icon').icon, 'gr-icons:expand-less');
-
- assert.equal(renderSpy.callCount, 1);
- assert.include(element._expandedFilePaths, path);
- element._togglePathExpanded(path);
- flushAsynchronousOperations();
-
- assert.equal(element.shadowRoot
- .querySelector('iron-icon').icon, 'gr-icons:expand-more');
- assert.equal(renderSpy.callCount, 1);
- assert.notInclude(element._expandedFilePaths, path);
- assert.equal(collapseStub.lastCall.args[0].length, 1);
- });
-
- test('expandAllDiffs and collapseAllDiffs', () => {
- const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs');
- const cursorUpdateStub = sandbox.stub(element.$.diffCursor,
- 'handleDiffUpdate');
-
- const path = 'path/to/my/file.txt';
- element._filesByPath = {[path]: {}};
- element.expandAllDiffs();
- flushAsynchronousOperations();
- assert.isTrue(element._showInlineDiffs);
- assert.isTrue(cursorUpdateStub.calledOnce);
- assert.equal(collapseStub.lastCall.args[0].length, 0);
-
- element.collapseAllDiffs();
- flushAsynchronousOperations();
- assert.equal(element._expandedFilePaths.length, 0);
- assert.isFalse(element._showInlineDiffs);
- assert.isTrue(cursorUpdateStub.calledTwice);
- assert.equal(collapseStub.lastCall.args[0].length, 1);
- });
-
- test('_expandedPathsChanged', done => {
- sandbox.stub(element, '_reviewFile');
- const path = 'path/to/my/file.txt';
- const diffs = [{
- path,
- style: {},
- reload() {
- done();
- },
- cancel() {},
- getCursorStops() { return []; },
- addEventListener(eventName, callback) {
- callback(new Event(eventName));
- },
- }];
- sinon.stub(element, 'diffs', {
- get() { return diffs; },
- });
- element.push('_expandedFilePaths', path);
- });
-
- test('_clearCollapsedDiffs', () => {
- const diff = {
- cancel: sinon.stub(),
- clearDiffContent: sinon.stub(),
- };
- element._clearCollapsedDiffs([diff]);
- assert.isTrue(diff.cancel.calledOnce);
- assert.isTrue(diff.clearDiffContent.calledOnce);
- });
-
- test('filesExpanded value updates to correct enum', () => {
- element._filesByPath = {
- 'foo.bar': {},
- 'baz.bar': {},
- };
- flushAsynchronousOperations();
- assert.equal(element.filesExpanded,
- GrFileListConstants.FilesExpandedState.NONE);
- element.push('_expandedFilePaths', 'baz.bar');
- flushAsynchronousOperations();
- assert.equal(element.filesExpanded,
- GrFileListConstants.FilesExpandedState.SOME);
- element.push('_expandedFilePaths', 'foo.bar');
- flushAsynchronousOperations();
- assert.equal(element.filesExpanded,
- GrFileListConstants.FilesExpandedState.ALL);
- element.collapseAllDiffs();
- flushAsynchronousOperations();
- assert.equal(element.filesExpanded,
- GrFileListConstants.FilesExpandedState.NONE);
- element.expandAllDiffs();
- flushAsynchronousOperations();
- assert.equal(element.filesExpanded,
- GrFileListConstants.FilesExpandedState.ALL);
- });
-
- test('_renderInOrder', done => {
- const reviewStub = sandbox.stub(element, '_reviewFile');
- let callCount = 0;
- const diffs = [{
- path: 'p0',
- style: {},
- reload() {
- assert.equal(callCount++, 2);
- return Promise.resolve();
- },
- }, {
- path: 'p1',
- style: {},
- reload() {
- assert.equal(callCount++, 1);
- return Promise.resolve();
- },
- }, {
- path: 'p2',
- style: {},
- reload() {
- assert.equal(callCount++, 0);
- return Promise.resolve();
- },
- }];
- element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3)
- .then(() => {
- assert.isFalse(reviewStub.called);
- assert.isTrue(loadCommentSpy.called);
- done();
- });
- });
-
- test('_renderInOrder logged in', done => {
- element._loggedIn = true;
- const reviewStub = sandbox.stub(element, '_reviewFile');
- let callCount = 0;
- const diffs = [{
- path: 'p0',
- style: {},
- reload() {
- assert.equal(reviewStub.callCount, 2);
- assert.equal(callCount++, 2);
- return Promise.resolve();
- },
- }, {
- path: 'p1',
- style: {},
- reload() {
- assert.equal(reviewStub.callCount, 1);
- assert.equal(callCount++, 1);
- return Promise.resolve();
- },
- }, {
- path: 'p2',
- style: {},
- reload() {
- assert.equal(reviewStub.callCount, 0);
- assert.equal(callCount++, 0);
- return Promise.resolve();
- },
- }];
- element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3)
- .then(() => {
- assert.equal(reviewStub.callCount, 3);
- done();
- });
- });
-
- test('_renderInOrder respects diffPrefs.manual_review', () => {
- element._loggedIn = true;
- element.diffPrefs = {manual_review: true};
- const reviewStub = sandbox.stub(element, '_reviewFile');
- const diffs = [{
- path: 'p',
- style: {},
- reload() { return Promise.resolve(); },
- }];
-
- return element._renderInOrder(['p'], diffs, 1).then(() => {
- assert.isFalse(reviewStub.called);
- delete element.diffPrefs.manual_review;
- return element._renderInOrder(['p'], diffs, 1).then(() => {
- assert.isTrue(reviewStub.called);
- assert.isTrue(reviewStub.calledWithExactly('p', true));
- });
- });
- });
-
- test('_loadingChanged fired from reload in debouncer', done => {
- sandbox.stub(element, '_getReviewedFiles').returns(Promise.resolve([]));
- element.changeNum = 123;
- element.patchRange = {patchNum: 12};
- element._filesByPath = {'foo.bar': {}};
-
- element.reload().then(() => {
- assert.isFalse(element._loading);
- element.flushDebouncer('loading-change');
- assert.isFalse(element.classList.contains('loading'));
- done();
- });
- assert.isTrue(element._loading);
- assert.isFalse(element.classList.contains('loading'));
- element.flushDebouncer('loading-change');
- assert.isTrue(element.classList.contains('loading'));
- });
-
- test('_loadingChanged does not set class when there are no files', () => {
- sandbox.stub(element, '_getReviewedFiles').returns(Promise.resolve([]));
- element.changeNum = 123;
- element.patchRange = {patchNum: 12};
- element.reload();
- assert.isTrue(element._loading);
- element.flushDebouncer('loading-change');
- assert.isFalse(element.classList.contains('loading'));
});
});
- suite('diff url file list', () => {
- test('diff url', () => {
- const diffStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiff')
- .returns('/c/gerrit/+/1/1/index.php');
- const change = {
- _number: 1,
- project: 'gerrit',
- };
- const path = 'index.php';
- const patchRange = {
- patchNum: 1,
- };
- assert.equal(
- element._computeDiffURL(change, patchRange, path, false),
- '/c/gerrit/+/1/1/index.php');
- diffStub.restore();
- });
+ test('editing actions', () => {
+ // Edit controls are guarded behind a dom-if initially and not rendered.
+ assert.isNotOk(dom(element.root)
+ .querySelector('gr-edit-file-controls'));
- test('diff url commit msg', () => {
- const diffStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiff')
- .returns('/c/gerrit/+/1/1//COMMIT_MSG');
- const change = {
- _number: 1,
- project: 'gerrit',
- };
- const path = '/COMMIT_MSG';
- const patchRange = {
- patchNum: 1,
- };
- assert.equal(
- element._computeDiffURL(change, patchRange, path, false),
- '/c/gerrit/+/1/1//COMMIT_MSG');
- diffStub.restore();
- });
+ element.editMode = true;
+ flushAsynchronousOperations();
+
+ // Commit message should not have edit controls.
+ const editControls =
+ Array.from(
+ dom(element.root)
+ .querySelectorAll('.row:not(.header-row)'))
+ .map(row => row.querySelector('gr-edit-file-controls'));
+ assert.isTrue(editControls[0].classList.contains('invisible'));
});
- suite('size bars', () => {
- test('_computeSizeBarLayout', () => {
- assert.isUndefined(element._computeSizeBarLayout(null));
- assert.isUndefined(element._computeSizeBarLayout({}));
- assert.deepEqual(element._computeSizeBarLayout({base: []}), {
- maxInserted: 0,
- maxDeleted: 0,
- maxAdditionWidth: 0,
- maxDeletionWidth: 0,
- deletionOffset: 0,
- });
+ test('reloadCommentsForThreadWithRootId', () => {
+ // Expand the commit message diff
+ MockInteractions.keyUpOn(element, 73, 'shift', 'i');
+ const diffs = renderAndGetNewDiffs(0);
+ flushAsynchronousOperations();
- const files = [
- {__path: '/COMMIT_MSG', lines_inserted: 10000},
- {__path: 'foo', lines_inserted: 4, lines_deleted: 10},
- {__path: 'bar', lines_inserted: 5, lines_deleted: 8},
- ];
- const layout = element._computeSizeBarLayout({base: files});
- assert.equal(layout.maxInserted, 5);
- assert.equal(layout.maxDeleted, 10);
- });
+ // Two comment threads should be generated by renderAndGetNewDiffs
+ const threadEls = diffs[0].getThreadEls();
+ assert.equal(threadEls.length, 2);
+ const threadElsByRootId = new Map(
+ threadEls.map(threadEl => [threadEl.rootId, threadEl]));
- test('_computeBarAdditionWidth', () => {
- const file = {
- __path: 'foo/bar.baz',
- lines_inserted: 5,
- lines_deleted: 0,
- };
- const stats = {
- maxInserted: 10,
- maxDeleted: 0,
- maxAdditionWidth: 60,
- maxDeletionWidth: 0,
- deletionOffset: 60,
- };
+ const thread1 = threadElsByRootId.get('503008e2_0ab203ee');
+ assert.equal(thread1.comments.length, 1);
+ assert.equal(thread1.comments[0].message, 'a comment');
+ assert.equal(thread1.comments[0].line, 10);
- // Uses half the space when file is half the largest addition and there
- // are no deletions.
- assert.equal(element._computeBarAdditionWidth(file, stats), 30);
+ const thread2 = threadElsByRootId.get('ecf0b9fa_fe1a5f62');
+ assert.equal(thread2.comments.length, 2);
+ assert.isTrue(thread2.comments[0].unresolved);
+ assert.equal(thread2.comments[0].message, 'another comment');
+ assert.equal(thread2.comments[0].line, 20);
- // If there are no insetions, there is no width.
- stats.maxInserted = 0;
- assert.equal(element._computeBarAdditionWidth(file, stats), 0);
-
- // If the insertions is not present on the file, there is no width.
- stats.maxInserted = 10;
- file.lines_inserted = undefined;
- assert.equal(element._computeBarAdditionWidth(file, stats), 0);
-
- // If the file is a commit message, returns zero.
- file.lines_inserted = 5;
- file.__path = '/COMMIT_MSG';
- assert.equal(element._computeBarAdditionWidth(file, stats), 0);
-
- // Width bottoms-out at the minimum width.
- file.__path = 'stuff.txt';
- file.lines_inserted = 1;
- stats.maxInserted = 1000000;
- assert.equal(element._computeBarAdditionWidth(file, stats), 1.5);
- });
-
- test('_computeBarAdditionX', () => {
- const file = {
- __path: 'foo/bar.baz',
- lines_inserted: 5,
- lines_deleted: 0,
- };
- const stats = {
- maxInserted: 10,
- maxDeleted: 0,
- maxAdditionWidth: 60,
- maxDeletionWidth: 0,
- deletionOffset: 60,
- };
- assert.equal(element._computeBarAdditionX(file, stats), 30);
- });
-
- test('_computeBarDeletionWidth', () => {
- const file = {
- __path: 'foo/bar.baz',
- lines_inserted: 0,
- lines_deleted: 5,
- };
- const stats = {
- maxInserted: 10,
- maxDeleted: 10,
- maxAdditionWidth: 30,
- maxDeletionWidth: 30,
- deletionOffset: 31,
- };
-
- // Uses a quarter the space when file is half the largest deletions and
- // there are equal additions.
- assert.equal(element._computeBarDeletionWidth(file, stats), 15);
-
- // If there are no deletions, there is no width.
- stats.maxDeleted = 0;
- assert.equal(element._computeBarDeletionWidth(file, stats), 0);
-
- // If the deletions is not present on the file, there is no width.
- stats.maxDeleted = 10;
- file.lines_deleted = undefined;
- assert.equal(element._computeBarDeletionWidth(file, stats), 0);
-
- // If the file is a commit message, returns zero.
- file.lines_deleted = 5;
- file.__path = '/COMMIT_MSG';
- assert.equal(element._computeBarDeletionWidth(file, stats), 0);
-
- // Width bottoms-out at the minimum width.
- file.__path = 'stuff.txt';
- file.lines_deleted = 1;
- stats.maxDeleted = 1000000;
- assert.equal(element._computeBarDeletionWidth(file, stats), 1.5);
- });
-
- test('_computeSizeBarsClass', () => {
- assert.equal(element._computeSizeBarsClass(false, 'foo/bar.baz'),
- 'sizeBars desktop hide');
- assert.equal(element._computeSizeBarsClass(true, '/COMMIT_MSG'),
- 'sizeBars desktop invisible');
- assert.equal(element._computeSizeBarsClass(true, 'foo/bar.baz'),
- 'sizeBars desktop ');
- });
- });
-
- suite('gr-file-list inline diff tests', () => {
- let element;
- let sandbox;
-
- const commitMsgComments = [
+ const commentStub =
+ sandbox.stub(element.changeComments, 'getCommentsForThread');
+ const commentStubRes1 = [
+ {
+ patch_set: 2,
+ id: '503008e2_0ab203ee',
+ line: 20,
+ updated: '2018-02-08 18:49:18.000000000',
+ message: 'edited text',
+ unresolved: false,
+ },
+ ];
+ const commentStubRes2 = [
{
patch_set: 2,
id: 'ecf0b9fa_fe1a5f62',
@@ -1446,453 +1870,58 @@
patch_set: 2,
id: '503008e2_0ab203ee',
line: 10,
+ in_reply_to: 'ecf0b9fa_fe1a5f62',
updated: '2018-02-14 22:07:43.000000000',
- message: 'a comment',
+ message: 'response',
unresolved: true,
},
{
patch_set: 2,
- id: 'cc788d2c_cb1d728c',
+ id: '503008e2_0ab203ef',
line: 20,
- in_reply_to: 'ecf0b9fa_fe1a5f62',
- updated: '2018-02-13 22:07:43.000000000',
- message: 'response',
+ in_reply_to: '503008e2_0ab203ee',
+ updated: '2018-02-15 22:07:43.000000000',
+ message: 'a third comment in the thread',
unresolved: true,
},
];
+ commentStub.withArgs('503008e2_0ab203ee').returns(
+ commentStubRes1);
+ commentStub.withArgs('ecf0b9fa_fe1a5f62').returns(
+ commentStubRes2);
- const setupDiff = function(diff) {
- const mock = document.createElement('mock-diff-response');
- diff.comments = {
- left: diff.path === '/COMMIT_MSG' ? commitMsgComments : [],
- right: [],
- meta: {
- changeNum: 1,
- patchRange: {
- basePatchNum: 'PARENT',
- patchNum: 2,
- },
- },
- };
- diff.prefs = {
- context: 10,
- tab_size: 8,
- font_size: 12,
- line_length: 100,
- cursor_blink_rate: 0,
- line_wrapping: false,
- intraline_difference: true,
- show_line_endings: true,
- show_tabs: true,
- show_whitespace_errors: true,
- syntax_highlighting: true,
- auto_hide_diff_table_header: true,
- theme: 'DEFAULT',
- ignore_whitespace: 'IGNORE_NONE',
- };
- diff.diff = mock.diffResponse;
- diff.$.diff.flushDebouncer('renderDiffTable');
- };
+ // Reload comments from the first comment thread, which should have a
+ // an updated message and a toggled resolve state.
+ element.reloadCommentsForThreadWithRootId('503008e2_0ab203ee',
+ '/COMMIT_MSG');
+ assert.equal(thread1.comments.length, 1);
+ assert.isFalse(thread1.comments[0].unresolved);
+ assert.equal(thread1.comments[0].message, 'edited text');
- const renderAndGetNewDiffs = function(index) {
- const diffs =
- Polymer.dom(element.root).querySelectorAll('gr-diff-host');
+ // Reload comments from the second comment thread, which should have a new
+ // reply.
+ element.reloadCommentsForThreadWithRootId('ecf0b9fa_fe1a5f62',
+ '/COMMIT_MSG');
+ assert.equal(thread2.comments.length, 3);
- for (let i = index; i < diffs.length; i++) {
- setupDiff(diffs[i]);
- }
+ const commentStubCount = commentStub.callCount;
+ const getThreadsSpy = sandbox.spy(diffs[0], 'getThreadEls');
- element._updateDiffCursor();
- element.$.diffCursor.handleDiffUpdate();
- return diffs;
- };
+ // Should not be getting threads when the file is not expanded.
+ element.reloadCommentsForThreadWithRootId('ecf0b9fa_fe1a5f62',
+ 'other/file');
+ assert.isFalse(getThreadsSpy.called);
+ assert.equal(commentStubCount, commentStub.callCount);
- setup(done => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- getPreferences() { return Promise.resolve({}); },
- getDiffComments() { return Promise.resolve({}); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
- });
- stub('gr-date-formatter', {
- _loadTimeFormat() { return Promise.resolve(''); },
- });
- stub('gr-diff-host', {
- reload() { return Promise.resolve(); },
- });
-
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = fixture('basic');
- element = commentApiWrapper.$.fileList;
- loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
- element.diffPrefs = {};
- sandbox.stub(element, '_reviewFile');
-
- // Stub methods on the changeComments object after changeComments has
- // been initialized.
- commentApiWrapper.loadComments().then(() => {
- sandbox.stub(element.changeComments, 'getPaths').returns({});
- sandbox.stub(element.changeComments, 'getCommentsBySideForPath')
- .returns({meta: {}, left: [], right: []});
- done();
- });
- element._loading = false;
- element.numFilesShown = 75;
- element.selectedIndex = 0;
- element._filesByPath = {
- '/COMMIT_MSG': {lines_inserted: 9},
- 'file_added_in_rev2.txt': {
- lines_inserted: 1,
- lines_deleted: 1,
- size_delta: 10,
- size: 100,
- },
- 'myfile.txt': {
- lines_inserted: 1,
- lines_deleted: 1,
- size_delta: 10,
- size: 100,
- },
- };
- element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
- element._loggedIn = true;
- element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
- sandbox.stub(window, 'fetch', () => Promise.resolve());
- flushAsynchronousOperations();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('cursor with individually opened files', () => {
- MockInteractions.keyUpOn(element, 73, null, 'i');
- flushAsynchronousOperations();
- let diffs = renderAndGetNewDiffs(0);
- const diffStops = diffs[0].getCursorStops();
-
- // 1 diff should be rendered.
- assert.equal(diffs.length, 1);
-
- // No line number is selected.
- assert.isFalse(diffStops[10].classList.contains('target-row'));
-
- // Tapping content on a line selects the line number.
- MockInteractions.tap(Polymer.dom(
- diffStops[10]).querySelectorAll('.contentText')[0]);
- flushAsynchronousOperations();
- assert.isTrue(diffStops[10].classList.contains('target-row'));
-
- // Keyboard shortcuts are still moving the file cursor, not the diff
- // cursor.
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- flushAsynchronousOperations();
- assert.isTrue(diffStops[10].classList.contains('target-row'));
- assert.isFalse(diffStops[11].classList.contains('target-row'));
-
- // The file cusor is now at 1.
- assert.equal(element.$.fileCursor.index, 1);
- MockInteractions.keyUpOn(element, 73, null, 'i');
- flushAsynchronousOperations();
-
- diffs = renderAndGetNewDiffs(1);
- // Two diffs should be rendered.
- assert.equal(diffs.length, 2);
- const diffStopsFirst = diffs[0].getCursorStops();
- const diffStopsSecond = diffs[1].getCursorStops();
-
- // The line on the first diff is stil selected
- assert.isTrue(diffStopsFirst[10].classList.contains('target-row'));
- assert.isFalse(diffStopsSecond[10].classList.contains('target-row'));
- });
-
- test('cursor with toggle all files', () => {
- MockInteractions.keyUpOn(element, 73, 'shift', 'i');
- flushAsynchronousOperations();
-
- const diffs = renderAndGetNewDiffs(0);
- const diffStops = diffs[0].getCursorStops();
-
- // 1 diff should be rendered.
- assert.equal(diffs.length, 3);
-
- // No line number is selected.
- assert.isFalse(diffStops[10].classList.contains('target-row'));
-
- // Tapping content on a line selects the line number.
- MockInteractions.tap(Polymer.dom(
- diffStops[10]).querySelectorAll('.contentText')[0]);
- flushAsynchronousOperations();
- assert.isTrue(diffStops[10].classList.contains('target-row'));
-
- // Keyboard shortcuts are still moving the file cursor, not the diff
- // cursor.
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- flushAsynchronousOperations();
- assert.isFalse(diffStops[10].classList.contains('target-row'));
- assert.isTrue(diffStops[11].classList.contains('target-row'));
-
- // The file cusor is still at 0.
- assert.equal(element.$.fileCursor.index, 0);
- });
-
- suite('n key presses', () => {
- let nKeySpy;
- let nextCommentStub;
- let nextChunkStub;
- let fileRows;
-
- setup(() => {
- sandbox.stub(element, '_renderInOrder').returns(Promise.resolve());
- nKeySpy = sandbox.spy(element, '_handleNextChunk');
- nextCommentStub = sandbox.stub(element.$.diffCursor,
- 'moveToNextCommentThread');
- nextChunkStub = sandbox.stub(element.$.diffCursor,
- 'moveToNextChunk');
- fileRows =
- Polymer.dom(element.root).querySelectorAll('.row:not(.header-row)');
- });
-
- test('n key with some files expanded and no shift key', () => {
- MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
- flushAsynchronousOperations();
- assert.equal(nextChunkStub.callCount, 1);
-
- // Handle N key should return before calling diff cursor functions.
- MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
- assert.isTrue(nKeySpy.called);
- assert.isFalse(nextCommentStub.called);
-
- // This is also called in diffCursor.moveToFirstChunk.
- assert.equal(nextChunkStub.callCount, 2);
- assert.equal(element.filesExpanded, 'some');
- });
-
- test('n key with some files expanded and shift key', () => {
- MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
- flushAsynchronousOperations();
- assert.equal(nextChunkStub.callCount, 1);
-
- MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
- assert.isTrue(nKeySpy.called);
- assert.isTrue(nextCommentStub.called);
-
- // This is also called in diffCursor.moveToFirstChunk.
- assert.equal(nextChunkStub.callCount, 1);
- assert.equal(element.filesExpanded, 'some');
- });
-
- test('n key without all files expanded and shift key', () => {
- MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i');
- flushAsynchronousOperations();
-
- MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
- assert.isTrue(nKeySpy.called);
- assert.isFalse(nextCommentStub.called);
-
- // This is also called in diffCursor.moveToFirstChunk.
- assert.equal(nextChunkStub.callCount, 2);
- assert.isTrue(element._showInlineDiffs);
- });
-
- test('n key without all files expanded and no shift key', () => {
- MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i');
- flushAsynchronousOperations();
-
- MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
- assert.isTrue(nKeySpy.called);
- assert.isTrue(nextCommentStub.called);
-
- // This is also called in diffCursor.moveToFirstChunk.
- assert.equal(nextChunkStub.callCount, 1);
- assert.isTrue(element._showInlineDiffs);
- });
- });
-
- test('_openSelectedFile behavior', () => {
- const _filesByPath = element._filesByPath;
- element.set('_filesByPath', {});
- const navStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
- // Noop when there are no files.
- element._openSelectedFile();
- assert.isFalse(navStub.called);
-
- element.set('_filesByPath', _filesByPath);
- flushAsynchronousOperations();
- // Navigates when a file is selected.
- element._openSelectedFile();
- assert.isTrue(navStub.called);
- });
-
- test('_displayLine', () => {
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut', () => false);
- sandbox.stub(element, 'modifierPressed', () => false);
- element._showInlineDiffs = true;
- const mockEvent = {preventDefault() {}};
-
- element._displayLine = false;
- element._handleCursorNext(mockEvent);
- assert.isTrue(element._displayLine);
-
- element._displayLine = false;
- element._handleCursorPrev(mockEvent);
- assert.isTrue(element._displayLine);
-
- element._displayLine = true;
- element._handleEscKey(mockEvent);
- assert.isFalse(element._displayLine);
- });
-
- suite('editMode behavior', () => {
- test('reviewed checkbox', () => {
- element._reviewFile.restore();
- const saveReviewStub = sandbox.stub(element, '_saveReviewedState');
-
- element.editMode = false;
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
- assert.isTrue(saveReviewStub.calledOnce);
-
- element.editMode = true;
- flushAsynchronousOperations();
-
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
- assert.isTrue(saveReviewStub.calledOnce);
- });
-
- test('_getReviewedFiles does not call API', () => {
- const apiSpy = sandbox.spy(element.$.restAPI, 'getReviewedFiles');
- element.editMode = true;
- return element._getReviewedFiles().then(files => {
- assert.equal(files.length, 0);
- assert.isFalse(apiSpy.called);
- });
- });
- });
-
- test('editing actions', () => {
- // Edit controls are guarded behind a dom-if initially and not rendered.
- assert.isNotOk(Polymer.dom(element.root)
- .querySelector('gr-edit-file-controls'));
-
- element.editMode = true;
- flushAsynchronousOperations();
-
- // Commit message should not have edit controls.
- const editControls =
- Array.from(
- Polymer.dom(element.root)
- .querySelectorAll('.row:not(.header-row)'))
- .map(row => row.querySelector('gr-edit-file-controls'));
- assert.isTrue(editControls[0].classList.contains('invisible'));
- });
-
- test('reloadCommentsForThreadWithRootId', () => {
- // Expand the commit message diff
- MockInteractions.keyUpOn(element, 73, 'shift', 'i');
- const diffs = renderAndGetNewDiffs(0);
- flushAsynchronousOperations();
-
- // Two comment threads should be generated by renderAndGetNewDiffs
- const threadEls = diffs[0].getThreadEls();
- assert.equal(threadEls.length, 2);
- const threadElsByRootId = new Map(
- threadEls.map(threadEl => [threadEl.rootId, threadEl]));
-
- const thread1 = threadElsByRootId.get('503008e2_0ab203ee');
- assert.equal(thread1.comments.length, 1);
- assert.equal(thread1.comments[0].message, 'a comment');
- assert.equal(thread1.comments[0].line, 10);
-
- const thread2 = threadElsByRootId.get('ecf0b9fa_fe1a5f62');
- assert.equal(thread2.comments.length, 2);
- assert.isTrue(thread2.comments[0].unresolved);
- assert.equal(thread2.comments[0].message, 'another comment');
- assert.equal(thread2.comments[0].line, 20);
-
- const commentStub =
- sandbox.stub(element.changeComments, 'getCommentsForThread');
- const commentStubRes1 = [
- {
- patch_set: 2,
- id: '503008e2_0ab203ee',
- line: 20,
- updated: '2018-02-08 18:49:18.000000000',
- message: 'edited text',
- unresolved: false,
- },
- ];
- const commentStubRes2 = [
- {
- patch_set: 2,
- id: 'ecf0b9fa_fe1a5f62',
- line: 20,
- updated: '2018-02-08 18:49:18.000000000',
- message: 'another comment',
- unresolved: true,
- },
- {
- patch_set: 2,
- id: '503008e2_0ab203ee',
- line: 10,
- in_reply_to: 'ecf0b9fa_fe1a5f62',
- updated: '2018-02-14 22:07:43.000000000',
- message: 'response',
- unresolved: true,
- },
- {
- patch_set: 2,
- id: '503008e2_0ab203ef',
- line: 20,
- in_reply_to: '503008e2_0ab203ee',
- updated: '2018-02-15 22:07:43.000000000',
- message: 'a third comment in the thread',
- unresolved: true,
- },
- ];
- commentStub.withArgs('503008e2_0ab203ee').returns(
- commentStubRes1);
- commentStub.withArgs('ecf0b9fa_fe1a5f62').returns(
- commentStubRes2);
-
- // Reload comments from the first comment thread, which should have a
- // an updated message and a toggled resolve state.
- element.reloadCommentsForThreadWithRootId('503008e2_0ab203ee',
- '/COMMIT_MSG');
- assert.equal(thread1.comments.length, 1);
- assert.isFalse(thread1.comments[0].unresolved);
- assert.equal(thread1.comments[0].message, 'edited text');
-
- // Reload comments from the second comment thread, which should have a new
- // reply.
- element.reloadCommentsForThreadWithRootId('ecf0b9fa_fe1a5f62',
- '/COMMIT_MSG');
- assert.equal(thread2.comments.length, 3);
-
- const commentStubCount = commentStub.callCount;
- const getThreadsSpy = sandbox.spy(diffs[0], 'getThreadEls');
-
- // Should not be getting threads when the file is not expanded.
- element.reloadCommentsForThreadWithRootId('ecf0b9fa_fe1a5f62',
- 'other/file');
- assert.isFalse(getThreadsSpy.called);
- assert.equal(commentStubCount, commentStub.callCount);
-
- // Should be query selecting diffs when the file is expanded.
- // Should not be fetching change comments when the rootId is not found
- // to match.
- element.reloadCommentsForThreadWithRootId('acf0b9fa_fe1a5f62',
- '/COMMIT_MSG');
- assert.isTrue(getThreadsSpy.called);
- assert.equal(commentStubCount, commentStub.callCount);
- });
+ // Should be query selecting diffs when the file is expanded.
+ // Should not be fetching change comments when the rootId is not found
+ // to match.
+ element.reloadCommentsForThreadWithRootId('acf0b9fa_fe1a5f62',
+ '/COMMIT_MSG');
+ assert.isTrue(getThreadsSpy.called);
+ assert.equal(commentStubCount, commentStub.callCount);
});
- a11ySuite('basic');
});
+ a11ySuite('basic');
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html
deleted file mode 100644
index 075b41e..0000000
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html
+++ /dev/null
@@ -1,110 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-included-in-dialog">
- <template>
- <style include="shared-styles">
- :host {
- background-color: var(--dialog-background-color);
- display: block;
- max-height: 80vh;
- overflow-y: auto;
- padding: 4.5em var(--spacing-l) var(--spacing-l) var(--spacing-l);
- }
- header {
- background-color: var(--dialog-background-color);
- border-bottom: 1px solid var(--border-color);
- left: 0;
- padding: var(--spacing-l);
- position: absolute;
- right: 0;
- top: 0;
- }
- #title {
- display: inline-block;
- font-family: var(--header-font-family);
- font-size: var(--font-size-h3);
- font-weight: var(--font-weight-h3);
- line-height: var(--line-height-h3);
- margin-top: var(--spacing-xs);
- }
- #filterInput {
- display: inline-block;
- float: right;
- margin: 0 var(--spacing-l);
- padding: var(--spacing-xs);
- }
- .closeButtonContainer {
- float: right;
- }
- ul {
- margin-bottom: var(--spacing-l);
- }
- ul li {
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius);
- background: var(--chip-background-color);
- display: inline-block;
- margin: 0 var(--spacing-xs) var(--spacing-s) var(--spacing-xs);
- padding: var(--spacing-xs) var(--spacing-s);
- }
- .loading.loaded {
- display: none;
- }
- </style>
- <header>
- <h1 id="title">Included In:</h1>
- <span class="closeButtonContainer">
- <gr-button id="closeButton"
- link
- on-click="_handleCloseTap">Close</gr-button>
- </span>
- <iron-input
- placeholder="Filter"
- on-bind-value-changed="_onFilterChanged">
- <input
- id="filterInput"
- is="iron-input"
- placeholder="Filter"
- on-bind-value-changed="_onFilterChanged">
- </iron-input>
- </header>
- <div class$="[[_computeLoadingClass(_loaded)]]">Loading...</div>
- <template
- is="dom-repeat"
- items="[[_computeGroups(_includedIn, _filterText)]]"
- as="group">
- <div>
- <span>[[group.title]]:</span>
- <ul>
- <template is="dom-repeat" items="[[group.items]]">
- <li>[[item]]</li>
- </template>
- </ul>
- </div>
- </template>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-included-in-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
index 01c9b6e..bdf8c1d 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
@@ -14,98 +14,109 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-included-in-dialog_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrIncludedInDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-included-in-dialog'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the user presses the close button.
+ *
+ * @event close
*/
- class GrIncludedInDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-included-in-dialog'; }
- /**
- * Fired when the user presses the close button.
- *
- * @event close
- */
- static get properties() {
- return {
+ static get properties() {
+ return {
+ /** @type {?} */
+ changeNum: {
+ type: Object,
+ observer: '_resetData',
+ },
/** @type {?} */
- changeNum: {
- type: Object,
- observer: '_resetData',
- },
- /** @type {?} */
- _includedIn: Object,
- _loaded: {
- type: Boolean,
- value: false,
- },
- _filterText: {
- type: String,
- value: '',
- },
- };
- }
-
- loadData() {
- if (!this.changeNum) { return; }
- this._filterText = '';
- return this.$.restAPI.getChangeIncludedIn(this.changeNum).then(
- configs => {
- if (!configs) { return; }
- this._includedIn = configs;
- this._loaded = true;
- });
- }
-
- _resetData() {
- this._includedIn = null;
- this._loaded = false;
- }
-
- _computeGroups(includedIn, filterText) {
- if (!includedIn) { return []; }
-
- const filter = item => !filterText.length ||
- item.toLowerCase().indexOf(filterText.toLowerCase()) !== -1;
-
- const groups = [
- {title: 'Branches', items: includedIn.branches.filter(filter)},
- {title: 'Tags', items: includedIn.tags.filter(filter)},
- ];
- if (includedIn.external) {
- for (const externalKey of Object.keys(includedIn.external)) {
- groups.push({
- title: externalKey,
- items: includedIn.external[externalKey].filter(filter),
- });
- }
- }
- return groups.filter(g => g.items.length);
- }
-
- _handleCloseTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('close', null, {bubbles: false});
- }
-
- _computeLoadingClass(loaded) {
- return loaded ? 'loading loaded' : 'loading';
- }
-
- _onFilterChanged() {
- this.debounce('filter-change', () => {
- this._filterText = this.$.filterInput.bindValue;
- }, 100);
- }
+ _includedIn: Object,
+ _loaded: {
+ type: Boolean,
+ value: false,
+ },
+ _filterText: {
+ type: String,
+ value: '',
+ },
+ };
}
- customElements.define(GrIncludedInDialog.is, GrIncludedInDialog);
-})();
+ loadData() {
+ if (!this.changeNum) { return; }
+ this._filterText = '';
+ return this.$.restAPI.getChangeIncludedIn(this.changeNum).then(
+ configs => {
+ if (!configs) { return; }
+ this._includedIn = configs;
+ this._loaded = true;
+ });
+ }
+
+ _resetData() {
+ this._includedIn = null;
+ this._loaded = false;
+ }
+
+ _computeGroups(includedIn, filterText) {
+ if (!includedIn) { return []; }
+
+ const filter = item => !filterText.length ||
+ item.toLowerCase().indexOf(filterText.toLowerCase()) !== -1;
+
+ const groups = [
+ {title: 'Branches', items: includedIn.branches.filter(filter)},
+ {title: 'Tags', items: includedIn.tags.filter(filter)},
+ ];
+ if (includedIn.external) {
+ for (const externalKey of Object.keys(includedIn.external)) {
+ groups.push({
+ title: externalKey,
+ items: includedIn.external[externalKey].filter(filter),
+ });
+ }
+ }
+ return groups.filter(g => g.items.length);
+ }
+
+ _handleCloseTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('close', null, {bubbles: false});
+ }
+
+ _computeLoadingClass(loaded) {
+ return loaded ? 'loading loaded' : 'loading';
+ }
+
+ _onFilterChanged() {
+ this.debounce('filter-change', () => {
+ this._filterText = this.$.filterInput.bindValue;
+ }, 100);
+ }
+}
+
+customElements.define(GrIncludedInDialog.is, GrIncludedInDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
new file mode 100644
index 0000000..b7d455c
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
@@ -0,0 +1,90 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ background-color: var(--dialog-background-color);
+ display: block;
+ max-height: 80vh;
+ overflow-y: auto;
+ padding: 4.5em var(--spacing-l) var(--spacing-l) var(--spacing-l);
+ }
+ header {
+ background-color: var(--dialog-background-color);
+ border-bottom: 1px solid var(--border-color);
+ left: 0;
+ padding: var(--spacing-l);
+ position: absolute;
+ right: 0;
+ top: 0;
+ }
+ #title {
+ display: inline-block;
+ font-family: var(--header-font-family);
+ font-size: var(--font-size-h3);
+ font-weight: var(--font-weight-h3);
+ line-height: var(--line-height-h3);
+ margin-top: var(--spacing-xs);
+ }
+ #filterInput {
+ display: inline-block;
+ float: right;
+ margin: 0 var(--spacing-l);
+ padding: var(--spacing-xs);
+ }
+ .closeButtonContainer {
+ float: right;
+ }
+ ul {
+ margin-bottom: var(--spacing-l);
+ }
+ ul li {
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ background: var(--chip-background-color);
+ display: inline-block;
+ margin: 0 var(--spacing-xs) var(--spacing-s) var(--spacing-xs);
+ padding: var(--spacing-xs) var(--spacing-s);
+ }
+ .loading.loaded {
+ display: none;
+ }
+ </style>
+ <header>
+ <h1 id="title">Included In:</h1>
+ <span class="closeButtonContainer">
+ <gr-button id="closeButton" link="" on-click="_handleCloseTap">Close</gr-button>
+ </span>
+ <iron-input placeholder="Filter" on-bind-value-changed="_onFilterChanged">
+ <input id="filterInput" is="iron-input" placeholder="Filter" on-bind-value-changed="_onFilterChanged">
+ </iron-input>
+ </header>
+ <div class\$="[[_computeLoadingClass(_loaded)]]">Loading...</div>
+ <template is="dom-repeat" items="[[_computeGroups(_includedIn, _filterText)]]" as="group">
+ <div>
+ <span>[[group.title]]:</span>
+ <ul>
+ <template is="dom-repeat" items="[[group.items]]">
+ <li>[[item]]</li>
+ </template>
+ </ul>
+ </div>
+ </template>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
index bec6c7b..c87c54f 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-included-in-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-included-in-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,54 +30,55 @@
</template>
</test-fixture>
-<script>
- suite('gr-included-in-dialog', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-included-in-dialog.js';
+suite('gr-included-in-dialog', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('_computeGroups', () => {
- const includedIn = {branches: [], tags: []};
- let filterText = '';
- assert.deepEqual(element._computeGroups(includedIn, filterText), []);
-
- includedIn.branches.push('master', 'development', 'stable-2.0');
- includedIn.tags.push('v1.9', 'v2.0', 'v2.1');
- assert.deepEqual(element._computeGroups(includedIn, filterText), [
- {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
- {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
- ]);
-
- includedIn.external = {};
- assert.deepEqual(element._computeGroups(includedIn, filterText), [
- {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
- {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
- ]);
-
- includedIn.external.foo = ['abc', 'def', 'ghi'];
- assert.deepEqual(element._computeGroups(includedIn, filterText), [
- {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
- {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
- {title: 'foo', items: ['abc', 'def', 'ghi']},
- ]);
-
- filterText = 'v2';
- assert.deepEqual(element._computeGroups(includedIn, filterText), [
- {title: 'Tags', items: ['v2.0', 'v2.1']},
- ]);
-
- // Filtering is case-insensitive.
- filterText = 'V2';
- assert.deepEqual(element._computeGroups(includedIn, filterText), [
- {title: 'Tags', items: ['v2.0', 'v2.1']},
- ]);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('_computeGroups', () => {
+ const includedIn = {branches: [], tags: []};
+ let filterText = '';
+ assert.deepEqual(element._computeGroups(includedIn, filterText), []);
+
+ includedIn.branches.push('master', 'development', 'stable-2.0');
+ includedIn.tags.push('v1.9', 'v2.0', 'v2.1');
+ assert.deepEqual(element._computeGroups(includedIn, filterText), [
+ {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
+ {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
+ ]);
+
+ includedIn.external = {};
+ assert.deepEqual(element._computeGroups(includedIn, filterText), [
+ {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
+ {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
+ ]);
+
+ includedIn.external.foo = ['abc', 'def', 'ghi'];
+ assert.deepEqual(element._computeGroups(includedIn, filterText), [
+ {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
+ {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
+ {title: 'foo', items: ['abc', 'def', 'ghi']},
+ ]);
+
+ filterText = 'v2';
+ assert.deepEqual(element._computeGroups(includedIn, filterText), [
+ {title: 'Tags', items: ['v2.0', 'v2.1']},
+ ]);
+
+ // Filtering is case-insensitive.
+ filterText = 'V2';
+ assert.deepEqual(element._computeGroups(includedIn, filterText), [
+ {title: 'Tags', items: ['v2.0', 'v2.1']},
+ ]);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
deleted file mode 100644
index 50c01aa..0000000
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
+++ /dev/null
@@ -1,140 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-selector/iron-selector.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../../styles/gr-voting-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-label-score-row">
- <template>
- <style include="gr-voting-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- .labelNameCell,
- .buttonsCell,
- .selectedValueCell {
- padding: var(--spacing-s) var(--spacing-m);
- display: table-cell;
- }
- /* We want the :hover highlight to extend to the border of the dialog. */
- .labelNameCell {
- padding-left: var(--spacing-xl);
- }
- .selectedValueCell {
- padding-right: var(--spacing-xl);
- }
- /* This is a trick to let the selectedValueCell take the remaining width. */
- .labelNameCell,
- .buttonsCell {
- white-space: nowrap;
- }
- .selectedValueCell {
- width: 75%;
- }
- .labelMessage {
- color: var(--deemphasized-text-color);
- }
- gr-button {
- min-width: 42px;
- box-sizing: border-box;
- --gr-button: {
- background-color: var(--button-background-color, var(--table-header-background-color));
- color: var(--primary-text-color);
- padding: 0 var(--spacing-m);
- @apply --vote-chip-styles;
- }
- }
- gr-button.iron-selected[vote="max"] {
- --button-background-color: var(--vote-color-approved);
- }
- gr-button.iron-selected[vote="positive"] {
- --button-background-color: var(--vote-color-recommended);
- }
- gr-button.iron-selected[vote="min"] {
- --button-background-color: var(--vote-color-rejected);
- }
- gr-button.iron-selected[vote="negative"] {
- --button-background-color: var(--vote-color-disliked);
- }
- gr-button.iron-selected[vote="neutral"] {
- --button-background-color: var(--vote-color-neutral);
- }
- .placeholder {
- display: inline-block;
- width: 42px;
- height: 1px;
- }
- .placeholder::before {
- content: ' ';
- }
- .selectedValueCell {
- color: var(--deemphasized-text-color);
- font-style: italic;
- }
- .selectedValueCell.hidden {
- display: none;
- }
- @media only screen and (max-width: 50em) {
- .selectedValueCell {
- display: none;
- }
- }
- </style>
- <span class="labelNameCell">[[label.name]]</span>
- <div class="buttonsCell">
- <template is="dom-repeat"
- items="[[_computeBlankItems(permittedLabels, label.name, 'start')]]"
- as="value">
- <span class="placeholder" data-label$="[[label.name]]"></span>
- </template>
- <iron-selector
- id="labelSelector"
- attr-for-selected="data-value"
- selected="[[_computeLabelValue(labels, permittedLabels, label)]]"
- hidden$="[[!_computeAnyPermittedLabelValues(permittedLabels, label.name)]]"
- on-selected-item-changed="_setSelectedValueText">
- <template is="dom-repeat"
- items="[[_items]]"
- as="value">
- <gr-button
- vote$="[[_computeVoteAttribute(value, index, _items.length)]]"
- has-tooltip
- data-name$="[[label.name]]"
- data-value$="[[value]]"
- title$="[[_computeLabelValueTitle(labels, label.name, value)]]">
- [[value]]</gr-button>
- </template>
- </iron-selector>
- <template is="dom-repeat"
- items="[[_computeBlankItems(permittedLabels, label.name, 'end')]]"
- as="value">
- <span class="placeholder" data-label$="[[label.name]]"></span>
- </template>
- <span class="labelMessage"
- hidden$="[[_computeAnyPermittedLabelValues(permittedLabels, label.name)]]">
- You don't have permission to edit this label.
- </span>
- </div>
- <div class$="selectedValueCell [[_computeHiddenClass(permittedLabels, label.name)]]">
- <span id="selectedValueLabel">[[_selectedValueText]]</span>
- </div>
- </template>
- <script src="gr-label-score-row.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
index 0316428..8541840 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
@@ -14,167 +14,176 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrLabelScoreRow extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-label-score-row'; }
+import '@polymer/iron-selector/iron-selector.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../../styles/gr-voting-styles.js';
+import '../../../styles/shared-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-label-score-row_html.js';
+
+/** @extends Polymer.Element */
+class GrLabelScoreRow extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-label-score-row'; }
+ /**
+ * Fired when any label is changed.
+ *
+ * @event labels-changed
+ */
+
+ static get properties() {
+ return {
/**
- * Fired when any label is changed.
- *
- * @event labels-changed
+ * @type {{ name: string }}
*/
+ label: Object,
+ labels: Object,
+ name: {
+ type: String,
+ reflectToAttribute: true,
+ },
+ permittedLabels: Object,
+ labelValues: Object,
+ _selectedValueText: {
+ type: String,
+ value: 'No value selected',
+ },
+ _items: {
+ type: Array,
+ computed: '_computePermittedLabelValues(permittedLabels, label.name)',
+ },
+ };
+ }
- static get properties() {
- return {
- /**
- * @type {{ name: string }}
- */
- label: Object,
- labels: Object,
- name: {
- type: String,
- reflectToAttribute: true,
- },
- permittedLabels: Object,
- labelValues: Object,
- _selectedValueText: {
- type: String,
- value: 'No value selected',
- },
- _items: {
- type: Array,
- computed: '_computePermittedLabelValues(permittedLabels, label.name)',
- },
- };
+ get selectedItem() {
+ if (!this._ironSelector) { return undefined; }
+ return this._ironSelector.selectedItem;
+ }
+
+ get selectedValue() {
+ if (!this._ironSelector) { return undefined; }
+ return this._ironSelector.selected;
+ }
+
+ setSelectedValue(value) {
+ // The selector may not be present if it’s not at the latest patch set.
+ if (!this._ironSelector) { return; }
+ this._ironSelector.select(value);
+ }
+
+ get _ironSelector() {
+ return this.$ && this.$.labelSelector;
+ }
+
+ _computeBlankItems(permittedLabels, label, side) {
+ if (!permittedLabels || !permittedLabels[label] ||
+ !permittedLabels[label].length || !this.labelValues ||
+ !Object.keys(this.labelValues).length) {
+ return [];
}
-
- get selectedItem() {
- if (!this._ironSelector) { return undefined; }
- return this._ironSelector.selectedItem;
+ const startPosition = this.labelValues[parseInt(
+ permittedLabels[label][0], 10)];
+ if (side === 'start') {
+ return new Array(startPosition);
}
+ const endPosition = this.labelValues[parseInt(
+ permittedLabels[label][permittedLabels[label].length - 1], 10)];
+ return new Array(Object.keys(this.labelValues).length - endPosition - 1);
+ }
- get selectedValue() {
- if (!this._ironSelector) { return undefined; }
- return this._ironSelector.selected;
- }
-
- setSelectedValue(value) {
- // The selector may not be present if it’s not at the latest patch set.
- if (!this._ironSelector) { return; }
- this._ironSelector.select(value);
- }
-
- get _ironSelector() {
- return this.$ && this.$.labelSelector;
- }
-
- _computeBlankItems(permittedLabels, label, side) {
- if (!permittedLabels || !permittedLabels[label] ||
- !permittedLabels[label].length || !this.labelValues ||
- !Object.keys(this.labelValues).length) {
- return [];
- }
- const startPosition = this.labelValues[parseInt(
- permittedLabels[label][0], 10)];
- if (side === 'start') {
- return new Array(startPosition);
- }
- const endPosition = this.labelValues[parseInt(
- permittedLabels[label][permittedLabels[label].length - 1], 10)];
- return new Array(Object.keys(this.labelValues).length - endPosition - 1);
- }
-
- _getLabelValue(labels, permittedLabels, label) {
- if (label.value) {
- return label.value;
- } else if (labels[label.name].hasOwnProperty('default_value') &&
- permittedLabels.hasOwnProperty(label.name)) {
- // default_value is an int, convert it to string label, e.g. "+1".
- return permittedLabels[label.name].find(
- value => parseInt(value, 10) === labels[label.name].default_value);
- }
- }
-
- /**
- * Maps the label value to exactly one of: min, max, positive, negative,
- * neutral. Used for the 'vote' attribute, because we don't want to
- * interfere with <iron-selector> using the 'class' attribute for setting
- * 'iron-selected'.
- */
- _computeVoteAttribute(value, index, totalItems) {
- if (value < 0 && index === 0) {
- return 'min';
- } else if (value < 0) {
- return 'negative';
- } else if (value > 0 && index === totalItems - 1) {
- return 'max';
- } else if (value > 0) {
- return 'positive';
- } else {
- return 'neutral';
- }
- }
-
- _computeLabelValue(labels, permittedLabels, label) {
- if ([labels, permittedLabels, label].some(arg => arg === undefined)) {
- return null;
- }
- if (!labels[label.name]) { return null; }
- const labelValue = this._getLabelValue(labels, permittedLabels, label);
- const len = permittedLabels[label.name] != null ?
- permittedLabels[label.name].length : 0;
- for (let i = 0; i < len; i++) {
- const val = permittedLabels[label.name][i];
- if (val === labelValue) {
- return val;
- }
- }
- return null;
- }
-
- _setSelectedValueText(e) {
- // Needed because when the selected item changes, it first changes to
- // nothing and then to the new item.
- if (!e.target.selectedItem) { return; }
- this._selectedValueText = e.target.selectedItem.getAttribute('title');
- // Needed to update the style of the selected button.
- this.updateStyles();
- const name = e.target.selectedItem.dataset.name;
- const value = e.target.selectedItem.dataset.value;
- this.dispatchEvent(new CustomEvent(
- 'labels-changed',
- {detail: {name, value}, bubbles: true, composed: true}));
- }
-
- _computeAnyPermittedLabelValues(permittedLabels, label) {
- return permittedLabels && permittedLabels.hasOwnProperty(label) &&
- permittedLabels[label].length;
- }
-
- _computeHiddenClass(permittedLabels, label) {
- return !this._computeAnyPermittedLabelValues(permittedLabels, label) ?
- 'hidden' : '';
- }
-
- _computePermittedLabelValues(permittedLabels, label) {
- // Polymer 2: check for undefined
- if ([permittedLabels, label].some(arg => arg === undefined)) {
- return undefined;
- }
-
- return permittedLabels[label];
- }
-
- _computeLabelValueTitle(labels, label, value) {
- return labels[label] &&
- labels[label].values &&
- labels[label].values[value];
+ _getLabelValue(labels, permittedLabels, label) {
+ if (label.value) {
+ return label.value;
+ } else if (labels[label.name].hasOwnProperty('default_value') &&
+ permittedLabels.hasOwnProperty(label.name)) {
+ // default_value is an int, convert it to string label, e.g. "+1".
+ return permittedLabels[label.name].find(
+ value => parseInt(value, 10) === labels[label.name].default_value);
}
}
- customElements.define(GrLabelScoreRow.is, GrLabelScoreRow);
-})();
+ /**
+ * Maps the label value to exactly one of: min, max, positive, negative,
+ * neutral. Used for the 'vote' attribute, because we don't want to
+ * interfere with <iron-selector> using the 'class' attribute for setting
+ * 'iron-selected'.
+ */
+ _computeVoteAttribute(value, index, totalItems) {
+ if (value < 0 && index === 0) {
+ return 'min';
+ } else if (value < 0) {
+ return 'negative';
+ } else if (value > 0 && index === totalItems - 1) {
+ return 'max';
+ } else if (value > 0) {
+ return 'positive';
+ } else {
+ return 'neutral';
+ }
+ }
+
+ _computeLabelValue(labels, permittedLabels, label) {
+ if ([labels, permittedLabels, label].some(arg => arg === undefined)) {
+ return null;
+ }
+ if (!labels[label.name]) { return null; }
+ const labelValue = this._getLabelValue(labels, permittedLabels, label);
+ const len = permittedLabels[label.name] != null ?
+ permittedLabels[label.name].length : 0;
+ for (let i = 0; i < len; i++) {
+ const val = permittedLabels[label.name][i];
+ if (val === labelValue) {
+ return val;
+ }
+ }
+ return null;
+ }
+
+ _setSelectedValueText(e) {
+ // Needed because when the selected item changes, it first changes to
+ // nothing and then to the new item.
+ if (!e.target.selectedItem) { return; }
+ this._selectedValueText = e.target.selectedItem.getAttribute('title');
+ // Needed to update the style of the selected button.
+ this.updateStyles();
+ const name = e.target.selectedItem.dataset.name;
+ const value = e.target.selectedItem.dataset.value;
+ this.dispatchEvent(new CustomEvent(
+ 'labels-changed',
+ {detail: {name, value}, bubbles: true, composed: true}));
+ }
+
+ _computeAnyPermittedLabelValues(permittedLabels, label) {
+ return permittedLabels && permittedLabels.hasOwnProperty(label) &&
+ permittedLabels[label].length;
+ }
+
+ _computeHiddenClass(permittedLabels, label) {
+ return !this._computeAnyPermittedLabelValues(permittedLabels, label) ?
+ 'hidden' : '';
+ }
+
+ _computePermittedLabelValues(permittedLabels, label) {
+ // Polymer 2: check for undefined
+ if ([permittedLabels, label].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ return permittedLabels[label];
+ }
+
+ _computeLabelValueTitle(labels, label, value) {
+ return labels[label] &&
+ labels[label].values &&
+ labels[label].values[value];
+ }
+}
+
+customElements.define(GrLabelScoreRow.is, GrLabelScoreRow);
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js
new file mode 100644
index 0000000..1b5b425
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js
@@ -0,0 +1,115 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="gr-voting-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="shared-styles">
+ .labelNameCell,
+ .buttonsCell,
+ .selectedValueCell {
+ padding: var(--spacing-s) var(--spacing-m);
+ display: table-cell;
+ }
+ /* We want the :hover highlight to extend to the border of the dialog. */
+ .labelNameCell {
+ padding-left: var(--spacing-xl);
+ }
+ .selectedValueCell {
+ padding-right: var(--spacing-xl);
+ }
+ /* This is a trick to let the selectedValueCell take the remaining width. */
+ .labelNameCell,
+ .buttonsCell {
+ white-space: nowrap;
+ }
+ .selectedValueCell {
+ width: 75%;
+ }
+ .labelMessage {
+ color: var(--deemphasized-text-color);
+ }
+ gr-button {
+ min-width: 42px;
+ box-sizing: border-box;
+ --gr-button: {
+ background-color: var(--button-background-color, var(--table-header-background-color));
+ color: var(--primary-text-color);
+ padding: 0 var(--spacing-m);
+ @apply --vote-chip-styles;
+ }
+ }
+ gr-button.iron-selected[vote="max"] {
+ --button-background-color: var(--vote-color-approved);
+ }
+ gr-button.iron-selected[vote="positive"] {
+ --button-background-color: var(--vote-color-recommended);
+ }
+ gr-button.iron-selected[vote="min"] {
+ --button-background-color: var(--vote-color-rejected);
+ }
+ gr-button.iron-selected[vote="negative"] {
+ --button-background-color: var(--vote-color-disliked);
+ }
+ gr-button.iron-selected[vote="neutral"] {
+ --button-background-color: var(--vote-color-neutral);
+ }
+ .placeholder {
+ display: inline-block;
+ width: 42px;
+ height: 1px;
+ }
+ .placeholder::before {
+ content: ' ';
+ }
+ .selectedValueCell {
+ color: var(--deemphasized-text-color);
+ font-style: italic;
+ }
+ .selectedValueCell.hidden {
+ display: none;
+ }
+ @media only screen and (max-width: 50em) {
+ .selectedValueCell {
+ display: none;
+ }
+ }
+ </style>
+ <span class="labelNameCell">[[label.name]]</span>
+ <div class="buttonsCell">
+ <template is="dom-repeat" items="[[_computeBlankItems(permittedLabels, label.name, 'start')]]" as="value">
+ <span class="placeholder" data-label\$="[[label.name]]"></span>
+ </template>
+ <iron-selector id="labelSelector" attr-for-selected="data-value" selected="[[_computeLabelValue(labels, permittedLabels, label)]]" hidden\$="[[!_computeAnyPermittedLabelValues(permittedLabels, label.name)]]" on-selected-item-changed="_setSelectedValueText">
+ <template is="dom-repeat" items="[[_items]]" as="value">
+ <gr-button vote\$="[[_computeVoteAttribute(value, index, _items.length)]]" has-tooltip="" data-name\$="[[label.name]]" data-value\$="[[value]]" title\$="[[_computeLabelValueTitle(labels, label.name, value)]]">
+ [[value]]</gr-button>
+ </template>
+ </iron-selector>
+ <template is="dom-repeat" items="[[_computeBlankItems(permittedLabels, label.name, 'end')]]" as="value">
+ <span class="placeholder" data-label\$="[[label.name]]"></span>
+ </template>
+ <span class="labelMessage" hidden\$="[[_computeAnyPermittedLabelValues(permittedLabels, label.name)]]">
+ You don't have permission to edit this label.
+ </span>
+ </div>
+ <div class\$="selectedValueCell [[_computeHiddenClass(permittedLabels, label.name)]]">
+ <span id="selectedValueLabel">[[_selectedValueText]]</span>
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
index b10b932..374472f 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
@@ -19,16 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-label-score-row</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-label-score-row.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,340 +30,342 @@
</template>
</test-fixture>
-<script>
- suite('gr-label-row-score tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-label-score-row.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-label-row-score tests', () => {
+ let element;
+ let sandbox;
- setup(done => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.labels = {
- 'Code-Review': {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: 0,
- value: 1,
- all: [{
- _account_id: 123,
- value: 1,
- }],
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.labels = {
+ 'Code-Review': {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
},
- 'Verified': {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: 0,
+ default_value: 0,
+ value: 1,
+ all: [{
+ _account_id: 123,
value: 1,
- all: [{
- _account_id: 123,
- value: 1,
- }],
+ }],
+ },
+ 'Verified': {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
},
- };
+ default_value: 0,
+ value: 1,
+ all: [{
+ _account_id: 123,
+ value: 1,
+ }],
+ },
+ };
+
+ element.permittedLabels = {
+ 'Code-Review': [
+ '-2',
+ '-1',
+ ' 0',
+ '+1',
+ '+2',
+ ],
+ 'Verified': [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ };
+
+ element.labelValues = {'0': 2, '1': 3, '2': 4, '-2': 0, '-1': 1};
+
+ element.label = {
+ name: 'Verified',
+ value: '+1',
+ };
+
+ flush(done);
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('label picker', () => {
+ const labelsChangedHandler = sandbox.stub();
+ element.addEventListener('labels-changed', labelsChangedHandler);
+ assert.ok(element.$.labelSelector);
+ MockInteractions.tap(element.shadowRoot
+ .querySelector(
+ 'gr-button[data-value="-1"]'));
+ flushAsynchronousOperations();
+ assert.strictEqual(element.selectedValue, '-1');
+ assert.strictEqual(element.selectedItem
+ .textContent.trim(), '-1');
+ assert.strictEqual(
+ element.$.selectedValueLabel.textContent.trim(), 'bad');
+ const detail = labelsChangedHandler.args[0][0].detail;
+ assert.equal(detail.name, 'Verified');
+ assert.equal(detail.value, '-1');
+ });
+
+ test('_computeVoteAttribute', () => {
+ let value = 1;
+ let index = 0;
+ const totalItems = 5;
+ // positive and first position
+ assert.equal(element._computeVoteAttribute(value, index,
+ totalItems), 'positive');
+ // negative and first position
+ value = -1;
+ assert.equal(element._computeVoteAttribute(value, index,
+ totalItems), 'min');
+ // negative but not first position
+ index = 1;
+ assert.equal(element._computeVoteAttribute(value, index,
+ totalItems), 'negative');
+ // neutral
+ value = 0;
+ assert.equal(element._computeVoteAttribute(value, index,
+ totalItems), 'neutral');
+ // positive but not last position
+ value = 1;
+ assert.equal(element._computeVoteAttribute(value, index,
+ totalItems), 'positive');
+ // positive and last position
+ index = 4;
+ assert.equal(element._computeVoteAttribute(value, index,
+ totalItems), 'max');
+ // negative and last position
+ value = -1;
+ assert.equal(element._computeVoteAttribute(value, index,
+ totalItems), 'negative');
+ });
+
+ test('correct item is selected', () => {
+ // 1 should be the value of the selected item
+ assert.strictEqual(element.$.labelSelector.selected, '+1');
+ assert.strictEqual(
+ element.$.labelSelector.selectedItem
+ .textContent.trim(), '+1');
+ assert.strictEqual(
+ element.$.selectedValueLabel.textContent.trim(), 'good');
+ });
+
+ test('do not display tooltips on touch devices', () => {
+ const verifiedBtn = element.shadowRoot
+ .querySelector(
+ 'iron-selector > gr-button[data-value="-1"]');
+
+ // On touch devices, tooltips should not be shown.
+ verifiedBtn._isTouchDevice = true;
+ verifiedBtn._handleShowTooltip();
+ assert.isNotOk(verifiedBtn._tooltip);
+ verifiedBtn._handleHideTooltip();
+ assert.isNotOk(verifiedBtn._tooltip);
+
+ // On other devices, tooltips should be shown.
+ verifiedBtn._isTouchDevice = false;
+ verifiedBtn._handleShowTooltip();
+ assert.isOk(verifiedBtn._tooltip);
+ verifiedBtn._handleHideTooltip();
+ assert.isNotOk(verifiedBtn._tooltip);
+ });
+
+ test('_computeLabelValue', () => {
+ assert.strictEqual(element._computeLabelValue(element.labels,
+ element.permittedLabels,
+ element.label), '+1');
+ });
+
+ test('_computeBlankItems', () => {
+ element.labelValues = {
+ '-2': 0,
+ '-1': 1,
+ '0': 2,
+ '1': 3,
+ '2': 4,
+ };
+
+ assert.strictEqual(element._computeBlankItems(element.permittedLabels,
+ 'Code-Review').length, 0);
+
+ assert.strictEqual(element._computeBlankItems(element.permittedLabels,
+ 'Verified').length, 1);
+ });
+
+ test('labelValues returns no keys', () => {
+ element.labelValues = {};
+
+ assert.deepEqual(element._computeBlankItems(element.permittedLabels,
+ 'Code-Review'), []);
+ });
+
+ test('changes in label score are reflected in the DOM', () => {
+ element.labels = {
+ 'Code-Review': {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
+ },
+ default_value: 0,
+ },
+ 'Verified': {
+ values: {
+ ' 0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
+ },
+ default_value: 0,
+ },
+ };
+ const selector = element.$.labelSelector;
+ element.set('label', {name: 'Verified', value: ' 0'});
+ flushAsynchronousOperations();
+ assert.strictEqual(selector.selected, ' 0');
+ assert.strictEqual(
+ element.$.selectedValueLabel.textContent.trim(), 'No score');
+ });
+
+ test('without permitted labels', () => {
+ element.permittedLabels = {
+ Verified: [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ };
+ flushAsynchronousOperations();
+ assert.isOk(element.$.labelSelector);
+ assert.isFalse(element.$.labelSelector.hidden);
+
+ element.permittedLabels = {};
+ flushAsynchronousOperations();
+ assert.isOk(element.$.labelSelector);
+ assert.isTrue(element.$.labelSelector.hidden);
+
+ element.permittedLabels = {Verified: []};
+ flushAsynchronousOperations();
+ assert.isOk(element.$.labelSelector);
+ assert.isTrue(element.$.labelSelector.hidden);
+ });
+
+ test('asymetrical labels', done => {
+ element.permittedLabels = {
+ 'Code-Review': [
+ '-2',
+ '-1',
+ ' 0',
+ '+1',
+ '+2',
+ ],
+ 'Verified': [
+ ' 0',
+ '+1',
+ ],
+ };
+ flush(() => {
+ assert.strictEqual(element.$.labelSelector
+ .items.length, 2);
+ assert.strictEqual(
+ dom(element.root).querySelectorAll('.placeholder').length,
+ 3);
element.permittedLabels = {
'Code-Review': [
+ ' 0',
+ '+1',
+ ],
+ 'Verified': [
'-2',
'-1',
' 0',
'+1',
'+2',
],
- 'Verified': [
- '-1',
- ' 0',
- '+1',
- ],
- };
-
- element.labelValues = {'0': 2, '1': 3, '2': 4, '-2': 0, '-1': 1};
-
- element.label = {
- name: 'Verified',
- value: '+1',
- };
-
- flush(done);
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('label picker', () => {
- const labelsChangedHandler = sandbox.stub();
- element.addEventListener('labels-changed', labelsChangedHandler);
- assert.ok(element.$.labelSelector);
- MockInteractions.tap(element.shadowRoot
- .querySelector(
- 'gr-button[data-value="-1"]'));
- flushAsynchronousOperations();
- assert.strictEqual(element.selectedValue, '-1');
- assert.strictEqual(element.selectedItem
- .textContent.trim(), '-1');
- assert.strictEqual(
- element.$.selectedValueLabel.textContent.trim(), 'bad');
- const detail = labelsChangedHandler.args[0][0].detail;
- assert.equal(detail.name, 'Verified');
- assert.equal(detail.value, '-1');
- });
-
- test('_computeVoteAttribute', () => {
- let value = 1;
- let index = 0;
- const totalItems = 5;
- // positive and first position
- assert.equal(element._computeVoteAttribute(value, index,
- totalItems), 'positive');
- // negative and first position
- value = -1;
- assert.equal(element._computeVoteAttribute(value, index,
- totalItems), 'min');
- // negative but not first position
- index = 1;
- assert.equal(element._computeVoteAttribute(value, index,
- totalItems), 'negative');
- // neutral
- value = 0;
- assert.equal(element._computeVoteAttribute(value, index,
- totalItems), 'neutral');
- // positive but not last position
- value = 1;
- assert.equal(element._computeVoteAttribute(value, index,
- totalItems), 'positive');
- // positive and last position
- index = 4;
- assert.equal(element._computeVoteAttribute(value, index,
- totalItems), 'max');
- // negative and last position
- value = -1;
- assert.equal(element._computeVoteAttribute(value, index,
- totalItems), 'negative');
- });
-
- test('correct item is selected', () => {
- // 1 should be the value of the selected item
- assert.strictEqual(element.$.labelSelector.selected, '+1');
- assert.strictEqual(
- element.$.labelSelector.selectedItem
- .textContent.trim(), '+1');
- assert.strictEqual(
- element.$.selectedValueLabel.textContent.trim(), 'good');
- });
-
- test('do not display tooltips on touch devices', () => {
- const verifiedBtn = element.shadowRoot
- .querySelector(
- 'iron-selector > gr-button[data-value="-1"]');
-
- // On touch devices, tooltips should not be shown.
- verifiedBtn._isTouchDevice = true;
- verifiedBtn._handleShowTooltip();
- assert.isNotOk(verifiedBtn._tooltip);
- verifiedBtn._handleHideTooltip();
- assert.isNotOk(verifiedBtn._tooltip);
-
- // On other devices, tooltips should be shown.
- verifiedBtn._isTouchDevice = false;
- verifiedBtn._handleShowTooltip();
- assert.isOk(verifiedBtn._tooltip);
- verifiedBtn._handleHideTooltip();
- assert.isNotOk(verifiedBtn._tooltip);
- });
-
- test('_computeLabelValue', () => {
- assert.strictEqual(element._computeLabelValue(element.labels,
- element.permittedLabels,
- element.label), '+1');
- });
-
- test('_computeBlankItems', () => {
- element.labelValues = {
- '-2': 0,
- '-1': 1,
- '0': 2,
- '1': 3,
- '2': 4,
- };
-
- assert.strictEqual(element._computeBlankItems(element.permittedLabels,
- 'Code-Review').length, 0);
-
- assert.strictEqual(element._computeBlankItems(element.permittedLabels,
- 'Verified').length, 1);
- });
-
- test('labelValues returns no keys', () => {
- element.labelValues = {};
-
- assert.deepEqual(element._computeBlankItems(element.permittedLabels,
- 'Code-Review'), []);
- });
-
- test('changes in label score are reflected in the DOM', () => {
- element.labels = {
- 'Code-Review': {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: 0,
- },
- 'Verified': {
- values: {
- ' 0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: 0,
- },
- };
- const selector = element.$.labelSelector;
- element.set('label', {name: 'Verified', value: ' 0'});
- flushAsynchronousOperations();
- assert.strictEqual(selector.selected, ' 0');
- assert.strictEqual(
- element.$.selectedValueLabel.textContent.trim(), 'No score');
- });
-
- test('without permitted labels', () => {
- element.permittedLabels = {
- Verified: [
- '-1',
- ' 0',
- '+1',
- ],
- };
- flushAsynchronousOperations();
- assert.isOk(element.$.labelSelector);
- assert.isFalse(element.$.labelSelector.hidden);
-
- element.permittedLabels = {};
- flushAsynchronousOperations();
- assert.isOk(element.$.labelSelector);
- assert.isTrue(element.$.labelSelector.hidden);
-
- element.permittedLabels = {Verified: []};
- flushAsynchronousOperations();
- assert.isOk(element.$.labelSelector);
- assert.isTrue(element.$.labelSelector.hidden);
- });
-
- test('asymetrical labels', done => {
- element.permittedLabels = {
- 'Code-Review': [
- '-2',
- '-1',
- ' 0',
- '+1',
- '+2',
- ],
- 'Verified': [
- ' 0',
- '+1',
- ],
};
flush(() => {
assert.strictEqual(element.$.labelSelector
- .items.length, 2);
+ .items.length, 5);
assert.strictEqual(
- Polymer.dom(element.root).querySelectorAll('.placeholder').length,
- 3);
-
- element.permittedLabels = {
- 'Code-Review': [
- ' 0',
- '+1',
- ],
- 'Verified': [
- '-2',
- '-1',
- ' 0',
- '+1',
- '+2',
- ],
- };
- flush(() => {
- assert.strictEqual(element.$.labelSelector
- .items.length, 5);
- assert.strictEqual(
- Polymer.dom(element.root).querySelectorAll('.placeholder').length,
- 0);
- done();
- });
+ dom(element.root).querySelectorAll('.placeholder').length,
+ 0);
+ done();
});
});
-
- test('default_value', () => {
- element.permittedLabels = {
- Verified: [
- '-1',
- ' 0',
- '+1',
- ],
- };
- element.labels = {
- Verified: {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: -1,
- },
- };
- element.label = {
- name: 'Verified',
- value: null,
- };
- flushAsynchronousOperations();
- assert.strictEqual(element.selectedValue, '-1');
- });
-
- test('default_value is null if not permitted', () => {
- element.permittedLabels = {
- Verified: [
- '-1',
- ' 0',
- '+1',
- ],
- };
- element.labels = {
- 'Code-Review': {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: -1,
- },
- };
- element.label = {
- name: 'Code-Review',
- value: null,
- };
- flushAsynchronousOperations();
- assert.isNull(element.selectedValue);
- });
});
+
+ test('default_value', () => {
+ element.permittedLabels = {
+ Verified: [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ };
+ element.labels = {
+ Verified: {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
+ },
+ default_value: -1,
+ },
+ };
+ element.label = {
+ name: 'Verified',
+ value: null,
+ };
+ flushAsynchronousOperations();
+ assert.strictEqual(element.selectedValue, '-1');
+ });
+
+ test('default_value is null if not permitted', () => {
+ element.permittedLabels = {
+ Verified: [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ };
+ element.labels = {
+ 'Code-Review': {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
+ },
+ default_value: -1,
+ },
+ };
+ element.label = {
+ name: 'Code-Review',
+ value: null,
+ };
+ flushAsynchronousOperations();
+ assert.isNull(element.selectedValue);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html
deleted file mode 100644
index 8a8a9d5..0000000
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html
+++ /dev/null
@@ -1,62 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-label-score-row/gr-label-score-row.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-label-scores">
- <template>
- <style include="shared-styles">
- .scoresTable {
- display: table;
- width: 100%;
- }
- .mergedMessage {
- font-style: italic;
- text-align: center;
- width: 100%;
- }
- gr-label-score-row:hover {
- background-color: var(--hover-background-color);
- }
- gr-label-score-row {
- display: table-row;
- }
- gr-label-score-row.no-access {
- display: var(--label-no-access-display, table-row);
- }
- </style>
- <div class="scoresTable">
- <template is="dom-repeat" items="[[_labels]]" as="label">
- <gr-label-score-row
- class$="[[_computeLabelAccessClass(label.name, permittedLabels)]]"
- label="[[label]]"
- name="[[label.name]]"
- labels="[[change.labels]]"
- permitted-labels="[[permittedLabels]]"
- label-values="[[_labelValues]]"></gr-label-score-row>
- </template>
- </div>
- <div class="mergedMessage"
- hidden$="[[!_changeIsMerged(change.status)]]">
- Because this change has been merged, votes may not be decreased.
- </div>
- </template>
- <script src="gr-label-scores.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
index dbfdb6a..2d6825b 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
@@ -14,135 +14,143 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrLabelScores extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-label-scores'; }
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-label-score-row/gr-label-score-row.js';
+import '../../../styles/shared-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-label-scores_html.js';
- static get properties() {
- return {
- _labels: {
- type: Array,
- computed: '_computeLabels(change.labels.*, account)',
- },
- permittedLabels: {
- type: Object,
- observer: '_computeColumns',
- },
- /** @type {?} */
- change: Object,
- /** @type {?} */
- account: Object,
+/** @extends Polymer.Element */
+class GrLabelScores extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _labelValues: Object,
- };
- }
+ static get is() { return 'gr-label-scores'; }
- getLabelValues() {
- const labels = {};
- for (const label in this.permittedLabels) {
- if (!this.permittedLabels.hasOwnProperty(label)) { continue; }
+ static get properties() {
+ return {
+ _labels: {
+ type: Array,
+ computed: '_computeLabels(change.labels.*, account)',
+ },
+ permittedLabels: {
+ type: Object,
+ observer: '_computeColumns',
+ },
+ /** @type {?} */
+ change: Object,
+ /** @type {?} */
+ account: Object,
- const selectorEl = this.shadowRoot
- .querySelector(`gr-label-score-row[name="${label}"]`);
- if (!selectorEl) { continue; }
-
- // The user may have not voted on this label.
- if (!selectorEl.selectedItem) { continue; }
-
- const selectedVal = parseInt(selectorEl.selectedValue, 10);
-
- // Only send the selection if the user changed it.
- let prevVal = this._getVoteForAccount(this.change.labels, label,
- this.account);
- if (prevVal !== null) {
- prevVal = parseInt(prevVal, 10);
- }
- if (selectedVal !== prevVal) {
- labels[label] = selectedVal;
- }
- }
- return labels;
- }
-
- _getStringLabelValue(labels, labelName, numberValue) {
- for (const k in labels[labelName].values) {
- if (parseInt(k, 10) === numberValue) {
- return k;
- }
- }
- return numberValue;
- }
-
- _getVoteForAccount(labels, labelName, account) {
- const votes = labels[labelName];
- if (votes.all && votes.all.length > 0) {
- for (let i = 0; i < votes.all.length; i++) {
- if (votes.all[i]._account_id == account._account_id) {
- return this._getStringLabelValue(
- labels, labelName, votes.all[i].value);
- }
- }
- }
- return null;
- }
-
- _computeLabels(labelRecord, account) {
- // Polymer 2: check for undefined
- if ([labelRecord, account].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const labelsObj = labelRecord.base;
- if (!labelsObj) { return []; }
- return Object.keys(labelsObj).sort()
- .map(key => {
- return {
- name: key,
- value: this._getVoteForAccount(labelsObj, key, this.account),
- };
- });
- }
-
- _computeColumns(permittedLabels) {
- const labels = Object.keys(permittedLabels);
- const values = {};
- for (const label of labels) {
- for (const value of permittedLabels[label]) {
- values[parseInt(value, 10)] = true;
- }
- }
-
- const orderedValues = Object.keys(values).sort((a, b) => a - b);
-
- for (let i = 0; i < orderedValues.length; i++) {
- values[orderedValues[i]] = i;
- }
- this._labelValues = values;
- }
-
- _changeIsMerged(changeStatus) {
- return changeStatus === 'MERGED';
- }
-
- /**
- * @param {string|undefined} label
- * @param {Object|undefined} permittedLabels
- * @return {string}
- */
- _computeLabelAccessClass(label, permittedLabels) {
- if (label == null || permittedLabels == null) {
- return '';
- }
-
- return permittedLabels.hasOwnProperty(label) &&
- permittedLabels[label].length ? 'access' : 'no-access';
- }
+ _labelValues: Object,
+ };
}
- customElements.define(GrLabelScores.is, GrLabelScores);
-})();
+ getLabelValues() {
+ const labels = {};
+ for (const label in this.permittedLabels) {
+ if (!this.permittedLabels.hasOwnProperty(label)) { continue; }
+
+ const selectorEl = this.shadowRoot
+ .querySelector(`gr-label-score-row[name="${label}"]`);
+ if (!selectorEl) { continue; }
+
+ // The user may have not voted on this label.
+ if (!selectorEl.selectedItem) { continue; }
+
+ const selectedVal = parseInt(selectorEl.selectedValue, 10);
+
+ // Only send the selection if the user changed it.
+ let prevVal = this._getVoteForAccount(this.change.labels, label,
+ this.account);
+ if (prevVal !== null) {
+ prevVal = parseInt(prevVal, 10);
+ }
+ if (selectedVal !== prevVal) {
+ labels[label] = selectedVal;
+ }
+ }
+ return labels;
+ }
+
+ _getStringLabelValue(labels, labelName, numberValue) {
+ for (const k in labels[labelName].values) {
+ if (parseInt(k, 10) === numberValue) {
+ return k;
+ }
+ }
+ return numberValue;
+ }
+
+ _getVoteForAccount(labels, labelName, account) {
+ const votes = labels[labelName];
+ if (votes.all && votes.all.length > 0) {
+ for (let i = 0; i < votes.all.length; i++) {
+ if (votes.all[i]._account_id == account._account_id) {
+ return this._getStringLabelValue(
+ labels, labelName, votes.all[i].value);
+ }
+ }
+ }
+ return null;
+ }
+
+ _computeLabels(labelRecord, account) {
+ // Polymer 2: check for undefined
+ if ([labelRecord, account].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const labelsObj = labelRecord.base;
+ if (!labelsObj) { return []; }
+ return Object.keys(labelsObj).sort()
+ .map(key => {
+ return {
+ name: key,
+ value: this._getVoteForAccount(labelsObj, key, this.account),
+ };
+ });
+ }
+
+ _computeColumns(permittedLabels) {
+ const labels = Object.keys(permittedLabels);
+ const values = {};
+ for (const label of labels) {
+ for (const value of permittedLabels[label]) {
+ values[parseInt(value, 10)] = true;
+ }
+ }
+
+ const orderedValues = Object.keys(values).sort((a, b) => a - b);
+
+ for (let i = 0; i < orderedValues.length; i++) {
+ values[orderedValues[i]] = i;
+ }
+ this._labelValues = values;
+ }
+
+ _changeIsMerged(changeStatus) {
+ return changeStatus === 'MERGED';
+ }
+
+ /**
+ * @param {string|undefined} label
+ * @param {Object|undefined} permittedLabels
+ * @return {string}
+ */
+ _computeLabelAccessClass(label, permittedLabels) {
+ if (label == null || permittedLabels == null) {
+ return '';
+ }
+
+ return permittedLabels.hasOwnProperty(label) &&
+ permittedLabels[label].length ? 'access' : 'no-access';
+ }
+}
+
+customElements.define(GrLabelScores.is, GrLabelScores);
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.js b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.js
new file mode 100644
index 0000000..b9c53c3
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.js
@@ -0,0 +1,48 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ .scoresTable {
+ display: table;
+ width: 100%;
+ }
+ .mergedMessage {
+ font-style: italic;
+ text-align: center;
+ width: 100%;
+ }
+ gr-label-score-row:hover {
+ background-color: var(--hover-background-color);
+ }
+ gr-label-score-row {
+ display: table-row;
+ }
+ gr-label-score-row.no-access {
+ display: var(--label-no-access-display, table-row);
+ }
+ </style>
+ <div class="scoresTable">
+ <template is="dom-repeat" items="[[_labels]]" as="label">
+ <gr-label-score-row class\$="[[_computeLabelAccessClass(label.name, permittedLabels)]]" label="[[label]]" name="[[label.name]]" labels="[[change.labels]]" permitted-labels="[[permittedLabels]]" label-values="[[_labelValues]]"></gr-label-score-row>
+ </template>
+ </div>
+ <div class="mergedMessage" hidden\$="[[!_changeIsMerged(change.status)]]">
+ Because this change has been merged, votes may not be decreased.
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html
index 9e93110..a01454c0 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-label-scores</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-label-scores.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,166 +30,166 @@
</template>
</test-fixture>
-<script>
- suite('gr-label-scores tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-label-scores.js';
+suite('gr-label-scores tests', () => {
+ let element;
+ let sandbox;
- setup(done => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(false); },
- });
- element = fixture('basic');
- element.change = {
- _number: '123',
- labels: {
- 'Code-Review': {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: 0,
- value: 1,
- all: [{
- _account_id: 123,
- value: 1,
- }],
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(false); },
+ });
+ element = fixture('basic');
+ element.change = {
+ _number: '123',
+ labels: {
+ 'Code-Review': {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
},
- 'Verified': {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: 0,
+ default_value: 0,
+ value: 1,
+ all: [{
+ _account_id: 123,
value: 1,
- all: [{
- _account_id: 123,
- value: 1,
- }],
- },
+ }],
},
- };
+ 'Verified': {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
+ },
+ default_value: 0,
+ value: 1,
+ all: [{
+ _account_id: 123,
+ value: 1,
+ }],
+ },
+ },
+ };
- element.account = {
- _account_id: 123,
- };
+ element.account = {
+ _account_id: 123,
+ };
- element.permittedLabels = {
- 'Code-Review': [
- '-2',
- '-1',
- ' 0',
- '+1',
- '+2',
- ],
- 'Verified': [
- '-1',
- ' 0',
- '+1',
- ],
- };
- flush(done);
- });
+ element.permittedLabels = {
+ 'Code-Review': [
+ '-2',
+ '-1',
+ ' 0',
+ '+1',
+ '+2',
+ ],
+ 'Verified': [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ };
+ flush(done);
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('get and set label scores', () => {
- for (const label in element.permittedLabels) {
- if (element.permittedLabels.hasOwnProperty(label)) {
- const row = element.shadowRoot
- .querySelector('gr-label-score-row[name="' + label + '"]');
- row.setSelectedValue(-1);
- }
+ test('get and set label scores', () => {
+ for (const label in element.permittedLabels) {
+ if (element.permittedLabels.hasOwnProperty(label)) {
+ const row = element.shadowRoot
+ .querySelector('gr-label-score-row[name="' + label + '"]');
+ row.setSelectedValue(-1);
}
- assert.deepEqual(element.getLabelValues(), {
- 'Code-Review': -1,
- 'Verified': -1,
- });
- });
-
- test('_getVoteForAccount', () => {
- const labelName = 'Code-Review';
- assert.strictEqual(element._getVoteForAccount(
- element.change.labels, labelName, element.account),
- '+1');
- });
-
- test('_computeColumns', () => {
- element._computeColumns(element.permittedLabels);
- assert.deepEqual(element._labelValues, {
- '-2': 0,
- '-1': 1,
- '0': 2,
- '1': 3,
- '2': 4,
- });
- });
-
- test('_computeLabelAccessClass undefined case', () => {
- assert.strictEqual(
- element._computeLabelAccessClass(undefined, undefined), '');
- assert.strictEqual(
- element._computeLabelAccessClass('', undefined), '');
- assert.strictEqual(
- element._computeLabelAccessClass(undefined, {}), '');
- });
-
- test('_computeLabelAccessClass has access', () => {
- assert.strictEqual(
- element._computeLabelAccessClass('foo', {foo: ['']}), 'access');
- });
-
- test('_computeLabelAccessClass no access', () => {
- assert.strictEqual(
- element._computeLabelAccessClass('zap', {foo: ['']}), 'no-access');
- });
-
- test('changes in label score are reflected in _labels', () => {
- element.change = {
- _number: '123',
- labels: {
- 'Code-Review': {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: 0,
- },
- 'Verified': {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: 0,
- },
- },
- };
- assert.deepEqual(element._labels [
- {name: 'Code-Review', value: null},
- {name: 'Verified', value: null}
- ]);
- element.set(['change', 'labels', 'Verified', 'all'],
- [{_account_id: 123, value: 1}]);
- assert.deepEqual(element._labels, [
- {name: 'Code-Review', value: null},
- {name: 'Verified', value: '+1'},
- ]);
+ }
+ assert.deepEqual(element.getLabelValues(), {
+ 'Code-Review': -1,
+ 'Verified': -1,
});
});
+
+ test('_getVoteForAccount', () => {
+ const labelName = 'Code-Review';
+ assert.strictEqual(element._getVoteForAccount(
+ element.change.labels, labelName, element.account),
+ '+1');
+ });
+
+ test('_computeColumns', () => {
+ element._computeColumns(element.permittedLabels);
+ assert.deepEqual(element._labelValues, {
+ '-2': 0,
+ '-1': 1,
+ '0': 2,
+ '1': 3,
+ '2': 4,
+ });
+ });
+
+ test('_computeLabelAccessClass undefined case', () => {
+ assert.strictEqual(
+ element._computeLabelAccessClass(undefined, undefined), '');
+ assert.strictEqual(
+ element._computeLabelAccessClass('', undefined), '');
+ assert.strictEqual(
+ element._computeLabelAccessClass(undefined, {}), '');
+ });
+
+ test('_computeLabelAccessClass has access', () => {
+ assert.strictEqual(
+ element._computeLabelAccessClass('foo', {foo: ['']}), 'access');
+ });
+
+ test('_computeLabelAccessClass no access', () => {
+ assert.strictEqual(
+ element._computeLabelAccessClass('zap', {foo: ['']}), 'no-access');
+ });
+
+ test('changes in label score are reflected in _labels', () => {
+ element.change = {
+ _number: '123',
+ labels: {
+ 'Code-Review': {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
+ },
+ default_value: 0,
+ },
+ 'Verified': {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
+ },
+ default_value: 0,
+ },
+ },
+ };
+ assert.deepEqual(element._labels [
+ ({name: 'Code-Review', value: null}, {name: 'Verified', value: null})
+ ]);
+ element.set(['change', 'labels', 'Verified', 'all'],
+ [{_account_id: 123, value: 1}]);
+ assert.deepEqual(element._labels, [
+ {name: 'Code-Review', value: null},
+ {name: 'Verified', value: '+1'},
+ ]);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index 13a4213..e5cbaf1 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,379 +14,396 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+:\s*(.*)/;
- const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?$/;
+import '@polymer/iron-icon/iron-icon.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../shared/gr-account-label/gr-account-label.js';
+import '../../shared/gr-account-chip/gr-account-chip.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-formatted-text/gr-formatted-text.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import '../../../styles/gr-voting-styles.js';
+import '../gr-comment-list/gr-comment-list.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-message_html.js';
+
+const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+:\s*(.*)/;
+const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?$/;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrMessage extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-message'; }
+ /**
+ * Fired when this message's reply link is tapped.
+ *
+ * @event reply
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the message's timestamp is tapped.
+ *
+ * @event message-anchor-tap
*/
- class GrMessage extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-message'; }
- /**
- * Fired when this message's reply link is tapped.
- *
- * @event reply
- */
- /**
- * Fired when the message's timestamp is tapped.
- *
- * @event message-anchor-tap
- */
+ /**
+ * Fired when a change message is deleted.
+ *
+ * @event change-message-deleted
+ */
- /**
- * Fired when a change message is deleted.
- *
- * @event change-message-deleted
- */
+ static get properties() {
+ return {
+ changeNum: Number,
+ /** @type {?} */
+ message: Object,
+ author: {
+ type: Object,
+ computed: '_computeAuthor(message)',
+ },
+ comments: {
+ type: Object,
+ observer: '_commentsChanged',
+ },
+ config: Object,
+ hideAutomated: {
+ type: Boolean,
+ value: false,
+ },
+ hidden: {
+ type: Boolean,
+ computed: '_computeIsHidden(hideAutomated, isAutomated)',
+ reflectToAttribute: true,
+ },
+ isAutomated: {
+ type: Boolean,
+ computed: '_computeIsAutomated(message)',
+ },
+ showOnBehalfOf: {
+ type: Boolean,
+ computed: '_computeShowOnBehalfOf(message)',
+ },
+ showReplyButton: {
+ type: Boolean,
+ computed: '_computeShowReplyButton(message, _loggedIn)',
+ },
+ projectName: {
+ type: String,
+ observer: '_projectNameChanged',
+ },
- static get properties() {
- return {
- changeNum: Number,
- /** @type {?} */
- message: Object,
- author: {
- type: Object,
- computed: '_computeAuthor(message)',
- },
- comments: {
- type: Object,
- observer: '_commentsChanged',
- },
- config: Object,
- hideAutomated: {
- type: Boolean,
- value: false,
- },
- hidden: {
- type: Boolean,
- computed: '_computeIsHidden(hideAutomated, isAutomated)',
- reflectToAttribute: true,
- },
- isAutomated: {
- type: Boolean,
- computed: '_computeIsAutomated(message)',
- },
- showOnBehalfOf: {
- type: Boolean,
- computed: '_computeShowOnBehalfOf(message)',
- },
- showReplyButton: {
- type: Boolean,
- computed: '_computeShowReplyButton(message, _loggedIn)',
- },
- projectName: {
- type: String,
- observer: '_projectNameChanged',
- },
+ /**
+ * A mapping from label names to objects representing the minimum and
+ * maximum possible values for that label.
+ */
+ labelExtremes: Object,
- /**
- * A mapping from label names to objects representing the minimum and
- * maximum possible values for that label.
- */
- labelExtremes: Object,
+ /**
+ * @type {{ commentlinks: Array }}
+ */
+ _projectConfig: Object,
+ // Computed property needed to trigger Polymer value observing.
+ _expanded: {
+ type: Object,
+ computed: '_computeExpanded(message.expanded)',
+ },
+ _messageContentExpanded: {
+ type: String,
+ computed:
+ '_computeMessageContentExpanded(message.message, message.tag)',
+ },
+ _messageContentCollapsed: {
+ type: String,
+ computed:
+ '_computeMessageContentCollapsed(message.message, message.tag)',
+ },
+ _commentCountText: {
+ type: Number,
+ computed: '_computeCommentCountText(comments)',
+ },
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+ _isAdmin: {
+ type: Boolean,
+ value: false,
+ },
+ _isDeletingChangeMsg: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
- /**
- * @type {{ commentlinks: Array }}
- */
- _projectConfig: Object,
- // Computed property needed to trigger Polymer value observing.
- _expanded: {
- type: Object,
- computed: '_computeExpanded(message.expanded)',
- },
- _messageContentExpanded: {
- type: String,
- computed:
- '_computeMessageContentExpanded(message.message, message.tag)',
- },
- _messageContentCollapsed: {
- type: String,
- computed:
- '_computeMessageContentCollapsed(message.message, message.tag)',
- },
- _commentCountText: {
- type: Number,
- computed: '_computeCommentCountText(comments)',
- },
- _loggedIn: {
- type: Boolean,
- value: false,
- },
- _isAdmin: {
- type: Boolean,
- value: false,
- },
- _isDeletingChangeMsg: {
- type: Boolean,
- value: false,
- },
- };
- }
+ static get observers() {
+ return [
+ '_updateExpandedClass(message.expanded)',
+ ];
+ }
- static get observers() {
- return [
- '_updateExpandedClass(message.expanded)',
- ];
- }
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('click',
+ e => this._handleClick(e));
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('click',
- e => this._handleClick(e));
- }
+ /** @override */
+ ready() {
+ super.ready();
+ this.$.restAPI.getConfig().then(config => {
+ this.config = config;
+ });
+ this.$.restAPI.getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ });
+ this.$.restAPI.getIsAdmin().then(isAdmin => {
+ this._isAdmin = isAdmin;
+ });
+ }
- /** @override */
- ready() {
- super.ready();
- this.$.restAPI.getConfig().then(config => {
- this.config = config;
- });
- this.$.restAPI.getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- });
- this.$.restAPI.getIsAdmin().then(isAdmin => {
- this._isAdmin = isAdmin;
- });
- }
-
- _updateExpandedClass(expanded) {
- if (expanded) {
- this.classList.add('expanded');
- } else {
- this.classList.remove('expanded');
- }
- }
-
- _computeCommentCountText(comments) {
- if (!comments) return undefined;
- let count = 0;
- for (const file in comments) {
- if (comments.hasOwnProperty(file)) {
- const commentArray = comments[file] || [];
- count += commentArray.length;
- }
- }
- if (count === 0) {
- return undefined;
- } else if (count === 1) {
- return '1 comment';
- } else {
- return `${count} comments`;
- }
- }
-
- _computeMessageContentExpanded(content, tag) {
- return this._computeMessageContent(content, tag, true);
- }
-
- _computeMessageContentCollapsed(content, tag) {
- return this._computeMessageContent(content, tag, false);
- }
-
- _computeMessageContent(content, tag, isExpanded) {
- content = content || '';
- tag = tag || '';
- const isNewPatchSet = tag.endsWith(':newPatchSet') ||
- tag.endsWith(':newWipPatchSet');
- const lines = content.split('\n');
- const filteredLines = lines.filter(line => {
- if (!isExpanded && line.startsWith('>')) {
- return false;
- }
- if (line.startsWith('(') && line.endsWith(' comment)')) {
- return false;
- }
- if (line.startsWith('(') && line.endsWith(' comments)')) {
- return false;
- }
- if (!isNewPatchSet && line.match(PATCH_SET_PREFIX_PATTERN)) {
- return false;
- }
- return true;
- });
- const mappedLines = filteredLines.map(line => {
- // The change message formatting is not very consistent, so
- // unfortunately we have to do a bit of tweaking here:
- // Labels should be stripped from lines like this:
- // Patch Set 29: Verified+1
- // Rebase messages (which have a ':newPatchSet' tag) should be kept on
- // lines like this:
- // Patch Set 27: Patch Set 26 was rebased
- if (isNewPatchSet) {
- line = line.replace(PATCH_SET_PREFIX_PATTERN, '$1');
- }
- return line;
- });
- return mappedLines.join('\n').trim();
- }
-
- _isMessageContentEmpty() {
- return !this._messageContentExpanded
- || this._messageContentExpanded.length === 0;
- }
-
- _computeAuthor(message) {
- return message.author || message.updated_by;
- }
-
- _computeShowOnBehalfOf(message) {
- const author = message.author || message.updated_by;
- return !!(author && message.real_author &&
- author._account_id != message.real_author._account_id);
- }
-
- _computeShowReplyButton(message, loggedIn) {
- return message && !!message.message && loggedIn &&
- !this._computeIsAutomated(message);
- }
-
- _computeExpanded(expanded) {
- return expanded;
- }
-
- /**
- * If there is no value set on the message object as to whether _expanded
- * should be true or not, then _expanded is set to true if there are
- * inline comments (otherwise false).
- */
- _commentsChanged(value) {
- if (this.message && this.message.expanded === undefined) {
- this.set('message.expanded', Object.keys(value || {}).length > 0);
- }
- }
-
- _handleClick(e) {
- if (this.message.expanded) { return; }
- e.stopPropagation();
- this.set('message.expanded', true);
- }
-
- _handleAuthorClick(e) {
- if (!this.message.expanded) { return; }
- e.stopPropagation();
- this.set('message.expanded', false);
- }
-
- _computeIsAutomated(message) {
- return !!(message.reviewer ||
- this._computeIsReviewerUpdate(message) ||
- (message.tag && message.tag.startsWith('autogenerated')));
- }
-
- _computeIsHidden(hideAutomated, isAutomated) {
- return hideAutomated && isAutomated;
- }
-
- _computeIsReviewerUpdate(event) {
- return event.type === 'REVIEWER_UPDATE';
- }
-
- _getScores(message, labelExtremes) {
- if (!message || !message.message || !labelExtremes) {
- return [];
- }
- const line = message.message.split('\n', 1)[0];
- const patchSetPrefix = PATCH_SET_PREFIX_PATTERN;
- if (!line.match(patchSetPrefix)) {
- return [];
- }
- const scoresRaw = line.split(patchSetPrefix)[1];
- if (!scoresRaw) {
- return [];
- }
- return scoresRaw.split(' ')
- .map(s => s.match(LABEL_TITLE_SCORE_PATTERN))
- .filter(ms =>
- ms && ms.length === 4 && labelExtremes.hasOwnProperty(ms[2]))
- .map(ms => {
- const label = ms[2];
- const value = ms[1] === '-' ? 'removed' : ms[3];
- return {label, value};
- });
- }
-
- _computeScoreClass(score, labelExtremes) {
- // Polymer 2: check for undefined
- if ([score, labelExtremes].some(arg => arg === undefined)) {
- return '';
- }
- if (score.value === 'removed') {
- return 'removed';
- }
- const classes = [];
- if (score.value > 0) {
- classes.push('positive');
- } else if (score.value < 0) {
- classes.push('negative');
- }
- const extremes = labelExtremes[score.label];
- if (extremes) {
- const intScore = parseInt(score.value, 10);
- if (intScore === extremes.max) {
- classes.push('max');
- } else if (intScore === extremes.min) {
- classes.push('min');
- }
- }
- return classes.join(' ');
- }
-
- _computeClass(expanded) {
- const classes = [];
- classes.push(expanded ? 'expanded' : 'collapsed');
- return classes.join(' ');
- }
-
- _handleAnchorClick(e) {
- e.preventDefault();
- this.dispatchEvent(new CustomEvent('message-anchor-tap', {
- bubbles: true,
- composed: true,
- detail: {id: this.message.id},
- }));
- }
-
- _handleReplyTap(e) {
- e.preventDefault();
- this.fire('reply', {message: this.message});
- }
-
- _handleDeleteMessage(e) {
- e.preventDefault();
- if (!this.message || !this.message.id) return;
- this._isDeletingChangeMsg = true;
- this.$.restAPI.deleteChangeCommitMessage(this.changeNum, this.message.id)
- .then(() => {
- this._isDeletingChangeMsg = false;
- this.fire('change-message-deleted', {message: this.message});
- });
- }
-
- _projectNameChanged(name) {
- this.$.restAPI.getProjectConfig(name).then(config => {
- this._projectConfig = config;
- });
- }
-
- _computeExpandToggleIcon(expanded) {
- return expanded ? 'gr-icons:expand-less' : 'gr-icons:expand-more';
- }
-
- _toggleExpanded(e) {
- e.stopPropagation();
- this.set('message.expanded', !this.message.expanded);
+ _updateExpandedClass(expanded) {
+ if (expanded) {
+ this.classList.add('expanded');
+ } else {
+ this.classList.remove('expanded');
}
}
- customElements.define(GrMessage.is, GrMessage);
-})();
+ _computeCommentCountText(comments) {
+ if (!comments) return undefined;
+ let count = 0;
+ for (const file in comments) {
+ if (comments.hasOwnProperty(file)) {
+ const commentArray = comments[file] || [];
+ count += commentArray.length;
+ }
+ }
+ if (count === 0) {
+ return undefined;
+ } else if (count === 1) {
+ return '1 comment';
+ } else {
+ return `${count} comments`;
+ }
+ }
+
+ _computeMessageContentExpanded(content, tag) {
+ return this._computeMessageContent(content, tag, true);
+ }
+
+ _computeMessageContentCollapsed(content, tag) {
+ return this._computeMessageContent(content, tag, false);
+ }
+
+ _computeMessageContent(content, tag, isExpanded) {
+ content = content || '';
+ tag = tag || '';
+ const isNewPatchSet = tag.endsWith(':newPatchSet') ||
+ tag.endsWith(':newWipPatchSet');
+ const lines = content.split('\n');
+ const filteredLines = lines.filter(line => {
+ if (!isExpanded && line.startsWith('>')) {
+ return false;
+ }
+ if (line.startsWith('(') && line.endsWith(' comment)')) {
+ return false;
+ }
+ if (line.startsWith('(') && line.endsWith(' comments)')) {
+ return false;
+ }
+ if (!isNewPatchSet && line.match(PATCH_SET_PREFIX_PATTERN)) {
+ return false;
+ }
+ return true;
+ });
+ const mappedLines = filteredLines.map(line => {
+ // The change message formatting is not very consistent, so
+ // unfortunately we have to do a bit of tweaking here:
+ // Labels should be stripped from lines like this:
+ // Patch Set 29: Verified+1
+ // Rebase messages (which have a ':newPatchSet' tag) should be kept on
+ // lines like this:
+ // Patch Set 27: Patch Set 26 was rebased
+ if (isNewPatchSet) {
+ line = line.replace(PATCH_SET_PREFIX_PATTERN, '$1');
+ }
+ return line;
+ });
+ return mappedLines.join('\n').trim();
+ }
+
+ _isMessageContentEmpty() {
+ return !this._messageContentExpanded
+ || this._messageContentExpanded.length === 0;
+ }
+
+ _computeAuthor(message) {
+ return message.author || message.updated_by;
+ }
+
+ _computeShowOnBehalfOf(message) {
+ const author = message.author || message.updated_by;
+ return !!(author && message.real_author &&
+ author._account_id != message.real_author._account_id);
+ }
+
+ _computeShowReplyButton(message, loggedIn) {
+ return message && !!message.message && loggedIn &&
+ !this._computeIsAutomated(message);
+ }
+
+ _computeExpanded(expanded) {
+ return expanded;
+ }
+
+ /**
+ * If there is no value set on the message object as to whether _expanded
+ * should be true or not, then _expanded is set to true if there are
+ * inline comments (otherwise false).
+ */
+ _commentsChanged(value) {
+ if (this.message && this.message.expanded === undefined) {
+ this.set('message.expanded', Object.keys(value || {}).length > 0);
+ }
+ }
+
+ _handleClick(e) {
+ if (this.message.expanded) { return; }
+ e.stopPropagation();
+ this.set('message.expanded', true);
+ }
+
+ _handleAuthorClick(e) {
+ if (!this.message.expanded) { return; }
+ e.stopPropagation();
+ this.set('message.expanded', false);
+ }
+
+ _computeIsAutomated(message) {
+ return !!(message.reviewer ||
+ this._computeIsReviewerUpdate(message) ||
+ (message.tag && message.tag.startsWith('autogenerated')));
+ }
+
+ _computeIsHidden(hideAutomated, isAutomated) {
+ return hideAutomated && isAutomated;
+ }
+
+ _computeIsReviewerUpdate(event) {
+ return event.type === 'REVIEWER_UPDATE';
+ }
+
+ _getScores(message, labelExtremes) {
+ if (!message || !message.message || !labelExtremes) {
+ return [];
+ }
+ const line = message.message.split('\n', 1)[0];
+ const patchSetPrefix = PATCH_SET_PREFIX_PATTERN;
+ if (!line.match(patchSetPrefix)) {
+ return [];
+ }
+ const scoresRaw = line.split(patchSetPrefix)[1];
+ if (!scoresRaw) {
+ return [];
+ }
+ return scoresRaw.split(' ')
+ .map(s => s.match(LABEL_TITLE_SCORE_PATTERN))
+ .filter(ms =>
+ ms && ms.length === 4 && labelExtremes.hasOwnProperty(ms[2]))
+ .map(ms => {
+ const label = ms[2];
+ const value = ms[1] === '-' ? 'removed' : ms[3];
+ return {label, value};
+ });
+ }
+
+ _computeScoreClass(score, labelExtremes) {
+ // Polymer 2: check for undefined
+ if ([score, labelExtremes].some(arg => arg === undefined)) {
+ return '';
+ }
+ if (score.value === 'removed') {
+ return 'removed';
+ }
+ const classes = [];
+ if (score.value > 0) {
+ classes.push('positive');
+ } else if (score.value < 0) {
+ classes.push('negative');
+ }
+ const extremes = labelExtremes[score.label];
+ if (extremes) {
+ const intScore = parseInt(score.value, 10);
+ if (intScore === extremes.max) {
+ classes.push('max');
+ } else if (intScore === extremes.min) {
+ classes.push('min');
+ }
+ }
+ return classes.join(' ');
+ }
+
+ _computeClass(expanded) {
+ const classes = [];
+ classes.push(expanded ? 'expanded' : 'collapsed');
+ return classes.join(' ');
+ }
+
+ _handleAnchorClick(e) {
+ e.preventDefault();
+ this.dispatchEvent(new CustomEvent('message-anchor-tap', {
+ bubbles: true,
+ composed: true,
+ detail: {id: this.message.id},
+ }));
+ }
+
+ _handleReplyTap(e) {
+ e.preventDefault();
+ this.fire('reply', {message: this.message});
+ }
+
+ _handleDeleteMessage(e) {
+ e.preventDefault();
+ if (!this.message || !this.message.id) return;
+ this._isDeletingChangeMsg = true;
+ this.$.restAPI.deleteChangeCommitMessage(this.changeNum, this.message.id)
+ .then(() => {
+ this._isDeletingChangeMsg = false;
+ this.fire('change-message-deleted', {message: this.message});
+ });
+ }
+
+ _projectNameChanged(name) {
+ this.$.restAPI.getProjectConfig(name).then(config => {
+ this._projectConfig = config;
+ });
+ }
+
+ _computeExpandToggleIcon(expanded) {
+ return expanded ? 'gr-icons:expand-less' : 'gr-icons:expand-more';
+ }
+
+ _toggleExpanded(e) {
+ e.stopPropagation();
+ this.set('message.expanded', !this.message.expanded);
+ }
+}
+
+customElements.define(GrMessage.is, GrMessage);
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
similarity index 65%
rename from polygerrit-ui/app/elements/change/gr-message/gr-message.html
rename to polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
index f7ef7e6..49ced2a 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
@@ -1,36 +1,22 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @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.js';
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-account-label/gr-account-label.html">
-<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../../styles/gr-voting-styles.html">
-
-<link rel="import" href="../gr-comment-list/gr-comment-list.html">
-
-<dom-module id="gr-message">
- <template>
+export const htmlTemplate = html`
<style include="gr-voting-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -193,16 +179,16 @@
}
}
</style>
- <div class$="[[_computeClass(_expanded)]]">
+ <div class\$="[[_computeClass(_expanded)]]">
<div class="contentContainer">
<div class="author" on-click="_handleAuthorClick">
- <span hidden$="[[!showOnBehalfOf]]">
+ <span hidden\$="[[!showOnBehalfOf]]">
<span class="name">[[message.real_author.name]]</span>
on behalf of
</span>
<gr-account-label account="[[author]]" class="authorLabel"></gr-account-label>
<template is="dom-repeat" items="[[_getScores(message, labelExtremes)]]" as="score">
- <span class$="score [[_computeScoreClass(score, labelExtremes)]]">
+ <span class\$="score [[_computeScoreClass(score, labelExtremes)]]">
[[score.label]] [[score.value]]
</span>
</template>
@@ -216,32 +202,18 @@
<template is="dom-if" if="[[message.message]]">
<div class="content">
<div class="message hideOnOpen">[[_messageContentCollapsed]]</div>
- <gr-formatted-text
- no-trailing-margin
- class="message hideOnCollapsed"
- content="[[_messageContentExpanded]]"
- config="[[_projectConfig.commentlinks]]"></gr-formatted-text>
+ <gr-formatted-text no-trailing-margin="" class="message hideOnCollapsed" content="[[_messageContentExpanded]]" config="[[_projectConfig.commentlinks]]"></gr-formatted-text>
<template is="dom-if" if="[[!_isMessageContentEmpty()]]">
- <div class="replyActionContainer" hidden$="[[!showReplyButton]]" hidden>
- <gr-button
- class="replyBtn"
- link small on-click="_handleReplyTap">
+ <div class="replyActionContainer" hidden\$="[[!showReplyButton]]" hidden="">
+ <gr-button class="replyBtn" link="" small="" on-click="_handleReplyTap">
Reply
</gr-button>
- <gr-button
- disabled$=[[_isDeletingChangeMsg]]
- class="deleteBtn" hidden$="[[!_isAdmin]]" hidden
- link small on-click="_handleDeleteMessage">
+ <gr-button disabled\$="[[_isDeletingChangeMsg]]" class="deleteBtn" hidden\$="[[!_isAdmin]]" hidden="" link="" small="" on-click="_handleDeleteMessage">
Delete
</gr-button>
</div>
</template>
- <gr-comment-list
- comments="[[comments]]"
- change-num="[[changeNum]]"
- patch-num="[[message._revision_number]]"
- project-name="[[projectName]]"
- project-config="[[_projectConfig]]"></gr-comment-list>
+ <gr-comment-list comments="[[comments]]" change-num="[[changeNum]]" patch-num="[[message._revision_number]]" project-name="[[projectName]]" project-config="[[_projectConfig]]"></gr-comment-list>
</div>
</template>
<template is="dom-if" if="[[_computeIsReviewerUpdate(message)]]">
@@ -249,8 +221,7 @@
<template is="dom-repeat" items="[[message.updates]]" as="update">
<div class="updateCategory">
[[update.message]]
- <template
- is="dom-repeat" items="[[update.reviewers]]" as="reviewer">
+ <template is="dom-repeat" items="[[update.reviewers]]" as="reviewer">
<gr-account-chip account="[[reviewer]]">
</gr-account-chip>
</template>
@@ -264,29 +235,17 @@
</template>
<template is="dom-if" if="[[!message.id]]">
<span class="date">
- <gr-date-formatter
- has-tooltip
- show-date-and-time
- date-str="[[message.date]]"></gr-date-formatter>
+ <gr-date-formatter has-tooltip="" show-date-and-time="" date-str="[[message.date]]"></gr-date-formatter>
</span>
</template>
<template is="dom-if" if="[[message.id]]">
<span class="date" on-click="_handleAnchorClick">
- <gr-date-formatter
- has-tooltip
- show-date-and-time
- date-str="[[message.date]]"></gr-date-formatter>
+ <gr-date-formatter has-tooltip="" show-date-and-time="" date-str="[[message.date]]"></gr-date-formatter>
</span>
</template>
- <iron-icon
- id="expandToggle"
- on-click="_toggleExpanded"
- title="Toggle expanded state"
- icon="[[_computeExpandToggleIcon(_expanded)]]"></iron-icon>
+ <iron-icon id="expandToggle" on-click="_toggleExpanded" title="Toggle expanded state" icon="[[_computeExpandToggleIcon(_expanded)]]"></iron-icon>
</span>
</div>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-message.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index f22f17e..8864056 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-message</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-message.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,386 +30,388 @@
</template>
</test-fixture>
-<script>
- suite('gr-message tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-message.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-message tests', () => {
+ let element;
- suite('when admin and logged in', () => {
- setup(done => {
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- getPreferences() { return Promise.resolve({}); },
- getConfig() { return Promise.resolve({}); },
- getIsAdmin() { return Promise.resolve(true); },
- deleteChangeCommitMessage() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- flush(done);
+ suite('when admin and logged in', () => {
+ setup(done => {
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
+ getPreferences() { return Promise.resolve({}); },
+ getConfig() { return Promise.resolve({}); },
+ getIsAdmin() { return Promise.resolve(true); },
+ deleteChangeCommitMessage() { return Promise.resolve({}); },
});
+ element = fixture('basic');
+ flush(done);
+ });
- test('reply event', done => {
- element.message = {
- id: '47c43261_55aa2c41',
- author: {
- _account_id: 1115495,
- name: 'Andrew Bonventre',
- email: 'andybons@chromium.org',
- },
- date: '2016-01-12 20:24:49.448000000',
- message: 'Uploaded patch set 1.',
- _revision_number: 1,
- };
+ test('reply event', done => {
+ element.message = {
+ id: '47c43261_55aa2c41',
+ author: {
+ _account_id: 1115495,
+ name: 'Andrew Bonventre',
+ email: 'andybons@chromium.org',
+ },
+ date: '2016-01-12 20:24:49.448000000',
+ message: 'Uploaded patch set 1.',
+ _revision_number: 1,
+ };
- element.addEventListener('reply', e => {
- assert.deepEqual(e.detail.message, element.message);
- done();
- });
- flushAsynchronousOperations();
- assert.isFalse(
- element.shadowRoot.querySelector('.replyActionContainer').hidden
- );
- MockInteractions.tap(element.shadowRoot.querySelector('.replyBtn'));
+ element.addEventListener('reply', e => {
+ assert.deepEqual(e.detail.message, element.message);
+ done();
});
+ flushAsynchronousOperations();
+ assert.isFalse(
+ element.shadowRoot.querySelector('.replyActionContainer').hidden
+ );
+ MockInteractions.tap(element.shadowRoot.querySelector('.replyBtn'));
+ });
- test('can see delete button', () => {
- element.message = {
- id: '47c43261_55aa2c41',
- author: {
- _account_id: 1115495,
- name: 'Andrew Bonventre',
- email: 'andybons@chromium.org',
- },
- date: '2016-01-12 20:24:49.448000000',
- message: 'Uploaded patch set 1.',
- _revision_number: 1,
- };
+ test('can see delete button', () => {
+ element.message = {
+ id: '47c43261_55aa2c41',
+ author: {
+ _account_id: 1115495,
+ name: 'Andrew Bonventre',
+ email: 'andybons@chromium.org',
+ },
+ date: '2016-01-12 20:24:49.448000000',
+ message: 'Uploaded patch set 1.',
+ _revision_number: 1,
+ };
- flushAsynchronousOperations();
- assert.isFalse(element.shadowRoot.querySelector('.deleteBtn').hidden);
+ flushAsynchronousOperations();
+ assert.isFalse(element.shadowRoot.querySelector('.deleteBtn').hidden);
+ });
+
+ test('delete change message', done => {
+ element.message = {
+ id: '47c43261_55aa2c41',
+ author: {
+ _account_id: 1115495,
+ name: 'Andrew Bonventre',
+ email: 'andybons@chromium.org',
+ },
+ date: '2016-01-12 20:24:49.448000000',
+ message: 'Uploaded patch set 1.',
+ _revision_number: 1,
+ };
+
+ element.addEventListener('change-message-deleted', e => {
+ assert.deepEqual(e.detail.message, element.message);
+ assert.isFalse(element.shadowRoot.querySelector('.deleteBtn').disabled);
+ done();
});
+ flushAsynchronousOperations();
+ MockInteractions.tap(element.shadowRoot.querySelector('.deleteBtn'));
+ assert.isTrue(element.shadowRoot.querySelector('.deleteBtn').disabled);
+ });
- test('delete change message', done => {
- element.message = {
- id: '47c43261_55aa2c41',
- author: {
- _account_id: 1115495,
- name: 'Andrew Bonventre',
- email: 'andybons@chromium.org',
- },
- date: '2016-01-12 20:24:49.448000000',
- message: 'Uploaded patch set 1.',
- _revision_number: 1,
- };
+ test('autogenerated prefix hiding', () => {
+ element.message = {
+ tag: 'autogenerated:gerrit:test',
+ updated: '2016-01-12 20:24:49.448000000',
+ };
- element.addEventListener('change-message-deleted', e => {
- assert.deepEqual(e.detail.message, element.message);
- assert.isFalse(element.shadowRoot.querySelector('.deleteBtn').disabled);
- done();
- });
- flushAsynchronousOperations();
- MockInteractions.tap(element.shadowRoot.querySelector('.deleteBtn'));
- assert.isTrue(element.shadowRoot.querySelector('.deleteBtn').disabled);
- });
+ assert.isTrue(element.isAutomated);
+ assert.isFalse(element.hidden);
- test('autogenerated prefix hiding', () => {
- element.message = {
- tag: 'autogenerated:gerrit:test',
- updated: '2016-01-12 20:24:49.448000000',
- };
+ element.hideAutomated = true;
- assert.isTrue(element.isAutomated);
- assert.isFalse(element.hidden);
+ assert.isTrue(element.hidden);
+ });
- element.hideAutomated = true;
+ test('reviewer message treated as autogenerated', () => {
+ element.message = {
+ tag: 'autogenerated:gerrit:test',
+ updated: '2016-01-12 20:24:49.448000000',
+ reviewer: {},
+ };
- assert.isTrue(element.hidden);
- });
+ assert.isTrue(element.isAutomated);
+ assert.isFalse(element.hidden);
- test('reviewer message treated as autogenerated', () => {
- element.message = {
- tag: 'autogenerated:gerrit:test',
- updated: '2016-01-12 20:24:49.448000000',
- reviewer: {},
- };
+ element.hideAutomated = true;
- assert.isTrue(element.isAutomated);
- assert.isFalse(element.hidden);
+ assert.isTrue(element.hidden);
+ });
- element.hideAutomated = true;
+ test('batch reviewer message treated as autogenerated', () => {
+ element.message = {
+ type: 'REVIEWER_UPDATE',
+ updated: '2016-01-12 20:24:49.448000000',
+ reviewer: {},
+ };
- assert.isTrue(element.hidden);
- });
+ assert.isTrue(element.isAutomated);
+ assert.isFalse(element.hidden);
- test('batch reviewer message treated as autogenerated', () => {
- element.message = {
- type: 'REVIEWER_UPDATE',
- updated: '2016-01-12 20:24:49.448000000',
- reviewer: {},
- };
+ element.hideAutomated = true;
- assert.isTrue(element.isAutomated);
- assert.isFalse(element.hidden);
+ assert.isTrue(element.hidden);
+ });
- element.hideAutomated = true;
+ test('tag that is not autogenerated prefix does not hide', () => {
+ element.message = {
+ tag: 'something',
+ updated: '2016-01-12 20:24:49.448000000',
+ };
- assert.isTrue(element.hidden);
- });
+ assert.isFalse(element.isAutomated);
+ assert.isFalse(element.hidden);
- test('tag that is not autogenerated prefix does not hide', () => {
- element.message = {
- tag: 'something',
- updated: '2016-01-12 20:24:49.448000000',
- };
+ element.hideAutomated = true;
- assert.isFalse(element.isAutomated);
- assert.isFalse(element.hidden);
+ assert.isFalse(element.hidden);
+ });
- element.hideAutomated = true;
+ test('reply button hidden unless logged in', () => {
+ const message = {
+ message: 'Uploaded patch set 1.',
+ };
+ assert.isFalse(element._computeShowReplyButton(message, false));
+ assert.isTrue(element._computeShowReplyButton(message, true));
+ });
- assert.isFalse(element.hidden);
- });
+ test('_computeShowOnBehalfOf', () => {
+ const message = {
+ message: '...',
+ };
+ assert.isNotOk(element._computeShowOnBehalfOf(message));
+ message.author = {_account_id: 1115495};
+ assert.isNotOk(element._computeShowOnBehalfOf(message));
+ message.real_author = {_account_id: 1115495};
+ assert.isNotOk(element._computeShowOnBehalfOf(message));
+ message.real_author._account_id = 123456;
+ assert.isOk(element._computeShowOnBehalfOf(message));
+ message.updated_by = message.author;
+ delete message.author;
+ assert.isOk(element._computeShowOnBehalfOf(message));
+ delete message.updated_by;
+ assert.isNotOk(element._computeShowOnBehalfOf(message));
+ });
- test('reply button hidden unless logged in', () => {
- const message = {
- message: 'Uploaded patch set 1.',
- };
- assert.isFalse(element._computeShowReplyButton(message, false));
- assert.isTrue(element._computeShowReplyButton(message, true));
- });
-
- test('_computeShowOnBehalfOf', () => {
- const message = {
- message: '...',
- };
- assert.isNotOk(element._computeShowOnBehalfOf(message));
- message.author = {_account_id: 1115495};
- assert.isNotOk(element._computeShowOnBehalfOf(message));
- message.real_author = {_account_id: 1115495};
- assert.isNotOk(element._computeShowOnBehalfOf(message));
- message.real_author._account_id = 123456;
- assert.isOk(element._computeShowOnBehalfOf(message));
- message.updated_by = message.author;
- delete message.author;
- assert.isOk(element._computeShowOnBehalfOf(message));
- delete message.updated_by;
- assert.isNotOk(element._computeShowOnBehalfOf(message));
- });
-
- ['Trybot-Ready', 'Tryjob-Request', 'Commit-Queue'].forEach(label => {
- test(`${label} ignored for color voting`, () => {
- element.message = {
- author: {},
- expanded: false,
- message: `Patch Set 1: ${label}+1`,
- };
- assert.isNotOk(
- Polymer.dom(element.root).querySelector('.negativeVote'));
- assert.isNotOk(
- Polymer.dom(element.root).querySelector('.positiveVote'));
- });
- });
-
- test('clicking on date link fires event', () => {
- element.message = {
- type: 'REVIEWER_UPDATE',
- updated: '2016-01-12 20:24:49.448000000',
- reviewer: {},
- id: '47c43261_55aa2c41',
- };
- flushAsynchronousOperations();
- const stub = sinon.stub();
- element.addEventListener('message-anchor-tap', stub);
- const dateEl = element.shadowRoot
- .querySelector('.date');
- assert.ok(dateEl);
- MockInteractions.tap(dateEl);
-
- assert.isTrue(stub.called);
- assert.deepEqual(stub.lastCall.args[0].detail, {id: element.message.id});
- });
-
- suite('compute messages', () => {
- test('empty', () => {
- assert.equal(element._computeMessageContent('', '', true), '');
- assert.equal(element._computeMessageContent('', '', false), '');
- });
-
- test('new patchset', () => {
- const original = 'Uploaded patch set 1.';
- const tag = 'autogenerated:gerrit:newPatchSet';
- let actual = element._computeMessageContent(original, tag, true);
- assert.equal(actual, original);
- actual = element._computeMessageContent(original, tag, false);
- assert.equal(actual, original);
- });
-
- test('new patchset rebased', () => {
- const original = 'Patch Set 27: Patch Set 26 was rebased';
- const tag = 'autogenerated:gerrit:newPatchSet';
- const expected = 'Patch Set 26 was rebased';
- let actual = element._computeMessageContent(original, tag, true);
- assert.equal(actual, expected);
- actual = element._computeMessageContent(original, tag, false);
- assert.equal(actual, expected);
- });
-
- test('ready for review', () => {
- const original = 'Patch Set 1:\n\nThis change is ready for review.';
- const tag = undefined;
- const expected = 'This change is ready for review.';
- let actual = element._computeMessageContent(original, tag, true);
- assert.equal(actual, expected);
- actual = element._computeMessageContent(original, tag, false);
- assert.equal(actual, expected);
- });
-
- test('vote', () => {
- const original = 'Patch Set 1: Code-Style+1';
- const tag = undefined;
- const expected = '';
- let actual = element._computeMessageContent(original, tag, true);
- assert.equal(actual, expected);
- actual = element._computeMessageContent(original, tag, false);
- assert.equal(actual, expected);
- });
-
- test('comments', () => {
- const original = 'Patch Set 1:\n\n(3 comments)';
- const tag = undefined;
- const expected = '';
- let actual = element._computeMessageContent(original, tag, true);
- assert.equal(actual, expected);
- actual = element._computeMessageContent(original, tag, false);
- assert.equal(actual, expected);
- });
- });
-
- test('votes', () => {
+ ['Trybot-Ready', 'Tryjob-Request', 'Commit-Queue'].forEach(label => {
+ test(`${label} ignored for color voting`, () => {
element.message = {
author: {},
expanded: false,
- message: 'Patch Set 1: Verified+1 Code-Review-2 Trybot-Label3+1 Blub+1',
+ message: `Patch Set 1: ${label}+1`,
};
- element.labelExtremes = {
- 'Verified': {max: 1, min: -1},
- 'Code-Review': {max: 2, min: -2},
- 'Trybot-Label3': {max: 3, min: 0},
- };
- flushAsynchronousOperations();
- const scoreChips = Polymer.dom(element.root).querySelectorAll('.score');
- assert.equal(scoreChips.length, 3);
-
- assert.isTrue(scoreChips[0].classList.contains('positive'));
- assert.isTrue(scoreChips[0].classList.contains('max'));
-
- assert.isTrue(scoreChips[1].classList.contains('negative'));
- assert.isTrue(scoreChips[1].classList.contains('min'));
-
- assert.isTrue(scoreChips[2].classList.contains('positive'));
- assert.isFalse(scoreChips[2].classList.contains('min'));
- });
-
- test('removed votes', () => {
- element.message = {
- author: {},
- expanded: false,
- message: 'Patch Set 1: Verified+1 -Code-Review -Commit-Queue',
- };
- element.labelExtremes = {
- 'Verified': {max: 1, min: -1},
- 'Code-Review': {max: 2, min: -2},
- 'Commit-Queue': {max: 3, min: 0},
- };
- flushAsynchronousOperations();
- const scoreChips = Polymer.dom(element.root).querySelectorAll('.score');
- assert.equal(scoreChips.length, 3);
-
- assert.isTrue(scoreChips[1].classList.contains('removed'));
- assert.isTrue(scoreChips[2].classList.contains('removed'));
- });
-
- test('false negative vote', () => {
- element.message = {
- author: {},
- expanded: false,
- message: 'Patch Set 1: Cherry Picked from branch stable-2.14.',
- };
- element.labelExtremes = {};
- const scoreChips = Polymer.dom(element.root).querySelectorAll('.score');
- assert.equal(scoreChips.length, 0);
+ assert.isNotOk(
+ dom(element.root).querySelector('.negativeVote'));
+ assert.isNotOk(
+ dom(element.root).querySelector('.positiveVote'));
});
});
- suite('when not logged in', () => {
- setup(done => {
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(false); },
- getPreferences() { return Promise.resolve({}); },
- getConfig() { return Promise.resolve({}); },
- getIsAdmin() { return Promise.resolve(false); },
- deleteChangeCommitMessage() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- flush(done);
+ test('clicking on date link fires event', () => {
+ element.message = {
+ type: 'REVIEWER_UPDATE',
+ updated: '2016-01-12 20:24:49.448000000',
+ reviewer: {},
+ id: '47c43261_55aa2c41',
+ };
+ flushAsynchronousOperations();
+ const stub = sinon.stub();
+ element.addEventListener('message-anchor-tap', stub);
+ const dateEl = element.shadowRoot
+ .querySelector('.date');
+ assert.ok(dateEl);
+ MockInteractions.tap(dateEl);
+
+ assert.isTrue(stub.called);
+ assert.deepEqual(stub.lastCall.args[0].detail, {id: element.message.id});
+ });
+
+ suite('compute messages', () => {
+ test('empty', () => {
+ assert.equal(element._computeMessageContent('', '', true), '');
+ assert.equal(element._computeMessageContent('', '', false), '');
});
- test('reply and delete button should be hidden', () => {
- element.message = {
- id: '47c43261_55aa2c41',
- author: {
- _account_id: 1115495,
- name: 'Andrew Bonventre',
- email: 'andybons@chromium.org',
- },
- date: '2016-01-12 20:24:49.448000000',
- message: 'Uploaded patch set 1.',
- _revision_number: 1,
- };
+ test('new patchset', () => {
+ const original = 'Uploaded patch set 1.';
+ const tag = 'autogenerated:gerrit:newPatchSet';
+ let actual = element._computeMessageContent(original, tag, true);
+ assert.equal(actual, original);
+ actual = element._computeMessageContent(original, tag, false);
+ assert.equal(actual, original);
+ });
- flushAsynchronousOperations();
- assert.isTrue(
- element.shadowRoot.querySelector('.replyActionContainer').hidden
- );
- assert.isTrue(
- element.shadowRoot.querySelector('.deleteBtn').hidden
- );
+ test('new patchset rebased', () => {
+ const original = 'Patch Set 27: Patch Set 26 was rebased';
+ const tag = 'autogenerated:gerrit:newPatchSet';
+ const expected = 'Patch Set 26 was rebased';
+ let actual = element._computeMessageContent(original, tag, true);
+ assert.equal(actual, expected);
+ actual = element._computeMessageContent(original, tag, false);
+ assert.equal(actual, expected);
+ });
+
+ test('ready for review', () => {
+ const original = 'Patch Set 1:\n\nThis change is ready for review.';
+ const tag = undefined;
+ const expected = 'This change is ready for review.';
+ let actual = element._computeMessageContent(original, tag, true);
+ assert.equal(actual, expected);
+ actual = element._computeMessageContent(original, tag, false);
+ assert.equal(actual, expected);
+ });
+
+ test('vote', () => {
+ const original = 'Patch Set 1: Code-Style+1';
+ const tag = undefined;
+ const expected = '';
+ let actual = element._computeMessageContent(original, tag, true);
+ assert.equal(actual, expected);
+ actual = element._computeMessageContent(original, tag, false);
+ assert.equal(actual, expected);
+ });
+
+ test('comments', () => {
+ const original = 'Patch Set 1:\n\n(3 comments)';
+ const tag = undefined;
+ const expected = '';
+ let actual = element._computeMessageContent(original, tag, true);
+ assert.equal(actual, expected);
+ actual = element._computeMessageContent(original, tag, false);
+ assert.equal(actual, expected);
});
});
- suite('when logged in but not admin', () => {
- setup(done => {
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- getConfig() { return Promise.resolve({}); },
- getIsAdmin() { return Promise.resolve(false); },
- deleteChangeCommitMessage() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- flush(done);
- });
+ test('votes', () => {
+ element.message = {
+ author: {},
+ expanded: false,
+ message: 'Patch Set 1: Verified+1 Code-Review-2 Trybot-Label3+1 Blub+1',
+ };
+ element.labelExtremes = {
+ 'Verified': {max: 1, min: -1},
+ 'Code-Review': {max: 2, min: -2},
+ 'Trybot-Label3': {max: 3, min: 0},
+ };
+ flushAsynchronousOperations();
+ const scoreChips = dom(element.root).querySelectorAll('.score');
+ assert.equal(scoreChips.length, 3);
- test('can see reply but not delete button', () => {
- element.message = {
- id: '47c43261_55aa2c41',
- author: {
- _account_id: 1115495,
- name: 'Andrew Bonventre',
- email: 'andybons@chromium.org',
- },
- date: '2016-01-12 20:24:49.448000000',
- message: 'Uploaded patch set 1.',
- _revision_number: 1,
- };
+ assert.isTrue(scoreChips[0].classList.contains('positive'));
+ assert.isTrue(scoreChips[0].classList.contains('max'));
- flushAsynchronousOperations();
- assert.isFalse(
- element.shadowRoot.querySelector('.replyActionContainer').hidden
- );
- assert.isTrue(
- element.shadowRoot.querySelector('.deleteBtn').hidden
- );
- });
+ assert.isTrue(scoreChips[1].classList.contains('negative'));
+ assert.isTrue(scoreChips[1].classList.contains('min'));
+
+ assert.isTrue(scoreChips[2].classList.contains('positive'));
+ assert.isFalse(scoreChips[2].classList.contains('min'));
+ });
+
+ test('removed votes', () => {
+ element.message = {
+ author: {},
+ expanded: false,
+ message: 'Patch Set 1: Verified+1 -Code-Review -Commit-Queue',
+ };
+ element.labelExtremes = {
+ 'Verified': {max: 1, min: -1},
+ 'Code-Review': {max: 2, min: -2},
+ 'Commit-Queue': {max: 3, min: 0},
+ };
+ flushAsynchronousOperations();
+ const scoreChips = dom(element.root).querySelectorAll('.score');
+ assert.equal(scoreChips.length, 3);
+
+ assert.isTrue(scoreChips[1].classList.contains('removed'));
+ assert.isTrue(scoreChips[2].classList.contains('removed'));
+ });
+
+ test('false negative vote', () => {
+ element.message = {
+ author: {},
+ expanded: false,
+ message: 'Patch Set 1: Cherry Picked from branch stable-2.14.',
+ };
+ element.labelExtremes = {};
+ const scoreChips = dom(element.root).querySelectorAll('.score');
+ assert.equal(scoreChips.length, 0);
});
});
+
+ suite('when not logged in', () => {
+ setup(done => {
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(false); },
+ getPreferences() { return Promise.resolve({}); },
+ getConfig() { return Promise.resolve({}); },
+ getIsAdmin() { return Promise.resolve(false); },
+ deleteChangeCommitMessage() { return Promise.resolve({}); },
+ });
+ element = fixture('basic');
+ flush(done);
+ });
+
+ test('reply and delete button should be hidden', () => {
+ element.message = {
+ id: '47c43261_55aa2c41',
+ author: {
+ _account_id: 1115495,
+ name: 'Andrew Bonventre',
+ email: 'andybons@chromium.org',
+ },
+ date: '2016-01-12 20:24:49.448000000',
+ message: 'Uploaded patch set 1.',
+ _revision_number: 1,
+ };
+
+ flushAsynchronousOperations();
+ assert.isTrue(
+ element.shadowRoot.querySelector('.replyActionContainer').hidden
+ );
+ assert.isTrue(
+ element.shadowRoot.querySelector('.deleteBtn').hidden
+ );
+ });
+ });
+
+ suite('when logged in but not admin', () => {
+ setup(done => {
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
+ getConfig() { return Promise.resolve({}); },
+ getIsAdmin() { return Promise.resolve(false); },
+ deleteChangeCommitMessage() { return Promise.resolve({}); },
+ });
+ element = fixture('basic');
+ flush(done);
+ });
+
+ test('can see reply but not delete button', () => {
+ element.message = {
+ id: '47c43261_55aa2c41',
+ author: {
+ _account_id: 1115495,
+ name: 'Andrew Bonventre',
+ email: 'andybons@chromium.org',
+ },
+ date: '2016-01-12 20:24:49.448000000',
+ message: 'Uploaded patch set 1.',
+ _revision_number: 1,
+ };
+
+ flushAsynchronousOperations();
+ assert.isFalse(
+ element.shadowRoot.querySelector('.replyActionContainer').hidden
+ );
+ assert.isTrue(
+ element.shadowRoot.querySelector('.deleteBtn').hidden
+ );
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
deleted file mode 100644
index 60ec6b0..0000000
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
+++ /dev/null
@@ -1,129 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../gr-message/gr-message.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-messages-list">
- <template>
- <style include="shared-styles">
- :host,
- .messageListControls {
- display: flex;
- justify-content: space-between;
- }
- .header {
- align-items: center;
- border-top: 1px solid var(--border-color);
- border-bottom: 1px solid var(--border-color);
- display: flex;
- justify-content: space-between;
- padding: var(--spacing-s) var(--spacing-l);
- }
- #messageControlsContainer {
- padding: 0 var(--spacing-l);
- }
- .highlighted {
- animation: 3s fadeOut;
- }
- @keyframes fadeOut {
- 0% { background-color: var(--emphasis-color); }
- 100% { background-color: var(--view-background-color); }
- }
- #messageControlsContainer {
- align-items: center;
- background-color: var(--background-color-secondary);
- border-bottom: 1px solid var(--border-color);
- display: flex;
- height: 2.25em;
- justify-content: center;
- }
- #messageControlsContainer gr-button {
- padding: var(--spacing-s) 0;
- }
- .container {
- align-items: center;
- display: flex;
- }
- gr-message:not(:last-of-type) {
- border-bottom: 1px solid var(--border-color);
- }
- gr-message:nth-child(2n) {
- background-color: var(--background-color-secondary);
- }
- gr-message:nth-child(2n+1) {
- background-color: var(--background-color-tertiary);
- }
- </style>
- <div class="header">
- <span
- id="automatedMessageToggleContainer"
- class="container"
- hidden$="[[!_hasAutomatedMessages(messages)]]">
- <paper-toggle-button
- id="automatedMessageToggle"
- checked="{{_hideAutomated}}"></paper-toggle-button>Only comments
- <span class="transparent separator"></span>
- </span>
- <gr-button
- id="collapse-messages"
- link
- title="[[_expandCollapseTitle]]"
- on-click="_handleExpandCollapseTap">
- [[_computeExpandCollapseMessage(_expanded)]]
- </gr-button>
- </div>
- <span
- id="messageControlsContainer"
- hidden$="[[_computeShowHideTextHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]">
- <gr-button id="oldMessagesBtn" link on-click="_handleShowAllTap">
- [[_computeNumMessagesText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]
- </gr-button>
- <span
- class="container"
- hidden$="[[_computeIncrementHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]">
- <span class="transparent separator"></span>
- <gr-button id="incrementMessagesBtn" link
- on-click="_handleIncrementShownMessages">
- [[_computeIncrementText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]
- </gr-button>
- </span>
- </span>
- <template
- is="dom-repeat"
- items="[[_visibleMessages]]"
- as="message">
- <gr-message
- change-num="[[changeNum]]"
- message="[[message]]"
- comments="[[_computeCommentsForMessage(changeComments, message)]]"
- hide-automated="[[_hideAutomated]]"
- project-name="[[projectName]]"
- show-reply-button="[[showReplyButtons]]"
- on-message-anchor-tap="_handleAnchorClick"
- label-extremes="[[_labelExtremes]]"
- data-message-id$="[[message.id]]"></gr-message>
- </template>
- <gr-reporting id="reporting" category="message-list"></gr-reporting>
- </template>
- <script src="gr-messages-list.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
index 6f74e4b..eaac988 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,416 +14,429 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const MAX_INITIAL_SHOWN_MESSAGES = 20;
- const MESSAGES_INCREMENT = 5;
+import '@polymer/paper-toggle-button/paper-toggle-button.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../shared/gr-button/gr-button.js';
+import '../gr-message/gr-message.js';
+import '../../../styles/shared-styles.js';
+import {flush, dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-messages-list_html.js';
- const ReportingEvent = {
- SHOW_ALL: 'show-all-messages',
- SHOW_MORE: 'show-more-messages',
- };
+const MAX_INITIAL_SHOWN_MESSAGES = 20;
+const MESSAGES_INCREMENT = 5;
- /**
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
- */
- class GrMessagesList extends Polymer.mixinBehaviors( [
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-messages-list'; }
+const ReportingEvent = {
+ SHOW_ALL: 'show-all-messages',
+ SHOW_MORE: 'show-more-messages',
+};
- static get properties() {
- return {
- changeNum: Number,
- messages: {
- type: Array,
- value() { return []; },
- },
- reviewerUpdates: {
- type: Array,
- value() { return []; },
- },
- changeComments: Object,
- projectName: String,
- showReplyButtons: {
- type: Boolean,
- value: false,
- },
- labels: Object,
+/**
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrMessagesList extends mixinBehaviors( [
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _expanded: {
- type: Boolean,
- value: false,
- observer: '_expandedChanged',
- },
+ static get is() { return 'gr-messages-list'; }
- _expandCollapseTitle: {
- type: String,
- },
+ static get properties() {
+ return {
+ changeNum: Number,
+ messages: {
+ type: Array,
+ value() { return []; },
+ },
+ reviewerUpdates: {
+ type: Array,
+ value() { return []; },
+ },
+ changeComments: Object,
+ projectName: String,
+ showReplyButtons: {
+ type: Boolean,
+ value: false,
+ },
+ labels: Object,
- _hideAutomated: {
- type: Boolean,
- value: false,
- },
- /**
- * The messages after processing and including merged reviewer updates.
- */
- _processedMessages: {
- type: Array,
- computed: '_computeItems(messages, reviewerUpdates)',
- observer: '_processedMessagesChanged',
- },
- /**
- * The subset of _processedMessages that is visible to the user.
- */
- _visibleMessages: {
- type: Array,
- value() { return []; },
- },
+ _expanded: {
+ type: Boolean,
+ value: false,
+ observer: '_expandedChanged',
+ },
- _labelExtremes: {
- type: Object,
- computed: '_computeLabelExtremes(labels.*)',
- },
- };
- }
+ _expandCollapseTitle: {
+ type: String,
+ },
- scrollToMessage(messageID) {
- let el = this.shadowRoot
- .querySelector('[data-message-id="' + messageID + '"]');
- // If the message is hidden, expand the hidden messages back to that
- // point.
- if (!el) {
- let index;
- for (index = 0; index < this._processedMessages.length; index++) {
- if (this._processedMessages[index].id === messageID) {
- break;
- }
- }
- if (index === this._processedMessages.length) { return; }
+ _hideAutomated: {
+ type: Boolean,
+ value: false,
+ },
+ /**
+ * The messages after processing and including merged reviewer updates.
+ */
+ _processedMessages: {
+ type: Array,
+ computed: '_computeItems(messages, reviewerUpdates)',
+ observer: '_processedMessagesChanged',
+ },
+ /**
+ * The subset of _processedMessages that is visible to the user.
+ */
+ _visibleMessages: {
+ type: Array,
+ value() { return []; },
+ },
- const newMessages = this._processedMessages.slice(index,
- -this._visibleMessages.length);
- // Add newMessages to the beginning of _visibleMessages.
- this.splice(...['_visibleMessages', 0, 0].concat(newMessages));
- // Allow the dom-repeat to stamp.
- Polymer.dom.flush();
- el = this.shadowRoot
- .querySelector('[data-message-id="' + messageID + '"]');
- }
+ _labelExtremes: {
+ type: Object,
+ computed: '_computeLabelExtremes(labels.*)',
+ },
+ };
+ }
- el.set('message.expanded', true);
- let top = el.offsetTop;
- for (let offsetParent = el.offsetParent;
- offsetParent;
- offsetParent = offsetParent.offsetParent) {
- top += offsetParent.offsetTop;
- }
- window.scrollTo(0, top);
- this._highlightEl(el);
- }
-
- _isAutomated(message) {
- return !!(message.reviewer ||
- (message.tag && message.tag.startsWith('autogenerated')));
- }
-
- _computeItems(messages, reviewerUpdates) {
- // Polymer 2: check for undefined
- if ([messages, reviewerUpdates].some(arg => arg === undefined)) {
- return [];
- }
-
- messages = messages || [];
- reviewerUpdates = reviewerUpdates || [];
- let mi = 0;
- let ri = 0;
- let result = [];
- let mDate;
- let rDate;
- for (let i = 0; i < messages.length; i++) {
- messages[i]._index = i;
- }
-
- while (mi < messages.length || ri < reviewerUpdates.length) {
- if (mi >= messages.length) {
- result = result.concat(reviewerUpdates.slice(ri));
+ scrollToMessage(messageID) {
+ let el = this.shadowRoot
+ .querySelector('[data-message-id="' + messageID + '"]');
+ // If the message is hidden, expand the hidden messages back to that
+ // point.
+ if (!el) {
+ let index;
+ for (index = 0; index < this._processedMessages.length; index++) {
+ if (this._processedMessages[index].id === messageID) {
break;
}
- if (ri >= reviewerUpdates.length) {
- result = result.concat(messages.slice(mi));
- break;
- }
- mDate = mDate || util.parseDate(messages[mi].date);
- rDate = rDate || util.parseDate(reviewerUpdates[ri].date);
- if (rDate < mDate) {
- result.push(reviewerUpdates[ri++]);
- rDate = null;
- } else {
- result.push(messages[mi++]);
- mDate = null;
- }
}
- return result;
- }
+ if (index === this._processedMessages.length) { return; }
- _expandedChanged(exp) {
- if (this._processedMessages) {
- for (let i = 0; i < this._processedMessages.length; i++) {
- this._processedMessages[i].expanded = exp;
- }
- }
- // _visibleMessages is a subarray of _processedMessages
- // _processedMessages contains all items from _visibleMessages
- // At this point all _visibleMessages.expanded values are set,
- // and notifyPath must be used to notify Polymer about changes.
- if (this._visibleMessages) {
- for (let i = 0; i < this._visibleMessages.length; i++) {
- this.notifyPath(`_visibleMessages.${i}.expanded`);
- }
- }
-
- if (this._expanded) {
- this._expandCollapseTitle = this.createTitle(
- this.Shortcut.COLLAPSE_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
- } else {
- this._expandCollapseTitle = this.createTitle(
- this.Shortcut.EXPAND_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
- }
- }
-
- _highlightEl(el) {
- const highlightedEls =
- Polymer.dom(this.root).querySelectorAll('.highlighted');
- for (const highlighedEl of highlightedEls) {
- highlighedEl.classList.remove('highlighted');
- }
- function handleAnimationEnd() {
- el.removeEventListener('animationend', handleAnimationEnd);
- el.classList.remove('highlighted');
- }
- el.addEventListener('animationend', handleAnimationEnd);
- el.classList.add('highlighted');
- }
-
- /**
- * @param {boolean} expand
- */
- handleExpandCollapse(expand) {
- this._expanded = expand;
- }
-
- _handleExpandCollapseTap(e) {
- e.preventDefault();
- this.handleExpandCollapse(!this._expanded);
- }
-
- _handleAnchorClick(e) {
- this.scrollToMessage(e.detail.id);
- }
-
- _hasAutomatedMessages(messages) {
- if (!messages) { return false; }
- for (const message of messages) {
- if (this._isAutomated(message)) {
- return true;
- }
- }
- return false;
- }
-
- _computeExpandCollapseMessage(expanded) {
- return expanded ? 'Collapse all' : 'Expand all';
- }
-
- /**
- * Computes message author's file comments for change's message.
- * Method uses this.messages to find next message and relies on messages
- * to be sorted by date field descending.
- *
- * @param {!Object} changeComments changeComment object, which includes
- * a method to get all published comments (including robot comments),
- * which returns a Hash of arrays of comments, filename as key.
- * @param {!Object} message
- * @return {!Object} Hash of arrays of comments, filename as key.
- */
- _computeCommentsForMessage(changeComments, message) {
- if ([changeComments, message].some(arg => arg === undefined)) {
- return [];
- }
- const comments = changeComments.getAllPublishedComments();
- if (message._index === undefined || !comments || !this.messages) {
- return [];
- }
- const messages = this.messages || [];
- const index = message._index;
- const authorId = message.author && message.author._account_id;
- const mDate = util.parseDate(message.date).getTime();
- // NB: Messages array has oldest messages first.
- let nextMDate;
- if (index > 0) {
- for (let i = index - 1; i >= 0; i--) {
- if (messages[i] && messages[i].author &&
- messages[i].author._account_id === authorId) {
- nextMDate = util.parseDate(messages[i].date).getTime();
- break;
- }
- }
- }
- const msgComments = {};
- for (const file in comments) {
- if (!comments.hasOwnProperty(file)) { continue; }
- const fileComments = comments[file];
- for (let i = 0; i < fileComments.length; i++) {
- if (fileComments[i].author &&
- fileComments[i].author._account_id !== authorId) {
- continue;
- }
- const cDate = util.parseDate(fileComments[i].updated).getTime();
- if (cDate <= mDate) {
- if (nextMDate && cDate <= nextMDate) {
- continue;
- }
- msgComments[file] = msgComments[file] || [];
- msgComments[file].push(fileComments[i]);
- }
- }
- }
- return msgComments;
- }
-
- /**
- * Returns the number of messages to splice to the beginning of
- * _visibleMessages. This is the minimum of the total number of messages
- * remaining in the list and the number of messages needed to display five
- * more visible messages in the list.
- */
- _getDelta(visibleMessages, messages, hideAutomated) {
- if ([visibleMessages, messages].some(arg => arg === undefined)) {
- return 0;
- }
-
- let delta = MESSAGES_INCREMENT;
- const msgsRemaining = messages.length - visibleMessages.length;
-
- if (hideAutomated) {
- let counter = 0;
- let i;
- for (i = msgsRemaining; i > 0 && counter < MESSAGES_INCREMENT; i--) {
- if (!this._isAutomated(messages[i - 1])) { counter++; }
- }
- delta = msgsRemaining - i;
- }
- return Math.min(msgsRemaining, delta);
- }
-
- /**
- * Gets the number of messages that would be visible, but do not currently
- * exist in _visibleMessages.
- */
- _numRemaining(visibleMessages, messages, hideAutomated) {
- if ([visibleMessages, messages].some(arg => arg === undefined)) {
- return 0;
- }
-
- if (hideAutomated) {
- return this._getHumanMessages(messages).length -
- this._getHumanMessages(visibleMessages).length;
- }
- return messages.length - visibleMessages.length;
- }
-
- _computeIncrementText(visibleMessages, messages, hideAutomated) {
- let delta = this._getDelta(visibleMessages, messages, hideAutomated);
- delta = Math.min(
- this._numRemaining(visibleMessages, messages, hideAutomated), delta);
- return 'Show ' + Math.min(MESSAGES_INCREMENT, delta) + ' more';
- }
-
- _getHumanMessages(messages) {
- return messages.filter(msg => !this._isAutomated(msg));
- }
-
- _computeShowHideTextHidden(visibleMessages, messages,
- hideAutomated) {
- if ([visibleMessages, messages].some(arg => arg === undefined)) {
- return 0;
- }
-
- if (hideAutomated) {
- messages = this._getHumanMessages(messages);
- visibleMessages = this._getHumanMessages(visibleMessages);
- }
- return visibleMessages.length >= messages.length;
- }
-
- _handleShowAllTap() {
- this._visibleMessages = this._processedMessages;
- this.$.reporting.reportInteraction(ReportingEvent.SHOW_ALL);
- }
-
- _handleIncrementShownMessages() {
- const delta = this._getDelta(this._visibleMessages,
- this._processedMessages, this._hideAutomated);
- const len = this._visibleMessages.length;
- const newMessages = this._processedMessages.slice(-(len + delta), -len);
- // Add newMessages to the beginning of _visibleMessages
+ const newMessages = this._processedMessages.slice(index,
+ -this._visibleMessages.length);
+ // Add newMessages to the beginning of _visibleMessages.
this.splice(...['_visibleMessages', 0, 0].concat(newMessages));
- this.$.reporting.reportInteraction(ReportingEvent.SHOW_MORE);
+ // Allow the dom-repeat to stamp.
+ flush();
+ el = this.shadowRoot
+ .querySelector('[data-message-id="' + messageID + '"]');
}
- _processedMessagesChanged(messages) {
- if (messages) {
- this._visibleMessages = messages.slice(-MAX_INITIAL_SHOWN_MESSAGES);
+ el.set('message.expanded', true);
+ let top = el.offsetTop;
+ for (let offsetParent = el.offsetParent;
+ offsetParent;
+ offsetParent = offsetParent.offsetParent) {
+ top += offsetParent.offsetTop;
+ }
+ window.scrollTo(0, top);
+ this._highlightEl(el);
+ }
- if (messages.length === 0) return;
- const tags = messages.map(message => message.tag || message.type ||
- (message.comments ? 'comments' : 'none'));
- const tagsCounted = tags.reduce((acc, val) => {
- acc[val] = (acc[val] || 0) + 1;
- return acc;
- }, {all: messages.length});
- this.$.reporting.reportInteraction('messages-count', tagsCounted);
+ _isAutomated(message) {
+ return !!(message.reviewer ||
+ (message.tag && message.tag.startsWith('autogenerated')));
+ }
+
+ _computeItems(messages, reviewerUpdates) {
+ // Polymer 2: check for undefined
+ if ([messages, reviewerUpdates].some(arg => arg === undefined)) {
+ return [];
+ }
+
+ messages = messages || [];
+ reviewerUpdates = reviewerUpdates || [];
+ let mi = 0;
+ let ri = 0;
+ let result = [];
+ let mDate;
+ let rDate;
+ for (let i = 0; i < messages.length; i++) {
+ messages[i]._index = i;
+ }
+
+ while (mi < messages.length || ri < reviewerUpdates.length) {
+ if (mi >= messages.length) {
+ result = result.concat(reviewerUpdates.slice(ri));
+ break;
+ }
+ if (ri >= reviewerUpdates.length) {
+ result = result.concat(messages.slice(mi));
+ break;
+ }
+ mDate = mDate || util.parseDate(messages[mi].date);
+ rDate = rDate || util.parseDate(reviewerUpdates[ri].date);
+ if (rDate < mDate) {
+ result.push(reviewerUpdates[ri++]);
+ rDate = null;
+ } else {
+ result.push(messages[mi++]);
+ mDate = null;
+ }
+ }
+ return result;
+ }
+
+ _expandedChanged(exp) {
+ if (this._processedMessages) {
+ for (let i = 0; i < this._processedMessages.length; i++) {
+ this._processedMessages[i].expanded = exp;
+ }
+ }
+ // _visibleMessages is a subarray of _processedMessages
+ // _processedMessages contains all items from _visibleMessages
+ // At this point all _visibleMessages.expanded values are set,
+ // and notifyPath must be used to notify Polymer about changes.
+ if (this._visibleMessages) {
+ for (let i = 0; i < this._visibleMessages.length; i++) {
+ this.notifyPath(`_visibleMessages.${i}.expanded`);
}
}
- _computeNumMessagesText(visibleMessages, messages,
- hideAutomated) {
- const total =
- this._numRemaining(visibleMessages, messages, hideAutomated);
- return total === 1 ? 'Show 1 message' : 'Show all ' + total + ' messages';
- }
-
- _computeIncrementHidden(visibleMessages, messages,
- hideAutomated) {
- const total =
- this._numRemaining(visibleMessages, messages, hideAutomated);
- return total <= this._getDelta(visibleMessages, messages, hideAutomated);
- }
-
- /**
- * Compute a mapping from label name to objects representing the minimum and
- * maximum possible values for that label.
- */
- _computeLabelExtremes(labelRecord) {
- const extremes = {};
- const labels = labelRecord.base;
- if (!labels) { return extremes; }
- for (const key of Object.keys(labels)) {
- if (!labels[key] || !labels[key].values) { continue; }
- const values = Object.keys(labels[key].values)
- .map(v => parseInt(v, 10));
- values.sort((a, b) => a - b);
- if (!values.length) { continue; }
- extremes[key] = {min: values[0], max: values[values.length - 1]};
- }
- return extremes;
+ if (this._expanded) {
+ this._expandCollapseTitle = this.createTitle(
+ this.Shortcut.COLLAPSE_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
+ } else {
+ this._expandCollapseTitle = this.createTitle(
+ this.Shortcut.EXPAND_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
}
}
- customElements.define(GrMessagesList.is, GrMessagesList);
-})();
+ _highlightEl(el) {
+ const highlightedEls =
+ dom(this.root).querySelectorAll('.highlighted');
+ for (const highlighedEl of highlightedEls) {
+ highlighedEl.classList.remove('highlighted');
+ }
+ function handleAnimationEnd() {
+ el.removeEventListener('animationend', handleAnimationEnd);
+ el.classList.remove('highlighted');
+ }
+ el.addEventListener('animationend', handleAnimationEnd);
+ el.classList.add('highlighted');
+ }
+
+ /**
+ * @param {boolean} expand
+ */
+ handleExpandCollapse(expand) {
+ this._expanded = expand;
+ }
+
+ _handleExpandCollapseTap(e) {
+ e.preventDefault();
+ this.handleExpandCollapse(!this._expanded);
+ }
+
+ _handleAnchorClick(e) {
+ this.scrollToMessage(e.detail.id);
+ }
+
+ _hasAutomatedMessages(messages) {
+ if (!messages) { return false; }
+ for (const message of messages) {
+ if (this._isAutomated(message)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ _computeExpandCollapseMessage(expanded) {
+ return expanded ? 'Collapse all' : 'Expand all';
+ }
+
+ /**
+ * Computes message author's file comments for change's message.
+ * Method uses this.messages to find next message and relies on messages
+ * to be sorted by date field descending.
+ *
+ * @param {!Object} changeComments changeComment object, which includes
+ * a method to get all published comments (including robot comments),
+ * which returns a Hash of arrays of comments, filename as key.
+ * @param {!Object} message
+ * @return {!Object} Hash of arrays of comments, filename as key.
+ */
+ _computeCommentsForMessage(changeComments, message) {
+ if ([changeComments, message].some(arg => arg === undefined)) {
+ return [];
+ }
+ const comments = changeComments.getAllPublishedComments();
+ if (message._index === undefined || !comments || !this.messages) {
+ return [];
+ }
+ const messages = this.messages || [];
+ const index = message._index;
+ const authorId = message.author && message.author._account_id;
+ const mDate = util.parseDate(message.date).getTime();
+ // NB: Messages array has oldest messages first.
+ let nextMDate;
+ if (index > 0) {
+ for (let i = index - 1; i >= 0; i--) {
+ if (messages[i] && messages[i].author &&
+ messages[i].author._account_id === authorId) {
+ nextMDate = util.parseDate(messages[i].date).getTime();
+ break;
+ }
+ }
+ }
+ const msgComments = {};
+ for (const file in comments) {
+ if (!comments.hasOwnProperty(file)) { continue; }
+ const fileComments = comments[file];
+ for (let i = 0; i < fileComments.length; i++) {
+ if (fileComments[i].author &&
+ fileComments[i].author._account_id !== authorId) {
+ continue;
+ }
+ const cDate = util.parseDate(fileComments[i].updated).getTime();
+ if (cDate <= mDate) {
+ if (nextMDate && cDate <= nextMDate) {
+ continue;
+ }
+ msgComments[file] = msgComments[file] || [];
+ msgComments[file].push(fileComments[i]);
+ }
+ }
+ }
+ return msgComments;
+ }
+
+ /**
+ * Returns the number of messages to splice to the beginning of
+ * _visibleMessages. This is the minimum of the total number of messages
+ * remaining in the list and the number of messages needed to display five
+ * more visible messages in the list.
+ */
+ _getDelta(visibleMessages, messages, hideAutomated) {
+ if ([visibleMessages, messages].some(arg => arg === undefined)) {
+ return 0;
+ }
+
+ let delta = MESSAGES_INCREMENT;
+ const msgsRemaining = messages.length - visibleMessages.length;
+
+ if (hideAutomated) {
+ let counter = 0;
+ let i;
+ for (i = msgsRemaining; i > 0 && counter < MESSAGES_INCREMENT; i--) {
+ if (!this._isAutomated(messages[i - 1])) { counter++; }
+ }
+ delta = msgsRemaining - i;
+ }
+ return Math.min(msgsRemaining, delta);
+ }
+
+ /**
+ * Gets the number of messages that would be visible, but do not currently
+ * exist in _visibleMessages.
+ */
+ _numRemaining(visibleMessages, messages, hideAutomated) {
+ if ([visibleMessages, messages].some(arg => arg === undefined)) {
+ return 0;
+ }
+
+ if (hideAutomated) {
+ return this._getHumanMessages(messages).length -
+ this._getHumanMessages(visibleMessages).length;
+ }
+ return messages.length - visibleMessages.length;
+ }
+
+ _computeIncrementText(visibleMessages, messages, hideAutomated) {
+ let delta = this._getDelta(visibleMessages, messages, hideAutomated);
+ delta = Math.min(
+ this._numRemaining(visibleMessages, messages, hideAutomated), delta);
+ return 'Show ' + Math.min(MESSAGES_INCREMENT, delta) + ' more';
+ }
+
+ _getHumanMessages(messages) {
+ return messages.filter(msg => !this._isAutomated(msg));
+ }
+
+ _computeShowHideTextHidden(visibleMessages, messages,
+ hideAutomated) {
+ if ([visibleMessages, messages].some(arg => arg === undefined)) {
+ return 0;
+ }
+
+ if (hideAutomated) {
+ messages = this._getHumanMessages(messages);
+ visibleMessages = this._getHumanMessages(visibleMessages);
+ }
+ return visibleMessages.length >= messages.length;
+ }
+
+ _handleShowAllTap() {
+ this._visibleMessages = this._processedMessages;
+ this.$.reporting.reportInteraction(ReportingEvent.SHOW_ALL);
+ }
+
+ _handleIncrementShownMessages() {
+ const delta = this._getDelta(this._visibleMessages,
+ this._processedMessages, this._hideAutomated);
+ const len = this._visibleMessages.length;
+ const newMessages = this._processedMessages.slice(-(len + delta), -len);
+ // Add newMessages to the beginning of _visibleMessages
+ this.splice(...['_visibleMessages', 0, 0].concat(newMessages));
+ this.$.reporting.reportInteraction(ReportingEvent.SHOW_MORE);
+ }
+
+ _processedMessagesChanged(messages) {
+ if (messages) {
+ this._visibleMessages = messages.slice(-MAX_INITIAL_SHOWN_MESSAGES);
+
+ if (messages.length === 0) return;
+ const tags = messages.map(message => message.tag || message.type ||
+ (message.comments ? 'comments' : 'none'));
+ const tagsCounted = tags.reduce((acc, val) => {
+ acc[val] = (acc[val] || 0) + 1;
+ return acc;
+ }, {all: messages.length});
+ this.$.reporting.reportInteraction('messages-count', tagsCounted);
+ }
+ }
+
+ _computeNumMessagesText(visibleMessages, messages,
+ hideAutomated) {
+ const total =
+ this._numRemaining(visibleMessages, messages, hideAutomated);
+ return total === 1 ? 'Show 1 message' : 'Show all ' + total + ' messages';
+ }
+
+ _computeIncrementHidden(visibleMessages, messages,
+ hideAutomated) {
+ const total =
+ this._numRemaining(visibleMessages, messages, hideAutomated);
+ return total <= this._getDelta(visibleMessages, messages, hideAutomated);
+ }
+
+ /**
+ * Compute a mapping from label name to objects representing the minimum and
+ * maximum possible values for that label.
+ */
+ _computeLabelExtremes(labelRecord) {
+ const extremes = {};
+ const labels = labelRecord.base;
+ if (!labels) { return extremes; }
+ for (const key of Object.keys(labels)) {
+ if (!labels[key] || !labels[key].values) { continue; }
+ const values = Object.keys(labels[key].values)
+ .map(v => parseInt(v, 10));
+ values.sort((a, b) => a - b);
+ if (!values.length) { continue; }
+ extremes[key] = {min: values[0], max: values[values.length - 1]};
+ }
+ return extremes;
+ }
+}
+
+customElements.define(GrMessagesList.is, GrMessagesList);
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
new file mode 100644
index 0000000..3e9f7b5
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
@@ -0,0 +1,93 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host,
+ .messageListControls {
+ display: flex;
+ justify-content: space-between;
+ }
+ .header {
+ align-items: center;
+ border-top: 1px solid var(--border-color);
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ padding: var(--spacing-s) var(--spacing-l);
+ }
+ #messageControlsContainer {
+ padding: 0 var(--spacing-l);
+ }
+ .highlighted {
+ animation: 3s fadeOut;
+ }
+ @keyframes fadeOut {
+ 0% { background-color: var(--emphasis-color); }
+ 100% { background-color: var(--view-background-color); }
+ }
+ #messageControlsContainer {
+ align-items: center;
+ background-color: var(--background-color-secondary);
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ height: 2.25em;
+ justify-content: center;
+ }
+ #messageControlsContainer gr-button {
+ padding: var(--spacing-s) 0;
+ }
+ .container {
+ align-items: center;
+ display: flex;
+ }
+ gr-message:not(:last-of-type) {
+ border-bottom: 1px solid var(--border-color);
+ }
+ gr-message:nth-child(2n) {
+ background-color: var(--background-color-secondary);
+ }
+ gr-message:nth-child(2n+1) {
+ background-color: var(--background-color-tertiary);
+ }
+ </style>
+ <div class="header">
+ <span id="automatedMessageToggleContainer" class="container" hidden\$="[[!_hasAutomatedMessages(messages)]]">
+ <paper-toggle-button id="automatedMessageToggle" checked="{{_hideAutomated}}"></paper-toggle-button>Only comments
+ <span class="transparent separator"></span>
+ </span>
+ <gr-button id="collapse-messages" link="" title="[[_expandCollapseTitle]]" on-click="_handleExpandCollapseTap">
+ [[_computeExpandCollapseMessage(_expanded)]]
+ </gr-button>
+ </div>
+ <span id="messageControlsContainer" hidden\$="[[_computeShowHideTextHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]">
+ <gr-button id="oldMessagesBtn" link="" on-click="_handleShowAllTap">
+ [[_computeNumMessagesText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]
+ </gr-button>
+ <span class="container" hidden\$="[[_computeIncrementHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]">
+ <span class="transparent separator"></span>
+ <gr-button id="incrementMessagesBtn" link="" on-click="_handleIncrementShownMessages">
+ [[_computeIncrementText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]
+ </gr-button>
+ </span>
+ </span>
+ <template is="dom-repeat" items="[[_visibleMessages]]" as="message">
+ <gr-message change-num="[[changeNum]]" message="[[message]]" comments="[[_computeCommentsForMessage(changeComments, message)]]" hide-automated="[[_hideAutomated]]" project-name="[[projectName]]" show-reply-button="[[showReplyButtons]]" on-message-anchor-tap="_handleAnchorClick" label-extremes="[[_labelExtremes]]" data-message-id\$="[[message.id]]"></gr-message>
+ </template>
+ <gr-reporting id="reporting" category="message-list"></gr-reporting>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index 2ee2a81..a5cc939 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -19,17 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-messages-list</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
-
-<link rel="import" href="gr-messages-list.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<dom-module id="comment-api-mock">
<template>
@@ -38,8 +31,7 @@
change-comments="[[_changeComments]]"></gr-messages-list>
<gr-comment-api id="commentAPI"></gr-comment-api>
</template>
- <script src="../../diff/gr-comment-api/gr-comment-api-mock_test.js"></script>
-</dom-module>
+ </dom-module>
<test-fixture id="basic">
<template>
@@ -49,574 +41,578 @@
</template>
</test-fixture>
-<script>
- const randomMessage = function(opt_params) {
- const params = opt_params || {};
- const author1 = {
- _account_id: 1115495,
- name: 'Andrew Bonventre',
- email: 'andybons@chromium.org',
- };
- return {
- id: params.id || Math.random().toString(),
- date: params.date || '2016-01-12 20:28:33.038000',
- message: params.message || Math.random().toString(),
- _revision_number: params._revision_number || 1,
- author: params.author || author1,
- };
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../diff/gr-comment-api/gr-comment-api.js';
+import './gr-messages-list.js';
+import '../../diff/gr-comment-api/gr-comment-api-mock_test.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+const randomMessage = function(opt_params) {
+ const params = opt_params || {};
+ const author1 = {
+ _account_id: 1115495,
+ name: 'Andrew Bonventre',
+ email: 'andybons@chromium.org',
+ };
+ return {
+ id: params.id || Math.random().toString(),
+ date: params.date || '2016-01-12 20:28:33.038000',
+ message: params.message || Math.random().toString(),
+ _revision_number: params._revision_number || 1,
+ author: params.author || author1,
+ };
+};
+
+const randomAutomated = function(opt_params) {
+ return Object.assign({tag: 'autogenerated:gerrit:replace'},
+ randomMessage(opt_params));
+};
+
+suite('gr-messages-list tests', () => {
+ let element;
+ let messages;
+ let sandbox;
+ let commentApiWrapper;
+
+ const getMessages = function() {
+ return dom(element.root).querySelectorAll('gr-message');
};
- const randomAutomated = function(opt_params) {
- return Object.assign({tag: 'autogenerated:gerrit:replace'},
- randomMessage(opt_params));
+ const author = {
+ _account_id: 42,
+ name: 'Marvin the Paranoid Android',
+ email: 'marvin@sirius.org',
};
- suite('gr-messages-list tests', async () => {
- await readyToTest();
+ const comments = {
+ file1: [
+ {
+ message: 'message text',
+ updated: '2016-09-27 00:18:03.000000000',
+ in_reply_to: '6505d749_f0bec0aa',
+ line: 62,
+ id: '6505d749_10ed44b2',
+ patch_set: 2,
+ author: {
+ email: 'some@email.com',
+ _account_id: 123,
+ },
+ },
+ {
+ message: 'message text',
+ updated: '2016-09-27 00:18:03.000000000',
+ in_reply_to: 'c5912363_6b820105',
+ line: 42,
+ id: '450a935e_0f1c05db',
+ patch_set: 2,
+ author,
+ },
+ {
+ message: 'message text',
+ updated: '2016-09-27 00:18:03.000000000',
+ in_reply_to: '6505d749_f0bec0aa',
+ line: 62,
+ id: '6505d749_10ed44b2',
+ patch_set: 2,
+ author,
+ },
+ ],
+ file2: [
+ {
+ message: 'message text',
+ updated: '2016-09-27 00:18:03.000000000',
+ in_reply_to: 'c5912363_4b7d450a',
+ line: 132,
+ id: '450a935e_4f260d25',
+ patch_set: 2,
+ author,
+ },
+ ],
+ };
+
+ suite('basic tests', () => {
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getLoggedIn() { return Promise.resolve(false); },
+ getDiffComments() { return Promise.resolve(comments); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ });
+ sandbox = sinon.sandbox.create();
+ messages = _.times(3, randomMessage);
+ // Element must be wrapped in an element with direct access to the
+ // comment API.
+ commentApiWrapper = fixture('basic');
+ element = commentApiWrapper.$.messagesList;
+ element.messages = messages;
+
+ // Stub methods on the changeComments object after changeComments has
+ // been initialized.
+ return commentApiWrapper.loadComments();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('show some old messages', () => {
+ assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
+ element.messages = _.times(26, randomMessage);
+ flushAsynchronousOperations();
+
+ assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
+ assert.equal(getMessages().length, 20);
+ assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase()
+ .trim(), 'SHOW 5 MORE');
+ MockInteractions.tap(element.$.incrementMessagesBtn);
+ flushAsynchronousOperations();
+
+ assert.equal(getMessages().length, 25);
+ assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase()
+ .trim(), 'SHOW 1 MORE');
+ MockInteractions.tap(element.$.incrementMessagesBtn);
+ flushAsynchronousOperations();
+
+ assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
+ assert.equal(getMessages().length, 26);
+ });
+
+ test('show all old messages', () => {
+ assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
+ element.messages = _.times(26, randomMessage);
+ flushAsynchronousOperations();
+
+ assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
+ assert.equal(getMessages().length, 20);
+ assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
+ 'SHOW ALL 6 MESSAGES');
+ MockInteractions.tap(element.$.oldMessagesBtn);
+ flushAsynchronousOperations();
+
+ assert.equal(getMessages().length, 26);
+ assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
+ });
+
+ test('message count respects automated', () => {
+ element.messages = _.times(10, randomAutomated)
+ .concat(_.times(11, randomMessage));
+ flushAsynchronousOperations();
+
+ assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
+ 'SHOW 1 MESSAGE');
+ assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
+ MockInteractions.tap(element.$.automatedMessageToggle);
+ flushAsynchronousOperations();
+
+ assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
+ });
+
+ test('message count still respects non-automated on toggle', () => {
+ element.messages = _.times(10, randomMessage)
+ .concat(_.times(11, randomAutomated));
+ flushAsynchronousOperations();
+
+ assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
+ 'SHOW 1 MESSAGE');
+ assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
+ MockInteractions.tap(element.$.automatedMessageToggle);
+ flushAsynchronousOperations();
+
+ assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
+ 'SHOW 1 MESSAGE');
+ assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
+ });
+
+ test('show all messages respects expand', () => {
+ element.messages = _.times(10, randomAutomated)
+ .concat(_.times(11, randomMessage));
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('#collapse-messages')); // Expand all.
+ flushAsynchronousOperations();
+
+ let messages = getMessages();
+ assert.equal(messages.length, 20);
+ for (const message of messages) {
+ assert.isTrue(message._expanded);
+ }
+
+ MockInteractions.tap(element.$.oldMessagesBtn);
+ flushAsynchronousOperations();
+
+ messages = getMessages();
+ assert.equal(messages.length, 21);
+ for (const message of messages) {
+ assert.isTrue(message._expanded);
+ }
+ });
+
+ test('show all messages respects collapse', () => {
+ element.messages = _.times(10, randomAutomated)
+ .concat(_.times(11, randomMessage));
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('#collapse-messages')); // Expand all.
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('#collapse-messages')); // Collapse all.
+ flushAsynchronousOperations();
+
+ let messages = getMessages();
+ assert.equal(messages.length, 20);
+ for (const message of messages) {
+ assert.isFalse(message._expanded);
+ }
+
+ MockInteractions.tap(element.$.oldMessagesBtn);
+ flushAsynchronousOperations();
+
+ messages = getMessages();
+ assert.equal(messages.length, 21);
+ for (const message of messages) {
+ assert.isFalse(message._expanded);
+ }
+ });
+
+ test('expand/collapse all', () => {
+ let allMessageEls = getMessages();
+ for (const message of allMessageEls) {
+ message._expanded = false;
+ }
+ MockInteractions.tap(allMessageEls[1]);
+ assert.isTrue(allMessageEls[1]._expanded);
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('#collapse-messages'));
+ allMessageEls = getMessages();
+ for (const message of allMessageEls) {
+ assert.isTrue(message._expanded);
+ }
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('#collapse-messages'));
+ allMessageEls = getMessages();
+ for (const message of allMessageEls) {
+ assert.isFalse(message._expanded);
+ }
+ });
+
+ test('expand/collapse from external keypress', () => {
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('#collapse-messages'));
+ let allMessageEls = getMessages();
+ for (const message of allMessageEls) {
+ assert.isTrue(message._expanded);
+ }
+
+ // Expand/collapse all text also changes.
+ assert.equal(element.shadowRoot
+ .querySelector('#collapse-messages').textContent.trim(),
+ 'Collapse all');
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('#collapse-messages'));
+ allMessageEls = getMessages();
+ for (const message of allMessageEls) {
+ assert.isFalse(message._expanded);
+ }
+ // Expand/collapse all text also changes.
+ assert.equal(element.shadowRoot
+ .querySelector('#collapse-messages').textContent.trim(),
+ 'Expand all');
+ });
+
+ test('hide messages does not appear when no automated messages', () => {
+ assert.isOk(element.shadowRoot
+ .querySelector('#automatedMessageToggleContainer[hidden]'));
+ });
+
+ test('scroll to message', () => {
+ const allMessageEls = getMessages();
+ for (const message of allMessageEls) {
+ message.set('message.expanded', false);
+ }
+
+ const scrollToStub = sandbox.stub(window, 'scrollTo');
+ const highlightStub = sandbox.stub(element, '_highlightEl');
+
+ element.scrollToMessage('invalid');
+
+ for (const message of allMessageEls) {
+ assert.isFalse(message._expanded,
+ 'expected gr-message to not be expanded');
+ }
+
+ const messageID = messages[1].id;
+ element.scrollToMessage(messageID);
+ assert.isTrue(
+ element.shadowRoot
+ .querySelector('[data-message-id="' + messageID + '"]')
+ ._expanded);
+
+ assert.isTrue(scrollToStub.calledOnce);
+ assert.isTrue(highlightStub.calledOnce);
+ });
+
+ test('scroll to message offscreen', () => {
+ const scrollToStub = sandbox.stub(window, 'scrollTo');
+ const highlightStub = sandbox.stub(element, '_highlightEl');
+ element.messages = _.times(25, randomMessage);
+ flushAsynchronousOperations();
+ assert.isFalse(scrollToStub.called);
+ assert.isFalse(highlightStub.called);
+
+ const messageID = element.messages[1].id;
+ element.scrollToMessage(messageID);
+ assert.isTrue(scrollToStub.calledOnce);
+ assert.isTrue(highlightStub.calledOnce);
+ assert.equal(element._visibleMessages.length, 24);
+ assert.isTrue(
+ element.shadowRoot
+ .querySelector('[data-message-id="' + messageID + '"]')
+ ._expanded);
+ });
+
+ test('messages', () => {
+ const messages = [].concat(
+ randomMessage(),
+ {
+ _index: 5,
+ _revision_number: 4,
+ message: 'Uploaded patch set 4.',
+ date: '2016-09-28 13:36:33.000000000',
+ author,
+ id: '8c19ccc949c6d482b061be6a28e10782abf0e7af',
+ },
+ {
+ _index: 6,
+ _revision_number: 4,
+ message: 'Patch Set 4:\n\n(6 comments)',
+ date: '2016-09-28 13:36:33.000000000',
+ author,
+ id: 'e7bfdbc842f6b6d8064bc68e0f52b673f40c0ca5',
+ }
+ );
+ element.messages = messages;
+ const isAuthor = function(author, message) {
+ return message.author._account_id === author._account_id;
+ };
+ const isMarvin = isAuthor.bind(null, author);
+ flushAsynchronousOperations();
+ const messageElements = getMessages();
+ assert.equal(messageElements.length, messages.length);
+ assert.deepEqual(messageElements[1].message, messages[1]);
+ assert.deepEqual(messageElements[2].message, messages[2]);
+ assert.deepEqual(messageElements[1].comments.file1,
+ comments.file1.filter(isMarvin));
+ assert.deepEqual(messageElements[1].comments.file2,
+ comments.file2.filter(isMarvin));
+ assert.deepEqual(messageElements[2].comments, {});
+ });
+
+ test('messages without author do not throw', () => {
+ const messages = [{
+ _index: 5,
+ _revision_number: 4,
+ message: 'Uploaded patch set 4.',
+ date: '2016-09-28 13:36:33.000000000',
+ id: '8c19ccc949c6d482b061be6a28e10782abf0e7af',
+ }];
+ element.messages = messages;
+ flushAsynchronousOperations();
+ const messageEls = getMessages();
+ assert.equal(messageEls.length, 1);
+ assert.equal(messageEls[0].message.message, messages[0].message);
+ });
+
+ test('hide increment text if increment >= total remaining', () => {
+ // Test with stubbed return values, as _numRemaining and _getDelta have
+ // their own tests.
+ sandbox.stub(element, '_getDelta').returns(5);
+ const remainingStub = sandbox.stub(element, '_numRemaining').returns(6);
+ assert.isFalse(element._computeIncrementHidden(null, null, null));
+ remainingStub.restore();
+
+ sandbox.stub(element, '_numRemaining').returns(4);
+ assert.isTrue(element._computeIncrementHidden(null, null, null));
+ });
+ });
+
+ suite('gr-messages-list automate tests', () => {
let element;
let messages;
let sandbox;
let commentApiWrapper;
const getMessages = function() {
- return Polymer.dom(element.root).querySelectorAll('gr-message');
+ return dom(element.root).querySelectorAll('gr-message');
+ };
+ const getHiddenMessages = function() {
+ return dom(element.root).querySelectorAll('gr-message[hidden]');
};
- const author = {
- _account_id: 42,
- name: 'Marvin the Paranoid Android',
- email: 'marvin@sirius.org',
+ const randomMessageReviewer = {
+ reviewer: {},
+ date: '2016-01-13 20:30:33.038000',
};
- const comments = {
- file1: [
- {
- message: 'message text',
- updated: '2016-09-27 00:18:03.000000000',
- in_reply_to: '6505d749_f0bec0aa',
- line: 62,
- id: '6505d749_10ed44b2',
- patch_set: 2,
- author: {
- email: 'some@email.com',
- _account_id: 123,
- },
- },
- {
- message: 'message text',
- updated: '2016-09-27 00:18:03.000000000',
- in_reply_to: 'c5912363_6b820105',
- line: 42,
- id: '450a935e_0f1c05db',
- patch_set: 2,
- author,
- },
- {
- message: 'message text',
- updated: '2016-09-27 00:18:03.000000000',
- in_reply_to: '6505d749_f0bec0aa',
- line: 62,
- id: '6505d749_10ed44b2',
- patch_set: 2,
- author,
- },
- ],
- file2: [
- {
- message: 'message text',
- updated: '2016-09-27 00:18:03.000000000',
- in_reply_to: 'c5912363_4b7d450a',
- line: 132,
- id: '450a935e_4f260d25',
- patch_set: 2,
- author,
- },
- ],
- };
-
- suite('basic tests', () => {
- setup(() => {
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getLoggedIn() { return Promise.resolve(false); },
- getDiffComments() { return Promise.resolve(comments); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
- });
- sandbox = sinon.sandbox.create();
- messages = _.times(3, randomMessage);
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = fixture('basic');
- element = commentApiWrapper.$.messagesList;
- element.messages = messages;
-
- // Stub methods on the changeComments object after changeComments has
- // been initialized.
- return commentApiWrapper.loadComments();
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getLoggedIn() { return Promise.resolve(false); },
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
});
- teardown(() => {
- sandbox.restore();
- });
+ sandbox = sinon.sandbox.create();
+ messages = _.times(2, randomAutomated);
+ messages.push(randomMessageReviewer);
- test('show some old messages', () => {
- assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
- element.messages = _.times(26, randomMessage);
- flushAsynchronousOperations();
+ // Element must be wrapped in an element with direct access to the
+ // comment API.
+ commentApiWrapper = fixture('basic');
+ element = commentApiWrapper.$.messagesList;
+ sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
+ element.messages = messages;
- assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
- assert.equal(getMessages().length, 20);
- assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase()
- .trim(), 'SHOW 5 MORE');
- MockInteractions.tap(element.$.incrementMessagesBtn);
- flushAsynchronousOperations();
-
- assert.equal(getMessages().length, 25);
- assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase()
- .trim(), 'SHOW 1 MORE');
- MockInteractions.tap(element.$.incrementMessagesBtn);
- flushAsynchronousOperations();
-
- assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
- assert.equal(getMessages().length, 26);
- });
-
- test('show all old messages', () => {
- assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
- element.messages = _.times(26, randomMessage);
- flushAsynchronousOperations();
-
- assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
- assert.equal(getMessages().length, 20);
- assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
- 'SHOW ALL 6 MESSAGES');
- MockInteractions.tap(element.$.oldMessagesBtn);
- flushAsynchronousOperations();
-
- assert.equal(getMessages().length, 26);
- assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
- });
-
- test('message count respects automated', () => {
- element.messages = _.times(10, randomAutomated)
- .concat(_.times(11, randomMessage));
- flushAsynchronousOperations();
-
- assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
- 'SHOW 1 MESSAGE');
- assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
- MockInteractions.tap(element.$.automatedMessageToggle);
- flushAsynchronousOperations();
-
- assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
- });
-
- test('message count still respects non-automated on toggle', () => {
- element.messages = _.times(10, randomMessage)
- .concat(_.times(11, randomAutomated));
- flushAsynchronousOperations();
-
- assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
- 'SHOW 1 MESSAGE');
- assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
- MockInteractions.tap(element.$.automatedMessageToggle);
- flushAsynchronousOperations();
-
- assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
- 'SHOW 1 MESSAGE');
- assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
- });
-
- test('show all messages respects expand', () => {
- element.messages = _.times(10, randomAutomated)
- .concat(_.times(11, randomMessage));
- flushAsynchronousOperations();
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('#collapse-messages')); // Expand all.
- flushAsynchronousOperations();
-
- let messages = getMessages();
- assert.equal(messages.length, 20);
- for (const message of messages) {
- assert.isTrue(message._expanded);
- }
-
- MockInteractions.tap(element.$.oldMessagesBtn);
- flushAsynchronousOperations();
-
- messages = getMessages();
- assert.equal(messages.length, 21);
- for (const message of messages) {
- assert.isTrue(message._expanded);
- }
- });
-
- test('show all messages respects collapse', () => {
- element.messages = _.times(10, randomAutomated)
- .concat(_.times(11, randomMessage));
- flushAsynchronousOperations();
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('#collapse-messages')); // Expand all.
- MockInteractions.tap(element.shadowRoot
- .querySelector('#collapse-messages')); // Collapse all.
- flushAsynchronousOperations();
-
- let messages = getMessages();
- assert.equal(messages.length, 20);
- for (const message of messages) {
- assert.isFalse(message._expanded);
- }
-
- MockInteractions.tap(element.$.oldMessagesBtn);
- flushAsynchronousOperations();
-
- messages = getMessages();
- assert.equal(messages.length, 21);
- for (const message of messages) {
- assert.isFalse(message._expanded);
- }
- });
-
- test('expand/collapse all', () => {
- let allMessageEls = getMessages();
- for (const message of allMessageEls) {
- message._expanded = false;
- }
- MockInteractions.tap(allMessageEls[1]);
- assert.isTrue(allMessageEls[1]._expanded);
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('#collapse-messages'));
- allMessageEls = getMessages();
- for (const message of allMessageEls) {
- assert.isTrue(message._expanded);
- }
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('#collapse-messages'));
- allMessageEls = getMessages();
- for (const message of allMessageEls) {
- assert.isFalse(message._expanded);
- }
- });
-
- test('expand/collapse from external keypress', () => {
- MockInteractions.tap(element.shadowRoot
- .querySelector('#collapse-messages'));
- let allMessageEls = getMessages();
- for (const message of allMessageEls) {
- assert.isTrue(message._expanded);
- }
-
- // Expand/collapse all text also changes.
- assert.equal(element.shadowRoot
- .querySelector('#collapse-messages').textContent.trim(),
- 'Collapse all');
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('#collapse-messages'));
- allMessageEls = getMessages();
- for (const message of allMessageEls) {
- assert.isFalse(message._expanded);
- }
- // Expand/collapse all text also changes.
- assert.equal(element.shadowRoot
- .querySelector('#collapse-messages').textContent.trim(),
- 'Expand all');
- });
-
- test('hide messages does not appear when no automated messages', () => {
- assert.isOk(element.shadowRoot
- .querySelector('#automatedMessageToggleContainer[hidden]'));
- });
-
- test('scroll to message', () => {
- const allMessageEls = getMessages();
- for (const message of allMessageEls) {
- message.set('message.expanded', false);
- }
-
- const scrollToStub = sandbox.stub(window, 'scrollTo');
- const highlightStub = sandbox.stub(element, '_highlightEl');
-
- element.scrollToMessage('invalid');
-
- for (const message of allMessageEls) {
- assert.isFalse(message._expanded,
- 'expected gr-message to not be expanded');
- }
-
- const messageID = messages[1].id;
- element.scrollToMessage(messageID);
- assert.isTrue(
- element.shadowRoot
- .querySelector('[data-message-id="' + messageID + '"]')
- ._expanded);
-
- assert.isTrue(scrollToStub.calledOnce);
- assert.isTrue(highlightStub.calledOnce);
- });
-
- test('scroll to message offscreen', () => {
- const scrollToStub = sandbox.stub(window, 'scrollTo');
- const highlightStub = sandbox.stub(element, '_highlightEl');
- element.messages = _.times(25, randomMessage);
- flushAsynchronousOperations();
- assert.isFalse(scrollToStub.called);
- assert.isFalse(highlightStub.called);
-
- const messageID = element.messages[1].id;
- element.scrollToMessage(messageID);
- assert.isTrue(scrollToStub.calledOnce);
- assert.isTrue(highlightStub.calledOnce);
- assert.equal(element._visibleMessages.length, 24);
- assert.isTrue(
- element.shadowRoot
- .querySelector('[data-message-id="' + messageID + '"]')
- ._expanded);
- });
-
- test('messages', () => {
- const messages = [].concat(
- randomMessage(),
- {
- _index: 5,
- _revision_number: 4,
- message: 'Uploaded patch set 4.',
- date: '2016-09-28 13:36:33.000000000',
- author,
- id: '8c19ccc949c6d482b061be6a28e10782abf0e7af',
- },
- {
- _index: 6,
- _revision_number: 4,
- message: 'Patch Set 4:\n\n(6 comments)',
- date: '2016-09-28 13:36:33.000000000',
- author,
- id: 'e7bfdbc842f6b6d8064bc68e0f52b673f40c0ca5',
- }
- );
- element.messages = messages;
- const isAuthor = function(author, message) {
- return message.author._account_id === author._account_id;
- };
- const isMarvin = isAuthor.bind(null, author);
- flushAsynchronousOperations();
- const messageElements = getMessages();
- assert.equal(messageElements.length, messages.length);
- assert.deepEqual(messageElements[1].message, messages[1]);
- assert.deepEqual(messageElements[2].message, messages[2]);
- assert.deepEqual(messageElements[1].comments.file1,
- comments.file1.filter(isMarvin));
- assert.deepEqual(messageElements[1].comments.file2,
- comments.file2.filter(isMarvin));
- assert.deepEqual(messageElements[2].comments, {});
- });
-
- test('messages without author do not throw', () => {
- const messages = [{
- _index: 5,
- _revision_number: 4,
- message: 'Uploaded patch set 4.',
- date: '2016-09-28 13:36:33.000000000',
- id: '8c19ccc949c6d482b061be6a28e10782abf0e7af',
- }];
- element.messages = messages;
- flushAsynchronousOperations();
- const messageEls = getMessages();
- assert.equal(messageEls.length, 1);
- assert.equal(messageEls[0].message.message, messages[0].message);
- });
-
- test('hide increment text if increment >= total remaining', () => {
- // Test with stubbed return values, as _numRemaining and _getDelta have
- // their own tests.
- sandbox.stub(element, '_getDelta').returns(5);
- const remainingStub = sandbox.stub(element, '_numRemaining').returns(6);
- assert.isFalse(element._computeIncrementHidden(null, null, null));
- remainingStub.restore();
-
- sandbox.stub(element, '_numRemaining').returns(4);
- assert.isTrue(element._computeIncrementHidden(null, null, null));
- });
+ // Stub methods on the changeComments object after changeComments has
+ // been initialized.
+ return commentApiWrapper.loadComments();
});
- suite('gr-messages-list automate tests', () => {
- let element;
- let messages;
- let sandbox;
- let commentApiWrapper;
+ teardown(() => {
+ sandbox.restore();
+ });
- const getMessages = function() {
- return Polymer.dom(element.root).querySelectorAll('gr-message');
- };
- const getHiddenMessages = function() {
- return Polymer.dom(element.root).querySelectorAll('gr-message[hidden]');
- };
+ test('hide autogenerated button is not hidden', () => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('#automatedMessageToggle[hidden]'));
+ });
- const randomMessageReviewer = {
- reviewer: {},
- date: '2016-01-13 20:30:33.038000',
- };
+ test('autogenerated messages are not hidden initially', () => {
+ const allHiddenMessageEls = getHiddenMessages();
- setup(() => {
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getLoggedIn() { return Promise.resolve(false); },
- getDiffComments() { return Promise.resolve({}); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
+ // There are no hidden messages.
+ assert.isFalse(!!allHiddenMessageEls.length);
+ });
+
+ test('autogenerated messages hidden after comments only toggle', () => {
+ let allHiddenMessageEls = getHiddenMessages();
+
+ element._hideAutomated = false;
+ MockInteractions.tap(element.$.automatedMessageToggle);
+ flushAsynchronousOperations();
+ const allMessageEls = getMessages();
+ allHiddenMessageEls = getHiddenMessages();
+
+ // Autogenerated messages are now hidden.
+ assert.equal(allHiddenMessageEls.length, allMessageEls.length);
+ });
+
+ test('autogenerated messages not hidden after comments only toggle',
+ () => {
+ let allHiddenMessageEls = getHiddenMessages();
+
+ element._hideAutomated = true;
+ MockInteractions.tap(element.$.automatedMessageToggle);
+ allHiddenMessageEls = getHiddenMessages();
+
+ // Autogenerated messages are now hidden.
+ assert.isFalse(!!allHiddenMessageEls.length);
});
- sandbox = sinon.sandbox.create();
- messages = _.times(2, randomAutomated);
- messages.push(randomMessageReviewer);
+ test('_getDelta', () => {
+ let messages = [randomMessage()];
+ assert.equal(element._getDelta([], messages, false), 1);
+ assert.equal(element._getDelta([], messages, true), 1);
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = fixture('basic');
- element = commentApiWrapper.$.messagesList;
- sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
- element.messages = messages;
+ messages = _.times(7, randomMessage);
+ assert.equal(element._getDelta([], messages, false), 5);
+ assert.equal(element._getDelta([], messages, true), 5);
- // Stub methods on the changeComments object after changeComments has
- // been initialized.
- return commentApiWrapper.loadComments();
- });
+ messages = _.times(4, randomMessage)
+ .concat(_.times(2, randomAutomated))
+ .concat(_.times(3, randomMessage));
- teardown(() => {
- sandbox.restore();
- });
+ const dummyArr = _.times(2, randomMessage);
+ assert.equal(element._getDelta(dummyArr, messages, false), 5);
+ assert.equal(element._getDelta(dummyArr, messages, true), 7);
+ });
- test('hide autogenerated button is not hidden', () => {
- assert.isNotOk(element.shadowRoot
- .querySelector('#automatedMessageToggle[hidden]'));
- });
+ test('_getHumanMessages', () => {
+ assert.equal(
+ element._getHumanMessages(_.times(5, randomAutomated)).length, 0);
+ assert.equal(
+ element._getHumanMessages(_.times(5, randomMessage)).length, 5);
- test('autogenerated messages are not hidden initially', () => {
- const allHiddenMessageEls = getHiddenMessages();
+ let messages = _.shuffle(_.times(5, randomMessage)
+ .concat(_.times(5, randomAutomated)));
+ messages = element._getHumanMessages(messages);
+ assert.equal(messages.length, 5);
+ assert.isFalse(element._hasAutomatedMessages(messages));
+ });
- // There are no hidden messages.
- assert.isFalse(!!allHiddenMessageEls.length);
- });
-
- test('autogenerated messages hidden after comments only toggle', () => {
- let allHiddenMessageEls = getHiddenMessages();
-
- element._hideAutomated = false;
- MockInteractions.tap(element.$.automatedMessageToggle);
- flushAsynchronousOperations();
- const allMessageEls = getMessages();
- allHiddenMessageEls = getHiddenMessages();
-
- // Autogenerated messages are now hidden.
- assert.equal(allHiddenMessageEls.length, allMessageEls.length);
- });
-
- test('autogenerated messages not hidden after comments only toggle',
- () => {
- let allHiddenMessageEls = getHiddenMessages();
-
- element._hideAutomated = true;
- MockInteractions.tap(element.$.automatedMessageToggle);
- allHiddenMessageEls = getHiddenMessages();
-
- // Autogenerated messages are now hidden.
- assert.isFalse(!!allHiddenMessageEls.length);
+ test('initially show only 20 messages', () => {
+ sandbox.stub(element.$.reporting, 'reportInteraction',
+ (eventName, details) => {
+ assert.equal(typeof(eventName), 'string');
+ if (details) {
+ assert.equal(typeof(details), 'object');
+ }
});
+ const messages = Array.from(Array(23).keys())
+ .map(() => {
+ return {};
+ });
+ element._processedMessagesChanged(messages);
- test('_getDelta', () => {
- let messages = [randomMessage()];
- assert.equal(element._getDelta([], messages, false), 1);
- assert.equal(element._getDelta([], messages, true), 1);
+ assert.equal(element._visibleMessages.length, 20);
+ });
- messages = _.times(7, randomMessage);
- assert.equal(element._getDelta([], messages, false), 5);
- assert.equal(element._getDelta([], messages, true), 5);
+ test('_computeLabelExtremes', () => {
+ const computeSpy = sandbox.spy(element, '_computeLabelExtremes');
- messages = _.times(4, randomMessage)
- .concat(_.times(2, randomAutomated))
- .concat(_.times(3, randomMessage));
+ element.labels = null;
+ assert.isTrue(computeSpy.calledOnce);
+ assert.deepEqual(computeSpy.lastCall.returnValue, {});
- const dummyArr = _.times(2, randomMessage);
- assert.equal(element._getDelta(dummyArr, messages, false), 5);
- assert.equal(element._getDelta(dummyArr, messages, true), 7);
- });
+ element.labels = {};
+ assert.isTrue(computeSpy.calledTwice);
+ assert.deepEqual(computeSpy.lastCall.returnValue, {});
- test('_getHumanMessages', () => {
- assert.equal(
- element._getHumanMessages(_.times(5, randomAutomated)).length, 0);
- assert.equal(
- element._getHumanMessages(_.times(5, randomMessage)).length, 5);
+ element.labels = {'my-label': {}};
+ assert.isTrue(computeSpy.calledThrice);
+ assert.deepEqual(computeSpy.lastCall.returnValue, {});
- let messages = _.shuffle(_.times(5, randomMessage)
- .concat(_.times(5, randomAutomated)));
- messages = element._getHumanMessages(messages);
- assert.equal(messages.length, 5);
- assert.isFalse(element._hasAutomatedMessages(messages));
- });
+ element.labels = {'my-label': {values: {}}};
+ assert.equal(computeSpy.callCount, 4);
+ assert.deepEqual(computeSpy.lastCall.returnValue, {});
- test('initially show only 20 messages', () => {
- sandbox.stub(element.$.reporting, 'reportInteraction',
- (eventName, details) => {
- assert.equal(typeof(eventName), 'string');
- if (details) {
- assert.equal(typeof(details), 'object');
- }
- });
- const messages = Array.from(Array(23).keys())
- .map(() => {
- return {};
- });
- element._processedMessagesChanged(messages);
+ element.labels = {'my-label': {values: {'-12': {}}}};
+ assert.equal(computeSpy.callCount, 5);
+ assert.deepEqual(computeSpy.lastCall.returnValue,
+ {'my-label': {min: -12, max: -12}});
- assert.equal(element._visibleMessages.length, 20);
- });
+ element.labels = {
+ 'my-label': {values: {'-2': {}, '-1': {}, '0': {}, '+1': {}, '+2': {}}},
+ };
+ assert.equal(computeSpy.callCount, 6);
+ assert.deepEqual(computeSpy.lastCall.returnValue,
+ {'my-label': {min: -2, max: 2}});
- test('_computeLabelExtremes', () => {
- const computeSpy = sandbox.spy(element, '_computeLabelExtremes');
-
- element.labels = null;
- assert.isTrue(computeSpy.calledOnce);
- assert.deepEqual(computeSpy.lastCall.returnValue, {});
-
- element.labels = {};
- assert.isTrue(computeSpy.calledTwice);
- assert.deepEqual(computeSpy.lastCall.returnValue, {});
-
- element.labels = {'my-label': {}};
- assert.isTrue(computeSpy.calledThrice);
- assert.deepEqual(computeSpy.lastCall.returnValue, {});
-
- element.labels = {'my-label': {values: {}}};
- assert.equal(computeSpy.callCount, 4);
- assert.deepEqual(computeSpy.lastCall.returnValue, {});
-
- element.labels = {'my-label': {values: {'-12': {}}}};
- assert.equal(computeSpy.callCount, 5);
- assert.deepEqual(computeSpy.lastCall.returnValue,
- {'my-label': {min: -12, max: -12}});
-
- element.labels = {
- 'my-label': {values: {'-2': {}, '-1': {}, '0': {}, '+1': {}, '+2': {}}},
- };
- assert.equal(computeSpy.callCount, 6);
- assert.deepEqual(computeSpy.lastCall.returnValue,
- {'my-label': {min: -2, max: 2}});
-
- element.labels = {
- 'my-label': {values: {'-12': {}}},
- 'other-label': {values: {'-1': {}, ' 0': {}, '+1': {}}},
- };
- assert.equal(computeSpy.callCount, 7);
- assert.deepEqual(computeSpy.lastCall.returnValue, {
- 'my-label': {min: -12, max: -12},
- 'other-label': {min: -1, max: 1},
- });
+ element.labels = {
+ 'my-label': {values: {'-12': {}}},
+ 'other-label': {values: {'-1': {}, ' 0': {}, '+1': {}}},
+ };
+ assert.equal(computeSpy.callCount, 7);
+ assert.deepEqual(computeSpy.lastCall.returnValue, {
+ 'my-label': {min: -12, max: -12},
+ 'other-label': {min: -1, max: 1},
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
deleted file mode 100644
index 696ffdf..0000000
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
+++ /dev/null
@@ -1,189 +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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-related-changes-list">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- }
- h3 {
- margin: var(--spacing-m) 0 0;
- }
- section {
- margin-bottom: 1.4em; /* Same as line height for collapse purposes */
- }
- a {
- display: block;
- }
- .changeContainer,
- a {
- max-width: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .changeContainer {
- display: flex;
- }
- .changeContainer.thisChange:before {
- content: '➔';
- width: 1.2em;
- }
- h4,
- section div {
- display: flex;
- }
- h4:before,
- section div:before {
- content: ' ';
- flex-shrink: 0;
- width: 1.2em;
- }
- .note {
- color: var(--error-text-color);
- }
- .relatedChanges a {
- display: inline-block;
- }
- .strikethrough {
- color: var(--deemphasized-text-color);
- text-decoration: line-through;
- }
- .status {
- color: var(--deemphasized-text-color);
- font-weight: var(--font-weight-bold);
- margin-left: var(--spacing-xs);
- }
- .notCurrent {
- color: #e65100;
- }
- .indirectAncestor {
- color: #33691e;
- }
- .submittable {
- color: #1b5e20;
- }
- .submittableCheck {
- color: var(--vote-text-color-recommended);
- display: none;
- }
- .submittableCheck.submittable {
- display: inline;
- }
- .hidden,
- .mobile {
- display: none;
- }
- @media screen and (max-width: 60em) {
- .mobile {
- display: block;
- }
- }
- </style>
- <div>
- <section class="relatedChanges" hidden$="[[!_relatedResponse.changes.length]]" hidden>
- <h4>Relation chain</h4>
- <template
- is="dom-repeat"
- items="[[_relatedResponse.changes]]"
- as="related">
- <div class$="rightIndent [[_computeChangeContainerClass(change, related)]]">
- <a href$="[[_computeChangeURL(related._change_number, related.project, related._revision_number)]]"
- class$="[[_computeLinkClass(related)]]"
- title$="[[related.commit.subject]]">
- [[related.commit.subject]]
- </a>
- <span class$="[[_computeChangeStatusClass(related)]]">
- ([[_computeChangeStatus(related)]])
- </span>
- </div>
- </template>
- </section>
- <section
- id="submittedTogether"
- class$="[[_computeSubmittedTogetherClass(_submittedTogether)]]">
- <h4>Submitted together</h4>
- <template is="dom-repeat" items="[[_submittedTogether.changes]]" as="related">
- <div class$="[[_computeChangeContainerClass(change, related)]]">
- <a href$="[[_computeChangeURL(related._number, related.project)]]"
- class$="[[_computeLinkClass(related)]]"
- title$="[[related.project]]: [[related.branch]]: [[related.subject]]">
- [[related.project]]: [[related.branch]]: [[related.subject]]
- </a>
- <span
- tabindex="-1"
- title="Submittable"
- class$="submittableCheck [[_computeLinkClass(related)]]">✓</span>
- </div>
- </template>
- <template is="dom-if" if="[[_submittedTogether.non_visible_changes]]">
- <div class="note">
- [[_computeNonVisibleChangesNote(_submittedTogether.non_visible_changes)]]
- </div>
- </template>
- </section>
- <section hidden$="[[!_sameTopic.length]]" hidden>
- <h4>Same topic</h4>
- <template is="dom-repeat" items="[[_sameTopic]]" as="change">
- <div>
- <a href$="[[_computeChangeURL(change._number, change.project)]]"
- class$="[[_computeLinkClass(change)]]"
- title$="[[change.project]]: [[change.branch]]: [[change.subject]]">
- [[change.project]]: [[change.branch]]: [[change.subject]]
- </a>
- </div>
- </template>
- </section>
- <section hidden$="[[!_conflicts.length]]" hidden>
- <h4>Merge conflicts</h4>
- <template is="dom-repeat" items="[[_conflicts]]" as="change">
- <div>
- <a href$="[[_computeChangeURL(change._number, change.project)]]"
- class$="[[_computeLinkClass(change)]]"
- title$="[[change.subject]]">
- [[change.subject]]
- </a>
- </div>
- </template>
- </section>
- <section hidden$="[[!_cherryPicks.length]]" hidden>
- <h4>Cherry picks</h4>
- <template is="dom-repeat" items="[[_cherryPicks]]" as="change">
- <div>
- <a href$="[[_computeChangeURL(change._number, change.project)]]"
- class$="[[_computeLinkClass(change)]]"
- title$="[[change.branch]]: [[change.subject]]">
- [[change.branch]]: [[change.subject]]
- </a>
- </div>
- </template>
- </section>
- </div>
- <div hidden$="[[!loading]]">Loading...</div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-related-changes-list.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
index d4a2398..c4af481 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -14,384 +14,397 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-related-changes-list_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrRelatedChangesList extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-related-changes-list'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * Fired when a new section is loaded so that the change view can determine
+ * a show more button is needed, sometimes before all the sections finish
+ * loading.
+ *
+ * @event new-section-loaded
*/
- class GrRelatedChangesList extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-related-changes-list'; }
- /**
- * Fired when a new section is loaded so that the change view can determine
- * a show more button is needed, sometimes before all the sections finish
- * loading.
- *
- * @event new-section-loaded
- */
- static get properties() {
- return {
- change: Object,
- hasParent: {
- type: Boolean,
- notify: true,
- value: false,
- },
- patchNum: String,
- parentChange: Object,
- hidden: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- loading: {
- type: Boolean,
- notify: true,
- },
- mergeable: Boolean,
- _connectedRevisions: {
- type: Array,
- computed: '_computeConnectedRevisions(change, patchNum, ' +
- '_relatedResponse.changes)',
- },
- /** @type {?} */
- _relatedResponse: {
- type: Object,
- value() { return {changes: []}; },
- },
- /** @type {?} */
- _submittedTogether: {
- type: Object,
- value() { return {changes: []}; },
- },
- _conflicts: {
- type: Array,
- value() { return []; },
- },
- _cherryPicks: {
- type: Array,
- value() { return []; },
- },
- _sameTopic: {
- type: Array,
- value() { return []; },
- },
- };
- }
-
- static get observers() {
- return [
- '_resultsChanged(_relatedResponse, _submittedTogether, ' +
- '_conflicts, _cherryPicks, _sameTopic)',
- ];
- }
-
- clear() {
- this.loading = true;
- this.hidden = true;
-
- this._relatedResponse = {changes: []};
- this._submittedTogether = {changes: []};
- this._conflicts = [];
- this._cherryPicks = [];
- this._sameTopic = [];
- }
-
- reload() {
- if (!this.change || !this.patchNum) {
- return Promise.resolve();
- }
- this.loading = true;
- const promises = [
- this._getRelatedChanges().then(response => {
- this._relatedResponse = response;
- this._fireReloadEvent();
- this.hasParent = this._calculateHasParent(this.change.change_id,
- response.changes);
- }),
- this._getSubmittedTogether().then(response => {
- this._submittedTogether = response;
- this._fireReloadEvent();
- }),
- this._getCherryPicks().then(response => {
- this._cherryPicks = response;
- this._fireReloadEvent();
- }),
- ];
-
- // Get conflicts if change is open and is mergeable.
- if (this.changeIsOpen(this.change) && this.mergeable) {
- promises.push(this._getConflicts().then(response => {
- // Because the server doesn't always return a response and the
- // template expects an array, always return an array.
- this._conflicts = response ? response : [];
- this._fireReloadEvent();
- }));
- }
-
- promises.push(this._getServerConfig().then(config => {
- if (this.change.topic && !config.change.submit_whole_topic) {
- return this._getChangesWithSameTopic().then(response => {
- this._sameTopic = response;
- });
- } else {
- this._sameTopic = [];
- }
- return this._sameTopic;
- }));
-
- return Promise.all(promises).then(() => {
- this.loading = false;
- });
- }
-
- _fireReloadEvent() {
- // The listener on the change computes height of the related changes
- // section, so they have to be rendered first, and inside a dom-repeat,
- // that requires a flush.
- Polymer.dom.flush();
- this.dispatchEvent(new CustomEvent('new-section-loaded'));
- }
-
- /**
- * Determines whether or not the given change has a parent change. If there
- * is a relation chain, and the change id is not the last item of the
- * relation chain, there is a parent.
- *
- * @param {number} currentChangeId
- * @param {!Array} relatedChanges
- * @return {boolean}
- */
- _calculateHasParent(currentChangeId, relatedChanges) {
- return relatedChanges.length > 0 &&
- relatedChanges[relatedChanges.length - 1].change_id !==
- currentChangeId;
- }
-
- _getRelatedChanges() {
- return this.$.restAPI.getRelatedChanges(this.change._number,
- this.patchNum);
- }
-
- _getSubmittedTogether() {
- return this.$.restAPI.getChangesSubmittedTogether(this.change._number);
- }
-
- _getServerConfig() {
- return this.$.restAPI.getConfig();
- }
-
- _getConflicts() {
- return this.$.restAPI.getChangeConflicts(this.change._number);
- }
-
- _getCherryPicks() {
- return this.$.restAPI.getChangeCherryPicks(this.change.project,
- this.change.change_id, this.change._number);
- }
-
- _getChangesWithSameTopic() {
- return this.$.restAPI.getChangesWithSameTopic(this.change.topic,
- this.change._number);
- }
-
- /**
- * @param {number} changeNum
- * @param {string} project
- * @param {number=} opt_patchNum
- * @return {string}
- */
- _computeChangeURL(changeNum, project, opt_patchNum) {
- return Gerrit.Nav.getUrlForChangeById(changeNum, project, opt_patchNum);
- }
-
- _computeChangeContainerClass(currentChange, relatedChange) {
- const classes = ['changeContainer'];
- if ([relatedChange, currentChange].some(arg => arg === undefined)) {
- return classes;
- }
- if (this._changesEqual(relatedChange, currentChange)) {
- classes.push('thisChange');
- }
- return classes.join(' ');
- }
-
- /**
- * Do the given objects describe the same change? Compares the changes by
- * their numbers.
- *
- * @see /Documentation/rest-api-changes.html#change-info
- * @see /Documentation/rest-api-changes.html#related-change-and-commit-info
- * @param {!Object} a Either ChangeInfo or RelatedChangeAndCommitInfo
- * @param {!Object} b Either ChangeInfo or RelatedChangeAndCommitInfo
- * @return {boolean}
- */
- _changesEqual(a, b) {
- const aNum = this._getChangeNumber(a);
- const bNum = this._getChangeNumber(b);
- return aNum === bNum;
- }
-
- /**
- * Get the change number from either a ChangeInfo (such as those included in
- * SubmittedTogetherInfo responses) or get the change number from a
- * RelatedChangeAndCommitInfo (such as those included in a
- * RelatedChangesInfo response).
- *
- * @see /Documentation/rest-api-changes.html#change-info
- * @see /Documentation/rest-api-changes.html#related-change-and-commit-info
- *
- * @param {!Object} change Either a ChangeInfo or a
- * RelatedChangeAndCommitInfo object.
- * @return {number}
- */
- _getChangeNumber(change) {
- // Default to 0 if change property is not defined.
- if (!change) return 0;
-
- if (change.hasOwnProperty('_change_number')) {
- return change._change_number;
- }
- return change._number;
- }
-
- _computeLinkClass(change) {
- const statuses = [];
- if (change.status == this.ChangeStatus.ABANDONED) {
- statuses.push('strikethrough');
- }
- if (change.submittable) {
- statuses.push('submittable');
- }
- return statuses.join(' ');
- }
-
- _computeChangeStatusClass(change) {
- const classes = ['status'];
- if (change._revision_number != change._current_revision_number) {
- classes.push('notCurrent');
- } else if (this._isIndirectAncestor(change)) {
- classes.push('indirectAncestor');
- } else if (change.submittable) {
- classes.push('submittable');
- } else if (change.status == this.ChangeStatus.NEW) {
- classes.push('hidden');
- }
- return classes.join(' ');
- }
-
- _computeChangeStatus(change) {
- switch (change.status) {
- case this.ChangeStatus.MERGED:
- return 'Merged';
- case this.ChangeStatus.ABANDONED:
- return 'Abandoned';
- }
- if (change._revision_number != change._current_revision_number) {
- return 'Not current';
- } else if (this._isIndirectAncestor(change)) {
- return 'Indirect ancestor';
- } else if (change.submittable) {
- return 'Submittable';
- }
- return '';
- }
-
- _resultsChanged(related, submittedTogether, conflicts,
- cherryPicks, sameTopic) {
- // Polymer 2: check for undefined
- if ([
- related,
- submittedTogether,
- conflicts,
- cherryPicks,
- sameTopic,
- ].some(arg => arg === undefined)) {
- return;
- }
-
- const results = [
- related && related.changes,
- submittedTogether && submittedTogether.changes,
- conflicts,
- cherryPicks,
- sameTopic,
- ];
- for (let i = 0; i < results.length; i++) {
- if (results[i] && results[i].length > 0) {
- this.hidden = false;
- this.fire('update', null, {bubbles: false});
- return;
- }
- }
- this.hidden = true;
- }
-
- _isIndirectAncestor(change) {
- return !this._connectedRevisions.includes(change.commit.commit);
- }
-
- _computeConnectedRevisions(change, patchNum, relatedChanges) {
- // Polymer 2: check for undefined
- if ([change, patchNum, relatedChanges].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const connected = [];
- let changeRevision;
- if (!change) { return []; }
- for (const rev in change.revisions) {
- if (this.patchNumEquals(change.revisions[rev]._number, patchNum)) {
- changeRevision = rev;
- }
- }
- const commits = relatedChanges.map(c => c.commit);
- let pos = commits.length - 1;
-
- while (pos >= 0) {
- const commit = commits[pos].commit;
- connected.push(commit);
- if (commit == changeRevision) {
- break;
- }
- pos--;
- }
- while (pos >= 0) {
- for (let i = 0; i < commits[pos].parents.length; i++) {
- if (connected.includes(commits[pos].parents[i].commit)) {
- connected.push(commits[pos].commit);
- break;
- }
- }
- --pos;
- }
- return connected;
- }
-
- _computeSubmittedTogetherClass(submittedTogether) {
- if (!submittedTogether || (
- submittedTogether.changes.length === 0 &&
- !submittedTogether.non_visible_changes)) {
- return 'hidden';
- }
- return '';
- }
-
- _computeNonVisibleChangesNote(n) {
- const noun = n === 1 ? 'change' : 'changes';
- return `(+ ${n} non-visible ${noun})`;
- }
+ static get properties() {
+ return {
+ change: Object,
+ hasParent: {
+ type: Boolean,
+ notify: true,
+ value: false,
+ },
+ patchNum: String,
+ parentChange: Object,
+ hidden: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ loading: {
+ type: Boolean,
+ notify: true,
+ },
+ mergeable: Boolean,
+ _connectedRevisions: {
+ type: Array,
+ computed: '_computeConnectedRevisions(change, patchNum, ' +
+ '_relatedResponse.changes)',
+ },
+ /** @type {?} */
+ _relatedResponse: {
+ type: Object,
+ value() { return {changes: []}; },
+ },
+ /** @type {?} */
+ _submittedTogether: {
+ type: Object,
+ value() { return {changes: []}; },
+ },
+ _conflicts: {
+ type: Array,
+ value() { return []; },
+ },
+ _cherryPicks: {
+ type: Array,
+ value() { return []; },
+ },
+ _sameTopic: {
+ type: Array,
+ value() { return []; },
+ },
+ };
}
- customElements.define(GrRelatedChangesList.is, GrRelatedChangesList);
-})();
+ static get observers() {
+ return [
+ '_resultsChanged(_relatedResponse, _submittedTogether, ' +
+ '_conflicts, _cherryPicks, _sameTopic)',
+ ];
+ }
+
+ clear() {
+ this.loading = true;
+ this.hidden = true;
+
+ this._relatedResponse = {changes: []};
+ this._submittedTogether = {changes: []};
+ this._conflicts = [];
+ this._cherryPicks = [];
+ this._sameTopic = [];
+ }
+
+ reload() {
+ if (!this.change || !this.patchNum) {
+ return Promise.resolve();
+ }
+ this.loading = true;
+ const promises = [
+ this._getRelatedChanges().then(response => {
+ this._relatedResponse = response;
+ this._fireReloadEvent();
+ this.hasParent = this._calculateHasParent(this.change.change_id,
+ response.changes);
+ }),
+ this._getSubmittedTogether().then(response => {
+ this._submittedTogether = response;
+ this._fireReloadEvent();
+ }),
+ this._getCherryPicks().then(response => {
+ this._cherryPicks = response;
+ this._fireReloadEvent();
+ }),
+ ];
+
+ // Get conflicts if change is open and is mergeable.
+ if (this.changeIsOpen(this.change) && this.mergeable) {
+ promises.push(this._getConflicts().then(response => {
+ // Because the server doesn't always return a response and the
+ // template expects an array, always return an array.
+ this._conflicts = response ? response : [];
+ this._fireReloadEvent();
+ }));
+ }
+
+ promises.push(this._getServerConfig().then(config => {
+ if (this.change.topic && !config.change.submit_whole_topic) {
+ return this._getChangesWithSameTopic().then(response => {
+ this._sameTopic = response;
+ });
+ } else {
+ this._sameTopic = [];
+ }
+ return this._sameTopic;
+ }));
+
+ return Promise.all(promises).then(() => {
+ this.loading = false;
+ });
+ }
+
+ _fireReloadEvent() {
+ // The listener on the change computes height of the related changes
+ // section, so they have to be rendered first, and inside a dom-repeat,
+ // that requires a flush.
+ flush();
+ this.dispatchEvent(new CustomEvent('new-section-loaded'));
+ }
+
+ /**
+ * Determines whether or not the given change has a parent change. If there
+ * is a relation chain, and the change id is not the last item of the
+ * relation chain, there is a parent.
+ *
+ * @param {number} currentChangeId
+ * @param {!Array} relatedChanges
+ * @return {boolean}
+ */
+ _calculateHasParent(currentChangeId, relatedChanges) {
+ return relatedChanges.length > 0 &&
+ relatedChanges[relatedChanges.length - 1].change_id !==
+ currentChangeId;
+ }
+
+ _getRelatedChanges() {
+ return this.$.restAPI.getRelatedChanges(this.change._number,
+ this.patchNum);
+ }
+
+ _getSubmittedTogether() {
+ return this.$.restAPI.getChangesSubmittedTogether(this.change._number);
+ }
+
+ _getServerConfig() {
+ return this.$.restAPI.getConfig();
+ }
+
+ _getConflicts() {
+ return this.$.restAPI.getChangeConflicts(this.change._number);
+ }
+
+ _getCherryPicks() {
+ return this.$.restAPI.getChangeCherryPicks(this.change.project,
+ this.change.change_id, this.change._number);
+ }
+
+ _getChangesWithSameTopic() {
+ return this.$.restAPI.getChangesWithSameTopic(this.change.topic,
+ this.change._number);
+ }
+
+ /**
+ * @param {number} changeNum
+ * @param {string} project
+ * @param {number=} opt_patchNum
+ * @return {string}
+ */
+ _computeChangeURL(changeNum, project, opt_patchNum) {
+ return Gerrit.Nav.getUrlForChangeById(changeNum, project, opt_patchNum);
+ }
+
+ _computeChangeContainerClass(currentChange, relatedChange) {
+ const classes = ['changeContainer'];
+ if ([relatedChange, currentChange].some(arg => arg === undefined)) {
+ return classes;
+ }
+ if (this._changesEqual(relatedChange, currentChange)) {
+ classes.push('thisChange');
+ }
+ return classes.join(' ');
+ }
+
+ /**
+ * Do the given objects describe the same change? Compares the changes by
+ * their numbers.
+ *
+ * @see /Documentation/rest-api-changes.html#change-info
+ * @see /Documentation/rest-api-changes.html#related-change-and-commit-info
+ * @param {!Object} a Either ChangeInfo or RelatedChangeAndCommitInfo
+ * @param {!Object} b Either ChangeInfo or RelatedChangeAndCommitInfo
+ * @return {boolean}
+ */
+ _changesEqual(a, b) {
+ const aNum = this._getChangeNumber(a);
+ const bNum = this._getChangeNumber(b);
+ return aNum === bNum;
+ }
+
+ /**
+ * Get the change number from either a ChangeInfo (such as those included in
+ * SubmittedTogetherInfo responses) or get the change number from a
+ * RelatedChangeAndCommitInfo (such as those included in a
+ * RelatedChangesInfo response).
+ *
+ * @see /Documentation/rest-api-changes.html#change-info
+ * @see /Documentation/rest-api-changes.html#related-change-and-commit-info
+ *
+ * @param {!Object} change Either a ChangeInfo or a
+ * RelatedChangeAndCommitInfo object.
+ * @return {number}
+ */
+ _getChangeNumber(change) {
+ // Default to 0 if change property is not defined.
+ if (!change) return 0;
+
+ if (change.hasOwnProperty('_change_number')) {
+ return change._change_number;
+ }
+ return change._number;
+ }
+
+ _computeLinkClass(change) {
+ const statuses = [];
+ if (change.status == this.ChangeStatus.ABANDONED) {
+ statuses.push('strikethrough');
+ }
+ if (change.submittable) {
+ statuses.push('submittable');
+ }
+ return statuses.join(' ');
+ }
+
+ _computeChangeStatusClass(change) {
+ const classes = ['status'];
+ if (change._revision_number != change._current_revision_number) {
+ classes.push('notCurrent');
+ } else if (this._isIndirectAncestor(change)) {
+ classes.push('indirectAncestor');
+ } else if (change.submittable) {
+ classes.push('submittable');
+ } else if (change.status == this.ChangeStatus.NEW) {
+ classes.push('hidden');
+ }
+ return classes.join(' ');
+ }
+
+ _computeChangeStatus(change) {
+ switch (change.status) {
+ case this.ChangeStatus.MERGED:
+ return 'Merged';
+ case this.ChangeStatus.ABANDONED:
+ return 'Abandoned';
+ }
+ if (change._revision_number != change._current_revision_number) {
+ return 'Not current';
+ } else if (this._isIndirectAncestor(change)) {
+ return 'Indirect ancestor';
+ } else if (change.submittable) {
+ return 'Submittable';
+ }
+ return '';
+ }
+
+ _resultsChanged(related, submittedTogether, conflicts,
+ cherryPicks, sameTopic) {
+ // Polymer 2: check for undefined
+ if ([
+ related,
+ submittedTogether,
+ conflicts,
+ cherryPicks,
+ sameTopic,
+ ].some(arg => arg === undefined)) {
+ return;
+ }
+
+ const results = [
+ related && related.changes,
+ submittedTogether && submittedTogether.changes,
+ conflicts,
+ cherryPicks,
+ sameTopic,
+ ];
+ for (let i = 0; i < results.length; i++) {
+ if (results[i] && results[i].length > 0) {
+ this.hidden = false;
+ this.fire('update', null, {bubbles: false});
+ return;
+ }
+ }
+ this.hidden = true;
+ }
+
+ _isIndirectAncestor(change) {
+ return !this._connectedRevisions.includes(change.commit.commit);
+ }
+
+ _computeConnectedRevisions(change, patchNum, relatedChanges) {
+ // Polymer 2: check for undefined
+ if ([change, patchNum, relatedChanges].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const connected = [];
+ let changeRevision;
+ if (!change) { return []; }
+ for (const rev in change.revisions) {
+ if (this.patchNumEquals(change.revisions[rev]._number, patchNum)) {
+ changeRevision = rev;
+ }
+ }
+ const commits = relatedChanges.map(c => c.commit);
+ let pos = commits.length - 1;
+
+ while (pos >= 0) {
+ const commit = commits[pos].commit;
+ connected.push(commit);
+ if (commit == changeRevision) {
+ break;
+ }
+ pos--;
+ }
+ while (pos >= 0) {
+ for (let i = 0; i < commits[pos].parents.length; i++) {
+ if (connected.includes(commits[pos].parents[i].commit)) {
+ connected.push(commits[pos].commit);
+ break;
+ }
+ }
+ --pos;
+ }
+ return connected;
+ }
+
+ _computeSubmittedTogetherClass(submittedTogether) {
+ if (!submittedTogether || (
+ submittedTogether.changes.length === 0 &&
+ !submittedTogether.non_visible_changes)) {
+ return 'hidden';
+ }
+ return '';
+ }
+
+ _computeNonVisibleChangesNote(n) {
+ const noun = n === 1 ? 'change' : 'changes';
+ return `(+ ${n} non-visible ${noun})`;
+ }
+}
+
+customElements.define(GrRelatedChangesList.is, GrRelatedChangesList);
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js
new file mode 100644
index 0000000..1d8551d
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js
@@ -0,0 +1,161 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ }
+ h3 {
+ margin: var(--spacing-m) 0 0;
+ }
+ section {
+ margin-bottom: 1.4em; /* Same as line height for collapse purposes */
+ }
+ a {
+ display: block;
+ }
+ .changeContainer,
+ a {
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .changeContainer {
+ display: flex;
+ }
+ .changeContainer.thisChange:before {
+ content: '➔';
+ width: 1.2em;
+ }
+ h4,
+ section div {
+ display: flex;
+ }
+ h4:before,
+ section div:before {
+ content: ' ';
+ flex-shrink: 0;
+ width: 1.2em;
+ }
+ .note {
+ color: var(--error-text-color);
+ }
+ .relatedChanges a {
+ display: inline-block;
+ }
+ .strikethrough {
+ color: var(--deemphasized-text-color);
+ text-decoration: line-through;
+ }
+ .status {
+ color: var(--deemphasized-text-color);
+ font-weight: var(--font-weight-bold);
+ margin-left: var(--spacing-xs);
+ }
+ .notCurrent {
+ color: #e65100;
+ }
+ .indirectAncestor {
+ color: #33691e;
+ }
+ .submittable {
+ color: #1b5e20;
+ }
+ .submittableCheck {
+ color: var(--vote-text-color-recommended);
+ display: none;
+ }
+ .submittableCheck.submittable {
+ display: inline;
+ }
+ .hidden,
+ .mobile {
+ display: none;
+ }
+ @media screen and (max-width: 60em) {
+ .mobile {
+ display: block;
+ }
+ }
+ </style>
+ <div>
+ <section class="relatedChanges" hidden\$="[[!_relatedResponse.changes.length]]" hidden="">
+ <h4>Relation chain</h4>
+ <template is="dom-repeat" items="[[_relatedResponse.changes]]" as="related">
+ <div class\$="rightIndent [[_computeChangeContainerClass(change, related)]]">
+ <a href\$="[[_computeChangeURL(related._change_number, related.project, related._revision_number)]]" class\$="[[_computeLinkClass(related)]]" title\$="[[related.commit.subject]]">
+ [[related.commit.subject]]
+ </a>
+ <span class\$="[[_computeChangeStatusClass(related)]]">
+ ([[_computeChangeStatus(related)]])
+ </span>
+ </div>
+ </template>
+ </section>
+ <section id="submittedTogether" class\$="[[_computeSubmittedTogetherClass(_submittedTogether)]]">
+ <h4>Submitted together</h4>
+ <template is="dom-repeat" items="[[_submittedTogether.changes]]" as="related">
+ <div class\$="[[_computeChangeContainerClass(change, related)]]">
+ <a href\$="[[_computeChangeURL(related._number, related.project)]]" class\$="[[_computeLinkClass(related)]]" title\$="[[related.project]]: [[related.branch]]: [[related.subject]]">
+ [[related.project]]: [[related.branch]]: [[related.subject]]
+ </a>
+ <span tabindex="-1" title="Submittable" class\$="submittableCheck [[_computeLinkClass(related)]]">✓</span>
+ </div>
+ </template>
+ <template is="dom-if" if="[[_submittedTogether.non_visible_changes]]">
+ <div class="note">
+ [[_computeNonVisibleChangesNote(_submittedTogether.non_visible_changes)]]
+ </div>
+ </template>
+ </section>
+ <section hidden\$="[[!_sameTopic.length]]" hidden="">
+ <h4>Same topic</h4>
+ <template is="dom-repeat" items="[[_sameTopic]]" as="change">
+ <div>
+ <a href\$="[[_computeChangeURL(change._number, change.project)]]" class\$="[[_computeLinkClass(change)]]" title\$="[[change.project]]: [[change.branch]]: [[change.subject]]">
+ [[change.project]]: [[change.branch]]: [[change.subject]]
+ </a>
+ </div>
+ </template>
+ </section>
+ <section hidden\$="[[!_conflicts.length]]" hidden="">
+ <h4>Merge conflicts</h4>
+ <template is="dom-repeat" items="[[_conflicts]]" as="change">
+ <div>
+ <a href\$="[[_computeChangeURL(change._number, change.project)]]" class\$="[[_computeLinkClass(change)]]" title\$="[[change.subject]]">
+ [[change.subject]]
+ </a>
+ </div>
+ </template>
+ </section>
+ <section hidden\$="[[!_cherryPicks.length]]" hidden="">
+ <h4>Cherry picks</h4>
+ <template is="dom-repeat" items="[[_cherryPicks]]" as="change">
+ <div>
+ <a href\$="[[_computeChangeURL(change._number, change.project)]]" class\$="[[_computeLinkClass(change)]]" title\$="[[change.branch]]: [[change.subject]]">
+ [[change.branch]]: [[change.subject]]
+ </a>
+ </div>
+ </template>
+ </section>
+ </div>
+ <div hidden\$="[[!loading]]">Loading...</div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
index 9b8ebed..94e0fe5 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-related-changes-list</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-related-changes-list.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,238 +30,259 @@
</template>
</test-fixture>
-<script>
- suite('gr-related-changes-list tests', async () => {
- await readyToTest();
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-related-changes-list.js';
+suite('gr-related-changes-list tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('connected revisions', () => {
+ const change = {
+ revisions: {
+ 'e3c6d60783bfdec9ebae7dcfec4662360433449e': {
+ _number: 1,
+ },
+ '26e5e4c9c7ae31cbd876271cca281ce22b413997': {
+ _number: 2,
+ },
+ 'bf7884d695296ca0c91702ba3e2bc8df0f69a907': {
+ _number: 7,
+ },
+ 'b5fc49f2e67d1889d5275cac04ad3648f2ec7fe3': {
+ _number: 5,
+ },
+ 'd6bcee67570859ccb684873a85cf50b1f0e96fda': {
+ _number: 6,
+ },
+ 'cc960918a7f90388f4a9e05753d0f7b90ad44546': {
+ _number: 3,
+ },
+ '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6': {
+ _number: 4,
+ },
+ },
+ };
+ let patchNum = 7;
+ let relatedChanges = [
+ {
+ commit: {
+ commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
+ parents: [
+ {
+ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
+ parents: [
+ {
+ commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
+ parents: [
+ {
+ commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
+ parents: [
+ {
+ commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
+ parents: [
+ {
+ commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce',
+ parents: [
+ {
+ commit: '455ed9cd27a16bf6991f04dcc57ef575dc4d5e75',
+ },
+ ],
+ },
+ },
+ ];
+
+ let connectedChanges =
+ element._computeConnectedRevisions(change, patchNum, relatedChanges);
+ assert.deepEqual(connectedChanges, [
+ '613bc4f81741a559c6667ac08d71dcc3348f73ce',
+ 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
+ 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
+ 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
+ '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
+ '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
+ '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
+ ]);
+
+ patchNum = 4;
+ relatedChanges = [
+ {
+ commit: {
+ commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
+ parents: [
+ {
+ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
+ parents: [
+ {
+ commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
+ parents: [
+ {
+ commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b',
+ parents: [
+ {
+ commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
+ parents: [
+ {
+ commit: 'af815dac54318826b7f1fa468acc76349ffc588e',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: 'af815dac54318826b7f1fa468acc76349ffc588e',
+ parents: [
+ {
+ commit: '58f76e406e24cb8b0f5d64c7f5ac1e8616d0a22c',
+ },
+ ],
+ },
+ },
+ ];
+
+ connectedChanges =
+ element._computeConnectedRevisions(change, patchNum, relatedChanges);
+ assert.deepEqual(connectedChanges, [
+ 'af815dac54318826b7f1fa468acc76349ffc588e',
+ '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
+ '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
+ 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b',
+ ]);
+ });
+
+ test('_computeChangeContainerClass', () => {
+ const change1 = {change_id: 123, _number: 0};
+ const change2 = {change_id: 456, _change_number: 1};
+ const change3 = {change_id: 123, _number: 2};
+
+ assert.notEqual(element._computeChangeContainerClass(
+ change1, change1).indexOf('thisChange'), -1);
+ assert.equal(element._computeChangeContainerClass(
+ change1, change2).indexOf('thisChange'), -1);
+ assert.equal(element._computeChangeContainerClass(
+ change1, change3).indexOf('thisChange'), -1);
+ });
+
+ test('_changesEqual', () => {
+ const change1 = {change_id: 123, _number: 0};
+ const change2 = {change_id: 456, _number: 1};
+ const change3 = {change_id: 123, _number: 2};
+ const change4 = {change_id: 123, _change_number: 1};
+
+ assert.isTrue(element._changesEqual(change1, change1));
+ assert.isFalse(element._changesEqual(change1, change2));
+ assert.isFalse(element._changesEqual(change1, change3));
+ assert.isTrue(element._changesEqual(change2, change4));
+ });
+
+ test('_getChangeNumber', () => {
+ const change1 = {change_id: 123, _number: 0};
+ const change2 = {change_id: 456, _change_number: 1};
+ assert.equal(element._getChangeNumber(change1), 0);
+ assert.equal(element._getChangeNumber(change2), 1);
+ });
+
+ test('event for section loaded fires for each section ', () => {
+ const loadedStub = sandbox.stub();
+ element.patchNum = 7;
+ element.change = {
+ change_id: 123,
+ status: 'NEW',
+ };
+ element.mergeable = true;
+ element.addEventListener('new-section-loaded', loadedStub);
+ sandbox.stub(element, '_getRelatedChanges')
+ .returns(Promise.resolve({changes: []}));
+ sandbox.stub(element, '_getSubmittedTogether')
+ .returns(Promise.resolve());
+ sandbox.stub(element, '_getCherryPicks')
+ .returns(Promise.resolve());
+ sandbox.stub(element, '_getConflicts')
+ .returns(Promise.resolve());
+
+ return element.reload().then(() => {
+ assert.equal(loadedStub.callCount, 4);
+ });
+ });
+
+ suite('_getConflicts resolves undefined', () => {
let element;
- let sandbox;
setup(() => {
element = fixture('basic');
- sandbox = sinon.sandbox.create();
- });
- teardown(() => {
- sandbox.restore();
- });
-
- test('connected revisions', () => {
- const change = {
- revisions: {
- 'e3c6d60783bfdec9ebae7dcfec4662360433449e': {
- _number: 1,
- },
- '26e5e4c9c7ae31cbd876271cca281ce22b413997': {
- _number: 2,
- },
- 'bf7884d695296ca0c91702ba3e2bc8df0f69a907': {
- _number: 7,
- },
- 'b5fc49f2e67d1889d5275cac04ad3648f2ec7fe3': {
- _number: 5,
- },
- 'd6bcee67570859ccb684873a85cf50b1f0e96fda': {
- _number: 6,
- },
- 'cc960918a7f90388f4a9e05753d0f7b90ad44546': {
- _number: 3,
- },
- '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6': {
- _number: 4,
- },
- },
- };
- let patchNum = 7;
- let relatedChanges = [
- {
- commit: {
- commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
- parents: [
- {
- commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
- },
- ],
- },
- },
- {
- commit: {
- commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
- parents: [
- {
- commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
- },
- ],
- },
- },
- {
- commit: {
- commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
- parents: [
- {
- commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
- },
- ],
- },
- },
- {
- commit: {
- commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
- parents: [
- {
- commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
- },
- ],
- },
- },
- {
- commit: {
- commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
- parents: [
- {
- commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce',
- },
- ],
- },
- },
- {
- commit: {
- commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce',
- parents: [
- {
- commit: '455ed9cd27a16bf6991f04dcc57ef575dc4d5e75',
- },
- ],
- },
- },
- ];
-
- let connectedChanges =
- element._computeConnectedRevisions(change, patchNum, relatedChanges);
- assert.deepEqual(connectedChanges, [
- '613bc4f81741a559c6667ac08d71dcc3348f73ce',
- 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
- 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
- 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
- '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
- '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
- '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
- ]);
-
- patchNum = 4;
- relatedChanges = [
- {
- commit: {
- commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
- parents: [
- {
- commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
- },
- ],
- },
- },
- {
- commit: {
- commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
- parents: [
- {
- commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
- },
- ],
- },
- },
- {
- commit: {
- commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
- parents: [
- {
- commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
- },
- ],
- },
- },
- {
- commit: {
- commit: 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b',
- parents: [
- {
- commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
- },
- ],
- },
- },
- {
- commit: {
- commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
- parents: [
- {
- commit: 'af815dac54318826b7f1fa468acc76349ffc588e',
- },
- ],
- },
- },
- {
- commit: {
- commit: 'af815dac54318826b7f1fa468acc76349ffc588e',
- parents: [
- {
- commit: '58f76e406e24cb8b0f5d64c7f5ac1e8616d0a22c',
- },
- ],
- },
- },
- ];
-
- connectedChanges =
- element._computeConnectedRevisions(change, patchNum, relatedChanges);
- assert.deepEqual(connectedChanges, [
- 'af815dac54318826b7f1fa468acc76349ffc588e',
- '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
- '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
- 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b',
- ]);
- });
-
- test('_computeChangeContainerClass', () => {
- const change1 = {change_id: 123, _number: 0};
- const change2 = {change_id: 456, _change_number: 1};
- const change3 = {change_id: 123, _number: 2};
-
- assert.notEqual(element._computeChangeContainerClass(
- change1, change1).indexOf('thisChange'), -1);
- assert.equal(element._computeChangeContainerClass(
- change1, change2).indexOf('thisChange'), -1);
- assert.equal(element._computeChangeContainerClass(
- change1, change3).indexOf('thisChange'), -1);
- });
-
- test('_changesEqual', () => {
- const change1 = {change_id: 123, _number: 0};
- const change2 = {change_id: 456, _number: 1};
- const change3 = {change_id: 123, _number: 2};
- const change4 = {change_id: 123, _change_number: 1};
-
- assert.isTrue(element._changesEqual(change1, change1));
- assert.isFalse(element._changesEqual(change1, change2));
- assert.isFalse(element._changesEqual(change1, change3));
- assert.isTrue(element._changesEqual(change2, change4));
- });
-
- test('_getChangeNumber', () => {
- const change1 = {change_id: 123, _number: 0};
- const change2 = {change_id: 456, _change_number: 1};
- assert.equal(element._getChangeNumber(change1), 0);
- assert.equal(element._getChangeNumber(change2), 1);
- });
-
- test('event for section loaded fires for each section ', () => {
- const loadedStub = sandbox.stub();
- element.patchNum = 7;
- element.change = {
- change_id: 123,
- status: 'NEW',
- };
- element.mergeable = true;
- element.addEventListener('new-section-loaded', loadedStub);
sandbox.stub(element, '_getRelatedChanges')
.returns(Promise.resolve({changes: []}));
sandbox.stub(element, '_getSubmittedTogether')
@@ -275,283 +291,263 @@
.returns(Promise.resolve());
sandbox.stub(element, '_getConflicts')
.returns(Promise.resolve());
-
- return element.reload().then(() => {
- assert.equal(loadedStub.callCount, 4);
- });
});
- suite('_getConflicts resolves undefined', () => {
- let element;
-
- setup(() => {
- element = fixture('basic');
-
- sandbox.stub(element, '_getRelatedChanges')
- .returns(Promise.resolve({changes: []}));
- sandbox.stub(element, '_getSubmittedTogether')
- .returns(Promise.resolve());
- sandbox.stub(element, '_getCherryPicks')
- .returns(Promise.resolve());
- sandbox.stub(element, '_getConflicts')
- .returns(Promise.resolve());
- });
-
- test('_conflicts are an empty array', () => {
- element.patchNum = 7;
- element.change = {
- change_id: 123,
- status: 'NEW',
- };
- element.mergeable = true;
- element.reload();
- assert.equal(element._conflicts.length, 0);
- });
- });
-
- suite('get conflicts tests', () => {
- let element;
- let conflictsStub;
-
- setup(() => {
- element = fixture('basic');
-
- sandbox.stub(element, '_getRelatedChanges')
- .returns(Promise.resolve({changes: []}));
- sandbox.stub(element, '_getSubmittedTogether')
- .returns(Promise.resolve());
- sandbox.stub(element, '_getCherryPicks')
- .returns(Promise.resolve());
- conflictsStub = sandbox.stub(element, '_getConflicts')
- .returns(Promise.resolve());
- });
-
- test('request conflicts if open and mergeable', () => {
- element.patchNum = 7;
- element.change = {
- change_id: 123,
- status: 'NEW',
- };
- element.mergeable = true;
- element.reload();
- assert.isTrue(conflictsStub.called);
- });
-
- test('does not request conflicts if closed and mergeable', () => {
- element.patchNum = 7;
- element.change = {
- change_id: 123,
- status: 'MERGED',
- };
- element.reload();
- assert.isFalse(conflictsStub.called);
- });
-
- test('does not request conflicts if open and not mergeable', () => {
- element.patchNum = 7;
- element.change = {
- change_id: 123,
- status: 'NEW',
- };
- element.mergeable = false;
- element.reload();
- assert.isFalse(conflictsStub.called);
- });
-
- test('doesnt request conflicts if closed and not mergeable', () => {
- element.patchNum = 7;
- element.change = {
- change_id: 123,
- status: 'MERGED',
- };
- element.mergeable = false;
- element.reload();
- assert.isFalse(conflictsStub.called);
- });
- });
-
- test('_calculateHasParent', () => {
- const changeId = 123;
- const relatedChanges = [];
-
- assert.equal(element._calculateHasParent(changeId, relatedChanges),
- false);
-
- relatedChanges.push({change_id: 123});
- assert.equal(element._calculateHasParent(changeId, relatedChanges),
- false);
-
- relatedChanges.push({change_id: 234});
- assert.equal(element._calculateHasParent(changeId, relatedChanges),
- true);
- });
-
- suite('hidden attribute and update event', () => {
- const changes = [{
- project: 'foo/bar',
- change_id: 'Ideadbeef',
- commit: {
- commit: 'deadbeef',
- parents: [{commit: 'abc123'}],
- author: {},
- subject: 'do that thing',
- },
- _change_number: 12345,
- _revision_number: 1,
- _current_revision_number: 1,
- status: 'NEW',
- }];
-
- test('clear and empties', () => {
- element._relatedResponse = {changes};
- element._submittedTogether = {changes};
- element._conflicts = changes;
- element._cherryPicks = changes;
- element._sameTopic = changes;
-
- element.hidden = false;
- element.clear();
- assert.isTrue(element.hidden);
- assert.equal(element._relatedResponse.changes.length, 0);
- assert.equal(element._submittedTogether.changes.length, 0);
- assert.equal(element._conflicts.length, 0);
- assert.equal(element._cherryPicks.length, 0);
- assert.equal(element._sameTopic.length, 0);
- });
-
- test('update fires', () => {
- const updateHandler = sandbox.stub();
- element.addEventListener('update', updateHandler);
-
- element._resultsChanged({}, {}, [], [], []);
- assert.isTrue(element.hidden);
- assert.isFalse(updateHandler.called);
-
- element._resultsChanged({}, {}, [], [], ['test']);
- assert.isFalse(element.hidden);
- assert.isTrue(updateHandler.called);
- });
-
- suite('hiding and unhiding', () => {
- test('related response', () => {
- assert.isTrue(element.hidden);
- element._resultsChanged({changes}, {}, [], [], []);
- assert.isFalse(element.hidden);
- });
-
- test('submitted together', () => {
- assert.isTrue(element.hidden);
- element._resultsChanged({}, {changes}, [], [], []);
- assert.isFalse(element.hidden);
- });
-
- test('conflicts', () => {
- assert.isTrue(element.hidden);
- element._resultsChanged({}, {}, changes, [], []);
- assert.isFalse(element.hidden);
- });
-
- test('cherrypicks', () => {
- assert.isTrue(element.hidden);
- element._resultsChanged({}, {}, [], changes, []);
- assert.isFalse(element.hidden);
- });
-
- test('same topic', () => {
- assert.isTrue(element.hidden);
- element._resultsChanged({}, {}, [], [], changes);
- assert.isFalse(element.hidden);
- });
- });
- });
-
- test('_computeChangeURL uses Gerrit.Nav', () => {
- const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForChangeById');
- element._computeChangeURL(123, 'abc/def', 12);
- assert.isTrue(getUrlStub.called);
- });
-
- suite('submitted together changes', () => {
- const change = {
- project: 'foo/bar',
- change_id: 'Ideadbeef',
- commit: {
- commit: 'deadbeef',
- parents: [{commit: 'abc123'}],
- author: {},
- subject: 'do that thing',
- },
- _change_number: 12345,
- _revision_number: 1,
- _current_revision_number: 1,
+ test('_conflicts are an empty array', () => {
+ element.patchNum = 7;
+ element.change = {
+ change_id: 123,
status: 'NEW',
};
+ element.mergeable = true;
+ element.reload();
+ assert.equal(element._conflicts.length, 0);
+ });
+ });
- test('_computeSubmittedTogetherClass', () => {
- assert.strictEqual(
- element._computeSubmittedTogetherClass(undefined),
- 'hidden');
- assert.strictEqual(
- element._computeSubmittedTogetherClass({changes: []}),
- 'hidden');
- assert.strictEqual(
- element._computeSubmittedTogetherClass({changes: [{}]}),
- '');
- assert.strictEqual(
- element._computeSubmittedTogetherClass({
- changes: [],
- non_visible_changes: 0,
- }),
- 'hidden');
- assert.strictEqual(
- element._computeSubmittedTogetherClass({
- changes: [],
- non_visible_changes: 1,
- }),
- '');
- assert.strictEqual(
- element._computeSubmittedTogetherClass({
- changes: [{}],
- non_visible_changes: 1,
- }),
- '');
+ suite('get conflicts tests', () => {
+ let element;
+ let conflictsStub;
+
+ setup(() => {
+ element = fixture('basic');
+
+ sandbox.stub(element, '_getRelatedChanges')
+ .returns(Promise.resolve({changes: []}));
+ sandbox.stub(element, '_getSubmittedTogether')
+ .returns(Promise.resolve());
+ sandbox.stub(element, '_getCherryPicks')
+ .returns(Promise.resolve());
+ conflictsStub = sandbox.stub(element, '_getConflicts')
+ .returns(Promise.resolve());
+ });
+
+ test('request conflicts if open and mergeable', () => {
+ element.patchNum = 7;
+ element.change = {
+ change_id: 123,
+ status: 'NEW',
+ };
+ element.mergeable = true;
+ element.reload();
+ assert.isTrue(conflictsStub.called);
+ });
+
+ test('does not request conflicts if closed and mergeable', () => {
+ element.patchNum = 7;
+ element.change = {
+ change_id: 123,
+ status: 'MERGED',
+ };
+ element.reload();
+ assert.isFalse(conflictsStub.called);
+ });
+
+ test('does not request conflicts if open and not mergeable', () => {
+ element.patchNum = 7;
+ element.change = {
+ change_id: 123,
+ status: 'NEW',
+ };
+ element.mergeable = false;
+ element.reload();
+ assert.isFalse(conflictsStub.called);
+ });
+
+ test('doesnt request conflicts if closed and not mergeable', () => {
+ element.patchNum = 7;
+ element.change = {
+ change_id: 123,
+ status: 'MERGED',
+ };
+ element.mergeable = false;
+ element.reload();
+ assert.isFalse(conflictsStub.called);
+ });
+ });
+
+ test('_calculateHasParent', () => {
+ const changeId = 123;
+ const relatedChanges = [];
+
+ assert.equal(element._calculateHasParent(changeId, relatedChanges),
+ false);
+
+ relatedChanges.push({change_id: 123});
+ assert.equal(element._calculateHasParent(changeId, relatedChanges),
+ false);
+
+ relatedChanges.push({change_id: 234});
+ assert.equal(element._calculateHasParent(changeId, relatedChanges),
+ true);
+ });
+
+ suite('hidden attribute and update event', () => {
+ const changes = [{
+ project: 'foo/bar',
+ change_id: 'Ideadbeef',
+ commit: {
+ commit: 'deadbeef',
+ parents: [{commit: 'abc123'}],
+ author: {},
+ subject: 'do that thing',
+ },
+ _change_number: 12345,
+ _revision_number: 1,
+ _current_revision_number: 1,
+ status: 'NEW',
+ }];
+
+ test('clear and empties', () => {
+ element._relatedResponse = {changes};
+ element._submittedTogether = {changes};
+ element._conflicts = changes;
+ element._cherryPicks = changes;
+ element._sameTopic = changes;
+
+ element.hidden = false;
+ element.clear();
+ assert.isTrue(element.hidden);
+ assert.equal(element._relatedResponse.changes.length, 0);
+ assert.equal(element._submittedTogether.changes.length, 0);
+ assert.equal(element._conflicts.length, 0);
+ assert.equal(element._cherryPicks.length, 0);
+ assert.equal(element._sameTopic.length, 0);
+ });
+
+ test('update fires', () => {
+ const updateHandler = sandbox.stub();
+ element.addEventListener('update', updateHandler);
+
+ element._resultsChanged({}, {}, [], [], []);
+ assert.isTrue(element.hidden);
+ assert.isFalse(updateHandler.called);
+
+ element._resultsChanged({}, {}, [], [], ['test']);
+ assert.isFalse(element.hidden);
+ assert.isTrue(updateHandler.called);
+ });
+
+ suite('hiding and unhiding', () => {
+ test('related response', () => {
+ assert.isTrue(element.hidden);
+ element._resultsChanged({changes}, {}, [], [], []);
+ assert.isFalse(element.hidden);
});
- test('no submitted together changes', () => {
- flushAsynchronousOperations();
- assert.include(element.$.submittedTogether.className, 'hidden');
+ test('submitted together', () => {
+ assert.isTrue(element.hidden);
+ element._resultsChanged({}, {changes}, [], [], []);
+ assert.isFalse(element.hidden);
});
- test('no non-visible submitted together changes', () => {
- element._submittedTogether = {changes: [change]};
- flushAsynchronousOperations();
- assert.notInclude(element.$.submittedTogether.className, 'hidden');
- assert.isNull(element.shadowRoot
- .querySelector('.note'));
+ test('conflicts', () => {
+ assert.isTrue(element.hidden);
+ element._resultsChanged({}, {}, changes, [], []);
+ assert.isFalse(element.hidden);
});
- test('no visible submitted together changes', () => {
- // Technically this should never happen, but worth asserting the logic.
- element._submittedTogether = {changes: [], non_visible_changes: 1};
- flushAsynchronousOperations();
- assert.notInclude(element.$.submittedTogether.className, 'hidden');
- assert.isNotNull(element.shadowRoot
- .querySelector('.note'));
- assert.strictEqual(
- element.shadowRoot
- .querySelector('.note').innerText, '(+ 1 non-visible change)');
+ test('cherrypicks', () => {
+ assert.isTrue(element.hidden);
+ element._resultsChanged({}, {}, [], changes, []);
+ assert.isFalse(element.hidden);
});
- test('visible and non-visible submitted together changes', () => {
- element._submittedTogether = {changes: [change], non_visible_changes: 2};
- flushAsynchronousOperations();
- assert.notInclude(element.$.submittedTogether.className, 'hidden');
- assert.isNotNull(element.shadowRoot
- .querySelector('.note'));
- assert.strictEqual(
- element.shadowRoot
- .querySelector('.note').innerText, '(+ 2 non-visible changes)');
+ test('same topic', () => {
+ assert.isTrue(element.hidden);
+ element._resultsChanged({}, {}, [], [], changes);
+ assert.isFalse(element.hidden);
});
});
});
+
+ test('_computeChangeURL uses Gerrit.Nav', () => {
+ const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForChangeById');
+ element._computeChangeURL(123, 'abc/def', 12);
+ assert.isTrue(getUrlStub.called);
+ });
+
+ suite('submitted together changes', () => {
+ const change = {
+ project: 'foo/bar',
+ change_id: 'Ideadbeef',
+ commit: {
+ commit: 'deadbeef',
+ parents: [{commit: 'abc123'}],
+ author: {},
+ subject: 'do that thing',
+ },
+ _change_number: 12345,
+ _revision_number: 1,
+ _current_revision_number: 1,
+ status: 'NEW',
+ };
+
+ test('_computeSubmittedTogetherClass', () => {
+ assert.strictEqual(
+ element._computeSubmittedTogetherClass(undefined),
+ 'hidden');
+ assert.strictEqual(
+ element._computeSubmittedTogetherClass({changes: []}),
+ 'hidden');
+ assert.strictEqual(
+ element._computeSubmittedTogetherClass({changes: [{}]}),
+ '');
+ assert.strictEqual(
+ element._computeSubmittedTogetherClass({
+ changes: [],
+ non_visible_changes: 0,
+ }),
+ 'hidden');
+ assert.strictEqual(
+ element._computeSubmittedTogetherClass({
+ changes: [],
+ non_visible_changes: 1,
+ }),
+ '');
+ assert.strictEqual(
+ element._computeSubmittedTogetherClass({
+ changes: [{}],
+ non_visible_changes: 1,
+ }),
+ '');
+ });
+
+ test('no submitted together changes', () => {
+ flushAsynchronousOperations();
+ assert.include(element.$.submittedTogether.className, 'hidden');
+ });
+
+ test('no non-visible submitted together changes', () => {
+ element._submittedTogether = {changes: [change]};
+ flushAsynchronousOperations();
+ assert.notInclude(element.$.submittedTogether.className, 'hidden');
+ assert.isNull(element.shadowRoot
+ .querySelector('.note'));
+ });
+
+ test('no visible submitted together changes', () => {
+ // Technically this should never happen, but worth asserting the logic.
+ element._submittedTogether = {changes: [], non_visible_changes: 1};
+ flushAsynchronousOperations();
+ assert.notInclude(element.$.submittedTogether.className, 'hidden');
+ assert.isNotNull(element.shadowRoot
+ .querySelector('.note'));
+ assert.strictEqual(
+ element.shadowRoot
+ .querySelector('.note').innerText, '(+ 1 non-visible change)');
+ });
+
+ test('visible and non-visible submitted together changes', () => {
+ element._submittedTogether = {changes: [change], non_visible_changes: 2};
+ flushAsynchronousOperations();
+ assert.notInclude(element.$.submittedTogether.className, 'hidden');
+ assert.isNotNull(element.shadowRoot
+ .querySelector('.note'));
+ assert.strictEqual(
+ element.shadowRoot
+ .querySelector('.note').innerText, '(+ 2 non-visible changes)');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
index 5fd3795..0c9108c 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
@@ -19,17 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reply-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="../../plugins/gr-plugin-host/gr-plugin-host.html">
-<link rel="import" href="gr-reply-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -43,130 +36,133 @@
</template>
</test-fixture>
-<script>
- suite('gr-reply-dialog tests', async () => {
- await readyToTest();
- let element;
- let changeNum;
- let patchNum;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../plugins/gr-plugin-host/gr-plugin-host.js';
+import './gr-reply-dialog.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-reply-dialog tests', () => {
+ let element;
+ let changeNum;
+ let patchNum;
- let sandbox;
+ let sandbox;
- const setupElement = element => {
- element.change = {
- _number: changeNum,
- labels: {
- 'Verified': {
- values: {
- '-1': 'Fails',
- ' 0': 'No score',
- '+1': 'Verified',
- },
- default_value: 0,
+ const setupElement = element => {
+ element.change = {
+ _number: changeNum,
+ labels: {
+ 'Verified': {
+ values: {
+ '-1': 'Fails',
+ ' 0': 'No score',
+ '+1': 'Verified',
},
- 'Code-Review': {
- values: {
- '-2': 'Do not submit',
- '-1': 'I would prefer that you didn\'t submit this',
- ' 0': 'No score',
- '+1': 'Looks good to me, but someone else must approve',
- '+2': 'Looks good to me, approved',
- },
- all: [{_account_id: 42, value: 0}],
- default_value: 0,
- },
+ default_value: 0,
},
- };
- element.patchNum = patchNum;
- element.permittedLabels = {
- 'Code-Review': [
- '-1',
- ' 0',
- '+1',
- ],
- 'Verified': [
- '-1',
- ' 0',
- '+1',
- ],
- };
- sandbox.stub(element, 'fetchChangeUpdates')
- .returns(Promise.resolve({isLatest: true}));
+ 'Code-Review': {
+ values: {
+ '-2': 'Do not submit',
+ '-1': 'I would prefer that you didn\'t submit this',
+ ' 0': 'No score',
+ '+1': 'Looks good to me, but someone else must approve',
+ '+2': 'Looks good to me, approved',
+ },
+ all: [{_account_id: 42, value: 0}],
+ default_value: 0,
+ },
+ },
};
+ element.patchNum = patchNum;
+ element.permittedLabels = {
+ 'Code-Review': [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ 'Verified': [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ };
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
+ };
- setup(() => {
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ sandbox = sinon.sandbox.create();
- changeNum = 42;
- patchNum = 1;
+ changeNum = 42;
+ patchNum = 1;
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getAccount() { return Promise.resolve({_account_id: 42}); },
- });
-
- element = fixture('basic');
- setupElement(element);
-
- // Allow the elements created by dom-repeat to be stamped.
- flushAsynchronousOperations();
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getAccount() { return Promise.resolve({_account_id: 42}); },
});
- teardown(() => {
- sandbox.restore();
- });
+ element = fixture('basic');
+ setupElement(element);
- test('_submit blocked when invalid email is supplied to ccs', () => {
- const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve());
- // Stub the below function to avoid side effects from the send promise
- // resolving.
- sandbox.stub(element, '_purgeReviewersPendingRemove');
+ // Allow the elements created by dom-repeat to be stamped.
+ flushAsynchronousOperations();
+ });
- element.$.ccs.$.entry.setText('test');
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button.send'));
- assert.isFalse(sendStub.called);
- flushAsynchronousOperations();
+ teardown(() => {
+ sandbox.restore();
+ });
- element.$.ccs.$.entry.setText('test@test.test');
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button.send'));
- assert.isTrue(sendStub.called);
- });
+ test('_submit blocked when invalid email is supplied to ccs', () => {
+ const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve());
+ // Stub the below function to avoid side effects from the send promise
+ // resolving.
+ sandbox.stub(element, '_purgeReviewersPendingRemove');
- test('lgtm plugin', done => {
- Gerrit._testOnly_resetPlugins();
- const pluginHost = fixture('plugin-host');
- pluginHost.config = {
- plugin: {
- js_resource_paths: [],
- html_resource_paths: [
- new URL('test/plugin.html?' + Math.random(),
- window.location.href).toString(),
- ],
- },
- };
- element = fixture('basic');
- setupElement(element);
- const importSpy =
- sandbox.spy(element.shadowRoot
- .querySelector('gr-endpoint-decorator'), '_import');
- Gerrit.awaitPluginsLoaded().then(() => {
- Promise.all(importSpy.returnValues).then(() => {
- flush(() => {
- const textarea = element.$.textarea.getNativeTextarea();
- textarea.value = 'LGTM';
- textarea.dispatchEvent(new CustomEvent(
- 'input', {bubbles: true, composed: true}));
- const labelScoreRows = Polymer.dom(element.$.labelScores.root)
- .querySelector('gr-label-score-row[name="Code-Review"]');
- const selectedBtn = Polymer.dom(labelScoreRows.root)
- .querySelector('gr-button[data-value="+1"].iron-selected');
- assert.isOk(selectedBtn);
- done();
- });
+ element.$.ccs.$.entry.setText('test');
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button.send'));
+ assert.isFalse(sendStub.called);
+ flushAsynchronousOperations();
+
+ element.$.ccs.$.entry.setText('test@test.test');
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button.send'));
+ assert.isTrue(sendStub.called);
+ });
+
+ test('lgtm plugin', done => {
+ Gerrit._testOnly_resetPlugins();
+ const pluginHost = fixture('plugin-host');
+ pluginHost.config = {
+ plugin: {
+ js_resource_paths: [],
+ html_resource_paths: [
+ new URL('test/plugin.html?' + Math.random(),
+ window.location.href).toString(),
+ ],
+ },
+ };
+ element = fixture('basic');
+ setupElement(element);
+ const importSpy =
+ sandbox.spy(element.shadowRoot
+ .querySelector('gr-endpoint-decorator'), '_import');
+ Gerrit.awaitPluginsLoaded().then(() => {
+ Promise.all(importSpy.returnValues).then(() => {
+ flush(() => {
+ const textarea = element.$.textarea.getNativeTextarea();
+ textarea.value = 'LGTM';
+ textarea.dispatchEvent(new CustomEvent(
+ 'input', {bubbles: true, composed: true}));
+ const labelScoreRows = dom(element.$.labelScores.root)
+ .querySelector('gr-label-score-row[name="Code-Review"]');
+ const selectedBtn = dom(labelScoreRows.root)
+ .querySelector('gr-button[data-value="+1"].iron-selected');
+ assert.isOk(selectedBtn);
+ done();
});
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
deleted file mode 100644
index 8424b5d..0000000
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ /dev/null
@@ -1,325 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
-<link rel="import" href="../../shared/gr-textarea/gr-textarea.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-storage/gr-storage.html">
-<link rel="import" href="../../shared/gr-account-list/gr-account-list.html">
-<link rel="import" href="../gr-label-scores/gr-label-scores.html">
-<link rel="import" href="../gr-thread-list/gr-thread-list.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../change/gr-comment-list/gr-comment-list.html">
-<script src="../../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
-<script src="../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js"></script>
-
-<dom-module id="gr-reply-dialog">
- <template>
- <style include="shared-styles">
- :host {
- background-color: var(--dialog-background-color);
- display: block;
- max-height: 90vh;
- }
- :host([disabled]) {
- pointer-events: none;
- }
- :host([disabled]) .container {
- opacity: .5;
- }
- .container {
- display: flex;
- flex-direction: column;
- max-height: 100%;
- }
- section {
- border-top: 1px solid var(--border-color);
- flex-shrink: 0;
- padding: var(--spacing-m) var(--spacing-xl);
- width: 100%;
- }
- section.labelsContainer {
- /* We want the :hover highlight to extend to the border of the dialog. */
- padding: var(--spacing-m) 0;
- }
- .actions {
- background-color: var(--dialog-background-color);
- bottom: 0;
- display: flex;
- justify-content: space-between;
- position: sticky;
- /* @see Issue 8602 */
- z-index: 1;
- }
- .actions .right gr-button {
- margin-left: var(--spacing-l);
- }
- .peopleContainer,
- .labelsContainer {
- flex-shrink: 0;
- }
- .peopleContainer {
- border-top: none;
- display: table;
- }
- .peopleList {
- display: flex;
- }
- .peopleListLabel {
- color: var(--deemphasized-text-color);
- margin-top: var(--spacing-xs);
- min-width: 6em;
- padding-right: var(--spacing-m);
- }
- gr-account-list {
- display: flex;
- flex-wrap: wrap;
- flex: 1;
- }
- #reviewerConfirmationOverlay {
- padding: var(--spacing-l);
- text-align: center;
- }
- .reviewerConfirmationButtons {
- margin-top: var(--spacing-l);
- }
- .groupName {
- font-weight: var(--font-weight-bold);
- }
- .groupSize {
- font-style: italic;
- }
- .textareaContainer {
- min-height: 12em;
- position: relative;
- }
- .textareaContainer,
- #textarea,
- gr-endpoint-decorator {
- display: flex;
- width: 100%;
- }
- gr-endpoint-decorator[name="reply-label-scores"] {
- display: block;
- }
- .previewContainer gr-formatted-text {
- background: var(--table-header-background-color);
- padding: var(--spacing-l);
- }
- .draftsContainer h3 {
- margin-top: var(--spacing-xs);
- }
- #checkingStatusLabel,
- #notLatestLabel {
- margin-left: var(--spacing-l);
- }
- #checkingStatusLabel {
- color: var(--deemphasized-text-color);
- font-style: italic;
- }
- #notLatestLabel,
- #savingLabel {
- color: var(--error-text-color);
- }
- #savingLabel {
- display: none;
- }
- #savingLabel.saving {
- display: inline;
- }
- #pluginMessage {
- color: var(--deemphasized-text-color);
- margin-left: var(--spacing-l);
- margin-bottom: var(--spacing-m);
- }
- #pluginMessage:empty {
- display: none;
- }
- </style>
- <div class="container" tabindex="-1">
- <section class="peopleContainer">
- <div class="peopleList">
- <div class="peopleListLabel">Reviewers</div>
- <gr-account-list
- id="reviewers"
- accounts="{{_reviewers}}"
- removable-values="[[change.removable_reviewers]]"
- filter="[[filterReviewerSuggestion]]"
- pending-confirmation="{{_reviewerPendingConfirmation}}"
- placeholder="Add reviewer..."
- on-account-text-changed="_handleAccountTextEntry"
- suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
- </gr-account-list>
- </div>
- <div class="peopleList">
- <div class="peopleListLabel">CC</div>
- <gr-account-list
- id="ccs"
- accounts="{{_ccs}}"
- filter="[[filterCCSuggestion]]"
- pending-confirmation="{{_ccPendingConfirmation}}"
- allow-any-input
- placeholder="Add CC..."
- on-account-text-changed="_handleAccountTextEntry"
- suggestions-provider="[[_getCcSuggestionsProvider(change)]]">
- </gr-account-list>
- </div>
- <gr-overlay
- id="reviewerConfirmationOverlay"
- on-iron-overlay-canceled="_cancelPendingReviewer">
- <div class="reviewerConfirmation">
- Group
- <span class="groupName">
- [[_pendingConfirmationDetails.group.name]]
- </span>
- has
- <span class="groupSize">
- [[_pendingConfirmationDetails.count]]
- </span>
- members.
- <br>
- Are you sure you want to add them all?
- </div>
- <div class="reviewerConfirmationButtons">
- <gr-button on-click="_confirmPendingReviewer">Yes</gr-button>
- <gr-button on-click="_cancelPendingReviewer">No</gr-button>
- </div>
- </gr-overlay>
- </section>
- <section class="textareaContainer">
- <gr-endpoint-decorator name="reply-text">
- <gr-textarea
- id="textarea"
- class="message"
- autocomplete="on"
- placeholder=[[_messagePlaceholder]]
- fixed-position-dropdown
- hide-border="true"
- monospace="true"
- disabled="{{disabled}}"
- rows="4"
- text="{{draft}}"
- on-bind-value-changed="_handleHeightChanged">
- </gr-textarea>
- </gr-endpoint-decorator>
- </section>
- <section class="previewContainer">
- <label>
- <input type="checkbox" checked="{{_previewFormatting::change}}">
- Preview formatting
- </label>
- <gr-formatted-text
- content="[[draft]]"
- hidden$="[[!_previewFormatting]]"
- config="[[projectConfig.commentlinks]]"></gr-formatted-text>
- </section>
- <section class="labelsContainer">
- <gr-endpoint-decorator name="reply-label-scores">
- <gr-label-scores
- id="labelScores"
- account="[[_account]]"
- change="[[change]]"
- on-labels-changed="_handleLabelsChanged"
- permitted-labels=[[permittedLabels]]></gr-label-scores>
- </gr-endpoint-decorator>
- <div id="pluginMessage">[[_pluginMessage]]</div>
- </section>
- <section class="draftsContainer" hidden$="[[_computeHideDraftList(draftCommentThreads)]]">
- <div class="includeComments">
- <input type="checkbox" id="includeComments"
- checked="{{_includeComments::change}}">
- <label for="includeComments">Publish [[_computeDraftsTitle(draftCommentThreads)]]</label>
- </div>
- <gr-thread-list
- id="commentList"
- hidden$="[[!_includeComments]]"
- threads="[[draftCommentThreads]]"
- change="[[change]]"
- change-num="[[change._number]]"
- logged-in="true"
- hide-toggle-buttons
- on-thread-list-modified="_onThreadListModified">
- </gr-thread-list>
- <span
- id="savingLabel"
- class$="[[_computeSavingLabelClass(_savingComments)]]">
- Saving comments...
- </span>
- </section>
- <section class="actions">
- <div class="left">
- <span
- id="checkingStatusLabel"
- hidden$="[[!_isState(knownLatestState, 'checking')]]">
- Checking whether patch [[patchNum]] is latest...
- </span>
- <span
- id="notLatestLabel"
- hidden$="[[!_isState(knownLatestState, 'not-latest')]]">
- [[_computePatchSetWarning(patchNum, _labelsChanged)]]
- <gr-button link on-click="_reload">Reload</gr-button>
- </span>
- </div>
- <div class="right">
- <gr-button
- link
- id="cancelButton"
- class="action cancel"
- on-click="_cancelTapHandler">Cancel</gr-button>
- <template is="dom-if" if="[[canBeStarted]]">
- <!-- Use 'Send' here as the change may only about reviewers / ccs
- and when this button is visible, the next button will always
- be 'Start review' -->
- <gr-button
- link
- disabled="[[_isState(knownLatestState, 'not-latest')]]"
- class="action save"
- has-tooltip
- title="[[_saveTooltip]]"
- on-click="_saveClickHandler">Save</gr-button>
- </template>
- <gr-button
- id="sendButton"
- primary
- disabled="[[_sendDisabled]]"
- class="action send"
- has-tooltip
- title$="[[_computeSendButtonTooltip(canBeStarted)]]"
- on-click="_sendTapHandler">[[_sendButtonLabel]]</gr-button>
- </div>
- </section>
- </div>
- <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- <gr-storage id="storage"></gr-storage>
- <gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="gr-reply-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index b1a05f5..305505d 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,887 +14,916 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../shared/gr-account-chip/gr-account-chip.js';
+import '../../shared/gr-textarea/gr-textarea.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-formatted-text/gr-formatted-text.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-storage/gr-storage.js';
+import '../../shared/gr-account-list/gr-account-list.js';
+import '../gr-label-scores/gr-label-scores.js';
+import '../gr-thread-list/gr-thread-list.js';
+import '../../../styles/shared-styles.js';
+import '../gr-comment-list/gr-comment-list.js';
+import '../../../scripts/gr-display-name-utils/gr-display-name-utils.js';
+import '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-reply-dialog_html.js';
- const FocusTarget = {
- ANY: 'any',
- BODY: 'body',
- CCS: 'cc',
- REVIEWERS: 'reviewers',
- };
+const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
- const ReviewerTypes = {
- REVIEWER: 'REVIEWER',
- CC: 'CC',
- };
+const FocusTarget = {
+ ANY: 'any',
+ BODY: 'body',
+ CCS: 'cc',
+ REVIEWERS: 'reviewers',
+};
- const LatestPatchState = {
- LATEST: 'latest',
- CHECKING: 'checking',
- NOT_LATEST: 'not-latest',
- };
+const ReviewerTypes = {
+ REVIEWER: 'REVIEWER',
+ CC: 'CC',
+};
- const ButtonLabels = {
- START_REVIEW: 'Start review',
- SEND: 'Send',
- };
+const LatestPatchState = {
+ LATEST: 'latest',
+ CHECKING: 'checking',
+ NOT_LATEST: 'not-latest',
+};
- const ButtonTooltips = {
- SAVE: 'Save but do not send notification or change review state',
- START_REVIEW: 'Mark as ready for review and send reply',
- SEND: 'Send reply',
- };
+const ButtonLabels = {
+ START_REVIEW: 'Start review',
+ SEND: 'Send',
+};
- const EMPTY_REPLY_MESSAGE = 'Cannot send an empty reply.';
+const ButtonTooltips = {
+ SAVE: 'Save but do not send notification or change review state',
+ START_REVIEW: 'Mark as ready for review and send reply',
+ SEND: 'Send reply',
+};
- const SEND_REPLY_TIMING_LABEL = 'SendReply';
+const EMPTY_REPLY_MESSAGE = 'Cannot send an empty reply.';
+
+const SEND_REPLY_TIMING_LABEL = 'SendReply';
+
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrReplyDialog extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-reply-dialog'; }
+ /**
+ * Fired when a reply is successfully sent.
+ *
+ * @event send
+ */
/**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * Fired when the user presses the cancel button.
+ *
+ * @event cancel
*/
- class GrReplyDialog extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-reply-dialog'; }
+
+ /**
+ * Fired when the main textarea's value changes, which may have triggered
+ * a change in size for the dialog.
+ *
+ * @event autogrow
+ */
+
+ /**
+ * Fires to show an alert when a send is attempted on the non-latest patch.
+ *
+ * @event show-alert
+ */
+
+ /**
+ * Fires when the reply dialog believes that the server side diff drafts
+ * have been updated and need to be refreshed.
+ *
+ * @event comment-refresh
+ */
+
+ /**
+ * Fires when the state of the send button (enabled/disabled) changes.
+ *
+ * @event send-disabled-changed
+ */
+
+ constructor() {
+ super();
+ this.FocusTarget = FocusTarget;
+ }
+
+ static get properties() {
+ return {
/**
- * Fired when a reply is successfully sent.
- *
- * @event send
+ * @type {{ _number: number, removable_reviewers: Array }}
*/
-
- /**
- * Fired when the user presses the cancel button.
- *
- * @event cancel
- */
-
- /**
- * Fired when the main textarea's value changes, which may have triggered
- * a change in size for the dialog.
- *
- * @event autogrow
- */
-
- /**
- * Fires to show an alert when a send is attempted on the non-latest patch.
- *
- * @event show-alert
- */
-
- /**
- * Fires when the reply dialog believes that the server side diff drafts
- * have been updated and need to be refreshed.
- *
- * @event comment-refresh
- */
-
- /**
- * Fires when the state of the send button (enabled/disabled) changes.
- *
- * @event send-disabled-changed
- */
-
- constructor() {
- super();
- this.FocusTarget = FocusTarget;
- }
-
- static get properties() {
- return {
+ change: Object,
+ patchNum: String,
+ canBeStarted: {
+ type: Boolean,
+ value: false,
+ },
+ disabled: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ draft: {
+ type: String,
+ value: '',
+ observer: '_draftChanged',
+ },
+ quote: {
+ type: String,
+ value: '',
+ },
+ /** @type {!Function} */
+ filterReviewerSuggestion: {
+ type: Function,
+ value() {
+ return this._filterReviewerSuggestionGenerator(false);
+ },
+ },
+ /** @type {!Function} */
+ filterCCSuggestion: {
+ type: Function,
+ value() {
+ return this._filterReviewerSuggestionGenerator(true);
+ },
+ },
+ permittedLabels: Object,
/**
- * @type {{ _number: number, removable_reviewers: Array }}
+ * @type {{ commentlinks: Array }}
*/
- change: Object,
- patchNum: String,
- canBeStarted: {
- type: Boolean,
- value: false,
- },
- disabled: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- draft: {
- type: String,
- value: '',
- observer: '_draftChanged',
- },
- quote: {
- type: String,
- value: '',
- },
- /** @type {!Function} */
- filterReviewerSuggestion: {
- type: Function,
- value() {
- return this._filterReviewerSuggestionGenerator(false);
- },
- },
- /** @type {!Function} */
- filterCCSuggestion: {
- type: Function,
- value() {
- return this._filterReviewerSuggestionGenerator(true);
- },
- },
- permittedLabels: Object,
- /**
- * @type {{ commentlinks: Array }}
- */
- projectConfig: Object,
- knownLatestState: String,
- underReview: {
- type: Boolean,
- value: true,
- },
+ projectConfig: Object,
+ knownLatestState: String,
+ underReview: {
+ type: Boolean,
+ value: true,
+ },
- _account: Object,
- _ccs: Array,
- /** @type {?Object} */
- _ccPendingConfirmation: {
- type: Object,
- observer: '_reviewerPendingConfirmationUpdated',
+ _account: Object,
+ _ccs: Array,
+ /** @type {?Object} */
+ _ccPendingConfirmation: {
+ type: Object,
+ observer: '_reviewerPendingConfirmationUpdated',
+ },
+ _messagePlaceholder: {
+ type: String,
+ computed: '_computeMessagePlaceholder(canBeStarted)',
+ },
+ _owner: Object,
+ /** @type {?} */
+ _pendingConfirmationDetails: Object,
+ _includeComments: {
+ type: Boolean,
+ value: true,
+ },
+ _reviewers: Array,
+ /** @type {?Object} */
+ _reviewerPendingConfirmation: {
+ type: Object,
+ observer: '_reviewerPendingConfirmationUpdated',
+ },
+ _previewFormatting: {
+ type: Boolean,
+ value: false,
+ observer: '_handleHeightChanged',
+ },
+ _reviewersPendingRemove: {
+ type: Object,
+ value: {
+ CC: [],
+ REVIEWER: [],
},
- _messagePlaceholder: {
- type: String,
- computed: '_computeMessagePlaceholder(canBeStarted)',
- },
- _owner: Object,
- /** @type {?} */
- _pendingConfirmationDetails: Object,
- _includeComments: {
- type: Boolean,
- value: true,
- },
- _reviewers: Array,
- /** @type {?Object} */
- _reviewerPendingConfirmation: {
- type: Object,
- observer: '_reviewerPendingConfirmationUpdated',
- },
- _previewFormatting: {
- type: Boolean,
- value: false,
- observer: '_handleHeightChanged',
- },
- _reviewersPendingRemove: {
- type: Object,
- value: {
- CC: [],
- REVIEWER: [],
- },
- },
- _sendButtonLabel: {
- type: String,
- computed: '_computeSendButtonLabel(canBeStarted)',
- },
- _savingComments: Boolean,
- _reviewersMutated: {
- type: Boolean,
- value: false,
- },
- _labelsChanged: {
- type: Boolean,
- value: false,
- },
- _saveTooltip: {
- type: String,
- value: ButtonTooltips.SAVE,
- readOnly: true,
- },
- _pluginMessage: {
- type: String,
- value: '',
- },
- _sendDisabled: {
- type: Boolean,
- computed: '_computeSendButtonDisabled(_sendButtonLabel, ' +
- 'draftCommentThreads, draft, _reviewersMutated, _labelsChanged, ' +
- '_includeComments, disabled)',
- observer: '_sendDisabledChanged',
- },
- draftCommentThreads: {
- type: Array,
- observer: '_handleHeightChanged',
- },
- };
- }
+ },
+ _sendButtonLabel: {
+ type: String,
+ computed: '_computeSendButtonLabel(canBeStarted)',
+ },
+ _savingComments: Boolean,
+ _reviewersMutated: {
+ type: Boolean,
+ value: false,
+ },
+ _labelsChanged: {
+ type: Boolean,
+ value: false,
+ },
+ _saveTooltip: {
+ type: String,
+ value: ButtonTooltips.SAVE,
+ readOnly: true,
+ },
+ _pluginMessage: {
+ type: String,
+ value: '',
+ },
+ _sendDisabled: {
+ type: Boolean,
+ computed: '_computeSendButtonDisabled(_sendButtonLabel, ' +
+ 'draftCommentThreads, draft, _reviewersMutated, _labelsChanged, ' +
+ '_includeComments, disabled)',
+ observer: '_sendDisabledChanged',
+ },
+ draftCommentThreads: {
+ type: Array,
+ observer: '_handleHeightChanged',
+ },
+ };
+ }
- get keyBindings() {
- return {
- 'esc': '_handleEscKey',
- 'ctrl+enter meta+enter': '_handleEnterKey',
- };
- }
+ get keyBindings() {
+ return {
+ 'esc': '_handleEscKey',
+ 'ctrl+enter meta+enter': '_handleEnterKey',
+ };
+ }
- static get observers() {
- return [
- '_changeUpdated(change.reviewers.*, change.owner)',
- '_ccsChanged(_ccs.splices)',
- '_reviewersChanged(_reviewers.splices)',
- ];
- }
+ static get observers() {
+ return [
+ '_changeUpdated(change.reviewers.*, change.owner)',
+ '_ccsChanged(_ccs.splices)',
+ '_reviewersChanged(_reviewers.splices)',
+ ];
+ }
- /** @override */
- attached() {
- super.attached();
- this._getAccount().then(account => {
- this._account = account || {};
- });
- }
+ /** @override */
+ attached() {
+ super.attached();
+ this._getAccount().then(account => {
+ this._account = account || {};
+ });
+ }
- /** @override */
- ready() {
- super.ready();
- this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this);
- }
+ /** @override */
+ ready() {
+ super.ready();
+ this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this);
+ }
- open(opt_focusTarget) {
- this.knownLatestState = LatestPatchState.CHECKING;
- this.fetchChangeUpdates(this.change, this.$.restAPI)
- .then(result => {
- this.knownLatestState = result.isLatest ?
- LatestPatchState.LATEST : LatestPatchState.NOT_LATEST;
- });
-
- this._focusOn(opt_focusTarget);
- if (this.quote && this.quote.length) {
- // If a reply quote has been provided, use it and clear the property.
- this.draft = this.quote;
- this.quote = '';
- } else {
- // Otherwise, check for an unsaved draft in localstorage.
- this.draft = this._loadStoredDraft();
- }
- if (this.$.restAPI.hasPendingDiffDrafts()) {
- this._savingComments = true;
- this.$.restAPI.awaitPendingDiffDrafts().then(() => {
- this.fire('comment-refresh');
- this._savingComments = false;
+ open(opt_focusTarget) {
+ this.knownLatestState = LatestPatchState.CHECKING;
+ this.fetchChangeUpdates(this.change, this.$.restAPI)
+ .then(result => {
+ this.knownLatestState = result.isLatest ?
+ LatestPatchState.LATEST : LatestPatchState.NOT_LATEST;
});
- }
+
+ this._focusOn(opt_focusTarget);
+ if (this.quote && this.quote.length) {
+ // If a reply quote has been provided, use it and clear the property.
+ this.draft = this.quote;
+ this.quote = '';
+ } else {
+ // Otherwise, check for an unsaved draft in localstorage.
+ this.draft = this._loadStoredDraft();
}
-
- focus() {
- this._focusOn(FocusTarget.ANY);
- }
-
- getFocusStops() {
- const end = this._sendDisabled ? this.$.cancelButton : this.$.sendButton;
- return {
- start: this.$.reviewers.focusStart,
- end,
- };
- }
-
- setLabelValue(label, value) {
- const selectorEl =
- this.$.labelScores.shadowRoot
- .querySelector(`gr-label-score-row[name="${label}"]`);
- if (!selectorEl) { return; }
- selectorEl.setSelectedValue(value);
- }
-
- getLabelValue(label) {
- const selectorEl =
- this.$.labelScores.shadowRoot
- .querySelector(`gr-label-score-row[name="${label}"]`);
- if (!selectorEl) { return null; }
-
- return selectorEl.selectedValue;
- }
-
- _handleEscKey(e) {
- this.cancel();
- }
-
- _handleEnterKey(e) {
- this._submit();
- }
-
- _ccsChanged(splices) {
- this._reviewerTypeChanged(splices, ReviewerTypes.CC);
- }
-
- _reviewersChanged(splices) {
- this._reviewerTypeChanged(splices, ReviewerTypes.REVIEWER);
- }
-
- _reviewerTypeChanged(splices, reviewerType) {
- if (splices && splices.indexSplices) {
- this._reviewersMutated = true;
- this._processReviewerChange(splices.indexSplices,
- reviewerType);
- let key;
- let index;
- let account;
- // Remove any accounts that already exist as a CC for reviewer
- // or vice versa.
- const isReviewer = ReviewerTypes.REVIEWER === reviewerType;
- for (const splice of splices.indexSplices) {
- for (let i = 0; i < splice.addedCount; i++) {
- account = splice.object[splice.index + i];
- key = this._accountOrGroupKey(account);
- const array = isReviewer ? this._ccs : this._reviewers;
- index = array.findIndex(
- account => this._accountOrGroupKey(account) === key);
- if (index >= 0) {
- this.splice(isReviewer ? '_ccs' : '_reviewers', index, 1);
- const moveFrom = isReviewer ? 'CC' : 'reviewer';
- const moveTo = isReviewer ? 'reviewer' : 'CC';
- const message = (account.name || account.email || key) +
- ` moved from ${moveFrom} to ${moveTo}.`;
- this.fire('show-alert', {message});
- }
- }
- }
- }
- }
-
- _processReviewerChange(indexSplices, type) {
- for (const splice of indexSplices) {
- for (const account of splice.removed) {
- if (!this._reviewersPendingRemove[type]) {
- console.err('Invalid type ' + type + ' for reviewer.');
- return;
- }
- this._reviewersPendingRemove[type].push(account);
- }
- }
- }
-
- /**
- * Resets the state of the _reviewersPendingRemove object, and removes
- * accounts if necessary.
- *
- * @param {boolean} isCancel true if the action is a cancel.
- * @param {Object=} opt_accountIdsTransferred map of account IDs that must
- * not be removed, because they have been readded in another state.
- */
- _purgeReviewersPendingRemove(isCancel, opt_accountIdsTransferred) {
- let reviewerArr;
- const keep = opt_accountIdsTransferred || {};
- for (const type in this._reviewersPendingRemove) {
- if (this._reviewersPendingRemove.hasOwnProperty(type)) {
- if (!isCancel) {
- reviewerArr = this._reviewersPendingRemove[type];
- for (let i = 0; i < reviewerArr.length; i++) {
- if (!keep[reviewerArr[i]._account_id]) {
- this._removeAccount(reviewerArr[i], type);
- }
- }
- }
- this._reviewersPendingRemove[type] = [];
- }
- }
- }
-
- /**
- * Removes an account from the change, both on the backend and the client.
- * Does nothing if the account is a pending addition.
- *
- * @param {!Object} account
- * @param {string} type
- */
- _removeAccount(account, type) {
- if (account._pendingAdd) { return; }
-
- return this.$.restAPI.removeChangeReviewer(this.change._number,
- account._account_id).then(response => {
- if (!response.ok) { return response; }
-
- const reviewers = this.change.reviewers[type] || [];
- for (let i = 0; i < reviewers.length; i++) {
- if (reviewers[i]._account_id == account._account_id) {
- this.splice(`change.reviewers.${type}`, i, 1);
- break;
- }
- }
+ if (this.$.restAPI.hasPendingDiffDrafts()) {
+ this._savingComments = true;
+ this.$.restAPI.awaitPendingDiffDrafts().then(() => {
+ this.fire('comment-refresh');
+ this._savingComments = false;
});
}
-
- _mapReviewer(reviewer) {
- let reviewerId;
- let confirmed;
- if (reviewer.account) {
- reviewerId = reviewer.account._account_id || reviewer.account.email;
- } else if (reviewer.group) {
- reviewerId = reviewer.group.id;
- confirmed = reviewer.group.confirmed;
- }
- return {reviewer: reviewerId, confirmed};
- }
-
- send(includeComments, startReview) {
- this.$.reporting.time(SEND_REPLY_TIMING_LABEL);
- const labels = this.$.labelScores.getLabelValues();
-
- const obj = {
- drafts: includeComments ? 'PUBLISH_ALL_REVISIONS' : 'KEEP',
- labels,
- };
-
- if (startReview) {
- obj.ready = true;
- }
-
- if (this.draft != null) {
- obj.message = this.draft;
- }
-
- const accountAdditions = {};
- obj.reviewers = this.$.reviewers.additions().map(reviewer => {
- if (reviewer.account) {
- accountAdditions[reviewer.account._account_id] = true;
- }
- return this._mapReviewer(reviewer);
- });
- const ccsEl = this.$.ccs;
- if (ccsEl) {
- for (let reviewer of ccsEl.additions()) {
- if (reviewer.account) {
- accountAdditions[reviewer.account._account_id] = true;
- }
- reviewer = this._mapReviewer(reviewer);
- reviewer.state = 'CC';
- obj.reviewers.push(reviewer);
- }
- }
-
- this.disabled = true;
-
- const errFn = this._handle400Error.bind(this);
- return this._saveReview(obj, errFn)
- .then(response => {
- if (!response) {
- // Null or undefined response indicates that an error handler
- // took responsibility, so just return.
- return {};
- }
- if (!response.ok) {
- this.fire('server-error', {response});
- return {};
- }
-
- this.draft = '';
- this._includeComments = true;
- this.fire('send', null, {bubbles: false});
- return accountAdditions;
- })
- .then(result => {
- this.disabled = false;
- return result;
- })
- .catch(err => {
- this.disabled = false;
- throw err;
- });
- }
-
- _focusOn(section) {
- // Safeguard- always want to focus on something.
- if (!section || section === FocusTarget.ANY) {
- section = this._chooseFocusTarget();
- }
- if (section === FocusTarget.BODY) {
- const textarea = this.$.textarea;
- textarea.async(textarea.getNativeTextarea()
- .focus.bind(textarea.getNativeTextarea()));
- } else if (section === FocusTarget.REVIEWERS) {
- const reviewerEntry = this.$.reviewers.focusStart;
- reviewerEntry.async(reviewerEntry.focus);
- } else if (section === FocusTarget.CCS) {
- const ccEntry = this.$.ccs.focusStart;
- ccEntry.async(ccEntry.focus);
- }
- }
-
- _chooseFocusTarget() {
- // If we are the owner and the reviewers field is empty, focus on that.
- if (this._account && this.change && this.change.owner &&
- this._account._account_id === this.change.owner._account_id &&
- (!this._reviewers || this._reviewers.length === 0)) {
- return FocusTarget.REVIEWERS;
- }
-
- // Default to BODY.
- return FocusTarget.BODY;
- }
-
- _handle400Error(response) {
- // A call to _saveReview could fail with a server error if erroneous
- // reviewers were requested. This is signalled with a 400 Bad Request
- // status. The default gr-rest-api-interface error handling would
- // result in a large JSON response body being displayed to the user in
- // the gr-error-manager toast.
- //
- // We can modify the error handling behavior by passing this function
- // through to restAPI as a custom error handling function. Since we're
- // short-circuiting restAPI we can do our own response parsing and fire
- // the server-error ourselves.
- //
- this.disabled = false;
-
- // Using response.clone() here, because getResponseObject() and
- // potentially the generic error handler will want to call text() on the
- // response object, which can only be done once per object.
- const jsonPromise = this.$.restAPI.getResponseObject(response.clone());
- return jsonPromise.then(result => {
- // Only perform custom error handling for 400s and a parseable
- // ReviewResult response.
- if (response.status === 400 && result) {
- const errors = [];
- for (const state of ['reviewers', 'ccs']) {
- if (!result.hasOwnProperty(state)) { continue; }
- for (const reviewer of Object.values(result[state])) {
- if (reviewer.error) {
- errors.push(reviewer.error);
- }
- }
- }
- response = {
- ok: false,
- status: response.status,
- text() { return Promise.resolve(errors.join(', ')); },
- };
- }
- this.fire('server-error', {response});
- return null; // Means that the error has been handled.
- });
- }
-
- _computeHideDraftList(draftCommentThreads) {
- return draftCommentThreads.length === 0;
- }
-
- _computeDraftsTitle(draftCommentThreads) {
- const total = draftCommentThreads.length;
- if (total == 0) { return ''; }
- if (total == 1) { return '1 Draft'; }
- if (total > 1) { return total + ' Drafts'; }
- }
-
- _computeMessagePlaceholder(canBeStarted) {
- return canBeStarted ?
- 'Add a note for your reviewers...' :
- 'Say something nice...';
- }
-
- _changeUpdated(changeRecord, owner) {
- // Polymer 2: check for undefined
- if ([changeRecord, owner].some(arg => arg === undefined)) {
- return;
- }
-
- this._rebuildReviewerArrays(changeRecord.base, owner);
- }
-
- _rebuildReviewerArrays(change, owner) {
- this._owner = owner;
-
- const reviewers = [];
- const ccs = [];
-
- for (const key in change) {
- if (change.hasOwnProperty(key)) {
- if (key !== 'REVIEWER' && key !== 'CC') {
- console.warn('unexpected reviewer state:', key);
- continue;
- }
- for (const entry of change[key]) {
- if (entry._account_id === owner._account_id) {
- continue;
- }
- switch (key) {
- case 'REVIEWER':
- reviewers.push(entry);
- break;
- case 'CC':
- ccs.push(entry);
- break;
- }
- }
- }
- }
-
- this._ccs = ccs;
- this._reviewers = reviewers;
- }
-
- _accountOrGroupKey(entry) {
- return entry.id || entry._account_id;
- }
-
- /**
- * Generates a function to filter out reviewer/CC entries. When isCCs is
- * truthy, the function filters out entries that already exist in this._ccs.
- * When falsy, the function filters entries that exist in this._reviewers.
- *
- * @param {boolean} isCCs
- * @return {!Function}
- */
- _filterReviewerSuggestionGenerator(isCCs) {
- return suggestion => {
- let entry;
- if (suggestion.account) {
- entry = suggestion.account;
- } else if (suggestion.group) {
- entry = suggestion.group;
- } else {
- console.warn(
- 'received suggestion that was neither account nor group:',
- suggestion);
- }
- if (entry._account_id === this._owner._account_id) {
- return false;
- }
-
- const key = this._accountOrGroupKey(entry);
- const finder = entry => this._accountOrGroupKey(entry) === key;
- if (isCCs) {
- return this._ccs.find(finder) === undefined;
- }
- return this._reviewers.find(finder) === undefined;
- };
- }
-
- _getAccount() {
- return this.$.restAPI.getAccount();
- }
-
- _cancelTapHandler(e) {
- e.preventDefault();
- this.cancel();
- }
-
- cancel() {
- this.fire('cancel', null, {bubbles: false});
- this.$.textarea.closeDropdown();
- this._purgeReviewersPendingRemove(true);
- this._rebuildReviewerArrays(this.change.reviewers, this._owner);
- }
-
- _saveClickHandler(e) {
- e.preventDefault();
- if (!this.$.ccs.submitEntryText()) {
- // Do not proceed with the save if there is an invalid email entry in
- // the text field of the CC entry.
- return;
- }
- this.send(this._includeComments, false).then(keepReviewers => {
- this._purgeReviewersPendingRemove(false, keepReviewers);
- });
- }
-
- _sendTapHandler(e) {
- e.preventDefault();
- this._submit();
- }
-
- _submit() {
- if (!this.$.ccs.submitEntryText()) {
- // Do not proceed with the send if there is an invalid email entry in
- // the text field of the CC entry.
- return;
- }
- if (this._sendDisabled) {
- this.dispatchEvent(new CustomEvent('show-alert', {
- bubbles: true,
- composed: true,
- detail: {message: EMPTY_REPLY_MESSAGE},
- }));
- return;
- }
- return this.send(this._includeComments, this.canBeStarted)
- .then(keepReviewers => {
- this._purgeReviewersPendingRemove(false, keepReviewers);
- })
- .catch(err => {
- this.dispatchEvent(new CustomEvent('show-error', {
- bubbles: true,
- composed: true,
- detail: {message: `Error submitting review ${err}`},
- }));
- });
- }
-
- _saveReview(review, opt_errFn) {
- return this.$.restAPI.saveChangeReview(this.change._number, this.patchNum,
- review, opt_errFn);
- }
-
- _reviewerPendingConfirmationUpdated(reviewer) {
- if (reviewer === null) {
- this.$.reviewerConfirmationOverlay.close();
- } else {
- this._pendingConfirmationDetails =
- this._ccPendingConfirmation || this._reviewerPendingConfirmation;
- this.$.reviewerConfirmationOverlay.open();
- }
- }
-
- _confirmPendingReviewer() {
- if (this._ccPendingConfirmation) {
- this.$.ccs.confirmGroup(this._ccPendingConfirmation.group);
- this._focusOn(FocusTarget.CCS);
- } else {
- this.$.reviewers.confirmGroup(this._reviewerPendingConfirmation.group);
- this._focusOn(FocusTarget.REVIEWERS);
- }
- }
-
- _cancelPendingReviewer() {
- this._ccPendingConfirmation = null;
- this._reviewerPendingConfirmation = null;
-
- const target =
- this._ccPendingConfirmation ? FocusTarget.CCS : FocusTarget.REVIEWERS;
- this._focusOn(target);
- }
-
- _getStorageLocation() {
- // Tests trigger this method without setting change.
- if (!this.change) { return {}; }
- return {
- changeNum: this.change._number,
- patchNum: '@change',
- path: '@change',
- };
- }
-
- _loadStoredDraft() {
- const draft = this.$.storage.getDraftComment(this._getStorageLocation());
- return draft ? draft.message : '';
- }
-
- _handleAccountTextEntry() {
- // When either of the account entries has input added to the autocomplete,
- // it should trigger the save button to enable/
- //
- // Note: if the text is removed, the save button will not get disabled.
- this._reviewersMutated = true;
- }
-
- _draftChanged(newDraft, oldDraft) {
- this.debounce('store', () => {
- if (!newDraft.length && oldDraft) {
- // If the draft has been modified to be empty, then erase the storage
- // entry.
- this.$.storage.eraseDraftComment(this._getStorageLocation());
- } else if (newDraft.length) {
- this.$.storage.setDraftComment(this._getStorageLocation(),
- this.draft);
- }
- }, STORAGE_DEBOUNCE_INTERVAL_MS);
- }
-
- _handleHeightChanged(e) {
- this.fire('autogrow');
- }
-
- _handleLabelsChanged() {
- this._labelsChanged = Object.keys(
- this.$.labelScores.getLabelValues()).length !== 0;
- }
-
- _isState(knownLatestState, value) {
- return knownLatestState === value;
- }
-
- _reload() {
- // Load the current change without any patch range.
- Gerrit.Nav.navigateToChange(this.change);
- this.cancel();
- }
-
- _computeSendButtonLabel(canBeStarted) {
- return canBeStarted ? ButtonLabels.START_REVIEW : ButtonLabels.SEND;
- }
-
- _computeSendButtonTooltip(canBeStarted) {
- return canBeStarted ? ButtonTooltips.START_REVIEW : ButtonTooltips.SEND;
- }
-
- _computeSavingLabelClass(savingComments) {
- return savingComments ? 'saving' : '';
- }
-
- _computeSendButtonDisabled(
- buttonLabel, draftCommentThreads, text, reviewersMutated,
- labelsChanged, includeComments, disabled) {
- // Polymer 2: check for undefined
- if ([
- buttonLabel,
- draftCommentThreads,
- text,
- reviewersMutated,
- labelsChanged,
- includeComments,
- disabled,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- if (disabled) { return true; }
- if (buttonLabel === ButtonLabels.START_REVIEW) { return false; }
- const hasDrafts = includeComments && draftCommentThreads.length;
- return !hasDrafts && !text.length && !reviewersMutated && !labelsChanged;
- }
-
- _computePatchSetWarning(patchNum, labelsChanged) {
- let str = `Patch ${patchNum} is not latest.`;
- if (labelsChanged) {
- str += ' Voting on a non-latest patch will have no effect.';
- }
- return str;
- }
-
- setPluginMessage(message) {
- this._pluginMessage = message;
- }
-
- _sendDisabledChanged(sendDisabled) {
- this.dispatchEvent(new CustomEvent('send-disabled-changed'));
- }
-
- _getReviewerSuggestionsProvider(change) {
- const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI,
- change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER);
- provider.init();
- return provider;
- }
-
- _getCcSuggestionsProvider(change) {
- const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI,
- change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.CC);
- provider.init();
- return provider;
- }
-
- _onThreadListModified() {
- // TODO(taoalpha): this won't propogate the changes to the files
- // should consider replacing this with either top level events
- // or gerrit level events
-
- // emit the event so change-view can also get updated with latest changes
- this.fire('comment-refresh');
- }
}
- customElements.define(GrReplyDialog.is, GrReplyDialog);
-})();
+ focus() {
+ this._focusOn(FocusTarget.ANY);
+ }
+
+ getFocusStops() {
+ const end = this._sendDisabled ? this.$.cancelButton : this.$.sendButton;
+ return {
+ start: this.$.reviewers.focusStart,
+ end,
+ };
+ }
+
+ setLabelValue(label, value) {
+ const selectorEl =
+ this.$.labelScores.shadowRoot
+ .querySelector(`gr-label-score-row[name="${label}"]`);
+ if (!selectorEl) { return; }
+ selectorEl.setSelectedValue(value);
+ }
+
+ getLabelValue(label) {
+ const selectorEl =
+ this.$.labelScores.shadowRoot
+ .querySelector(`gr-label-score-row[name="${label}"]`);
+ if (!selectorEl) { return null; }
+
+ return selectorEl.selectedValue;
+ }
+
+ _handleEscKey(e) {
+ this.cancel();
+ }
+
+ _handleEnterKey(e) {
+ this._submit();
+ }
+
+ _ccsChanged(splices) {
+ this._reviewerTypeChanged(splices, ReviewerTypes.CC);
+ }
+
+ _reviewersChanged(splices) {
+ this._reviewerTypeChanged(splices, ReviewerTypes.REVIEWER);
+ }
+
+ _reviewerTypeChanged(splices, reviewerType) {
+ if (splices && splices.indexSplices) {
+ this._reviewersMutated = true;
+ this._processReviewerChange(splices.indexSplices,
+ reviewerType);
+ let key;
+ let index;
+ let account;
+ // Remove any accounts that already exist as a CC for reviewer
+ // or vice versa.
+ const isReviewer = ReviewerTypes.REVIEWER === reviewerType;
+ for (const splice of splices.indexSplices) {
+ for (let i = 0; i < splice.addedCount; i++) {
+ account = splice.object[splice.index + i];
+ key = this._accountOrGroupKey(account);
+ const array = isReviewer ? this._ccs : this._reviewers;
+ index = array.findIndex(
+ account => this._accountOrGroupKey(account) === key);
+ if (index >= 0) {
+ this.splice(isReviewer ? '_ccs' : '_reviewers', index, 1);
+ const moveFrom = isReviewer ? 'CC' : 'reviewer';
+ const moveTo = isReviewer ? 'reviewer' : 'CC';
+ const message = (account.name || account.email || key) +
+ ` moved from ${moveFrom} to ${moveTo}.`;
+ this.fire('show-alert', {message});
+ }
+ }
+ }
+ }
+ }
+
+ _processReviewerChange(indexSplices, type) {
+ for (const splice of indexSplices) {
+ for (const account of splice.removed) {
+ if (!this._reviewersPendingRemove[type]) {
+ console.err('Invalid type ' + type + ' for reviewer.');
+ return;
+ }
+ this._reviewersPendingRemove[type].push(account);
+ }
+ }
+ }
+
+ /**
+ * Resets the state of the _reviewersPendingRemove object, and removes
+ * accounts if necessary.
+ *
+ * @param {boolean} isCancel true if the action is a cancel.
+ * @param {Object=} opt_accountIdsTransferred map of account IDs that must
+ * not be removed, because they have been readded in another state.
+ */
+ _purgeReviewersPendingRemove(isCancel, opt_accountIdsTransferred) {
+ let reviewerArr;
+ const keep = opt_accountIdsTransferred || {};
+ for (const type in this._reviewersPendingRemove) {
+ if (this._reviewersPendingRemove.hasOwnProperty(type)) {
+ if (!isCancel) {
+ reviewerArr = this._reviewersPendingRemove[type];
+ for (let i = 0; i < reviewerArr.length; i++) {
+ if (!keep[reviewerArr[i]._account_id]) {
+ this._removeAccount(reviewerArr[i], type);
+ }
+ }
+ }
+ this._reviewersPendingRemove[type] = [];
+ }
+ }
+ }
+
+ /**
+ * Removes an account from the change, both on the backend and the client.
+ * Does nothing if the account is a pending addition.
+ *
+ * @param {!Object} account
+ * @param {string} type
+ */
+ _removeAccount(account, type) {
+ if (account._pendingAdd) { return; }
+
+ return this.$.restAPI.removeChangeReviewer(this.change._number,
+ account._account_id).then(response => {
+ if (!response.ok) { return response; }
+
+ const reviewers = this.change.reviewers[type] || [];
+ for (let i = 0; i < reviewers.length; i++) {
+ if (reviewers[i]._account_id == account._account_id) {
+ this.splice(`change.reviewers.${type}`, i, 1);
+ break;
+ }
+ }
+ });
+ }
+
+ _mapReviewer(reviewer) {
+ let reviewerId;
+ let confirmed;
+ if (reviewer.account) {
+ reviewerId = reviewer.account._account_id || reviewer.account.email;
+ } else if (reviewer.group) {
+ reviewerId = reviewer.group.id;
+ confirmed = reviewer.group.confirmed;
+ }
+ return {reviewer: reviewerId, confirmed};
+ }
+
+ send(includeComments, startReview) {
+ this.$.reporting.time(SEND_REPLY_TIMING_LABEL);
+ const labels = this.$.labelScores.getLabelValues();
+
+ const obj = {
+ drafts: includeComments ? 'PUBLISH_ALL_REVISIONS' : 'KEEP',
+ labels,
+ };
+
+ if (startReview) {
+ obj.ready = true;
+ }
+
+ if (this.draft != null) {
+ obj.message = this.draft;
+ }
+
+ const accountAdditions = {};
+ obj.reviewers = this.$.reviewers.additions().map(reviewer => {
+ if (reviewer.account) {
+ accountAdditions[reviewer.account._account_id] = true;
+ }
+ return this._mapReviewer(reviewer);
+ });
+ const ccsEl = this.$.ccs;
+ if (ccsEl) {
+ for (let reviewer of ccsEl.additions()) {
+ if (reviewer.account) {
+ accountAdditions[reviewer.account._account_id] = true;
+ }
+ reviewer = this._mapReviewer(reviewer);
+ reviewer.state = 'CC';
+ obj.reviewers.push(reviewer);
+ }
+ }
+
+ this.disabled = true;
+
+ const errFn = this._handle400Error.bind(this);
+ return this._saveReview(obj, errFn)
+ .then(response => {
+ if (!response) {
+ // Null or undefined response indicates that an error handler
+ // took responsibility, so just return.
+ return {};
+ }
+ if (!response.ok) {
+ this.fire('server-error', {response});
+ return {};
+ }
+
+ this.draft = '';
+ this._includeComments = true;
+ this.fire('send', null, {bubbles: false});
+ return accountAdditions;
+ })
+ .then(result => {
+ this.disabled = false;
+ return result;
+ })
+ .catch(err => {
+ this.disabled = false;
+ throw err;
+ });
+ }
+
+ _focusOn(section) {
+ // Safeguard- always want to focus on something.
+ if (!section || section === FocusTarget.ANY) {
+ section = this._chooseFocusTarget();
+ }
+ if (section === FocusTarget.BODY) {
+ const textarea = this.$.textarea;
+ textarea.async(textarea.getNativeTextarea()
+ .focus.bind(textarea.getNativeTextarea()));
+ } else if (section === FocusTarget.REVIEWERS) {
+ const reviewerEntry = this.$.reviewers.focusStart;
+ reviewerEntry.async(reviewerEntry.focus);
+ } else if (section === FocusTarget.CCS) {
+ const ccEntry = this.$.ccs.focusStart;
+ ccEntry.async(ccEntry.focus);
+ }
+ }
+
+ _chooseFocusTarget() {
+ // If we are the owner and the reviewers field is empty, focus on that.
+ if (this._account && this.change && this.change.owner &&
+ this._account._account_id === this.change.owner._account_id &&
+ (!this._reviewers || this._reviewers.length === 0)) {
+ return FocusTarget.REVIEWERS;
+ }
+
+ // Default to BODY.
+ return FocusTarget.BODY;
+ }
+
+ _handle400Error(response) {
+ // A call to _saveReview could fail with a server error if erroneous
+ // reviewers were requested. This is signalled with a 400 Bad Request
+ // status. The default gr-rest-api-interface error handling would
+ // result in a large JSON response body being displayed to the user in
+ // the gr-error-manager toast.
+ //
+ // We can modify the error handling behavior by passing this function
+ // through to restAPI as a custom error handling function. Since we're
+ // short-circuiting restAPI we can do our own response parsing and fire
+ // the server-error ourselves.
+ //
+ this.disabled = false;
+
+ // Using response.clone() here, because getResponseObject() and
+ // potentially the generic error handler will want to call text() on the
+ // response object, which can only be done once per object.
+ const jsonPromise = this.$.restAPI.getResponseObject(response.clone());
+ return jsonPromise.then(result => {
+ // Only perform custom error handling for 400s and a parseable
+ // ReviewResult response.
+ if (response.status === 400 && result) {
+ const errors = [];
+ for (const state of ['reviewers', 'ccs']) {
+ if (!result.hasOwnProperty(state)) { continue; }
+ for (const reviewer of Object.values(result[state])) {
+ if (reviewer.error) {
+ errors.push(reviewer.error);
+ }
+ }
+ }
+ response = {
+ ok: false,
+ status: response.status,
+ text() { return Promise.resolve(errors.join(', ')); },
+ };
+ }
+ this.fire('server-error', {response});
+ return null; // Means that the error has been handled.
+ });
+ }
+
+ _computeHideDraftList(draftCommentThreads) {
+ return draftCommentThreads.length === 0;
+ }
+
+ _computeDraftsTitle(draftCommentThreads) {
+ const total = draftCommentThreads.length;
+ if (total == 0) { return ''; }
+ if (total == 1) { return '1 Draft'; }
+ if (total > 1) { return total + ' Drafts'; }
+ }
+
+ _computeMessagePlaceholder(canBeStarted) {
+ return canBeStarted ?
+ 'Add a note for your reviewers...' :
+ 'Say something nice...';
+ }
+
+ _changeUpdated(changeRecord, owner) {
+ // Polymer 2: check for undefined
+ if ([changeRecord, owner].some(arg => arg === undefined)) {
+ return;
+ }
+
+ this._rebuildReviewerArrays(changeRecord.base, owner);
+ }
+
+ _rebuildReviewerArrays(change, owner) {
+ this._owner = owner;
+
+ const reviewers = [];
+ const ccs = [];
+
+ for (const key in change) {
+ if (change.hasOwnProperty(key)) {
+ if (key !== 'REVIEWER' && key !== 'CC') {
+ console.warn('unexpected reviewer state:', key);
+ continue;
+ }
+ for (const entry of change[key]) {
+ if (entry._account_id === owner._account_id) {
+ continue;
+ }
+ switch (key) {
+ case 'REVIEWER':
+ reviewers.push(entry);
+ break;
+ case 'CC':
+ ccs.push(entry);
+ break;
+ }
+ }
+ }
+ }
+
+ this._ccs = ccs;
+ this._reviewers = reviewers;
+ }
+
+ _accountOrGroupKey(entry) {
+ return entry.id || entry._account_id;
+ }
+
+ /**
+ * Generates a function to filter out reviewer/CC entries. When isCCs is
+ * truthy, the function filters out entries that already exist in this._ccs.
+ * When falsy, the function filters entries that exist in this._reviewers.
+ *
+ * @param {boolean} isCCs
+ * @return {!Function}
+ */
+ _filterReviewerSuggestionGenerator(isCCs) {
+ return suggestion => {
+ let entry;
+ if (suggestion.account) {
+ entry = suggestion.account;
+ } else if (suggestion.group) {
+ entry = suggestion.group;
+ } else {
+ console.warn(
+ 'received suggestion that was neither account nor group:',
+ suggestion);
+ }
+ if (entry._account_id === this._owner._account_id) {
+ return false;
+ }
+
+ const key = this._accountOrGroupKey(entry);
+ const finder = entry => this._accountOrGroupKey(entry) === key;
+ if (isCCs) {
+ return this._ccs.find(finder) === undefined;
+ }
+ return this._reviewers.find(finder) === undefined;
+ };
+ }
+
+ _getAccount() {
+ return this.$.restAPI.getAccount();
+ }
+
+ _cancelTapHandler(e) {
+ e.preventDefault();
+ this.cancel();
+ }
+
+ cancel() {
+ this.fire('cancel', null, {bubbles: false});
+ this.$.textarea.closeDropdown();
+ this._purgeReviewersPendingRemove(true);
+ this._rebuildReviewerArrays(this.change.reviewers, this._owner);
+ }
+
+ _saveClickHandler(e) {
+ e.preventDefault();
+ if (!this.$.ccs.submitEntryText()) {
+ // Do not proceed with the save if there is an invalid email entry in
+ // the text field of the CC entry.
+ return;
+ }
+ this.send(this._includeComments, false).then(keepReviewers => {
+ this._purgeReviewersPendingRemove(false, keepReviewers);
+ });
+ }
+
+ _sendTapHandler(e) {
+ e.preventDefault();
+ this._submit();
+ }
+
+ _submit() {
+ if (!this.$.ccs.submitEntryText()) {
+ // Do not proceed with the send if there is an invalid email entry in
+ // the text field of the CC entry.
+ return;
+ }
+ if (this._sendDisabled) {
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ bubbles: true,
+ composed: true,
+ detail: {message: EMPTY_REPLY_MESSAGE},
+ }));
+ return;
+ }
+ return this.send(this._includeComments, this.canBeStarted)
+ .then(keepReviewers => {
+ this._purgeReviewersPendingRemove(false, keepReviewers);
+ })
+ .catch(err => {
+ this.dispatchEvent(new CustomEvent('show-error', {
+ bubbles: true,
+ composed: true,
+ detail: {message: `Error submitting review ${err}`},
+ }));
+ });
+ }
+
+ _saveReview(review, opt_errFn) {
+ return this.$.restAPI.saveChangeReview(this.change._number, this.patchNum,
+ review, opt_errFn);
+ }
+
+ _reviewerPendingConfirmationUpdated(reviewer) {
+ if (reviewer === null) {
+ this.$.reviewerConfirmationOverlay.close();
+ } else {
+ this._pendingConfirmationDetails =
+ this._ccPendingConfirmation || this._reviewerPendingConfirmation;
+ this.$.reviewerConfirmationOverlay.open();
+ }
+ }
+
+ _confirmPendingReviewer() {
+ if (this._ccPendingConfirmation) {
+ this.$.ccs.confirmGroup(this._ccPendingConfirmation.group);
+ this._focusOn(FocusTarget.CCS);
+ } else {
+ this.$.reviewers.confirmGroup(this._reviewerPendingConfirmation.group);
+ this._focusOn(FocusTarget.REVIEWERS);
+ }
+ }
+
+ _cancelPendingReviewer() {
+ this._ccPendingConfirmation = null;
+ this._reviewerPendingConfirmation = null;
+
+ const target =
+ this._ccPendingConfirmation ? FocusTarget.CCS : FocusTarget.REVIEWERS;
+ this._focusOn(target);
+ }
+
+ _getStorageLocation() {
+ // Tests trigger this method without setting change.
+ if (!this.change) { return {}; }
+ return {
+ changeNum: this.change._number,
+ patchNum: '@change',
+ path: '@change',
+ };
+ }
+
+ _loadStoredDraft() {
+ const draft = this.$.storage.getDraftComment(this._getStorageLocation());
+ return draft ? draft.message : '';
+ }
+
+ _handleAccountTextEntry() {
+ // When either of the account entries has input added to the autocomplete,
+ // it should trigger the save button to enable/
+ //
+ // Note: if the text is removed, the save button will not get disabled.
+ this._reviewersMutated = true;
+ }
+
+ _draftChanged(newDraft, oldDraft) {
+ this.debounce('store', () => {
+ if (!newDraft.length && oldDraft) {
+ // If the draft has been modified to be empty, then erase the storage
+ // entry.
+ this.$.storage.eraseDraftComment(this._getStorageLocation());
+ } else if (newDraft.length) {
+ this.$.storage.setDraftComment(this._getStorageLocation(),
+ this.draft);
+ }
+ }, STORAGE_DEBOUNCE_INTERVAL_MS);
+ }
+
+ _handleHeightChanged(e) {
+ this.fire('autogrow');
+ }
+
+ _handleLabelsChanged() {
+ this._labelsChanged = Object.keys(
+ this.$.labelScores.getLabelValues()).length !== 0;
+ }
+
+ _isState(knownLatestState, value) {
+ return knownLatestState === value;
+ }
+
+ _reload() {
+ // Load the current change without any patch range.
+ Gerrit.Nav.navigateToChange(this.change);
+ this.cancel();
+ }
+
+ _computeSendButtonLabel(canBeStarted) {
+ return canBeStarted ? ButtonLabels.START_REVIEW : ButtonLabels.SEND;
+ }
+
+ _computeSendButtonTooltip(canBeStarted) {
+ return canBeStarted ? ButtonTooltips.START_REVIEW : ButtonTooltips.SEND;
+ }
+
+ _computeSavingLabelClass(savingComments) {
+ return savingComments ? 'saving' : '';
+ }
+
+ _computeSendButtonDisabled(
+ buttonLabel, draftCommentThreads, text, reviewersMutated,
+ labelsChanged, includeComments, disabled) {
+ // Polymer 2: check for undefined
+ if ([
+ buttonLabel,
+ draftCommentThreads,
+ text,
+ reviewersMutated,
+ labelsChanged,
+ includeComments,
+ disabled,
+ ].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ if (disabled) { return true; }
+ if (buttonLabel === ButtonLabels.START_REVIEW) { return false; }
+ const hasDrafts = includeComments && draftCommentThreads.length;
+ return !hasDrafts && !text.length && !reviewersMutated && !labelsChanged;
+ }
+
+ _computePatchSetWarning(patchNum, labelsChanged) {
+ let str = `Patch ${patchNum} is not latest.`;
+ if (labelsChanged) {
+ str += ' Voting on a non-latest patch will have no effect.';
+ }
+ return str;
+ }
+
+ setPluginMessage(message) {
+ this._pluginMessage = message;
+ }
+
+ _sendDisabledChanged(sendDisabled) {
+ this.dispatchEvent(new CustomEvent('send-disabled-changed'));
+ }
+
+ _getReviewerSuggestionsProvider(change) {
+ const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI,
+ change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER);
+ provider.init();
+ return provider;
+ }
+
+ _getCcSuggestionsProvider(change) {
+ const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI,
+ change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.CC);
+ provider.init();
+ return provider;
+ }
+
+ _onThreadListModified() {
+ // TODO(taoalpha): this won't propogate the changes to the files
+ // should consider replacing this with either top level events
+ // or gerrit level events
+
+ // emit the event so change-view can also get updated with latest changes
+ this.fire('comment-refresh');
+ }
+}
+
+customElements.define(GrReplyDialog.is, GrReplyDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
new file mode 100644
index 0000000..0c98834
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
@@ -0,0 +1,229 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ background-color: var(--dialog-background-color);
+ display: block;
+ max-height: 90vh;
+ }
+ :host([disabled]) {
+ pointer-events: none;
+ }
+ :host([disabled]) .container {
+ opacity: .5;
+ }
+ .container {
+ display: flex;
+ flex-direction: column;
+ max-height: 100%;
+ }
+ section {
+ border-top: 1px solid var(--border-color);
+ flex-shrink: 0;
+ padding: var(--spacing-m) var(--spacing-xl);
+ width: 100%;
+ }
+ section.labelsContainer {
+ /* We want the :hover highlight to extend to the border of the dialog. */
+ padding: var(--spacing-m) 0;
+ }
+ .actions {
+ background-color: var(--dialog-background-color);
+ bottom: 0;
+ display: flex;
+ justify-content: space-between;
+ position: sticky;
+ /* @see Issue 8602 */
+ z-index: 1;
+ }
+ .actions .right gr-button {
+ margin-left: var(--spacing-l);
+ }
+ .peopleContainer,
+ .labelsContainer {
+ flex-shrink: 0;
+ }
+ .peopleContainer {
+ border-top: none;
+ display: table;
+ }
+ .peopleList {
+ display: flex;
+ }
+ .peopleListLabel {
+ color: var(--deemphasized-text-color);
+ margin-top: var(--spacing-xs);
+ min-width: 6em;
+ padding-right: var(--spacing-m);
+ }
+ gr-account-list {
+ display: flex;
+ flex-wrap: wrap;
+ flex: 1;
+ }
+ #reviewerConfirmationOverlay {
+ padding: var(--spacing-l);
+ text-align: center;
+ }
+ .reviewerConfirmationButtons {
+ margin-top: var(--spacing-l);
+ }
+ .groupName {
+ font-weight: var(--font-weight-bold);
+ }
+ .groupSize {
+ font-style: italic;
+ }
+ .textareaContainer {
+ min-height: 12em;
+ position: relative;
+ }
+ .textareaContainer,
+ #textarea,
+ gr-endpoint-decorator {
+ display: flex;
+ width: 100%;
+ }
+ gr-endpoint-decorator[name="reply-label-scores"] {
+ display: block;
+ }
+ .previewContainer gr-formatted-text {
+ background: var(--table-header-background-color);
+ padding: var(--spacing-l);
+ }
+ .draftsContainer h3 {
+ margin-top: var(--spacing-xs);
+ }
+ #checkingStatusLabel,
+ #notLatestLabel {
+ margin-left: var(--spacing-l);
+ }
+ #checkingStatusLabel {
+ color: var(--deemphasized-text-color);
+ font-style: italic;
+ }
+ #notLatestLabel,
+ #savingLabel {
+ color: var(--error-text-color);
+ }
+ #savingLabel {
+ display: none;
+ }
+ #savingLabel.saving {
+ display: inline;
+ }
+ #pluginMessage {
+ color: var(--deemphasized-text-color);
+ margin-left: var(--spacing-l);
+ margin-bottom: var(--spacing-m);
+ }
+ #pluginMessage:empty {
+ display: none;
+ }
+ </style>
+ <div class="container" tabindex="-1">
+ <section class="peopleContainer">
+ <div class="peopleList">
+ <div class="peopleListLabel">Reviewers</div>
+ <gr-account-list id="reviewers" accounts="{{_reviewers}}" removable-values="[[change.removable_reviewers]]" filter="[[filterReviewerSuggestion]]" pending-confirmation="{{_reviewerPendingConfirmation}}" placeholder="Add reviewer..." on-account-text-changed="_handleAccountTextEntry" suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
+ </gr-account-list>
+ </div>
+ <div class="peopleList">
+ <div class="peopleListLabel">CC</div>
+ <gr-account-list id="ccs" accounts="{{_ccs}}" filter="[[filterCCSuggestion]]" pending-confirmation="{{_ccPendingConfirmation}}" allow-any-input="" placeholder="Add CC..." on-account-text-changed="_handleAccountTextEntry" suggestions-provider="[[_getCcSuggestionsProvider(change)]]">
+ </gr-account-list>
+ </div>
+ <gr-overlay id="reviewerConfirmationOverlay" on-iron-overlay-canceled="_cancelPendingReviewer">
+ <div class="reviewerConfirmation">
+ Group
+ <span class="groupName">
+ [[_pendingConfirmationDetails.group.name]]
+ </span>
+ has
+ <span class="groupSize">
+ [[_pendingConfirmationDetails.count]]
+ </span>
+ members.
+ <br>
+ Are you sure you want to add them all?
+ </div>
+ <div class="reviewerConfirmationButtons">
+ <gr-button on-click="_confirmPendingReviewer">Yes</gr-button>
+ <gr-button on-click="_cancelPendingReviewer">No</gr-button>
+ </div>
+ </gr-overlay>
+ </section>
+ <section class="textareaContainer">
+ <gr-endpoint-decorator name="reply-text">
+ <gr-textarea id="textarea" class="message" autocomplete="on" placeholder="[[_messagePlaceholder]]" fixed-position-dropdown="" hide-border="true" monospace="true" disabled="{{disabled}}" rows="4" text="{{draft}}" on-bind-value-changed="_handleHeightChanged">
+ </gr-textarea>
+ </gr-endpoint-decorator>
+ </section>
+ <section class="previewContainer">
+ <label>
+ <input type="checkbox" checked="{{_previewFormatting::change}}">
+ Preview formatting
+ </label>
+ <gr-formatted-text content="[[draft]]" hidden\$="[[!_previewFormatting]]" config="[[projectConfig.commentlinks]]"></gr-formatted-text>
+ </section>
+ <section class="labelsContainer">
+ <gr-endpoint-decorator name="reply-label-scores">
+ <gr-label-scores id="labelScores" account="[[_account]]" change="[[change]]" on-labels-changed="_handleLabelsChanged" permitted-labels="[[permittedLabels]]"></gr-label-scores>
+ </gr-endpoint-decorator>
+ <div id="pluginMessage">[[_pluginMessage]]</div>
+ </section>
+ <section class="draftsContainer" hidden\$="[[_computeHideDraftList(draftCommentThreads)]]">
+ <div class="includeComments">
+ <input type="checkbox" id="includeComments" checked="{{_includeComments::change}}">
+ <label for="includeComments">Publish [[_computeDraftsTitle(draftCommentThreads)]]</label>
+ </div>
+ <gr-thread-list id="commentList" hidden\$="[[!_includeComments]]" threads="[[draftCommentThreads]]" change="[[change]]" change-num="[[change._number]]" logged-in="true" hide-toggle-buttons="" on-thread-list-modified="_onThreadListModified">
+ </gr-thread-list>
+ <span id="savingLabel" class\$="[[_computeSavingLabelClass(_savingComments)]]">
+ Saving comments...
+ </span>
+ </section>
+ <section class="actions">
+ <div class="left">
+ <span id="checkingStatusLabel" hidden\$="[[!_isState(knownLatestState, 'checking')]]">
+ Checking whether patch [[patchNum]] is latest...
+ </span>
+ <span id="notLatestLabel" hidden\$="[[!_isState(knownLatestState, 'not-latest')]]">
+ [[_computePatchSetWarning(patchNum, _labelsChanged)]]
+ <gr-button link="" on-click="_reload">Reload</gr-button>
+ </span>
+ </div>
+ <div class="right">
+ <gr-button link="" id="cancelButton" class="action cancel" on-click="_cancelTapHandler">Cancel</gr-button>
+ <template is="dom-if" if="[[canBeStarted]]">
+ <!-- Use 'Send' here as the change may only about reviewers / ccs
+ and when this button is visible, the next button will always
+ be 'Start review' -->
+ <gr-button link="" disabled="[[_isState(knownLatestState, 'not-latest')]]" class="action save" has-tooltip="" title="[[_saveTooltip]]" on-click="_saveClickHandler">Save</gr-button>
+ </template>
+ <gr-button id="sendButton" primary="" disabled="[[_sendDisabled]]" class="action send" has-tooltip="" title\$="[[_computeSendButtonTooltip(canBeStarted)]]" on-click="_sendTapHandler">[[_sendButtonLabel]]</gr-button>
+ </div>
+ </section>
+ </div>
+ <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ <gr-storage id="storage"></gr-storage>
+ <gr-reporting id="reporting"></gr-reporting>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index b7c2330..f7361597 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -19,16 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reply-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="/bower_components/iron-overlay-behavior/iron-overlay-manager.html">
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-reply-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,1196 +30,1198 @@
</template>
</test-fixture>
-<script>
- function cloneableResponse(status, text) {
- return {
- ok: false,
- status,
- text() {
- return Promise.resolve(text);
- },
- clone() {
- return {
- ok: false,
- status,
- text() {
- return Promise.resolve(text);
- },
- };
- },
- };
- }
-
- suite('gr-reply-dialog tests', async () => {
- await readyToTest();
- let element;
- let changeNum;
- let patchNum;
-
- let sandbox;
- let getDraftCommentStub;
- let setDraftCommentStub;
- let eraseDraftCommentStub;
-
- let lastId = 0;
- const makeAccount = function() { return {_account_id: lastId++}; };
- const makeGroup = function() { return {id: lastId++}; };
-
- setup(() => {
- sandbox = sinon.sandbox.create();
-
- changeNum = 42;
- patchNum = 1;
-
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getAccount() { return Promise.resolve({}); },
- getChange() { return Promise.resolve({}); },
- getChangeSuggestedReviewers() { return Promise.resolve([]); },
- });
-
- element = fixture('basic');
- element.change = {
- _number: changeNum,
- labels: {
- 'Verified': {
- values: {
- '-1': 'Fails',
- ' 0': 'No score',
- '+1': 'Verified',
- },
- default_value: 0,
- },
- 'Code-Review': {
- values: {
- '-2': 'Do not submit',
- '-1': 'I would prefer that you didn\'t submit this',
- ' 0': 'No score',
- '+1': 'Looks good to me, but someone else must approve',
- '+2': 'Looks good to me, approved',
- },
- default_value: 0,
- },
+<script type="module">
+import {IronOverlayManager} from '@polymer/iron-overlay-behavior/iron-overlay-manager.js';
+import '../../../test/common-test-setup.js';
+import './gr-reply-dialog.js';
+function cloneableResponse(status, text) {
+ return {
+ ok: false,
+ status,
+ text() {
+ return Promise.resolve(text);
+ },
+ clone() {
+ return {
+ ok: false,
+ status,
+ text() {
+ return Promise.resolve(text);
},
};
- element.patchNum = patchNum;
- element.permittedLabels = {
- 'Code-Review': [
- '-1',
- ' 0',
- '+1',
- ],
- 'Verified': [
- '-1',
- ' 0',
- '+1',
- ],
- };
+ },
+ };
+}
- getDraftCommentStub = sandbox.stub(element.$.storage, 'getDraftComment');
- setDraftCommentStub = sandbox.stub(element.$.storage, 'setDraftComment');
- eraseDraftCommentStub = sandbox.stub(element.$.storage,
- 'eraseDraftComment');
+suite('gr-reply-dialog tests', () => {
+ let element;
+ let changeNum;
+ let patchNum;
- sandbox.stub(element, 'fetchChangeUpdates')
- .returns(Promise.resolve({isLatest: true}));
+ let sandbox;
+ let getDraftCommentStub;
+ let setDraftCommentStub;
+ let eraseDraftCommentStub;
- // Allow the elements created by dom-repeat to be stamped.
- flushAsynchronousOperations();
+ let lastId = 0;
+ const makeAccount = function() { return {_account_id: lastId++}; };
+ const makeGroup = function() { return {id: lastId++}; };
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+
+ changeNum = 42;
+ patchNum = 1;
+
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getAccount() { return Promise.resolve({}); },
+ getChange() { return Promise.resolve({}); },
+ getChangeSuggestedReviewers() { return Promise.resolve([]); },
});
- teardown(() => {
- sandbox.restore();
- });
-
- function stubSaveReview(jsonResponseProducer) {
- return sandbox.stub(
- element,
- '_saveReview',
- review => new Promise((resolve, reject) => {
- try {
- const result = jsonResponseProducer(review) || {};
- const resultStr =
- element.$.restAPI.JSON_PREFIX + JSON.stringify(result);
- resolve({
- ok: true,
- text() {
- return Promise.resolve(resultStr);
- },
- });
- } catch (err) {
- reject(err);
- }
- }));
- }
-
- test('default to publishing draft comments with reply', done => {
- // Async tick is needed because iron-selector content is distributed and
- // distributed content requires an observer to be set up.
- // Note: Double flush seems to be needed in Safari. {@see Issue 4963}.
- flush(() => {
- flush(() => {
- element.draft = 'I wholeheartedly disapprove';
-
- stubSaveReview(review => {
- assert.deepEqual(review, {
- drafts: 'PUBLISH_ALL_REVISIONS',
- labels: {
- 'Code-Review': 0,
- 'Verified': 0,
- },
- message: 'I wholeheartedly disapprove',
- reviewers: [],
- });
- assert.isFalse(element.$.commentList.hidden);
- done();
- });
-
- // This is needed on non-Blink engines most likely due to the ways in
- // which the dom-repeat elements are stamped.
- flush(() => {
- MockInteractions.tap(element.shadowRoot
- .querySelector('.send'));
- });
- });
- });
- });
-
- test('keep draft comments with reply', done => {
- MockInteractions.tap(element.shadowRoot.querySelector('#includeComments'));
- assert.equal(element._includeComments, false);
-
- // Async tick is needed because iron-selector content is distributed and
- // distributed content requires an observer to be set up.
- // Note: Double flush seems to be needed in Safari. {@see Issue 4963}.
- flush(() => {
- flush(() => {
- element.draft = 'I wholeheartedly disapprove';
-
- stubSaveReview(review => {
- assert.deepEqual(review, {
- drafts: 'KEEP',
- labels: {
- 'Code-Review': 0,
- 'Verified': 0,
- },
- message: 'I wholeheartedly disapprove',
- reviewers: [],
- });
- assert.isTrue(element.$.commentList.hidden);
- done();
- });
-
- // This is needed on non-Blink engines most likely due to the ways in
- // which the dom-repeat elements are stamped.
- flush(() => {
- MockInteractions.tap(element.shadowRoot
- .querySelector('.send'));
- });
- });
- });
- });
-
- test('label picker', done => {
- element.draft = 'I wholeheartedly disapprove';
- stubSaveReview(review => {
- assert.deepEqual(review, {
- drafts: 'PUBLISH_ALL_REVISIONS',
- labels: {
- 'Code-Review': -1,
- 'Verified': -1,
+ element = fixture('basic');
+ element.change = {
+ _number: changeNum,
+ labels: {
+ 'Verified': {
+ values: {
+ '-1': 'Fails',
+ ' 0': 'No score',
+ '+1': 'Verified',
},
- message: 'I wholeheartedly disapprove',
- reviewers: [],
- });
- });
+ default_value: 0,
+ },
+ 'Code-Review': {
+ values: {
+ '-2': 'Do not submit',
+ '-1': 'I would prefer that you didn\'t submit this',
+ ' 0': 'No score',
+ '+1': 'Looks good to me, but someone else must approve',
+ '+2': 'Looks good to me, approved',
+ },
+ default_value: 0,
+ },
+ },
+ };
+ element.patchNum = patchNum;
+ element.permittedLabels = {
+ 'Code-Review': [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ 'Verified': [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ };
- sandbox.stub(element.$.labelScores, 'getLabelValues', () => {
- return {
- 'Code-Review': -1,
- 'Verified': -1,
- };
- });
+ getDraftCommentStub = sandbox.stub(element.$.storage, 'getDraftComment');
+ setDraftCommentStub = sandbox.stub(element.$.storage, 'setDraftComment');
+ eraseDraftCommentStub = sandbox.stub(element.$.storage,
+ 'eraseDraftComment');
- element.addEventListener('send', () => {
- // Flush to ensure properties are updated.
- flush(() => {
- assert.isFalse(element.disabled,
- 'Element should be enabled when done sending reply.');
- assert.equal(element.draft.length, 0);
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
+
+ // Allow the elements created by dom-repeat to be stamped.
+ flushAsynchronousOperations();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ function stubSaveReview(jsonResponseProducer) {
+ return sandbox.stub(
+ element,
+ '_saveReview',
+ review => new Promise((resolve, reject) => {
+ try {
+ const result = jsonResponseProducer(review) || {};
+ const resultStr =
+ element.$.restAPI.JSON_PREFIX + JSON.stringify(result);
+ resolve({
+ ok: true,
+ text() {
+ return Promise.resolve(resultStr);
+ },
+ });
+ } catch (err) {
+ reject(err);
+ }
+ }));
+ }
+
+ test('default to publishing draft comments with reply', done => {
+ // Async tick is needed because iron-selector content is distributed and
+ // distributed content requires an observer to be set up.
+ // Note: Double flush seems to be needed in Safari. {@see Issue 4963}.
+ flush(() => {
+ flush(() => {
+ element.draft = 'I wholeheartedly disapprove';
+
+ stubSaveReview(review => {
+ assert.deepEqual(review, {
+ drafts: 'PUBLISH_ALL_REVISIONS',
+ labels: {
+ 'Code-Review': 0,
+ 'Verified': 0,
+ },
+ message: 'I wholeheartedly disapprove',
+ reviewers: [],
+ });
+ assert.isFalse(element.$.commentList.hidden);
done();
});
- });
- // This is needed on non-Blink engines most likely due to the ways in
- // which the dom-repeat elements are stamped.
- flush(() => {
- MockInteractions.tap(element.shadowRoot
- .querySelector('.send'));
- assert.isTrue(element.disabled);
- });
- });
-
- test('getlabelValue returns value', done => {
- flush(() => {
- element.shadowRoot
- .querySelector('gr-label-scores')
- .shadowRoot
- .querySelector(`gr-label-score-row[name="Verified"]`)
- .setSelectedValue(-1);
- assert.equal('-1', element.getLabelValue('Verified'));
- done();
- });
- });
-
- test('getlabelValue when no score is selected', done => {
- flush(() => {
- element.shadowRoot
- .querySelector('gr-label-scores')
- .shadowRoot
- .querySelector(`gr-label-score-row[name="Code-Review"]`)
- .setSelectedValue(-1);
- assert.strictEqual(element.getLabelValue('Verified'), ' 0');
- done();
- });
- });
-
- test('setlabelValue', done => {
- element._account = {_account_id: 1};
- flush(() => {
- const label = 'Verified';
- const value = '+1';
- element.setLabelValue(label, value);
-
- const labels = element.$.labelScores.getLabelValues();
- assert.deepEqual(labels, {
- 'Code-Review': 0,
- 'Verified': 1,
+ // This is needed on non-Blink engines most likely due to the ways in
+ // which the dom-repeat elements are stamped.
+ flush(() => {
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.send'));
});
+ });
+ });
+ });
+
+ test('keep draft comments with reply', done => {
+ MockInteractions.tap(element.shadowRoot.querySelector('#includeComments'));
+ assert.equal(element._includeComments, false);
+
+ // Async tick is needed because iron-selector content is distributed and
+ // distributed content requires an observer to be set up.
+ // Note: Double flush seems to be needed in Safari. {@see Issue 4963}.
+ flush(() => {
+ flush(() => {
+ element.draft = 'I wholeheartedly disapprove';
+
+ stubSaveReview(review => {
+ assert.deepEqual(review, {
+ drafts: 'KEEP',
+ labels: {
+ 'Code-Review': 0,
+ 'Verified': 0,
+ },
+ message: 'I wholeheartedly disapprove',
+ reviewers: [],
+ });
+ assert.isTrue(element.$.commentList.hidden);
+ done();
+ });
+
+ // This is needed on non-Blink engines most likely due to the ways in
+ // which the dom-repeat elements are stamped.
+ flush(() => {
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.send'));
+ });
+ });
+ });
+ });
+
+ test('label picker', done => {
+ element.draft = 'I wholeheartedly disapprove';
+ stubSaveReview(review => {
+ assert.deepEqual(review, {
+ drafts: 'PUBLISH_ALL_REVISIONS',
+ labels: {
+ 'Code-Review': -1,
+ 'Verified': -1,
+ },
+ message: 'I wholeheartedly disapprove',
+ reviewers: [],
+ });
+ });
+
+ sandbox.stub(element.$.labelScores, 'getLabelValues', () => {
+ return {
+ 'Code-Review': -1,
+ 'Verified': -1,
+ };
+ });
+
+ element.addEventListener('send', () => {
+ // Flush to ensure properties are updated.
+ flush(() => {
+ assert.isFalse(element.disabled,
+ 'Element should be enabled when done sending reply.');
+ assert.equal(element.draft.length, 0);
done();
});
});
- function getActiveElement() {
- return Polymer.IronOverlayManager.deepActiveElement;
- }
+ // This is needed on non-Blink engines most likely due to the ways in
+ // which the dom-repeat elements are stamped.
+ flush(() => {
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.send'));
+ assert.isTrue(element.disabled);
+ });
+ });
- function isVisible(el) {
- assert.ok(el);
- return getComputedStyle(el).getPropertyValue('display') != 'none';
- }
+ test('getlabelValue returns value', done => {
+ flush(() => {
+ element.shadowRoot
+ .querySelector('gr-label-scores')
+ .shadowRoot
+ .querySelector(`gr-label-score-row[name="Verified"]`)
+ .setSelectedValue(-1);
+ assert.equal('-1', element.getLabelValue('Verified'));
+ done();
+ });
+ });
- function overlayObserver(mode) {
- return new Promise(resolve => {
- function listener() {
- element.removeEventListener('iron-overlay-' + mode, listener);
- resolve();
- }
- element.addEventListener('iron-overlay-' + mode, listener);
+ test('getlabelValue when no score is selected', done => {
+ flush(() => {
+ element.shadowRoot
+ .querySelector('gr-label-scores')
+ .shadowRoot
+ .querySelector(`gr-label-score-row[name="Code-Review"]`)
+ .setSelectedValue(-1);
+ assert.strictEqual(element.getLabelValue('Verified'), ' 0');
+ done();
+ });
+ });
+
+ test('setlabelValue', done => {
+ element._account = {_account_id: 1};
+ flush(() => {
+ const label = 'Verified';
+ const value = '+1';
+ element.setLabelValue(label, value);
+
+ const labels = element.$.labelScores.getLabelValues();
+ assert.deepEqual(labels, {
+ 'Code-Review': 0,
+ 'Verified': 1,
});
- }
+ done();
+ });
+ });
- function isFocusInsideElement(element) {
- // In Polymer 2 focused element either <paper-input> or nested
- // native input <input> element depending on the current focus
- // in browser window.
- // For example, the focus is changed if the developer console
- // get a focus.
- let activeElement = getActiveElement();
- while (activeElement) {
- if (activeElement === element) {
- return true;
- }
- if (activeElement.parentElement) {
- activeElement = activeElement.parentElement;
- } else {
- activeElement = activeElement.getRootNode().host;
- }
+ function getActiveElement() {
+ return IronOverlayManager.deepActiveElement;
+ }
+
+ function isVisible(el) {
+ assert.ok(el);
+ return getComputedStyle(el).getPropertyValue('display') != 'none';
+ }
+
+ function overlayObserver(mode) {
+ return new Promise(resolve => {
+ function listener() {
+ element.removeEventListener('iron-overlay-' + mode, listener);
+ resolve();
}
- return false;
+ element.addEventListener('iron-overlay-' + mode, listener);
+ });
+ }
+
+ function isFocusInsideElement(element) {
+ // In Polymer 2 focused element either <paper-input> or nested
+ // native input <input> element depending on the current focus
+ // in browser window.
+ // For example, the focus is changed if the developer console
+ // get a focus.
+ let activeElement = getActiveElement();
+ while (activeElement) {
+ if (activeElement === element) {
+ return true;
+ }
+ if (activeElement.parentElement) {
+ activeElement = activeElement.parentElement;
+ } else {
+ activeElement = activeElement.getRootNode().host;
+ }
}
+ return false;
+ }
- function testConfirmationDialog(done, cc) {
- const yesButton = element
- .shadowRoot
- .querySelector('.reviewerConfirmationButtons gr-button:first-child');
- const noButton = element
- .shadowRoot
- .querySelector('.reviewerConfirmationButtons gr-button:last-child');
+ function testConfirmationDialog(done, cc) {
+ const yesButton = element
+ .shadowRoot
+ .querySelector('.reviewerConfirmationButtons gr-button:first-child');
+ const noButton = element
+ .shadowRoot
+ .querySelector('.reviewerConfirmationButtons gr-button:last-child');
- element._ccPendingConfirmation = null;
- element._reviewerPendingConfirmation = null;
- flushAsynchronousOperations();
- assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
+ element._ccPendingConfirmation = null;
+ element._reviewerPendingConfirmation = null;
+ flushAsynchronousOperations();
+ assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
- // Cause the confirmation dialog to display.
- let observer = overlayObserver('opened');
- const group = {
- id: 'id',
- name: 'name',
+ // Cause the confirmation dialog to display.
+ let observer = overlayObserver('opened');
+ const group = {
+ id: 'id',
+ name: 'name',
+ };
+ if (cc) {
+ element._ccPendingConfirmation = {
+ group,
+ count: 10,
};
- if (cc) {
- element._ccPendingConfirmation = {
- group,
- count: 10,
- };
- } else {
- element._reviewerPendingConfirmation = {
- group,
- count: 10,
- };
- }
- flushAsynchronousOperations();
+ } else {
+ element._reviewerPendingConfirmation = {
+ group,
+ count: 10,
+ };
+ }
+ flushAsynchronousOperations();
- if (cc) {
- assert.deepEqual(
- element._ccPendingConfirmation,
- element._pendingConfirmationDetails);
- } else {
- assert.deepEqual(
- element._reviewerPendingConfirmation,
- element._pendingConfirmationDetails);
- }
+ if (cc) {
+ assert.deepEqual(
+ element._ccPendingConfirmation,
+ element._pendingConfirmationDetails);
+ } else {
+ assert.deepEqual(
+ element._reviewerPendingConfirmation,
+ element._pendingConfirmationDetails);
+ }
- observer
- .then(() => {
- assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay));
- observer = overlayObserver('closed');
- const expected = 'Group name has 10 members';
- assert.notEqual(
- element.$.reviewerConfirmationOverlay.innerText
- .indexOf(expected),
- -1);
- MockInteractions.tap(noButton); // close the overlay
- return observer;
- }).then(() => {
- assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
+ observer
+ .then(() => {
+ assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay));
+ observer = overlayObserver('closed');
+ const expected = 'Group name has 10 members';
+ assert.notEqual(
+ element.$.reviewerConfirmationOverlay.innerText
+ .indexOf(expected),
+ -1);
+ MockInteractions.tap(noButton); // close the overlay
+ return observer;
+ }).then(() => {
+ assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
- // We should be focused on account entry input.
+ // We should be focused on account entry input.
+ assert.isTrue(
+ isFocusInsideElement(
+ element.$.reviewers.$.entry.$.input.$.input
+ )
+ );
+
+ // No reviewer/CC should have been added.
+ assert.equal(element.$.ccs.additions().length, 0);
+ assert.equal(element.$.reviewers.additions().length, 0);
+
+ // Reopen confirmation dialog.
+ observer = overlayObserver('opened');
+ if (cc) {
+ element._ccPendingConfirmation = {
+ group,
+ count: 10,
+ };
+ } else {
+ element._reviewerPendingConfirmation = {
+ group,
+ count: 10,
+ };
+ }
+ return observer;
+ })
+ .then(() => {
+ assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay));
+ observer = overlayObserver('closed');
+ MockInteractions.tap(yesButton); // Confirm the group.
+ return observer;
+ })
+ .then(() => {
+ assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
+ const additions = cc ?
+ element.$.ccs.additions() :
+ element.$.reviewers.additions();
+ assert.deepEqual(
+ additions,
+ [
+ {
+ group: {
+ id: 'id',
+ name: 'name',
+ confirmed: true,
+ _group: true,
+ _pendingAdd: true,
+ },
+ },
+ ]);
+
+ // We should be focused on account entry input.
+ if (cc) {
+ assert.isTrue(
+ isFocusInsideElement(
+ element.$.ccs.$.entry.$.input.$.input
+ )
+ );
+ } else {
assert.isTrue(
isFocusInsideElement(
element.$.reviewers.$.entry.$.input.$.input
)
);
+ }
+ })
+ .then(done);
+ }
- // No reviewer/CC should have been added.
- assert.equal(element.$.ccs.additions().length, 0);
- assert.equal(element.$.reviewers.additions().length, 0);
+ test('cc confirmation', done => {
+ testConfirmationDialog(done, true);
+ });
- // Reopen confirmation dialog.
- observer = overlayObserver('opened');
- if (cc) {
- element._ccPendingConfirmation = {
- group,
- count: 10,
- };
- } else {
- element._reviewerPendingConfirmation = {
- group,
- count: 10,
- };
- }
- return observer;
- })
- .then(() => {
- assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay));
- observer = overlayObserver('closed');
- MockInteractions.tap(yesButton); // Confirm the group.
- return observer;
- })
- .then(() => {
- assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
- const additions = cc ?
- element.$.ccs.additions() :
- element.$.reviewers.additions();
- assert.deepEqual(
- additions,
- [
- {
- group: {
- id: 'id',
- name: 'name',
- confirmed: true,
- _group: true,
- _pendingAdd: true,
- },
- },
- ]);
+ test('reviewer confirmation', done => {
+ testConfirmationDialog(done, false);
+ });
- // We should be focused on account entry input.
- if (cc) {
- assert.isTrue(
- isFocusInsideElement(
- element.$.ccs.$.entry.$.input.$.input
- )
- );
- } else {
- assert.isTrue(
- isFocusInsideElement(
- element.$.reviewers.$.entry.$.input.$.input
- )
- );
- }
- })
- .then(done);
- }
+ test('_getStorageLocation', () => {
+ const actual = element._getStorageLocation();
+ assert.equal(actual.changeNum, changeNum);
+ assert.equal(actual.patchNum, '@change');
+ assert.equal(actual.path, '@change');
+ });
- test('cc confirmation', done => {
- testConfirmationDialog(done, true);
+ test('_reviewersMutated when account-text-change is fired from ccs', () => {
+ flushAsynchronousOperations();
+ assert.isFalse(element._reviewersMutated);
+ assert.isTrue(element.$.ccs.allowAnyInput);
+ assert.isFalse(element.shadowRoot
+ .querySelector('#reviewers').allowAnyInput);
+ element.$.ccs.dispatchEvent(new CustomEvent('account-text-changed',
+ {bubbles: true, composed: true}));
+ assert.isTrue(element._reviewersMutated);
+ });
+
+ test('gets draft from storage on open', () => {
+ const storedDraft = 'hello world';
+ getDraftCommentStub.returns({message: storedDraft});
+ element.open();
+ assert.isTrue(getDraftCommentStub.called);
+ assert.equal(element.draft, storedDraft);
+ });
+
+ test('gets draft from storage even when text is already present', () => {
+ const storedDraft = 'hello world';
+ getDraftCommentStub.returns({message: storedDraft});
+ element.draft = 'foo bar';
+ element.open();
+ assert.isTrue(getDraftCommentStub.called);
+ assert.equal(element.draft, storedDraft);
+ });
+
+ test('blank if no stored draft', () => {
+ getDraftCommentStub.returns(null);
+ element.draft = 'foo bar';
+ element.open();
+ assert.isTrue(getDraftCommentStub.called);
+ assert.equal(element.draft, '');
+ });
+
+ test('does not check stored draft when quote is present', () => {
+ const storedDraft = 'hello world';
+ const quote = '> foo bar';
+ getDraftCommentStub.returns({message: storedDraft});
+ element.quote = quote;
+ element.open();
+ assert.isFalse(getDraftCommentStub.called);
+ assert.equal(element.draft, quote);
+ assert.isNotOk(element.quote);
+ });
+
+ test('updates stored draft on edits', () => {
+ const firstEdit = 'hello';
+ const location = element._getStorageLocation();
+
+ element.draft = firstEdit;
+ element.flushDebouncer('store');
+
+ assert.isTrue(setDraftCommentStub.calledWith(location, firstEdit));
+
+ element.draft = '';
+ element.flushDebouncer('store');
+
+ assert.isTrue(eraseDraftCommentStub.calledWith(location));
+ });
+
+ test('400 converts to human-readable server-error', done => {
+ sandbox.stub(window, 'fetch', () => {
+ const text = '....{"reviewers":{"id1":{"error":"first error"}},' +
+ '"ccs":{"id2":{"error":"second error"}}}';
+ return Promise.resolve(cloneableResponse(400, text));
});
- test('reviewer confirmation', done => {
- testConfirmationDialog(done, false);
- });
-
- test('_getStorageLocation', () => {
- const actual = element._getStorageLocation();
- assert.equal(actual.changeNum, changeNum);
- assert.equal(actual.patchNum, '@change');
- assert.equal(actual.path, '@change');
- });
-
- test('_reviewersMutated when account-text-change is fired from ccs', () => {
- flushAsynchronousOperations();
- assert.isFalse(element._reviewersMutated);
- assert.isTrue(element.$.ccs.allowAnyInput);
- assert.isFalse(element.shadowRoot
- .querySelector('#reviewers').allowAnyInput);
- element.$.ccs.dispatchEvent(new CustomEvent('account-text-changed',
- {bubbles: true, composed: true}));
- assert.isTrue(element._reviewersMutated);
- });
-
- test('gets draft from storage on open', () => {
- const storedDraft = 'hello world';
- getDraftCommentStub.returns({message: storedDraft});
- element.open();
- assert.isTrue(getDraftCommentStub.called);
- assert.equal(element.draft, storedDraft);
- });
-
- test('gets draft from storage even when text is already present', () => {
- const storedDraft = 'hello world';
- getDraftCommentStub.returns({message: storedDraft});
- element.draft = 'foo bar';
- element.open();
- assert.isTrue(getDraftCommentStub.called);
- assert.equal(element.draft, storedDraft);
- });
-
- test('blank if no stored draft', () => {
- getDraftCommentStub.returns(null);
- element.draft = 'foo bar';
- element.open();
- assert.isTrue(getDraftCommentStub.called);
- assert.equal(element.draft, '');
- });
-
- test('does not check stored draft when quote is present', () => {
- const storedDraft = 'hello world';
- const quote = '> foo bar';
- getDraftCommentStub.returns({message: storedDraft});
- element.quote = quote;
- element.open();
- assert.isFalse(getDraftCommentStub.called);
- assert.equal(element.draft, quote);
- assert.isNotOk(element.quote);
- });
-
- test('updates stored draft on edits', () => {
- const firstEdit = 'hello';
- const location = element._getStorageLocation();
-
- element.draft = firstEdit;
- element.flushDebouncer('store');
-
- assert.isTrue(setDraftCommentStub.calledWith(location, firstEdit));
-
- element.draft = '';
- element.flushDebouncer('store');
-
- assert.isTrue(eraseDraftCommentStub.calledWith(location));
- });
-
- test('400 converts to human-readable server-error', done => {
- sandbox.stub(window, 'fetch', () => {
- const text = '....{"reviewers":{"id1":{"error":"first error"}},' +
- '"ccs":{"id2":{"error":"second error"}}}';
- return Promise.resolve(cloneableResponse(400, text));
- });
-
- element.addEventListener('server-error', event => {
- if (event.target !== element) {
- return;
- }
- event.detail.response.text().then(body => {
- assert.equal(body, 'first error, second error');
- done();
- });
- });
-
- // Async tick is needed because iron-selector content is distributed and
- // distributed content requires an observer to be set up.
- flush(() => { element.send(); });
- });
-
- test('non-json 400 is treated as a normal server-error', done => {
- sandbox.stub(window, 'fetch', () => {
- const text = 'Comment validation error!';
- return Promise.resolve(cloneableResponse(400, text));
- });
-
- element.addEventListener('server-error', event => {
- if (event.target !== element) {
- return;
- }
- event.detail.response.text().then(body => {
- assert.equal(body, 'Comment validation error!');
- done();
- });
- });
-
- // Async tick is needed because iron-selector content is distributed and
- // distributed content requires an observer to be set up.
- flush(() => { element.send(); });
- });
-
- test('filterReviewerSuggestion', () => {
- const owner = makeAccount();
- const reviewer1 = makeAccount();
- const reviewer2 = makeGroup();
- const cc1 = makeAccount();
- const cc2 = makeGroup();
- let filter = element._filterReviewerSuggestionGenerator(false);
-
- element._owner = owner;
- element._reviewers = [reviewer1, reviewer2];
- element._ccs = [cc1, cc2];
-
- assert.isTrue(filter({account: makeAccount()}));
- assert.isTrue(filter({group: makeGroup()}));
-
- // Owner should be excluded.
- assert.isFalse(filter({account: owner}));
-
- // Existing and pending reviewers should be excluded when isCC = false.
- assert.isFalse(filter({account: reviewer1}));
- assert.isFalse(filter({group: reviewer2}));
-
- filter = element._filterReviewerSuggestionGenerator(true);
-
- // Existing and pending CCs should be excluded when isCC = true;.
- assert.isFalse(filter({account: cc1}));
- assert.isFalse(filter({group: cc2}));
- });
-
- test('_focusOn', () => {
- sandbox.spy(element, '_chooseFocusTarget');
- flushAsynchronousOperations();
- const textareaStub = sandbox.stub(element.$.textarea, 'async');
- const reviewerEntryStub = sandbox.stub(element.$.reviewers.focusStart,
- 'async');
- const ccStub = sandbox.stub(element.$.ccs.focusStart, 'async');
- element._focusOn();
- assert.equal(element._chooseFocusTarget.callCount, 1);
- assert.deepEqual(textareaStub.callCount, 1);
- assert.deepEqual(reviewerEntryStub.callCount, 0);
- assert.deepEqual(ccStub.callCount, 0);
-
- element._focusOn(element.FocusTarget.ANY);
- assert.equal(element._chooseFocusTarget.callCount, 2);
- assert.deepEqual(textareaStub.callCount, 2);
- assert.deepEqual(reviewerEntryStub.callCount, 0);
- assert.deepEqual(ccStub.callCount, 0);
-
- element._focusOn(element.FocusTarget.BODY);
- assert.equal(element._chooseFocusTarget.callCount, 2);
- assert.deepEqual(textareaStub.callCount, 3);
- assert.deepEqual(reviewerEntryStub.callCount, 0);
- assert.deepEqual(ccStub.callCount, 0);
-
- element._focusOn(element.FocusTarget.REVIEWERS);
- assert.equal(element._chooseFocusTarget.callCount, 2);
- assert.deepEqual(textareaStub.callCount, 3);
- assert.deepEqual(reviewerEntryStub.callCount, 1);
- assert.deepEqual(ccStub.callCount, 0);
-
- element._focusOn(element.FocusTarget.CCS);
- assert.equal(element._chooseFocusTarget.callCount, 2);
- assert.deepEqual(textareaStub.callCount, 3);
- assert.deepEqual(reviewerEntryStub.callCount, 1);
- assert.deepEqual(ccStub.callCount, 1);
- });
-
- test('_chooseFocusTarget', () => {
- element._account = null;
- assert.strictEqual(
- element._chooseFocusTarget(), element.FocusTarget.BODY);
-
- element._account = {_account_id: 1};
- assert.strictEqual(
- element._chooseFocusTarget(), element.FocusTarget.BODY);
-
- element.change.owner = {_account_id: 2};
- assert.strictEqual(
- element._chooseFocusTarget(), element.FocusTarget.BODY);
-
- element.change.owner._account_id = 1;
- element.change._reviewers = null;
- assert.strictEqual(
- element._chooseFocusTarget(), element.FocusTarget.REVIEWERS);
-
- element._reviewers = [];
- assert.strictEqual(
- element._chooseFocusTarget(), element.FocusTarget.REVIEWERS);
-
- element._reviewers.push({});
- assert.strictEqual(
- element._chooseFocusTarget(), element.FocusTarget.BODY);
- });
-
- test('only send labels that have changed', done => {
- flush(() => {
- stubSaveReview(review => {
- assert.deepEqual(review.labels, {
- 'Code-Review': 0,
- 'Verified': -1,
- });
- });
-
- element.addEventListener('send', () => {
- done();
- });
- // Without wrapping this test in flush(), the below two calls to
- // MockInteractions.tap() cause a race in some situations in shadow DOM.
- // The send button can be tapped before the others, causing the test to
- // fail.
-
- element.shadowRoot
- .querySelector('gr-label-scores').shadowRoot
- .querySelector(
- 'gr-label-score-row[name="Verified"]')
- .setSelectedValue(-1);
- MockInteractions.tap(element.shadowRoot
- .querySelector('.send'));
- });
- });
-
- test('_processReviewerChange', () => {
- const mockIndexSplices = function(toRemove) {
- return [{
- removed: [toRemove],
- }];
- };
-
- element._processReviewerChange(
- mockIndexSplices(makeAccount()), 'REVIEWER');
- assert.equal(element._reviewersPendingRemove.REVIEWER.length, 1);
- });
-
- test('_purgeReviewersPendingRemove', () => {
- const removeStub = sandbox.stub(element, '_removeAccount');
- const mock = function() {
- element._reviewersPendingRemove = {
- test: [makeAccount()],
- test2: [makeAccount(), makeAccount()],
- };
- };
- const checkObjEmpty = function(obj) {
- for (const prop in obj) {
- if (obj.hasOwnProperty(prop) && obj[prop].length) { return false; }
- }
- return true;
- };
- mock();
- element._purgeReviewersPendingRemove(true); // Cancel
- assert.isFalse(removeStub.called);
- assert.isTrue(checkObjEmpty(element._reviewersPendingRemove));
-
- mock();
- element._purgeReviewersPendingRemove(false); // Submit
- assert.isTrue(removeStub.called);
- assert.isTrue(checkObjEmpty(element._reviewersPendingRemove));
- });
-
- test('_removeAccount', done => {
- sandbox.stub(element.$.restAPI, 'removeChangeReviewer')
- .returns(Promise.resolve({ok: true}));
- const arr = [makeAccount(), makeAccount()];
- element.change.reviewers = {
- REVIEWER: arr.slice(),
- };
-
- element._removeAccount(arr[1], 'REVIEWER').then(() => {
- assert.equal(element.change.reviewers.REVIEWER.length, 1);
- assert.deepEqual(element.change.reviewers.REVIEWER, arr.slice(0, 1));
+ element.addEventListener('server-error', event => {
+ if (event.target !== element) {
+ return;
+ }
+ event.detail.response.text().then(body => {
+ assert.equal(body, 'first error, second error');
done();
});
});
- test('moving from cc to reviewer', () => {
- element._reviewersPendingRemove = {
- CC: [],
- REVIEWER: [],
- };
- flushAsynchronousOperations();
+ // Async tick is needed because iron-selector content is distributed and
+ // distributed content requires an observer to be set up.
+ flush(() => { element.send(); });
+ });
- const reviewer1 = makeAccount();
- const reviewer2 = makeAccount();
- const reviewer3 = makeAccount();
- const cc1 = makeAccount();
- const cc2 = makeAccount();
- const cc3 = makeAccount();
- const cc4 = makeAccount();
- element._reviewers = [reviewer1, reviewer2, reviewer3];
- element._ccs = [cc1, cc2, cc3, cc4];
- element.push('_reviewers', cc1);
- flushAsynchronousOperations();
-
- assert.deepEqual(element._reviewers,
- [reviewer1, reviewer2, reviewer3, cc1]);
- assert.deepEqual(element._ccs, [cc2, cc3, cc4]);
- assert.deepEqual(element._reviewersPendingRemove.CC, [cc1]);
-
- element.push('_reviewers', cc4, cc3);
- flushAsynchronousOperations();
-
- assert.deepEqual(element._reviewers,
- [reviewer1, reviewer2, reviewer3, cc1, cc4, cc3]);
- assert.deepEqual(element._ccs, [cc2]);
- assert.deepEqual(element._reviewersPendingRemove.CC, [cc1, cc4, cc3]);
+ test('non-json 400 is treated as a normal server-error', done => {
+ sandbox.stub(window, 'fetch', () => {
+ const text = 'Comment validation error!';
+ return Promise.resolve(cloneableResponse(400, text));
});
- test('moving from reviewer to cc', () => {
- element._reviewersPendingRemove = {
- CC: [],
- REVIEWER: [],
- };
- flushAsynchronousOperations();
-
- const reviewer1 = makeAccount();
- const reviewer2 = makeAccount();
- const reviewer3 = makeAccount();
- const cc1 = makeAccount();
- const cc2 = makeAccount();
- const cc3 = makeAccount();
- const cc4 = makeAccount();
- element._reviewers = [reviewer1, reviewer2, reviewer3];
- element._ccs = [cc1, cc2, cc3, cc4];
- element.push('_ccs', reviewer1);
- flushAsynchronousOperations();
-
- assert.deepEqual(element._reviewers,
- [reviewer2, reviewer3]);
- assert.deepEqual(element._ccs, [cc1, cc2, cc3, cc4, reviewer1]);
- assert.deepEqual(element._reviewersPendingRemove.REVIEWER, [reviewer1]);
-
- element.push('_ccs', reviewer3, reviewer2);
- flushAsynchronousOperations();
-
- assert.deepEqual(element._reviewers, []);
- assert.deepEqual(element._ccs,
- [cc1, cc2, cc3, cc4, reviewer1, reviewer3, reviewer2]);
- assert.deepEqual(element._reviewersPendingRemove.REVIEWER,
- [reviewer1, reviewer3, reviewer2]);
- });
-
- test('migrate reviewers between states', done => {
- element._reviewersPendingRemove = {
- CC: [],
- REVIEWER: [],
- };
- flushAsynchronousOperations();
- const reviewers = element.$.reviewers;
- const ccs = element.$.ccs;
- const reviewer1 = makeAccount();
- const reviewer2 = makeAccount();
- const cc1 = makeAccount();
- const cc2 = makeAccount();
- const cc3 = makeAccount();
- element._reviewers = [reviewer1, reviewer2];
- element._ccs = [cc1, cc2, cc3];
-
- const mutations = [];
-
- stubSaveReview(review => mutations.push(...review.reviewers));
-
- sandbox.stub(element, '_removeAccount', (account, type) => {
- mutations.push({state: 'REMOVED', account});
- return Promise.resolve();
- });
-
- // Remove and add to other field.
- reviewers.fire('remove', {account: reviewer1});
- ccs.$.entry.fire('add', {value: {account: reviewer1}});
- ccs.fire('remove', {account: cc1});
- ccs.fire('remove', {account: cc3});
- reviewers.$.entry.fire('add', {value: {account: cc1}});
-
- // Add to other field without removing from former field.
- // (Currently not possible in UI, but this is a good consistency check).
- reviewers.$.entry.fire('add', {value: {account: cc2}});
- ccs.$.entry.fire('add', {value: {account: reviewer2}});
- const mapReviewer = function(reviewer, opt_state) {
- const result = {reviewer: reviewer._account_id, confirmed: undefined};
- if (opt_state) {
- result.state = opt_state;
- }
- return result;
- };
-
- // Send and purge and verify moves, delete cc3.
- element.send()
- .then(keepReviewers =>
- element._purgeReviewersPendingRemove(false, keepReviewers))
- .then(() => {
- assert.deepEqual(
- mutations, [
- mapReviewer(cc1),
- mapReviewer(cc2),
- mapReviewer(reviewer1, 'CC'),
- mapReviewer(reviewer2, 'CC'),
- {account: cc3, state: 'REMOVED'},
- ]);
- done();
- });
- });
-
- test('emits cancel on esc key', () => {
- const cancelHandler = sandbox.spy();
- element.addEventListener('cancel', cancelHandler);
- MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc');
- flushAsynchronousOperations();
-
- assert.isTrue(cancelHandler.called);
- });
-
- test('should not send on enter key', () => {
- stubSaveReview(() => undefined);
- element.addEventListener('send', () => assert.fail('wrongly called'));
- MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
- flushAsynchronousOperations();
- });
-
- test('emit send on ctrl+enter key', done => {
- stubSaveReview(() => undefined);
- element.addEventListener('send', () => done());
- MockInteractions.pressAndReleaseKeyOn(element, 13, 'ctrl', 'enter');
- flushAsynchronousOperations();
- });
-
- test('_computeMessagePlaceholder', () => {
- assert.equal(
- element._computeMessagePlaceholder(false),
- 'Say something nice...');
- assert.equal(
- element._computeMessagePlaceholder(true),
- 'Add a note for your reviewers...');
- });
-
- test('_computeSendButtonLabel', () => {
- assert.equal(
- element._computeSendButtonLabel(false),
- 'Send');
- assert.equal(
- element._computeSendButtonLabel(true),
- 'Start review');
- });
-
- test('_handle400Error reviewrs and CCs', done => {
- const error1 = 'error 1';
- const error2 = 'error 2';
- const error3 = 'error 3';
- const text = ')]}\'' + JSON.stringify({
- reviewers: {
- username1: {
- input: 'user 1',
- error: error1,
- },
- username2: {
- input: 'user 2',
- error: error2,
- },
- },
- ccs: {
- username3: {
- input: 'user 3',
- error: error3,
- },
- },
- });
- element.addEventListener('server-error', e => {
- e.detail.response.text().then(text => {
- assert.equal(text, [error1, error2, error3].join(', '));
- done();
- });
- });
- element._handle400Error(cloneableResponse(400, text));
- });
-
- test('_handle400Error CCs only', done => {
- const error1 = 'error 1';
- const text = ')]}\'' + JSON.stringify({
- ccs: {
- username1: {
- input: 'user 1',
- error: error1,
- },
- },
- });
- element.addEventListener('server-error', e => {
- e.detail.response.text().then(text => {
- assert.equal(text, error1);
- done();
- });
- });
- element._handle400Error(cloneableResponse(400, text));
- });
-
- test('fires height change when the drafts comments load', done => {
- // Flush DOM operations before binding to the autogrow event so we don't
- // catch the events fired from the initial layout.
- flush(() => {
- const autoGrowHandler = sinon.stub();
- element.addEventListener('autogrow', autoGrowHandler);
- element.draftCommentThreads = [];
- flush(() => {
- assert.isTrue(autoGrowHandler.called);
- done();
- });
+ element.addEventListener('server-error', event => {
+ if (event.target !== element) {
+ return;
+ }
+ event.detail.response.text().then(body => {
+ assert.equal(body, 'Comment validation error!');
+ done();
});
});
- suite('post review API', () => {
- let startReviewStub;
+ // Async tick is needed because iron-selector content is distributed and
+ // distributed content requires an observer to be set up.
+ flush(() => { element.send(); });
+ });
- setup(() => {
- startReviewStub = sandbox.stub(
- element.$.restAPI,
- 'startReview',
- () => Promise.resolve());
- });
+ test('filterReviewerSuggestion', () => {
+ const owner = makeAccount();
+ const reviewer1 = makeAccount();
+ const reviewer2 = makeGroup();
+ const cc1 = makeAccount();
+ const cc2 = makeGroup();
+ let filter = element._filterReviewerSuggestionGenerator(false);
- test('ready property in review input on start review', () => {
- stubSaveReview(review => {
- assert.isTrue(review.ready);
- return {ready: true};
- });
- return element.send(true, true).then(() => {
- assert.isFalse(startReviewStub.called);
- });
- });
+ element._owner = owner;
+ element._reviewers = [reviewer1, reviewer2];
+ element._ccs = [cc1, cc2];
- test('no ready property in review input on save review', () => {
- stubSaveReview(review => {
- assert.isUndefined(review.ready);
- });
- return element.send(true, false).then(() => {
- assert.isFalse(startReviewStub.called);
- });
- });
- });
+ assert.isTrue(filter({account: makeAccount()}));
+ assert.isTrue(filter({group: makeGroup()}));
- suite('start review and save buttons', () => {
- let sendStub;
+ // Owner should be excluded.
+ assert.isFalse(filter({account: owner}));
- setup(() => {
- sendStub = sandbox.stub(element, 'send', () => Promise.resolve());
- element.canBeStarted = true;
- // Flush to make both Start/Save buttons appear in DOM.
- flushAsynchronousOperations();
- });
+ // Existing and pending reviewers should be excluded when isCC = false.
+ assert.isFalse(filter({account: reviewer1}));
+ assert.isFalse(filter({group: reviewer2}));
- test('start review sets ready', () => {
- MockInteractions.tap(element.shadowRoot
- .querySelector('.send'));
- flushAsynchronousOperations();
- assert.isTrue(sendStub.calledWith(true, true));
- });
+ filter = element._filterReviewerSuggestionGenerator(true);
- test('save review doesn\'t set ready', () => {
- MockInteractions.tap(element.shadowRoot
- .querySelector('.save'));
- flushAsynchronousOperations();
- assert.isTrue(sendStub.calledWith(true, false));
- });
- });
+ // Existing and pending CCs should be excluded when isCC = true;.
+ assert.isFalse(filter({account: cc1}));
+ assert.isFalse(filter({group: cc2}));
+ });
- test('buttons disabled until all API calls are resolved', () => {
+ test('_focusOn', () => {
+ sandbox.spy(element, '_chooseFocusTarget');
+ flushAsynchronousOperations();
+ const textareaStub = sandbox.stub(element.$.textarea, 'async');
+ const reviewerEntryStub = sandbox.stub(element.$.reviewers.focusStart,
+ 'async');
+ const ccStub = sandbox.stub(element.$.ccs.focusStart, 'async');
+ element._focusOn();
+ assert.equal(element._chooseFocusTarget.callCount, 1);
+ assert.deepEqual(textareaStub.callCount, 1);
+ assert.deepEqual(reviewerEntryStub.callCount, 0);
+ assert.deepEqual(ccStub.callCount, 0);
+
+ element._focusOn(element.FocusTarget.ANY);
+ assert.equal(element._chooseFocusTarget.callCount, 2);
+ assert.deepEqual(textareaStub.callCount, 2);
+ assert.deepEqual(reviewerEntryStub.callCount, 0);
+ assert.deepEqual(ccStub.callCount, 0);
+
+ element._focusOn(element.FocusTarget.BODY);
+ assert.equal(element._chooseFocusTarget.callCount, 2);
+ assert.deepEqual(textareaStub.callCount, 3);
+ assert.deepEqual(reviewerEntryStub.callCount, 0);
+ assert.deepEqual(ccStub.callCount, 0);
+
+ element._focusOn(element.FocusTarget.REVIEWERS);
+ assert.equal(element._chooseFocusTarget.callCount, 2);
+ assert.deepEqual(textareaStub.callCount, 3);
+ assert.deepEqual(reviewerEntryStub.callCount, 1);
+ assert.deepEqual(ccStub.callCount, 0);
+
+ element._focusOn(element.FocusTarget.CCS);
+ assert.equal(element._chooseFocusTarget.callCount, 2);
+ assert.deepEqual(textareaStub.callCount, 3);
+ assert.deepEqual(reviewerEntryStub.callCount, 1);
+ assert.deepEqual(ccStub.callCount, 1);
+ });
+
+ test('_chooseFocusTarget', () => {
+ element._account = null;
+ assert.strictEqual(
+ element._chooseFocusTarget(), element.FocusTarget.BODY);
+
+ element._account = {_account_id: 1};
+ assert.strictEqual(
+ element._chooseFocusTarget(), element.FocusTarget.BODY);
+
+ element.change.owner = {_account_id: 2};
+ assert.strictEqual(
+ element._chooseFocusTarget(), element.FocusTarget.BODY);
+
+ element.change.owner._account_id = 1;
+ element.change._reviewers = null;
+ assert.strictEqual(
+ element._chooseFocusTarget(), element.FocusTarget.REVIEWERS);
+
+ element._reviewers = [];
+ assert.strictEqual(
+ element._chooseFocusTarget(), element.FocusTarget.REVIEWERS);
+
+ element._reviewers.push({});
+ assert.strictEqual(
+ element._chooseFocusTarget(), element.FocusTarget.BODY);
+ });
+
+ test('only send labels that have changed', done => {
+ flush(() => {
stubSaveReview(review => {
+ assert.deepEqual(review.labels, {
+ 'Code-Review': 0,
+ 'Verified': -1,
+ });
+ });
+
+ element.addEventListener('send', () => {
+ done();
+ });
+ // Without wrapping this test in flush(), the below two calls to
+ // MockInteractions.tap() cause a race in some situations in shadow DOM.
+ // The send button can be tapped before the others, causing the test to
+ // fail.
+
+ element.shadowRoot
+ .querySelector('gr-label-scores').shadowRoot
+ .querySelector(
+ 'gr-label-score-row[name="Verified"]')
+ .setSelectedValue(-1);
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.send'));
+ });
+ });
+
+ test('_processReviewerChange', () => {
+ const mockIndexSplices = function(toRemove) {
+ return [{
+ removed: [toRemove],
+ }];
+ };
+
+ element._processReviewerChange(
+ mockIndexSplices(makeAccount()), 'REVIEWER');
+ assert.equal(element._reviewersPendingRemove.REVIEWER.length, 1);
+ });
+
+ test('_purgeReviewersPendingRemove', () => {
+ const removeStub = sandbox.stub(element, '_removeAccount');
+ const mock = function() {
+ element._reviewersPendingRemove = {
+ test: [makeAccount()],
+ test2: [makeAccount(), makeAccount()],
+ };
+ };
+ const checkObjEmpty = function(obj) {
+ for (const prop in obj) {
+ if (obj.hasOwnProperty(prop) && obj[prop].length) { return false; }
+ }
+ return true;
+ };
+ mock();
+ element._purgeReviewersPendingRemove(true); // Cancel
+ assert.isFalse(removeStub.called);
+ assert.isTrue(checkObjEmpty(element._reviewersPendingRemove));
+
+ mock();
+ element._purgeReviewersPendingRemove(false); // Submit
+ assert.isTrue(removeStub.called);
+ assert.isTrue(checkObjEmpty(element._reviewersPendingRemove));
+ });
+
+ test('_removeAccount', done => {
+ sandbox.stub(element.$.restAPI, 'removeChangeReviewer')
+ .returns(Promise.resolve({ok: true}));
+ const arr = [makeAccount(), makeAccount()];
+ element.change.reviewers = {
+ REVIEWER: arr.slice(),
+ };
+
+ element._removeAccount(arr[1], 'REVIEWER').then(() => {
+ assert.equal(element.change.reviewers.REVIEWER.length, 1);
+ assert.deepEqual(element.change.reviewers.REVIEWER, arr.slice(0, 1));
+ done();
+ });
+ });
+
+ test('moving from cc to reviewer', () => {
+ element._reviewersPendingRemove = {
+ CC: [],
+ REVIEWER: [],
+ };
+ flushAsynchronousOperations();
+
+ const reviewer1 = makeAccount();
+ const reviewer2 = makeAccount();
+ const reviewer3 = makeAccount();
+ const cc1 = makeAccount();
+ const cc2 = makeAccount();
+ const cc3 = makeAccount();
+ const cc4 = makeAccount();
+ element._reviewers = [reviewer1, reviewer2, reviewer3];
+ element._ccs = [cc1, cc2, cc3, cc4];
+ element.push('_reviewers', cc1);
+ flushAsynchronousOperations();
+
+ assert.deepEqual(element._reviewers,
+ [reviewer1, reviewer2, reviewer3, cc1]);
+ assert.deepEqual(element._ccs, [cc2, cc3, cc4]);
+ assert.deepEqual(element._reviewersPendingRemove.CC, [cc1]);
+
+ element.push('_reviewers', cc4, cc3);
+ flushAsynchronousOperations();
+
+ assert.deepEqual(element._reviewers,
+ [reviewer1, reviewer2, reviewer3, cc1, cc4, cc3]);
+ assert.deepEqual(element._ccs, [cc2]);
+ assert.deepEqual(element._reviewersPendingRemove.CC, [cc1, cc4, cc3]);
+ });
+
+ test('moving from reviewer to cc', () => {
+ element._reviewersPendingRemove = {
+ CC: [],
+ REVIEWER: [],
+ };
+ flushAsynchronousOperations();
+
+ const reviewer1 = makeAccount();
+ const reviewer2 = makeAccount();
+ const reviewer3 = makeAccount();
+ const cc1 = makeAccount();
+ const cc2 = makeAccount();
+ const cc3 = makeAccount();
+ const cc4 = makeAccount();
+ element._reviewers = [reviewer1, reviewer2, reviewer3];
+ element._ccs = [cc1, cc2, cc3, cc4];
+ element.push('_ccs', reviewer1);
+ flushAsynchronousOperations();
+
+ assert.deepEqual(element._reviewers,
+ [reviewer2, reviewer3]);
+ assert.deepEqual(element._ccs, [cc1, cc2, cc3, cc4, reviewer1]);
+ assert.deepEqual(element._reviewersPendingRemove.REVIEWER, [reviewer1]);
+
+ element.push('_ccs', reviewer3, reviewer2);
+ flushAsynchronousOperations();
+
+ assert.deepEqual(element._reviewers, []);
+ assert.deepEqual(element._ccs,
+ [cc1, cc2, cc3, cc4, reviewer1, reviewer3, reviewer2]);
+ assert.deepEqual(element._reviewersPendingRemove.REVIEWER,
+ [reviewer1, reviewer3, reviewer2]);
+ });
+
+ test('migrate reviewers between states', done => {
+ element._reviewersPendingRemove = {
+ CC: [],
+ REVIEWER: [],
+ };
+ flushAsynchronousOperations();
+ const reviewers = element.$.reviewers;
+ const ccs = element.$.ccs;
+ const reviewer1 = makeAccount();
+ const reviewer2 = makeAccount();
+ const cc1 = makeAccount();
+ const cc2 = makeAccount();
+ const cc3 = makeAccount();
+ element._reviewers = [reviewer1, reviewer2];
+ element._ccs = [cc1, cc2, cc3];
+
+ const mutations = [];
+
+ stubSaveReview(review => mutations.push(...review.reviewers));
+
+ sandbox.stub(element, '_removeAccount', (account, type) => {
+ mutations.push({state: 'REMOVED', account});
+ return Promise.resolve();
+ });
+
+ // Remove and add to other field.
+ reviewers.fire('remove', {account: reviewer1});
+ ccs.$.entry.fire('add', {value: {account: reviewer1}});
+ ccs.fire('remove', {account: cc1});
+ ccs.fire('remove', {account: cc3});
+ reviewers.$.entry.fire('add', {value: {account: cc1}});
+
+ // Add to other field without removing from former field.
+ // (Currently not possible in UI, but this is a good consistency check).
+ reviewers.$.entry.fire('add', {value: {account: cc2}});
+ ccs.$.entry.fire('add', {value: {account: reviewer2}});
+ const mapReviewer = function(reviewer, opt_state) {
+ const result = {reviewer: reviewer._account_id, confirmed: undefined};
+ if (opt_state) {
+ result.state = opt_state;
+ }
+ return result;
+ };
+
+ // Send and purge and verify moves, delete cc3.
+ element.send()
+ .then(keepReviewers =>
+ element._purgeReviewersPendingRemove(false, keepReviewers))
+ .then(() => {
+ assert.deepEqual(
+ mutations, [
+ mapReviewer(cc1),
+ mapReviewer(cc2),
+ mapReviewer(reviewer1, 'CC'),
+ mapReviewer(reviewer2, 'CC'),
+ {account: cc3, state: 'REMOVED'},
+ ]);
+ done();
+ });
+ });
+
+ test('emits cancel on esc key', () => {
+ const cancelHandler = sandbox.spy();
+ element.addEventListener('cancel', cancelHandler);
+ MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc');
+ flushAsynchronousOperations();
+
+ assert.isTrue(cancelHandler.called);
+ });
+
+ test('should not send on enter key', () => {
+ stubSaveReview(() => undefined);
+ element.addEventListener('send', () => assert.fail('wrongly called'));
+ MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
+ flushAsynchronousOperations();
+ });
+
+ test('emit send on ctrl+enter key', done => {
+ stubSaveReview(() => undefined);
+ element.addEventListener('send', () => done());
+ MockInteractions.pressAndReleaseKeyOn(element, 13, 'ctrl', 'enter');
+ flushAsynchronousOperations();
+ });
+
+ test('_computeMessagePlaceholder', () => {
+ assert.equal(
+ element._computeMessagePlaceholder(false),
+ 'Say something nice...');
+ assert.equal(
+ element._computeMessagePlaceholder(true),
+ 'Add a note for your reviewers...');
+ });
+
+ test('_computeSendButtonLabel', () => {
+ assert.equal(
+ element._computeSendButtonLabel(false),
+ 'Send');
+ assert.equal(
+ element._computeSendButtonLabel(true),
+ 'Start review');
+ });
+
+ test('_handle400Error reviewrs and CCs', done => {
+ const error1 = 'error 1';
+ const error2 = 'error 2';
+ const error3 = 'error 3';
+ const text = ')]}\'' + JSON.stringify({
+ reviewers: {
+ username1: {
+ input: 'user 1',
+ error: error1,
+ },
+ username2: {
+ input: 'user 2',
+ error: error2,
+ },
+ },
+ ccs: {
+ username3: {
+ input: 'user 3',
+ error: error3,
+ },
+ },
+ });
+ element.addEventListener('server-error', e => {
+ e.detail.response.text().then(text => {
+ assert.equal(text, [error1, error2, error3].join(', '));
+ done();
+ });
+ });
+ element._handle400Error(cloneableResponse(400, text));
+ });
+
+ test('_handle400Error CCs only', done => {
+ const error1 = 'error 1';
+ const text = ')]}\'' + JSON.stringify({
+ ccs: {
+ username1: {
+ input: 'user 1',
+ error: error1,
+ },
+ },
+ });
+ element.addEventListener('server-error', e => {
+ e.detail.response.text().then(text => {
+ assert.equal(text, error1);
+ done();
+ });
+ });
+ element._handle400Error(cloneableResponse(400, text));
+ });
+
+ test('fires height change when the drafts comments load', done => {
+ // Flush DOM operations before binding to the autogrow event so we don't
+ // catch the events fired from the initial layout.
+ flush(() => {
+ const autoGrowHandler = sinon.stub();
+ element.addEventListener('autogrow', autoGrowHandler);
+ element.draftCommentThreads = [];
+ flush(() => {
+ assert.isTrue(autoGrowHandler.called);
+ done();
+ });
+ });
+ });
+
+ suite('post review API', () => {
+ let startReviewStub;
+
+ setup(() => {
+ startReviewStub = sandbox.stub(
+ element.$.restAPI,
+ 'startReview',
+ () => Promise.resolve());
+ });
+
+ test('ready property in review input on start review', () => {
+ stubSaveReview(review => {
+ assert.isTrue(review.ready);
return {ready: true};
});
return element.send(true, true).then(() => {
- assert.isFalse(element.disabled);
+ assert.isFalse(startReviewStub.called);
});
});
- suite('error handling', () => {
- const expectedDraft = 'draft';
- const expectedError = new Error('test');
-
- setup(() => {
- element.draft = expectedDraft;
+ test('no ready property in review input on save review', () => {
+ stubSaveReview(review => {
+ assert.isUndefined(review.ready);
});
-
- function assertDialogOpenAndEnabled() {
- assert.strictEqual(expectedDraft, element.draft);
- assert.isFalse(element.disabled);
- }
-
- test('error occurs in _saveReview', () => {
- stubSaveReview(review => {
- throw expectedError;
- });
- return element.send(true, true).catch(err => {
- assert.strictEqual(expectedError, err);
- assertDialogOpenAndEnabled();
- });
+ return element.send(true, false).then(() => {
+ assert.isFalse(startReviewStub.called);
});
-
- suite('pending diff drafts?', () => {
- test('yes', () => {
- const promise = mockPromise();
- const refreshHandler = sandbox.stub();
-
- element.addEventListener('comment-refresh', refreshHandler);
- sandbox.stub(element.$.restAPI, 'hasPendingDiffDrafts').returns(true);
- element.$.restAPI._pendingRequests.sendDiffDraft = [promise];
- element.open();
-
- assert.isFalse(refreshHandler.called);
- assert.isTrue(element._savingComments);
-
- promise.resolve();
-
- return element.$.restAPI.awaitPendingDiffDrafts().then(() => {
- assert.isTrue(refreshHandler.called);
- assert.isFalse(element._savingComments);
- });
- });
-
- test('no', () => {
- sandbox.stub(element.$.restAPI, 'hasPendingDiffDrafts').returns(false);
- element.open();
- assert.notOk(element._savingComments);
- });
- });
- });
-
- test('_computeSendButtonDisabled', () => {
- const fn = element._computeSendButtonDisabled.bind(element);
- assert.isFalse(fn(
- /* buttonLabel= */ 'Start review',
- /* draftCommentThreads= */ [],
- /* text= */ '',
- /* reviewersMutated= */ false,
- /* labelsChanged= */ false,
- /* includeComments= */ false,
- /* disabled= */ false
- ));
- assert.isTrue(fn(
- /* buttonLabel= */ 'Send',
- /* draftCommentThreads= */ [],
- /* text= */ '',
- /* reviewersMutated= */ false,
- /* labelsChanged= */ false,
- /* includeComments= */ false,
- /* disabled= */ false
- ));
- // Mock nonempty comment draft array, with seding comments.
- assert.isFalse(fn(
- /* buttonLabel= */ 'Send',
- /* draftCommentThreads= */ [{comments: [{__draft: true}]}],
- /* text= */ '',
- /* reviewersMutated= */ false,
- /* labelsChanged= */ false,
- /* includeComments= */ true,
- /* disabled= */ false
- ));
- // Mock nonempty comment draft array, without seding comments.
- assert.isTrue(fn(
- /* buttonLabel= */ 'Send',
- /* draftCommentThreads= */ [{comments: [{__draft: true}]}],
- /* text= */ '',
- /* reviewersMutated= */ false,
- /* labelsChanged= */ false,
- /* includeComments= */ false,
- /* disabled= */ false
- ));
- // Mock nonempty change message.
- assert.isFalse(fn(
- /* buttonLabel= */ 'Send',
- /* draftCommentThreads= */ {},
- /* text= */ 'test',
- /* reviewersMutated= */ false,
- /* labelsChanged= */ false,
- /* includeComments= */ false,
- /* disabled= */ false
- ));
- // Mock reviewers mutated.
- assert.isFalse(fn(
- /* buttonLabel= */ 'Send',
- /* draftCommentThreads= */ {},
- /* text= */ '',
- /* reviewersMutated= */ true,
- /* labelsChanged= */ false,
- /* includeComments= */ false,
- /* disabled= */ false
- ));
- // Mock labels changed.
- assert.isFalse(fn(
- /* buttonLabel= */ 'Send',
- /* draftCommentThreads= */ {},
- /* text= */ '',
- /* reviewersMutated= */ false,
- /* labelsChanged= */ true,
- /* includeComments= */ false,
- /* disabled= */ false
- ));
- // Whole dialog is disabled.
- assert.isTrue(fn(
- /* buttonLabel= */ 'Send',
- /* draftCommentThreads= */ {},
- /* text= */ '',
- /* reviewersMutated= */ false,
- /* labelsChanged= */ true,
- /* includeComments= */ false,
- /* disabled= */ true
- ));
- });
-
- test('_submit blocked when no mutations exist', () => {
- const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve());
- // Stub the below function to avoid side effects from the send promise
- // resolving.
- sandbox.stub(element, '_purgeReviewersPendingRemove');
- element.draftCommentThreads = [];
- flushAsynchronousOperations();
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button.send'));
- assert.isFalse(sendStub.called);
-
- element.draftCommentThreads = [{comments: [{__draft: true}]}];
- flushAsynchronousOperations();
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button.send'));
- assert.isTrue(sendStub.called);
- });
-
- test('getFocusStops', () => {
- // Setting draftCommentThreads to an empty object causes _sendDisabled to be
- // computed to false.
- element.draftCommentThreads = [];
- assert.equal(element.getFocusStops().end, element.$.cancelButton);
- element.draftCommentThreads = [{comments: [{__draft: true}]}];
- assert.equal(element.getFocusStops().end, element.$.sendButton);
- });
-
- test('setPluginMessage', () => {
- element.setPluginMessage('foo');
- assert.equal(element.$.pluginMessage.textContent, 'foo');
});
});
+
+ suite('start review and save buttons', () => {
+ let sendStub;
+
+ setup(() => {
+ sendStub = sandbox.stub(element, 'send', () => Promise.resolve());
+ element.canBeStarted = true;
+ // Flush to make both Start/Save buttons appear in DOM.
+ flushAsynchronousOperations();
+ });
+
+ test('start review sets ready', () => {
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.send'));
+ flushAsynchronousOperations();
+ assert.isTrue(sendStub.calledWith(true, true));
+ });
+
+ test('save review doesn\'t set ready', () => {
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.save'));
+ flushAsynchronousOperations();
+ assert.isTrue(sendStub.calledWith(true, false));
+ });
+ });
+
+ test('buttons disabled until all API calls are resolved', () => {
+ stubSaveReview(review => {
+ return {ready: true};
+ });
+ return element.send(true, true).then(() => {
+ assert.isFalse(element.disabled);
+ });
+ });
+
+ suite('error handling', () => {
+ const expectedDraft = 'draft';
+ const expectedError = new Error('test');
+
+ setup(() => {
+ element.draft = expectedDraft;
+ });
+
+ function assertDialogOpenAndEnabled() {
+ assert.strictEqual(expectedDraft, element.draft);
+ assert.isFalse(element.disabled);
+ }
+
+ test('error occurs in _saveReview', () => {
+ stubSaveReview(review => {
+ throw expectedError;
+ });
+ return element.send(true, true).catch(err => {
+ assert.strictEqual(expectedError, err);
+ assertDialogOpenAndEnabled();
+ });
+ });
+
+ suite('pending diff drafts?', () => {
+ test('yes', () => {
+ const promise = mockPromise();
+ const refreshHandler = sandbox.stub();
+
+ element.addEventListener('comment-refresh', refreshHandler);
+ sandbox.stub(element.$.restAPI, 'hasPendingDiffDrafts').returns(true);
+ element.$.restAPI._pendingRequests.sendDiffDraft = [promise];
+ element.open();
+
+ assert.isFalse(refreshHandler.called);
+ assert.isTrue(element._savingComments);
+
+ promise.resolve();
+
+ return element.$.restAPI.awaitPendingDiffDrafts().then(() => {
+ assert.isTrue(refreshHandler.called);
+ assert.isFalse(element._savingComments);
+ });
+ });
+
+ test('no', () => {
+ sandbox.stub(element.$.restAPI, 'hasPendingDiffDrafts').returns(false);
+ element.open();
+ assert.notOk(element._savingComments);
+ });
+ });
+ });
+
+ test('_computeSendButtonDisabled', () => {
+ const fn = element._computeSendButtonDisabled.bind(element);
+ assert.isFalse(fn(
+ /* buttonLabel= */ 'Start review',
+ /* draftCommentThreads= */ [],
+ /* text= */ '',
+ /* reviewersMutated= */ false,
+ /* labelsChanged= */ false,
+ /* includeComments= */ false,
+ /* disabled= */ false
+ ));
+ assert.isTrue(fn(
+ /* buttonLabel= */ 'Send',
+ /* draftCommentThreads= */ [],
+ /* text= */ '',
+ /* reviewersMutated= */ false,
+ /* labelsChanged= */ false,
+ /* includeComments= */ false,
+ /* disabled= */ false
+ ));
+ // Mock nonempty comment draft array, with seding comments.
+ assert.isFalse(fn(
+ /* buttonLabel= */ 'Send',
+ /* draftCommentThreads= */ [{comments: [{__draft: true}]}],
+ /* text= */ '',
+ /* reviewersMutated= */ false,
+ /* labelsChanged= */ false,
+ /* includeComments= */ true,
+ /* disabled= */ false
+ ));
+ // Mock nonempty comment draft array, without seding comments.
+ assert.isTrue(fn(
+ /* buttonLabel= */ 'Send',
+ /* draftCommentThreads= */ [{comments: [{__draft: true}]}],
+ /* text= */ '',
+ /* reviewersMutated= */ false,
+ /* labelsChanged= */ false,
+ /* includeComments= */ false,
+ /* disabled= */ false
+ ));
+ // Mock nonempty change message.
+ assert.isFalse(fn(
+ /* buttonLabel= */ 'Send',
+ /* draftCommentThreads= */ {},
+ /* text= */ 'test',
+ /* reviewersMutated= */ false,
+ /* labelsChanged= */ false,
+ /* includeComments= */ false,
+ /* disabled= */ false
+ ));
+ // Mock reviewers mutated.
+ assert.isFalse(fn(
+ /* buttonLabel= */ 'Send',
+ /* draftCommentThreads= */ {},
+ /* text= */ '',
+ /* reviewersMutated= */ true,
+ /* labelsChanged= */ false,
+ /* includeComments= */ false,
+ /* disabled= */ false
+ ));
+ // Mock labels changed.
+ assert.isFalse(fn(
+ /* buttonLabel= */ 'Send',
+ /* draftCommentThreads= */ {},
+ /* text= */ '',
+ /* reviewersMutated= */ false,
+ /* labelsChanged= */ true,
+ /* includeComments= */ false,
+ /* disabled= */ false
+ ));
+ // Whole dialog is disabled.
+ assert.isTrue(fn(
+ /* buttonLabel= */ 'Send',
+ /* draftCommentThreads= */ {},
+ /* text= */ '',
+ /* reviewersMutated= */ false,
+ /* labelsChanged= */ true,
+ /* includeComments= */ false,
+ /* disabled= */ true
+ ));
+ });
+
+ test('_submit blocked when no mutations exist', () => {
+ const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve());
+ // Stub the below function to avoid side effects from the send promise
+ // resolving.
+ sandbox.stub(element, '_purgeReviewersPendingRemove');
+ element.draftCommentThreads = [];
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button.send'));
+ assert.isFalse(sendStub.called);
+
+ element.draftCommentThreads = [{comments: [{__draft: true}]}];
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button.send'));
+ assert.isTrue(sendStub.called);
+ });
+
+ test('getFocusStops', () => {
+ // Setting draftCommentThreads to an empty object causes _sendDisabled to be
+ // computed to false.
+ element.draftCommentThreads = [];
+ assert.equal(element.getFocusStops().end, element.$.cancelButton);
+ element.draftCommentThreads = [{comments: [{__draft: true}]}];
+ assert.equal(element.getFocusStops().end, element.$.sendButton);
+ });
+
+ test('setPluginMessage', () => {
+ element.setPluginMessage('foo');
+ assert.equal(element.$.pluginMessage.textContent, 'foo');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
deleted file mode 100644
index 132ce11..0000000
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
+++ /dev/null
@@ -1,76 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-reviewer-list">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- }
- :host([disabled]) {
- opacity: .8;
- pointer-events: none;
- }
- .container {
- display: block;
- /* This is a bit of a hack. We tried to use margin-top with
- :not(:first-child) before, but :first-child does not understand
- whether a child is visible or not. So adding a margin for every
- child and then a negative one at the top does the trick. */
- margin-top: calc(0px - var(--spacing-s));
- }
- .container > * {
- margin-top: var(--spacing-s);
- }
- gr-button {
- --gr-button: {
- padding: 0px 0px;
- }
- }
- </style>
- <div class="container">
- <template is="dom-repeat" items="[[_displayedReviewers]]" as="reviewer">
- <gr-account-chip class="reviewer" account="[[reviewer]]"
- on-remove="_handleRemove"
- additional-text="[[_computeReviewerTooltip(reviewer, change)]]"
- removable="[[_computeCanRemoveReviewer(reviewer, mutable)]]">
- </gr-account-chip>
- </template>
- <gr-button
- class="hiddenReviewers"
- link
- hidden$="[[!_hiddenReviewerCount]]"
- on-click="_handleViewAll">and [[_hiddenReviewerCount]] more</gr-button>
- <div class="controlsContainer" hidden$="[[!mutable]]">
- <gr-button
- link
- id="addReviewer"
- class="addReviewer"
- on-click="_handleAddTap">[[_addLabel]]</gr-button>
- </div>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-reviewer-list.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index ddc6275..57337fb 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,276 +14,284 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../shared/gr-account-chip/gr-account-chip.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-reviewer-list_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrReviewerList extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-reviewer-list'; }
+ /**
+ * Fired when the "Add reviewer..." button is tapped.
+ *
+ * @event show-reply-dialog
+ */
+
+ static get properties() {
+ return {
+ change: Object,
+ disabled: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ mutable: {
+ type: Boolean,
+ value: false,
+ },
+ reviewersOnly: {
+ type: Boolean,
+ value: false,
+ },
+ ccsOnly: {
+ type: Boolean,
+ value: false,
+ },
+ maxReviewersDisplayed: Number,
+
+ _displayedReviewers: {
+ type: Array,
+ value() { return []; },
+ },
+ _reviewers: {
+ type: Array,
+ value() { return []; },
+ },
+ _showInput: {
+ type: Boolean,
+ value: false,
+ },
+ _addLabel: {
+ type: String,
+ computed: '_computeAddLabel(ccsOnly)',
+ },
+ _hiddenReviewerCount: {
+ type: Number,
+ computed: '_computeHiddenCount(_reviewers, _displayedReviewers)',
+ },
+
+ // Used for testing.
+ _lastAutocompleteRequest: Object,
+ _xhrPromise: Object,
+ };
+ }
+
+ static get observers() {
+ return [
+ '_reviewersChanged(change.reviewers.*, change.owner)',
+ ];
+ }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * 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]
+ * }]
*/
- class GrReviewerList extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-reviewer-list'; }
- /**
- * Fired when the "Add reviewer..." button is tapped.
- *
- * @event show-reply-dialog
- */
-
- static get properties() {
+ _permittedLabelsToNumericScores(labels) {
+ if (!labels) return [];
+ return Object.keys(labels).map(label => {
return {
- change: Object,
- disabled: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- mutable: {
- type: Boolean,
- value: false,
- },
- reviewersOnly: {
- type: Boolean,
- value: false,
- },
- ccsOnly: {
- type: Boolean,
- value: false,
- },
- maxReviewersDisplayed: Number,
-
- _displayedReviewers: {
- type: Array,
- value() { return []; },
- },
- _reviewers: {
- type: Array,
- value() { return []; },
- },
- _showInput: {
- type: Boolean,
- value: false,
- },
- _addLabel: {
- type: String,
- computed: '_computeAddLabel(ccsOnly)',
- },
- _hiddenReviewerCount: {
- type: Number,
- computed: '_computeHiddenCount(_reviewers, _displayedReviewers)',
- },
-
- // Used for testing.
- _lastAutocompleteRequest: Object,
- _xhrPromise: Object,
+ label,
+ scores: labels[label].map(v => parseInt(v, 10)),
};
- }
+ });
+ }
- static get observers() {
- return [
- '_reviewersChanged(change.reviewers.*, change.owner)',
- ];
- }
+ /**
+ * Returns hash of labels to max permitted score.
+ *
+ * @param {!Object} change
+ * @returns {!Object} labels to max permitted scores hash
+ */
+ _getMaxPermittedScores(change) {
+ return this._permittedLabelsToNumericScores(change.permitted_labels)
+ .map(({label, scores}) => {
+ return {
+ [label]: scores
+ .map(v => parseInt(v, 10))
+ .reduce((a, b) => Math.max(a, b))};
+ })
+ .reduce((acc, i) => Object.assign(acc, i), {});
+ }
- /**
- * 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) {
- if (!labels) return [];
- return Object.keys(labels).map(label => {
- return {
- label,
- scores: labels[label].map(v => parseInt(v, 10)),
- };
- });
- }
-
- /**
- * Returns hash of labels to max permitted score.
- *
- * @param {!Object} change
- * @returns {!Object} labels to max permitted scores hash
- */
- _getMaxPermittedScores(change) {
- return this._permittedLabelsToNumericScores(change.permitted_labels)
- .map(({label, scores}) => {
- return {
- [label]: scores
- .map(v => parseInt(v, 10))
- .reduce((a, b) => Math.max(a, b))};
- })
- .reduce((acc, i) => Object.assign(acc, i), {});
- }
-
- /**
- * Returns max permitted score for reviewer.
- *
- * @param {!Object} reviewer
- * @param {!Object} change
- * @param {string} label
- * @return {number}
- */
- _getReviewerPermittedScore(reviewer, change, label) {
- // 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[label].all) { return NaN; }
- const detailed = change.labels[label].all.filter(
- ({_account_id}) => reviewer._account_id === _account_id).pop();
- if (!detailed) {
- return NaN;
- }
- if (detailed.hasOwnProperty('permitted_voting_range')) {
- return detailed.permitted_voting_range.max;
- } else if (detailed.hasOwnProperty('value')) {
- // If preset, user can vote on the label.
- return 0;
- }
+ /**
+ * Returns max permitted score for reviewer.
+ *
+ * @param {!Object} reviewer
+ * @param {!Object} change
+ * @param {string} label
+ * @return {number}
+ */
+ _getReviewerPermittedScore(reviewer, change, label) {
+ // 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[label].all) { return NaN; }
+ const detailed = change.labels[label].all.filter(
+ ({_account_id}) => reviewer._account_id === _account_id).pop();
+ if (!detailed) {
return NaN;
}
+ if (detailed.hasOwnProperty('permitted_voting_range')) {
+ return detailed.permitted_voting_range.max;
+ } else if (detailed.hasOwnProperty('value')) {
+ // If preset, user can vote on the label.
+ return 0;
+ }
+ return NaN;
+ }
- _computeReviewerTooltip(reviewer, change) {
- if (!change || !change.labels) { return ''; }
- const maxScores = [];
- const maxPermitted = this._getMaxPermittedScores(change);
- for (const label of Object.keys(change.labels)) {
- const maxScore =
- this._getReviewerPermittedScore(reviewer, change, label);
- if (isNaN(maxScore) || maxScore < 0) { continue; }
- if (maxScore > 0 && maxScore === maxPermitted[label]) {
- maxScores.push(`${label}: +${maxScore}`);
- } else {
- maxScores.push(`${label}`);
- }
- }
- if (maxScores.length) {
- return 'Votable: ' + maxScores.join(', ');
+ _computeVoteableText(reviewer, change) {
+ if (!change || !change.labels) { return ''; }
+ const maxScores = [];
+ const maxPermitted = this._getMaxPermittedScores(change);
+ for (const label of Object.keys(change.labels)) {
+ const maxScore =
+ this._getReviewerPermittedScore(reviewer, change, label);
+ if (isNaN(maxScore) || maxScore < 0) { continue; }
+ if (maxScore > 0 && maxScore === maxPermitted[label]) {
+ maxScores.push(`${label}: +${maxScore}`);
} else {
- return '';
+ maxScores.push(`${label}`);
}
}
+ return maxScores.join(', ');
+ }
- _reviewersChanged(changeRecord, owner) {
- // Polymer 2: check for undefined
- if ([changeRecord, owner].some(arg => arg === undefined)) {
- return;
- }
-
- let result = [];
- const reviewers = changeRecord.base;
- for (const key in reviewers) {
- if (this.reviewersOnly && key !== 'REVIEWER') {
- continue;
- }
- if (this.ccsOnly && key !== 'CC') {
- continue;
- }
- if (key === 'REVIEWER' || key === 'CC') {
- result = result.concat(reviewers[key]);
- }
- }
- this._reviewers = result
- .filter(reviewer => reviewer._account_id != owner._account_id);
-
- // If there is one or two more than the max reviewers, don't show the
- // 'show more' button, because it takes up just as much space.
- if (this.maxReviewersDisplayed &&
- this._reviewers.length > this.maxReviewersDisplayed + 2) {
- this._displayedReviewers =
- this._reviewers.slice(0, this.maxReviewersDisplayed);
- } else {
- this._displayedReviewers = this._reviewers;
- }
+ _reviewersChanged(changeRecord, owner) {
+ // Polymer 2: check for undefined
+ if ([changeRecord, owner].some(arg => arg === undefined)) {
+ return;
}
- _computeHiddenCount(reviewers, displayedReviewers) {
- // Polymer 2: check for undefined
- if ([reviewers, displayedReviewers].some(arg => arg === undefined)) {
- return undefined;
+ let result = [];
+ const reviewers = changeRecord.base;
+ for (const key in reviewers) {
+ if (this.reviewersOnly && key !== 'REVIEWER') {
+ continue;
}
-
- return reviewers.length - displayedReviewers.length;
- }
-
- _computeCanRemoveReviewer(reviewer, mutable) {
- if (!mutable) { return false; }
-
- let current;
- for (let i = 0; i < this.change.removable_reviewers.length; i++) {
- current = this.change.removable_reviewers[i];
- if (current._account_id === reviewer._account_id ||
- (!reviewer._account_id && current.email === reviewer.email)) {
- return true;
- }
+ if (this.ccsOnly && key !== 'CC') {
+ continue;
}
- return false;
- }
-
- _handleRemove(e) {
- e.preventDefault();
- const target = Polymer.dom(e).rootTarget;
- if (!target.account) { return; }
- const accountID = target.account._account_id || target.account.email;
- this.disabled = true;
- this._xhrPromise = this._removeReviewer(accountID).then(response => {
- this.disabled = false;
- if (!response.ok) { return response; }
-
- const reviewers = this.change.reviewers;
-
- for (const type of ['REVIEWER', 'CC']) {
- reviewers[type] = reviewers[type] || [];
- for (let i = 0; i < reviewers[type].length; i++) {
- if (reviewers[type][i]._account_id == accountID ||
- reviewers[type][i].email == accountID) {
- this.splice('change.reviewers.' + type, i, 1);
- break;
- }
- }
- }
- })
- .catch(err => {
- this.disabled = false;
- throw err;
- });
- }
-
- _handleAddTap(e) {
- e.preventDefault();
- const value = {};
- if (this.reviewersOnly) {
- value.reviewersOnly = true;
+ if (key === 'REVIEWER' || key === 'CC') {
+ result = result.concat(reviewers[key]);
}
- if (this.ccsOnly) {
- value.ccsOnly = true;
- }
- this.fire('show-reply-dialog', {value});
}
+ this._reviewers = result
+ .filter(reviewer => reviewer._account_id != owner._account_id);
- _handleViewAll(e) {
+ // If there is one or two more than the max reviewers, don't show the
+ // 'show more' button, because it takes up just as much space.
+ if (this.maxReviewersDisplayed &&
+ this._reviewers.length > this.maxReviewersDisplayed + 2) {
+ this._displayedReviewers =
+ this._reviewers.slice(0, this.maxReviewersDisplayed);
+ } else {
this._displayedReviewers = this._reviewers;
}
-
- _removeReviewer(id) {
- return this.$.restAPI.removeChangeReviewer(this.change._number, id);
- }
-
- _computeAddLabel(ccsOnly) {
- return ccsOnly ? 'Add CC' : 'Add reviewer';
- }
}
- customElements.define(GrReviewerList.is, GrReviewerList);
-})();
+ _computeHiddenCount(reviewers, displayedReviewers) {
+ // Polymer 2: check for undefined
+ if ([reviewers, displayedReviewers].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ return reviewers.length - displayedReviewers.length;
+ }
+
+ _computeCanRemoveReviewer(reviewer, mutable) {
+ if (!mutable) { return false; }
+
+ let current;
+ for (let i = 0; i < this.change.removable_reviewers.length; i++) {
+ current = this.change.removable_reviewers[i];
+ if (current._account_id === reviewer._account_id ||
+ (!reviewer._account_id && current.email === reviewer.email)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ _handleRemove(e) {
+ e.preventDefault();
+ const target = dom(e).rootTarget;
+ if (!target.account) { return; }
+ const accountID = target.account._account_id || target.account.email;
+ this.disabled = true;
+ this._xhrPromise = this._removeReviewer(accountID).then(response => {
+ this.disabled = false;
+ if (!response.ok) { return response; }
+
+ const reviewers = this.change.reviewers;
+
+ for (const type of ['REVIEWER', 'CC']) {
+ reviewers[type] = reviewers[type] || [];
+ for (let i = 0; i < reviewers[type].length; i++) {
+ if (reviewers[type][i]._account_id == accountID ||
+ reviewers[type][i].email == accountID) {
+ this.splice('change.reviewers.' + type, i, 1);
+ break;
+ }
+ }
+ }
+ })
+ .catch(err => {
+ this.disabled = false;
+ throw err;
+ });
+ }
+
+ _handleAddTap(e) {
+ e.preventDefault();
+ const value = {};
+ if (this.reviewersOnly) {
+ value.reviewersOnly = true;
+ }
+ if (this.ccsOnly) {
+ value.ccsOnly = true;
+ }
+ this.fire('show-reply-dialog', {value});
+ }
+
+ _handleViewAll(e) {
+ this._displayedReviewers = this._reviewers;
+ }
+
+ _removeReviewer(id) {
+ return this.$.restAPI.removeChangeReviewer(this.change._number, id);
+ }
+
+ _computeAddLabel(ccsOnly) {
+ return ccsOnly ? 'Add CC' : 'Add reviewer';
+ }
+}
+
+customElements.define(GrReviewerList.is, GrReviewerList);
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.js
new file mode 100644
index 0000000..c5df61d
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.js
@@ -0,0 +1,56 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ }
+ :host([disabled]) {
+ opacity: .8;
+ pointer-events: none;
+ }
+ .container {
+ display: block;
+ /* This is a bit of a hack. We tried to use margin-top with
+ :not(:first-child) before, but :first-child does not understand
+ whether a child is visible or not. So adding a margin for every
+ child and then a negative one at the top does the trick. */
+ margin-top: calc(0px - var(--spacing-s));
+ }
+ .container > * {
+ margin-top: var(--spacing-s);
+ }
+ gr-button {
+ --gr-button: {
+ padding: 0px 0px;
+ }
+ }
+ </style>
+ <div class="container">
+ <template is="dom-repeat" items="[[_displayedReviewers]]" as="reviewer">
+ <gr-account-chip class="reviewer" account="[[reviewer]]" on-remove="_handleRemove" voteable-text="[[_computeVoteableText(reviewer, change)]]" removable="[[_computeCanRemoveReviewer(reviewer, mutable)]]">
+ </gr-account-chip>
+ </template>
+ <gr-button class="hiddenReviewers" link="" hidden\$="[[!_hiddenReviewerCount]]" on-click="_handleViewAll">and [[_hiddenReviewerCount]] more</gr-button>
+ <div class="controlsContainer" hidden\$="[[!mutable]]">
+ <gr-button link="" id="addReviewer" class="addReviewer" on-click="_handleAddTap">[[_addLabel]]</gr-button>
+ </div>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index be65a86..ab60078 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reviewer-list</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-reviewer-list.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,78 +30,65 @@
</template>
</test-fixture>
-<script>
- suite('gr-reviewer-list tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-reviewer-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-reviewer-list tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- removeChangeReviewer() {
- return Promise.resolve({ok: true});
- },
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ removeChangeReviewer() {
+ return Promise.resolve({ok: true});
+ },
});
+ });
- teardown(() => {
- sandbox.restore();
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('controls hidden on immutable element', () => {
+ element.mutable = false;
+ assert.isTrue(element.shadowRoot
+ .querySelector('.controlsContainer').hasAttribute('hidden'));
+ element.mutable = true;
+ assert.isFalse(element.shadowRoot
+ .querySelector('.controlsContainer').hasAttribute('hidden'));
+ });
+
+ test('add reviewer button opens reply dialog', done => {
+ element.addEventListener('show-reply-dialog', () => {
+ done();
});
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.addReviewer'));
+ });
- test('controls hidden on immutable element', () => {
- element.mutable = false;
- assert.isTrue(element.shadowRoot
- .querySelector('.controlsContainer').hasAttribute('hidden'));
- element.mutable = true;
- assert.isFalse(element.shadowRoot
- .querySelector('.controlsContainer').hasAttribute('hidden'));
- });
-
- test('add reviewer button opens reply dialog', done => {
- element.addEventListener('show-reply-dialog', () => {
- done();
- });
- MockInteractions.tap(element.shadowRoot
- .querySelector('.addReviewer'));
- });
-
- test('only show remove for removable reviewers', () => {
- element.mutable = true;
- element.change = {
- owner: {
- _account_id: 1,
- },
- reviewers: {
- REVIEWER: [
- {
- _account_id: 2,
- name: 'Bojack Horseman',
- email: 'SecretariatRulez96@hotmail.com',
- },
- {
- _account_id: 3,
- name: 'Pinky Penguin',
- },
- ],
- CC: [
- {
- _account_id: 4,
- name: 'Diane Nguyen',
- email: 'macarthurfellow2B@juno.com',
- },
- {
- email: 'test@e.mail',
- },
- ],
- },
- removable_reviewers: [
+ test('only show remove for removable reviewers', () => {
+ element.mutable = true;
+ element.change = {
+ owner: {
+ _account_id: 1,
+ },
+ reviewers: {
+ REVIEWER: [
+ {
+ _account_id: 2,
+ name: 'Bojack Horseman',
+ email: 'SecretariatRulez96@hotmail.com',
+ },
{
_account_id: 3,
name: 'Pinky Penguin',
},
+ ],
+ CC: [
{
_account_id: 4,
name: 'Diane Nguyen',
@@ -116,230 +98,245 @@
email: 'test@e.mail',
},
],
- };
- flushAsynchronousOperations();
- const chips =
- Polymer.dom(element.root).querySelectorAll('gr-account-chip');
- assert.equal(chips.length, 4);
+ },
+ removable_reviewers: [
+ {
+ _account_id: 3,
+ name: 'Pinky Penguin',
+ },
+ {
+ _account_id: 4,
+ name: 'Diane Nguyen',
+ email: 'macarthurfellow2B@juno.com',
+ },
+ {
+ email: 'test@e.mail',
+ },
+ ],
+ };
+ flushAsynchronousOperations();
+ const chips =
+ dom(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);
+ for (const el of Array.from(chips)) {
+ const accountID = el.account._account_id || el.account.email;
+ assert.ok(accountID);
- const buttonEl = el.shadowRoot
- .querySelector('gr-button');
- assert.isNotNull(buttonEl);
- if (accountID == 2) {
- assert.isTrue(buttonEl.hasAttribute('hidden'));
- } else {
- assert.isFalse(buttonEl.hasAttribute('hidden'));
- }
+ const buttonEl = el.shadowRoot
+ .querySelector('gr-button');
+ assert.isNotNull(buttonEl);
+ if (accountID == 2) {
+ assert.isTrue(buttonEl.hasAttribute('hidden'));
+ } else {
+ assert.isFalse(buttonEl.hasAttribute('hidden'));
}
- });
-
- test('tracking reviewers and ccs', () => {
- let counter = 0;
- function makeAccount() {
- return {_account_id: counter++};
- }
-
- const owner = makeAccount();
- const reviewer = makeAccount();
- const cc = makeAccount();
- const reviewers = {
- REMOVED: [makeAccount()],
- REVIEWER: [owner, reviewer],
- CC: [owner, cc],
- };
-
- element.ccsOnly = false;
- element.reviewersOnly = false;
- element.change = {
- owner,
- reviewers,
- };
- assert.deepEqual(element._reviewers, [reviewer, cc]);
-
- element.reviewersOnly = true;
- element.change = {
- owner,
- reviewers,
- };
- assert.deepEqual(element._reviewers, [reviewer]);
-
- element.ccsOnly = true;
- element.reviewersOnly = false;
- element.change = {
- owner,
- reviewers,
- };
- assert.deepEqual(element._reviewers, [cc]);
- });
-
- test('_handleAddTap passes mode with event', () => {
- const fireStub = sandbox.stub(element, 'fire');
- const e = {preventDefault() {}};
-
- element.ccsOnly = false;
- element.reviewersOnly = false;
- element._handleAddTap(e);
- assert.isTrue(fireStub.calledWith('show-reply-dialog', {value: {}}));
-
- element.reviewersOnly = true;
- element._handleAddTap(e);
- assert.isTrue(fireStub.lastCall.calledWith('show-reply-dialog',
- {value: {reviewersOnly: true}}));
-
- element.ccsOnly = true;
- element.reviewersOnly = false;
- element._handleAddTap(e);
- assert.isTrue(fireStub.lastCall.calledWith('show-reply-dialog',
- {value: {ccsOnly: true}}));
- });
-
- test('no show all reviewers button with 6 reviewers', () => {
- const reviewers = [];
- element.maxReviewersDisplayed = 5;
- for (let i = 0; i < 6; i++) {
- reviewers.push(
- {email: i+'reviewer@google.com', name: 'reviewer-' + i});
- }
- element.ccsOnly = true;
-
- element.change = {
- owner: {
- _account_id: 1,
- },
- reviewers: {
- CC: reviewers,
- },
- };
- assert.equal(element._hiddenReviewerCount, 0);
- assert.equal(element._displayedReviewers.length, 6);
- assert.equal(element._reviewers.length, 6);
- assert.isTrue(element.shadowRoot
- .querySelector('.hiddenReviewers').hidden);
- });
-
- test('show all reviewers button with 8 reviewers', () => {
- const reviewers = [];
- element.maxReviewersDisplayed = 5;
- for (let i = 0; i < 8; i++) {
- reviewers.push(
- {email: i+'reviewer@google.com', name: 'reviewer-' + i});
- }
- element.ccsOnly = true;
-
- element.change = {
- owner: {
- _account_id: 1,
- },
- reviewers: {
- CC: reviewers,
- },
- };
- assert.equal(element._hiddenReviewerCount, 3);
- assert.equal(element._displayedReviewers.length, 5);
- assert.equal(element._reviewers.length, 8);
- assert.isFalse(element.shadowRoot
- .querySelector('.hiddenReviewers').hidden);
- });
-
- test('no maxReviewersDisplayed', () => {
- const reviewers = [];
- for (let i = 0; i < 7; i++) {
- reviewers.push(
- {email: i+'reviewer@google.com', name: 'reviewer-' + i});
- }
- element.ccsOnly = true;
-
- element.change = {
- owner: {
- _account_id: 1,
- },
- reviewers: {
- CC: reviewers,
- },
- };
- assert.equal(element._hiddenReviewerCount, 0);
- assert.equal(element._displayedReviewers.length, 7);
- assert.equal(element._reviewers.length, 7);
- assert.isTrue(element.shadowRoot
- .querySelector('.hiddenReviewers').hidden);
- });
-
- test('show all reviewers button', () => {
- const reviewers = [];
- element.maxReviewersDisplayed = 5;
- for (let i = 0; i < 100; i++) {
- reviewers.push(
- {email: i+'reviewer@google.com', name: 'reviewer-' + i});
- }
- element.ccsOnly = true;
-
- element.change = {
- owner: {
- _account_id: 1,
- },
- reviewers: {
- CC: reviewers,
- },
- };
- assert.equal(element._hiddenReviewerCount, 95);
- assert.equal(element._displayedReviewers.length, 5);
- assert.equal(element._reviewers.length, 100);
- assert.isFalse(element.shadowRoot
- .querySelector('.hiddenReviewers').hidden);
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('.hiddenReviewers'));
-
- assert.equal(element._hiddenReviewerCount, 0);
- assert.equal(element._displayedReviewers.length, 100);
- assert.equal(element._reviewers.length, 100);
- assert.isTrue(element.shadowRoot
- .querySelector('.hiddenReviewers').hidden);
- });
-
- test('votable labels', () => {
- const change = {
- labels: {
- Foo: {
- all: [{_account_id: 7, permitted_voting_range: {max: 2}}],
- },
- Bar: {
- all: [{_account_id: 1, permitted_voting_range: {max: 1}},
- {_account_id: 7, permitted_voting_range: {max: 1}}],
- },
- FooBar: {
- all: [{_account_id: 7, value: 0}],
- },
- },
- permitted_labels: {
- Foo: ['-1', ' 0', '+1', '+2'],
- FooBar: ['-1', ' 0'],
- },
- };
- assert.strictEqual(
- element._computeReviewerTooltip({_account_id: 1}, change),
- 'Votable: Bar');
- assert.strictEqual(
- element._computeReviewerTooltip({_account_id: 7}, change),
- 'Votable: Foo: +2, Bar, FooBar');
- assert.strictEqual(
- element._computeReviewerTooltip({_account_id: 2}, change),
- '');
- });
-
- test('fails gracefully when all is not included', () => {
- const change = {
- labels: {Foo: {}},
- permitted_labels: {
- Foo: ['-1', ' 0', '+1', '+2'],
- },
- };
- assert.strictEqual(
- element._computeReviewerTooltip({_account_id: 1}, change), '');
- });
+ }
});
+
+ test('tracking reviewers and ccs', () => {
+ let counter = 0;
+ function makeAccount() {
+ return {_account_id: counter++};
+ }
+
+ const owner = makeAccount();
+ const reviewer = makeAccount();
+ const cc = makeAccount();
+ const reviewers = {
+ REMOVED: [makeAccount()],
+ REVIEWER: [owner, reviewer],
+ CC: [owner, cc],
+ };
+
+ element.ccsOnly = false;
+ element.reviewersOnly = false;
+ element.change = {
+ owner,
+ reviewers,
+ };
+ assert.deepEqual(element._reviewers, [reviewer, cc]);
+
+ element.reviewersOnly = true;
+ element.change = {
+ owner,
+ reviewers,
+ };
+ assert.deepEqual(element._reviewers, [reviewer]);
+
+ element.ccsOnly = true;
+ element.reviewersOnly = false;
+ element.change = {
+ owner,
+ reviewers,
+ };
+ assert.deepEqual(element._reviewers, [cc]);
+ });
+
+ test('_handleAddTap passes mode with event', () => {
+ const fireStub = sandbox.stub(element, 'fire');
+ const e = {preventDefault() {}};
+
+ element.ccsOnly = false;
+ element.reviewersOnly = false;
+ element._handleAddTap(e);
+ assert.isTrue(fireStub.calledWith('show-reply-dialog', {value: {}}));
+
+ element.reviewersOnly = true;
+ element._handleAddTap(e);
+ assert.isTrue(fireStub.lastCall.calledWith('show-reply-dialog',
+ {value: {reviewersOnly: true}}));
+
+ element.ccsOnly = true;
+ element.reviewersOnly = false;
+ element._handleAddTap(e);
+ assert.isTrue(fireStub.lastCall.calledWith('show-reply-dialog',
+ {value: {ccsOnly: true}}));
+ });
+
+ test('no show all reviewers button with 6 reviewers', () => {
+ const reviewers = [];
+ element.maxReviewersDisplayed = 5;
+ for (let i = 0; i < 6; i++) {
+ reviewers.push(
+ {email: i+'reviewer@google.com', name: 'reviewer-' + i});
+ }
+ element.ccsOnly = true;
+
+ element.change = {
+ owner: {
+ _account_id: 1,
+ },
+ reviewers: {
+ CC: reviewers,
+ },
+ };
+ assert.equal(element._hiddenReviewerCount, 0);
+ assert.equal(element._displayedReviewers.length, 6);
+ assert.equal(element._reviewers.length, 6);
+ assert.isTrue(element.shadowRoot
+ .querySelector('.hiddenReviewers').hidden);
+ });
+
+ test('show all reviewers button with 8 reviewers', () => {
+ const reviewers = [];
+ element.maxReviewersDisplayed = 5;
+ for (let i = 0; i < 8; i++) {
+ reviewers.push(
+ {email: i+'reviewer@google.com', name: 'reviewer-' + i});
+ }
+ element.ccsOnly = true;
+
+ element.change = {
+ owner: {
+ _account_id: 1,
+ },
+ reviewers: {
+ CC: reviewers,
+ },
+ };
+ assert.equal(element._hiddenReviewerCount, 3);
+ assert.equal(element._displayedReviewers.length, 5);
+ assert.equal(element._reviewers.length, 8);
+ assert.isFalse(element.shadowRoot
+ .querySelector('.hiddenReviewers').hidden);
+ });
+
+ test('no maxReviewersDisplayed', () => {
+ const reviewers = [];
+ for (let i = 0; i < 7; i++) {
+ reviewers.push(
+ {email: i+'reviewer@google.com', name: 'reviewer-' + i});
+ }
+ element.ccsOnly = true;
+
+ element.change = {
+ owner: {
+ _account_id: 1,
+ },
+ reviewers: {
+ CC: reviewers,
+ },
+ };
+ assert.equal(element._hiddenReviewerCount, 0);
+ assert.equal(element._displayedReviewers.length, 7);
+ assert.equal(element._reviewers.length, 7);
+ assert.isTrue(element.shadowRoot
+ .querySelector('.hiddenReviewers').hidden);
+ });
+
+ test('show all reviewers button', () => {
+ const reviewers = [];
+ element.maxReviewersDisplayed = 5;
+ for (let i = 0; i < 100; i++) {
+ reviewers.push(
+ {email: i+'reviewer@google.com', name: 'reviewer-' + i});
+ }
+ element.ccsOnly = true;
+
+ element.change = {
+ owner: {
+ _account_id: 1,
+ },
+ reviewers: {
+ CC: reviewers,
+ },
+ };
+ assert.equal(element._hiddenReviewerCount, 95);
+ assert.equal(element._displayedReviewers.length, 5);
+ assert.equal(element._reviewers.length, 100);
+ assert.isFalse(element.shadowRoot
+ .querySelector('.hiddenReviewers').hidden);
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.hiddenReviewers'));
+
+ assert.equal(element._hiddenReviewerCount, 0);
+ assert.equal(element._displayedReviewers.length, 100);
+ assert.equal(element._reviewers.length, 100);
+ assert.isTrue(element.shadowRoot
+ .querySelector('.hiddenReviewers').hidden);
+ });
+
+ test('votable labels', () => {
+ const change = {
+ labels: {
+ Foo: {
+ all: [{_account_id: 7, permitted_voting_range: {max: 2}}],
+ },
+ Bar: {
+ all: [{_account_id: 1, permitted_voting_range: {max: 1}},
+ {_account_id: 7, permitted_voting_range: {max: 1}}],
+ },
+ FooBar: {
+ all: [{_account_id: 7, value: 0}],
+ },
+ },
+ permitted_labels: {
+ Foo: ['-1', ' 0', '+1', '+2'],
+ FooBar: ['-1', ' 0'],
+ },
+ };
+ assert.strictEqual(
+ element._computeVoteableText({_account_id: 1}, change),
+ 'Bar');
+ assert.strictEqual(
+ element._computeVoteableText({_account_id: 7}, change),
+ 'Foo: +2, Bar, FooBar');
+ assert.strictEqual(
+ element._computeVoteableText({_account_id: 2}, change),
+ '');
+ });
+
+ test('fails gracefully when all is not included', () => {
+ const change = {
+ labels: {Foo: {}},
+ permitted_labels: {
+ Foo: ['-1', ' 0', '+1', '+2'],
+ },
+ };
+ assert.strictEqual(
+ element._computeVoteableText({_account_id: 1}, change), '');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
deleted file mode 100644
index f04a39a..0000000
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
+++ /dev/null
@@ -1,103 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-comment-thread/gr-comment-thread.html">
-
-<dom-module id="gr-thread-list">
- <template>
- <style include="shared-styles">
- #threads {
- display: block;
- padding: var(--spacing-l);
- }
- gr-comment-thread {
- display: block;
- margin-bottom: var(--spacing-m);
- max-width: 80ch;
- }
- .header {
- align-items: center;
- background-color: var(--table-header-background-color);
- border-bottom: 1px solid var(--border-color);
- border-top: 1px solid var(--border-color);
- display: flex;
- justify-content: left;
- min-height: 3.2em;
- padding: var(--spacing-m) var(--spacing-l);
- }
- .toggleItem.draftToggle {
- display: none;
- }
- .toggleItem.draftToggle.show {
- display: flex;
- }
- .toggleItem {
- align-items: center;
- display: flex;
- margin-right: var(--spacing-l);
- }
- .draftsOnly:not(.unresolvedOnly) gr-comment-thread[has-draft],
- .unresolvedOnly:not(.draftsOnly) gr-comment-thread[unresolved],
- .draftsOnly.unresolvedOnly gr-comment-thread[has-draft][unresolved] {
- display: block
- }
- </style>
- <template is="dom-if" if="[[!hideToggleButtons]]">
- <div class="header">
- <div class="toggleItem">
- <paper-toggle-button
- id="unresolvedToggle"
- checked="{{_unresolvedOnly}}"></paper-toggle-button>
- Only unresolved threads</div>
- <div class$="toggleItem draftToggle [[_computeShowDraftToggle(loggedIn)]]">
- <paper-toggle-button
- id="draftToggle"
- checked="{{_draftsOnly}}"></paper-toggle-button>
- Only threads with drafts</div>
- </div>
- </template>
- <div id="threads">
- <template is="dom-if" if="[[!threads.length]]">
- [[_computeNoThreadsMessage(tab)]]
- </template>
- <template
- is="dom-repeat"
- items="[[_filteredThreads]]"
- as="thread"
- initial-count="5"
- target-framerate="60">
- <gr-comment-thread
- show-file-path
- change-num="[[changeNum]]"
- comments="[[thread.comments]]"
- comment-side="[[thread.commentSide]]"
- project-name="[[change.project]]"
- is-on-parent="[[_isOnParent(thread.commentSide)]]"
- line-num="[[thread.line]]"
- patch-num="[[thread.patchNum]]"
- path="[[thread.path]]"
- root-id="{{thread.rootId}}"
- on-thread-changed="_handleCommentsChanged"
- on-thread-discard="_handleThreadDiscard"></gr-comment-thread>
- </template>
- </div>
- </template>
- <script src="gr-thread-list.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
index e99367e..850bfb4 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
@@ -14,200 +14,209 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '@polymer/paper-toggle-button/paper-toggle-button.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-comment-thread/gr-comment-thread.js';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-thread-list_html.js';
+
+/**
+ * Fired when a comment is saved or deleted
+ *
+ * @event thread-list-modified
+ * @extends Polymer.Element
+ */
+const NO_THREADS_MESSAGE = 'There are no inline comment threads on any diff '
+ + 'for this change.';
+const NO_ROBOT_COMMENTS_THREADS_MESSAGE = 'There are no findings for this ' +
+ 'patchset.';
+const FINDINGS_TAB_NAME = '__gerrit_internal_findings';
+
+class GrThreadList extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-thread-list'; }
+
+ static get properties() {
+ return {
+ /** @type {?} */
+ change: Object,
+ threads: Array,
+ changeNum: String,
+ loggedIn: Boolean,
+ _sortedThreads: {
+ type: Array,
+ },
+ _filteredThreads: {
+ type: Array,
+ computed: '_computeFilteredThreads(_sortedThreads, ' +
+ '_unresolvedOnly, _draftsOnly,' +
+ 'onlyShowRobotCommentsWithHumanReply)',
+ },
+ _unresolvedOnly: {
+ type: Boolean,
+ value: false,
+ },
+ _draftsOnly: {
+ type: Boolean,
+ value: false,
+ },
+ /* Boolean properties used must default to false if passed as attribute
+ by the parent */
+ onlyShowRobotCommentsWithHumanReply: {
+ type: Boolean,
+ value: false,
+ },
+ hideToggleButtons: {
+ type: Boolean,
+ value: false,
+ },
+ tab: {
+ type: String,
+ value: '',
+ },
+ };
+ }
+
+ static get observers() { return ['_computeSortedThreads(threads.*)']; }
+
+ _computeShowDraftToggle(loggedIn) {
+ return loggedIn ? 'show' : '';
+ }
+
+ _computeNoThreadsMessage(tab) {
+ if (tab === FINDINGS_TAB_NAME) {
+ return NO_ROBOT_COMMENTS_THREADS_MESSAGE;
+ }
+ return NO_THREADS_MESSAGE;
+ }
/**
- * Fired when a comment is saved or deleted
+ * Order as follows:
+ * - Unresolved threads with drafts (reverse chronological)
+ * - Unresolved threads without drafts (reverse chronological)
+ * - Resolved threads with drafts (reverse chronological)
+ * - Resolved threads without drafts (reverse chronological)
*
- * @event thread-list-modified
- * @extends Polymer.Element
+ * @param {!Object} changeRecord
*/
- const NO_THREADS_MESSAGE = 'There are no inline comment threads on any diff '
- + 'for this change.';
- const NO_ROBOT_COMMENTS_THREADS_MESSAGE = 'There are no findings for this ' +
- 'patchset.';
- const FINDINGS_TAB_NAME = '__gerrit_internal_findings';
+ _computeSortedThreads(changeRecord) {
+ const threads = changeRecord.base;
+ if (!threads) { return []; }
+ this._updateSortedThreads(threads);
+ }
- class GrThreadList extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-thread-list'; }
-
- static get properties() {
- return {
- /** @type {?} */
- change: Object,
- threads: Array,
- changeNum: String,
- loggedIn: Boolean,
- _sortedThreads: {
- type: Array,
- },
- _filteredThreads: {
- type: Array,
- computed: '_computeFilteredThreads(_sortedThreads, ' +
- '_unresolvedOnly, _draftsOnly,' +
- 'onlyShowRobotCommentsWithHumanReply)',
- },
- _unresolvedOnly: {
- type: Boolean,
- value: false,
- },
- _draftsOnly: {
- type: Boolean,
- value: false,
- },
- /* Boolean properties used must default to false if passed as attribute
- by the parent */
- onlyShowRobotCommentsWithHumanReply: {
- type: Boolean,
- value: false,
- },
- hideToggleButtons: {
- type: Boolean,
- value: false,
- },
- tab: {
- type: String,
- value: '',
- },
- };
- }
-
- static get observers() { return ['_computeSortedThreads(threads.*)']; }
-
- _computeShowDraftToggle(loggedIn) {
- return loggedIn ? 'show' : '';
- }
-
- _computeNoThreadsMessage(tab) {
- if (tab === FINDINGS_TAB_NAME) {
- return NO_ROBOT_COMMENTS_THREADS_MESSAGE;
- }
- return NO_THREADS_MESSAGE;
- }
-
- /**
- * Order as follows:
- * - Unresolved threads with drafts (reverse chronological)
- * - Unresolved threads without drafts (reverse chronological)
- * - Resolved threads with drafts (reverse chronological)
- * - Resolved threads without drafts (reverse chronological)
- *
- * @param {!Object} changeRecord
- */
- _computeSortedThreads(changeRecord) {
- const threads = changeRecord.base;
- if (!threads) { return []; }
- this._updateSortedThreads(threads);
- }
-
- _updateSortedThreads(threads) {
- this._sortedThreads =
- threads.map(this._getThreadWithSortInfo).sort((c1, c2) => {
- const c1Date = c1.__date || util.parseDate(c1.updated);
- const c2Date = c2.__date || util.parseDate(c2.updated);
- const dateCompare = c2Date - c1Date;
- if (c2.unresolved || c1.unresolved) {
- if (!c1.unresolved) { return 1; }
- if (!c2.unresolved) { return -1; }
- }
- if (c2.hasDraft || c1.hasDraft) {
- if (!c1.hasDraft) { return 1; }
- if (!c2.hasDraft) { return -1; }
- }
-
- if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) {
- return 0;
- }
- return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
- });
- }
-
- _computeFilteredThreads(sortedThreads, unresolvedOnly, draftsOnly,
- onlyShowRobotCommentsWithHumanReply) {
- // Polymer 2: check for undefined
- if ([
- sortedThreads,
- unresolvedOnly,
- draftsOnly,
- onlyShowRobotCommentsWithHumanReply,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- return sortedThreads.filter(c => {
- if (draftsOnly) {
- return c.hasDraft;
- } else if (unresolvedOnly) {
- return c.unresolved;
- } else {
- const comments = c && c.thread && c.thread.comments;
- let robotComment = false;
- let humanReplyToRobotComment = false;
- comments.forEach(comment => {
- if (comment.robot_id) {
- robotComment = true;
- } else if (robotComment) {
- // Robot comment exists and human comment exists after it
- humanReplyToRobotComment = true;
- }
- });
- if (robotComment && onlyShowRobotCommentsWithHumanReply) {
- return humanReplyToRobotComment;
+ _updateSortedThreads(threads) {
+ this._sortedThreads =
+ threads.map(this._getThreadWithSortInfo).sort((c1, c2) => {
+ const c1Date = c1.__date || util.parseDate(c1.updated);
+ const c2Date = c2.__date || util.parseDate(c2.updated);
+ const dateCompare = c2Date - c1Date;
+ if (c2.unresolved || c1.unresolved) {
+ if (!c1.unresolved) { return 1; }
+ if (!c2.unresolved) { return -1; }
}
- return c;
- }
- }).map(threadInfo => threadInfo.thread);
+ if (c2.hasDraft || c1.hasDraft) {
+ if (!c1.hasDraft) { return 1; }
+ if (!c2.hasDraft) { return -1; }
+ }
+
+ if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) {
+ return 0;
+ }
+ return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
+ });
+ }
+
+ _computeFilteredThreads(sortedThreads, unresolvedOnly, draftsOnly,
+ onlyShowRobotCommentsWithHumanReply) {
+ // Polymer 2: check for undefined
+ if ([
+ sortedThreads,
+ unresolvedOnly,
+ draftsOnly,
+ onlyShowRobotCommentsWithHumanReply,
+ ].some(arg => arg === undefined)) {
+ return undefined;
}
- _getThreadWithSortInfo(thread) {
- const lastComment = thread.comments[thread.comments.length - 1] || {};
-
- const lastNonDraftComment =
- (lastComment.__draft && thread.comments.length > 1) ?
- thread.comments[thread.comments.length - 2] :
- lastComment;
-
- return {
- thread,
- // Use the unresolved bit for the last non draft comment. This is what
- // anybody other than the current user would see.
- unresolved: !!lastNonDraftComment.unresolved,
- hasDraft: !!lastComment.__draft,
- updated: lastComment.updated,
- };
- }
-
- removeThread(rootId) {
- for (let i = 0; i < this.threads.length; i++) {
- if (this.threads[i].rootId === rootId) {
- this.splice('threads', i, 1);
- // Needed to ensure threads get re-rendered in the correct order.
- Polymer.dom.flush();
- return;
+ return sortedThreads.filter(c => {
+ if (draftsOnly) {
+ return c.hasDraft;
+ } else if (unresolvedOnly) {
+ return c.unresolved;
+ } else {
+ const comments = c && c.thread && c.thread.comments;
+ let robotComment = false;
+ let humanReplyToRobotComment = false;
+ comments.forEach(comment => {
+ if (comment.robot_id) {
+ robotComment = true;
+ } else if (robotComment) {
+ // Robot comment exists and human comment exists after it
+ humanReplyToRobotComment = true;
+ }
+ });
+ if (robotComment && onlyShowRobotCommentsWithHumanReply) {
+ return humanReplyToRobotComment;
}
+ return c;
}
- }
+ }).map(threadInfo => threadInfo.thread);
+ }
- _handleThreadDiscard(e) {
- this.removeThread(e.detail.rootId);
- }
+ _getThreadWithSortInfo(thread) {
+ const lastComment = thread.comments[thread.comments.length - 1] || {};
- _handleCommentsChanged(e) {
- // Reset threads so thread computations occur on deep array changes to
- // threads comments that are not observed naturally.
- this._updateSortedThreads(this.threads);
+ const lastNonDraftComment =
+ (lastComment.__draft && thread.comments.length > 1) ?
+ thread.comments[thread.comments.length - 2] :
+ lastComment;
- this.dispatchEvent(new CustomEvent('thread-list-modified',
- {detail: {rootId: e.detail.rootId, path: e.detail.path}}));
- }
+ return {
+ thread,
+ // Use the unresolved bit for the last non draft comment. This is what
+ // anybody other than the current user would see.
+ unresolved: !!lastNonDraftComment.unresolved,
+ hasDraft: !!lastComment.__draft,
+ updated: lastComment.updated,
+ };
+ }
- _isOnParent(side) {
- return !!side;
+ removeThread(rootId) {
+ for (let i = 0; i < this.threads.length; i++) {
+ if (this.threads[i].rootId === rootId) {
+ this.splice('threads', i, 1);
+ // Needed to ensure threads get re-rendered in the correct order.
+ flush();
+ return;
+ }
}
}
- customElements.define(GrThreadList.is, GrThreadList);
-})();
+ _handleThreadDiscard(e) {
+ this.removeThread(e.detail.rootId);
+ }
+
+ _handleCommentsChanged(e) {
+ // Reset threads so thread computations occur on deep array changes to
+ // threads comments that are not observed naturally.
+ this._updateSortedThreads(this.threads);
+
+ this.dispatchEvent(new CustomEvent('thread-list-modified',
+ {detail: {rootId: e.detail.rootId, path: e.detail.path}}));
+ }
+
+ _isOnParent(side) {
+ return !!side;
+ }
+}
+
+customElements.define(GrThreadList.is, GrThreadList);
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js
new file mode 100644
index 0000000..b9f0b01
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js
@@ -0,0 +1,75 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ #threads {
+ display: block;
+ padding: var(--spacing-l);
+ }
+ gr-comment-thread {
+ display: block;
+ margin-bottom: var(--spacing-m);
+ max-width: 80ch;
+ }
+ .header {
+ align-items: center;
+ background-color: var(--table-header-background-color);
+ border-bottom: 1px solid var(--border-color);
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ justify-content: left;
+ min-height: 3.2em;
+ padding: var(--spacing-m) var(--spacing-l);
+ }
+ .toggleItem.draftToggle {
+ display: none;
+ }
+ .toggleItem.draftToggle.show {
+ display: flex;
+ }
+ .toggleItem {
+ align-items: center;
+ display: flex;
+ margin-right: var(--spacing-l);
+ }
+ .draftsOnly:not(.unresolvedOnly) gr-comment-thread[has-draft],
+ .unresolvedOnly:not(.draftsOnly) gr-comment-thread[unresolved],
+ .draftsOnly.unresolvedOnly gr-comment-thread[has-draft][unresolved] {
+ display: block
+ }
+ </style>
+ <template is="dom-if" if="[[!hideToggleButtons]]">
+ <div class="header">
+ <div class="toggleItem">
+ <paper-toggle-button id="unresolvedToggle" checked="{{_unresolvedOnly}}"></paper-toggle-button>
+ Only unresolved threads</div>
+ <div class\$="toggleItem draftToggle [[_computeShowDraftToggle(loggedIn)]]">
+ <paper-toggle-button id="draftToggle" checked="{{_draftsOnly}}"></paper-toggle-button>
+ Only threads with drafts</div>
+ </div>
+ </template>
+ <div id="threads">
+ <template is="dom-if" if="[[!threads.length]]">
+ [[_computeNoThreadsMessage(tab)]]
+ </template>
+ <template is="dom-repeat" items="[[_filteredThreads]]" as="thread" initial-count="5" target-framerate="60">
+ <gr-comment-thread show-file-path="" change-num="[[changeNum]]" comments="[[thread.comments]]" comment-side="[[thread.commentSide]]" project-name="[[change.project]]" is-on-parent="[[_isOnParent(thread.commentSide)]]" line-num="[[thread.line]]" patch-num="[[thread.patchNum]]" path="[[thread.path]]" root-id="{{thread.rootId}}" on-thread-changed="_handleCommentsChanged" on-thread-discard="_handleThreadDiscard"></gr-comment-thread>
+ </template>
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
index 30c598a..cb6ba28 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-thread-list</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-thread-list.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,338 +30,340 @@
</template>
</test-fixture>
-<script>
- suite('gr-thread-list tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let threadElements;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-thread-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-thread-list tests', () => {
+ let element;
+ let sandbox;
+ let threadElements;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.onlyShowRobotCommentsWithHumanReply = true;
- element.threads = [
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'ecf0b9fa_fe1a5f62',
- line: 5,
- updated: '2018-02-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.onlyShowRobotCommentsWithHumanReply = true;
+ element.threads = [
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
},
- {
- id: '503008e2_0ab203ee',
- path: '/COMMIT_MSG',
- line: 5,
- in_reply_to: 'ecf0b9fa_fe1a5f62',
- updated: '2018-02-13 22:48:48.018000000',
- message: 'draft',
- unresolved: false,
- __draft: true,
- __draftID: '0.m683trwff68',
- __editing: false,
- patch_set: '2',
+ patch_set: 4,
+ id: 'ecf0b9fa_fe1a5f62',
+ line: 5,
+ updated: '2018-02-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ },
+ {
+ id: '503008e2_0ab203ee',
+ path: '/COMMIT_MSG',
+ line: 5,
+ in_reply_to: 'ecf0b9fa_fe1a5f62',
+ updated: '2018-02-13 22:48:48.018000000',
+ message: 'draft',
+ unresolved: false,
+ __draft: true,
+ __draftID: '0.m683trwff68',
+ __editing: false,
+ patch_set: '2',
+ },
+ ],
+ patchNum: 4,
+ path: '/COMMIT_MSG',
+ line: 5,
+ rootId: 'ecf0b9fa_fe1a5f62',
+ start_datetime: '2018-02-08 18:49:18.000000000',
+ },
+ {
+ comments: [
+ {
+ __path: 'test.txt',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
},
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 5,
- rootId: 'ecf0b9fa_fe1a5f62',
- start_datetime: '2018-02-08 18:49:18.000000000',
- },
- {
- comments: [
- {
- __path: 'test.txt',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 3,
- id: '09a9fb0a_1484e6cf',
- side: 'PARENT',
- updated: '2018-02-13 22:47:19.000000000',
- message: 'Some comment on another patchset.',
- unresolved: false,
+ patch_set: 3,
+ id: '09a9fb0a_1484e6cf',
+ side: 'PARENT',
+ updated: '2018-02-13 22:47:19.000000000',
+ message: 'Some comment on another patchset.',
+ unresolved: false,
+ },
+ ],
+ patchNum: 3,
+ path: 'test.txt',
+ rootId: '09a9fb0a_1484e6cf',
+ start_datetime: '2018-02-13 22:47:19.000000000',
+ commentSide: 'PARENT',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
},
- ],
- patchNum: 3,
- path: 'test.txt',
- rootId: '09a9fb0a_1484e6cf',
- start_datetime: '2018-02-13 22:47:19.000000000',
- commentSide: 'PARENT',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 2,
- id: '8caddf38_44770ec1',
- line: 4,
- updated: '2018-02-13 22:48:40.000000000',
- message: 'Another unresolved comment',
- unresolved: true,
+ patch_set: 2,
+ id: '8caddf38_44770ec1',
+ line: 4,
+ updated: '2018-02-13 22:48:40.000000000',
+ message: 'Another unresolved comment',
+ unresolved: true,
+ },
+ ],
+ patchNum: 2,
+ path: '/COMMIT_MSG',
+ line: 4,
+ rootId: '8caddf38_44770ec1',
+ start_datetime: '2018-02-13 22:48:40.000000000',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
},
- ],
- patchNum: 2,
- path: '/COMMIT_MSG',
- line: 4,
- rootId: '8caddf38_44770ec1',
- start_datetime: '2018-02-13 22:48:40.000000000',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 2,
- id: 'scaddf38_44770ec1',
- line: 4,
- updated: '2018-02-14 22:48:40.000000000',
- message: 'Yet another unresolved comment',
- unresolved: true,
+ patch_set: 2,
+ id: 'scaddf38_44770ec1',
+ line: 4,
+ updated: '2018-02-14 22:48:40.000000000',
+ message: 'Yet another unresolved comment',
+ unresolved: true,
+ },
+ ],
+ patchNum: 2,
+ path: '/COMMIT_MSG',
+ line: 4,
+ rootId: 'scaddf38_44770ec1',
+ start_datetime: '2018-02-14 22:48:40.000000000',
+ },
+ {
+ comments: [
+ {
+ id: 'zcf0b9fa_fe1a5f62',
+ path: '/COMMIT_MSG',
+ line: 6,
+ updated: '2018-02-15 22:48:48.018000000',
+ message: 'resolved draft',
+ unresolved: false,
+ __draft: true,
+ __draftID: '0.m683trwff68',
+ __editing: false,
+ patch_set: '2',
+ },
+ ],
+ patchNum: 4,
+ path: '/COMMIT_MSG',
+ line: 6,
+ rootId: 'zcf0b9fa_fe1a5f62',
+ start_datetime: '2018-02-09 18:49:18.000000000',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
},
- ],
- patchNum: 2,
- path: '/COMMIT_MSG',
- line: 4,
- rootId: 'scaddf38_44770ec1',
- start_datetime: '2018-02-14 22:48:40.000000000',
- },
- {
- comments: [
- {
- id: 'zcf0b9fa_fe1a5f62',
- path: '/COMMIT_MSG',
- line: 6,
- updated: '2018-02-15 22:48:48.018000000',
- message: 'resolved draft',
- unresolved: false,
- __draft: true,
- __draftID: '0.m683trwff68',
- __editing: false,
- patch_set: '2',
+ patch_set: 4,
+ id: 'rc1',
+ line: 5,
+ updated: '2019-02-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ robot_id: 'rc1',
+ },
+ ],
+ patchNum: 4,
+ path: '/COMMIT_MSG',
+ line: 5,
+ rootId: 'rc1',
+ start_datetime: '2019-02-08 18:49:18.000000000',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
},
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 6,
- rootId: 'zcf0b9fa_fe1a5f62',
- start_datetime: '2018-02-09 18:49:18.000000000',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'rc1',
- line: 5,
- updated: '2019-02-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- robot_id: 'rc1',
+ patch_set: 4,
+ id: 'rc2',
+ line: 5,
+ updated: '2019-03-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ robot_id: 'rc2',
+ },
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
},
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 5,
- rootId: 'rc1',
- start_datetime: '2019-02-08 18:49:18.000000000',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'rc2',
- line: 5,
- updated: '2019-03-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- robot_id: 'rc2',
- },
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'c2_1',
- line: 5,
- updated: '2019-03-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- },
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 5,
- rootId: 'rc2',
- start_datetime: '2019-03-08 18:49:18.000000000',
- },
- ];
- flushAsynchronousOperations();
- threadElements = Polymer.dom(element.root)
- .querySelectorAll('gr-comment-thread');
- });
+ patch_set: 4,
+ id: 'c2_1',
+ line: 5,
+ updated: '2019-03-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ },
+ ],
+ patchNum: 4,
+ path: '/COMMIT_MSG',
+ line: 5,
+ rootId: 'rc2',
+ start_datetime: '2019-03-08 18:49:18.000000000',
+ },
+ ];
+ flushAsynchronousOperations();
+ threadElements = dom(element.root)
+ .querySelectorAll('gr-comment-thread');
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('draft toggle only appears when logged in', () => {
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('.draftToggle')).display,
- 'none');
- element.loggedIn = true;
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('.draftToggle')).display,
- 'none');
- });
+ test('draft toggle only appears when logged in', () => {
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('.draftToggle')).display,
+ 'none');
+ element.loggedIn = true;
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('.draftToggle')).display,
+ 'none');
+ });
- test('there are five threads by default', () => {
- assert.equal(Polymer.dom(element.root)
- .querySelectorAll('gr-comment-thread').length, 5);
- });
+ test('there are five threads by default', () => {
+ assert.equal(dom(element.root)
+ .querySelectorAll('gr-comment-thread').length, 5);
+ });
- test('_computeSortedThreads', () => {
- assert.equal(element._sortedThreads.length, 7);
- // Draft and unresolved
- assert.equal(element._sortedThreads[0].thread.rootId,
- 'ecf0b9fa_fe1a5f62');
- // Unresolved robot comment
- assert.equal(element._sortedThreads[1].thread.rootId,
- 'rc2');
- // Unresolved robot comment
- assert.equal(element._sortedThreads[2].thread.rootId,
- 'rc1');
- // unresolved
- assert.equal(element._sortedThreads[3].thread.rootId,
- 'scaddf38_44770ec1');
- // unresolved
- assert.equal(element._sortedThreads[4].thread.rootId,
- '8caddf38_44770ec1');
- // resolved and draft
- assert.equal(element._sortedThreads[5].thread.rootId,
- 'zcf0b9fa_fe1a5f62');
- // resolved
- assert.equal(element._sortedThreads[6].thread.rootId,
- '09a9fb0a_1484e6cf');
- });
+ test('_computeSortedThreads', () => {
+ assert.equal(element._sortedThreads.length, 7);
+ // Draft and unresolved
+ assert.equal(element._sortedThreads[0].thread.rootId,
+ 'ecf0b9fa_fe1a5f62');
+ // Unresolved robot comment
+ assert.equal(element._sortedThreads[1].thread.rootId,
+ 'rc2');
+ // Unresolved robot comment
+ assert.equal(element._sortedThreads[2].thread.rootId,
+ 'rc1');
+ // unresolved
+ assert.equal(element._sortedThreads[3].thread.rootId,
+ 'scaddf38_44770ec1');
+ // unresolved
+ assert.equal(element._sortedThreads[4].thread.rootId,
+ '8caddf38_44770ec1');
+ // resolved and draft
+ assert.equal(element._sortedThreads[5].thread.rootId,
+ 'zcf0b9fa_fe1a5f62');
+ // resolved
+ assert.equal(element._sortedThreads[6].thread.rootId,
+ '09a9fb0a_1484e6cf');
+ });
- test('filtered threads do not contain robot comments without reply', () => {
- const thread = element.threads.find(thread => thread.rootId === 'rc1');
- assert.equal(element._filteredThreads.includes(thread), false);
- });
+ test('filtered threads do not contain robot comments without reply', () => {
+ const thread = element.threads.find(thread => thread.rootId === 'rc1');
+ assert.equal(element._filteredThreads.includes(thread), false);
+ });
- test('filtered threads contains robot comments with reply', () => {
- const thread = element.threads.find(thread => thread.rootId === 'rc2');
- assert.equal(element._filteredThreads.includes(thread), true);
- });
+ test('filtered threads contains robot comments with reply', () => {
+ const thread = element.threads.find(thread => thread.rootId === 'rc2');
+ assert.equal(element._filteredThreads.includes(thread), true);
+ });
- test('thread removal', () => {
- threadElements[1].fire('thread-discard', {rootId: 'rc2'});
- flushAsynchronousOperations();
- assert.equal(element._sortedThreads.length, 6);
- assert.equal(element._sortedThreads[0].thread.rootId,
- 'ecf0b9fa_fe1a5f62');
- // Unresolved robot comment
- assert.equal(element._sortedThreads[1].thread.rootId,
- 'rc1');
- // unresolved
- assert.equal(element._sortedThreads[2].thread.rootId,
- 'scaddf38_44770ec1');
- // unresolved
- assert.equal(element._sortedThreads[3].thread.rootId,
- '8caddf38_44770ec1');
- // resolved and draft
- assert.equal(element._sortedThreads[4].thread.rootId,
- 'zcf0b9fa_fe1a5f62');
- // resolved
- assert.equal(element._sortedThreads[5].thread.rootId,
- '09a9fb0a_1484e6cf');
- });
+ test('thread removal', () => {
+ threadElements[1].fire('thread-discard', {rootId: 'rc2'});
+ flushAsynchronousOperations();
+ assert.equal(element._sortedThreads.length, 6);
+ assert.equal(element._sortedThreads[0].thread.rootId,
+ 'ecf0b9fa_fe1a5f62');
+ // Unresolved robot comment
+ assert.equal(element._sortedThreads[1].thread.rootId,
+ 'rc1');
+ // unresolved
+ assert.equal(element._sortedThreads[2].thread.rootId,
+ 'scaddf38_44770ec1');
+ // unresolved
+ assert.equal(element._sortedThreads[3].thread.rootId,
+ '8caddf38_44770ec1');
+ // resolved and draft
+ assert.equal(element._sortedThreads[4].thread.rootId,
+ 'zcf0b9fa_fe1a5f62');
+ // resolved
+ assert.equal(element._sortedThreads[5].thread.rootId,
+ '09a9fb0a_1484e6cf');
+ });
- test('toggle unresolved only shows unresolved comments', () => {
- MockInteractions.tap(element.shadowRoot.querySelector(
- '#unresolvedToggle'));
- flushAsynchronousOperations();
- assert.equal(Polymer.dom(element.root)
- .querySelectorAll('gr-comment-thread').length, 5);
- });
+ test('toggle unresolved only shows unresolved comments', () => {
+ MockInteractions.tap(element.shadowRoot.querySelector(
+ '#unresolvedToggle'));
+ flushAsynchronousOperations();
+ assert.equal(dom(element.root)
+ .querySelectorAll('gr-comment-thread').length, 5);
+ });
- test('toggle drafts only shows threads with draft comments', () => {
- MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle'));
- flushAsynchronousOperations();
- assert.equal(Polymer.dom(element.root)
- .querySelectorAll('gr-comment-thread').length, 2);
- });
+ test('toggle drafts only shows threads with draft comments', () => {
+ MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle'));
+ flushAsynchronousOperations();
+ assert.equal(dom(element.root)
+ .querySelectorAll('gr-comment-thread').length, 2);
+ });
- test('toggle drafts and unresolved only shows threads with drafts and ' +
- 'publicly unresolved ', () => {
- MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle'));
- MockInteractions.tap(element.shadowRoot.querySelector(
- '#unresolvedToggle'));
- flushAsynchronousOperations();
- assert.equal(Polymer.dom(element.root)
- .querySelectorAll('gr-comment-thread').length, 2);
- });
+ test('toggle drafts and unresolved only shows threads with drafts and ' +
+ 'publicly unresolved ', () => {
+ MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle'));
+ MockInteractions.tap(element.shadowRoot.querySelector(
+ '#unresolvedToggle'));
+ flushAsynchronousOperations();
+ assert.equal(dom(element.root)
+ .querySelectorAll('gr-comment-thread').length, 2);
+ });
- test('modification events are consumed and displatched', () => {
- sandbox.spy(element, '_handleCommentsChanged');
- const dispatchSpy = sandbox.stub();
- element.addEventListener('thread-list-modified', dispatchSpy);
- threadElements[0].fire('thread-changed', {
- rootId: 'ecf0b9fa_fe1a5f62', path: '/COMMIT_MSG'});
- assert.isTrue(element._handleCommentsChanged.called);
- assert.isTrue(dispatchSpy.called);
- assert.equal(dispatchSpy.lastCall.args[0].detail.rootId,
- 'ecf0b9fa_fe1a5f62');
- assert.equal(dispatchSpy.lastCall.args[0].detail.path, '/COMMIT_MSG');
- });
+ test('modification events are consumed and displatched', () => {
+ sandbox.spy(element, '_handleCommentsChanged');
+ const dispatchSpy = sandbox.stub();
+ element.addEventListener('thread-list-modified', dispatchSpy);
+ threadElements[0].fire('thread-changed', {
+ rootId: 'ecf0b9fa_fe1a5f62', path: '/COMMIT_MSG'});
+ assert.isTrue(element._handleCommentsChanged.called);
+ assert.isTrue(dispatchSpy.called);
+ assert.equal(dispatchSpy.lastCall.args[0].detail.rootId,
+ 'ecf0b9fa_fe1a5f62');
+ assert.equal(dispatchSpy.lastCall.args[0].detail.path, '/COMMIT_MSG');
+ });
- suite('findings tab', () => {
- setup(done => {
- element.hideToggleButtons = true;
- flush(() => {
- done();
- });
+ suite('findings tab', () => {
+ setup(done => {
+ element.hideToggleButtons = true;
+ flush(() => {
+ done();
});
- test('toggle buttons are hidden', () => {
- assert.equal(element.shadowRoot.querySelector('.header').style.display,
- 'none');
- });
+ });
+ test('toggle buttons are hidden', () => {
+ assert.equal(element.shadowRoot.querySelector('.header').style.display,
+ 'none');
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.html b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.html
deleted file mode 100644
index e3cee56..0000000
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.html
+++ /dev/null
@@ -1,82 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-shell-command/gr-shell-command.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-upload-help-dialog">
- <template>
- <style include="shared-styles">
- :host {
- background-color: var(--dialog-background-color);
- display: block;
- }
- .main {
- width: 100%;
- }
- ol {
- margin-left: var(--spacing-l);
- list-style: decimal;
- }
- p {
- margin-bottom: var(--spacing-m);
- }
- </style>
- <gr-dialog
- confirm-label="Done"
- cancel-label=""
- on-confirm="_handleCloseTap">
- <div class="header" slot="header">How to update this change:</div>
- <div class="main" slot="main">
- <ol>
- <li>
- <p>
- Checkout this change locally and make your desired modifications
- to the files.
- </p>
- <template is="dom-if" if="[[_fetchCommand]]">
- <gr-shell-command command="[[_fetchCommand]]"></gr-shell-command>
- </template>
- </li>
- <li>
- <p>
- Update the local commit with your modifications using the following
- command.
- </p>
- <gr-shell-command command="[[_commitCommand]]"></gr-shell-command>
- <p>
- Leave the "Change-Id:" line of the commit message as is.
- </p>
- </li>
- <li>
- <p>Push the updated commit to Gerrit.</p>
- <gr-shell-command command="[[_pushCommand]]"></gr-shell-command>
- </li>
- <li>
- <p>Refresh this page to view the the update.</p>
- </li>
- </ol>
- </div>
- </gr-dialog>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-upload-help-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
index 60cbd42..1ab3926 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
@@ -14,133 +14,144 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const COMMIT_COMMAND = 'git add . && git commit --amend --no-edit';
- const PUSH_COMMAND_PREFIX = 'git push origin HEAD:refs/for/';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-shell-command/gr-shell-command.js';
+import '../../../styles/shared-styles.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-upload-help-dialog_html.js';
- // Command names correspond to download plugin definitions.
- const PREFERRED_FETCH_COMMAND_ORDER = [
- 'checkout',
- 'cherry pick',
- 'pull',
- ];
+const COMMIT_COMMAND = 'git add . && git commit --amend --no-edit';
+const PUSH_COMMAND_PREFIX = 'git push origin HEAD:refs/for/';
+// Command names correspond to download plugin definitions.
+const PREFERRED_FETCH_COMMAND_ORDER = [
+ 'checkout',
+ 'cherry pick',
+ 'pull',
+];
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrUploadHelpDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-upload-help-dialog'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the user presses the close button.
+ *
+ * @event close
*/
- class GrUploadHelpDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-upload-help-dialog'; }
- /**
- * Fired when the user presses the close button.
- *
- * @event close
- */
- static get properties() {
- return {
- revision: Object,
- targetBranch: String,
- _commitCommand: {
- type: String,
- value: COMMIT_COMMAND,
- readOnly: true,
- },
- _fetchCommand: {
- type: String,
- computed: '_computeFetchCommand(revision, ' +
- '_preferredDownloadCommand, _preferredDownloadScheme)',
- },
- _preferredDownloadCommand: String,
- _preferredDownloadScheme: String,
- _pushCommand: {
- type: String,
- computed: '_computePushCommand(targetBranch)',
- },
- };
- }
+ static get properties() {
+ return {
+ revision: Object,
+ targetBranch: String,
+ _commitCommand: {
+ type: String,
+ value: COMMIT_COMMAND,
+ readOnly: true,
+ },
+ _fetchCommand: {
+ type: String,
+ computed: '_computeFetchCommand(revision, ' +
+ '_preferredDownloadCommand, _preferredDownloadScheme)',
+ },
+ _preferredDownloadCommand: String,
+ _preferredDownloadScheme: String,
+ _pushCommand: {
+ type: String,
+ computed: '_computePushCommand(targetBranch)',
+ },
+ };
+ }
- /** @override */
- attached() {
- super.attached();
- this.$.restAPI.getLoggedIn()
- .then(loggedIn => {
- if (loggedIn) {
- return this.$.restAPI.getPreferences();
- }
- })
- .then(prefs => {
- if (prefs) {
- this._preferredDownloadCommand = prefs.download_command;
- this._preferredDownloadScheme = prefs.download_scheme;
- }
- });
- }
+ /** @override */
+ attached() {
+ super.attached();
+ this.$.restAPI.getLoggedIn()
+ .then(loggedIn => {
+ if (loggedIn) {
+ return this.$.restAPI.getPreferences();
+ }
+ })
+ .then(prefs => {
+ if (prefs) {
+ this._preferredDownloadCommand = prefs.download_command;
+ this._preferredDownloadScheme = prefs.download_scheme;
+ }
+ });
+ }
- _handleCloseTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('close', null, {bubbles: false});
- }
+ _handleCloseTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('close', null, {bubbles: false});
+ }
- _computeFetchCommand(revision, preferredDownloadCommand,
- preferredDownloadScheme) {
- // Polymer 2: check for undefined
- if ([
- revision,
- preferredDownloadCommand,
- preferredDownloadScheme,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- if (!revision) { return; }
- if (!revision || !revision.fetch) { return; }
-
- let scheme = preferredDownloadScheme;
- if (!scheme) {
- const keys = Object.keys(revision.fetch).sort();
- if (keys.length === 0) {
- return;
- }
- scheme = keys[0];
- }
-
- if (!revision.fetch[scheme] || !revision.fetch[scheme].commands) {
- return;
- }
-
- const cmds = {};
- Object.entries(revision.fetch[scheme].commands).forEach(([key, cmd]) => {
- cmds[key.toLowerCase()] = cmd;
- });
-
- if (preferredDownloadCommand &&
- cmds[preferredDownloadCommand.toLowerCase()]) {
- return cmds[preferredDownloadCommand.toLowerCase()];
- }
-
- // If no supported command preference is given, look for known commands
- // from the downloads plugin in order of preference.
- for (let i = 0; i < PREFERRED_FETCH_COMMAND_ORDER.length; i++) {
- if (cmds[PREFERRED_FETCH_COMMAND_ORDER[i]]) {
- return cmds[PREFERRED_FETCH_COMMAND_ORDER[i]];
- }
- }
-
+ _computeFetchCommand(revision, preferredDownloadCommand,
+ preferredDownloadScheme) {
+ // Polymer 2: check for undefined
+ if ([
+ revision,
+ preferredDownloadCommand,
+ preferredDownloadScheme,
+ ].some(arg => arg === undefined)) {
return undefined;
}
- _computePushCommand(targetBranch) {
- return PUSH_COMMAND_PREFIX + targetBranch;
+ if (!revision) { return; }
+ if (!revision || !revision.fetch) { return; }
+
+ let scheme = preferredDownloadScheme;
+ if (!scheme) {
+ const keys = Object.keys(revision.fetch).sort();
+ if (keys.length === 0) {
+ return;
+ }
+ scheme = keys[0];
}
+
+ if (!revision.fetch[scheme] || !revision.fetch[scheme].commands) {
+ return;
+ }
+
+ const cmds = {};
+ Object.entries(revision.fetch[scheme].commands).forEach(([key, cmd]) => {
+ cmds[key.toLowerCase()] = cmd;
+ });
+
+ if (preferredDownloadCommand &&
+ cmds[preferredDownloadCommand.toLowerCase()]) {
+ return cmds[preferredDownloadCommand.toLowerCase()];
+ }
+
+ // If no supported command preference is given, look for known commands
+ // from the downloads plugin in order of preference.
+ for (let i = 0; i < PREFERRED_FETCH_COMMAND_ORDER.length; i++) {
+ if (cmds[PREFERRED_FETCH_COMMAND_ORDER[i]]) {
+ return cmds[PREFERRED_FETCH_COMMAND_ORDER[i]];
+ }
+ }
+
+ return undefined;
}
- customElements.define(GrUploadHelpDialog.is, GrUploadHelpDialog);
-})();
+ _computePushCommand(targetBranch) {
+ return PUSH_COMMAND_PREFIX + targetBranch;
+ }
+}
+
+customElements.define(GrUploadHelpDialog.is, GrUploadHelpDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js
new file mode 100644
index 0000000..3f4fc42
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js
@@ -0,0 +1,70 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ background-color: var(--dialog-background-color);
+ display: block;
+ }
+ .main {
+ width: 100%;
+ }
+ ol {
+ margin-left: var(--spacing-xl);
+ list-style: decimal;
+ }
+ p {
+ margin-bottom: var(--spacing-m);
+ }
+ </style>
+ <gr-dialog confirm-label="Done" cancel-label="" on-confirm="_handleCloseTap">
+ <div class="header" slot="header">How to update this change:</div>
+ <div class="main" slot="main">
+ <ol>
+ <li>
+ <p>
+ Checkout this change locally and make your desired modifications
+ to the files.
+ </p>
+ <template is="dom-if" if="[[_fetchCommand]]">
+ <gr-shell-command command="[[_fetchCommand]]"></gr-shell-command>
+ </template>
+ </li>
+ <li>
+ <p>
+ Update the local commit with your modifications using the following
+ command.
+ </p>
+ <gr-shell-command command="[[_commitCommand]]"></gr-shell-command>
+ <p>
+ Leave the "Change-Id:" line of the commit message as is.
+ </p>
+ </li>
+ <li>
+ <p>Push the updated commit to Gerrit.</p>
+ <gr-shell-command command="[[_pushCommand]]"></gr-shell-command>
+ </li>
+ <li>
+ <p>Refresh this page to view the the update.</p>
+ </li>
+ </ol>
+ </div>
+ </gr-dialog>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html
index 3af5449..bea8113 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-upload-help-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-upload-help-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,93 +30,94 @@
</template>
</test-fixture>
-<script>
- suite('gr-upload-help-dialog tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-upload-help-dialog.js';
+suite('gr-upload-help-dialog tests', () => {
+ let element;
- setup(() => {
- element = fixture('basic');
- });
+ setup(() => {
+ element = fixture('basic');
+ });
- test('constructs push command from branch', () => {
- element.targetBranch = 'foo';
- assert.equal(element._pushCommand, 'git push origin HEAD:refs/for/foo');
+ test('constructs push command from branch', () => {
+ element.targetBranch = 'foo';
+ assert.equal(element._pushCommand, 'git push origin HEAD:refs/for/foo');
- element.targetBranch = 'master';
- assert.equal(element._pushCommand,
- 'git push origin HEAD:refs/for/master');
- });
+ element.targetBranch = 'master';
+ assert.equal(element._pushCommand,
+ 'git push origin HEAD:refs/for/master');
+ });
- suite('fetch command', () => {
- const testRev = {
- fetch: {
- http: {
- commands: {
- Checkout: 'http checkout',
- Pull: 'http pull',
- },
- },
- ssh: {
- commands: {
- Pull: 'ssh pull',
- },
+ suite('fetch command', () => {
+ const testRev = {
+ fetch: {
+ http: {
+ commands: {
+ Checkout: 'http checkout',
+ Pull: 'http pull',
},
},
- };
+ ssh: {
+ commands: {
+ Pull: 'ssh pull',
+ },
+ },
+ },
+ };
- test('null cases', () => {
- assert.isUndefined(element._computeFetchCommand());
- assert.isUndefined(element._computeFetchCommand({}));
- assert.isUndefined(element._computeFetchCommand({fetch: null}));
- assert.isUndefined(element._computeFetchCommand({fetch: {}}));
- });
+ test('null cases', () => {
+ assert.isUndefined(element._computeFetchCommand());
+ assert.isUndefined(element._computeFetchCommand({}));
+ assert.isUndefined(element._computeFetchCommand({fetch: null}));
+ assert.isUndefined(element._computeFetchCommand({fetch: {}}));
+ });
- test('not all defined', () => {
- assert.isUndefined(
- element._computeFetchCommand(testRev, undefined, ''));
- assert.isUndefined(
- element._computeFetchCommand(testRev, '', undefined));
- assert.isUndefined(
- element._computeFetchCommand(undefined, '', ''));
- });
+ test('not all defined', () => {
+ assert.isUndefined(
+ element._computeFetchCommand(testRev, undefined, ''));
+ assert.isUndefined(
+ element._computeFetchCommand(testRev, '', undefined));
+ assert.isUndefined(
+ element._computeFetchCommand(undefined, '', ''));
+ });
- test('insufficiently defined scheme', () => {
- assert.isUndefined(
- element._computeFetchCommand(testRev, '', 'badscheme'));
+ test('insufficiently defined scheme', () => {
+ assert.isUndefined(
+ element._computeFetchCommand(testRev, '', 'badscheme'));
- const rev = Object.assign({}, testRev);
- rev.fetch = Object.assign({}, testRev.fetch, {nocmds: {commands: {}}});
- assert.isUndefined(
- element._computeFetchCommand(rev, '', 'nocmds'));
+ const rev = Object.assign({}, testRev);
+ rev.fetch = Object.assign({}, testRev.fetch, {nocmds: {commands: {}}});
+ assert.isUndefined(
+ element._computeFetchCommand(rev, '', 'nocmds'));
- rev.fetch.nocmds.commands.unsupported = 'unsupported';
- assert.isUndefined(
- element._computeFetchCommand(rev, '', 'nocmds'));
- });
+ rev.fetch.nocmds.commands.unsupported = 'unsupported';
+ assert.isUndefined(
+ element._computeFetchCommand(rev, '', 'nocmds'));
+ });
- test('default scheme and command', () => {
- const cmd = element._computeFetchCommand(testRev, '', '');
- assert.isTrue(cmd === 'http checkout' || cmd === 'ssh pull');
- });
+ test('default scheme and command', () => {
+ const cmd = element._computeFetchCommand(testRev, '', '');
+ assert.isTrue(cmd === 'http checkout' || cmd === 'ssh pull');
+ });
- test('default command', () => {
- assert.strictEqual(
- element._computeFetchCommand(testRev, '', 'http'),
- 'http checkout');
- assert.strictEqual(
- element._computeFetchCommand(testRev, '', 'ssh'),
- 'ssh pull');
- });
+ test('default command', () => {
+ assert.strictEqual(
+ element._computeFetchCommand(testRev, '', 'http'),
+ 'http checkout');
+ assert.strictEqual(
+ element._computeFetchCommand(testRev, '', 'ssh'),
+ 'ssh pull');
+ });
- test('user preferred scheme and command', () => {
- assert.strictEqual(
- element._computeFetchCommand(testRev, 'PULL', 'http'),
- 'http pull');
- assert.strictEqual(
- element._computeFetchCommand(testRev, 'badcmd', 'http'),
- 'http checkout');
- });
+ test('user preferred scheme and command', () => {
+ assert.strictEqual(
+ element._computeFetchCommand(testRev, 'PULL', 'http'),
+ 'http pull');
+ assert.strictEqual(
+ element._computeFetchCommand(testRev, 'badcmd', 'http'),
+ 'http checkout');
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
deleted file mode 100644
index 5152ef9..0000000
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
+++ /dev/null
@@ -1,56 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-avatar/gr-avatar.html">
-
-<dom-module id="gr-account-dropdown">
- <template>
- <style include="shared-styles">
- gr-dropdown {
- padding: 0 var(--spacing-m);
- --gr-button: {
- color: var(--header-text-color);
- }
- --gr-dropdown-item: {
- color: var(--primary-text-color);
- }
- }
- gr-avatar {
- height: 2em;
- width: 2em;
- vertical-align: middle;
- }
- </style>
- <gr-dropdown
- link
- items=[[links]]
- top-content=[[topContent]]
- horizontal-align="right">
- <span hidden$="[[_hasAvatars]]" hidden>[[_accountName(account)]]</span>
- <gr-avatar account="[[account]]" hidden$="[[!_hasAvatars]]" hidden
- image-size="56" aria-label="Account avatar"></gr-avatar>
- </gr-dropdown>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-account-dropdown.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
index 66c00f9..444986b 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,106 +14,118 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js';
- const INTERPOLATE_URL_PATTERN = /\$\{([\w]+)\}/g;
+import '../../../scripts/bundled-polymer.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-dropdown/gr-dropdown.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-avatar/gr-avatar.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-account-dropdown_html.js';
- /**
- * @appliesMixin Gerrit.DisplayNameMixin
- * @extends Polymer.Element
- */
- class GrAccountDropdown extends Polymer.mixinBehaviors( [
- Gerrit.DisplayNameBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-account-dropdown'; }
+const INTERPOLATE_URL_PATTERN = /\$\{([\w]+)\}/g;
- static get properties() {
- return {
- account: Object,
- config: Object,
- links: {
- type: Array,
- computed: '_getLinks(_switchAccountUrl, _path)',
- },
- topContent: {
- type: Array,
- computed: '_getTopContent(account)',
- },
- _path: {
- type: String,
- value: '/',
- },
- _hasAvatars: Boolean,
- _switchAccountUrl: String,
- };
- }
+/**
+ * @appliesMixin Gerrit.DisplayNameMixin
+ * @extends Polymer.Element
+ */
+class GrAccountDropdown extends mixinBehaviors( [
+ Gerrit.DisplayNameBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** @override */
- attached() {
- super.attached();
- this._handleLocationChange();
- this.listen(window, 'location-change', '_handleLocationChange');
- this.$.restAPI.getConfig().then(cfg => {
- this.config = cfg;
+ static get is() { return 'gr-account-dropdown'; }
- if (cfg && cfg.auth && cfg.auth.switch_account_url) {
- this._switchAccountUrl = cfg.auth.switch_account_url;
- } else {
- this._switchAccountUrl = '';
- }
- this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
- });
- }
-
- /** @override */
- detached() {
- super.detached();
- this.unlisten(window, 'location-change', '_handleLocationChange');
- }
-
- _getLinks(switchAccountUrl, path) {
- // Polymer 2: check for undefined
- if ([switchAccountUrl, path].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const links = [{name: 'Settings', url: '/settings/'}];
- if (switchAccountUrl) {
- const replacements = {path};
- const url = this._interpolateUrl(switchAccountUrl, replacements);
- links.push({name: 'Switch account', url, external: true});
- }
- links.push({name: 'Sign out', url: '/logout'});
- return links;
- }
-
- _getTopContent(account) {
- return [
- {text: this._accountName(account), bold: true},
- {text: account.email ? account.email : ''},
- ];
- }
-
- _handleLocationChange() {
- this._path =
- window.location.pathname +
- window.location.search +
- window.location.hash;
- }
-
- _interpolateUrl(url, replacements) {
- return url.replace(
- INTERPOLATE_URL_PATTERN,
- (match, p1) => replacements[p1] || '');
- }
-
- _accountName(account) {
- return this.getUserName(this.config, account, true);
- }
+ static get properties() {
+ return {
+ account: Object,
+ config: Object,
+ links: {
+ type: Array,
+ computed: '_getLinks(_switchAccountUrl, _path)',
+ },
+ topContent: {
+ type: Array,
+ computed: '_getTopContent(account)',
+ },
+ _path: {
+ type: String,
+ value: '/',
+ },
+ _hasAvatars: Boolean,
+ _switchAccountUrl: String,
+ };
}
- customElements.define(GrAccountDropdown.is, GrAccountDropdown);
-})();
+ /** @override */
+ attached() {
+ super.attached();
+ this._handleLocationChange();
+ this.listen(window, 'location-change', '_handleLocationChange');
+ this.$.restAPI.getConfig().then(cfg => {
+ this.config = cfg;
+
+ if (cfg && cfg.auth && cfg.auth.switch_account_url) {
+ this._switchAccountUrl = cfg.auth.switch_account_url;
+ } else {
+ this._switchAccountUrl = '';
+ }
+ this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
+ });
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this.unlisten(window, 'location-change', '_handleLocationChange');
+ }
+
+ _getLinks(switchAccountUrl, path) {
+ // Polymer 2: check for undefined
+ if ([switchAccountUrl, path].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const links = [{name: 'Settings', url: '/settings/'}];
+ if (switchAccountUrl) {
+ const replacements = {path};
+ const url = this._interpolateUrl(switchAccountUrl, replacements);
+ links.push({name: 'Switch account', url, external: true});
+ }
+ links.push({name: 'Sign out', url: '/logout'});
+ return links;
+ }
+
+ _getTopContent(account) {
+ return [
+ {text: this._accountName(account), bold: true},
+ {text: account.email ? account.email : ''},
+ ];
+ }
+
+ _handleLocationChange() {
+ this._path =
+ window.location.pathname +
+ window.location.search +
+ window.location.hash;
+ }
+
+ _interpolateUrl(url, replacements) {
+ return url.replace(
+ INTERPOLATE_URL_PATTERN,
+ (match, p1) => replacements[p1] || '');
+ }
+
+ _accountName(account) {
+ return this.getUserName(this.config, account);
+ }
+}
+
+customElements.define(GrAccountDropdown.is, GrAccountDropdown);
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js
new file mode 100644
index 0000000..e22db65
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js
@@ -0,0 +1,41 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ gr-dropdown {
+ padding: 0 var(--spacing-m);
+ --gr-button: {
+ color: var(--header-text-color);
+ }
+ --gr-dropdown-item: {
+ color: var(--primary-text-color);
+ }
+ }
+ gr-avatar {
+ height: 2em;
+ width: 2em;
+ vertical-align: middle;
+ }
+ </style>
+ <gr-dropdown link="" items="[[links]]" top-content="[[topContent]]" horizontal-align="right">
+ <span hidden\$="[[_hasAvatars]]" hidden="">[[_accountName(account)]]</span>
+ <gr-avatar account="[[account]]" hidden\$="[[!_hasAvatars]]" hidden="" image-size="56" aria-label="Account avatar"></gr-avatar>
+ </gr-dropdown>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
index 0a11df1..d9d932a 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-account-dropdown</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-account-dropdown.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,93 +30,94 @@
</template>
</test-fixture>
-<script>
- suite('gr-account-dropdown tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-account-dropdown.js';
+suite('gr-account-dropdown tests', () => {
+ let element;
- setup(() => {
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- });
- element = fixture('basic');
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ });
+ element = fixture('basic');
+ });
+
+ test('account information', () => {
+ element.account = {name: 'John Doe', email: 'john@doe.com'};
+ assert.deepEqual(element.topContent,
+ [{text: 'John Doe', bold: true}, {text: 'john@doe.com'}]);
+ });
+
+ test('test for account without a name', () => {
+ element.account = {id: '0001'};
+ assert.deepEqual(element.topContent,
+ [{text: 'Anonymous', bold: true}, {text: ''}]);
+ });
+
+ test('test for account without a name but using config', () => {
+ element.config = {
+ user: {
+ anonymous_coward_name: 'WikiGerrit',
+ },
+ };
+ element.account = {id: '0001'};
+ assert.deepEqual(element.topContent,
+ [{text: 'WikiGerrit', bold: true}, {text: ''}]);
+ });
+
+ test('test for account name as an email', () => {
+ element.config = {
+ user: {
+ anonymous_coward_name: 'WikiGerrit',
+ },
+ };
+ element.account = {email: 'john@doe.com'};
+ assert.deepEqual(element.topContent,
+ [{text: 'john@doe.com', bold: true}, {text: 'john@doe.com'}]);
+ });
+
+ test('switch account', () => {
+ // Missing params.
+ assert.isUndefined(element._getLinks());
+ assert.isUndefined(element._getLinks(null));
+
+ // No switch account link.
+ assert.equal(element._getLinks(null, '').length, 2);
+
+ // Unparameterized switch account link.
+ let links = element._getLinks('/switch-account', '');
+ assert.equal(links.length, 3);
+ assert.deepEqual(links[1], {
+ name: 'Switch account',
+ url: '/switch-account',
+ external: true,
});
- test('account information', () => {
- element.account = {name: 'John Doe', email: 'john@doe.com'};
- assert.deepEqual(element.topContent,
- [{text: 'John Doe', bold: true}, {text: 'john@doe.com'}]);
- });
-
- test('test for account without a name', () => {
- element.account = {id: '0001'};
- assert.deepEqual(element.topContent,
- [{text: 'Anonymous', bold: true}, {text: ''}]);
- });
-
- test('test for account without a name but using config', () => {
- element.config = {
- user: {
- anonymous_coward_name: 'WikiGerrit',
- },
- };
- element.account = {id: '0001'};
- assert.deepEqual(element.topContent,
- [{text: 'WikiGerrit', bold: true}, {text: ''}]);
- });
-
- test('test for account name as an email', () => {
- element.config = {
- user: {
- anonymous_coward_name: 'WikiGerrit',
- },
- };
- element.account = {email: 'john@doe.com'};
- assert.deepEqual(element.topContent,
- [{text: 'john@doe.com', bold: true}, {text: 'john@doe.com'}]);
- });
-
- test('switch account', () => {
- // Missing params.
- assert.isUndefined(element._getLinks());
- assert.isUndefined(element._getLinks(null));
-
- // No switch account link.
- assert.equal(element._getLinks(null, '').length, 2);
-
- // Unparameterized switch account link.
- let links = element._getLinks('/switch-account', '');
- assert.equal(links.length, 3);
- assert.deepEqual(links[1], {
- name: 'Switch account',
- url: '/switch-account',
- external: true,
- });
-
- // Parameterized switch account link.
- links = element._getLinks('/switch-account${path}', '/c/123');
- assert.equal(links.length, 3);
- assert.deepEqual(links[1], {
- name: 'Switch account',
- url: '/switch-account/c/123',
- external: true,
- });
- });
-
- test('_interpolateUrl', () => {
- const replacements = {
- foo: 'bar',
- test: 'TEST',
- };
- const interpolate = function(url) {
- return element._interpolateUrl(url, replacements);
- };
-
- assert.equal(interpolate('test'), 'test');
- assert.equal(interpolate('${test}'), 'TEST');
- assert.equal(
- interpolate('${}, ${test}, ${TEST}, ${foo}'),
- '${}, TEST, , bar');
+ // Parameterized switch account link.
+ links = element._getLinks('/switch-account${path}', '/c/123');
+ assert.equal(links.length, 3);
+ assert.deepEqual(links[1], {
+ name: 'Switch account',
+ url: '/switch-account/c/123',
+ external: true,
});
});
+
+ test('_interpolateUrl', () => {
+ const replacements = {
+ foo: 'bar',
+ test: 'TEST',
+ };
+ const interpolate = function(url) {
+ return element._interpolateUrl(url, replacements);
+ };
+
+ assert.equal(interpolate('test'), 'test');
+ assert.equal(interpolate('${test}'), 'TEST');
+ assert.equal(
+ interpolate('${}, ${test}, ${TEST}, ${foo}'),
+ '${}, TEST, , bar');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.html b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.html
deleted file mode 100644
index ffd7f896..0000000
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.html
+++ /dev/null
@@ -1,60 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-error-dialog">
- <template>
- <style include="shared-styles">
- .main {
- max-height: 40em;
- max-width: 60em;
- overflow-y: auto;
- white-space: pre-wrap;
- }
- @media screen and (max-width: 50em) {
- .main {
- max-height: none;
- max-width: 50em;
- }
- }
- .signInLink {
- text-decoration: none;
- }
- </style>
- <gr-dialog
- id="dialog"
- cancel-label=""
- on-confirm="_handleConfirm"
- confirm-label="Dismiss"
- confirm-on-enter>
- <div class="header" slot="header">An error occurred</div>
- <div class="main" slot="main">[[text]]</div>
- <gr-button
- id="signIn"
- class$="signInLink"
- hidden$="[[!showSignInButton]]"
- link
- slot="footer">
- <a href$="[[loginUrl]]" class="signInLink">Sign in</a>
- </gr-button>
- </gr-dialog>
- </template>
- <script src="gr-error-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
index 63339c9..6814d89 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
@@ -14,44 +14,51 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrErrorDialog extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-error-dialog'; }
- /**
- * Fired when the dismiss button is pressed.
- *
- * @event dismiss
- */
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../../styles/shared-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-error-dialog_html.js';
- static get properties() {
- return {
- text: String,
- /**
- * loginUrl to open on "sign in" button click
- */
- loginUrl: {
- type: String,
- value: '/login',
- },
- /**
- * Show/hide "Sign In" button in dialog
- */
- showSignInButton: {
- type: Boolean,
- value: false,
- },
- };
- }
+/** @extends Polymer.Element */
+class GrErrorDialog extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _handleConfirm() {
- this.dispatchEvent(new CustomEvent('dismiss'));
- }
+ static get is() { return 'gr-error-dialog'; }
+ /**
+ * Fired when the dismiss button is pressed.
+ *
+ * @event dismiss
+ */
+
+ static get properties() {
+ return {
+ text: String,
+ /**
+ * loginUrl to open on "sign in" button click
+ */
+ loginUrl: {
+ type: String,
+ value: '/login',
+ },
+ /**
+ * Show/hide "Sign In" button in dialog
+ */
+ showSignInButton: {
+ type: Boolean,
+ value: false,
+ },
+ };
}
- customElements.define(GrErrorDialog.is, GrErrorDialog);
-})();
+ _handleConfirm() {
+ this.dispatchEvent(new CustomEvent('dismiss'));
+ }
+}
+
+customElements.define(GrErrorDialog.is, GrErrorDialog);
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js
new file mode 100644
index 0000000..e18d1bd
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js
@@ -0,0 +1,44 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ .main {
+ max-height: 40em;
+ max-width: 60em;
+ overflow-y: auto;
+ white-space: pre-wrap;
+ }
+ @media screen and (max-width: 50em) {
+ .main {
+ max-height: none;
+ max-width: 50em;
+ }
+ }
+ .signInLink {
+ text-decoration: none;
+ }
+ </style>
+ <gr-dialog id="dialog" cancel-label="" on-confirm="_handleConfirm" confirm-label="Dismiss" confirm-on-enter="">
+ <div class="header" slot="header">An error occurred</div>
+ <div class="main" slot="main">[[text]]</div>
+ <gr-button id="signIn" class\$="signInLink" hidden\$="[[!showSignInButton]]" link="" slot="footer">
+ <a href\$="[[loginUrl]]" class="signInLink">Sign in</a>
+ </gr-button>
+ </gr-dialog>
+`;
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html
index c87f8bb..2e6cff0 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-error-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-error-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,18 +30,19 @@
</template>
</test-fixture>
-<script>
- suite('gr-error-dialog tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-error-dialog.js';
+suite('gr-error-dialog tests', () => {
+ let element;
- setup(() => {
- element = fixture('basic');
- });
-
- test('dismiss tap fires event', done => {
- element.addEventListener('dismiss', () => { done(); });
- MockInteractions.tap(element.$.dialog.$.confirm);
- });
+ setup(() => {
+ element = fixture('basic');
});
+
+ test('dismiss tap fires event', done => {
+ element.addEventListener('dismiss', () => { done(); });
+ MockInteractions.tap(element.$.dialog.$.confirm);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
deleted file mode 100644
index 104d5b0..0000000
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
+++ /dev/null
@@ -1,52 +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.
--->
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../core/gr-error-dialog/gr-error-dialog.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../shared/gr-alert/gr-alert.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<!-- Import to get Gerrit interface -->
-<!-- TODO(taoalpha): decouple gr-gerrit from gr-js-api-interface -->
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-
-<dom-module id="gr-error-manager">
- <template>
- <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>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- <gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="gr-error-manager.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index b828774..e2284a9 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -14,384 +14,405 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+/* Import to get Gerrit interface */
+/* TODO(taoalpha): decouple gr-gerrit from gr-js-api-interface */
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- const HIDE_ALERT_TIMEOUT_MS = 5000;
- const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
- const STALE_CREDENTIAL_THRESHOLD_MS = 10 * 60 * 1000;
- const SIGN_IN_WIDTH_PX = 690;
- const SIGN_IN_HEIGHT_PX = 500;
- const TOO_MANY_FILES = 'too many files to find conflicts';
- const AUTHENTICATION_REQUIRED = 'Authentication required\n';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../gr-error-dialog/gr-error-dialog.js';
+import '../gr-reporting/gr-reporting.js';
+import '../../shared/gr-alert/gr-alert.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-error-manager_html.js';
+
+const HIDE_ALERT_TIMEOUT_MS = 5000;
+const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
+const STALE_CREDENTIAL_THRESHOLD_MS = 10 * 60 * 1000;
+const SIGN_IN_WIDTH_PX = 690;
+const SIGN_IN_HEIGHT_PX = 500;
+const TOO_MANY_FILES = 'too many files to find conflicts';
+const AUTHENTICATION_REQUIRED = 'Authentication required\n';
+
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrErrorManager extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-error-manager'; }
+
+ static get properties() {
+ return {
+ /**
+ * The ID of the account that was logged in when the app was launched. If
+ * not set, then there was no account at launch.
+ */
+ knownAccountId: Number,
+
+ /** @type {?Object} */
+ _alertElement: Object,
+ /** @type {?number} */
+ _hideAlertHandle: Number,
+ _refreshingCredentials: {
+ type: Boolean,
+ value: false,
+ },
+
+ /**
+ * The time (in milliseconds) since the most recent credential check.
+ */
+ _lastCredentialCheck: {
+ type: Number,
+ value() { return Date.now(); },
+ },
+
+ loginUrl: {
+ type: String,
+ value: '/login',
+ },
+ };
+ }
+
+ constructor() {
+ super();
+
+ /** @type {!Gerrit.Auth} */
+ this._authService = Gerrit.Auth;
+
+ /** @type {?Function} */
+ this._authErrorHandlerDeregistrationHook;
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this.listen(document, 'server-error', '_handleServerError');
+ this.listen(document, 'network-error', '_handleNetworkError');
+ this.listen(document, 'show-alert', '_handleShowAlert');
+ this.listen(document, 'show-error', '_handleShowErrorDialog');
+ this.listen(document, 'visibilitychange', '_handleVisibilityChange');
+ this.listen(document, 'show-auth-required', '_handleAuthRequired');
+
+ this._authErrorHandlerDeregistrationHook = Gerrit.on('auth-error',
+ event => {
+ this._handleAuthError(event.message, event.action);
+ });
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this._clearHideAlertHandle();
+ this.unlisten(document, 'server-error', '_handleServerError');
+ this.unlisten(document, 'network-error', '_handleNetworkError');
+ this.unlisten(document, 'show-auth-required', '_handleAuthRequired');
+ this.unlisten(document, 'visibilitychange', '_handleVisibilityChange');
+ this.unlisten(document, 'show-error', '_handleShowErrorDialog');
+
+ this._authErrorHandlerDeregistrationHook();
+ }
+
+ _shouldSuppressError(msg) {
+ return msg.includes(TOO_MANY_FILES);
+ }
+
+ _handleAuthRequired() {
+ this._showAuthErrorAlert(
+ 'Log in is required to perform that action.', 'Log in.');
+ }
+
+ _handleAuthError(msg, action) {
+ this.$.noInteractionOverlay.open().then(() => {
+ this._showAuthErrorAlert(msg, action);
+ });
+ }
+
+ _handleServerError(e) {
+ const {request, response} = e.detail;
+ response.text().then(errorText => {
+ const url = request && (request.anonymizedUrl || request.url);
+ const {status, statusText} = response;
+ if (response.status === 403
+ && !this._authService.isAuthed
+ && errorText === AUTHENTICATION_REQUIRED) {
+ // if not authed previously, this is trying to access auth required APIs
+ // show auth required alert
+ this._handleAuthRequired();
+ } else if (response.status === 403
+ && this._authService.isAuthed
+ && errorText === AUTHENTICATION_REQUIRED) {
+ // The app was logged at one point and is now getting auth errors.
+ // This indicates the auth token may no longer valid.
+ // Re-check on auth
+ this._authService.clearCache();
+ this.$.restAPI.getLoggedIn();
+ } else if (!this._shouldSuppressError(errorText)) {
+ const trace =
+ response.headers && response.headers.get('X-Gerrit-Trace');
+ if (response.status === 404) {
+ this._showNotFoundMessageWithTip({
+ status,
+ statusText,
+ errorText,
+ url,
+ trace,
+ });
+ } else {
+ this._showErrorDialog(this._constructServerErrorMsg({
+ status,
+ statusText,
+ errorText,
+ url,
+ trace,
+ }));
+ }
+ }
+ console.log(`server error: ${errorText}`);
+ });
+ }
+
+ _showNotFoundMessageWithTip({status, statusText, errorText, url, trace}) {
+ this.$.restAPI.getLoggedIn().then(isLoggedIn => {
+ const tip = isLoggedIn ?
+ 'You might have not enough privileges.' :
+ 'You might have not enough privileges. Sign in and try again.';
+ this._showErrorDialog(this._constructServerErrorMsg({
+ status,
+ statusText,
+ errorText,
+ url,
+ trace,
+ tip,
+ }), {
+ showSignInButton: !isLoggedIn,
+ });
+ });
+ return;
+ }
+
+ _constructServerErrorMsg({errorText, status, statusText, url, trace, tip}) {
+ let err = '';
+ if (tip) {
+ err += `${tip}\n\n`;
+ }
+ err += `Error ${status}`;
+ if (statusText) { err += ` (${statusText})`; }
+ if (errorText || url) { err += ': '; }
+ if (errorText) { err += errorText; }
+ if (url) { err += `\nEndpoint: ${url}`; }
+ if (trace) { err += `\nTrace Id: ${trace}`; }
+ return err;
+ }
+
+ _handleShowAlert(e) {
+ this._showAlert(e.detail.message, e.detail.action, e.detail.callback,
+ e.detail.dismissOnNavigation);
+ }
+
+ _handleNetworkError(e) {
+ this._showAlert('Server unavailable');
+ console.error(e.detail.error.message);
+ }
/**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * @param {string} text
+ * @param {?string=} opt_actionText
+ * @param {?Function=} opt_actionCallback
+ * @param {?boolean=} opt_dismissOnNavigation
*/
- class GrErrorManager extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-error-manager'; }
-
- static get properties() {
- return {
- /**
- * The ID of the account that was logged in when the app was launched. If
- * not set, then there was no account at launch.
- */
- knownAccountId: Number,
-
- /** @type {?Object} */
- _alertElement: Object,
- /** @type {?number} */
- _hideAlertHandle: Number,
- _refreshingCredentials: {
- type: Boolean,
- value: false,
- },
-
- /**
- * The time (in milliseconds) since the most recent credential check.
- */
- _lastCredentialCheck: {
- type: Number,
- value() { return Date.now(); },
- },
-
- loginUrl: {
- type: String,
- value: '/login',
- },
- };
- }
-
- constructor() {
- super();
-
- /** @type {!Gerrit.Auth} */
- this._authService = Gerrit.Auth;
-
- /** @type {?Function} */
- this._authErrorHandlerDeregistrationHook;
- }
-
- /** @override */
- attached() {
- super.attached();
- this.listen(document, 'server-error', '_handleServerError');
- this.listen(document, 'network-error', '_handleNetworkError');
- this.listen(document, 'show-alert', '_handleShowAlert');
- this.listen(document, 'show-error', '_handleShowErrorDialog');
- this.listen(document, 'visibilitychange', '_handleVisibilityChange');
- this.listen(document, 'show-auth-required', '_handleAuthRequired');
-
- this._authErrorHandlerDeregistrationHook = Gerrit.on('auth-error',
- event => {
- this._handleAuthError(event.message, event.action);
- });
- }
-
- /** @override */
- detached() {
- super.detached();
- this._clearHideAlertHandle();
- this.unlisten(document, 'server-error', '_handleServerError');
- this.unlisten(document, 'network-error', '_handleNetworkError');
- this.unlisten(document, 'show-auth-required', '_handleAuthRequired');
- this.unlisten(document, 'visibilitychange', '_handleVisibilityChange');
- this.unlisten(document, 'show-error', '_handleShowErrorDialog');
-
- this._authErrorHandlerDeregistrationHook();
- }
-
- _shouldSuppressError(msg) {
- return msg.includes(TOO_MANY_FILES);
- }
-
- _handleAuthRequired() {
- this._showAuthErrorAlert(
- 'Log in is required to perform that action.', 'Log in.');
- }
-
- _handleAuthError(msg, action) {
- this.$.noInteractionOverlay.open().then(() => {
- this._showAuthErrorAlert(msg, action);
- });
- }
-
- _handleServerError(e) {
- const {request, response} = e.detail;
- response.text().then(errorText => {
- const url = request && (request.anonymizedUrl || request.url);
- const {status, statusText} = response;
- if (response.status === 403
- && !this._authService.isAuthed
- && errorText === AUTHENTICATION_REQUIRED) {
- // if not authed previously, this is trying to access auth required APIs
- // show auth required alert
- this._handleAuthRequired();
- } else if (response.status === 403
- && this._authService.isAuthed
- && errorText === AUTHENTICATION_REQUIRED) {
- // The app was logged at one point and is now getting auth errors.
- // This indicates the auth token may no longer valid.
- // Re-check on auth
- this._authService.clearCache();
- this.$.restAPI.getLoggedIn();
- } else if (!this._shouldSuppressError(errorText)) {
- const trace =
- response.headers && response.headers.get('X-Gerrit-Trace');
- if (response.status === 404) {
- this._showNotFoundMessageWithTip({
- status,
- statusText,
- errorText,
- url,
- trace,
- });
- } else {
- this._showErrorDialog(this._constructServerErrorMsg({
- status,
- statusText,
- errorText,
- url,
- trace,
- }));
- }
- }
- console.log(`server error: ${errorText}`);
- });
- }
-
- _showNotFoundMessageWithTip({status, statusText, errorText, url, trace}) {
- this.$.restAPI.getLoggedIn().then(isLoggedIn => {
- const tip = isLoggedIn ?
- 'You might have not enough privileges.' :
- 'You might have not enough privileges. Sign in and try again.';
- this._showErrorDialog(this._constructServerErrorMsg({
- status,
- statusText,
- errorText,
- url,
- trace,
- tip,
- }), {
- showSignInButton: !isLoggedIn,
- });
- });
- return;
- }
-
- _constructServerErrorMsg({errorText, status, statusText, url, trace, tip}) {
- let err = '';
- if (tip) {
- err += `${tip}\n\n`;
- }
- err += `Error ${status}`;
- if (statusText) { err += ` (${statusText})`; }
- if (errorText || url) { err += ': '; }
- if (errorText) { err += errorText; }
- if (url) { err += `\nEndpoint: ${url}`; }
- if (trace) { err += `\nTrace Id: ${trace}`; }
- return err;
- }
-
- _handleShowAlert(e) {
- this._showAlert(e.detail.message, e.detail.action, e.detail.callback,
- e.detail.dismissOnNavigation);
- }
-
- _handleNetworkError(e) {
- this._showAlert('Server unavailable');
- console.error(e.detail.error.message);
- }
-
- /**
- * @param {string} text
- * @param {?string=} opt_actionText
- * @param {?Function=} opt_actionCallback
- * @param {?boolean=} opt_dismissOnNavigation
- */
- _showAlert(text, opt_actionText, opt_actionCallback,
- opt_dismissOnNavigation) {
- if (this._alertElement) {
- // do not override auth alerts
- if (this._alertElement.type === 'AUTH') return;
- this._hideAlert();
- }
-
- this._clearHideAlertHandle();
- if (opt_dismissOnNavigation) {
- // Persist alert until navigation.
- this.listen(document, 'location-change', '_hideAlert');
- } else {
- this._hideAlertHandle =
- this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS);
- }
- const el = this._createToastAlert();
- el.show(text, opt_actionText, opt_actionCallback);
- this._alertElement = el;
- }
-
- _hideAlert() {
- if (!this._alertElement) { return; }
-
- this._alertElement.hide();
- this._alertElement = null;
-
- // Remove listener for page navigation, if it exists.
- this.unlisten(document, 'location-change', '_hideAlert');
- }
-
- _clearHideAlertHandle() {
- if (this._hideAlertHandle != null) {
- this.cancelAsync(this._hideAlertHandle);
- this._hideAlertHandle = null;
- }
- }
-
- _showAuthErrorAlert(errorText, actionText) {
- // hide any existing alert like `reload`
- // as auth error should have the highest priority
- if (this._alertElement) {
- this._alertElement.hide();
- }
-
- this._alertElement = this._createToastAlert();
- this._alertElement.type = 'AUTH';
- this._alertElement.show(errorText, actionText,
- this._createLoginPopup.bind(this));
-
- this._refreshingCredentials = true;
- this._requestCheckLoggedIn();
- if (!document.hidden) {
- this._handleVisibilityChange();
- }
- }
-
- _createToastAlert() {
- const el = document.createElement('gr-alert');
- el.toast = true;
- return el;
- }
-
- _handleVisibilityChange() {
- // Ignore when the page is transitioning to hidden (or hidden is
- // undefined).
- if (document.hidden !== false) { return; }
-
- // 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;
- if (!this._refreshingCredentials &&
- this.knownAccountId !== undefined &&
- timeSinceLastCheck > STALE_CREDENTIAL_THRESHOLD_MS) {
- this._lastCredentialCheck = Date.now();
-
- // check auth status in case:
- // - user signed out
- // - user switched account
- this._checkSignedIn();
- }
- }
-
- _requestCheckLoggedIn() {
- this.debounce(
- 'checkLoggedIn', this._checkSignedIn, CHECK_SIGN_IN_INTERVAL_MS);
- }
-
- _checkSignedIn() {
- this._lastCredentialCheck = Date.now();
-
- // force to refetch account info
- this.$.restAPI.invalidateAccountsCache();
- this._authService.clearCache();
-
- this.$.restAPI.getLoggedIn().then(isLoggedIn => {
- // do nothing if its refreshing
- 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();
- } else {
- // check account
- this.$.restAPI.getAccount().then(account => {
- if (this._refreshingCredentials) {
- // If the credentials were refreshed but the account is different
- // then reload the page completely.
- if (account._account_id !== this.knownAccountId) {
- this._reloadPage();
- return;
- }
-
- this._handleCredentialRefreshed();
- }
- });
- }
- });
- }
-
- _reloadPage() {
- window.location.reload();
- }
-
- _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 = [
- 'width=' + SIGN_IN_WIDTH_PX,
- 'height=' + SIGN_IN_HEIGHT_PX,
- 'left=' + left,
- 'top=' + top,
- ];
- window.open(this.getBaseUrl() +
- '/login/%3FcloseAfterLogin', '_blank', options.join(','));
- this.listen(window, 'focus', '_handleWindowFocus');
- }
-
- _handleCredentialRefreshed() {
- this.unlisten(window, 'focus', '_handleWindowFocus');
- this._refreshingCredentials = false;
+ _showAlert(text, opt_actionText, opt_actionCallback,
+ opt_dismissOnNavigation) {
+ if (this._alertElement) {
+ // do not override auth alerts
+ if (this._alertElement.type === 'AUTH') return;
this._hideAlert();
- this._showAlert('Credentials refreshed.');
- this.$.noInteractionOverlay.close();
-
- // Clear the cache for auth
- this._authService.clearCache();
}
- _handleWindowFocus() {
- this.flushDebouncer('checkLoggedIn');
+ this._clearHideAlertHandle();
+ if (opt_dismissOnNavigation) {
+ // Persist alert until navigation.
+ this.listen(document, 'location-change', '_hideAlert');
+ } else {
+ this._hideAlertHandle =
+ this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS);
}
+ const el = this._createToastAlert();
+ el.show(text, opt_actionText, opt_actionCallback);
+ this._alertElement = el;
+ }
- _handleShowErrorDialog(e) {
- this._showErrorDialog(e.detail.message);
- }
+ _hideAlert() {
+ if (!this._alertElement) { return; }
- _handleDismissErrorDialog() {
- this.$.errorOverlay.close();
- }
+ this._alertElement.hide();
+ this._alertElement = null;
- _showErrorDialog(message, opt_options) {
- this.$.reporting.reportErrorDialog(message);
- this.$.errorDialog.text = message;
- this.$.errorDialog.showSignInButton =
- opt_options && opt_options.showSignInButton;
- this.$.errorOverlay.open();
+ // Remove listener for page navigation, if it exists.
+ this.unlisten(document, 'location-change', '_hideAlert');
+ }
+
+ _clearHideAlertHandle() {
+ if (this._hideAlertHandle != null) {
+ this.cancelAsync(this._hideAlertHandle);
+ this._hideAlertHandle = null;
}
}
- customElements.define(GrErrorManager.is, GrErrorManager);
-})();
+ _showAuthErrorAlert(errorText, actionText) {
+ // hide any existing alert like `reload`
+ // as auth error should have the highest priority
+ if (this._alertElement) {
+ this._alertElement.hide();
+ }
+
+ this._alertElement = this._createToastAlert();
+ this._alertElement.type = 'AUTH';
+ this._alertElement.show(errorText, actionText,
+ this._createLoginPopup.bind(this));
+
+ this._refreshingCredentials = true;
+ this._requestCheckLoggedIn();
+ if (!document.hidden) {
+ this._handleVisibilityChange();
+ }
+ }
+
+ _createToastAlert() {
+ const el = document.createElement('gr-alert');
+ el.toast = true;
+ return el;
+ }
+
+ _handleVisibilityChange() {
+ // Ignore when the page is transitioning to hidden (or hidden is
+ // undefined).
+ if (document.hidden !== false) { return; }
+
+ // 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;
+ if (!this._refreshingCredentials &&
+ this.knownAccountId !== undefined &&
+ timeSinceLastCheck > STALE_CREDENTIAL_THRESHOLD_MS) {
+ this._lastCredentialCheck = Date.now();
+
+ // check auth status in case:
+ // - user signed out
+ // - user switched account
+ this._checkSignedIn();
+ }
+ }
+
+ _requestCheckLoggedIn() {
+ this.debounce(
+ 'checkLoggedIn', this._checkSignedIn, CHECK_SIGN_IN_INTERVAL_MS);
+ }
+
+ _checkSignedIn() {
+ this._lastCredentialCheck = Date.now();
+
+ // force to refetch account info
+ this.$.restAPI.invalidateAccountsCache();
+ this._authService.clearCache();
+
+ this.$.restAPI.getLoggedIn().then(isLoggedIn => {
+ // do nothing if its refreshing
+ 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();
+ } else {
+ // check account
+ this.$.restAPI.getAccount().then(account => {
+ if (this._refreshingCredentials) {
+ // If the credentials were refreshed but the account is different
+ // then reload the page completely.
+ if (account._account_id !== this.knownAccountId) {
+ this._reloadPage();
+ return;
+ }
+
+ this._handleCredentialRefreshed();
+ }
+ });
+ }
+ });
+ }
+
+ _reloadPage() {
+ window.location.reload();
+ }
+
+ _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 = [
+ 'width=' + SIGN_IN_WIDTH_PX,
+ 'height=' + SIGN_IN_HEIGHT_PX,
+ 'left=' + left,
+ 'top=' + top,
+ ];
+ window.open(this.getBaseUrl() +
+ '/login/%3FcloseAfterLogin', '_blank', options.join(','));
+ this.listen(window, 'focus', '_handleWindowFocus');
+ }
+
+ _handleCredentialRefreshed() {
+ this.unlisten(window, 'focus', '_handleWindowFocus');
+ this._refreshingCredentials = false;
+ this._hideAlert();
+ this._showAlert('Credentials refreshed.');
+ this.$.noInteractionOverlay.close();
+
+ // Clear the cache for auth
+ this._authService.clearCache();
+ }
+
+ _handleWindowFocus() {
+ this.flushDebouncer('checkLoggedIn');
+ }
+
+ _handleShowErrorDialog(e) {
+ this._showErrorDialog(e.detail.message);
+ }
+
+ _handleDismissErrorDialog() {
+ this.$.errorOverlay.close();
+ }
+
+ _showErrorDialog(message, opt_options) {
+ this.$.reporting.reportErrorDialog(message);
+ this.$.errorDialog.text = message;
+ this.$.errorDialog.showSignInButton =
+ opt_options && opt_options.showSignInButton;
+ this.$.errorOverlay.open();
+ }
+}
+
+customElements.define(GrErrorManager.is, GrErrorManager);
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js
new file mode 100644
index 0000000..5661d1e
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js
@@ -0,0 +1,27 @@
+/**
+ * @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.js';
+
+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>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ <gr-reporting id="reporting"></gr-reporting>
+`;
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index e984577..1d3b1fe 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -19,14 +19,15 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-error-manager</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html" />
-<link rel="import" href="gr-error-manager.html">
-
-<script>void (0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-error-manager.js';
+void (0);
+</script>
<test-fixture id="basic">
<template>
@@ -34,465 +35,467 @@
</template>
</test-fixture>
-<script>
- suite('gr-error-manager tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-error-manager.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-error-manager tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('when authed', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
+ sandbox.stub(window, 'fetch')
+ .returns(Promise.resolve({ok: true, status: 204}));
+ element = fixture('basic');
+ element._authService.clearCache();
});
- teardown(() => {
- sandbox.restore();
+ test('does not show auth error on 403 by default', done => {
+ const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
+ const responseText = Promise.resolve('server says no.');
+ element.fire('server-error',
+ {response: {status: 403, text() { return responseText; }}}
+ );
+ flush(() => {
+ assert.isFalse(showAuthErrorStub.calledOnce);
+ done();
+ });
});
- suite('when authed', () => {
- setup(() => {
- sandbox.stub(window, 'fetch')
- .returns(Promise.resolve({ok: true, status: 204}));
- element = fixture('basic');
- element._authService.clearCache();
- });
-
- test('does not show auth error on 403 by default', done => {
- const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
- const responseText = Promise.resolve('server says no.');
- element.fire('server-error',
- {response: {status: 403, text() { return responseText; }}}
- );
- flush(() => {
- assert.isFalse(showAuthErrorStub.calledOnce);
- done();
- });
- });
-
- test('show auth required for 403 with auth error and not authed before',
- done => {
- const showAuthErrorStub = sandbox.stub(
- element, '_showAuthErrorAlert'
- );
- const responseText = Promise.resolve('Authentication required\n');
- sinon.stub(element.$.restAPI, 'getLoggedIn')
- .returns(Promise.resolve(true));
- element.fire('server-error',
- {response: {status: 403, text() { return responseText; }}}
- );
- flush(() => {
- assert.isTrue(showAuthErrorStub.calledOnce);
- done();
- });
- });
-
- test('recheck auth for 403 with auth error if authed before', done => {
- // starts with authed state
- element.$.restAPI.getLoggedIn();
- const responseText = Promise.resolve('Authentication required\n');
- sinon.stub(element.$.restAPI, 'getLoggedIn')
- .returns(Promise.resolve(true));
- element.fire('server-error',
- {response: {status: 403, text() { return responseText; }}}
- );
- flush(() => {
- assert.isTrue(element.$.restAPI.getLoggedIn.calledOnce);
- done();
- });
- });
-
- test('show logged in error', () => {
- sandbox.stub(element, '_showAuthErrorAlert');
- element.fire('show-auth-required');
- assert.isTrue(element._showAuthErrorAlert.calledWithExactly(
- 'Log in is required to perform that action.', 'Log in.'));
- });
-
- test('show normal Error', done => {
- const showErrorStub = sandbox.stub(element, '_showErrorDialog');
- const textSpy = sandbox.spy(() => Promise.resolve('ZOMG'));
- element.fire('server-error', {response: {status: 500, text: textSpy}});
-
- assert.isTrue(textSpy.called);
- flush(() => {
- assert.isTrue(showErrorStub.calledOnce);
- assert.isTrue(showErrorStub.lastCall.calledWithExactly(
- 'Error 500: ZOMG'));
- done();
- });
- });
-
- test('_constructServerErrorMsg', () => {
- const errorText = 'change conflicts';
- const status = 409;
- const statusText = 'Conflict';
- const url = '/my/test/url';
-
- assert.equal(element._constructServerErrorMsg({status}),
- 'Error 409');
- assert.equal(element._constructServerErrorMsg({status, url}),
- 'Error 409: \nEndpoint: /my/test/url');
- assert.equal(element.
- _constructServerErrorMsg({status, statusText, url}),
- 'Error 409 (Conflict): \nEndpoint: /my/test/url');
- assert.equal(element._constructServerErrorMsg({
- status,
- statusText,
- errorText,
- url,
- }), 'Error 409 (Conflict): change conflicts' +
- '\nEndpoint: /my/test/url');
- assert.equal(element._constructServerErrorMsg({
- status,
- statusText,
- errorText,
- url,
- trace: 'xxxxx',
- }), 'Error 409 (Conflict): change conflicts' +
- '\nEndpoint: /my/test/url\nTrace Id: xxxxx');
- });
-
- test('extract trace id from headers if exists', done => {
- const textSpy = sandbox.spy(
- () => Promise.resolve('500')
- );
- const headers = new Headers();
- headers.set('X-Gerrit-Trace', 'xxxx');
- element.fire('server-error', {
- response: {
- headers,
- status: 500,
- text: textSpy,
- },
- });
- flush(() => {
- assert.equal(
- element.$.errorDialog.text,
- 'Error 500: 500\nTrace Id: xxxx'
+ test('show auth required for 403 with auth error and not authed before',
+ done => {
+ const showAuthErrorStub = sandbox.stub(
+ element, '_showAuthErrorAlert'
);
- done();
- });
- });
-
- test('suppress TOO_MANY_FILES error', done => {
- const showAlertStub = sandbox.stub(element, '_showAlert');
- const textSpy = sandbox.spy(
- () => Promise.resolve('too many files to find conflicts')
- );
- element.fire('server-error', {response: {status: 500, text: textSpy}});
-
- assert.isTrue(textSpy.called);
- flush(() => {
- assert.isFalse(showAlertStub.called);
- done();
- });
- });
-
- test('show network error', done => {
- const consoleErrorStub = sandbox.stub(console, 'error');
- const showAlertStub = sandbox.stub(element, '_showAlert');
- element.fire('network-error', {error: new Error('ZOMG')});
- flush(() => {
- assert.isTrue(showAlertStub.calledOnce);
- assert.isTrue(showAlertStub.lastCall.calledWithExactly(
- 'Server unavailable'));
- assert.isTrue(consoleErrorStub.calledOnce);
- assert.isTrue(consoleErrorStub.lastCall.calledWithExactly('ZOMG'));
- done();
- });
- });
-
- test('show auth refresh toast', done => {
- // starts with authed state
- element.$.restAPI.getLoggedIn();
- const refreshStub = sandbox.stub(element.$.restAPI, 'getAccount',
- () => Promise.resolve({}));
- const toastSpy = sandbox.spy(element, '_createToastAlert');
- const windowOpen = sandbox.stub(window, 'open');
- const responseText = Promise.resolve('Authentication required\n');
- // fake failed auth
- window.fetch.returns(Promise.resolve({status: 403}));
- element.fire('server-error',
- {response: {status: 403, text() { return responseText; }}}
- );
- assert.equal(window.fetch.callCount, 1);
- flush(() => {
- // here needs two flush as there are two chanined
- // promises on server-error handler and flush only flushes one
- assert.equal(window.fetch.callCount, 2);
+ const responseText = Promise.resolve('Authentication required\n');
+ sinon.stub(element.$.restAPI, 'getLoggedIn')
+ .returns(Promise.resolve(true));
+ element.fire('server-error',
+ {response: {status: 403, text() { return responseText; }}}
+ );
flush(() => {
- // auth-error fired
- assert.isTrue(toastSpy.called);
+ assert.isTrue(showAuthErrorStub.calledOnce);
+ done();
+ });
+ });
- // toast
- let toast = toastSpy.lastCall.returnValue;
+ test('recheck auth for 403 with auth error if authed before', done => {
+ // starts with authed state
+ element.$.restAPI.getLoggedIn();
+ const responseText = Promise.resolve('Authentication required\n');
+ sinon.stub(element.$.restAPI, 'getLoggedIn')
+ .returns(Promise.resolve(true));
+ element.fire('server-error',
+ {response: {status: 403, text() { return responseText; }}}
+ );
+ flush(() => {
+ assert.isTrue(element.$.restAPI.getLoggedIn.calledOnce);
+ done();
+ });
+ });
+
+ test('show logged in error', () => {
+ sandbox.stub(element, '_showAuthErrorAlert');
+ element.fire('show-auth-required');
+ assert.isTrue(element._showAuthErrorAlert.calledWithExactly(
+ 'Log in is required to perform that action.', 'Log in.'));
+ });
+
+ test('show normal Error', done => {
+ const showErrorStub = sandbox.stub(element, '_showErrorDialog');
+ const textSpy = sandbox.spy(() => Promise.resolve('ZOMG'));
+ element.fire('server-error', {response: {status: 500, text: textSpy}});
+
+ assert.isTrue(textSpy.called);
+ flush(() => {
+ assert.isTrue(showErrorStub.calledOnce);
+ assert.isTrue(showErrorStub.lastCall.calledWithExactly(
+ 'Error 500: ZOMG'));
+ done();
+ });
+ });
+
+ test('_constructServerErrorMsg', () => {
+ const errorText = 'change conflicts';
+ const status = 409;
+ const statusText = 'Conflict';
+ const url = '/my/test/url';
+
+ assert.equal(element._constructServerErrorMsg({status}),
+ 'Error 409');
+ assert.equal(element._constructServerErrorMsg({status, url}),
+ 'Error 409: \nEndpoint: /my/test/url');
+ assert.equal(element.
+ _constructServerErrorMsg({status, statusText, url}),
+ 'Error 409 (Conflict): \nEndpoint: /my/test/url');
+ assert.equal(element._constructServerErrorMsg({
+ status,
+ statusText,
+ errorText,
+ url,
+ }), 'Error 409 (Conflict): change conflicts' +
+ '\nEndpoint: /my/test/url');
+ assert.equal(element._constructServerErrorMsg({
+ status,
+ statusText,
+ errorText,
+ url,
+ trace: 'xxxxx',
+ }), 'Error 409 (Conflict): change conflicts' +
+ '\nEndpoint: /my/test/url\nTrace Id: xxxxx');
+ });
+
+ test('extract trace id from headers if exists', done => {
+ const textSpy = sandbox.spy(
+ () => Promise.resolve('500')
+ );
+ const headers = new Headers();
+ headers.set('X-Gerrit-Trace', 'xxxx');
+ element.fire('server-error', {
+ response: {
+ headers,
+ status: 500,
+ text: textSpy,
+ },
+ });
+ flush(() => {
+ assert.equal(
+ element.$.errorDialog.text,
+ 'Error 500: 500\nTrace Id: xxxx'
+ );
+ done();
+ });
+ });
+
+ test('suppress TOO_MANY_FILES error', done => {
+ const showAlertStub = sandbox.stub(element, '_showAlert');
+ const textSpy = sandbox.spy(
+ () => Promise.resolve('too many files to find conflicts')
+ );
+ element.fire('server-error', {response: {status: 500, text: textSpy}});
+
+ assert.isTrue(textSpy.called);
+ flush(() => {
+ assert.isFalse(showAlertStub.called);
+ done();
+ });
+ });
+
+ test('show network error', done => {
+ const consoleErrorStub = sandbox.stub(console, 'error');
+ const showAlertStub = sandbox.stub(element, '_showAlert');
+ element.fire('network-error', {error: new Error('ZOMG')});
+ flush(() => {
+ assert.isTrue(showAlertStub.calledOnce);
+ assert.isTrue(showAlertStub.lastCall.calledWithExactly(
+ 'Server unavailable'));
+ assert.isTrue(consoleErrorStub.calledOnce);
+ assert.isTrue(consoleErrorStub.lastCall.calledWithExactly('ZOMG'));
+ done();
+ });
+ });
+
+ test('show auth refresh toast', done => {
+ // starts with authed state
+ element.$.restAPI.getLoggedIn();
+ const refreshStub = sandbox.stub(element.$.restAPI, 'getAccount',
+ () => Promise.resolve({}));
+ const toastSpy = sandbox.spy(element, '_createToastAlert');
+ const windowOpen = sandbox.stub(window, 'open');
+ const responseText = Promise.resolve('Authentication required\n');
+ // fake failed auth
+ window.fetch.returns(Promise.resolve({status: 403}));
+ element.fire('server-error',
+ {response: {status: 403, text() { return responseText; }}}
+ );
+ assert.equal(window.fetch.callCount, 1);
+ flush(() => {
+ // here needs two flush as there are two chanined
+ // promises on server-error handler and flush only flushes one
+ assert.equal(window.fetch.callCount, 2);
+ flush(() => {
+ // auth-error fired
+ assert.isTrue(toastSpy.called);
+
+ // toast
+ let toast = toastSpy.lastCall.returnValue;
+ assert.isOk(toast);
+ assert.include(
+ dom(toast.root).textContent, 'Credentials expired.');
+ assert.include(
+ dom(toast.root).textContent, 'Refresh credentials');
+
+ // noInteractionOverlay
+ const noInteractionOverlay = element.$.noInteractionOverlay;
+ assert.isOk(noInteractionOverlay);
+ sinon.spy(noInteractionOverlay, 'close');
+ assert.equal(
+ noInteractionOverlay.backdropElement.getAttribute('opened'),
+ '');
+ assert.isFalse(windowOpen.called);
+ MockInteractions.tap(toast.shadowRoot
+ .querySelector('gr-button.action'));
+ assert.isTrue(windowOpen.called);
+
+ // @see Issue 5822: noopener breaks closeAfterLogin
+ assert.equal(windowOpen.lastCall.args[2].indexOf('noopener=yes'),
+ -1);
+
+ const hideToastSpy = sandbox.spy(toast, 'hide');
+
+ // now fake authed
+ window.fetch.returns(Promise.resolve({status: 204}));
+ element._handleWindowFocus();
+ element.flushDebouncer('checkLoggedIn');
+ flush(() => {
+ assert.isTrue(refreshStub.called);
+ assert.isTrue(hideToastSpy.called);
+
+ // toast update
+ assert.notStrictEqual(toastSpy.lastCall.returnValue, toast);
+ toast = toastSpy.lastCall.returnValue;
assert.isOk(toast);
assert.include(
- Polymer.dom(toast.root).textContent, 'Credentials expired.');
- assert.include(
- Polymer.dom(toast.root).textContent, 'Refresh credentials');
+ dom(toast.root).textContent, 'Credentials refreshed');
- // noInteractionOverlay
- const noInteractionOverlay = element.$.noInteractionOverlay;
- assert.isOk(noInteractionOverlay);
- sinon.spy(noInteractionOverlay, 'close');
- assert.equal(
- noInteractionOverlay.backdropElement.getAttribute('opened'),
- '');
- assert.isFalse(windowOpen.called);
- MockInteractions.tap(toast.shadowRoot
- .querySelector('gr-button.action'));
- assert.isTrue(windowOpen.called);
-
- // @see Issue 5822: noopener breaks closeAfterLogin
- assert.equal(windowOpen.lastCall.args[2].indexOf('noopener=yes'),
- -1);
-
- const hideToastSpy = sandbox.spy(toast, 'hide');
-
- // now fake authed
- window.fetch.returns(Promise.resolve({status: 204}));
- element._handleWindowFocus();
- element.flushDebouncer('checkLoggedIn');
- flush(() => {
- assert.isTrue(refreshStub.called);
- assert.isTrue(hideToastSpy.called);
-
- // toast update
- assert.notStrictEqual(toastSpy.lastCall.returnValue, toast);
- toast = toastSpy.lastCall.returnValue;
- assert.isOk(toast);
- assert.include(
- Polymer.dom(toast.root).textContent, 'Credentials refreshed');
-
- // close overlay
- assert.isTrue(noInteractionOverlay.close.called);
- done();
- });
- });
- });
- });
-
- test('auth toast should dismiss existing toast', done => {
- // starts with authed state
- element.$.restAPI.getLoggedIn();
- const toastSpy = sandbox.spy(element, '_createToastAlert');
- const responseText = Promise.resolve('Authentication required\n');
-
- // fake an alert
- element.fire('show-alert', {message: 'test reload', action: 'reload'});
- const toast = toastSpy.lastCall.returnValue;
- assert.isOk(toast);
- assert.include(
- Polymer.dom(toast.root).textContent, 'test reload');
-
- // fake auth
- window.fetch.returns(Promise.resolve({status: 403}));
- element.fire('server-error',
- {response: {status: 403, text() { return responseText; }}}
- );
- assert.equal(window.fetch.callCount, 1);
- flush(() => {
- // here needs two flush as there are two chanined
- // promises on server-error handler and flush only flushes one
- assert.equal(window.fetch.callCount, 2);
- flush(() => {
- // toast
- const toast = toastSpy.lastCall.returnValue;
- assert.include(
- Polymer.dom(toast.root).textContent, 'Credentials expired.');
- assert.include(
- Polymer.dom(toast.root).textContent, 'Refresh credentials');
+ // close overlay
+ assert.isTrue(noInteractionOverlay.close.called);
done();
});
});
});
+ });
- test('regular toast should dismiss regular toast', () => {
- // starts with authed state
- element.$.restAPI.getLoggedIn();
- const toastSpy = sandbox.spy(element, '_createToastAlert');
+ test('auth toast should dismiss existing toast', done => {
+ // starts with authed state
+ element.$.restAPI.getLoggedIn();
+ const toastSpy = sandbox.spy(element, '_createToastAlert');
+ const responseText = Promise.resolve('Authentication required\n');
- // fake an alert
- element.fire('show-alert', {message: 'test reload', action: 'reload'});
- let toast = toastSpy.lastCall.returnValue;
- assert.isOk(toast);
- assert.include(
- Polymer.dom(toast.root).textContent, 'test reload');
+ // fake an alert
+ element.fire('show-alert', {message: 'test reload', action: 'reload'});
+ const toast = toastSpy.lastCall.returnValue;
+ assert.isOk(toast);
+ assert.include(
+ dom(toast.root).textContent, 'test reload');
- // new alert
- element.fire('show-alert', {message: 'second-test', action: 'reload'});
-
- toast = toastSpy.lastCall.returnValue;
- assert.include(Polymer.dom(toast.root).textContent, 'second-test');
- });
-
- test('regular toast should not dismiss auth toast', done => {
- // starts with authed state
- element.$.restAPI.getLoggedIn();
- const toastSpy = sandbox.spy(element, '_createToastAlert');
- const responseText = Promise.resolve('Authentication required\n');
-
- // fake auth
- window.fetch.returns(Promise.resolve({status: 403}));
- element.fire('server-error',
- {response: {status: 403, text() { return responseText; }}}
- );
- assert.equal(window.fetch.callCount, 1);
+ // fake auth
+ window.fetch.returns(Promise.resolve({status: 403}));
+ element.fire('server-error',
+ {response: {status: 403, text() { return responseText; }}}
+ );
+ assert.equal(window.fetch.callCount, 1);
+ flush(() => {
+ // here needs two flush as there are two chanined
+ // promises on server-error handler and flush only flushes one
+ assert.equal(window.fetch.callCount, 2);
flush(() => {
- // here needs two flush as there are two chanined
- // promises on server-error handler and flush only flushes one
- assert.equal(window.fetch.callCount, 2);
- flush(() => {
- let toast = toastSpy.lastCall.returnValue;
- assert.include(
- Polymer.dom(toast.root).textContent, 'Credentials expired.');
- assert.include(
- Polymer.dom(toast.root).textContent, 'Refresh credentials');
+ // toast
+ const toast = toastSpy.lastCall.returnValue;
+ assert.include(
+ dom(toast.root).textContent, 'Credentials expired.');
+ assert.include(
+ dom(toast.root).textContent, 'Refresh credentials');
+ done();
+ });
+ });
+ });
- // fake an alert
- element.fire('show-alert', {
- message: 'test-alert', action: 'reload',
- });
- flush(() => {
- toast = toastSpy.lastCall.returnValue;
- assert.isOk(toast);
- assert.include(
- Polymer.dom(toast.root).textContent, 'Credentials expired.');
- done();
- });
+ test('regular toast should dismiss regular toast', () => {
+ // starts with authed state
+ element.$.restAPI.getLoggedIn();
+ const toastSpy = sandbox.spy(element, '_createToastAlert');
+
+ // fake an alert
+ element.fire('show-alert', {message: 'test reload', action: 'reload'});
+ let toast = toastSpy.lastCall.returnValue;
+ assert.isOk(toast);
+ assert.include(
+ dom(toast.root).textContent, 'test reload');
+
+ // new alert
+ element.fire('show-alert', {message: 'second-test', action: 'reload'});
+
+ toast = toastSpy.lastCall.returnValue;
+ assert.include(dom(toast.root).textContent, 'second-test');
+ });
+
+ test('regular toast should not dismiss auth toast', done => {
+ // starts with authed state
+ element.$.restAPI.getLoggedIn();
+ const toastSpy = sandbox.spy(element, '_createToastAlert');
+ const responseText = Promise.resolve('Authentication required\n');
+
+ // fake auth
+ window.fetch.returns(Promise.resolve({status: 403}));
+ element.fire('server-error',
+ {response: {status: 403, text() { return responseText; }}}
+ );
+ assert.equal(window.fetch.callCount, 1);
+ flush(() => {
+ // here needs two flush as there are two chanined
+ // promises on server-error handler and flush only flushes one
+ assert.equal(window.fetch.callCount, 2);
+ flush(() => {
+ let toast = toastSpy.lastCall.returnValue;
+ assert.include(
+ dom(toast.root).textContent, 'Credentials expired.');
+ assert.include(
+ dom(toast.root).textContent, 'Refresh credentials');
+
+ // fake an alert
+ element.fire('show-alert', {
+ message: 'test-alert', action: 'reload',
+ });
+ flush(() => {
+ toast = toastSpy.lastCall.returnValue;
+ assert.isOk(toast);
+ assert.include(
+ dom(toast.root).textContent, 'Credentials expired.');
+ done();
});
});
});
+ });
- test('show alert', () => {
- const alertObj = {message: 'foo'};
- sandbox.stub(element, '_showAlert');
- element.fire('show-alert', alertObj);
- assert.isTrue(element._showAlert.calledOnce);
- assert.equal(element._showAlert.lastCall.args[0], 'foo');
- assert.isNotOk(element._showAlert.lastCall.args[1]);
- assert.isNotOk(element._showAlert.lastCall.args[2]);
- });
+ test('show alert', () => {
+ const alertObj = {message: 'foo'};
+ sandbox.stub(element, '_showAlert');
+ element.fire('show-alert', alertObj);
+ assert.isTrue(element._showAlert.calledOnce);
+ assert.equal(element._showAlert.lastCall.args[0], 'foo');
+ assert.isNotOk(element._showAlert.lastCall.args[1]);
+ assert.isNotOk(element._showAlert.lastCall.args[2]);
+ });
- test('checks stale credentials on visibility change', () => {
- const refreshStub = sandbox.stub(element,
- '_checkSignedIn');
- sandbox.stub(Date, 'now').returns(999999);
- element._lastCredentialCheck = 0;
- element._handleVisibilityChange();
+ test('checks stale credentials on visibility change', () => {
+ const refreshStub = sandbox.stub(element,
+ '_checkSignedIn');
+ sandbox.stub(Date, 'now').returns(999999);
+ element._lastCredentialCheck = 0;
+ element._handleVisibilityChange();
- // Since there is no known account, it should not test credentials.
- assert.isFalse(refreshStub.called);
- assert.equal(element._lastCredentialCheck, 0);
+ // Since there is no known account, it should not test credentials.
+ assert.isFalse(refreshStub.called);
+ assert.equal(element._lastCredentialCheck, 0);
- element.knownAccountId = 123;
- element._handleVisibilityChange();
+ element.knownAccountId = 123;
+ element._handleVisibilityChange();
- // Should test credentials, since there is a known account.
- assert.isTrue(refreshStub.called);
- assert.equal(element._lastCredentialCheck, 999999);
- });
+ // Should test credentials, since there is a known account.
+ assert.isTrue(refreshStub.called);
+ assert.equal(element._lastCredentialCheck, 999999);
+ });
- test('refreshes with same credentials', done => {
- const accountPromise = Promise.resolve({_account_id: 1234});
- sandbox.stub(element.$.restAPI, 'getAccount')
- .returns(accountPromise);
- const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
- const handleRefreshStub = sandbox.stub(element,
- '_handleCredentialRefreshed');
- const reloadStub = sandbox.stub(element, '_reloadPage');
+ test('refreshes with same credentials', done => {
+ const accountPromise = Promise.resolve({_account_id: 1234});
+ sandbox.stub(element.$.restAPI, 'getAccount')
+ .returns(accountPromise);
+ const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
+ const handleRefreshStub = sandbox.stub(element,
+ '_handleCredentialRefreshed');
+ const reloadStub = sandbox.stub(element, '_reloadPage');
- element.knownAccountId = 1234;
- element._refreshingCredentials = true;
- element._checkSignedIn();
+ element.knownAccountId = 1234;
+ element._refreshingCredentials = true;
+ element._checkSignedIn();
- flush(() => {
- assert.isFalse(requestCheckStub.called);
- assert.isTrue(handleRefreshStub.called);
- assert.isFalse(reloadStub.called);
- done();
- });
- });
-
- test('_showAlert hides existing alerts', () => {
- element._alertElement = element._createToastAlert();
- const hideStub = sandbox.stub(element, '_hideAlert');
- element._showAlert();
- assert.isTrue(hideStub.calledOnce);
- });
-
- test('show-error', () => {
- const openStub = sandbox.stub(element.$.errorOverlay, 'open');
- const closeStub = sandbox.stub(element.$.errorOverlay, 'close');
- const reportStub = sandbox.stub(
- element.$.reporting,
- 'reportErrorDialog'
- );
-
- const message = 'test message';
- element.fire('show-error', {message});
- flushAsynchronousOperations();
-
- assert.isTrue(openStub.called);
- assert.isTrue(reportStub.called);
- assert.equal(element.$.errorDialog.text, message);
-
- element.$.errorDialog.fire('dismiss');
- flushAsynchronousOperations();
-
- assert.isTrue(closeStub.called);
- });
-
- test('reloads when refreshed credentials differ', done => {
- const accountPromise = Promise.resolve({_account_id: 1234});
- sandbox.stub(element.$.restAPI, 'getAccount')
- .returns(accountPromise);
- const requestCheckStub = sandbox.stub(
- element,
- '_requestCheckLoggedIn');
- const handleRefreshStub = sandbox.stub(element,
- '_handleCredentialRefreshed');
- const reloadStub = sandbox.stub(element, '_reloadPage');
-
- element.knownAccountId = 4321; // Different from 1234
- element._refreshingCredentials = true;
- element._checkSignedIn();
-
- flush(() => {
- assert.isFalse(requestCheckStub.called);
- assert.isFalse(handleRefreshStub.called);
- assert.isTrue(reloadStub.called);
- done();
- });
+ flush(() => {
+ assert.isFalse(requestCheckStub.called);
+ assert.isTrue(handleRefreshStub.called);
+ assert.isFalse(reloadStub.called);
+ done();
});
});
- suite('when not authed', () => {
- setup(() => {
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(false); },
- });
- element = fixture('basic');
- });
+ test('_showAlert hides existing alerts', () => {
+ element._alertElement = element._createToastAlert();
+ const hideStub = sandbox.stub(element, '_hideAlert');
+ element._showAlert();
+ assert.isTrue(hideStub.calledOnce);
+ });
- test('refresh loop continues on credential fail', done => {
- const requestCheckStub = sandbox.stub(
- element,
- '_requestCheckLoggedIn');
- const handleRefreshStub = sandbox.stub(element,
- '_handleCredentialRefreshed');
- const reloadStub = sandbox.stub(element, '_reloadPage');
+ test('show-error', () => {
+ const openStub = sandbox.stub(element.$.errorOverlay, 'open');
+ const closeStub = sandbox.stub(element.$.errorOverlay, 'close');
+ const reportStub = sandbox.stub(
+ element.$.reporting,
+ 'reportErrorDialog'
+ );
- element._refreshingCredentials = true;
- element._checkSignedIn();
+ const message = 'test message';
+ element.fire('show-error', {message});
+ flushAsynchronousOperations();
- flush(() => {
- assert.isTrue(requestCheckStub.called);
- assert.isFalse(handleRefreshStub.called);
- assert.isFalse(reloadStub.called);
- done();
- });
+ assert.isTrue(openStub.called);
+ assert.isTrue(reportStub.called);
+ assert.equal(element.$.errorDialog.text, message);
+
+ element.$.errorDialog.fire('dismiss');
+ flushAsynchronousOperations();
+
+ assert.isTrue(closeStub.called);
+ });
+
+ test('reloads when refreshed credentials differ', done => {
+ const accountPromise = Promise.resolve({_account_id: 1234});
+ sandbox.stub(element.$.restAPI, 'getAccount')
+ .returns(accountPromise);
+ const requestCheckStub = sandbox.stub(
+ element,
+ '_requestCheckLoggedIn');
+ const handleRefreshStub = sandbox.stub(element,
+ '_handleCredentialRefreshed');
+ const reloadStub = sandbox.stub(element, '_reloadPage');
+
+ element.knownAccountId = 4321; // Different from 1234
+ element._refreshingCredentials = true;
+ element._checkSignedIn();
+
+ flush(() => {
+ assert.isFalse(requestCheckStub.called);
+ assert.isFalse(handleRefreshStub.called);
+ assert.isTrue(reloadStub.called);
+ done();
});
});
});
+
+ suite('when not authed', () => {
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(false); },
+ });
+ element = fixture('basic');
+ });
+
+ test('refresh loop continues on credential fail', done => {
+ const requestCheckStub = sandbox.stub(
+ element,
+ '_requestCheckLoggedIn');
+ const handleRefreshStub = sandbox.stub(element,
+ '_handleCredentialRefreshed');
+ const reloadStub = sandbox.stub(element, '_reloadPage');
+
+ element._refreshingCredentials = true;
+ element._checkSignedIn();
+
+ flush(() => {
+ assert.isTrue(requestCheckStub.called);
+ assert.isFalse(handleRefreshStub.called);
+ assert.isFalse(reloadStub.called);
+ done();
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.html b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.html
deleted file mode 100644
index a863276..0000000
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.html
+++ /dev/null
@@ -1,48 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-key-binding-display">
- <template>
- <style include="shared-styles">
- .key {
- background-color: var(--chip-background-color);
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius);
- display: inline-block;
- font-weight: var(--font-weight-bold);
- padding: var(--spacing-xxs) var(--spacing-m);
- text-align: center;
- }
- </style>
- <template is="dom-repeat" items="[[binding]]">
- <template is="dom-if" if="[[index]]">
- or
- </template>
- <template
- is="dom-repeat"
- items="[[_computeModifiers(item)]]"
- as="modifier">
- <span class="key modifier">[[modifier]]</span>
- </template>
- <span class="key">[[_computeKey(item)]]</span>
- </template>
- </template>
- <script src="gr-key-binding-display.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
index 3d424bc..5d7ec27 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
@@ -14,30 +14,36 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrKeyBindingDisplay extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-key-binding-display'; }
+import '../../../styles/shared-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-key-binding-display_html.js';
- static get properties() {
- return {
- /** @type {Array<string>} */
- binding: Array,
- };
- }
+/** @extends Polymer.Element */
+class GrKeyBindingDisplay extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _computeModifiers(binding) {
- return binding.slice(0, binding.length - 1);
- }
+ static get is() { return 'gr-key-binding-display'; }
- _computeKey(binding) {
- return binding[binding.length - 1];
- }
+ static get properties() {
+ return {
+ /** @type {Array<string>} */
+ binding: Array,
+ };
}
- customElements.define(GrKeyBindingDisplay.is, GrKeyBindingDisplay);
-})();
+ _computeModifiers(binding) {
+ return binding.slice(0, binding.length - 1);
+ }
+
+ _computeKey(binding) {
+ return binding[binding.length - 1];
+ }
+}
+
+customElements.define(GrKeyBindingDisplay.is, GrKeyBindingDisplay);
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js
new file mode 100644
index 0000000..f98be3a
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js
@@ -0,0 +1,40 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ .key {
+ background-color: var(--chip-background-color);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ display: inline-block;
+ font-weight: var(--font-weight-bold);
+ padding: var(--spacing-xxs) var(--spacing-m);
+ text-align: center;
+ }
+ </style>
+ <template is="dom-repeat" items="[[binding]]">
+ <template is="dom-if" if="[[index]]">
+ or
+ </template>
+ <template is="dom-repeat" items="[[_computeModifiers(item)]]" as="modifier">
+ <span class="key modifier">[[modifier]]</span>
+ </template>
+ <span class="key">[[_computeKey(item)]]</span>
+ </template>
+`;
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html
index f682f0a..1bf0b14 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html
@@ -18,15 +18,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-key-binding-display</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-key-binding-display.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -34,37 +29,38 @@
</template>
</test-fixture>
-<script>
- suite('gr-key-binding-display tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-key-binding-display.js';
+suite('gr-key-binding-display tests', () => {
+ let element;
- setup(() => {
- element = fixture('basic');
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ suite('_computeKey', () => {
+ test('unmodified key', () => {
+ assert.strictEqual(element._computeKey(['x']), 'x');
});
- suite('_computeKey', () => {
- test('unmodified key', () => {
- assert.strictEqual(element._computeKey(['x']), 'x');
- });
-
- test('key with modifiers', () => {
- assert.strictEqual(element._computeKey(['Ctrl', 'x']), 'x');
- assert.strictEqual(element._computeKey(['Shift', 'Meta', 'x']), 'x');
- });
- });
-
- suite('_computeModifiers', () => {
- test('single unmodified key', () => {
- assert.deepEqual(element._computeModifiers(['x']), []);
- });
-
- test('key with modifiers', () => {
- assert.deepEqual(element._computeModifiers(['Ctrl', 'x']), ['Ctrl']);
- assert.deepEqual(
- element._computeModifiers(['Shift', 'Meta', 'x']),
- ['Shift', 'Meta']);
- });
+ test('key with modifiers', () => {
+ assert.strictEqual(element._computeKey(['Ctrl', 'x']), 'x');
+ assert.strictEqual(element._computeKey(['Shift', 'Meta', 'x']), 'x');
});
});
+
+ suite('_computeModifiers', () => {
+ test('single unmodified key', () => {
+ assert.deepEqual(element._computeModifiers(['x']), []);
+ });
+
+ test('key with modifiers', () => {
+ assert.deepEqual(element._computeModifiers(['Ctrl', 'x']), ['Ctrl']);
+ assert.deepEqual(
+ element._computeModifiers(['Shift', 'Meta', 'x']),
+ ['Shift', 'Meta']);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
index 4630ca7..371ba02 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
@@ -14,129 +14,140 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const {ShortcutSection} = window.Gerrit.KeyboardShortcutBinder;
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../shared/gr-button/gr-button.js';
+import '../gr-key-binding-display/gr-key-binding-display.js';
+import '../../../styles/shared-styles.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-keyboard-shortcuts-dialog_html.js';
+const {ShortcutSection} = window.Gerrit.KeyboardShortcutBinder;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrKeyboardShortcutsDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-keyboard-shortcuts-dialog'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * Fired when the user presses the close button.
+ *
+ * @event close
*/
- class GrKeyboardShortcutsDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-keyboard-shortcuts-dialog'; }
- /**
- * Fired when the user presses the close button.
- *
- * @event close
- */
- static get properties() {
- return {
- _left: Array,
- _right: Array,
+ static get properties() {
+ return {
+ _left: Array,
+ _right: Array,
- _propertyBySection: {
- type: Object,
- value() {
- return {
- [ShortcutSection.EVERYWHERE]: '_everywhere',
- [ShortcutSection.NAVIGATION]: '_navigation',
- [ShortcutSection.DASHBOARD]: '_dashboard',
- [ShortcutSection.CHANGE_LIST]: '_changeList',
- [ShortcutSection.ACTIONS]: '_actions',
- [ShortcutSection.REPLY_DIALOG]: '_replyDialog',
- [ShortcutSection.FILE_LIST]: '_fileList',
- [ShortcutSection.DIFFS]: '_diffs',
- };
- },
+ _propertyBySection: {
+ type: Object,
+ value() {
+ return {
+ [ShortcutSection.EVERYWHERE]: '_everywhere',
+ [ShortcutSection.NAVIGATION]: '_navigation',
+ [ShortcutSection.DASHBOARD]: '_dashboard',
+ [ShortcutSection.CHANGE_LIST]: '_changeList',
+ [ShortcutSection.ACTIONS]: '_actions',
+ [ShortcutSection.REPLY_DIALOG]: '_replyDialog',
+ [ShortcutSection.FILE_LIST]: '_fileList',
+ [ShortcutSection.DIFFS]: '_diffs',
+ };
},
- };
- }
-
- /** @override */
- ready() {
- super.ready();
- this._ensureAttribute('role', 'dialog');
- }
-
- /** @override */
- attached() {
- super.attached();
- this.addKeyboardShortcutDirectoryListener(
- this._onDirectoryUpdated.bind(this));
- }
-
- /** @override */
- detached() {
- super.detached();
- this.removeKeyboardShortcutDirectoryListener(
- this._onDirectoryUpdated.bind(this));
- }
-
- _handleCloseTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('close', null, {bubbles: false});
- }
-
- _onDirectoryUpdated(directory) {
- const left = [];
- const right = [];
-
- if (directory.has(ShortcutSection.EVERYWHERE)) {
- left.push({
- section: ShortcutSection.EVERYWHERE,
- shortcuts: directory.get(ShortcutSection.EVERYWHERE),
- });
- }
-
- if (directory.has(ShortcutSection.NAVIGATION)) {
- left.push({
- section: ShortcutSection.NAVIGATION,
- shortcuts: directory.get(ShortcutSection.NAVIGATION),
- });
- }
-
- if (directory.has(ShortcutSection.ACTIONS)) {
- right.push({
- section: ShortcutSection.ACTIONS,
- shortcuts: directory.get(ShortcutSection.ACTIONS),
- });
- }
-
- if (directory.has(ShortcutSection.REPLY_DIALOG)) {
- right.push({
- section: ShortcutSection.REPLY_DIALOG,
- shortcuts: directory.get(ShortcutSection.REPLY_DIALOG),
- });
- }
-
- if (directory.has(ShortcutSection.FILE_LIST)) {
- right.push({
- section: ShortcutSection.FILE_LIST,
- shortcuts: directory.get(ShortcutSection.FILE_LIST),
- });
- }
-
- if (directory.has(ShortcutSection.DIFFS)) {
- right.push({
- section: ShortcutSection.DIFFS,
- shortcuts: directory.get(ShortcutSection.DIFFS),
- });
- }
-
- this.set('_left', left);
- this.set('_right', right);
- }
+ },
+ };
}
- customElements.define(GrKeyboardShortcutsDialog.is,
- GrKeyboardShortcutsDialog);
-})();
+ /** @override */
+ ready() {
+ super.ready();
+ this._ensureAttribute('role', 'dialog');
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this.addKeyboardShortcutDirectoryListener(
+ this._onDirectoryUpdated.bind(this));
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this.removeKeyboardShortcutDirectoryListener(
+ this._onDirectoryUpdated.bind(this));
+ }
+
+ _handleCloseTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('close', null, {bubbles: false});
+ }
+
+ _onDirectoryUpdated(directory) {
+ const left = [];
+ const right = [];
+
+ if (directory.has(ShortcutSection.EVERYWHERE)) {
+ left.push({
+ section: ShortcutSection.EVERYWHERE,
+ shortcuts: directory.get(ShortcutSection.EVERYWHERE),
+ });
+ }
+
+ if (directory.has(ShortcutSection.NAVIGATION)) {
+ left.push({
+ section: ShortcutSection.NAVIGATION,
+ shortcuts: directory.get(ShortcutSection.NAVIGATION),
+ });
+ }
+
+ if (directory.has(ShortcutSection.ACTIONS)) {
+ right.push({
+ section: ShortcutSection.ACTIONS,
+ shortcuts: directory.get(ShortcutSection.ACTIONS),
+ });
+ }
+
+ if (directory.has(ShortcutSection.REPLY_DIALOG)) {
+ right.push({
+ section: ShortcutSection.REPLY_DIALOG,
+ shortcuts: directory.get(ShortcutSection.REPLY_DIALOG),
+ });
+ }
+
+ if (directory.has(ShortcutSection.FILE_LIST)) {
+ right.push({
+ section: ShortcutSection.FILE_LIST,
+ shortcuts: directory.get(ShortcutSection.FILE_LIST),
+ });
+ }
+
+ if (directory.has(ShortcutSection.DIFFS)) {
+ right.push({
+ section: ShortcutSection.DIFFS,
+ shortcuts: directory.get(ShortcutSection.DIFFS),
+ });
+ }
+
+ this.set('_left', left);
+ this.set('_right', right);
+ }
+}
+
+customElements.define(GrKeyboardShortcutsDialog.is,
+ GrKeyboardShortcutsDialog);
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
similarity index 64%
rename from polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
rename to polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
index a4424a2..380228f 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
@@ -1,29 +1,22 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.js';
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../gr-key-binding-display/gr-key-binding-display.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-keyboard-shortcuts-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -63,7 +56,7 @@
</style>
<header>
<h3>Keyboard shortcuts</h3>
- <gr-button link on-click="_handleCloseTap">Close</gr-button>
+ <gr-button link="" on-click="_handleCloseTap">Close</gr-button>
</header>
<main>
<table>
@@ -106,6 +99,4 @@
</template>
</main>
<footer></footer>
- </template>
- <script src="gr-keyboard-shortcuts-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html
index cc53db17..06946c5 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html
@@ -18,15 +18,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-key-binding-display</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-keyboard-shortcuts-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -34,150 +29,151 @@
</template>
</test-fixture>
-<script>
- suite('gr-keyboard-shortcuts-dialog tests', async () => {
- await readyToTest();
- const kb = window.Gerrit.KeyboardShortcutBinder;
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-keyboard-shortcuts-dialog.js';
+suite('gr-keyboard-shortcuts-dialog tests', () => {
+ const kb = window.Gerrit.KeyboardShortcutBinder;
+ let element;
- setup(() => {
- element = fixture('basic');
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ function update(directory) {
+ element._onDirectoryUpdated(directory);
+ flushAsynchronousOperations();
+ }
+
+ suite('_left and _right contents', () => {
+ test('empty dialog', () => {
+ assert.strictEqual(element._left.length, 0);
+ assert.strictEqual(element._right.length, 0);
});
- function update(directory) {
- element._onDirectoryUpdated(directory);
- flushAsynchronousOperations();
- }
+ test('everywhere goes on left', () => {
+ update(new Map([
+ [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']],
+ ]));
+ assert.deepEqual(
+ element._left,
+ [
+ {
+ section: kb.ShortcutSection.EVERYWHERE,
+ shortcuts: ['everywhere shortcuts'],
+ },
+ ]);
+ assert.strictEqual(element._right.length, 0);
+ });
- suite('_left and _right contents', () => {
- test('empty dialog', () => {
- assert.strictEqual(element._left.length, 0);
- assert.strictEqual(element._right.length, 0);
- });
+ test('navigation goes on left', () => {
+ update(new Map([
+ [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']],
+ ]));
+ assert.deepEqual(
+ element._left,
+ [
+ {
+ section: kb.ShortcutSection.NAVIGATION,
+ shortcuts: ['navigation shortcuts'],
+ },
+ ]);
+ assert.strictEqual(element._right.length, 0);
+ });
- test('everywhere goes on left', () => {
- update(new Map([
- [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']],
- ]));
- assert.deepEqual(
- element._left,
- [
- {
- section: kb.ShortcutSection.EVERYWHERE,
- shortcuts: ['everywhere shortcuts'],
- },
- ]);
- assert.strictEqual(element._right.length, 0);
- });
+ test('actions go on right', () => {
+ update(new Map([
+ [kb.ShortcutSection.ACTIONS, ['actions shortcuts']],
+ ]));
+ assert.deepEqual(
+ element._right,
+ [
+ {
+ section: kb.ShortcutSection.ACTIONS,
+ shortcuts: ['actions shortcuts'],
+ },
+ ]);
+ assert.strictEqual(element._left.length, 0);
+ });
- test('navigation goes on left', () => {
- update(new Map([
- [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']],
- ]));
- assert.deepEqual(
- element._left,
- [
- {
- section: kb.ShortcutSection.NAVIGATION,
- shortcuts: ['navigation shortcuts'],
- },
- ]);
- assert.strictEqual(element._right.length, 0);
- });
+ test('reply dialog goes on right', () => {
+ update(new Map([
+ [kb.ShortcutSection.REPLY_DIALOG, ['reply dialog shortcuts']],
+ ]));
+ assert.deepEqual(
+ element._right,
+ [
+ {
+ section: kb.ShortcutSection.REPLY_DIALOG,
+ shortcuts: ['reply dialog shortcuts'],
+ },
+ ]);
+ assert.strictEqual(element._left.length, 0);
+ });
- test('actions go on right', () => {
- update(new Map([
- [kb.ShortcutSection.ACTIONS, ['actions shortcuts']],
- ]));
- assert.deepEqual(
- element._right,
- [
- {
- section: kb.ShortcutSection.ACTIONS,
- shortcuts: ['actions shortcuts'],
- },
- ]);
- assert.strictEqual(element._left.length, 0);
- });
+ test('file list goes on right', () => {
+ update(new Map([
+ [kb.ShortcutSection.FILE_LIST, ['file list shortcuts']],
+ ]));
+ assert.deepEqual(
+ element._right,
+ [
+ {
+ section: kb.ShortcutSection.FILE_LIST,
+ shortcuts: ['file list shortcuts'],
+ },
+ ]);
+ assert.strictEqual(element._left.length, 0);
+ });
- test('reply dialog goes on right', () => {
- update(new Map([
- [kb.ShortcutSection.REPLY_DIALOG, ['reply dialog shortcuts']],
- ]));
- assert.deepEqual(
- element._right,
- [
- {
- section: kb.ShortcutSection.REPLY_DIALOG,
- shortcuts: ['reply dialog shortcuts'],
- },
- ]);
- assert.strictEqual(element._left.length, 0);
- });
+ test('diffs go on right', () => {
+ update(new Map([
+ [kb.ShortcutSection.DIFFS, ['diffs shortcuts']],
+ ]));
+ assert.deepEqual(
+ element._right,
+ [
+ {
+ section: kb.ShortcutSection.DIFFS,
+ shortcuts: ['diffs shortcuts'],
+ },
+ ]);
+ assert.strictEqual(element._left.length, 0);
+ });
- test('file list goes on right', () => {
- update(new Map([
- [kb.ShortcutSection.FILE_LIST, ['file list shortcuts']],
- ]));
- assert.deepEqual(
- element._right,
- [
- {
- section: kb.ShortcutSection.FILE_LIST,
- shortcuts: ['file list shortcuts'],
- },
- ]);
- assert.strictEqual(element._left.length, 0);
- });
-
- test('diffs go on right', () => {
- update(new Map([
- [kb.ShortcutSection.DIFFS, ['diffs shortcuts']],
- ]));
- assert.deepEqual(
- element._right,
- [
- {
- section: kb.ShortcutSection.DIFFS,
- shortcuts: ['diffs shortcuts'],
- },
- ]);
- assert.strictEqual(element._left.length, 0);
- });
-
- test('multiple sections on each side', () => {
- update(new Map([
- [kb.ShortcutSection.ACTIONS, ['actions shortcuts']],
- [kb.ShortcutSection.DIFFS, ['diffs shortcuts']],
- [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']],
- [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']],
- ]));
- assert.deepEqual(
- element._left,
- [
- {
- section: kb.ShortcutSection.EVERYWHERE,
- shortcuts: ['everywhere shortcuts'],
- },
- {
- section: kb.ShortcutSection.NAVIGATION,
- shortcuts: ['navigation shortcuts'],
- },
- ]);
- assert.deepEqual(
- element._right,
- [
- {
- section: kb.ShortcutSection.ACTIONS,
- shortcuts: ['actions shortcuts'],
- },
- {
- section: kb.ShortcutSection.DIFFS,
- shortcuts: ['diffs shortcuts'],
- },
- ]);
- });
+ test('multiple sections on each side', () => {
+ update(new Map([
+ [kb.ShortcutSection.ACTIONS, ['actions shortcuts']],
+ [kb.ShortcutSection.DIFFS, ['diffs shortcuts']],
+ [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']],
+ [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']],
+ ]));
+ assert.deepEqual(
+ element._left,
+ [
+ {
+ section: kb.ShortcutSection.EVERYWHERE,
+ shortcuts: ['everywhere shortcuts'],
+ },
+ {
+ section: kb.ShortcutSection.NAVIGATION,
+ shortcuts: ['navigation shortcuts'],
+ },
+ ]);
+ assert.deepEqual(
+ element._right,
+ [
+ {
+ section: kb.ShortcutSection.ACTIONS,
+ shortcuts: ['actions shortcuts'],
+ },
+ {
+ section: kb.ShortcutSection.DIFFS,
+ shortcuts: ['diffs shortcuts'],
+ },
+ ]);
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index 05765fb..c8ed50c 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -14,332 +14,349 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const DEFAULT_LINKS = [{
- title: 'Changes',
- links: [
- {
- url: '/q/status:open',
- name: 'Open',
+import '../../../behaviors/docs-url-behavior/docs-url-behavior.js';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../shared/gr-dropdown/gr-dropdown.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-account-dropdown/gr-account-dropdown.js';
+import '../gr-smart-search/gr-smart-search.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-main-header_html.js';
+
+const DEFAULT_LINKS = [{
+ title: 'Changes',
+ links: [
+ {
+ url: '/q/status:open',
+ name: 'Open',
+ },
+ {
+ url: '/q/status:merged',
+ name: 'Merged',
+ },
+ {
+ url: '/q/status:abandoned',
+ name: 'Abandoned',
+ },
+ ],
+}];
+
+const DOCUMENTATION_LINKS = [
+ {
+ url: '/index.html',
+ name: 'Table of Contents',
+ },
+ {
+ url: '/user-search.html',
+ name: 'Searching',
+ },
+ {
+ url: '/user-upload.html',
+ name: 'Uploading',
+ },
+ {
+ url: '/access-control.html',
+ name: 'Access Control',
+ },
+ {
+ url: '/rest-api.html',
+ name: 'REST API',
+ },
+ {
+ url: '/intro-project-owner.html',
+ name: 'Project Owner Guide',
+ },
+];
+
+// Set of authentication methods that can provide custom registration page.
+const AUTH_TYPES_WITH_REGISTER_URL = new Set([
+ 'LDAP',
+ 'LDAP_BIND',
+ 'CUSTOM_EXTENSION',
+]);
+
+/**
+ * @appliesMixin Gerrit.AdminNavMixin
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.DocsUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrMainHeader extends mixinBehaviors( [
+ Gerrit.AdminNavBehavior,
+ Gerrit.BaseUrlBehavior,
+ Gerrit.DocsUrlBehavior,
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-main-header'; }
+
+ static get properties() {
+ return {
+ searchQuery: {
+ type: String,
+ notify: true,
},
- {
- url: '/q/status:merged',
- name: 'Merged',
+ loggedIn: {
+ type: Boolean,
+ reflectToAttribute: true,
},
- {
- url: '/q/status:abandoned',
- name: 'Abandoned',
+ loading: {
+ type: Boolean,
+ reflectToAttribute: true,
},
- ],
- }];
- const DOCUMENTATION_LINKS = [
- {
- url: '/index.html',
- name: 'Table of Contents',
- },
- {
- url: '/user-search.html',
- name: 'Searching',
- },
- {
- url: '/user-upload.html',
- name: 'Uploading',
- },
- {
- url: '/access-control.html',
- name: 'Access Control',
- },
- {
- url: '/rest-api.html',
- name: 'REST API',
- },
- {
- url: '/intro-project-owner.html',
- name: 'Project Owner Guide',
- },
- ];
+ /** @type {?Object} */
+ _account: Object,
+ _adminLinks: {
+ type: Array,
+ value() { return []; },
+ },
+ _defaultLinks: {
+ type: Array,
+ value() {
+ return DEFAULT_LINKS;
+ },
+ },
+ _docBaseUrl: {
+ type: String,
+ value: null,
+ },
+ _links: {
+ type: Array,
+ computed: '_computeLinks(_defaultLinks, _userLinks, _adminLinks, ' +
+ '_topMenus, _docBaseUrl)',
+ },
+ loginUrl: {
+ type: String,
+ value: '/login',
+ },
+ _userLinks: {
+ type: Array,
+ value() { return []; },
+ },
+ _topMenus: {
+ type: Array,
+ value() { return []; },
+ },
+ _registerText: {
+ type: String,
+ value: 'Sign up',
+ },
+ _registerURL: {
+ type: String,
+ value: null,
+ },
+ };
+ }
- // Set of authentication methods that can provide custom registration page.
- const AUTH_TYPES_WITH_REGISTER_URL = new Set([
- 'LDAP',
- 'LDAP_BIND',
- 'CUSTOM_EXTENSION',
- ]);
+ static get observers() {
+ return [
+ '_accountLoaded(_account)',
+ ];
+ }
- /**
- * @appliesMixin Gerrit.AdminNavMixin
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.DocsUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
- */
- class GrMainHeader extends Polymer.mixinBehaviors( [
- Gerrit.AdminNavBehavior,
- Gerrit.BaseUrlBehavior,
- Gerrit.DocsUrlBehavior,
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-main-header'; }
+ /** @override */
+ ready() {
+ super.ready();
+ this._ensureAttribute('role', 'banner');
+ }
- static get properties() {
+ /** @override */
+ attached() {
+ super.attached();
+ this._loadAccount();
+ this._loadConfig();
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ }
+
+ reload() {
+ this._loadAccount();
+ }
+
+ _computeRelativeURL(path) {
+ return '//' + window.location.host + this.getBaseUrl() + path;
+ }
+
+ _computeLinks(defaultLinks, userLinks, adminLinks, topMenus, docBaseUrl) {
+ // Polymer 2: check for undefined
+ if ([
+ defaultLinks,
+ userLinks,
+ adminLinks,
+ topMenus,
+ docBaseUrl,
+ ].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const links = defaultLinks.map(menu => {
return {
- searchQuery: {
- type: String,
- notify: true,
- },
- loggedIn: {
- type: Boolean,
- reflectToAttribute: true,
- },
- loading: {
- type: Boolean,
- reflectToAttribute: true,
- },
-
- /** @type {?Object} */
- _account: Object,
- _adminLinks: {
- type: Array,
- value() { return []; },
- },
- _defaultLinks: {
- type: Array,
- value() {
- return DEFAULT_LINKS;
- },
- },
- _docBaseUrl: {
- type: String,
- value: null,
- },
- _links: {
- type: Array,
- computed: '_computeLinks(_defaultLinks, _userLinks, _adminLinks, ' +
- '_topMenus, _docBaseUrl)',
- },
- loginUrl: {
- type: String,
- value: '/login',
- },
- _userLinks: {
- type: Array,
- value() { return []; },
- },
- _topMenus: {
- type: Array,
- value() { return []; },
- },
- _registerText: {
- type: String,
- value: 'Sign up',
- },
- _registerURL: {
- type: String,
- value: null,
- },
+ title: menu.title,
+ links: menu.links.slice(),
};
- }
-
- static get observers() {
- return [
- '_accountLoaded(_account)',
- ];
- }
-
- /** @override */
- ready() {
- super.ready();
- this._ensureAttribute('role', 'banner');
- }
-
- /** @override */
- attached() {
- super.attached();
- this._loadAccount();
- this._loadConfig();
- }
-
- /** @override */
- detached() {
- super.detached();
- }
-
- reload() {
- this._loadAccount();
- }
-
- _computeRelativeURL(path) {
- return '//' + window.location.host + this.getBaseUrl() + path;
- }
-
- _computeLinks(defaultLinks, userLinks, adminLinks, topMenus, docBaseUrl) {
- // Polymer 2: check for undefined
- if ([
- defaultLinks,
- userLinks,
- adminLinks,
- topMenus,
- docBaseUrl,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const links = defaultLinks.map(menu => {
- return {
- title: menu.title,
- links: menu.links.slice(),
- };
- });
- if (userLinks && userLinks.length > 0) {
- links.push({
- title: 'Your',
- links: userLinks.slice(),
- });
- }
- const docLinks = this._getDocLinks(docBaseUrl, DOCUMENTATION_LINKS);
- if (docLinks.length) {
- links.push({
- title: 'Documentation',
- links: docLinks,
- class: 'hideOnMobile',
- });
- }
+ });
+ if (userLinks && userLinks.length > 0) {
links.push({
- title: 'Browse',
- links: adminLinks.slice(),
+ title: 'Your',
+ links: userLinks.slice(),
});
- const topMenuLinks = [];
- links.forEach(link => { topMenuLinks[link.title] = link.links; });
- for (const m of topMenus) {
- const items = m.items.map(this._fixCustomMenuItem).filter(link =>
- // Ignore GWT project links
- !link.url.includes('${projectName}')
- );
- if (m.name in topMenuLinks) {
- items.forEach(link => { topMenuLinks[m.name].push(link); });
- } else {
- links.push({
- title: m.name,
- links: topMenuLinks[m.name] = items,
+ }
+ const docLinks = this._getDocLinks(docBaseUrl, DOCUMENTATION_LINKS);
+ if (docLinks.length) {
+ links.push({
+ title: 'Documentation',
+ links: docLinks,
+ class: 'hideOnMobile',
+ });
+ }
+ links.push({
+ title: 'Browse',
+ links: adminLinks.slice(),
+ });
+ const topMenuLinks = [];
+ links.forEach(link => { topMenuLinks[link.title] = link.links; });
+ for (const m of topMenus) {
+ const items = m.items.map(this._fixCustomMenuItem).filter(link =>
+ // Ignore GWT project links
+ !link.url.includes('${projectName}')
+ );
+ if (m.name in topMenuLinks) {
+ items.forEach(link => { topMenuLinks[m.name].push(link); });
+ } else {
+ links.push({
+ title: m.name,
+ links: topMenuLinks[m.name] = items,
+ });
+ }
+ }
+ return links;
+ }
+
+ _getDocLinks(docBaseUrl, docLinks) {
+ if (!docBaseUrl || !docLinks) {
+ return [];
+ }
+ return docLinks.map(link => {
+ let url = docBaseUrl;
+ if (url && url[url.length - 1] === '/') {
+ url = url.substring(0, url.length - 1);
+ }
+ return {
+ url: url + link.url,
+ name: link.name,
+ target: '_blank',
+ };
+ });
+ }
+
+ _loadAccount() {
+ this.loading = true;
+ const promises = [
+ this.$.restAPI.getAccount(),
+ this.$.restAPI.getTopMenus(),
+ Gerrit.awaitPluginsLoaded(),
+ ];
+
+ return Promise.all(promises).then(result => {
+ const account = result[0];
+ this._account = account;
+ this.loggedIn = !!account;
+ this.loading = false;
+ this._topMenus = result[1];
+
+ return this.getAdminLinks(account,
+ this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI),
+ this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI))
+ .then(res => {
+ this._adminLinks = res.links;
});
- }
+ });
+ }
+
+ _loadConfig() {
+ this.$.restAPI.getConfig()
+ .then(config => {
+ this._retrieveRegisterURL(config);
+ return this.getDocsBaseUrl(config, this.$.restAPI);
+ })
+ .then(docBaseUrl => { this._docBaseUrl = docBaseUrl; });
+ }
+
+ _accountLoaded(account) {
+ if (!account) { return; }
+
+ this.$.restAPI.getPreferences().then(prefs => {
+ this._userLinks = prefs && prefs.my ?
+ prefs.my.map(this._fixCustomMenuItem) : [];
+ });
+ }
+
+ _retrieveRegisterURL(config) {
+ if (AUTH_TYPES_WITH_REGISTER_URL.has(config.auth.auth_type)) {
+ this._registerURL = config.auth.register_url;
+ if (config.auth.register_text) {
+ this._registerText = config.auth.register_text;
}
- return links;
- }
-
- _getDocLinks(docBaseUrl, docLinks) {
- if (!docBaseUrl || !docLinks) {
- return [];
- }
- return docLinks.map(link => {
- let url = docBaseUrl;
- if (url && url[url.length - 1] === '/') {
- url = url.substring(0, url.length - 1);
- }
- return {
- url: url + link.url,
- name: link.name,
- target: '_blank',
- };
- });
- }
-
- _loadAccount() {
- this.loading = true;
- const promises = [
- this.$.restAPI.getAccount(),
- this.$.restAPI.getTopMenus(),
- Gerrit.awaitPluginsLoaded(),
- ];
-
- return Promise.all(promises).then(result => {
- const account = result[0];
- this._account = account;
- this.loggedIn = !!account;
- this.loading = false;
- this._topMenus = result[1];
-
- return this.getAdminLinks(account,
- this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI),
- this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI))
- .then(res => {
- this._adminLinks = res.links;
- });
- });
- }
-
- _loadConfig() {
- this.$.restAPI.getConfig()
- .then(config => {
- this._retrieveRegisterURL(config);
- return this.getDocsBaseUrl(config, this.$.restAPI);
- })
- .then(docBaseUrl => { this._docBaseUrl = docBaseUrl; });
- }
-
- _accountLoaded(account) {
- if (!account) { return; }
-
- this.$.restAPI.getPreferences().then(prefs => {
- this._userLinks = prefs && prefs.my ?
- prefs.my.map(this._fixCustomMenuItem) : [];
- });
- }
-
- _retrieveRegisterURL(config) {
- if (AUTH_TYPES_WITH_REGISTER_URL.has(config.auth.auth_type)) {
- this._registerURL = config.auth.register_url;
- if (config.auth.register_text) {
- this._registerText = config.auth.register_text;
- }
- }
- }
-
- _computeIsInvisible(registerURL) {
- return registerURL ? '' : 'invisible';
- }
-
- _fixCustomMenuItem(linkObj) {
- // Normalize all urls to PolyGerrit style.
- if (linkObj.url.startsWith('#')) {
- linkObj.url = linkObj.url.slice(1);
- }
-
- // Delete target property due to complications of
- // https://bugs.chromium.org/p/gerrit/issues/detail?id=5888
- //
- // The server tries to guess whether URL is a view within the UI.
- // If not, it sets target='_blank' on the menu item. The server
- // makes assumptions that work for the GWT UI, but not PolyGerrit,
- // so we'll just disable it altogether for now.
- delete linkObj.target;
-
- return linkObj;
- }
-
- _generateSettingsLink() {
- return this.getBaseUrl() + '/settings/';
- }
-
- _onMobileSearchTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('mobile-search', null, {bubbles: false});
- }
-
- _computeLinkGroupClass(linkGroup) {
- if (linkGroup && linkGroup.class) {
- return linkGroup.class;
- }
-
- return '';
}
}
- customElements.define(GrMainHeader.is, GrMainHeader);
-})();
+ _computeIsInvisible(registerURL) {
+ return registerURL ? '' : 'invisible';
+ }
+
+ _fixCustomMenuItem(linkObj) {
+ // Normalize all urls to PolyGerrit style.
+ if (linkObj.url.startsWith('#')) {
+ linkObj.url = linkObj.url.slice(1);
+ }
+
+ // Delete target property due to complications of
+ // https://bugs.chromium.org/p/gerrit/issues/detail?id=5888
+ //
+ // The server tries to guess whether URL is a view within the UI.
+ // If not, it sets target='_blank' on the menu item. The server
+ // makes assumptions that work for the GWT UI, but not PolyGerrit,
+ // so we'll just disable it altogether for now.
+ delete linkObj.target;
+
+ return linkObj;
+ }
+
+ _generateSettingsLink() {
+ return this.getBaseUrl() + '/settings/';
+ }
+
+ _onMobileSearchTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('mobile-search', null, {bubbles: false});
+ }
+
+ _computeLinkGroupClass(linkGroup) {
+ if (linkGroup && linkGroup.class) {
+ return linkGroup.class;
+ }
+
+ return '';
+ }
+}
+
+customElements.define(GrMainHeader.is, GrMainHeader);
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js
similarity index 65%
rename from polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
rename to polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js
index 51717a8..307a081 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js
@@ -1,35 +1,22 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.js';
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/docs-url-behavior/docs-url-behavior.html">
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-account-dropdown/gr-account-dropdown.html">
-<link rel="import" href="../gr-smart-search/gr-smart-search.html">
-
-<dom-module id="gr-main-header">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -183,19 +170,15 @@
}
</style>
<nav>
- <a href$="[[_computeRelativeURL('/')]]" class="bigTitle">
+ <a href\$="[[_computeRelativeURL('/')]]" class="bigTitle">
<gr-endpoint-decorator name="header-title">
<span class="titleText"></span>
</gr-endpoint-decorator>
</a>
<ul class="links">
<template is="dom-repeat" items="[[_links]]" as="linkGroup">
- <li class$="[[_computeLinkGroupClass(linkGroup)]]">
- <gr-dropdown
- link
- down-arrow
- items = [[linkGroup.links]]
- horizontal-align="left">
+ <li class\$="[[_computeLinkGroupClass(linkGroup)]]">
+ <gr-dropdown link="" down-arrow="" items="[[linkGroup.links]]" horizontal-align="left">
<span class="linksTitle" id="[[linkGroup.title]]">
[[linkGroup.title]]
</span>
@@ -204,29 +187,18 @@
</template>
</ul>
<div class="rightItems">
- <gr-endpoint-decorator
- class="hideOnMobile"
- name="header-small-banner"></gr-endpoint-decorator>
- <gr-smart-search
- id="search"
- search-query="{{searchQuery}}"></gr-smart-search>
- <gr-endpoint-decorator
- class="hideOnMobile"
- name="header-browse-source"></gr-endpoint-decorator>
+ <gr-endpoint-decorator class="hideOnMobile" name="header-small-banner"></gr-endpoint-decorator>
+ <gr-smart-search id="search" search-query="{{searchQuery}}"></gr-smart-search>
+ <gr-endpoint-decorator class="hideOnMobile" name="header-browse-source"></gr-endpoint-decorator>
<div class="accountContainer" id="accountContainer">
- <iron-icon id="mobileSearch" icon="gr-icons:search" on-tap='_onMobileSearchTap'></iron-icon>
- <div class$="[[_computeIsInvisible(_registerURL)]]">
- <a
- class="registerButton"
- href$="[[_registerURL]]">
+ <iron-icon id="mobileSearch" icon="gr-icons:search" on-tap="_onMobileSearchTap"></iron-icon>
+ <div class\$="[[_computeIsInvisible(_registerURL)]]">
+ <a class="registerButton" href\$="[[_registerURL]]">
[[_registerText]]
</a>
</div>
- <a class="loginButton" href$="[[loginUrl]]">Sign in</a>
- <a
- class="settingsButton"
- href$="[[_generateSettingsLink()]]"
- title="Settings">
+ <a class="loginButton" href\$="[[loginUrl]]">Sign in</a>
+ <a class="settingsButton" href\$="[[_generateSettingsLink()]]" title="Settings">
<iron-icon icon="gr-icons:settings"></iron-icon>
</a>
<gr-account-dropdown account="[[_account]]"></gr-account-dropdown>
@@ -235,6 +207,4 @@
</nav>
<gr-js-api-interface id="jsAPI"></gr-js-api-interface>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-main-header.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
index 8fe3fca..c641575 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-main-header</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-main-header.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,379 +30,380 @@
</template>
</test-fixture>
-<script>
- suite('gr-main-header tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-main-header.js';
+suite('gr-main-header tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- probePath(path) { return Promise.resolve(false); },
- });
- stub('gr-main-header', {
- _loadAccount() {},
- });
- element = fixture('basic');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ probePath(path) { return Promise.resolve(false); },
});
-
- teardown(() => {
- sandbox.restore();
+ stub('gr-main-header', {
+ _loadAccount() {},
});
-
- test('link visibility', () => {
- element.loading = true;
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('.accountContainer')).display,
- 'none');
- element.loading = false;
- element.loggedIn = false;
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('.accountContainer')).display,
- 'none');
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('.loginButton')).display,
- 'none');
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('.registerButton')).display,
- 'none');
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('gr-account-dropdown')).display,
- 'none');
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('.settingsButton')).display,
- 'none');
- element.loggedIn = true;
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('.loginButton')).display,
- 'none');
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('.registerButton')).display,
- 'none');
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('gr-account-dropdown'))
- .display,
- 'none');
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('.settingsButton')).display,
- 'none');
- });
-
- test('fix my menu item', () => {
- assert.deepEqual([
- {url: 'https://awesometown.com/#hashyhash'},
- {url: 'url', target: '_blank'},
- ].map(element._fixCustomMenuItem), [
- {url: 'https://awesometown.com/#hashyhash'},
- {url: 'url'},
- ]);
- });
-
- test('user links', () => {
- const defaultLinks = [{
- title: 'Faves',
- links: [{
- name: 'Pinterest',
- url: 'https://pinterest.com',
- }],
- }];
- const userLinks = [{
- name: 'Facebook',
- url: 'https://facebook.com',
- }];
- const adminLinks = [{
- name: 'Repos',
- url: '/repos',
- }];
-
- // When no admin links are passed, it should use the default.
- assert.deepEqual(element._computeLinks(
- defaultLinks,
- /* userLinks= */[],
- adminLinks,
- /* topMenus= */[],
- /* docBaseUrl= */ ''
- ),
- defaultLinks.concat({
- title: 'Browse',
- links: adminLinks,
- }));
- assert.deepEqual(element._computeLinks(
- defaultLinks,
- userLinks,
- adminLinks,
- /* topMenus= */[],
- /* docBaseUrl= */ ''
- ),
- defaultLinks.concat([
- {
- title: 'Your',
- links: userLinks,
- },
- {
- title: 'Browse',
- links: adminLinks,
- }])
- );
- });
-
- test('documentation links', () => {
- const docLinks = [
- {
- name: 'Table of Contents',
- url: '/index.html',
- },
- ];
-
- assert.deepEqual(element._getDocLinks(null, docLinks), []);
- assert.deepEqual(element._getDocLinks('', docLinks), []);
- assert.deepEqual(element._getDocLinks('base', null), []);
- assert.deepEqual(element._getDocLinks('base', []), []);
-
- assert.deepEqual(element._getDocLinks('base', docLinks), [{
- name: 'Table of Contents',
- target: '_blank',
- url: 'base/index.html',
- }]);
-
- assert.deepEqual(element._getDocLinks('base/', docLinks), [{
- name: 'Table of Contents',
- target: '_blank',
- url: 'base/index.html',
- }]);
- });
-
- test('top menus', () => {
- const adminLinks = [{
- name: 'Repos',
- url: '/repos',
- }];
- const topMenus = [{
- name: 'Plugins',
- items: [{
- name: 'Manage',
- target: '_blank',
- url: 'https://gerrit/plugins/plugin-manager/static/index.html',
- }],
- }];
- assert.deepEqual(element._computeLinks(
- /* defaultLinks= */ [],
- /* userLinks= */ [],
- adminLinks,
- topMenus,
- /* baseDocUrl= */ ''
- ), [{
- title: 'Browse',
- links: adminLinks,
- },
- {
- title: 'Plugins',
- links: [{
- name: 'Manage',
- url: 'https://gerrit/plugins/plugin-manager/static/index.html',
- }],
- }]);
- });
-
- test('ignore top project menus', () => {
- const adminLinks = [{
- name: 'Repos',
- url: '/repos',
- }];
- const topMenus = [{
- name: 'Projects',
- items: [{
- name: 'Project Settings',
- target: '_blank',
- url: '/plugins/myplugin/${projectName}',
- }, {
- name: 'Project List',
- target: '_blank',
- url: '/plugins/myplugin/index.html',
- }],
- }];
- assert.deepEqual(element._computeLinks(
- /* defaultLinks= */ [],
- /* userLinks= */ [],
- adminLinks,
- topMenus,
- /* baseDocUrl= */ ''
- ), [{
- title: 'Browse',
- links: adminLinks,
- },
- {
- title: 'Projects',
- links: [{
- name: 'Project List',
- url: '/plugins/myplugin/index.html',
- }],
- }]);
- });
-
- test('merge top menus', () => {
- const adminLinks = [{
- name: 'Repos',
- url: '/repos',
- }];
- const topMenus = [{
- name: 'Plugins',
- items: [{
- name: 'Manage',
- target: '_blank',
- url: 'https://gerrit/plugins/plugin-manager/static/index.html',
- }],
- }, {
- name: 'Plugins',
- items: [{
- name: 'Create',
- target: '_blank',
- url: 'https://gerrit/plugins/plugin-manager/static/create.html',
- }],
- }];
- assert.deepEqual(element._computeLinks(
- /* defaultLinks= */ [],
- /* userLinks= */ [],
- adminLinks,
- topMenus,
- /* baseDocUrl= */ ''
- ), [{
- title: 'Browse',
- links: adminLinks,
- }, {
- title: 'Plugins',
- links: [{
- name: 'Manage',
- url: 'https://gerrit/plugins/plugin-manager/static/index.html',
- }, {
- name: 'Create',
- url: 'https://gerrit/plugins/plugin-manager/static/create.html',
- }],
- }]);
- });
-
- test('merge top menus in default links', () => {
- const defaultLinks = [{
- title: 'Faves',
- links: [{
- name: 'Pinterest',
- url: 'https://pinterest.com',
- }],
- }];
- const topMenus = [{
- name: 'Faves',
- items: [{
- name: 'Manage',
- target: '_blank',
- url: 'https://gerrit/plugins/plugin-manager/static/index.html',
- }],
- }];
- assert.deepEqual(element._computeLinks(
- defaultLinks,
- /* userLinks= */ [],
- /* adminLinks= */ [],
- topMenus,
- /* baseDocUrl= */ ''
- ), [{
- title: 'Faves',
- links: defaultLinks[0].links.concat([{
- name: 'Manage',
- url: 'https://gerrit/plugins/plugin-manager/static/index.html',
- }]),
- }, {
- title: 'Browse',
- links: [],
- }]);
- });
-
- test('merge top menus in user links', () => {
- const userLinks = [{
- name: 'Facebook',
- url: 'https://facebook.com',
- }];
- const topMenus = [{
- name: 'Your',
- items: [{
- name: 'Manage',
- target: '_blank',
- url: 'https://gerrit/plugins/plugin-manager/static/index.html',
- }],
- }];
- assert.deepEqual(element._computeLinks(
- /* defaultLinks= */ [],
- userLinks,
- /* adminLinks= */ [],
- topMenus,
- /* baseDocUrl= */ ''
- ), [{
- title: 'Your',
- links: userLinks.concat([{
- name: 'Manage',
- url: 'https://gerrit/plugins/plugin-manager/static/index.html',
- }]),
- }, {
- title: 'Browse',
- links: [],
- }]);
- });
-
- test('merge top menus in admin links', () => {
- const adminLinks = [{
- name: 'Repos',
- url: '/repos',
- }];
- const topMenus = [{
- name: 'Browse',
- items: [{
- name: 'Manage',
- target: '_blank',
- url: 'https://gerrit/plugins/plugin-manager/static/index.html',
- }],
- }];
- assert.deepEqual(element._computeLinks(
- /* defaultLinks= */ [],
- /* userLinks= */ [],
- adminLinks,
- topMenus,
- /* baseDocUrl= */ ''
- ), [{
- title: 'Browse',
- links: adminLinks.concat([{
- name: 'Manage',
- url: 'https://gerrit/plugins/plugin-manager/static/index.html',
- }]),
- }]);
- });
-
- test('register URL', () => {
- const config = {
- auth: {
- auth_type: 'LDAP',
- register_url: 'https//gerrit.example.com/register',
- },
- };
- element._retrieveRegisterURL(config);
- assert.equal(element._registerURL, config.auth.register_url);
- assert.equal(element._registerText, 'Sign up');
-
- config.auth.register_text = 'Create account';
- element._retrieveRegisterURL(config);
- assert.equal(element._registerURL, config.auth.register_url);
- assert.equal(element._registerText, config.auth.register_text);
- });
-
- test('register URL ignored for wrong auth type', () => {
- const config = {
- auth: {
- auth_type: 'OPENID',
- register_url: 'https//gerrit.example.com/register',
- },
- };
- element._retrieveRegisterURL(config);
- assert.equal(element._registerURL, null);
- assert.equal(element._registerText, 'Sign up');
- });
+ element = fixture('basic');
});
- </script>
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('link visibility', () => {
+ element.loading = true;
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('.accountContainer')).display,
+ 'none');
+ element.loading = false;
+ element.loggedIn = false;
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('.accountContainer')).display,
+ 'none');
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('.loginButton')).display,
+ 'none');
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('.registerButton')).display,
+ 'none');
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('gr-account-dropdown')).display,
+ 'none');
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('.settingsButton')).display,
+ 'none');
+ element.loggedIn = true;
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('.loginButton')).display,
+ 'none');
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('.registerButton')).display,
+ 'none');
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('gr-account-dropdown'))
+ .display,
+ 'none');
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('.settingsButton')).display,
+ 'none');
+ });
+
+ test('fix my menu item', () => {
+ assert.deepEqual([
+ {url: 'https://awesometown.com/#hashyhash'},
+ {url: 'url', target: '_blank'},
+ ].map(element._fixCustomMenuItem), [
+ {url: 'https://awesometown.com/#hashyhash'},
+ {url: 'url'},
+ ]);
+ });
+
+ test('user links', () => {
+ const defaultLinks = [{
+ title: 'Faves',
+ links: [{
+ name: 'Pinterest',
+ url: 'https://pinterest.com',
+ }],
+ }];
+ const userLinks = [{
+ name: 'Facebook',
+ url: 'https://facebook.com',
+ }];
+ const adminLinks = [{
+ name: 'Repos',
+ url: '/repos',
+ }];
+
+ // When no admin links are passed, it should use the default.
+ assert.deepEqual(element._computeLinks(
+ defaultLinks,
+ /* userLinks= */[],
+ adminLinks,
+ /* topMenus= */[],
+ /* docBaseUrl= */ ''
+ ),
+ defaultLinks.concat({
+ title: 'Browse',
+ links: adminLinks,
+ }));
+ assert.deepEqual(element._computeLinks(
+ defaultLinks,
+ userLinks,
+ adminLinks,
+ /* topMenus= */[],
+ /* docBaseUrl= */ ''
+ ),
+ defaultLinks.concat([
+ {
+ title: 'Your',
+ links: userLinks,
+ },
+ {
+ title: 'Browse',
+ links: adminLinks,
+ }])
+ );
+ });
+
+ test('documentation links', () => {
+ const docLinks = [
+ {
+ name: 'Table of Contents',
+ url: '/index.html',
+ },
+ ];
+
+ assert.deepEqual(element._getDocLinks(null, docLinks), []);
+ assert.deepEqual(element._getDocLinks('', docLinks), []);
+ assert.deepEqual(element._getDocLinks('base', null), []);
+ assert.deepEqual(element._getDocLinks('base', []), []);
+
+ assert.deepEqual(element._getDocLinks('base', docLinks), [{
+ name: 'Table of Contents',
+ target: '_blank',
+ url: 'base/index.html',
+ }]);
+
+ assert.deepEqual(element._getDocLinks('base/', docLinks), [{
+ name: 'Table of Contents',
+ target: '_blank',
+ url: 'base/index.html',
+ }]);
+ });
+
+ test('top menus', () => {
+ const adminLinks = [{
+ name: 'Repos',
+ url: '/repos',
+ }];
+ const topMenus = [{
+ name: 'Plugins',
+ items: [{
+ name: 'Manage',
+ target: '_blank',
+ url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+ }],
+ }];
+ assert.deepEqual(element._computeLinks(
+ /* defaultLinks= */ [],
+ /* userLinks= */ [],
+ adminLinks,
+ topMenus,
+ /* baseDocUrl= */ ''
+ ), [{
+ title: 'Browse',
+ links: adminLinks,
+ },
+ {
+ title: 'Plugins',
+ links: [{
+ name: 'Manage',
+ url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+ }],
+ }]);
+ });
+
+ test('ignore top project menus', () => {
+ const adminLinks = [{
+ name: 'Repos',
+ url: '/repos',
+ }];
+ const topMenus = [{
+ name: 'Projects',
+ items: [{
+ name: 'Project Settings',
+ target: '_blank',
+ url: '/plugins/myplugin/${projectName}',
+ }, {
+ name: 'Project List',
+ target: '_blank',
+ url: '/plugins/myplugin/index.html',
+ }],
+ }];
+ assert.deepEqual(element._computeLinks(
+ /* defaultLinks= */ [],
+ /* userLinks= */ [],
+ adminLinks,
+ topMenus,
+ /* baseDocUrl= */ ''
+ ), [{
+ title: 'Browse',
+ links: adminLinks,
+ },
+ {
+ title: 'Projects',
+ links: [{
+ name: 'Project List',
+ url: '/plugins/myplugin/index.html',
+ }],
+ }]);
+ });
+
+ test('merge top menus', () => {
+ const adminLinks = [{
+ name: 'Repos',
+ url: '/repos',
+ }];
+ const topMenus = [{
+ name: 'Plugins',
+ items: [{
+ name: 'Manage',
+ target: '_blank',
+ url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+ }],
+ }, {
+ name: 'Plugins',
+ items: [{
+ name: 'Create',
+ target: '_blank',
+ url: 'https://gerrit/plugins/plugin-manager/static/create.html',
+ }],
+ }];
+ assert.deepEqual(element._computeLinks(
+ /* defaultLinks= */ [],
+ /* userLinks= */ [],
+ adminLinks,
+ topMenus,
+ /* baseDocUrl= */ ''
+ ), [{
+ title: 'Browse',
+ links: adminLinks,
+ }, {
+ title: 'Plugins',
+ links: [{
+ name: 'Manage',
+ url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+ }, {
+ name: 'Create',
+ url: 'https://gerrit/plugins/plugin-manager/static/create.html',
+ }],
+ }]);
+ });
+
+ test('merge top menus in default links', () => {
+ const defaultLinks = [{
+ title: 'Faves',
+ links: [{
+ name: 'Pinterest',
+ url: 'https://pinterest.com',
+ }],
+ }];
+ const topMenus = [{
+ name: 'Faves',
+ items: [{
+ name: 'Manage',
+ target: '_blank',
+ url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+ }],
+ }];
+ assert.deepEqual(element._computeLinks(
+ defaultLinks,
+ /* userLinks= */ [],
+ /* adminLinks= */ [],
+ topMenus,
+ /* baseDocUrl= */ ''
+ ), [{
+ title: 'Faves',
+ links: defaultLinks[0].links.concat([{
+ name: 'Manage',
+ url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+ }]),
+ }, {
+ title: 'Browse',
+ links: [],
+ }]);
+ });
+
+ test('merge top menus in user links', () => {
+ const userLinks = [{
+ name: 'Facebook',
+ url: 'https://facebook.com',
+ }];
+ const topMenus = [{
+ name: 'Your',
+ items: [{
+ name: 'Manage',
+ target: '_blank',
+ url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+ }],
+ }];
+ assert.deepEqual(element._computeLinks(
+ /* defaultLinks= */ [],
+ userLinks,
+ /* adminLinks= */ [],
+ topMenus,
+ /* baseDocUrl= */ ''
+ ), [{
+ title: 'Your',
+ links: userLinks.concat([{
+ name: 'Manage',
+ url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+ }]),
+ }, {
+ title: 'Browse',
+ links: [],
+ }]);
+ });
+
+ test('merge top menus in admin links', () => {
+ const adminLinks = [{
+ name: 'Repos',
+ url: '/repos',
+ }];
+ const topMenus = [{
+ name: 'Browse',
+ items: [{
+ name: 'Manage',
+ target: '_blank',
+ url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+ }],
+ }];
+ assert.deepEqual(element._computeLinks(
+ /* defaultLinks= */ [],
+ /* userLinks= */ [],
+ adminLinks,
+ topMenus,
+ /* baseDocUrl= */ ''
+ ), [{
+ title: 'Browse',
+ links: adminLinks.concat([{
+ name: 'Manage',
+ url: 'https://gerrit/plugins/plugin-manager/static/index.html',
+ }]),
+ }]);
+ });
+
+ test('register URL', () => {
+ const config = {
+ auth: {
+ auth_type: 'LDAP',
+ register_url: 'https//gerrit.example.com/register',
+ },
+ };
+ element._retrieveRegisterURL(config);
+ assert.equal(element._registerURL, config.auth.register_url);
+ assert.equal(element._registerText, 'Sign up');
+
+ config.auth.register_text = 'Create account';
+ element._retrieveRegisterURL(config);
+ assert.equal(element._registerURL, config.auth.register_url);
+ assert.equal(element._registerText, config.auth.register_text);
+ });
+
+ test('register URL ignored for wrong auth type', () => {
+ const config = {
+ auth: {
+ auth_type: 'OPENID',
+ register_url: 'https//gerrit.example.com/register',
+ },
+ };
+ element._retrieveRegisterURL(config);
+ assert.equal(element._registerURL, null);
+ assert.equal(element._registerText, 'Sign up');
+ });
+});
+</script>
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
deleted file mode 100644
index e79277a..0000000
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ /dev/null
@@ -1,750 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<script>
- (function(window) {
- 'use strict';
-
- // Navigation parameters object format:
- //
- // Each object has a `view` property with a value from Gerrit.Nav.View. The
- // remaining properties depend on the value used for view.
- //
- // - Gerrit.Nav.View.CHANGE:
- // - `changeNum`, required, String: the numeric ID of the change.
- // - `project`, optional, String: the project name.
- // - `patchNum`, optional, Number: the patch for the right-hand-side of
- // the diff.
- // - `basePatchNum`, optional, Number: the patch for the left-hand-side
- // of the diff. If `basePatchNum` is provided, then `patchNum` must
- // also be provided.
- // - `edit`, optional, Boolean: whether or not to load the file list with
- // edit controls.
- // - `messageHash`, optional, String: the hash of the change message to
- // scroll to.
- //
- // - Gerrit.Nav.View.SEARCH:
- // - `query`, optional, String: the literal search query. If provided,
- // the string will be used as the query, and all other params will be
- // ignored.
- // - `owner`, optional, String: the owner name.
- // - `project`, optional, String: the project name.
- // - `branch`, optional, String: the branch name.
- // - `topic`, optional, String: the topic name.
- // - `hashtag`, optional, String: the hashtag name.
- // - `statuses`, optional, Array<String>: the list of change statuses to
- // search for. If more than one is provided, the search will OR them
- // together.
- // - `offset`, optional, Number: the offset for the query.
- //
- // - Gerrit.Nav.View.DIFF:
- // - `changeNum`, required, String: the numeric ID of the change.
- // - `path`, required, String: the filepath of the diff.
- // - `patchNum`, required, Number: the patch for the right-hand-side of
- // the diff.
- // - `basePatchNum`, optional, Number: the patch for the left-hand-side
- // of the diff. If `basePatchNum` is provided, then `patchNum` must
- // also be provided.
- // - `lineNum`, optional, Number: the line number to be selected on load.
- // - `leftSide`, optional, Boolean: if a `lineNum` is provided, a value
- // of true selects the line from base of the patch range. False by
- // default.
- //
- // - Gerrit.Nav.View.GROUP:
- // - `groupId`, required, String: the ID of the group.
- // - `detail`, optional, String: the name of the group detail view.
- // Takes any value from Gerrit.Nav.GroupDetailView.
- //
- // - Gerrit.Nav.View.REPO:
- // - `repoName`, required, String: the name of the repo
- // - `detail`, optional, String: the name of the repo detail view.
- // Takes any value from Gerrit.Nav.RepoDetailView.
- //
- // - Gerrit.Nav.View.DASHBOARD
- // - `repo`, optional, String.
- // - `sections`, optional, Array of objects with `title` and `query`
- // strings.
- // - `user`, optional, String.
- //
- // - Gerrit.Nav.View.ROOT:
- // - no possible parameters.
-
- window.Gerrit = window.Gerrit || {};
-
- // Prevent redefinition.
- if (window.Gerrit.hasOwnProperty('Nav')) { return; }
-
- const uninitialized = () => {
- console.warn('Use of uninitialized routing');
- };
-
- const EDIT_PATCHNUM = 'edit';
- const PARENT_PATCHNUM = 'PARENT';
-
- const USER_PLACEHOLDER_PATTERN = /\$\{user\}/g;
-
- // NOTE: These queries are tested in Java. Any changes made to definitions
- // here require corresponding changes to:
- // javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
- const DEFAULT_SECTIONS = [
- {
- // Changes with unpublished draft comments. This section is omitted when
- // viewing other users, so we don't need to filter anything out.
- name: 'Has draft comments',
- query: 'has:draft',
- selfOnly: true,
- hideIfEmpty: true,
- suffixForDashboard: 'limit:10',
- },
- {
- // Changes that are assigned to the viewed user.
- name: 'Assigned reviews',
- query: 'assignee:${user} (-is:wip OR owner:self OR assignee:self) ' +
- 'is:open -is:ignored',
- hideIfEmpty: true,
- suffixForDashboard: 'limit:25',
- },
- {
- // WIP open changes owned by viewing user. This section is omitted when
- // viewing other users, so we don't need to filter anything out.
- name: 'Work in progress',
- query: 'is:open owner:${user} is:wip',
- selfOnly: true,
- hideIfEmpty: true,
- suffixForDashboard: 'limit:25',
- },
- {
- // Non-WIP open changes owned by viewed user. Filter out changes ignored
- // by the viewing user.
- name: 'Outgoing reviews',
- query: 'is:open owner:${user} -is:wip -is:ignored',
- isOutgoing: true,
- suffixForDashboard: 'limit:25',
- },
- {
- // Non-WIP open changes not owned by the viewed user, that the viewed user
- // is associated with (as either a reviewer or the assignee). Changes
- // ignored by the viewing user are filtered out.
- name: 'Incoming reviews',
- query: 'is:open -owner:${user} -is:wip -is:ignored ' +
- '(reviewer:${user} OR assignee:${user})',
- suffixForDashboard: 'limit:25',
- },
- {
- // Open changes the viewed user is CCed on. Changes ignored by the viewing
- // user are filtered out.
- name: 'CCed on',
- query: 'is:open -is:ignored cc:${user}',
- suffixForDashboard: 'limit:10',
- },
- {
- name: 'Recently closed',
- // Closed changes where viewed user is owner, reviewer, or assignee.
- // Changes ignored by the viewing user are filtered out, and so are WIP
- // changes not owned by the viewing user (the one instance of
- // 'owner:self' is intentional and implements this logic).
- query: 'is:closed -is:ignored (-is:wip OR owner:self) ' +
- '(owner:${user} OR reviewer:${user} OR assignee:${user} ' +
- 'OR cc:${user})',
- suffixForDashboard: '-age:4w limit:10',
- },
- ];
-
- window.Gerrit.Nav = {
-
- View: {
- ADMIN: 'admin',
- AGREEMENTS: 'agreements',
- CHANGE: 'change',
- DASHBOARD: 'dashboard',
- DIFF: 'diff',
- DOCUMENTATION_SEARCH: 'documentation-search',
- EDIT: 'edit',
- GROUP: 'group',
- PLUGIN_SCREEN: 'plugin-screen',
- REPO: 'repo',
- ROOT: 'root',
- SEARCH: 'search',
- SETTINGS: 'settings',
- },
-
- GroupDetailView: {
- MEMBERS: 'members',
- LOG: 'log',
- },
-
- RepoDetailView: {
- ACCESS: 'access',
- BRANCHES: 'branches',
- COMMANDS: 'commands',
- DASHBOARDS: 'dashboards',
- TAGS: 'tags',
- },
-
- WeblinkType: {
- CHANGE: 'change',
- FILE: 'file',
- PATCHSET: 'patchset',
- },
-
- /** @type {Function} */
- _navigate: uninitialized,
-
- /** @type {Function} */
- _generateUrl: uninitialized,
-
- /** @type {Function} */
- _generateWeblinks: uninitialized,
-
- /** @type {Function} */
- mapCommentlinks: uninitialized,
-
- /**
- * @param {number=} patchNum
- * @param {number|string=} basePatchNum
- */
- _checkPatchRange(patchNum, basePatchNum) {
- if (basePatchNum && !patchNum) {
- throw new Error('Cannot use base patch number without patch number.');
- }
- },
-
- /**
- * Setup router implementation.
- *
- * @param {function(!string)} navigate the router-abstracted equivalent of
- * `window.location.href = ...`. Takes a string.
- * @param {function(!Object): string} generateUrl generates a URL given
- * navigation parameters, detailed in the file header.
- * @param {function(!Object): string} generateWeblinks weblinks generator
- * function takes single payload parameter with type property that
- * determines which
- * part of the UI is the consumer of the weblinks. type property can
- * be one of file, change, or patchset.
- * - For file type, payload will also contain string properties: repo,
- * commit, file.
- * - For patchset type, payload will also contain string properties:
- * repo, commit.
- * - For change type, payload will also contain string properties:
- * repo, commit. If server provides weblinks, those will be passed
- * as options.weblinks property on the main payload object.
- * @param {function(!Object): Object} mapCommentlinks provides an escape
- * hatch to modify the commentlinks object, e.g. if it contains any
- * relative URLs.
- */
- setup(navigate, generateUrl, generateWeblinks, mapCommentlinks) {
- this._navigate = navigate;
- this._generateUrl = generateUrl;
- this._generateWeblinks = generateWeblinks;
- this.mapCommentlinks = mapCommentlinks;
- },
-
- destroy() {
- this._navigate = uninitialized;
- this._generateUrl = uninitialized;
- this._generateWeblinks = uninitialized;
- this.mapCommentlinks = uninitialized;
- },
-
- /**
- * Generate a URL for the given route parameters.
- *
- * @param {Object} params
- * @return {string}
- */
- _getUrlFor(params) {
- return this._generateUrl(params);
- },
-
- getUrlForSearchQuery(query, opt_offset) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.SEARCH,
- query,
- offset: opt_offset,
- });
- },
-
- /**
- * @param {!string} project The name of the project.
- * @param {boolean=} opt_openOnly When true, only search open changes in
- * the project.
- * @param {string=} opt_host The host in which to search.
- * @return {string}
- */
- getUrlForProjectChanges(project, opt_openOnly, opt_host) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.SEARCH,
- project,
- statuses: opt_openOnly ? ['open'] : [],
- host: opt_host,
- });
- },
-
- /**
- * @param {string} branch The name of the branch.
- * @param {string} project The name of the project.
- * @param {string=} opt_status The status to search.
- * @param {string=} opt_host The host in which to search.
- * @return {string}
- */
- getUrlForBranch(branch, project, opt_status, opt_host) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.SEARCH,
- branch,
- project,
- statuses: opt_status ? [opt_status] : undefined,
- host: opt_host,
- });
- },
-
- /**
- * @param {string} topic The name of the topic.
- * @param {string=} opt_host The host in which to search.
- * @return {string}
- */
- getUrlForTopic(topic, opt_host) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.SEARCH,
- topic,
- statuses: ['open', 'merged'],
- host: opt_host,
- });
- },
-
- /**
- * @param {string} hashtag The name of the hashtag.
- * @return {string}
- */
- getUrlForHashtag(hashtag) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.SEARCH,
- hashtag,
- statuses: ['open', 'merged'],
- });
- },
-
- /**
- * Navigate to a search for changes with the given status.
- *
- * @param {string} status
- */
- navigateToStatusSearch(status) {
- this._navigate(this._getUrlFor({
- view: Gerrit.Nav.View.SEARCH,
- statuses: [status],
- }));
- },
-
- /**
- * Navigate to a search query
- *
- * @param {string} query
- * @param {number=} opt_offset
- */
- navigateToSearchQuery(query, opt_offset) {
- return this._navigate(this.getUrlForSearchQuery(query, opt_offset));
- },
-
- /**
- * Navigate to the user's dashboard
- */
- navigateToUserDashboard() {
- return this._navigate(this.getUrlForUserDashboard('self'));
- },
-
- /**
- * @param {!Object} change The change object.
- * @param {number=} opt_patchNum
- * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
- * used for none.
- * @param {boolean=} opt_isEdit
- * @param {string=} opt_messageHash
- * @return {string}
- */
- getUrlForChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit,
- opt_messageHash) {
- if (opt_basePatchNum === PARENT_PATCHNUM) {
- opt_basePatchNum = undefined;
- }
-
- this._checkPatchRange(opt_patchNum, opt_basePatchNum);
- return this._getUrlFor({
- view: Gerrit.Nav.View.CHANGE,
- changeNum: change._number,
- project: change.project,
- patchNum: opt_patchNum,
- basePatchNum: opt_basePatchNum,
- edit: opt_isEdit,
- host: change.internalHost || undefined,
- messageHash: opt_messageHash,
- });
- },
-
- /**
- * @param {number} changeNum
- * @param {string} project The name of the project.
- * @param {number=} opt_patchNum
- * @return {string}
- */
- getUrlForChangeById(changeNum, project, opt_patchNum) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.CHANGE,
- changeNum,
- project,
- patchNum: opt_patchNum,
- });
- },
-
- /**
- * @param {!Object} change The change object.
- * @param {number=} opt_patchNum
- * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
- * used for none.
- * @param {boolean=} opt_isEdit
- */
- navigateToChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit) {
- this._navigate(this.getUrlForChange(change, opt_patchNum,
- opt_basePatchNum, opt_isEdit));
- },
-
- /**
- * @param {{ _number: number, project: string }} change The change object.
- * @param {string} path The file path.
- * @param {number=} opt_patchNum
- * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
- * used for none.
- * @param {number|string=} opt_lineNum
- * @return {string}
- */
- getUrlForDiff(change, path, opt_patchNum, opt_basePatchNum, opt_lineNum) {
- return this.getUrlForDiffById(change._number, change.project, path,
- opt_patchNum, opt_basePatchNum, opt_lineNum);
- },
-
- /**
- * @param {number} changeNum
- * @param {string} project The name of the project.
- * @param {string} path The file path.
- * @param {number=} opt_patchNum
- * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
- * used for none.
- * @param {number=} opt_lineNum
- * @param {boolean=} opt_leftSide
- * @return {string}
- */
- getUrlForDiffById(changeNum, project, path, opt_patchNum,
- opt_basePatchNum, opt_lineNum, opt_leftSide) {
- if (opt_basePatchNum === PARENT_PATCHNUM) {
- opt_basePatchNum = undefined;
- }
-
- this._checkPatchRange(opt_patchNum, opt_basePatchNum);
- return this._getUrlFor({
- view: Gerrit.Nav.View.DIFF,
- changeNum,
- project,
- path,
- patchNum: opt_patchNum,
- basePatchNum: opt_basePatchNum,
- lineNum: opt_lineNum,
- leftSide: opt_leftSide,
- });
- },
-
- /**
- * @param {{ _number: number, project: string }} change The change object.
- * @param {string} path The file path.
- * @param {number=} opt_patchNum
- * @return {string}
- */
- getEditUrlForDiff(change, path, opt_patchNum) {
- return this.getEditUrlForDiffById(change._number, change.project, path,
- opt_patchNum);
- },
-
- /**
- * @param {number} changeNum
- * @param {string} project The name of the project.
- * @param {string} path The file path.
- * @param {number|string=} opt_patchNum The patchNum the file content
- * should be based on, or ${EDIT_PATCHNUM} if left undefined.
- * @return {string}
- */
- getEditUrlForDiffById(changeNum, project, path, opt_patchNum) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.EDIT,
- changeNum,
- project,
- path,
- patchNum: opt_patchNum || EDIT_PATCHNUM,
- });
- },
-
- /**
- * @param {!Object} change The change object.
- * @param {string} path The file path.
- * @param {number=} opt_patchNum
- * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
- * used for none.
- */
- navigateToDiff(change, path, opt_patchNum, opt_basePatchNum) {
- this._navigate(this.getUrlForDiff(change, path, opt_patchNum,
- opt_basePatchNum));
- },
-
- /**
- * @param {string} owner The name of the owner.
- * @return {string}
- */
- getUrlForOwner(owner) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.SEARCH,
- owner,
- });
- },
-
- /**
- * @param {string} user The name of the user.
- * @return {string}
- */
- getUrlForUserDashboard(user) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.DASHBOARD,
- user,
- });
- },
-
- /**
- * @return {string}
- */
- getUrlForRoot() {
- return this._getUrlFor({
- view: Gerrit.Nav.View.ROOT,
- });
- },
-
- /**
- * @param {string} repo The name of the repo.
- * @param {string} dashboard The ID of the dashboard, in the form of
- * '<ref>:<path>'.
- * @return {string}
- */
- getUrlForRepoDashboard(repo, dashboard) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.DASHBOARD,
- repo,
- dashboard,
- });
- },
-
- /**
- * Navigate to an arbitrary relative URL.
- *
- * @param {string} relativeUrl
- */
- navigateToRelativeUrl(relativeUrl) {
- if (!relativeUrl.startsWith('/')) {
- throw new Error('navigateToRelativeUrl with non-relative URL');
- }
- this._navigate(relativeUrl);
- },
-
- /**
- * @param {string} repoName
- * @return {string}
- */
- getUrlForRepo(repoName) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.REPO,
- repoName,
- });
- },
-
- /**
- * Navigate to a repo settings page.
- *
- * @param {string} repoName
- */
- navigateToRepo(repoName) {
- this._navigate(this.getUrlForRepo(repoName));
- },
-
- /**
- * @param {string} repoName
- * @return {string}
- */
- getUrlForRepoTags(repoName) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.REPO,
- repoName,
- detail: Gerrit.Nav.RepoDetailView.TAGS,
- });
- },
-
- /**
- * @param {string} repoName
- * @return {string}
- */
- getUrlForRepoBranches(repoName) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.REPO,
- repoName,
- detail: Gerrit.Nav.RepoDetailView.BRANCHES,
- });
- },
-
- /**
- * @param {string} repoName
- * @return {string}
- */
- getUrlForRepoAccess(repoName) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.REPO,
- repoName,
- detail: Gerrit.Nav.RepoDetailView.ACCESS,
- });
- },
-
- /**
- * @param {string} repoName
- * @return {string}
- */
- getUrlForRepoCommands(repoName) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.REPO,
- repoName,
- detail: Gerrit.Nav.RepoDetailView.COMMANDS,
- });
- },
-
- /**
- * @param {string} repoName
- * @return {string}
- */
- getUrlForRepoDashboards(repoName) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.REPO,
- repoName,
- detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
- });
- },
-
- /**
- * @param {string} groupId
- * @return {string}
- */
- getUrlForGroup(groupId) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.GROUP,
- groupId,
- });
- },
-
- /**
- * @param {string} groupId
- * @return {string}
- */
- getUrlForGroupLog(groupId) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.GROUP,
- groupId,
- detail: Gerrit.Nav.GroupDetailView.LOG,
- });
- },
-
- /**
- * @param {string} groupId
- * @return {string}
- */
- getUrlForGroupMembers(groupId) {
- return this._getUrlFor({
- view: Gerrit.Nav.View.GROUP,
- groupId,
- detail: Gerrit.Nav.GroupDetailView.MEMBERS,
- });
- },
-
- getUrlForSettings() {
- return this._getUrlFor({view: Gerrit.Nav.View.SETTINGS});
- },
-
- /**
- * @param {string} repo
- * @param {string} commit
- * @param {string} file
- * @param {Object=} opt_options
- * @return {
- * Array<{label: string, url: string}>|
- * {label: string, url: string}
- * }
- */
- getFileWebLinks(repo, commit, file, opt_options) {
- const params = {type: Gerrit.Nav.WeblinkType.FILE, repo, commit, file};
- if (opt_options) {
- params.options = opt_options;
- }
- return [].concat(this._generateWeblinks(params));
- },
-
- /**
- * @param {string} repo
- * @param {string} commit
- * @param {Object=} opt_options
- * @return {{label: string, url: string}}
- */
- getPatchSetWeblink(repo, commit, opt_options) {
- const params = {type: Gerrit.Nav.WeblinkType.PATCHSET, repo, commit};
- if (opt_options) {
- params.options = opt_options;
- }
- const result = this._generateWeblinks(params);
- if (Array.isArray(result)) {
- return result.pop();
- } else {
- return result;
- }
- },
-
- /**
- * @param {string} repo
- * @param {string} commit
- * @param {Object=} opt_options
- * @return {
- * Array<{label: string, url: string}>|
- * {label: string, url: string}
- * }
- */
- getChangeWeblinks(repo, commit, opt_options) {
- const params = {type: Gerrit.Nav.WeblinkType.CHANGE, repo, commit};
- if (opt_options) {
- params.options = opt_options;
- }
- return [].concat(this._generateWeblinks(params));
- },
-
- getUserDashboard(user = 'self', sections = DEFAULT_SECTIONS,
- title = '') {
- sections = sections
- .filter(section => (user === 'self' || !section.selfOnly))
- .map(section => Object.assign({}, section, {
- name: section.name,
- query: section.query.replace(USER_PLACEHOLDER_PATTERN, user),
- }));
- return {title, sections};
- },
- };
- })(window);
-</script>
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
new file mode 100644
index 0000000..c8724c3
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
@@ -0,0 +1,748 @@
+/**
+ * @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.
+ */
+(function(window) {
+ 'use strict';
+
+ // Navigation parameters object format:
+ //
+ // Each object has a `view` property with a value from Gerrit.Nav.View. The
+ // remaining properties depend on the value used for view.
+ //
+ // - Gerrit.Nav.View.CHANGE:
+ // - `changeNum`, required, String: the numeric ID of the change.
+ // - `project`, optional, String: the project name.
+ // - `patchNum`, optional, Number: the patch for the right-hand-side of
+ // the diff.
+ // - `basePatchNum`, optional, Number: the patch for the left-hand-side
+ // of the diff. If `basePatchNum` is provided, then `patchNum` must
+ // also be provided.
+ // - `edit`, optional, Boolean: whether or not to load the file list with
+ // edit controls.
+ // - `messageHash`, optional, String: the hash of the change message to
+ // scroll to.
+ //
+ // - Gerrit.Nav.View.SEARCH:
+ // - `query`, optional, String: the literal search query. If provided,
+ // the string will be used as the query, and all other params will be
+ // ignored.
+ // - `owner`, optional, String: the owner name.
+ // - `project`, optional, String: the project name.
+ // - `branch`, optional, String: the branch name.
+ // - `topic`, optional, String: the topic name.
+ // - `hashtag`, optional, String: the hashtag name.
+ // - `statuses`, optional, Array<String>: the list of change statuses to
+ // search for. If more than one is provided, the search will OR them
+ // together.
+ // - `offset`, optional, Number: the offset for the query.
+ //
+ // - Gerrit.Nav.View.DIFF:
+ // - `changeNum`, required, String: the numeric ID of the change.
+ // - `path`, required, String: the filepath of the diff.
+ // - `patchNum`, required, Number: the patch for the right-hand-side of
+ // the diff.
+ // - `basePatchNum`, optional, Number: the patch for the left-hand-side
+ // of the diff. If `basePatchNum` is provided, then `patchNum` must
+ // also be provided.
+ // - `lineNum`, optional, Number: the line number to be selected on load.
+ // - `leftSide`, optional, Boolean: if a `lineNum` is provided, a value
+ // of true selects the line from base of the patch range. False by
+ // default.
+ //
+ // - Gerrit.Nav.View.GROUP:
+ // - `groupId`, required, String: the ID of the group.
+ // - `detail`, optional, String: the name of the group detail view.
+ // Takes any value from Gerrit.Nav.GroupDetailView.
+ //
+ // - Gerrit.Nav.View.REPO:
+ // - `repoName`, required, String: the name of the repo
+ // - `detail`, optional, String: the name of the repo detail view.
+ // Takes any value from Gerrit.Nav.RepoDetailView.
+ //
+ // - Gerrit.Nav.View.DASHBOARD
+ // - `repo`, optional, String.
+ // - `sections`, optional, Array of objects with `title` and `query`
+ // strings.
+ // - `user`, optional, String.
+ //
+ // - Gerrit.Nav.View.ROOT:
+ // - no possible parameters.
+
+ window.Gerrit = window.Gerrit || {};
+
+ // Prevent redefinition.
+ if (window.Gerrit.hasOwnProperty('Nav')) { return; }
+
+ const uninitialized = () => {
+ console.warn('Use of uninitialized routing');
+ };
+
+ const EDIT_PATCHNUM = 'edit';
+ const PARENT_PATCHNUM = 'PARENT';
+
+ const USER_PLACEHOLDER_PATTERN = /\$\{user\}/g;
+
+ // NOTE: These queries are tested in Java. Any changes made to definitions
+ // here require corresponding changes to:
+ // javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+ const DEFAULT_SECTIONS = [
+ {
+ // Changes with unpublished draft comments. This section is omitted when
+ // viewing other users, so we don't need to filter anything out.
+ name: 'Has draft comments',
+ query: 'has:draft',
+ selfOnly: true,
+ hideIfEmpty: true,
+ suffixForDashboard: 'limit:10',
+ },
+ {
+ // Changes that are assigned to the viewed user.
+ name: 'Assigned reviews',
+ query: 'assignee:${user} (-is:wip OR owner:self OR assignee:self) ' +
+ 'is:open -is:ignored',
+ hideIfEmpty: true,
+ suffixForDashboard: 'limit:25',
+ },
+ {
+ // WIP open changes owned by viewing user. This section is omitted when
+ // viewing other users, so we don't need to filter anything out.
+ name: 'Work in progress',
+ query: 'is:open owner:${user} is:wip',
+ selfOnly: true,
+ hideIfEmpty: true,
+ suffixForDashboard: 'limit:25',
+ },
+ {
+ // Non-WIP open changes owned by viewed user. Filter out changes ignored
+ // by the viewing user.
+ name: 'Outgoing reviews',
+ query: 'is:open owner:${user} -is:wip -is:ignored',
+ isOutgoing: true,
+ suffixForDashboard: 'limit:25',
+ },
+ {
+ // Non-WIP open changes not owned by the viewed user, that the viewed user
+ // is associated with (as either a reviewer or the assignee). Changes
+ // ignored by the viewing user are filtered out.
+ name: 'Incoming reviews',
+ query: 'is:open -owner:${user} -is:wip -is:ignored ' +
+ '(reviewer:${user} OR assignee:${user})',
+ suffixForDashboard: 'limit:25',
+ },
+ {
+ // Open changes the viewed user is CCed on. Changes ignored by the viewing
+ // user are filtered out.
+ name: 'CCed on',
+ query: 'is:open -is:ignored cc:${user}',
+ suffixForDashboard: 'limit:10',
+ },
+ {
+ name: 'Recently closed',
+ // Closed changes where viewed user is owner, reviewer, or assignee.
+ // Changes ignored by the viewing user are filtered out, and so are WIP
+ // changes not owned by the viewing user (the one instance of
+ // 'owner:self' is intentional and implements this logic).
+ query: 'is:closed -is:ignored (-is:wip OR owner:self) ' +
+ '(owner:${user} OR reviewer:${user} OR assignee:${user} ' +
+ 'OR cc:${user})',
+ suffixForDashboard: '-age:4w limit:10',
+ },
+ ];
+
+ window.Gerrit.Nav = {
+
+ View: {
+ ADMIN: 'admin',
+ AGREEMENTS: 'agreements',
+ CHANGE: 'change',
+ DASHBOARD: 'dashboard',
+ DIFF: 'diff',
+ DOCUMENTATION_SEARCH: 'documentation-search',
+ EDIT: 'edit',
+ GROUP: 'group',
+ PLUGIN_SCREEN: 'plugin-screen',
+ REPO: 'repo',
+ ROOT: 'root',
+ SEARCH: 'search',
+ SETTINGS: 'settings',
+ },
+
+ GroupDetailView: {
+ MEMBERS: 'members',
+ LOG: 'log',
+ },
+
+ RepoDetailView: {
+ ACCESS: 'access',
+ BRANCHES: 'branches',
+ COMMANDS: 'commands',
+ DASHBOARDS: 'dashboards',
+ TAGS: 'tags',
+ },
+
+ WeblinkType: {
+ CHANGE: 'change',
+ FILE: 'file',
+ PATCHSET: 'patchset',
+ },
+
+ /** @type {Function} */
+ _navigate: uninitialized,
+
+ /** @type {Function} */
+ _generateUrl: uninitialized,
+
+ /** @type {Function} */
+ _generateWeblinks: uninitialized,
+
+ /** @type {Function} */
+ mapCommentlinks: uninitialized,
+
+ /**
+ * @param {number=} patchNum
+ * @param {number|string=} basePatchNum
+ */
+ _checkPatchRange(patchNum, basePatchNum) {
+ if (basePatchNum && !patchNum) {
+ throw new Error('Cannot use base patch number without patch number.');
+ }
+ },
+
+ /**
+ * Setup router implementation.
+ *
+ * @param {function(!string)} navigate the router-abstracted equivalent of
+ * `window.location.href = ...`. Takes a string.
+ * @param {function(!Object): string} generateUrl generates a URL given
+ * navigation parameters, detailed in the file header.
+ * @param {function(!Object): string} generateWeblinks weblinks generator
+ * function takes single payload parameter with type property that
+ * determines which
+ * part of the UI is the consumer of the weblinks. type property can
+ * be one of file, change, or patchset.
+ * - For file type, payload will also contain string properties: repo,
+ * commit, file.
+ * - For patchset type, payload will also contain string properties:
+ * repo, commit.
+ * - For change type, payload will also contain string properties:
+ * repo, commit. If server provides weblinks, those will be passed
+ * as options.weblinks property on the main payload object.
+ * @param {function(!Object): Object} mapCommentlinks provides an escape
+ * hatch to modify the commentlinks object, e.g. if it contains any
+ * relative URLs.
+ */
+ setup(navigate, generateUrl, generateWeblinks, mapCommentlinks) {
+ this._navigate = navigate;
+ this._generateUrl = generateUrl;
+ this._generateWeblinks = generateWeblinks;
+ this.mapCommentlinks = mapCommentlinks;
+ },
+
+ destroy() {
+ this._navigate = uninitialized;
+ this._generateUrl = uninitialized;
+ this._generateWeblinks = uninitialized;
+ this.mapCommentlinks = uninitialized;
+ },
+
+ /**
+ * Generate a URL for the given route parameters.
+ *
+ * @param {Object} params
+ * @return {string}
+ */
+ _getUrlFor(params) {
+ return this._generateUrl(params);
+ },
+
+ getUrlForSearchQuery(query, opt_offset) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.SEARCH,
+ query,
+ offset: opt_offset,
+ });
+ },
+
+ /**
+ * @param {!string} project The name of the project.
+ * @param {boolean=} opt_openOnly When true, only search open changes in
+ * the project.
+ * @param {string=} opt_host The host in which to search.
+ * @return {string}
+ */
+ getUrlForProjectChanges(project, opt_openOnly, opt_host) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.SEARCH,
+ project,
+ statuses: opt_openOnly ? ['open'] : [],
+ host: opt_host,
+ });
+ },
+
+ /**
+ * @param {string} branch The name of the branch.
+ * @param {string} project The name of the project.
+ * @param {string=} opt_status The status to search.
+ * @param {string=} opt_host The host in which to search.
+ * @return {string}
+ */
+ getUrlForBranch(branch, project, opt_status, opt_host) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.SEARCH,
+ branch,
+ project,
+ statuses: opt_status ? [opt_status] : undefined,
+ host: opt_host,
+ });
+ },
+
+ /**
+ * @param {string} topic The name of the topic.
+ * @param {string=} opt_host The host in which to search.
+ * @return {string}
+ */
+ getUrlForTopic(topic, opt_host) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.SEARCH,
+ topic,
+ statuses: ['open', 'merged'],
+ host: opt_host,
+ });
+ },
+
+ /**
+ * @param {string} hashtag The name of the hashtag.
+ * @return {string}
+ */
+ getUrlForHashtag(hashtag) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.SEARCH,
+ hashtag,
+ statuses: ['open', 'merged'],
+ });
+ },
+
+ /**
+ * Navigate to a search for changes with the given status.
+ *
+ * @param {string} status
+ */
+ navigateToStatusSearch(status) {
+ this._navigate(this._getUrlFor({
+ view: Gerrit.Nav.View.SEARCH,
+ statuses: [status],
+ }));
+ },
+
+ /**
+ * Navigate to a search query
+ *
+ * @param {string} query
+ * @param {number=} opt_offset
+ */
+ navigateToSearchQuery(query, opt_offset) {
+ return this._navigate(this.getUrlForSearchQuery(query, opt_offset));
+ },
+
+ /**
+ * Navigate to the user's dashboard
+ */
+ navigateToUserDashboard() {
+ return this._navigate(this.getUrlForUserDashboard('self'));
+ },
+
+ /**
+ * @param {!Object} change The change object.
+ * @param {number=} opt_patchNum
+ * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
+ * used for none.
+ * @param {boolean=} opt_isEdit
+ * @param {string=} opt_messageHash
+ * @return {string}
+ */
+ getUrlForChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit,
+ opt_messageHash) {
+ if (opt_basePatchNum === PARENT_PATCHNUM) {
+ opt_basePatchNum = undefined;
+ }
+
+ this._checkPatchRange(opt_patchNum, opt_basePatchNum);
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.CHANGE,
+ changeNum: change._number,
+ project: change.project,
+ patchNum: opt_patchNum,
+ basePatchNum: opt_basePatchNum,
+ edit: opt_isEdit,
+ host: change.internalHost || undefined,
+ messageHash: opt_messageHash,
+ });
+ },
+
+ /**
+ * @param {number} changeNum
+ * @param {string} project The name of the project.
+ * @param {number=} opt_patchNum
+ * @return {string}
+ */
+ getUrlForChangeById(changeNum, project, opt_patchNum) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.CHANGE,
+ changeNum,
+ project,
+ patchNum: opt_patchNum,
+ });
+ },
+
+ /**
+ * @param {!Object} change The change object.
+ * @param {number=} opt_patchNum
+ * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
+ * used for none.
+ * @param {boolean=} opt_isEdit
+ */
+ navigateToChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit) {
+ this._navigate(this.getUrlForChange(change, opt_patchNum,
+ opt_basePatchNum, opt_isEdit));
+ },
+
+ /**
+ * @param {{ _number: number, project: string }} change The change object.
+ * @param {string} path The file path.
+ * @param {number=} opt_patchNum
+ * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
+ * used for none.
+ * @param {number|string=} opt_lineNum
+ * @return {string}
+ */
+ getUrlForDiff(change, path, opt_patchNum, opt_basePatchNum, opt_lineNum) {
+ return this.getUrlForDiffById(change._number, change.project, path,
+ opt_patchNum, opt_basePatchNum, opt_lineNum);
+ },
+
+ /**
+ * @param {number} changeNum
+ * @param {string} project The name of the project.
+ * @param {string} path The file path.
+ * @param {number=} opt_patchNum
+ * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
+ * used for none.
+ * @param {number=} opt_lineNum
+ * @param {boolean=} opt_leftSide
+ * @return {string}
+ */
+ getUrlForDiffById(changeNum, project, path, opt_patchNum,
+ opt_basePatchNum, opt_lineNum, opt_leftSide) {
+ if (opt_basePatchNum === PARENT_PATCHNUM) {
+ opt_basePatchNum = undefined;
+ }
+
+ this._checkPatchRange(opt_patchNum, opt_basePatchNum);
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.DIFF,
+ changeNum,
+ project,
+ path,
+ patchNum: opt_patchNum,
+ basePatchNum: opt_basePatchNum,
+ lineNum: opt_lineNum,
+ leftSide: opt_leftSide,
+ });
+ },
+
+ /**
+ * @param {{ _number: number, project: string }} change The change object.
+ * @param {string} path The file path.
+ * @param {number=} opt_patchNum
+ * @return {string}
+ */
+ getEditUrlForDiff(change, path, opt_patchNum) {
+ return this.getEditUrlForDiffById(change._number, change.project, path,
+ opt_patchNum);
+ },
+
+ /**
+ * @param {number} changeNum
+ * @param {string} project The name of the project.
+ * @param {string} path The file path.
+ * @param {number|string=} opt_patchNum The patchNum the file content
+ * should be based on, or ${EDIT_PATCHNUM} if left undefined.
+ * @return {string}
+ */
+ getEditUrlForDiffById(changeNum, project, path, opt_patchNum) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.EDIT,
+ changeNum,
+ project,
+ path,
+ patchNum: opt_patchNum || EDIT_PATCHNUM,
+ });
+ },
+
+ /**
+ * @param {!Object} change The change object.
+ * @param {string} path The file path.
+ * @param {number=} opt_patchNum
+ * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
+ * used for none.
+ */
+ navigateToDiff(change, path, opt_patchNum, opt_basePatchNum) {
+ this._navigate(this.getUrlForDiff(change, path, opt_patchNum,
+ opt_basePatchNum));
+ },
+
+ /**
+ * @param {string} owner The name of the owner.
+ * @return {string}
+ */
+ getUrlForOwner(owner) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.SEARCH,
+ owner,
+ });
+ },
+
+ /**
+ * @param {string} user The name of the user.
+ * @return {string}
+ */
+ getUrlForUserDashboard(user) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.DASHBOARD,
+ user,
+ });
+ },
+
+ /**
+ * @return {string}
+ */
+ getUrlForRoot() {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.ROOT,
+ });
+ },
+
+ /**
+ * @param {string} repo The name of the repo.
+ * @param {string} dashboard The ID of the dashboard, in the form of
+ * '<ref>:<path>'.
+ * @return {string}
+ */
+ getUrlForRepoDashboard(repo, dashboard) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.DASHBOARD,
+ repo,
+ dashboard,
+ });
+ },
+
+ /**
+ * Navigate to an arbitrary relative URL.
+ *
+ * @param {string} relativeUrl
+ */
+ navigateToRelativeUrl(relativeUrl) {
+ if (!relativeUrl.startsWith('/')) {
+ throw new Error('navigateToRelativeUrl with non-relative URL');
+ }
+ this._navigate(relativeUrl);
+ },
+
+ /**
+ * @param {string} repoName
+ * @return {string}
+ */
+ getUrlForRepo(repoName) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.REPO,
+ repoName,
+ });
+ },
+
+ /**
+ * Navigate to a repo settings page.
+ *
+ * @param {string} repoName
+ */
+ navigateToRepo(repoName) {
+ this._navigate(this.getUrlForRepo(repoName));
+ },
+
+ /**
+ * @param {string} repoName
+ * @return {string}
+ */
+ getUrlForRepoTags(repoName) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.REPO,
+ repoName,
+ detail: Gerrit.Nav.RepoDetailView.TAGS,
+ });
+ },
+
+ /**
+ * @param {string} repoName
+ * @return {string}
+ */
+ getUrlForRepoBranches(repoName) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.REPO,
+ repoName,
+ detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+ });
+ },
+
+ /**
+ * @param {string} repoName
+ * @return {string}
+ */
+ getUrlForRepoAccess(repoName) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.REPO,
+ repoName,
+ detail: Gerrit.Nav.RepoDetailView.ACCESS,
+ });
+ },
+
+ /**
+ * @param {string} repoName
+ * @return {string}
+ */
+ getUrlForRepoCommands(repoName) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.REPO,
+ repoName,
+ detail: Gerrit.Nav.RepoDetailView.COMMANDS,
+ });
+ },
+
+ /**
+ * @param {string} repoName
+ * @return {string}
+ */
+ getUrlForRepoDashboards(repoName) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.REPO,
+ repoName,
+ detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
+ });
+ },
+
+ /**
+ * @param {string} groupId
+ * @return {string}
+ */
+ getUrlForGroup(groupId) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.GROUP,
+ groupId,
+ });
+ },
+
+ /**
+ * @param {string} groupId
+ * @return {string}
+ */
+ getUrlForGroupLog(groupId) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.GROUP,
+ groupId,
+ detail: Gerrit.Nav.GroupDetailView.LOG,
+ });
+ },
+
+ /**
+ * @param {string} groupId
+ * @return {string}
+ */
+ getUrlForGroupMembers(groupId) {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.GROUP,
+ groupId,
+ detail: Gerrit.Nav.GroupDetailView.MEMBERS,
+ });
+ },
+
+ getUrlForSettings() {
+ return this._getUrlFor({view: Gerrit.Nav.View.SETTINGS});
+ },
+
+ /**
+ * @param {string} repo
+ * @param {string} commit
+ * @param {string} file
+ * @param {Object=} opt_options
+ * @return {
+ * Array<{label: string, url: string}>|
+ * {label: string, url: string}
+ * }
+ */
+ getFileWebLinks(repo, commit, file, opt_options) {
+ const params = {type: Gerrit.Nav.WeblinkType.FILE, repo, commit, file};
+ if (opt_options) {
+ params.options = opt_options;
+ }
+ return [].concat(this._generateWeblinks(params));
+ },
+
+ /**
+ * @param {string} repo
+ * @param {string} commit
+ * @param {Object=} opt_options
+ * @return {{label: string, url: string}}
+ */
+ getPatchSetWeblink(repo, commit, opt_options) {
+ const params = {type: Gerrit.Nav.WeblinkType.PATCHSET, repo, commit};
+ if (opt_options) {
+ params.options = opt_options;
+ }
+ const result = this._generateWeblinks(params);
+ if (Array.isArray(result)) {
+ return result.pop();
+ } else {
+ return result;
+ }
+ },
+
+ /**
+ * @param {string} repo
+ * @param {string} commit
+ * @param {Object=} opt_options
+ * @return {
+ * Array<{label: string, url: string}>|
+ * {label: string, url: string}
+ * }
+ */
+ getChangeWeblinks(repo, commit, opt_options) {
+ const params = {type: Gerrit.Nav.WeblinkType.CHANGE, repo, commit};
+ if (opt_options) {
+ params.options = opt_options;
+ }
+ return [].concat(this._generateWeblinks(params));
+ },
+
+ getUserDashboard(user = 'self', sections = DEFAULT_SECTIONS,
+ title = '') {
+ sections = sections
+ .filter(section => (user === 'self' || !section.selfOnly))
+ .map(section => Object.assign({}, section, {
+ name: section.name,
+ query: section.query.replace(USER_PLACEHOLDER_PATTERN, user),
+ }));
+ return {title, sections};
+ },
+ };
+})(window);
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html
index f58780c..33f6215 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html
@@ -19,70 +19,67 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-navigation</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module">
+import '../../../test/common-test-setup.js';
+suite('gr-navigation tests', () => {
+ test('invalid patch ranges throw exceptions', () => {
+ assert.throw(() => Gerrit.Nav.getUrlForChange('123', undefined, 12));
+ assert.throw(() => Gerrit.Nav.getUrlForDiff('123', 'x.c', undefined, 12));
+ });
-<script>
- suite('gr-navigation tests', async () => {
- await readyToTest();
- test('invalid patch ranges throw exceptions', () => {
- assert.throw(() => Gerrit.Nav.getUrlForChange('123', undefined, 12));
- assert.throw(() => Gerrit.Nav.getUrlForDiff('123', 'x.c', undefined, 12));
+ suite('_getUserDashboard', () => {
+ const sections = [
+ {name: 'section 1', query: 'query 1'},
+ {name: 'section 2', query: 'query 2 for ${user}'},
+ {name: 'section 3', query: 'self only query', selfOnly: true},
+ {name: 'section 4', query: 'query 4', suffixForDashboard: 'suffix'},
+ ];
+
+ test('dashboard for self', () => {
+ const dashboard =
+ Gerrit.Nav.getUserDashboard('self', sections, 'title');
+ assert.deepEqual(
+ dashboard,
+ {
+ title: 'title',
+ sections: [
+ {name: 'section 1', query: 'query 1'},
+ {name: 'section 2', query: 'query 2 for self'},
+ {
+ name: 'section 3',
+ query: 'self only query',
+ selfOnly: true,
+ }, {
+ name: 'section 4',
+ query: 'query 4',
+ suffixForDashboard: 'suffix',
+ },
+ ],
+ });
});
- suite('_getUserDashboard', () => {
- const sections = [
- {name: 'section 1', query: 'query 1'},
- {name: 'section 2', query: 'query 2 for ${user}'},
- {name: 'section 3', query: 'self only query', selfOnly: true},
- {name: 'section 4', query: 'query 4', suffixForDashboard: 'suffix'},
- ];
-
- test('dashboard for self', () => {
- const dashboard =
- Gerrit.Nav.getUserDashboard('self', sections, 'title');
- assert.deepEqual(
- dashboard,
- {
- title: 'title',
- sections: [
- {name: 'section 1', query: 'query 1'},
- {name: 'section 2', query: 'query 2 for self'},
- {
- name: 'section 3',
- query: 'self only query',
- selfOnly: true,
- }, {
- name: 'section 4',
- query: 'query 4',
- suffixForDashboard: 'suffix',
- },
- ],
- });
- });
-
- test('dashboard for other user', () => {
- const dashboard =
- Gerrit.Nav.getUserDashboard('user', sections, 'title');
- assert.deepEqual(
- dashboard,
- {
- title: 'title',
- sections: [
- {name: 'section 1', query: 'query 1'},
- {name: 'section 2', query: 'query 2 for user'},
- {
- name: 'section 4',
- query: 'query 4',
- suffixForDashboard: 'suffix',
- },
- ],
- });
- });
+ test('dashboard for other user', () => {
+ const dashboard =
+ Gerrit.Nav.getUserDashboard('user', sections, 'title');
+ assert.deepEqual(
+ dashboard,
+ {
+ title: 'title',
+ sections: [
+ {name: 'section 1', query: 'query 1'},
+ {name: 'section 2', query: 'query 2 for user'},
+ {
+ name: 'section 4',
+ query: 'query 4',
+ suffixForDashboard: 'suffix',
+ },
+ ],
+ });
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
deleted file mode 100644
index 0ba8a22..0000000
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
+++ /dev/null
@@ -1,22 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<dom-module id="gr-reporting">
- <script src="gr-reporting.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index 106508c..ae5cb67 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -14,566 +14,560 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- // Latency reporting constants.
- const TIMING = {
- TYPE: 'timing-report',
- CATEGORY_UI_LATENCY: 'UI Latency',
- CATEGORY_RPC: 'RPC Timing',
- // Reported events - alphabetize below.
- APP_STARTED: 'App Started',
- };
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
- // Plugin-related reporting constants.
- const PLUGINS = {
- TYPE: 'lifecycle',
- // Reported events - alphabetize below.
- INSTALLED: 'Plugins installed',
- };
+// Latency reporting constants.
+const TIMING = {
+ TYPE: 'timing-report',
+ CATEGORY_UI_LATENCY: 'UI Latency',
+ CATEGORY_RPC: 'RPC Timing',
+ // Reported events - alphabetize below.
+ APP_STARTED: 'App Started',
+};
- // Chrome extension-related reporting constants.
- const EXTENSION = {
- TYPE: 'lifecycle',
- // Reported events - alphabetize below.
- DETECTED: 'Extension detected',
- };
+// Plugin-related reporting constants.
+const PLUGINS = {
+ TYPE: 'lifecycle',
+ // Reported events - alphabetize below.
+ INSTALLED: 'Plugins installed',
+};
- // Navigation reporting constants.
- const NAVIGATION = {
- TYPE: 'nav-report',
- CATEGORY: 'Location Changed',
- PAGE: 'Page',
- };
+// Chrome extension-related reporting constants.
+const EXTENSION = {
+ TYPE: 'lifecycle',
+ // Reported events - alphabetize below.
+ DETECTED: 'Extension detected',
+};
- const ERROR = {
- TYPE: 'error',
- CATEGORY: 'exception',
- };
+// Navigation reporting constants.
+const NAVIGATION = {
+ TYPE: 'nav-report',
+ CATEGORY: 'Location Changed',
+ PAGE: 'Page',
+};
- const ERROR_DIALOG = {
- TYPE: 'error',
- CATEGORY: 'Error Dialog',
- };
+const ERROR = {
+ TYPE: 'error',
+ CATEGORY: 'exception',
+};
- const TIMER = {
- CHANGE_DISPLAYED: 'ChangeDisplayed',
- CHANGE_LOAD_FULL: 'ChangeFullyLoaded',
- DASHBOARD_DISPLAYED: 'DashboardDisplayed',
- DIFF_VIEW_CONTENT_DISPLAYED: 'DiffViewOnlyContent',
- DIFF_VIEW_DISPLAYED: 'DiffViewDisplayed',
- DIFF_VIEW_LOAD_FULL: 'DiffViewFullyLoaded',
- FILE_LIST_DISPLAYED: 'FileListDisplayed',
- PLUGINS_LOADED: 'PluginsLoaded',
- STARTUP_CHANGE_DISPLAYED: 'StartupChangeDisplayed',
- STARTUP_CHANGE_LOAD_FULL: 'StartupChangeFullyLoaded',
- STARTUP_DASHBOARD_DISPLAYED: 'StartupDashboardDisplayed',
- STARTUP_DIFF_VIEW_CONTENT_DISPLAYED: 'StartupDiffViewOnlyContent',
- STARTUP_DIFF_VIEW_DISPLAYED: 'StartupDiffViewDisplayed',
- STARTUP_DIFF_VIEW_LOAD_FULL: 'StartupDiffViewFullyLoaded',
- STARTUP_FILE_LIST_DISPLAYED: 'StartupFileListDisplayed',
- WEB_COMPONENTS_READY: 'WebComponentsReady',
- METRICS_PLUGIN_LOADED: 'MetricsPluginLoaded',
- };
+const ERROR_DIALOG = {
+ TYPE: 'error',
+ CATEGORY: 'Error Dialog',
+};
- const STARTUP_TIMERS = {};
- STARTUP_TIMERS[TIMER.PLUGINS_LOADED] = 0;
- STARTUP_TIMERS[TIMER.METRICS_PLUGIN_LOADED] = 0;
- STARTUP_TIMERS[TIMER.STARTUP_CHANGE_DISPLAYED] = 0;
- STARTUP_TIMERS[TIMER.STARTUP_CHANGE_LOAD_FULL] = 0;
- STARTUP_TIMERS[TIMER.STARTUP_DASHBOARD_DISPLAYED] = 0;
- STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED] = 0;
- STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_DISPLAYED] = 0;
- STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_LOAD_FULL] = 0;
- STARTUP_TIMERS[TIMER.STARTUP_FILE_LIST_DISPLAYED] = 0;
- STARTUP_TIMERS[TIMING.APP_STARTED] = 0;
- // WebComponentsReady timer is triggered from gr-router.
- STARTUP_TIMERS[TIMER.WEB_COMPONENTS_READY] = 0;
+const TIMER = {
+ CHANGE_DISPLAYED: 'ChangeDisplayed',
+ CHANGE_LOAD_FULL: 'ChangeFullyLoaded',
+ DASHBOARD_DISPLAYED: 'DashboardDisplayed',
+ DIFF_VIEW_CONTENT_DISPLAYED: 'DiffViewOnlyContent',
+ DIFF_VIEW_DISPLAYED: 'DiffViewDisplayed',
+ DIFF_VIEW_LOAD_FULL: 'DiffViewFullyLoaded',
+ FILE_LIST_DISPLAYED: 'FileListDisplayed',
+ PLUGINS_LOADED: 'PluginsLoaded',
+ STARTUP_CHANGE_DISPLAYED: 'StartupChangeDisplayed',
+ STARTUP_CHANGE_LOAD_FULL: 'StartupChangeFullyLoaded',
+ STARTUP_DASHBOARD_DISPLAYED: 'StartupDashboardDisplayed',
+ STARTUP_DIFF_VIEW_CONTENT_DISPLAYED: 'StartupDiffViewOnlyContent',
+ STARTUP_DIFF_VIEW_DISPLAYED: 'StartupDiffViewDisplayed',
+ STARTUP_DIFF_VIEW_LOAD_FULL: 'StartupDiffViewFullyLoaded',
+ STARTUP_FILE_LIST_DISPLAYED: 'StartupFileListDisplayed',
+ WEB_COMPONENTS_READY: 'WebComponentsReady',
+ METRICS_PLUGIN_LOADED: 'MetricsPluginLoaded',
+};
- const INTERACTION_TYPE = 'interaction';
+const STARTUP_TIMERS = {};
+STARTUP_TIMERS[TIMER.PLUGINS_LOADED] = 0;
+STARTUP_TIMERS[TIMER.METRICS_PLUGIN_LOADED] = 0;
+STARTUP_TIMERS[TIMER.STARTUP_CHANGE_DISPLAYED] = 0;
+STARTUP_TIMERS[TIMER.STARTUP_CHANGE_LOAD_FULL] = 0;
+STARTUP_TIMERS[TIMER.STARTUP_DASHBOARD_DISPLAYED] = 0;
+STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED] = 0;
+STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_DISPLAYED] = 0;
+STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_LOAD_FULL] = 0;
+STARTUP_TIMERS[TIMER.STARTUP_FILE_LIST_DISPLAYED] = 0;
+STARTUP_TIMERS[TIMING.APP_STARTED] = 0;
+// WebComponentsReady timer is triggered from gr-router.
+STARTUP_TIMERS[TIMER.WEB_COMPONENTS_READY] = 0;
- const DRAFT_ACTION_TIMER = 'TimeBetweenDraftActions';
- const DRAFT_ACTION_TIMER_MAX = 2 * 60 * 1000; // 2 minutes.
+const INTERACTION_TYPE = 'interaction';
- let pending = [];
- let slowRpcList = [];
- const SLOW_RPC_THRESHOLD = 500;
+const DRAFT_ACTION_TIMER = 'TimeBetweenDraftActions';
+const DRAFT_ACTION_TIMER_MAX = 2 * 60 * 1000; // 2 minutes.
- // Variables that hold context info in global scope
- let reportRepoName = undefined;
+let pending = [];
+let slowRpcList = [];
+const SLOW_RPC_THRESHOLD = 500;
- const onError = function(oldOnError, msg, url, line, column, error) {
- if (oldOnError) {
- oldOnError(msg, url, line, column, error);
+// Variables that hold context info in global scope
+let reportRepoName = undefined;
+
+const onError = function(oldOnError, msg, url, line, column, error) {
+ if (oldOnError) {
+ oldOnError(msg, url, line, column, error);
+ }
+ if (error) {
+ line = line || error.lineNumber;
+ column = column || error.columnNumber;
+ let shortenedErrorStack = msg;
+ if (error.stack) {
+ const errorStackLines = error.stack.split('\n');
+ shortenedErrorStack = errorStackLines.slice(0,
+ Math.min(3, errorStackLines.length)).join('\n');
}
- if (error) {
- line = line || error.lineNumber;
- column = column || error.columnNumber;
- let shortenedErrorStack = msg;
- if (error.stack) {
- const errorStackLines = error.stack.split('\n');
- shortenedErrorStack = errorStackLines.slice(0,
- Math.min(3, errorStackLines.length)).join('\n');
- }
- msg = shortenedErrorStack || error.toString();
- }
+ msg = shortenedErrorStack || error.toString();
+ }
+ const payload = {
+ url,
+ line,
+ column,
+ error,
+ };
+ GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload);
+ return true;
+};
+
+const catchErrors = function(opt_context) {
+ const context = opt_context || window;
+ context.onerror = onError.bind(null, context.onerror);
+ context.addEventListener('unhandledrejection', e => {
+ const msg = e.reason.message;
const payload = {
- url,
- line,
- column,
- error,
+ error: e.reason,
};
GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload);
- return true;
- };
+ });
+};
+catchErrors();
- const catchErrors = function(opt_context) {
- const context = opt_context || window;
- context.onerror = onError.bind(null, context.onerror);
- context.addEventListener('unhandledrejection', e => {
- const msg = e.reason.message;
- const payload = {
- error: e.reason,
- };
- GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload);
+// PerformanceObserver interface is a browser API.
+if (window.PerformanceObserver) {
+ const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || [];
+ // Safari doesn't support longtask yet
+ if (supportedEntryTypes.includes('longtask')) {
+ const catchLongJsTasks = new PerformanceObserver(list => {
+ for (const task of list.getEntries()) {
+ // We are interested in longtask longer than 200 ms (default is 50 ms)
+ if (task.duration > 200) {
+ GrReporting.prototype.reporter(TIMING.TYPE,
+ TIMING.CATEGORY_UI_LATENCY, `Task ${task.name}`,
+ Math.round(task.duration), {}, false);
+ }
+ }
});
- };
- catchErrors();
-
- // PerformanceObserver interface is a browser API.
- if (window.PerformanceObserver) {
- const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || [];
- // Safari doesn't support longtask yet
- if (supportedEntryTypes.includes('longtask')) {
- const catchLongJsTasks = new PerformanceObserver(list => {
- for (const task of list.getEntries()) {
- // We are interested in longtask longer than 200 ms (default is 50 ms)
- if (task.duration > 200) {
- GrReporting.prototype.reporter(TIMING.TYPE,
- TIMING.CATEGORY_UI_LATENCY, `Task ${task.name}`,
- Math.round(task.duration), {}, false);
- }
- }
- });
- catchLongJsTasks.observe({entryTypes: ['longtask']});
- }
+ catchLongJsTasks.observe({entryTypes: ['longtask']});
}
+}
- document.addEventListener('visibilitychange', () => {
- const eventName = `Visibility changed to ${document.visibilityState}`;
- GrReporting.prototype.reporter(INTERACTION_TYPE, undefined, eventName,
- undefined, {}, true);
- });
+document.addEventListener('visibilitychange', () => {
+ const eventName = `Visibility changed to ${document.visibilityState}`;
+ GrReporting.prototype.reporter(INTERACTION_TYPE, undefined, eventName,
+ undefined, {}, true);
+});
- // The Polymer pass of JSCompiler requires this to be reassignable
- // eslint-disable-next-line prefer-const
- let GrReporting = Polymer({
- is: 'gr-reporting',
+// The Polymer pass of JSCompiler requires this to be reassignable
+// eslint-disable-next-line prefer-const
+let GrReporting = Polymer({
+ is: 'gr-reporting',
- properties: {
- category: String,
+ properties: {
+ category: String,
- _baselines: {
- type: Object,
- value: STARTUP_TIMERS, // Shared across all instances.
+ _baselines: {
+ type: Object,
+ value: STARTUP_TIMERS, // Shared across all instances.
+ },
+
+ _timers: {
+ type: Object,
+ value: {timeBetweenDraftActions: null}, // Shared across all instances.
+ },
+ },
+
+ get performanceTiming() {
+ return window.performance.timing;
+ },
+
+ get slowRpcSnapshot() {
+ return slowRpcList.slice();
+ },
+
+ now() {
+ return Math.round(window.performance.now());
+ },
+
+ _arePluginsLoaded() {
+ return this._baselines &&
+ !this._baselines.hasOwnProperty(TIMER.PLUGINS_LOADED);
+ },
+
+ _isMetricsPluginLoaded() {
+ return this._arePluginsLoaded() || this._baselines &&
+ !this._baselines.hasOwnProperty(TIMER.METRICS_PLUGIN_LOADED);
+ },
+
+ /**
+ * Reporter reports events. Events will be queued if metrics plugin is not
+ * yet installed.
+ *
+ * @param {string} type
+ * @param {string} category
+ * @param {string} eventName
+ * @param {string|number} eventValue
+ * @param {Object} eventDetails
+ * @param {boolean|undefined} opt_noLog If true, the event will not be
+ * logged to the JS console.
+ */
+ reporter(type, category, eventName, eventValue, eventDetails, opt_noLog) {
+ const eventInfo = this._createEventInfo(type, category,
+ eventName, eventValue, eventDetails);
+ if (type === ERROR.TYPE && category === ERROR.CATEGORY) {
+ console.error(eventValue && eventValue.error || eventName);
+ }
+
+ // We report events immediately when metrics plugin is loaded
+ if (this._isMetricsPluginLoaded() && !pending.length) {
+ this._reportEvent(eventInfo, opt_noLog);
+ } else {
+ // We cache until metrics plugin is loaded
+ pending.push([eventInfo, opt_noLog]);
+ if (this._isMetricsPluginLoaded()) {
+ pending.forEach(([eventInfo, opt_noLog]) => {
+ this._reportEvent(eventInfo, opt_noLog);
+ });
+ pending = [];
+ }
+ }
+ },
+
+ _reportEvent(eventInfo, opt_noLog) {
+ const {type, value, name} = eventInfo;
+ document.dispatchEvent(new CustomEvent(type, {detail: eventInfo}));
+ if (opt_noLog) { return; }
+ if (type !== ERROR.TYPE) {
+ if (value !== undefined) {
+ console.log(`Reporting: ${name}: ${value}`);
+ } else {
+ console.log(`Reporting: ${name}`);
+ }
+ }
+ },
+
+ _createEventInfo(type, category, name, value, eventDetails) {
+ const eventInfo = {
+ type,
+ category,
+ name,
+ value,
+ eventStart: this.now(),
+ };
+
+ if (typeof(eventDetails) === 'object' &&
+ Object.entries(eventDetails).length !== 0) {
+ eventInfo.eventDetails = JSON.stringify(eventDetails);
+ }
+ if (reportRepoName) {
+ eventInfo.repoName = reportRepoName;
+ }
+ const isInBackgroundTab = document.visibilityState === 'hidden';
+ if (isInBackgroundTab !== undefined) {
+ eventInfo.inBackgroundTab = isInBackgroundTab;
+ }
+
+ return eventInfo;
+ },
+
+ /**
+ * User-perceived app start time, should be reported when the app is ready.
+ */
+ appStarted() {
+ this.timeEnd(TIMING.APP_STARTED);
+ this._reportNavResTimes();
+ },
+
+ /**
+ * Browser's navigation and resource timings
+ */
+ _reportNavResTimes() {
+ const perfEvents = Object.keys(this.performanceTiming.toJSON());
+ perfEvents.forEach(
+ eventName => this._reportPerformanceTiming(eventName)
+ );
+ },
+
+ _reportPerformanceTiming(eventName, eventDetails) {
+ const eventTiming = this.performanceTiming[eventName];
+ if (eventTiming > 0) {
+ const elapsedTime = eventTiming -
+ this.performanceTiming.navigationStart;
+ // NavResTime - Navigation and resource timings.
+ this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY,
+ `NavResTime - ${eventName}`, elapsedTime, eventDetails, true);
+ }
+ },
+
+ beforeLocationChanged() {
+ for (const prop of Object.keys(this._baselines)) {
+ delete this._baselines[prop];
+ }
+ this.time(TIMER.CHANGE_DISPLAYED);
+ this.time(TIMER.CHANGE_LOAD_FULL);
+ this.time(TIMER.DASHBOARD_DISPLAYED);
+ this.time(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
+ this.time(TIMER.DIFF_VIEW_DISPLAYED);
+ this.time(TIMER.DIFF_VIEW_LOAD_FULL);
+ this.time(TIMER.FILE_LIST_DISPLAYED);
+ reportRepoName = undefined;
+ // reset slow rpc list since here start page loads which report these rpcs
+ slowRpcList = [];
+ },
+
+ locationChanged(page) {
+ this.reporter(
+ NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page);
+ },
+
+ dashboardDisplayed() {
+ if (this._baselines.hasOwnProperty(TIMER.STARTUP_DASHBOARD_DISPLAYED)) {
+ this.timeEnd(TIMER.STARTUP_DASHBOARD_DISPLAYED, {rpcList:
+ this.slowRpcSnapshot});
+ } else {
+ this.timeEnd(TIMER.DASHBOARD_DISPLAYED, {rpcList:
+ this.slowRpcSnapshot});
+ }
+ },
+
+ changeDisplayed() {
+ if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_DISPLAYED)) {
+ this.timeEnd(TIMER.STARTUP_CHANGE_DISPLAYED, {rpcList:
+ this.slowRpcSnapshot});
+ } else {
+ this.timeEnd(TIMER.CHANGE_DISPLAYED, {rpcList:
+ this.slowRpcSnapshot});
+ }
+ },
+
+ changeFullyLoaded() {
+ if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_LOAD_FULL)) {
+ this.timeEnd(TIMER.STARTUP_CHANGE_LOAD_FULL);
+ } else {
+ this.timeEnd(TIMER.CHANGE_LOAD_FULL);
+ }
+ },
+
+ diffViewDisplayed() {
+ if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_DISPLAYED)) {
+ this.timeEnd(TIMER.STARTUP_DIFF_VIEW_DISPLAYED, {rpcList:
+ this.slowRpcSnapshot});
+ } else {
+ this.timeEnd(TIMER.DIFF_VIEW_DISPLAYED, {rpcList:
+ this.slowRpcSnapshot});
+ }
+ },
+
+ diffViewFullyLoaded() {
+ if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL)) {
+ this.timeEnd(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL);
+ } else {
+ this.timeEnd(TIMER.DIFF_VIEW_LOAD_FULL);
+ }
+ },
+
+ diffViewContentDisplayed() {
+ if (this._baselines.hasOwnProperty(
+ TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED)) {
+ this.timeEnd(TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED);
+ } else {
+ this.timeEnd(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
+ }
+ },
+
+ fileListDisplayed() {
+ if (this._baselines.hasOwnProperty(TIMER.STARTUP_FILE_LIST_DISPLAYED)) {
+ this.timeEnd(TIMER.STARTUP_FILE_LIST_DISPLAYED);
+ } else {
+ this.timeEnd(TIMER.FILE_LIST_DISPLAYED);
+ }
+ },
+
+ reportExtension(name) {
+ this.reporter(EXTENSION.TYPE, EXTENSION.DETECTED, name);
+ },
+
+ pluginLoaded(name) {
+ if (name.startsWith('metrics-')) {
+ this.timeEnd(TIMER.METRICS_PLUGIN_LOADED);
+ }
+ },
+
+ pluginsLoaded(pluginsList) {
+ this.timeEnd(TIMER.PLUGINS_LOADED);
+ this.reporter(
+ PLUGINS.TYPE, PLUGINS.INSTALLED, PLUGINS.INSTALLED, undefined,
+ {pluginsList: pluginsList || []}, true);
+ },
+
+ /**
+ * Reset named timer.
+ */
+ time(name) {
+ this._baselines[name] = this.now();
+ window.performance.mark(`${name}-start`);
+ },
+
+ /**
+ * Finish named timer and report it to server.
+ */
+ timeEnd(name, eventDetails) {
+ if (!this._baselines.hasOwnProperty(name)) { return; }
+ const baseTime = this._baselines[name];
+ delete this._baselines[name];
+ this._reportTiming(name, this.now() - baseTime, eventDetails);
+
+ // Finalize the interval. Either from a registered start mark or
+ // the navigation start time (if baseTime is 0).
+ if (baseTime !== 0) {
+ window.performance.measure(name, `${name}-start`);
+ } else {
+ // Microsft Edge does not handle the 2nd param correctly
+ // (if undefined).
+ window.performance.measure(name);
+ }
+ },
+
+ /**
+ * Reports just line timeEnd, but additionally reports an average given a
+ * denominator and a separate reporiting name for the average.
+ *
+ * @param {string} name Timing name.
+ * @param {string} averageName Average timing name.
+ * @param {number} denominator Number by which to divide the total to
+ * compute the average.
+ */
+ timeEndWithAverage(name, averageName, denominator) {
+ if (!this._baselines.hasOwnProperty(name)) { return; }
+ const baseTime = this._baselines[name];
+ this.timeEnd(name);
+
+ // Guard against division by zero.
+ if (!denominator) { return; }
+ const time = this.now() - baseTime;
+ this._reportTiming(averageName, time / denominator);
+ },
+
+ /**
+ * Send a timing report with an arbitrary time value.
+ *
+ * @param {string} name Timing name.
+ * @param {number} time The time to report as an integer of milliseconds.
+ * @param {Object} eventDetails non sensitive details
+ */
+ _reportTiming(name, time, eventDetails) {
+ this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY, name, time,
+ eventDetails);
+ },
+
+ /**
+ * Get a timer object to for reporing a user timing. The start time will be
+ * the time that the object has been created, and the end time will be the
+ * time that the "end" method is called on the object.
+ *
+ * @param {string} name Timing name.
+ * @returns {!Object} The timer object.
+ */
+ getTimer(name) {
+ let called = false;
+ let start;
+ let max = null;
+
+ const timer = {
+
+ // Clear the timer and reset the start time.
+ reset: () => {
+ called = false;
+ start = this.now();
+ return timer;
},
- _timers: {
- type: Object,
- value: {timeBetweenDraftActions: null}, // Shared across all instances.
+ // Stop the timer and report the intervening time.
+ end: () => {
+ if (called) {
+ throw new Error(`Timer for "${name}" already ended.`);
+ }
+ called = true;
+ const time = this.now() - start;
+
+ // If a maximum is specified and the time exceeds it, do not report.
+ if (max && time > max) { return timer; }
+
+ this._reportTiming(name, time);
+ return timer;
},
- },
- get performanceTiming() {
- return window.performance.timing;
- },
+ // Set a maximum reportable time. If a maximum is set and the timer is
+ // ended after the specified amount of time, the value is not reported.
+ withMaximum(maximum) {
+ max = maximum;
+ return timer;
+ },
+ };
- get slowRpcSnapshot() {
- return slowRpcList.slice();
- },
+ // The timer is initialized to its creation time.
+ return timer.reset();
+ },
- now() {
- return Math.round(window.performance.now());
- },
+ /**
+ * Log timing information for an RPC.
+ *
+ * @param {string} anonymizedUrl The URL of the RPC with tokens obfuscated.
+ * @param {number} elapsed The time elapsed of the RPC.
+ */
+ reportRpcTiming(anonymizedUrl, elapsed) {
+ this.reporter(TIMING.TYPE, TIMING.CATEGORY_RPC, 'RPC-' + anonymizedUrl,
+ elapsed, {}, true);
+ if (elapsed >= SLOW_RPC_THRESHOLD) {
+ slowRpcList.push({anonymizedUrl, elapsed});
+ }
+ },
- _arePluginsLoaded() {
- return this._baselines &&
- !this._baselines.hasOwnProperty(TIMER.PLUGINS_LOADED);
- },
+ reportInteraction(eventName, details) {
+ this.reporter(INTERACTION_TYPE, this.category, eventName, undefined,
+ details, true);
+ },
- _isMetricsPluginLoaded() {
- return this._arePluginsLoaded() || this._baselines &&
- !this._baselines.hasOwnProperty(TIMER.METRICS_PLUGIN_LOADED);
- },
+ /**
+ * A draft interaction was started. Update the time-betweeen-draft-actions
+ * timer.
+ */
+ recordDraftInteraction() {
+ // If there is no timer defined, then this is the first interaction.
+ // Set up the timer so that it's ready to record the intervening time when
+ // called again.
+ const timer = this._timers.timeBetweenDraftActions;
+ if (!timer) {
+ // Create a timer with a maximum length.
+ this._timers.timeBetweenDraftActions = this.getTimer(DRAFT_ACTION_TIMER)
+ .withMaximum(DRAFT_ACTION_TIMER_MAX);
+ return;
+ }
- /**
- * Reporter reports events. Events will be queued if metrics plugin is not
- * yet installed.
- *
- * @param {string} type
- * @param {string} category
- * @param {string} eventName
- * @param {string|number} eventValue
- * @param {Object} eventDetails
- * @param {boolean|undefined} opt_noLog If true, the event will not be
- * logged to the JS console.
- */
- reporter(type, category, eventName, eventValue, eventDetails, opt_noLog) {
- const eventInfo = this._createEventInfo(type, category,
- eventName, eventValue, eventDetails);
- if (type === ERROR.TYPE && category === ERROR.CATEGORY) {
- console.error(eventValue && eventValue.error || eventName);
- }
+ // Mark the time and reinitialize the timer.
+ timer.end().reset();
+ },
- // We report events immediately when metrics plugin is loaded
- if (this._isMetricsPluginLoaded() && !pending.length) {
- this._reportEvent(eventInfo, opt_noLog);
- } else {
- // We cache until metrics plugin is loaded
- pending.push([eventInfo, opt_noLog]);
- if (this._isMetricsPluginLoaded()) {
- pending.forEach(([eventInfo, opt_noLog]) => {
- this._reportEvent(eventInfo, opt_noLog);
- });
- pending = [];
- }
- }
- },
+ reportErrorDialog(message) {
+ this.reporter(ERROR_DIALOG.TYPE, ERROR_DIALOG.CATEGORY,
+ 'ErrorDialog: ' + message, {error: new Error(message)});
+ },
- _reportEvent(eventInfo, opt_noLog) {
- const {type, value, name} = eventInfo;
- document.dispatchEvent(new CustomEvent(type, {detail: eventInfo}));
- if (opt_noLog) { return; }
- if (type !== ERROR.TYPE) {
- if (value !== undefined) {
- console.log(`Reporting: ${name}: ${value}`);
- } else {
- console.log(`Reporting: ${name}`);
- }
- }
- },
+ setRepoName(repoName) {
+ reportRepoName = repoName;
+ },
+});
- _createEventInfo(type, category, name, value, eventDetails) {
- const eventInfo = {
- type,
- category,
- name,
- value,
- eventStart: this.now(),
- };
-
- if (typeof(eventDetails) === 'object' &&
- Object.entries(eventDetails).length !== 0) {
- eventInfo.eventDetails = JSON.stringify(eventDetails);
- }
- if (reportRepoName) {
- eventInfo.repoName = reportRepoName;
- }
- const isInBackgroundTab = document.visibilityState === 'hidden';
- if (isInBackgroundTab !== undefined) {
- eventInfo.inBackgroundTab = isInBackgroundTab;
- }
-
- return eventInfo;
- },
-
- /**
- * User-perceived app start time, should be reported when the app is ready.
- */
- appStarted() {
- this.timeEnd(TIMING.APP_STARTED);
- this.pageLoaded();
- },
-
- /**
- * Page load time and other metrics, should be reported at any time
- * after navigation.
- */
- pageLoaded() {
- if (this.performanceTiming.loadEventEnd === 0) {
- console.error('pageLoaded should be called after window.onload');
- this.async(this.pageLoaded, 100);
- } else {
- const perfEvents = Object.keys(this.performanceTiming.toJSON());
- perfEvents.forEach(
- eventName => this._reportPerformanceTiming(eventName)
- );
- }
- },
-
- _reportPerformanceTiming(eventName, eventDetails) {
- const eventTiming = this.performanceTiming[eventName];
- if (eventTiming > 0) {
- const elapsedTime = eventTiming -
- this.performanceTiming.navigationStart;
- // NavResTime - Navigation and resource timings.
- this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY,
- `NavResTime - ${eventName}`, elapsedTime, eventDetails, true);
- }
- },
-
- beforeLocationChanged() {
- for (const prop of Object.keys(this._baselines)) {
- delete this._baselines[prop];
- }
- this.time(TIMER.CHANGE_DISPLAYED);
- this.time(TIMER.CHANGE_LOAD_FULL);
- this.time(TIMER.DASHBOARD_DISPLAYED);
- this.time(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
- this.time(TIMER.DIFF_VIEW_DISPLAYED);
- this.time(TIMER.DIFF_VIEW_LOAD_FULL);
- this.time(TIMER.FILE_LIST_DISPLAYED);
- reportRepoName = undefined;
- // reset slow rpc list since here start page loads which report these rpcs
- slowRpcList = [];
- },
-
- locationChanged(page) {
- this.reporter(
- NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page);
- },
-
- dashboardDisplayed() {
- if (this._baselines.hasOwnProperty(TIMER.STARTUP_DASHBOARD_DISPLAYED)) {
- this.timeEnd(TIMER.STARTUP_DASHBOARD_DISPLAYED, {rpcList:
- this.slowRpcSnapshot});
- } else {
- this.timeEnd(TIMER.DASHBOARD_DISPLAYED, {rpcList:
- this.slowRpcSnapshot});
- }
- },
-
- changeDisplayed() {
- if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_DISPLAYED)) {
- this.timeEnd(TIMER.STARTUP_CHANGE_DISPLAYED, {rpcList:
- this.slowRpcSnapshot});
- } else {
- this.timeEnd(TIMER.CHANGE_DISPLAYED, {rpcList:
- this.slowRpcSnapshot});
- }
- },
-
- changeFullyLoaded() {
- if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_LOAD_FULL)) {
- this.timeEnd(TIMER.STARTUP_CHANGE_LOAD_FULL);
- } else {
- this.timeEnd(TIMER.CHANGE_LOAD_FULL);
- }
- },
-
- diffViewDisplayed() {
- if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_DISPLAYED)) {
- this.timeEnd(TIMER.STARTUP_DIFF_VIEW_DISPLAYED, {rpcList:
- this.slowRpcSnapshot});
- } else {
- this.timeEnd(TIMER.DIFF_VIEW_DISPLAYED, {rpcList:
- this.slowRpcSnapshot});
- }
- },
-
- diffViewFullyLoaded() {
- if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL)) {
- this.timeEnd(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL);
- } else {
- this.timeEnd(TIMER.DIFF_VIEW_LOAD_FULL);
- }
- },
-
- diffViewContentDisplayed() {
- if (this._baselines.hasOwnProperty(
- TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED)) {
- this.timeEnd(TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED);
- } else {
- this.timeEnd(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
- }
- },
-
- fileListDisplayed() {
- if (this._baselines.hasOwnProperty(TIMER.STARTUP_FILE_LIST_DISPLAYED)) {
- this.timeEnd(TIMER.STARTUP_FILE_LIST_DISPLAYED);
- } else {
- this.timeEnd(TIMER.FILE_LIST_DISPLAYED);
- }
- },
-
- reportExtension(name) {
- this.reporter(EXTENSION.TYPE, EXTENSION.DETECTED, name);
- },
-
- pluginLoaded(name) {
- if (name.startsWith('metrics-')) {
- this.timeEnd(TIMER.METRICS_PLUGIN_LOADED);
- }
- },
-
- pluginsLoaded(pluginsList) {
- this.timeEnd(TIMER.PLUGINS_LOADED);
- this.reporter(
- PLUGINS.TYPE, PLUGINS.INSTALLED, PLUGINS.INSTALLED, undefined,
- {pluginsList: pluginsList || []}, true);
- },
-
- /**
- * Reset named timer.
- */
- time(name) {
- this._baselines[name] = this.now();
- window.performance.mark(`${name}-start`);
- },
-
- /**
- * Finish named timer and report it to server.
- */
- timeEnd(name, eventDetails) {
- if (!this._baselines.hasOwnProperty(name)) { return; }
- const baseTime = this._baselines[name];
- delete this._baselines[name];
- this._reportTiming(name, this.now() - baseTime, eventDetails);
-
- // Finalize the interval. Either from a registered start mark or
- // the navigation start time (if baseTime is 0).
- if (baseTime !== 0) {
- window.performance.measure(name, `${name}-start`);
- } else {
- // Microsft Edge does not handle the 2nd param correctly
- // (if undefined).
- window.performance.measure(name);
- }
- },
-
- /**
- * Reports just line timeEnd, but additionally reports an average given a
- * denominator and a separate reporiting name for the average.
- *
- * @param {string} name Timing name.
- * @param {string} averageName Average timing name.
- * @param {number} denominator Number by which to divide the total to
- * compute the average.
- */
- timeEndWithAverage(name, averageName, denominator) {
- if (!this._baselines.hasOwnProperty(name)) { return; }
- const baseTime = this._baselines[name];
- this.timeEnd(name);
-
- // Guard against division by zero.
- if (!denominator) { return; }
- const time = this.now() - baseTime;
- this._reportTiming(averageName, time / denominator);
- },
-
- /**
- * Send a timing report with an arbitrary time value.
- *
- * @param {string} name Timing name.
- * @param {number} time The time to report as an integer of milliseconds.
- * @param {Object} eventDetails non sensitive details
- */
- _reportTiming(name, time, eventDetails) {
- this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY, name, time,
- eventDetails);
- },
-
- /**
- * Get a timer object to for reporing a user timing. The start time will be
- * the time that the object has been created, and the end time will be the
- * time that the "end" method is called on the object.
- *
- * @param {string} name Timing name.
- * @returns {!Object} The timer object.
- */
- getTimer(name) {
- let called = false;
- let start;
- let max = null;
-
- const timer = {
-
- // Clear the timer and reset the start time.
- reset: () => {
- called = false;
- start = this.now();
- return timer;
- },
-
- // Stop the timer and report the intervening time.
- end: () => {
- if (called) {
- throw new Error(`Timer for "${name}" already ended.`);
- }
- called = true;
- const time = this.now() - start;
-
- // If a maximum is specified and the time exceeds it, do not report.
- if (max && time > max) { return timer; }
-
- this._reportTiming(name, time);
- return timer;
- },
-
- // Set a maximum reportable time. If a maximum is set and the timer is
- // ended after the specified amount of time, the value is not reported.
- withMaximum(maximum) {
- max = maximum;
- return timer;
- },
- };
-
- // The timer is initialized to its creation time.
- return timer.reset();
- },
-
- /**
- * Log timing information for an RPC.
- *
- * @param {string} anonymizedUrl The URL of the RPC with tokens obfuscated.
- * @param {number} elapsed The time elapsed of the RPC.
- */
- reportRpcTiming(anonymizedUrl, elapsed) {
- this.reporter(TIMING.TYPE, TIMING.CATEGORY_RPC, 'RPC-' + anonymizedUrl,
- elapsed, {}, true);
- if (elapsed >= SLOW_RPC_THRESHOLD) {
- slowRpcList.push({anonymizedUrl, elapsed});
- }
- },
-
- reportInteraction(eventName, details) {
- this.reporter(INTERACTION_TYPE, this.category, eventName, undefined,
- details, true);
- },
-
- /**
- * A draft interaction was started. Update the time-betweeen-draft-actions
- * timer.
- */
- recordDraftInteraction() {
- // If there is no timer defined, then this is the first interaction.
- // Set up the timer so that it's ready to record the intervening time when
- // called again.
- const timer = this._timers.timeBetweenDraftActions;
- if (!timer) {
- // Create a timer with a maximum length.
- this._timers.timeBetweenDraftActions = this.getTimer(DRAFT_ACTION_TIMER)
- .withMaximum(DRAFT_ACTION_TIMER_MAX);
- return;
- }
-
- // Mark the time and reinitialize the timer.
- timer.end().reset();
- },
-
- reportErrorDialog(message) {
- this.reporter(ERROR_DIALOG.TYPE, ERROR_DIALOG.CATEGORY,
- 'ErrorDialog: ' + message, {error: new Error(message)});
- },
-
- setRepoName(repoName) {
- reportRepoName = repoName;
- },
- });
-
- window.GrReporting = GrReporting;
- // Expose onerror installation so it would be accessible from tests.
- window.GrReporting._catchErrors = catchErrors;
- window.GrReporting.STARTUP_TIMERS = Object.assign({}, STARTUP_TIMERS);
-})();
+window.GrReporting = GrReporting;
+// Expose onerror installation so it would be accessible from tests.
+window.GrReporting._catchErrors = catchErrors;
+window.GrReporting.STARTUP_TIMERS = Object.assign({}, STARTUP_TIMERS);
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
index 19e4b74..ea6e8cc 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reporting</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-reporting.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,407 +30,404 @@
</template>
</test-fixture>
-<script>
- suite('gr-reporting tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let clock;
- let fakePerformance;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-reporting.js';
+suite('gr-reporting tests', () => {
+ let element;
+ let sandbox;
+ let clock;
+ let fakePerformance;
- const NOW_TIME = 100;
+ const NOW_TIME = 100;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ clock = sinon.useFakeTimers(NOW_TIME);
+ element = fixture('basic');
+ element._baselines = Object.assign({}, GrReporting.STARTUP_TIMERS);
+ fakePerformance = {
+ navigationStart: 1,
+ loadEventEnd: 2,
+ };
+ fakePerformance.toJSON = () => fakePerformance;
+ sinon.stub(element, 'performanceTiming',
+ {get() { return fakePerformance; }});
+ sandbox.stub(element, 'reporter');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ clock.restore();
+ });
+
+ test('appStarted', () => {
+ sandbox.stub(element, 'now').returns(42);
+ element.appStarted();
+ assert.isTrue(
+ element.reporter.calledWithMatch(
+ 'timing-report', 'UI Latency', 'App Started', 42
+ ));
+ assert.isTrue(
+ element.reporter.calledWithExactly(
+ 'timing-report', 'UI Latency', 'NavResTime - loadEventEnd',
+ fakePerformance.loadEventEnd - fakePerformance.navigationStart,
+ undefined, true)
+ );
+ });
+
+ test('WebComponentsReady', () => {
+ sandbox.stub(element, 'now').returns(42);
+ element.timeEnd('WebComponentsReady');
+ assert.isTrue(element.reporter.calledWithMatch(
+ 'timing-report', 'UI Latency', 'WebComponentsReady', 42
+ ));
+ });
+
+ test('beforeLocationChanged', () => {
+ element._baselines['garbage'] = 'monster';
+ sandbox.stub(element, 'time');
+ element.beforeLocationChanged();
+ assert.isTrue(element.time.calledWithExactly('DashboardDisplayed'));
+ assert.isTrue(element.time.calledWithExactly('ChangeDisplayed'));
+ assert.isTrue(element.time.calledWithExactly('ChangeFullyLoaded'));
+ assert.isTrue(element.time.calledWithExactly('DiffViewDisplayed'));
+ assert.isTrue(element.time.calledWithExactly('FileListDisplayed'));
+ assert.isFalse(element._baselines.hasOwnProperty('garbage'));
+ });
+
+ test('changeDisplayed', () => {
+ sandbox.spy(element, 'timeEnd');
+ element.changeDisplayed();
+ assert.isFalse(
+ element.timeEnd.calledWithExactly('ChangeDisplayed', {rpcList: []}));
+ assert.isTrue(
+ element.timeEnd.calledWithExactly('StartupChangeDisplayed',
+ {rpcList: []}));
+ element.changeDisplayed();
+ assert.isTrue(element.timeEnd.calledWithExactly('ChangeDisplayed',
+ {rpcList: []}));
+ });
+
+ test('changeFullyLoaded', () => {
+ sandbox.spy(element, 'timeEnd');
+ element.changeFullyLoaded();
+ assert.isFalse(
+ element.timeEnd.calledWithExactly('ChangeFullyLoaded'));
+ assert.isTrue(
+ element.timeEnd.calledWithExactly('StartupChangeFullyLoaded'));
+ element.changeFullyLoaded();
+ assert.isTrue(element.timeEnd.calledWithExactly('ChangeFullyLoaded'));
+ });
+
+ test('diffViewDisplayed', () => {
+ sandbox.spy(element, 'timeEnd');
+ element.diffViewDisplayed();
+ assert.isFalse(
+ element.timeEnd.calledWithExactly('DiffViewDisplayed',
+ {rpcList: []}));
+ assert.isTrue(
+ element.timeEnd.calledWithExactly('StartupDiffViewDisplayed',
+ {rpcList: []}));
+ element.diffViewDisplayed();
+ assert.isTrue(element.timeEnd.calledWithExactly('DiffViewDisplayed',
+ {rpcList: []}));
+ });
+
+ test('fileListDisplayed', () => {
+ sandbox.spy(element, 'timeEnd');
+ element.fileListDisplayed();
+ assert.isFalse(
+ element.timeEnd.calledWithExactly('FileListDisplayed'));
+ assert.isTrue(
+ element.timeEnd.calledWithExactly('StartupFileListDisplayed'));
+ element.fileListDisplayed();
+ assert.isTrue(element.timeEnd.calledWithExactly('FileListDisplayed'));
+ });
+
+ test('dashboardDisplayed', () => {
+ sandbox.spy(element, 'timeEnd');
+ element.dashboardDisplayed();
+ assert.isFalse(
+ element.timeEnd.calledWithExactly('DashboardDisplayed',
+ {rpcList: []}));
+ assert.isTrue(
+ element.timeEnd.calledWithExactly('StartupDashboardDisplayed',
+ {rpcList: []}));
+ element.dashboardDisplayed();
+ assert.isTrue(element.timeEnd.calledWithExactly('DashboardDisplayed',
+ {rpcList: []}));
+ });
+
+ test('dashboardDisplayed', () => {
+ sandbox.spy(element, 'timeEnd');
+ element.reportRpcTiming('/changes/*~*/comments', 500);
+ element.dashboardDisplayed();
+ assert.isTrue(
+ element.timeEnd.calledWithExactly('StartupDashboardDisplayed',
+ {rpcList: [
+ {
+ anonymizedUrl: '/changes/*~*/comments',
+ elapsed: 500,
+ },
+ ]}
+ ));
+ });
+
+ test('time and timeEnd', () => {
+ const nowStub = sandbox.stub(element, 'now').returns(0);
+ element.time('foo');
+ nowStub.returns(1);
+ element.time('bar');
+ nowStub.returns(2);
+ element.timeEnd('bar');
+ nowStub.returns(3);
+ element.timeEnd('foo');
+ assert.isTrue(element.reporter.calledWithMatch(
+ 'timing-report', 'UI Latency', 'foo', 3
+ ));
+ assert.isTrue(element.reporter.calledWithMatch(
+ 'timing-report', 'UI Latency', 'bar', 1
+ ));
+ });
+
+ test('timer object', () => {
+ const nowStub = sandbox.stub(element, 'now').returns(100);
+ const timer = element.getTimer('foo-bar');
+ nowStub.returns(150);
+ timer.end();
+ assert.isTrue(element.reporter.calledWithMatch(
+ 'timing-report', 'UI Latency', 'foo-bar', 50));
+ });
+
+ test('timer object double call', () => {
+ const timer = element.getTimer('foo-bar');
+ timer.end();
+ assert.isTrue(element.reporter.calledOnce);
+ assert.throws(() => {
+ timer.end();
+ }, 'Timer for "foo-bar" already ended.');
+ });
+
+ test('timer object maximum', () => {
+ const nowStub = sandbox.stub(element, 'now').returns(100);
+ const timer = element.getTimer('foo-bar').withMaximum(100);
+ nowStub.returns(150);
+ timer.end();
+ assert.isTrue(element.reporter.calledOnce);
+
+ timer.reset();
+ nowStub.returns(260);
+ timer.end();
+ assert.isTrue(element.reporter.calledOnce);
+ });
+
+ test('recordDraftInteraction', () => {
+ const key = 'TimeBetweenDraftActions';
+ const nowStub = sandbox.stub(element, 'now').returns(100);
+ const timingStub = sandbox.stub(element, '_reportTiming');
+ element.recordDraftInteraction();
+ assert.isFalse(timingStub.called);
+
+ nowStub.returns(200);
+ element.recordDraftInteraction();
+ assert.isTrue(timingStub.calledOnce);
+ assert.equal(timingStub.lastCall.args[0], key);
+ assert.equal(timingStub.lastCall.args[1], 100);
+
+ nowStub.returns(350);
+ element.recordDraftInteraction();
+ assert.isTrue(timingStub.calledTwice);
+ assert.equal(timingStub.lastCall.args[0], key);
+ assert.equal(timingStub.lastCall.args[1], 150);
+
+ nowStub.returns(370 + 2 * 60 * 1000);
+ element.recordDraftInteraction();
+ assert.isFalse(timingStub.calledThrice);
+ });
+
+ test('timeEndWithAverage', () => {
+ const nowStub = sandbox.stub(element, 'now').returns(0);
+ nowStub.returns(1000);
+ element.time('foo');
+ nowStub.returns(1100);
+ element.timeEndWithAverage('foo', 'bar', 10);
+ assert.isTrue(element.reporter.calledTwice);
+ assert.isTrue(element.reporter.calledWithMatch(
+ 'timing-report', 'UI Latency', 'foo', 100));
+ assert.isTrue(element.reporter.calledWithMatch(
+ 'timing-report', 'UI Latency', 'bar', 10));
+ });
+
+ test('reportExtension', () => {
+ element.reportExtension('foo');
+ assert.isTrue(element.reporter.calledWithExactly(
+ 'lifecycle', 'Extension detected', 'foo'
+ ));
+ });
+
+ test('reportInteraction', () => {
+ element.reporter.restore();
+ sandbox.spy(element, '_reportEvent');
+ element.pluginsLoaded(); // so we don't cache
+ element.reportInteraction('button-click', {name: 'sendReply'});
+ assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
+ {
+ type: 'interaction',
+ name: 'button-click',
+ eventDetails: JSON.stringify({name: 'sendReply'}),
+ }
+ ));
+ });
+
+ test('report start time', () => {
+ element.reporter.restore();
+ sandbox.stub(element, 'now').returns(42);
+ sandbox.spy(element, '_reportEvent');
+ const dispatchStub = sandbox.spy(document, 'dispatchEvent');
+ element.pluginsLoaded();
+ element.time('timeAction');
+ element.timeEnd('timeAction');
+ assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
+ {
+ type: 'timing-report',
+ category: 'UI Latency',
+ name: 'timeAction',
+ value: 0,
+ eventStart: 42,
+ }
+ ));
+ assert.equal(dispatchStub.getCall(2).args[0].detail.eventStart, 42);
+ });
+
+ suite('plugins', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
- clock = sinon.useFakeTimers(NOW_TIME);
- element = fixture('basic');
- element._baselines = Object.assign({}, GrReporting.STARTUP_TIMERS);
- fakePerformance = {
- navigationStart: 1,
- loadEventEnd: 2,
- };
- fakePerformance.toJSON = () => fakePerformance;
- sinon.stub(element, 'performanceTiming',
- {get() { return fakePerformance; }});
- sandbox.stub(element, 'reporter');
- });
-
- teardown(() => {
- sandbox.restore();
- clock.restore();
- });
-
- test('appStarted', () => {
- sandbox.stub(element, 'now').returns(42);
- element.appStarted();
- assert.isTrue(
- element.reporter.calledWithMatch(
- 'timing-report', 'UI Latency', 'App Started', 42
- ));
- });
-
- test('WebComponentsReady', () => {
- sandbox.stub(element, 'now').returns(42);
- element.timeEnd('WebComponentsReady');
- assert.isTrue(element.reporter.calledWithMatch(
- 'timing-report', 'UI Latency', 'WebComponentsReady', 42
- ));
- });
-
- test('pageLoaded', () => {
- element.pageLoaded();
- assert.isTrue(
- element.reporter.calledWithExactly(
- 'timing-report', 'UI Latency', 'NavResTime - loadEventEnd',
- fakePerformance.loadEventEnd - fakePerformance.navigationStart,
- undefined, true)
- );
- });
-
- test('beforeLocationChanged', () => {
- element._baselines['garbage'] = 'monster';
- sandbox.stub(element, 'time');
- element.beforeLocationChanged();
- assert.isTrue(element.time.calledWithExactly('DashboardDisplayed'));
- assert.isTrue(element.time.calledWithExactly('ChangeDisplayed'));
- assert.isTrue(element.time.calledWithExactly('ChangeFullyLoaded'));
- assert.isTrue(element.time.calledWithExactly('DiffViewDisplayed'));
- assert.isTrue(element.time.calledWithExactly('FileListDisplayed'));
- assert.isFalse(element._baselines.hasOwnProperty('garbage'));
- });
-
- test('changeDisplayed', () => {
- sandbox.spy(element, 'timeEnd');
- element.changeDisplayed();
- assert.isFalse(
- element.timeEnd.calledWithExactly('ChangeDisplayed', {rpcList: []}));
- assert.isTrue(
- element.timeEnd.calledWithExactly('StartupChangeDisplayed',
- {rpcList: []}));
- element.changeDisplayed();
- assert.isTrue(element.timeEnd.calledWithExactly('ChangeDisplayed',
- {rpcList: []}));
- });
-
- test('changeFullyLoaded', () => {
- sandbox.spy(element, 'timeEnd');
- element.changeFullyLoaded();
- assert.isFalse(
- element.timeEnd.calledWithExactly('ChangeFullyLoaded'));
- assert.isTrue(
- element.timeEnd.calledWithExactly('StartupChangeFullyLoaded'));
- element.changeFullyLoaded();
- assert.isTrue(element.timeEnd.calledWithExactly('ChangeFullyLoaded'));
- });
-
- test('diffViewDisplayed', () => {
- sandbox.spy(element, 'timeEnd');
- element.diffViewDisplayed();
- assert.isFalse(
- element.timeEnd.calledWithExactly('DiffViewDisplayed',
- {rpcList: []}));
- assert.isTrue(
- element.timeEnd.calledWithExactly('StartupDiffViewDisplayed',
- {rpcList: []}));
- element.diffViewDisplayed();
- assert.isTrue(element.timeEnd.calledWithExactly('DiffViewDisplayed',
- {rpcList: []}));
- });
-
- test('fileListDisplayed', () => {
- sandbox.spy(element, 'timeEnd');
- element.fileListDisplayed();
- assert.isFalse(
- element.timeEnd.calledWithExactly('FileListDisplayed'));
- assert.isTrue(
- element.timeEnd.calledWithExactly('StartupFileListDisplayed'));
- element.fileListDisplayed();
- assert.isTrue(element.timeEnd.calledWithExactly('FileListDisplayed'));
- });
-
- test('dashboardDisplayed', () => {
- sandbox.spy(element, 'timeEnd');
- element.dashboardDisplayed();
- assert.isFalse(
- element.timeEnd.calledWithExactly('DashboardDisplayed',
- {rpcList: []}));
- assert.isTrue(
- element.timeEnd.calledWithExactly('StartupDashboardDisplayed',
- {rpcList: []}));
- element.dashboardDisplayed();
- assert.isTrue(element.timeEnd.calledWithExactly('DashboardDisplayed',
- {rpcList: []}));
- });
-
- test('dashboardDisplayed', () => {
- sandbox.spy(element, 'timeEnd');
- element.reportRpcTiming('/changes/*~*/comments', 500);
- element.dashboardDisplayed();
- assert.isTrue(
- element.timeEnd.calledWithExactly('StartupDashboardDisplayed',
- {rpcList: [
- {
- anonymizedUrl: '/changes/*~*/comments',
- elapsed: 500,
- },
- ]}
- ));
- });
-
- test('time and timeEnd', () => {
- const nowStub = sandbox.stub(element, 'now').returns(0);
- element.time('foo');
- nowStub.returns(1);
- element.time('bar');
- nowStub.returns(2);
- element.timeEnd('bar');
- nowStub.returns(3);
- element.timeEnd('foo');
- assert.isTrue(element.reporter.calledWithMatch(
- 'timing-report', 'UI Latency', 'foo', 3
- ));
- assert.isTrue(element.reporter.calledWithMatch(
- 'timing-report', 'UI Latency', 'bar', 1
- ));
- });
-
- test('timer object', () => {
- const nowStub = sandbox.stub(element, 'now').returns(100);
- const timer = element.getTimer('foo-bar');
- nowStub.returns(150);
- timer.end();
- assert.isTrue(element.reporter.calledWithMatch(
- 'timing-report', 'UI Latency', 'foo-bar', 50));
- });
-
- test('timer object double call', () => {
- const timer = element.getTimer('foo-bar');
- timer.end();
- assert.isTrue(element.reporter.calledOnce);
- assert.throws(() => {
- timer.end();
- }, 'Timer for "foo-bar" already ended.');
- });
-
- test('timer object maximum', () => {
- const nowStub = sandbox.stub(element, 'now').returns(100);
- const timer = element.getTimer('foo-bar').withMaximum(100);
- nowStub.returns(150);
- timer.end();
- assert.isTrue(element.reporter.calledOnce);
-
- timer.reset();
- nowStub.returns(260);
- timer.end();
- assert.isTrue(element.reporter.calledOnce);
- });
-
- test('recordDraftInteraction', () => {
- const key = 'TimeBetweenDraftActions';
- const nowStub = sandbox.stub(element, 'now').returns(100);
- const timingStub = sandbox.stub(element, '_reportTiming');
- element.recordDraftInteraction();
- assert.isFalse(timingStub.called);
-
- nowStub.returns(200);
- element.recordDraftInteraction();
- assert.isTrue(timingStub.calledOnce);
- assert.equal(timingStub.lastCall.args[0], key);
- assert.equal(timingStub.lastCall.args[1], 100);
-
- nowStub.returns(350);
- element.recordDraftInteraction();
- assert.isTrue(timingStub.calledTwice);
- assert.equal(timingStub.lastCall.args[0], key);
- assert.equal(timingStub.lastCall.args[1], 150);
-
- nowStub.returns(370 + 2 * 60 * 1000);
- element.recordDraftInteraction();
- assert.isFalse(timingStub.calledThrice);
- });
-
- test('timeEndWithAverage', () => {
- const nowStub = sandbox.stub(element, 'now').returns(0);
- nowStub.returns(1000);
- element.time('foo');
- nowStub.returns(1100);
- element.timeEndWithAverage('foo', 'bar', 10);
- assert.isTrue(element.reporter.calledTwice);
- assert.isTrue(element.reporter.calledWithMatch(
- 'timing-report', 'UI Latency', 'foo', 100));
- assert.isTrue(element.reporter.calledWithMatch(
- 'timing-report', 'UI Latency', 'bar', 10));
- });
-
- test('reportExtension', () => {
- element.reportExtension('foo');
- assert.isTrue(element.reporter.calledWithExactly(
- 'lifecycle', 'Extension detected', 'foo'
- ));
- });
-
- test('reportInteraction', () => {
element.reporter.restore();
- sandbox.spy(element, '_reportEvent');
- element.pluginsLoaded(); // so we don't cache
- element.reportInteraction('button-click', {name: 'sendReply'});
- assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
- {
- type: 'interaction',
- name: 'button-click',
- eventDetails: JSON.stringify({name: 'sendReply'}),
- }
- ));
+ sandbox.stub(element, '_reportEvent');
});
- test('report start time', () => {
- element.reporter.restore();
+ test('pluginsLoaded reports time', () => {
sandbox.stub(element, 'now').returns(42);
- sandbox.spy(element, '_reportEvent');
- const dispatchStub = sandbox.spy(document, 'dispatchEvent');
element.pluginsLoaded();
- element.time('timeAction');
- element.timeEnd('timeAction');
- assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
+ assert.isTrue(element._reportEvent.calledWithMatch(
{
type: 'timing-report',
category: 'UI Latency',
- name: 'timeAction',
- value: 0,
- eventStart: 42,
+ name: 'PluginsLoaded',
+ value: 42,
}
));
- assert.equal(dispatchStub.getCall(2).args[0].detail.eventStart, 42);
});
- suite('plugins', () => {
- setup(() => {
- element.reporter.restore();
- sandbox.stub(element, '_reportEvent');
- });
-
- test('pluginsLoaded reports time', () => {
- sandbox.stub(element, 'now').returns(42);
- element.pluginsLoaded();
- assert.isTrue(element._reportEvent.calledWithMatch(
- {
- type: 'timing-report',
- category: 'UI Latency',
- name: 'PluginsLoaded',
- value: 42,
- }
- ));
- });
-
- test('pluginsLoaded reports plugins', () => {
- element.pluginsLoaded(['foo', 'bar']);
- assert.isTrue(element._reportEvent.calledWithMatch(
- {
- type: 'lifecycle',
- category: 'Plugins installed',
- eventDetails: JSON.stringify({pluginsList: ['foo', 'bar']}),
- }
- ));
- });
-
- test('caches reports if plugins are not loaded', () => {
- element.timeEnd('foo');
- assert.isFalse(element._reportEvent.called);
- });
-
- test('reports if plugins are loaded', () => {
- element.pluginsLoaded();
- assert.isTrue(element._reportEvent.called);
- });
-
- test('reports if metrics plugin xyz is loaded', () => {
- element.pluginLoaded('metrics-xyz');
- assert.isTrue(element._reportEvent.called);
- });
-
- test('reports cached events preserving order', () => {
- element.time('foo');
- element.time('bar');
- element.timeEnd('foo');
- element.pluginsLoaded();
- element.timeEnd('bar');
- assert.isTrue(element._reportEvent.getCall(0).calledWithMatch(
- {type: 'timing-report', category: 'UI Latency', name: 'foo'}
- ));
- assert.isTrue(element._reportEvent.getCall(1).calledWithMatch(
- {type: 'timing-report', category: 'UI Latency',
- name: 'PluginsLoaded'}
- ));
- assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
- {type: 'lifecycle', category: 'Plugins installed'}
- ));
- assert.isTrue(element._reportEvent.getCall(3).calledWithMatch(
- {type: 'timing-report', category: 'UI Latency', name: 'bar'}
- ));
- });
+ test('pluginsLoaded reports plugins', () => {
+ element.pluginsLoaded(['foo', 'bar']);
+ assert.isTrue(element._reportEvent.calledWithMatch(
+ {
+ type: 'lifecycle',
+ category: 'Plugins installed',
+ eventDetails: JSON.stringify({pluginsList: ['foo', 'bar']}),
+ }
+ ));
});
- test('search', () => {
- element.locationChanged('_handleSomeRoute');
- assert.isTrue(element.reporter.calledWithExactly(
- 'nav-report', 'Location Changed', 'Page', '_handleSomeRoute'));
+ test('caches reports if plugins are not loaded', () => {
+ element.timeEnd('foo');
+ assert.isFalse(element._reportEvent.called);
});
- suite('exception logging', () => {
- let fakeWindow;
- let reporter;
+ test('reports if plugins are loaded', () => {
+ element.pluginsLoaded();
+ assert.isTrue(element._reportEvent.called);
+ });
- const emulateThrow = function(msg, url, line, column, error) {
- return fakeWindow.onerror(msg, url, line, column, error);
- };
+ test('reports if metrics plugin xyz is loaded', () => {
+ element.pluginLoaded('metrics-xyz');
+ assert.isTrue(element._reportEvent.called);
+ });
- setup(() => {
- reporter = sandbox.stub(GrReporting.prototype, 'reporter');
- fakeWindow = {
- handlers: {},
- addEventListener(type, handler) {
- this.handlers[type] = handler;
- },
- };
- sandbox.stub(console, 'error');
- window.GrReporting._catchErrors(fakeWindow);
- });
-
- test('is reported', () => {
- const error = new Error('bar');
- error.stack = undefined;
- emulateThrow('bar', 'http://url', 4, 2, error);
- assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
- const payload = reporter.lastCall.args[3];
- assert.deepEqual(payload, {
- url: 'http://url',
- line: 4,
- column: 2,
- error,
- });
- });
-
- test('is reported with 3 lines of stack', () => {
- const error = new Error('bar');
- emulateThrow('bar', 'http://url', 4, 2, error);
- const expectedStack = error.stack.split('\n').slice(0, 3)
- .join('\n');
- assert.isTrue(reporter.calledWith('error', 'exception',
- expectedStack));
- });
-
- test('prevent default event handler', () => {
- assert.isTrue(emulateThrow());
- });
-
- test('unhandled rejection', () => {
- fakeWindow.handlers['unhandledrejection']({
- reason: {
- message: 'bar',
- },
- });
- assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
- });
+ test('reports cached events preserving order', () => {
+ element.time('foo');
+ element.time('bar');
+ element.timeEnd('foo');
+ element.pluginsLoaded();
+ element.timeEnd('bar');
+ assert.isTrue(element._reportEvent.getCall(0).calledWithMatch(
+ {type: 'timing-report', category: 'UI Latency', name: 'foo'}
+ ));
+ assert.isTrue(element._reportEvent.getCall(1).calledWithMatch(
+ {type: 'timing-report', category: 'UI Latency',
+ name: 'PluginsLoaded'}
+ ));
+ assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
+ {type: 'lifecycle', category: 'Plugins installed'}
+ ));
+ assert.isTrue(element._reportEvent.getCall(3).calledWithMatch(
+ {type: 'timing-report', category: 'UI Latency', name: 'bar'}
+ ));
});
});
+
+ test('search', () => {
+ element.locationChanged('_handleSomeRoute');
+ assert.isTrue(element.reporter.calledWithExactly(
+ 'nav-report', 'Location Changed', 'Page', '_handleSomeRoute'));
+ });
+
+ suite('exception logging', () => {
+ let fakeWindow;
+ let reporter;
+
+ const emulateThrow = function(msg, url, line, column, error) {
+ return fakeWindow.onerror(msg, url, line, column, error);
+ };
+
+ setup(() => {
+ reporter = sandbox.stub(GrReporting.prototype, 'reporter');
+ fakeWindow = {
+ handlers: {},
+ addEventListener(type, handler) {
+ this.handlers[type] = handler;
+ },
+ };
+ sandbox.stub(console, 'error');
+ window.GrReporting._catchErrors(fakeWindow);
+ });
+
+ test('is reported', () => {
+ const error = new Error('bar');
+ error.stack = undefined;
+ emulateThrow('bar', 'http://url', 4, 2, error);
+ assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
+ const payload = reporter.lastCall.args[3];
+ assert.deepEqual(payload, {
+ url: 'http://url',
+ line: 4,
+ column: 2,
+ error,
+ });
+ });
+
+ test('is reported with 3 lines of stack', () => {
+ const error = new Error('bar');
+ emulateThrow('bar', 'http://url', 4, 2, error);
+ const expectedStack = error.stack.split('\n').slice(0, 3)
+ .join('\n');
+ assert.isTrue(reporter.calledWith('error', 'exception',
+ expectedStack));
+ });
+
+ test('prevent default event handler', () => {
+ assert.isTrue(emulateThrow());
+ });
+
+ test('unhandled rejection', () => {
+ fakeWindow.handlers['unhandledrejection']({
+ reason: {
+ message: 'bar',
+ },
+ });
+ assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.html b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
deleted file mode 100644
index 71a5832..0000000
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.html
+++ /dev/null
@@ -1,34 +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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-reporting/gr-reporting.html">
-
-<dom-module id="gr-router">
- <template>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- <gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="/bower_components/page/page.js"></script>
- <script src="gr-router.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index ebac1e1..e461d1d 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -14,1520 +14,1535 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const RoutePattern = {
- ROOT: '/',
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../gr-navigation/gr-navigation.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-reporting/gr-reporting.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import page from 'page/page.mjs';
+self.page = page;
+import {htmlTemplate} from './gr-router_html.js';
- DASHBOARD: /^\/dashboard\/(.+)$/,
- CUSTOM_DASHBOARD: /^\/dashboard\/?$/,
- PROJECT_DASHBOARD: /^\/p\/(.+)\/\+\/dashboard\/(.+)/,
+const RoutePattern = {
+ ROOT: '/',
- AGREEMENTS: /^\/settings\/agreements\/?/,
- NEW_AGREEMENTS: /^\/settings\/new-agreement\/?/,
- REGISTER: /^\/register(\/.*)?$/,
+ DASHBOARD: /^\/dashboard\/(.+)$/,
+ CUSTOM_DASHBOARD: /^\/dashboard\/?$/,
+ PROJECT_DASHBOARD: /^\/p\/(.+)\/\+\/dashboard\/(.+)/,
- // Pattern for login and logout URLs intended to be passed-through. May
- // include a return URL.
- LOG_IN_OR_OUT: /\/log(in|out)(\/(.+))?$/,
+ AGREEMENTS: /^\/settings\/agreements\/?/,
+ NEW_AGREEMENTS: /^\/settings\/new-agreement\/?/,
+ REGISTER: /^\/register(\/.*)?$/,
- // Pattern for a catchall route when no other pattern is matched.
- DEFAULT: /.*/,
+ // Pattern for login and logout URLs intended to be passed-through. May
+ // include a return URL.
+ LOG_IN_OR_OUT: /\/log(in|out)(\/(.+))?$/,
- // Matches /admin/groups/[uuid-]<group>
- GROUP: /^\/admin\/groups\/(?:uuid-)?([^,]+)$/,
+ // Pattern for a catchall route when no other pattern is matched.
+ DEFAULT: /.*/,
- // Redirects /groups/self to /settings/#Groups for GWT compatibility
- GROUP_SELF: /^\/groups\/self/,
+ // Matches /admin/groups/[uuid-]<group>
+ GROUP: /^\/admin\/groups\/(?:uuid-)?([^,]+)$/,
- // Matches /admin/groups/[uuid-]<group>,info (backwords compat with gwtui)
- // Redirects to /admin/groups/[uuid-]<group>
- GROUP_INFO: /^\/admin\/groups\/(?:uuid-)?(.+),info$/,
+ // Redirects /groups/self to /settings/#Groups for GWT compatibility
+ GROUP_SELF: /^\/groups\/self/,
- // Matches /admin/groups/<group>,audit-log
- GROUP_AUDIT_LOG: /^\/admin\/groups\/(?:uuid-)?(.+),audit-log$/,
+ // Matches /admin/groups/[uuid-]<group>,info (backwords compat with gwtui)
+ // Redirects to /admin/groups/[uuid-]<group>
+ GROUP_INFO: /^\/admin\/groups\/(?:uuid-)?(.+),info$/,
- // Matches /admin/groups/[uuid-]<group>,members
- GROUP_MEMBERS: /^\/admin\/groups\/(?:uuid-)?(.+),members$/,
+ // Matches /admin/groups/<group>,audit-log
+ GROUP_AUDIT_LOG: /^\/admin\/groups\/(?:uuid-)?(.+),audit-log$/,
- // Matches /admin/groups[,<offset>][/].
- GROUP_LIST_OFFSET: /^\/admin\/groups(,(\d+))?(\/)?$/,
- GROUP_LIST_FILTER: '/admin/groups/q/filter::filter',
- GROUP_LIST_FILTER_OFFSET: '/admin/groups/q/filter::filter,:offset',
+ // Matches /admin/groups/[uuid-]<group>,members
+ GROUP_MEMBERS: /^\/admin\/groups\/(?:uuid-)?(.+),members$/,
- // Matches /admin/create-project
- LEGACY_CREATE_PROJECT: /^\/admin\/create-project\/?$/,
+ // Matches /admin/groups[,<offset>][/].
+ GROUP_LIST_OFFSET: /^\/admin\/groups(,(\d+))?(\/)?$/,
+ GROUP_LIST_FILTER: '/admin/groups/q/filter::filter',
+ GROUP_LIST_FILTER_OFFSET: '/admin/groups/q/filter::filter,:offset',
- // Matches /admin/create-project
- LEGACY_CREATE_GROUP: /^\/admin\/create-group\/?$/,
+ // Matches /admin/create-project
+ LEGACY_CREATE_PROJECT: /^\/admin\/create-project\/?$/,
- PROJECT_OLD: /^\/admin\/(projects)\/?(.+)?$/,
+ // Matches /admin/create-project
+ LEGACY_CREATE_GROUP: /^\/admin\/create-group\/?$/,
- // Matches /admin/repos/<repo>
- REPO: /^\/admin\/repos\/([^,]+)$/,
+ PROJECT_OLD: /^\/admin\/(projects)\/?(.+)?$/,
- // Matches /admin/repos/<repo>,commands.
- REPO_COMMANDS: /^\/admin\/repos\/(.+),commands$/,
+ // Matches /admin/repos/<repo>
+ REPO: /^\/admin\/repos\/([^,]+)$/,
- // Matches /admin/repos/<repos>,access.
- REPO_ACCESS: /^\/admin\/repos\/(.+),access$/,
+ // Matches /admin/repos/<repo>,commands.
+ REPO_COMMANDS: /^\/admin\/repos\/(.+),commands$/,
- // Matches /admin/repos/<repos>,access.
- REPO_DASHBOARDS: /^\/admin\/repos\/(.+),dashboards$/,
+ // Matches /admin/repos/<repos>,access.
+ REPO_ACCESS: /^\/admin\/repos\/(.+),access$/,
- // Matches /admin/repos[,<offset>][/].
- REPO_LIST_OFFSET: /^\/admin\/repos(,(\d+))?(\/)?$/,
- REPO_LIST_FILTER: '/admin/repos/q/filter::filter',
- REPO_LIST_FILTER_OFFSET: '/admin/repos/q/filter::filter,:offset',
+ // Matches /admin/repos/<repos>,access.
+ REPO_DASHBOARDS: /^\/admin\/repos\/(.+),dashboards$/,
- // Matches /admin/repos/<repo>,branches[,<offset>].
- BRANCH_LIST_OFFSET: /^\/admin\/repos\/(.+),branches(,(.+))?$/,
- BRANCH_LIST_FILTER: '/admin/repos/:repo,branches/q/filter::filter',
- BRANCH_LIST_FILTER_OFFSET:
- '/admin/repos/:repo,branches/q/filter::filter,:offset',
+ // Matches /admin/repos[,<offset>][/].
+ REPO_LIST_OFFSET: /^\/admin\/repos(,(\d+))?(\/)?$/,
+ REPO_LIST_FILTER: '/admin/repos/q/filter::filter',
+ REPO_LIST_FILTER_OFFSET: '/admin/repos/q/filter::filter,:offset',
- // Matches /admin/repos/<repo>,tags[,<offset>].
- TAG_LIST_OFFSET: /^\/admin\/repos\/(.+),tags(,(.+))?$/,
- TAG_LIST_FILTER: '/admin/repos/:repo,tags/q/filter::filter',
- TAG_LIST_FILTER_OFFSET:
- '/admin/repos/:repo,tags/q/filter::filter,:offset',
+ // Matches /admin/repos/<repo>,branches[,<offset>].
+ BRANCH_LIST_OFFSET: /^\/admin\/repos\/(.+),branches(,(.+))?$/,
+ BRANCH_LIST_FILTER: '/admin/repos/:repo,branches/q/filter::filter',
+ BRANCH_LIST_FILTER_OFFSET:
+ '/admin/repos/:repo,branches/q/filter::filter,:offset',
- PLUGINS: /^\/plugins\/(.+)$/,
+ // Matches /admin/repos/<repo>,tags[,<offset>].
+ TAG_LIST_OFFSET: /^\/admin\/repos\/(.+),tags(,(.+))?$/,
+ TAG_LIST_FILTER: '/admin/repos/:repo,tags/q/filter::filter',
+ TAG_LIST_FILTER_OFFSET:
+ '/admin/repos/:repo,tags/q/filter::filter,:offset',
- PLUGIN_LIST: /^\/admin\/plugins(\/)?$/,
+ PLUGINS: /^\/plugins\/(.+)$/,
- // Matches /admin/plugins[,<offset>][/].
- PLUGIN_LIST_OFFSET: /^\/admin\/plugins(,(\d+))?(\/)?$/,
- PLUGIN_LIST_FILTER: '/admin/plugins/q/filter::filter',
- PLUGIN_LIST_FILTER_OFFSET: '/admin/plugins/q/filter::filter,:offset',
+ PLUGIN_LIST: /^\/admin\/plugins(\/)?$/,
- QUERY: /^\/q\/([^,]+)(,(\d+))?$/,
+ // Matches /admin/plugins[,<offset>][/].
+ PLUGIN_LIST_OFFSET: /^\/admin\/plugins(,(\d+))?(\/)?$/,
+ PLUGIN_LIST_FILTER: '/admin/plugins/q/filter::filter',
+ PLUGIN_LIST_FILTER_OFFSET: '/admin/plugins/q/filter::filter,:offset',
- /**
- * Support vestigial params from GWT UI.
- *
- * @see Issue 7673.
- * @type {!RegExp}
- */
- QUERY_LEGACY_SUFFIX: /^\/q\/.+,n,z$/,
-
- // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/].
- CHANGE_LEGACY: /^\/c\/(\d+)\/?(((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
- CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/,
-
- // Matches
- // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..][<patchNum|edit>].
- // TODO(kaspern): Migrate completely to project based URLs, with backwards
- // compatibility for change-only.
- CHANGE: /^\/c\/(.+)\/\+\/(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
-
- // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>],edit
- CHANGE_EDIT: /^\/c\/(.+)\/\+\/(\d+)(\/(\d+))?,edit\/?$/,
-
- // Matches
- // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..]<patchNum|edit>/<path>.
- // TODO(kaspern): Migrate completely to project based URLs, with backwards
- // compatibility for change-only.
- // eslint-disable-next-line max-len
- DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))))\/?$/,
-
- // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit[#lineNum]
- DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(\#\d+)?$/,
-
- // Matches non-project-relative
- // /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>.
- DIFF_LEGACY: /^\/c\/(\d+)\/((-?\d+|edit)(\.\.(\d+|edit))?)\/(.+)/,
-
- // Matches diff routes using @\d+ to specify a file name (whether or not
- // the project name is included).
- // eslint-disable-next-line max-len
- DIFF_LEGACY_LINENUM: /^\/c\/((.+)\/\+\/)?(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?\/(.+))?)@[ab]?\d+$/,
-
- SETTINGS: /^\/settings\/?/,
- SETTINGS_LEGACY: /^\/settings\/VE\/(\S+)/,
-
- // Matches /c/<changeNum>/ /<URL tail>
- // Catches improperly encoded URLs (context: Issue 7100)
- IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/\ \/(.+)$/,
-
- PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/,
-
- DOCUMENTATION_SEARCH_FILTER: '/Documentation/q/filter::filter',
- DOCUMENTATION_SEARCH: /^\/Documentation\/q\/(.*)$/,
- DOCUMENTATION: /^\/Documentation(\/)?(.+)?/,
- };
+ QUERY: /^\/q\/([^,]+)(,(\d+))?$/,
/**
- * Pattern to recognize and parse the diff line locations as they appear in
- * the hash of diff URLs. In this format, a number on its own indicates that
- * line number in the revision of the diff. A number prefixed by either an 'a'
- * or a 'b' indicates that line number of the base of the diff.
+ * Support vestigial params from GWT UI.
*
- * @type {RegExp}
+ * @see Issue 7673.
+ * @type {!RegExp}
*/
- const LINE_ADDRESS_PATTERN = /^([ab]?)(\d+)$/;
+ QUERY_LEGACY_SUFFIX: /^\/q\/.+,n,z$/,
- /**
- * Pattern to recognize '+' in url-encoded strings for replacement with ' '.
- */
- const PLUS_PATTERN = /\+/g;
+ // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/].
+ CHANGE_LEGACY: /^\/c\/(\d+)\/?(((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
+ CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/,
- /**
- * Pattern to recognize leading '?' in window.location.search, for stripping.
- */
- const QUESTION_PATTERN = /^\?*/;
+ // Matches
+ // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..][<patchNum|edit>].
+ // TODO(kaspern): Migrate completely to project based URLs, with backwards
+ // compatibility for change-only.
+ CHANGE: /^\/c\/(.+)\/\+\/(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
- /**
- * GWT UI would use @\d+ at the end of a path to indicate linenum.
- */
- const LEGACY_LINENUM_PATTERN = /@([ab]?\d+)$/;
+ // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>],edit
+ CHANGE_EDIT: /^\/c\/(.+)\/\+\/(\d+)(\/(\d+))?,edit\/?$/,
- const LEGACY_QUERY_SUFFIX_PATTERN = /,n,z$/;
+ // Matches
+ // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..]<patchNum|edit>/<path>.
+ // TODO(kaspern): Migrate completely to project based URLs, with backwards
+ // compatibility for change-only.
+ // eslint-disable-next-line max-len
+ DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))))\/?$/,
- const REPO_TOKEN_PATTERN = /\$\{(project|repo)\}/g;
+ // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit[#lineNum]
+ DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(\#\d+)?$/,
- // Polymer makes `app` intrinsically defined on the window by virtue of the
- // custom element having the id "app", but it is made explicit here.
- const app = document.querySelector('#app');
- if (!app) {
- console.log('No gr-app found (running tests)');
+ // Matches non-project-relative
+ // /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>.
+ DIFF_LEGACY: /^\/c\/(\d+)\/((-?\d+|edit)(\.\.(\d+|edit))?)\/(.+)/,
+
+ // Matches diff routes using @\d+ to specify a file name (whether or not
+ // the project name is included).
+ // eslint-disable-next-line max-len
+ DIFF_LEGACY_LINENUM: /^\/c\/((.+)\/\+\/)?(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?\/(.+))?)@[ab]?\d+$/,
+
+ SETTINGS: /^\/settings\/?/,
+ SETTINGS_LEGACY: /^\/settings\/VE\/(\S+)/,
+
+ // Matches /c/<changeNum>/ /<URL tail>
+ // Catches improperly encoded URLs (context: Issue 7100)
+ IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/\ \/(.+)$/,
+
+ PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/,
+
+ DOCUMENTATION_SEARCH_FILTER: '/Documentation/q/filter::filter',
+ DOCUMENTATION_SEARCH: /^\/Documentation\/q\/(.*)$/,
+ DOCUMENTATION: /^\/Documentation(\/)?(.+)?/,
+};
+
+/**
+ * Pattern to recognize and parse the diff line locations as they appear in
+ * the hash of diff URLs. In this format, a number on its own indicates that
+ * line number in the revision of the diff. A number prefixed by either an 'a'
+ * or a 'b' indicates that line number of the base of the diff.
+ *
+ * @type {RegExp}
+ */
+const LINE_ADDRESS_PATTERN = /^([ab]?)(\d+)$/;
+
+/**
+ * Pattern to recognize '+' in url-encoded strings for replacement with ' '.
+ */
+const PLUS_PATTERN = /\+/g;
+
+/**
+ * Pattern to recognize leading '?' in window.location.search, for stripping.
+ */
+const QUESTION_PATTERN = /^\?*/;
+
+/**
+ * GWT UI would use @\d+ at the end of a path to indicate linenum.
+ */
+const LEGACY_LINENUM_PATTERN = /@([ab]?\d+)$/;
+
+const LEGACY_QUERY_SUFFIX_PATTERN = /,n,z$/;
+
+const REPO_TOKEN_PATTERN = /\$\{(project|repo)\}/g;
+
+// Polymer makes `app` intrinsically defined on the window by virtue of the
+// custom element having the id "app", but it is made explicit here.
+const app = document.querySelector('#app');
+if (!app) {
+ console.log('No gr-app found (running tests)');
+}
+
+// Setup listeners outside of the router component initialization.
+(function() {
+ const reporting = document.createElement('gr-reporting');
+
+ window.addEventListener('WebComponentsReady', () => {
+ reporting.timeEnd('WebComponentsReady');
+ });
+})();
+
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrRouter extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.FireBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-router'; }
+
+ static get properties() {
+ return {
+ _app: {
+ type: Object,
+ value: app,
+ },
+ _isRedirecting: Boolean,
+ // This variable is to differentiate between internal navigation (false)
+ // and for first navigation in app after loaded from server (true).
+ _isInitialLoad: {
+ type: Boolean,
+ value: true,
+ },
+ };
}
- // Setup listeners outside of the router component initialization.
- (function() {
- const reporting = document.createElement('gr-reporting');
+ start() {
+ if (!this._app) { return; }
+ this._startRouter();
+ }
- window.addEventListener('WebComponentsReady', () => {
- reporting.timeEnd('WebComponentsReady');
- });
- })();
+ _setParams(params) {
+ this._appElement().params = params;
+ }
+
+ _appElement() {
+ // In Polymer2 you have to reach through the shadow root of the app
+ // element. This obviously breaks encapsulation.
+ // TODO(brohlfs): Make this more elegant, e.g. by exposing app-element
+ // explicitly in app, or by delegating to it.
+ return document.getElementById('app-element') ||
+ document.getElementById('app').shadowRoot.getElementById(
+ 'app-element');
+ }
+
+ _redirect(url) {
+ this._isRedirecting = true;
+ page.redirect(url);
+ }
/**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
+ * @param {!Object} params
+ * @return {string}
*/
- class GrRouter extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.FireBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-router'; }
+ _generateUrl(params) {
+ const base = this.getBaseUrl();
+ let url = '';
+ const Views = Gerrit.Nav.View;
- static get properties() {
- return {
- _app: {
- type: Object,
- value: app,
- },
- _isRedirecting: Boolean,
- // This variable is to differentiate between internal navigation (false)
- // and for first navigation in app after loaded from server (true).
- _isInitialLoad: {
- type: Boolean,
- value: true,
- },
- };
+ if (params.view === Views.SEARCH) {
+ url = this._generateSearchUrl(params);
+ } else if (params.view === Views.CHANGE) {
+ url = this._generateChangeUrl(params);
+ } else if (params.view === Views.DASHBOARD) {
+ url = this._generateDashboardUrl(params);
+ } else if (params.view === Views.DIFF || params.view === Views.EDIT) {
+ url = this._generateDiffOrEditUrl(params);
+ } else if (params.view === Views.GROUP) {
+ url = this._generateGroupUrl(params);
+ } else if (params.view === Views.REPO) {
+ url = this._generateRepoUrl(params);
+ } else if (params.view === Views.ROOT) {
+ url = '/';
+ } else if (params.view === Views.SETTINGS) {
+ url = this._generateSettingsUrl(params);
+ } else {
+ throw new Error('Can\'t generate');
}
- start() {
- if (!this._app) { return; }
- this._startRouter();
+ return base + url;
+ }
+
+ _generateWeblinks(params) {
+ const type = params.type;
+ switch (type) {
+ case Gerrit.Nav.WeblinkType.FILE:
+ return this._getFileWebLinks(params);
+ case Gerrit.Nav.WeblinkType.CHANGE:
+ return this._getChangeWeblinks(params);
+ case Gerrit.Nav.WeblinkType.PATCHSET:
+ return this._getPatchSetWeblink(params);
+ default:
+ console.warn(`Unsupported weblink ${type}!`);
+ }
+ }
+
+ _getPatchSetWeblink(params) {
+ const {commit, options} = params;
+ const {weblinks, config} = options || {};
+ const name = commit && commit.slice(0, 7);
+ const weblink = this._getBrowseCommitWeblink(weblinks, config);
+ if (!weblink || !weblink.url) {
+ return {name};
+ } else {
+ return {name, url: weblink.url};
+ }
+ }
+
+ _firstCodeBrowserWeblink(weblinks) {
+ // This is an ordered whitelist of web link types that provide direct
+ // links to the commit in the url property.
+ const codeBrowserLinks = ['gitiles', 'browse', 'gitweb'];
+ for (let i = 0; i < codeBrowserLinks.length; i++) {
+ const weblink =
+ weblinks.find(weblink => weblink.name === codeBrowserLinks[i]);
+ if (weblink) { return weblink; }
+ }
+ return null;
+ }
+
+ _getBrowseCommitWeblink(weblinks, config) {
+ if (!weblinks) { return null; }
+ let weblink;
+ // Use primary weblink if configured and exists.
+ if (config && config.gerrit && config.gerrit.primary_weblink_name) {
+ weblink = weblinks.find(
+ weblink => weblink.name === config.gerrit.primary_weblink_name
+ );
+ }
+ if (!weblink) {
+ weblink = this._firstCodeBrowserWeblink(weblinks);
+ }
+ if (!weblink) { return null; }
+ return weblink;
+ }
+
+ _getChangeWeblinks({repo, commit, options: {weblinks, config}}) {
+ if (!weblinks || !weblinks.length) return [];
+ const commitWeblink = this._getBrowseCommitWeblink(weblinks, config);
+ return weblinks.filter(weblink =>
+ !commitWeblink ||
+ !commitWeblink.name ||
+ weblink.name !== commitWeblink.name);
+ }
+
+ _getFileWebLinks({repo, commit, file, options: {weblinks}}) {
+ return weblinks;
+ }
+
+ /**
+ * @param {!Object} params
+ * @return {string}
+ */
+ _generateSearchUrl(params) {
+ let offsetExpr = '';
+ if (params.offset && params.offset > 0) {
+ offsetExpr = ',' + params.offset;
}
- _setParams(params) {
- this._appElement().params = params;
+ if (params.query) {
+ return '/q/' + this.encodeURL(params.query, true) + offsetExpr;
}
- _appElement() {
- // In Polymer2 you have to reach through the shadow root of the app
- // element. This obviously breaks encapsulation.
- // TODO(brohlfs): Make this more elegant, e.g. by exposing app-element
- // explicitly in app, or by delegating to it.
- return document.getElementById('app-element') ||
- document.getElementById('app').shadowRoot.getElementById(
- 'app-element');
+ const operators = [];
+ if (params.owner) {
+ operators.push('owner:' + this.encodeURL(params.owner, false));
+ }
+ if (params.project) {
+ operators.push('project:' + this.encodeURL(params.project, false));
+ }
+ if (params.branch) {
+ operators.push('branch:' + this.encodeURL(params.branch, false));
+ }
+ if (params.topic) {
+ operators.push('topic:"' + this.encodeURL(params.topic, false) + '"');
+ }
+ if (params.hashtag) {
+ operators.push('hashtag:"' +
+ this.encodeURL(params.hashtag.toLowerCase(), false) + '"');
+ }
+ if (params.statuses) {
+ if (params.statuses.length === 1) {
+ operators.push(
+ 'status:' + this.encodeURL(params.statuses[0], false));
+ } else if (params.statuses.length > 1) {
+ operators.push(
+ '(' +
+ params.statuses.map(s => `status:${this.encodeURL(s, false)}`)
+ .join(' OR ') +
+ ')');
+ }
}
- _redirect(url) {
- this._isRedirecting = true;
- page.redirect(url);
+ return '/q/' + operators.join('+') + offsetExpr;
+ }
+
+ /**
+ * @param {!Object} params
+ * @return {string}
+ */
+ _generateChangeUrl(params) {
+ let range = this._getPatchRangeExpression(params);
+ if (range.length) { range = '/' + range; }
+ let suffix = `${range}`;
+ if (params.querystring) {
+ suffix += '?' + params.querystring;
+ } else if (params.edit) {
+ suffix += ',edit';
+ }
+ if (params.messageHash) {
+ suffix += params.messageHash;
+ }
+ if (params.project) {
+ const encodedProject = this.encodeURL(params.project, true);
+ return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
+ } else {
+ return `/c/${params.changeNum}${suffix}`;
+ }
+ }
+
+ /**
+ * @param {!Object} params
+ * @return {string}
+ */
+ _generateDashboardUrl(params) {
+ const repoName = params.repo || params.project || null;
+ if (params.sections) {
+ // Custom dashboard.
+ const queryParams = this._sectionsToEncodedParams(params.sections,
+ repoName);
+ if (params.title) {
+ queryParams.push('title=' + encodeURIComponent(params.title));
+ }
+ const user = params.user ? params.user : '';
+ return `/dashboard/${user}?${queryParams.join('&')}`;
+ } else if (repoName) {
+ // Project dashboard.
+ const encodedRepo = this.encodeURL(repoName, true);
+ return `/p/${encodedRepo}/+/dashboard/${params.dashboard}`;
+ } else {
+ // User dashboard.
+ return `/dashboard/${params.user || 'self'}`;
+ }
+ }
+
+ /**
+ * @param {!Array<!{name: string, query: string}>} sections
+ * @param {string=} opt_repoName
+ * @return {!Array<string>}
+ */
+ _sectionsToEncodedParams(sections, opt_repoName) {
+ return sections.map(section => {
+ // If there is a repo name provided, make sure to substitute it into the
+ // ${repo} (or legacy ${project}) query tokens.
+ const query = opt_repoName ?
+ section.query.replace(REPO_TOKEN_PATTERN, opt_repoName) :
+ section.query;
+ return encodeURIComponent(section.name) + '=' +
+ encodeURIComponent(query);
+ });
+ }
+
+ /**
+ * @param {!Object} params
+ * @return {string}
+ */
+ _generateDiffOrEditUrl(params) {
+ let range = this._getPatchRangeExpression(params);
+ if (range.length) { range = '/' + range; }
+
+ let suffix = `${range}/${this.encodeURL(params.path, true)}`;
+
+ if (params.view === Gerrit.Nav.View.EDIT) { suffix += ',edit'; }
+
+ if (params.lineNum) {
+ suffix += '#';
+ if (params.leftSide) { suffix += 'b'; }
+ suffix += params.lineNum;
}
- /**
- * @param {!Object} params
- * @return {string}
- */
- _generateUrl(params) {
- const base = this.getBaseUrl();
- let url = '';
- const Views = Gerrit.Nav.View;
+ if (params.project) {
+ const encodedProject = this.encodeURL(params.project, true);
+ return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
+ } else {
+ return `/c/${params.changeNum}${suffix}`;
+ }
+ }
- if (params.view === Views.SEARCH) {
- url = this._generateSearchUrl(params);
- } else if (params.view === Views.CHANGE) {
- url = this._generateChangeUrl(params);
- } else if (params.view === Views.DASHBOARD) {
- url = this._generateDashboardUrl(params);
- } else if (params.view === Views.DIFF || params.view === Views.EDIT) {
- url = this._generateDiffOrEditUrl(params);
- } else if (params.view === Views.GROUP) {
- url = this._generateGroupUrl(params);
- } else if (params.view === Views.REPO) {
- url = this._generateRepoUrl(params);
- } else if (params.view === Views.ROOT) {
- url = '/';
- } else if (params.view === Views.SETTINGS) {
- url = this._generateSettingsUrl(params);
+ /**
+ * @param {!Object} params
+ * @return {string}
+ */
+ _generateGroupUrl(params) {
+ let url = `/admin/groups/${this.encodeURL(params.groupId + '', true)}`;
+ if (params.detail === Gerrit.Nav.GroupDetailView.MEMBERS) {
+ url += ',members';
+ } else if (params.detail === Gerrit.Nav.GroupDetailView.LOG) {
+ url += ',audit-log';
+ }
+ return url;
+ }
+
+ /**
+ * @param {!Object} params
+ * @return {string}
+ */
+ _generateRepoUrl(params) {
+ let url = `/admin/repos/${this.encodeURL(params.repoName + '', true)}`;
+ if (params.detail === Gerrit.Nav.RepoDetailView.ACCESS) {
+ url += ',access';
+ } else if (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES) {
+ url += ',branches';
+ } else if (params.detail === Gerrit.Nav.RepoDetailView.TAGS) {
+ url += ',tags';
+ } else if (params.detail === Gerrit.Nav.RepoDetailView.COMMANDS) {
+ url += ',commands';
+ } else if (params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS) {
+ url += ',dashboards';
+ }
+ return url;
+ }
+
+ /**
+ * @param {!Object} params
+ * @return {string}
+ */
+ _generateSettingsUrl(params) {
+ return '/settings';
+ }
+
+ /**
+ * Given an object of parameters, potentially including a `patchNum` or a
+ * `basePatchNum` or both, return a string representation of that range. If
+ * no range is indicated in the params, the empty string is returned.
+ *
+ * @param {!Object} params
+ * @return {string}
+ */
+ _getPatchRangeExpression(params) {
+ let range = '';
+ if (params.patchNum) { range = '' + params.patchNum; }
+ if (params.basePatchNum) { range = params.basePatchNum + '..' + range; }
+ return range;
+ }
+
+ /**
+ * Given a set of params without a project, gets the project from the rest
+ * API project lookup and then sets the app params.
+ *
+ * @param {?Object} params
+ */
+ _normalizeLegacyRouteParams(params) {
+ if (!params.changeNum) { return Promise.resolve(); }
+
+ return this.$.restAPI.getFromProjectLookup(params.changeNum)
+ .then(project => {
+ // Show a 404 and terminate if the lookup request failed. Attempting
+ // to redirect after failing to get the project loops infinitely.
+ if (!project) {
+ this._show404();
+ return;
+ }
+
+ params.project = project;
+ this._normalizePatchRangeParams(params);
+ this._redirect(this._generateUrl(params));
+ });
+ }
+
+ /**
+ * Normalizes the params object, and determines if the URL needs to be
+ * modified to fit the proper schema.
+ *
+ * @param {*} params
+ * @return {boolean} whether or not the URL needs to be upgraded.
+ */
+ _normalizePatchRangeParams(params) {
+ const hasBasePatchNum = params.basePatchNum !== null &&
+ params.basePatchNum !== undefined;
+ const hasPatchNum = params.patchNum !== null &&
+ params.patchNum !== undefined;
+ let needsRedirect = false;
+
+ // Diffing a patch against itself is invalid, so if the base and revision
+ // patches are equal clear the base.
+ if (hasBasePatchNum &&
+ this.patchNumEquals(params.basePatchNum, params.patchNum)) {
+ needsRedirect = true;
+ params.basePatchNum = null;
+ } else if (hasBasePatchNum && !hasPatchNum) {
+ // Regexes set basePatchNum instead of patchNum when only one is
+ // specified. Redirect is not needed in this case.
+ params.patchNum = params.basePatchNum;
+ params.basePatchNum = null;
+ }
+ return needsRedirect;
+ }
+
+ /**
+ * Redirect the user to login using the given return-URL for redirection
+ * after authentication success.
+ *
+ * @param {string} returnUrl
+ */
+ _redirectToLogin(returnUrl) {
+ const basePath = this.getBaseUrl() || '';
+ page(
+ '/login/' + encodeURIComponent(returnUrl.substring(basePath.length)));
+ }
+
+ /**
+ * Hashes parsed by page.js exclude "inner" hashes, so a URL like "/a#b#c"
+ * is parsed to have a hash of "b" rather than "b#c". Instead, this method
+ * parses hashes correctly. Will return an empty string if there is no hash.
+ *
+ * @param {!string} canonicalPath
+ * @return {!string} Everything after the first '#' ("a#b#c" -> "b#c").
+ */
+ _getHashFromCanonicalPath(canonicalPath) {
+ return canonicalPath.split('#').slice(1)
+ .join('#');
+ }
+
+ _parseLineAddress(hash) {
+ const match = hash.match(LINE_ADDRESS_PATTERN);
+ if (!match) { return null; }
+ return {
+ leftSide: !!match[1],
+ lineNum: parseInt(match[2], 10),
+ };
+ }
+
+ /**
+ * Check to see if the user is logged in and return a promise that only
+ * resolves if the user is logged in. If the user us not logged in, the
+ * promise is rejected and the page is redirected to the login flow.
+ *
+ * @param {!Object} data The parsed route data.
+ * @return {!Promise<!Object>} A promise yielding the original route data
+ * (if it resolves).
+ */
+ _redirectIfNotLoggedIn(data) {
+ return this.$.restAPI.getLoggedIn().then(loggedIn => {
+ if (loggedIn) {
+ return Promise.resolve();
} else {
- throw new Error('Can\'t generate');
+ this._redirectToLogin(data.canonicalPath);
+ return Promise.reject(new Error());
}
+ });
+ }
- return base + url;
+ /** Page.js middleware that warms the REST API's logged-in cache line. */
+ _loadUserMiddleware(ctx, next) {
+ this.$.restAPI.getLoggedIn().then(() => { next(); });
+ }
+
+ /**
+ * Map a route to a method on the router.
+ *
+ * @param {!string|!RegExp} pattern The page.js pattern for the route.
+ * @param {!string} handlerName The method name for the handler. If the
+ * route is matched, the handler will be executed with `this` referring
+ * to the component. Its return value will be discarded so that it does
+ * not interfere with page.js.
+ * @param {?boolean=} opt_authRedirect If true, then auth is checked before
+ * executing the handler. If the user is not logged in, it will redirect
+ * to the login flow and the handler will not be executed. The login
+ * redirect specifies the matched URL to be used after successfull auth.
+ */
+ _mapRoute(pattern, handlerName, opt_authRedirect) {
+ if (!this[handlerName]) {
+ console.error('Attempted to map route to unknown method: ',
+ handlerName);
+ return;
+ }
+ page(pattern, this._loadUserMiddleware.bind(this), data => {
+ this.$.reporting.locationChanged(handlerName);
+ const promise = opt_authRedirect ?
+ this._redirectIfNotLoggedIn(data) : Promise.resolve();
+ promise.then(() => { this[handlerName](data); });
+ });
+ }
+
+ _startRouter() {
+ const base = this.getBaseUrl();
+ if (base) {
+ page.base(base);
}
- _generateWeblinks(params) {
- const type = params.type;
- switch (type) {
- case Gerrit.Nav.WeblinkType.FILE:
- return this._getFileWebLinks(params);
- case Gerrit.Nav.WeblinkType.CHANGE:
- return this._getChangeWeblinks(params);
- case Gerrit.Nav.WeblinkType.PATCHSET:
- return this._getPatchSetWeblink(params);
- default:
- console.warn(`Unsupported weblink ${type}!`);
+ Gerrit.Nav.setup(
+ url => { page.show(url); },
+ this._generateUrl.bind(this),
+ params => this._generateWeblinks(params),
+ x => x
+ );
+
+ page.exit('*', (ctx, next) => {
+ if (!this._isRedirecting) {
+ this.$.reporting.beforeLocationChanged();
}
- }
+ this._isRedirecting = false;
+ this._isInitialLoad = false;
+ next();
+ });
- _getPatchSetWeblink(params) {
- const {commit, options} = params;
- const {weblinks, config} = options || {};
- const name = commit && commit.slice(0, 7);
- const weblink = this._getBrowseCommitWeblink(weblinks, config);
- if (!weblink || !weblink.url) {
- return {name};
- } else {
- return {name, url: weblink.url};
- }
- }
+ // Middleware
+ page((ctx, next) => {
+ document.body.scrollTop = 0;
- _firstCodeBrowserWeblink(weblinks) {
- // This is an ordered whitelist of web link types that provide direct
- // links to the commit in the url property.
- const codeBrowserLinks = ['gitiles', 'browse', 'gitweb'];
- for (let i = 0; i < codeBrowserLinks.length; i++) {
- const weblink =
- weblinks.find(weblink => weblink.name === codeBrowserLinks[i]);
- if (weblink) { return weblink; }
- }
- return null;
- }
-
- _getBrowseCommitWeblink(weblinks, config) {
- if (!weblinks) { return null; }
- let weblink;
- // Use primary weblink if configured and exists.
- if (config && config.gerrit && config.gerrit.primary_weblink_name) {
- weblink = weblinks.find(
- weblink => weblink.name === config.gerrit.primary_weblink_name
- );
- }
- if (!weblink) {
- weblink = this._firstCodeBrowserWeblink(weblinks);
- }
- if (!weblink) { return null; }
- return weblink;
- }
-
- _getChangeWeblinks({repo, commit, options: {weblinks, config}}) {
- if (!weblinks || !weblinks.length) return [];
- const commitWeblink = this._getBrowseCommitWeblink(weblinks, config);
- return weblinks.filter(weblink =>
- !commitWeblink ||
- !commitWeblink.name ||
- weblink.name !== commitWeblink.name);
- }
-
- _getFileWebLinks({repo, commit, file, options: {weblinks}}) {
- return weblinks;
- }
-
- /**
- * @param {!Object} params
- * @return {string}
- */
- _generateSearchUrl(params) {
- let offsetExpr = '';
- if (params.offset && params.offset > 0) {
- offsetExpr = ',' + params.offset;
- }
-
- if (params.query) {
- return '/q/' + this.encodeURL(params.query, true) + offsetExpr;
- }
-
- const operators = [];
- if (params.owner) {
- operators.push('owner:' + this.encodeURL(params.owner, false));
- }
- if (params.project) {
- operators.push('project:' + this.encodeURL(params.project, false));
- }
- if (params.branch) {
- operators.push('branch:' + this.encodeURL(params.branch, false));
- }
- if (params.topic) {
- operators.push('topic:"' + this.encodeURL(params.topic, false) + '"');
- }
- if (params.hashtag) {
- operators.push('hashtag:"' +
- this.encodeURL(params.hashtag.toLowerCase(), false) + '"');
- }
- if (params.statuses) {
- if (params.statuses.length === 1) {
- operators.push(
- 'status:' + this.encodeURL(params.statuses[0], false));
- } else if (params.statuses.length > 1) {
- operators.push(
- '(' +
- params.statuses.map(s => `status:${this.encodeURL(s, false)}`)
- .join(' OR ') +
- ')');
- }
- }
-
- return '/q/' + operators.join('+') + offsetExpr;
- }
-
- /**
- * @param {!Object} params
- * @return {string}
- */
- _generateChangeUrl(params) {
- let range = this._getPatchRangeExpression(params);
- if (range.length) { range = '/' + range; }
- let suffix = `${range}`;
- if (params.querystring) {
- suffix += '?' + params.querystring;
- } else if (params.edit) {
- suffix += ',edit';
- }
- if (params.messageHash) {
- suffix += params.messageHash;
- }
- if (params.project) {
- const encodedProject = this.encodeURL(params.project, true);
- return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
- } else {
- return `/c/${params.changeNum}${suffix}`;
- }
- }
-
- /**
- * @param {!Object} params
- * @return {string}
- */
- _generateDashboardUrl(params) {
- const repoName = params.repo || params.project || null;
- if (params.sections) {
- // Custom dashboard.
- const queryParams = this._sectionsToEncodedParams(params.sections,
- repoName);
- if (params.title) {
- queryParams.push('title=' + encodeURIComponent(params.title));
- }
- const user = params.user ? params.user : '';
- return `/dashboard/${user}?${queryParams.join('&')}`;
- } else if (repoName) {
- // Project dashboard.
- const encodedRepo = this.encodeURL(repoName, true);
- return `/p/${encodedRepo}/+/dashboard/${params.dashboard}`;
- } else {
- // User dashboard.
- return `/dashboard/${params.user || 'self'}`;
- }
- }
-
- /**
- * @param {!Array<!{name: string, query: string}>} sections
- * @param {string=} opt_repoName
- * @return {!Array<string>}
- */
- _sectionsToEncodedParams(sections, opt_repoName) {
- return sections.map(section => {
- // If there is a repo name provided, make sure to substitute it into the
- // ${repo} (or legacy ${project}) query tokens.
- const query = opt_repoName ?
- section.query.replace(REPO_TOKEN_PATTERN, opt_repoName) :
- section.query;
- return encodeURIComponent(section.name) + '=' +
- encodeURIComponent(query);
- });
- }
-
- /**
- * @param {!Object} params
- * @return {string}
- */
- _generateDiffOrEditUrl(params) {
- let range = this._getPatchRangeExpression(params);
- if (range.length) { range = '/' + range; }
-
- let suffix = `${range}/${this.encodeURL(params.path, true)}`;
-
- if (params.view === Gerrit.Nav.View.EDIT) { suffix += ',edit'; }
-
- if (params.lineNum) {
- suffix += '#';
- if (params.leftSide) { suffix += 'b'; }
- suffix += params.lineNum;
- }
-
- if (params.project) {
- const encodedProject = this.encodeURL(params.project, true);
- return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
- } else {
- return `/c/${params.changeNum}${suffix}`;
- }
- }
-
- /**
- * @param {!Object} params
- * @return {string}
- */
- _generateGroupUrl(params) {
- let url = `/admin/groups/${this.encodeURL(params.groupId + '', true)}`;
- if (params.detail === Gerrit.Nav.GroupDetailView.MEMBERS) {
- url += ',members';
- } else if (params.detail === Gerrit.Nav.GroupDetailView.LOG) {
- url += ',audit-log';
- }
- return url;
- }
-
- /**
- * @param {!Object} params
- * @return {string}
- */
- _generateRepoUrl(params) {
- let url = `/admin/repos/${this.encodeURL(params.repoName + '', true)}`;
- if (params.detail === Gerrit.Nav.RepoDetailView.ACCESS) {
- url += ',access';
- } else if (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES) {
- url += ',branches';
- } else if (params.detail === Gerrit.Nav.RepoDetailView.TAGS) {
- url += ',tags';
- } else if (params.detail === Gerrit.Nav.RepoDetailView.COMMANDS) {
- url += ',commands';
- } else if (params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS) {
- url += ',dashboards';
- }
- return url;
- }
-
- /**
- * @param {!Object} params
- * @return {string}
- */
- _generateSettingsUrl(params) {
- return '/settings';
- }
-
- /**
- * Given an object of parameters, potentially including a `patchNum` or a
- * `basePatchNum` or both, return a string representation of that range. If
- * no range is indicated in the params, the empty string is returned.
- *
- * @param {!Object} params
- * @return {string}
- */
- _getPatchRangeExpression(params) {
- let range = '';
- if (params.patchNum) { range = '' + params.patchNum; }
- if (params.basePatchNum) { range = params.basePatchNum + '..' + range; }
- return range;
- }
-
- /**
- * Given a set of params without a project, gets the project from the rest
- * API project lookup and then sets the app params.
- *
- * @param {?Object} params
- */
- _normalizeLegacyRouteParams(params) {
- if (!params.changeNum) { return Promise.resolve(); }
-
- return this.$.restAPI.getFromProjectLookup(params.changeNum)
- .then(project => {
- // Show a 404 and terminate if the lookup request failed. Attempting
- // to redirect after failing to get the project loops infinitely.
- if (!project) {
- this._show404();
- return;
- }
-
- params.project = project;
- this._normalizePatchRangeParams(params);
- this._redirect(this._generateUrl(params));
- });
- }
-
- /**
- * Normalizes the params object, and determines if the URL needs to be
- * modified to fit the proper schema.
- *
- * @param {*} params
- * @return {boolean} whether or not the URL needs to be upgraded.
- */
- _normalizePatchRangeParams(params) {
- const hasBasePatchNum = params.basePatchNum !== null &&
- params.basePatchNum !== undefined;
- const hasPatchNum = params.patchNum !== null &&
- params.patchNum !== undefined;
- let needsRedirect = false;
-
- // Diffing a patch against itself is invalid, so if the base and revision
- // patches are equal clear the base.
- if (hasBasePatchNum &&
- this.patchNumEquals(params.basePatchNum, params.patchNum)) {
- needsRedirect = true;
- params.basePatchNum = null;
- } else if (hasBasePatchNum && !hasPatchNum) {
- // Regexes set basePatchNum instead of patchNum when only one is
- // specified. Redirect is not needed in this case.
- params.patchNum = params.basePatchNum;
- params.basePatchNum = null;
- }
- return needsRedirect;
- }
-
- /**
- * Redirect the user to login using the given return-URL for redirection
- * after authentication success.
- *
- * @param {string} returnUrl
- */
- _redirectToLogin(returnUrl) {
- const basePath = this.getBaseUrl() || '';
- page(
- '/login/' + encodeURIComponent(returnUrl.substring(basePath.length)));
- }
-
- /**
- * Hashes parsed by page.js exclude "inner" hashes, so a URL like "/a#b#c"
- * is parsed to have a hash of "b" rather than "b#c". Instead, this method
- * parses hashes correctly. Will return an empty string if there is no hash.
- *
- * @param {!string} canonicalPath
- * @return {!string} Everything after the first '#' ("a#b#c" -> "b#c").
- */
- _getHashFromCanonicalPath(canonicalPath) {
- return canonicalPath.split('#').slice(1)
- .join('#');
- }
-
- _parseLineAddress(hash) {
- const match = hash.match(LINE_ADDRESS_PATTERN);
- if (!match) { return null; }
- return {
- leftSide: !!match[1],
- lineNum: parseInt(match[2], 10),
- };
- }
-
- /**
- * Check to see if the user is logged in and return a promise that only
- * resolves if the user is logged in. If the user us not logged in, the
- * promise is rejected and the page is redirected to the login flow.
- *
- * @param {!Object} data The parsed route data.
- * @return {!Promise<!Object>} A promise yielding the original route data
- * (if it resolves).
- */
- _redirectIfNotLoggedIn(data) {
- return this.$.restAPI.getLoggedIn().then(loggedIn => {
- if (loggedIn) {
- return Promise.resolve();
- } else {
- this._redirectToLogin(data.canonicalPath);
- return Promise.reject(new Error());
- }
- });
- }
-
- /** Page.js middleware that warms the REST API's logged-in cache line. */
- _loadUserMiddleware(ctx, next) {
- this.$.restAPI.getLoggedIn().then(() => { next(); });
- }
-
- /**
- * Map a route to a method on the router.
- *
- * @param {!string|!RegExp} pattern The page.js pattern for the route.
- * @param {!string} handlerName The method name for the handler. If the
- * route is matched, the handler will be executed with `this` referring
- * to the component. Its return value will be discarded so that it does
- * not interfere with page.js.
- * @param {?boolean=} opt_authRedirect If true, then auth is checked before
- * executing the handler. If the user is not logged in, it will redirect
- * to the login flow and the handler will not be executed. The login
- * redirect specifies the matched URL to be used after successfull auth.
- */
- _mapRoute(pattern, handlerName, opt_authRedirect) {
- if (!this[handlerName]) {
- console.error('Attempted to map route to unknown method: ',
- handlerName);
+ if (ctx.hash.match(RoutePattern.PLUGIN_SCREEN)) {
+ // Redirect all urls using hash #/x/plugin/screen to /x/plugin/screen
+ // This is needed to allow plugins to add basic #/x/ screen links to
+ // any location.
+ this._redirect(ctx.hash);
return;
}
- page(pattern, this._loadUserMiddleware.bind(this), data => {
- this.$.reporting.locationChanged(handlerName);
- const promise = opt_authRedirect ?
- this._redirectIfNotLoggedIn(data) : Promise.resolve();
- promise.then(() => { this[handlerName](data); });
- });
- }
- _startRouter() {
+ // Fire asynchronously so that the URL is changed by the time the event
+ // is processed.
+ this.async(() => {
+ this.fire('location-change', {
+ hash: window.location.hash,
+ pathname: window.location.pathname,
+ });
+ }, 1);
+ next();
+ });
+
+ this._mapRoute(RoutePattern.ROOT, '_handleRootRoute');
+
+ this._mapRoute(RoutePattern.DASHBOARD, '_handleDashboardRoute');
+
+ this._mapRoute(RoutePattern.CUSTOM_DASHBOARD,
+ '_handleCustomDashboardRoute');
+
+ this._mapRoute(RoutePattern.PROJECT_DASHBOARD,
+ '_handleProjectDashboardRoute');
+
+ this._mapRoute(RoutePattern.GROUP_INFO, '_handleGroupInfoRoute', true);
+
+ this._mapRoute(RoutePattern.GROUP_AUDIT_LOG, '_handleGroupAuditLogRoute',
+ true);
+
+ this._mapRoute(RoutePattern.GROUP_MEMBERS, '_handleGroupMembersRoute',
+ true);
+
+ this._mapRoute(RoutePattern.GROUP_LIST_OFFSET,
+ '_handleGroupListOffsetRoute', true);
+
+ this._mapRoute(RoutePattern.GROUP_LIST_FILTER_OFFSET,
+ '_handleGroupListFilterOffsetRoute', true);
+
+ this._mapRoute(RoutePattern.GROUP_LIST_FILTER,
+ '_handleGroupListFilterRoute', true);
+
+ this._mapRoute(RoutePattern.GROUP_SELF, '_handleGroupSelfRedirectRoute',
+ true);
+
+ this._mapRoute(RoutePattern.GROUP, '_handleGroupRoute', true);
+
+ this._mapRoute(RoutePattern.PROJECT_OLD,
+ '_handleProjectsOldRoute');
+
+ this._mapRoute(RoutePattern.REPO_COMMANDS,
+ '_handleRepoCommandsRoute', true);
+
+ this._mapRoute(RoutePattern.REPO_ACCESS,
+ '_handleRepoAccessRoute');
+
+ this._mapRoute(RoutePattern.REPO_DASHBOARDS,
+ '_handleRepoDashboardsRoute');
+
+ this._mapRoute(RoutePattern.BRANCH_LIST_OFFSET,
+ '_handleBranchListOffsetRoute');
+
+ this._mapRoute(RoutePattern.BRANCH_LIST_FILTER_OFFSET,
+ '_handleBranchListFilterOffsetRoute');
+
+ this._mapRoute(RoutePattern.BRANCH_LIST_FILTER,
+ '_handleBranchListFilterRoute');
+
+ this._mapRoute(RoutePattern.TAG_LIST_OFFSET,
+ '_handleTagListOffsetRoute');
+
+ this._mapRoute(RoutePattern.TAG_LIST_FILTER_OFFSET,
+ '_handleTagListFilterOffsetRoute');
+
+ this._mapRoute(RoutePattern.TAG_LIST_FILTER,
+ '_handleTagListFilterRoute');
+
+ this._mapRoute(RoutePattern.LEGACY_CREATE_GROUP,
+ '_handleCreateGroupRoute', true);
+
+ this._mapRoute(RoutePattern.LEGACY_CREATE_PROJECT,
+ '_handleCreateProjectRoute', true);
+
+ this._mapRoute(RoutePattern.REPO_LIST_OFFSET,
+ '_handleRepoListOffsetRoute');
+
+ this._mapRoute(RoutePattern.REPO_LIST_FILTER_OFFSET,
+ '_handleRepoListFilterOffsetRoute');
+
+ this._mapRoute(RoutePattern.REPO_LIST_FILTER,
+ '_handleRepoListFilterRoute');
+
+ this._mapRoute(RoutePattern.REPO, '_handleRepoRoute');
+
+ this._mapRoute(RoutePattern.PLUGINS, '_handlePassThroughRoute');
+
+ this._mapRoute(RoutePattern.PLUGIN_LIST_OFFSET,
+ '_handlePluginListOffsetRoute', true);
+
+ this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER_OFFSET,
+ '_handlePluginListFilterOffsetRoute', true);
+
+ this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER,
+ '_handlePluginListFilterRoute', true);
+
+ this._mapRoute(RoutePattern.PLUGIN_LIST, '_handlePluginListRoute', true);
+
+ this._mapRoute(RoutePattern.QUERY_LEGACY_SUFFIX,
+ '_handleQueryLegacySuffixRoute');
+
+ this._mapRoute(RoutePattern.QUERY, '_handleQueryRoute');
+
+ this._mapRoute(RoutePattern.DIFF_LEGACY_LINENUM, '_handleLegacyLinenum');
+
+ this._mapRoute(RoutePattern.CHANGE_NUMBER_LEGACY,
+ '_handleChangeNumberLegacyRoute');
+
+ this._mapRoute(RoutePattern.DIFF_EDIT, '_handleDiffEditRoute', true);
+
+ this._mapRoute(RoutePattern.CHANGE_EDIT, '_handleChangeEditRoute', true);
+
+ this._mapRoute(RoutePattern.DIFF, '_handleDiffRoute');
+
+ this._mapRoute(RoutePattern.CHANGE, '_handleChangeRoute');
+
+ this._mapRoute(RoutePattern.CHANGE_LEGACY, '_handleChangeLegacyRoute');
+
+ this._mapRoute(RoutePattern.DIFF_LEGACY, '_handleDiffLegacyRoute');
+
+ this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true);
+
+ this._mapRoute(RoutePattern.NEW_AGREEMENTS, '_handleNewAgreementsRoute',
+ true);
+
+ this._mapRoute(RoutePattern.SETTINGS_LEGACY,
+ '_handleSettingsLegacyRoute', true);
+
+ this._mapRoute(RoutePattern.SETTINGS, '_handleSettingsRoute', true);
+
+ this._mapRoute(RoutePattern.REGISTER, '_handleRegisterRoute');
+
+ this._mapRoute(RoutePattern.LOG_IN_OR_OUT, '_handlePassThroughRoute');
+
+ this._mapRoute(RoutePattern.IMPROPERLY_ENCODED_PLUS,
+ '_handleImproperlyEncodedPlusRoute');
+
+ this._mapRoute(RoutePattern.PLUGIN_SCREEN, '_handlePluginScreen');
+
+ this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH_FILTER,
+ '_handleDocumentationSearchRoute');
+
+ // redirects /Documentation/q/* to /Documentation/q/filter:*
+ this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH,
+ '_handleDocumentationSearchRedirectRoute');
+
+ // Makes sure /Documentation/* links work (doin't return 404)
+ this._mapRoute(RoutePattern.DOCUMENTATION,
+ '_handleDocumentationRedirectRoute');
+
+ // Note: this route should appear last so it only catches URLs unmatched
+ // by other patterns.
+ this._mapRoute(RoutePattern.DEFAULT, '_handleDefaultRoute');
+
+ page.start();
+ }
+
+ /**
+ * @param {!Object} data
+ * @return {Promise|null} if handling the route involves asynchrony, then a
+ * promise is returned. Otherwise, synchronous handling returns null.
+ */
+ _handleRootRoute(data) {
+ if (data.querystring.match(/^closeAfterLogin/)) {
+ // Close child window on redirect after login.
+ window.close();
+ return null;
+ }
+ let hash = this._getHashFromCanonicalPath(data.canonicalPath);
+ // For backward compatibility with GWT links.
+ if (hash) {
+ // In certain login flows the server may redirect to a hash without
+ // a leading slash, which page.js doesn't handle correctly.
+ if (hash[0] !== '/') {
+ hash = '/' + hash;
+ }
+ if (hash.includes('/ /') && data.canonicalPath.includes('/+/')) {
+ // Path decodes all '+' to ' ' -- this breaks project-based URLs.
+ // See Issue 6888.
+ hash = hash.replace('/ /', '/+/');
+ }
const base = this.getBaseUrl();
- if (base) {
- page.base(base);
+ let newUrl = base + hash;
+ if (hash.startsWith('/VE/')) {
+ newUrl = base + '/settings' + hash;
}
-
- Gerrit.Nav.setup(
- url => { page.show(url); },
- this._generateUrl.bind(this),
- params => this._generateWeblinks(params),
- x => x
- );
-
- page.exit('*', (ctx, next) => {
- if (!this._isRedirecting) {
- this.$.reporting.beforeLocationChanged();
- }
- this._isRedirecting = false;
- this._isInitialLoad = false;
- next();
- });
-
- // Middleware
- page((ctx, next) => {
- document.body.scrollTop = 0;
-
- if (ctx.hash.match(RoutePattern.PLUGIN_SCREEN)) {
- // Redirect all urls using hash #/x/plugin/screen to /x/plugin/screen
- // This is needed to allow plugins to add basic #/x/ screen links to
- // any location.
- this._redirect(ctx.hash);
- return;
- }
-
- // Fire asynchronously so that the URL is changed by the time the event
- // is processed.
- this.async(() => {
- this.fire('location-change', {
- hash: window.location.hash,
- pathname: window.location.pathname,
- });
- }, 1);
- next();
- });
-
- this._mapRoute(RoutePattern.ROOT, '_handleRootRoute');
-
- this._mapRoute(RoutePattern.DASHBOARD, '_handleDashboardRoute');
-
- this._mapRoute(RoutePattern.CUSTOM_DASHBOARD,
- '_handleCustomDashboardRoute');
-
- this._mapRoute(RoutePattern.PROJECT_DASHBOARD,
- '_handleProjectDashboardRoute');
-
- this._mapRoute(RoutePattern.GROUP_INFO, '_handleGroupInfoRoute', true);
-
- this._mapRoute(RoutePattern.GROUP_AUDIT_LOG, '_handleGroupAuditLogRoute',
- true);
-
- this._mapRoute(RoutePattern.GROUP_MEMBERS, '_handleGroupMembersRoute',
- true);
-
- this._mapRoute(RoutePattern.GROUP_LIST_OFFSET,
- '_handleGroupListOffsetRoute', true);
-
- this._mapRoute(RoutePattern.GROUP_LIST_FILTER_OFFSET,
- '_handleGroupListFilterOffsetRoute', true);
-
- this._mapRoute(RoutePattern.GROUP_LIST_FILTER,
- '_handleGroupListFilterRoute', true);
-
- this._mapRoute(RoutePattern.GROUP_SELF, '_handleGroupSelfRedirectRoute',
- true);
-
- this._mapRoute(RoutePattern.GROUP, '_handleGroupRoute', true);
-
- this._mapRoute(RoutePattern.PROJECT_OLD,
- '_handleProjectsOldRoute');
-
- this._mapRoute(RoutePattern.REPO_COMMANDS,
- '_handleRepoCommandsRoute', true);
-
- this._mapRoute(RoutePattern.REPO_ACCESS,
- '_handleRepoAccessRoute');
-
- this._mapRoute(RoutePattern.REPO_DASHBOARDS,
- '_handleRepoDashboardsRoute');
-
- this._mapRoute(RoutePattern.BRANCH_LIST_OFFSET,
- '_handleBranchListOffsetRoute');
-
- this._mapRoute(RoutePattern.BRANCH_LIST_FILTER_OFFSET,
- '_handleBranchListFilterOffsetRoute');
-
- this._mapRoute(RoutePattern.BRANCH_LIST_FILTER,
- '_handleBranchListFilterRoute');
-
- this._mapRoute(RoutePattern.TAG_LIST_OFFSET,
- '_handleTagListOffsetRoute');
-
- this._mapRoute(RoutePattern.TAG_LIST_FILTER_OFFSET,
- '_handleTagListFilterOffsetRoute');
-
- this._mapRoute(RoutePattern.TAG_LIST_FILTER,
- '_handleTagListFilterRoute');
-
- this._mapRoute(RoutePattern.LEGACY_CREATE_GROUP,
- '_handleCreateGroupRoute', true);
-
- this._mapRoute(RoutePattern.LEGACY_CREATE_PROJECT,
- '_handleCreateProjectRoute', true);
-
- this._mapRoute(RoutePattern.REPO_LIST_OFFSET,
- '_handleRepoListOffsetRoute');
-
- this._mapRoute(RoutePattern.REPO_LIST_FILTER_OFFSET,
- '_handleRepoListFilterOffsetRoute');
-
- this._mapRoute(RoutePattern.REPO_LIST_FILTER,
- '_handleRepoListFilterRoute');
-
- this._mapRoute(RoutePattern.REPO, '_handleRepoRoute');
-
- this._mapRoute(RoutePattern.PLUGINS, '_handlePassThroughRoute');
-
- this._mapRoute(RoutePattern.PLUGIN_LIST_OFFSET,
- '_handlePluginListOffsetRoute', true);
-
- this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER_OFFSET,
- '_handlePluginListFilterOffsetRoute', true);
-
- this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER,
- '_handlePluginListFilterRoute', true);
-
- this._mapRoute(RoutePattern.PLUGIN_LIST, '_handlePluginListRoute', true);
-
- this._mapRoute(RoutePattern.QUERY_LEGACY_SUFFIX,
- '_handleQueryLegacySuffixRoute');
-
- this._mapRoute(RoutePattern.QUERY, '_handleQueryRoute');
-
- this._mapRoute(RoutePattern.DIFF_LEGACY_LINENUM, '_handleLegacyLinenum');
-
- this._mapRoute(RoutePattern.CHANGE_NUMBER_LEGACY,
- '_handleChangeNumberLegacyRoute');
-
- this._mapRoute(RoutePattern.DIFF_EDIT, '_handleDiffEditRoute', true);
-
- this._mapRoute(RoutePattern.CHANGE_EDIT, '_handleChangeEditRoute', true);
-
- this._mapRoute(RoutePattern.DIFF, '_handleDiffRoute');
-
- this._mapRoute(RoutePattern.CHANGE, '_handleChangeRoute');
-
- this._mapRoute(RoutePattern.CHANGE_LEGACY, '_handleChangeLegacyRoute');
-
- this._mapRoute(RoutePattern.DIFF_LEGACY, '_handleDiffLegacyRoute');
-
- this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true);
-
- this._mapRoute(RoutePattern.NEW_AGREEMENTS, '_handleNewAgreementsRoute',
- true);
-
- this._mapRoute(RoutePattern.SETTINGS_LEGACY,
- '_handleSettingsLegacyRoute', true);
-
- this._mapRoute(RoutePattern.SETTINGS, '_handleSettingsRoute', true);
-
- this._mapRoute(RoutePattern.REGISTER, '_handleRegisterRoute');
-
- this._mapRoute(RoutePattern.LOG_IN_OR_OUT, '_handlePassThroughRoute');
-
- this._mapRoute(RoutePattern.IMPROPERLY_ENCODED_PLUS,
- '_handleImproperlyEncodedPlusRoute');
-
- this._mapRoute(RoutePattern.PLUGIN_SCREEN, '_handlePluginScreen');
-
- this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH_FILTER,
- '_handleDocumentationSearchRoute');
-
- // redirects /Documentation/q/* to /Documentation/q/filter:*
- this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH,
- '_handleDocumentationSearchRedirectRoute');
-
- // Makes sure /Documentation/* links work (doin't return 404)
- this._mapRoute(RoutePattern.DOCUMENTATION,
- '_handleDocumentationRedirectRoute');
-
- // Note: this route should appear last so it only catches URLs unmatched
- // by other patterns.
- this._mapRoute(RoutePattern.DEFAULT, '_handleDefaultRoute');
-
- page.start();
+ this._redirect(newUrl);
+ return null;
}
+ return this.$.restAPI.getLoggedIn().then(loggedIn => {
+ if (loggedIn) {
+ this._redirect('/dashboard/self');
+ } else {
+ this._redirect('/q/status:open');
+ }
+ });
+ }
- /**
- * @param {!Object} data
- * @return {Promise|null} if handling the route involves asynchrony, then a
- * promise is returned. Otherwise, synchronous handling returns null.
- */
- _handleRootRoute(data) {
- if (data.querystring.match(/^closeAfterLogin/)) {
- // Close child window on redirect after login.
- window.close();
- return null;
+ /**
+ * Decode an application/x-www-form-urlencoded string.
+ *
+ * @param {string} qs The application/x-www-form-urlencoded string.
+ * @return {string} The decoded string.
+ */
+ _decodeQueryString(qs) {
+ return decodeURIComponent(qs.replace(PLUS_PATTERN, ' '));
+ }
+
+ /**
+ * Parse a query string (e.g. window.location.search) into an array of
+ * name/value pairs.
+ *
+ * @param {string} qs The application/x-www-form-urlencoded query string.
+ * @return {!Array<!Array<string>>} An array of name/value pairs, where each
+ * element is a 2-element array.
+ */
+ _parseQueryString(qs) {
+ qs = qs.replace(QUESTION_PATTERN, '');
+ if (!qs) {
+ return [];
+ }
+ const params = [];
+ qs.split('&').forEach(param => {
+ const idx = param.indexOf('=');
+ let name;
+ let value;
+ if (idx < 0) {
+ name = this._decodeQueryString(param);
+ value = '';
+ } else {
+ name = this._decodeQueryString(param.substring(0, idx));
+ value = this._decodeQueryString(param.substring(idx + 1));
}
- let hash = this._getHashFromCanonicalPath(data.canonicalPath);
- // For backward compatibility with GWT links.
- if (hash) {
- // In certain login flows the server may redirect to a hash without
- // a leading slash, which page.js doesn't handle correctly.
- if (hash[0] !== '/') {
- hash = '/' + hash;
- }
- if (hash.includes('/ /') && data.canonicalPath.includes('/+/')) {
- // Path decodes all '+' to ' ' -- this breaks project-based URLs.
- // See Issue 6888.
- hash = hash.replace('/ /', '/+/');
- }
- const base = this.getBaseUrl();
- let newUrl = base + hash;
- if (hash.startsWith('/VE/')) {
- newUrl = base + '/settings' + hash;
- }
- this._redirect(newUrl);
- return null;
+ if (name) {
+ params.push([name, value]);
}
- return this.$.restAPI.getLoggedIn().then(loggedIn => {
- if (loggedIn) {
- this._redirect('/dashboard/self');
+ });
+ return params;
+ }
+
+ /**
+ * Handle dashboard routes. These may be user, or project dashboards.
+ *
+ * @param {!Object} data The parsed route data.
+ */
+ _handleDashboardRoute(data) {
+ // User dashboard. We require viewing user to be logged in, else we
+ // redirect to login for self dashboard or simple owner search for
+ // other user dashboard.
+ return this.$.restAPI.getLoggedIn().then(loggedIn => {
+ if (!loggedIn) {
+ if (data.params[0].toLowerCase() === 'self') {
+ this._redirectToLogin(data.canonicalPath);
} else {
- this._redirect('/q/status:open');
+ this._redirect('/q/owner:' + encodeURIComponent(data.params[0]));
}
- });
- }
-
- /**
- * Decode an application/x-www-form-urlencoded string.
- *
- * @param {string} qs The application/x-www-form-urlencoded string.
- * @return {string} The decoded string.
- */
- _decodeQueryString(qs) {
- return decodeURIComponent(qs.replace(PLUS_PATTERN, ' '));
- }
-
- /**
- * Parse a query string (e.g. window.location.search) into an array of
- * name/value pairs.
- *
- * @param {string} qs The application/x-www-form-urlencoded query string.
- * @return {!Array<!Array<string>>} An array of name/value pairs, where each
- * element is a 2-element array.
- */
- _parseQueryString(qs) {
- qs = qs.replace(QUESTION_PATTERN, '');
- if (!qs) {
- return [];
- }
- const params = [];
- qs.split('&').forEach(param => {
- const idx = param.indexOf('=');
- let name;
- let value;
- if (idx < 0) {
- name = this._decodeQueryString(param);
- value = '';
- } else {
- name = this._decodeQueryString(param.substring(0, idx));
- value = this._decodeQueryString(param.substring(idx + 1));
- }
- if (name) {
- params.push([name, value]);
- }
- });
- return params;
- }
-
- /**
- * Handle dashboard routes. These may be user, or project dashboards.
- *
- * @param {!Object} data The parsed route data.
- */
- _handleDashboardRoute(data) {
- // User dashboard. We require viewing user to be logged in, else we
- // redirect to login for self dashboard or simple owner search for
- // other user dashboard.
- return this.$.restAPI.getLoggedIn().then(loggedIn => {
- if (!loggedIn) {
- if (data.params[0].toLowerCase() === 'self') {
- this._redirectToLogin(data.canonicalPath);
- } else {
- this._redirect('/q/owner:' + encodeURIComponent(data.params[0]));
- }
- } else {
- this._setParams({
- view: Gerrit.Nav.View.DASHBOARD,
- user: data.params[0],
- });
- }
- });
- }
-
- /**
- * Handle custom dashboard routes.
- *
- * @param {!Object} data The parsed route data.
- * @param {string=} opt_qs Optional query string associated with the route.
- * If not given, window.location.search is used. (Used by tests).
- */
- _handleCustomDashboardRoute(data, opt_qs) {
- // opt_qs may be provided by a test, and it may have a falsy value
- const qs = opt_qs !== undefined ? opt_qs : window.location.search;
- const queryParams = this._parseQueryString(qs);
- let title = 'Custom Dashboard';
- const titleParam = queryParams.find(
- elem => elem[0].toLowerCase() === 'title');
- if (titleParam) {
- title = titleParam[1];
- }
- // Dashboards support a foreach param which adds a base query to any
- // additional query.
- const forEachParam = queryParams.find(
- elem => elem[0].toLowerCase() === 'foreach');
- let forEachQuery = null;
- if (forEachParam) {
- forEachQuery = forEachParam[1];
- }
- const sectionParams = queryParams.filter(
- elem => elem[0] && elem[1] && elem[0].toLowerCase() !== 'title' &&
- elem[0].toLowerCase() !== 'foreach');
- const sections = sectionParams.map(elem => {
- const query = forEachQuery ? `${forEachQuery} ${elem[1]}` : elem[1];
- return {
- name: elem[0],
- query,
- };
- });
-
- if (sections.length > 0) {
- // Custom dashboard view.
+ } else {
this._setParams({
view: Gerrit.Nav.View.DASHBOARD,
- user: 'self',
- sections,
- title,
+ user: data.params[0],
});
- return Promise.resolve();
}
+ });
+ }
- // Redirect /dashboard/ -> /dashboard/self.
- this._redirect('/dashboard/self');
+ /**
+ * Handle custom dashboard routes.
+ *
+ * @param {!Object} data The parsed route data.
+ * @param {string=} opt_qs Optional query string associated with the route.
+ * If not given, window.location.search is used. (Used by tests).
+ */
+ _handleCustomDashboardRoute(data, opt_qs) {
+ // opt_qs may be provided by a test, and it may have a falsy value
+ const qs = opt_qs !== undefined ? opt_qs : window.location.search;
+ const queryParams = this._parseQueryString(qs);
+ let title = 'Custom Dashboard';
+ const titleParam = queryParams.find(
+ elem => elem[0].toLowerCase() === 'title');
+ if (titleParam) {
+ title = titleParam[1];
+ }
+ // Dashboards support a foreach param which adds a base query to any
+ // additional query.
+ const forEachParam = queryParams.find(
+ elem => elem[0].toLowerCase() === 'foreach');
+ let forEachQuery = null;
+ if (forEachParam) {
+ forEachQuery = forEachParam[1];
+ }
+ const sectionParams = queryParams.filter(
+ elem => elem[0] && elem[1] && elem[0].toLowerCase() !== 'title' &&
+ elem[0].toLowerCase() !== 'foreach');
+ const sections = sectionParams.map(elem => {
+ const query = forEachQuery ? `${forEachQuery} ${elem[1]}` : elem[1];
+ return {
+ name: elem[0],
+ query,
+ };
+ });
+
+ if (sections.length > 0) {
+ // Custom dashboard view.
+ this._setParams({
+ view: Gerrit.Nav.View.DASHBOARD,
+ user: 'self',
+ sections,
+ title,
+ });
return Promise.resolve();
}
- _handleProjectDashboardRoute(data) {
- const project = data.params[0];
- this._setParams({
- view: Gerrit.Nav.View.DASHBOARD,
- project,
- dashboard: decodeURIComponent(data.params[1]),
- });
- this.$.reporting.setRepoName(project);
- }
+ // Redirect /dashboard/ -> /dashboard/self.
+ this._redirect('/dashboard/self');
+ return Promise.resolve();
+ }
- _handleGroupInfoRoute(data) {
- this._redirect('/admin/groups/' + encodeURIComponent(data.params[0]));
- }
+ _handleProjectDashboardRoute(data) {
+ const project = data.params[0];
+ this._setParams({
+ view: Gerrit.Nav.View.DASHBOARD,
+ project,
+ dashboard: decodeURIComponent(data.params[1]),
+ });
+ this.$.reporting.setRepoName(project);
+ }
- _handleGroupSelfRedirectRoute(data) {
- this._redirect('/settings/#Groups');
- }
+ _handleGroupInfoRoute(data) {
+ this._redirect('/admin/groups/' + encodeURIComponent(data.params[0]));
+ }
- _handleGroupRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.GROUP,
- groupId: data.params[0],
- });
- }
+ _handleGroupSelfRedirectRoute(data) {
+ this._redirect('/settings/#Groups');
+ }
- _handleGroupAuditLogRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.GROUP,
- detail: Gerrit.Nav.GroupDetailView.LOG,
- groupId: data.params[0],
- });
- }
+ _handleGroupRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.GROUP,
+ groupId: data.params[0],
+ });
+ }
- _handleGroupMembersRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.GROUP,
- detail: Gerrit.Nav.GroupDetailView.MEMBERS,
- groupId: data.params[0],
- });
- }
+ _handleGroupAuditLogRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.GROUP,
+ detail: Gerrit.Nav.GroupDetailView.LOG,
+ groupId: data.params[0],
+ });
+ }
- _handleGroupListOffsetRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-group-list',
- offset: data.params[1] || 0,
- filter: null,
- openCreateModal: data.hash === 'create',
- });
- }
+ _handleGroupMembersRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.GROUP,
+ detail: Gerrit.Nav.GroupDetailView.MEMBERS,
+ groupId: data.params[0],
+ });
+ }
- _handleGroupListFilterOffsetRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-group-list',
- offset: data.params.offset,
- filter: data.params.filter,
- });
- }
+ _handleGroupListOffsetRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-admin-group-list',
+ offset: data.params[1] || 0,
+ filter: null,
+ openCreateModal: data.hash === 'create',
+ });
+ }
- _handleGroupListFilterRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-group-list',
- filter: data.params.filter || null,
- });
- }
+ _handleGroupListFilterOffsetRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-admin-group-list',
+ offset: data.params.offset,
+ filter: data.params.filter,
+ });
+ }
- _handleProjectsOldRoute(data) {
- let params = '';
- if (data.params[1]) {
- params = encodeURIComponent(data.params[1]);
- if (data.params[1].includes(',')) {
- params =
- encodeURIComponent(data.params[1]).replace('%2C', ',');
- }
- }
+ _handleGroupListFilterRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-admin-group-list',
+ filter: data.params.filter || null,
+ });
+ }
- this._redirect(`/admin/repos/${params}`);
- }
-
- _handleRepoCommandsRoute(data) {
- const repo = data.params[0];
- this._setParams({
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.COMMANDS,
- repo,
- });
- this.$.reporting.setRepoName(repo);
- }
-
- _handleRepoAccessRoute(data) {
- const repo = data.params[0];
- this._setParams({
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.ACCESS,
- repo,
- });
- this.$.reporting.setRepoName(repo);
- }
-
- _handleRepoDashboardsRoute(data) {
- const repo = data.params[0];
- this._setParams({
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
- repo,
- });
- this.$.reporting.setRepoName(repo);
- }
-
- _handleBranchListOffsetRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.BRANCHES,
- repo: data.params[0],
- offset: data.params[2] || 0,
- filter: null,
- });
- }
-
- _handleBranchListFilterOffsetRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.BRANCHES,
- repo: data.params.repo,
- offset: data.params.offset,
- filter: data.params.filter,
- });
- }
-
- _handleBranchListFilterRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.BRANCHES,
- repo: data.params.repo,
- filter: data.params.filter || null,
- });
- }
-
- _handleTagListOffsetRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.TAGS,
- repo: data.params[0],
- offset: data.params[2] || 0,
- filter: null,
- });
- }
-
- _handleTagListFilterOffsetRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.TAGS,
- repo: data.params.repo,
- offset: data.params.offset,
- filter: data.params.filter,
- });
- }
-
- _handleTagListFilterRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.TAGS,
- repo: data.params.repo,
- filter: data.params.filter || null,
- });
- }
-
- _handleRepoListOffsetRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-repo-list',
- offset: data.params[1] || 0,
- filter: null,
- openCreateModal: data.hash === 'create',
- });
- }
-
- _handleRepoListFilterOffsetRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-repo-list',
- offset: data.params.offset,
- filter: data.params.filter,
- });
- }
-
- _handleRepoListFilterRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-repo-list',
- filter: data.params.filter || null,
- });
- }
-
- _handleCreateProjectRoute(data) {
- // Redirects the legacy route to the new route, which displays the project
- // list with a hash 'create'.
- this._redirect('/admin/repos#create');
- }
-
- _handleCreateGroupRoute(data) {
- // Redirects the legacy route to the new route, which displays the group
- // list with a hash 'create'.
- this._redirect('/admin/groups#create');
- }
-
- _handleRepoRoute(data) {
- const repo = data.params[0];
- this._setParams({
- view: Gerrit.Nav.View.REPO,
- repo,
- });
- this.$.reporting.setRepoName(repo);
- }
-
- _handlePluginListOffsetRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-plugin-list',
- offset: data.params[1] || 0,
- filter: null,
- });
- }
-
- _handlePluginListFilterOffsetRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-plugin-list',
- offset: data.params.offset,
- filter: data.params.filter,
- });
- }
-
- _handlePluginListFilterRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-plugin-list',
- filter: data.params.filter || null,
- });
- }
-
- _handlePluginListRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-plugin-list',
- });
- }
-
- _handleQueryRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.SEARCH,
- query: data.params[0],
- offset: data.params[2],
- });
- }
-
- _handleQueryLegacySuffixRoute(ctx) {
- this._redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, ''));
- }
-
- _handleChangeNumberLegacyRoute(ctx) {
- this._redirect('/c/' + encodeURIComponent(ctx.params[0]));
- }
-
- _handleChangeRoute(ctx) {
- // Parameter order is based on the regex group number matched.
- const params = {
- project: ctx.params[0],
- changeNum: ctx.params[1],
- basePatchNum: ctx.params[4],
- patchNum: ctx.params[6],
- view: Gerrit.Nav.View.CHANGE,
- };
-
- this.$.reporting.setRepoName(params.project);
- this._redirectOrNavigate(params);
- }
-
- _handleDiffRoute(ctx) {
- // Parameter order is based on the regex group number matched.
- const params = {
- project: ctx.params[0],
- changeNum: ctx.params[1],
- basePatchNum: ctx.params[4],
- patchNum: ctx.params[6],
- path: ctx.params[8],
- view: Gerrit.Nav.View.DIFF,
- };
-
- const address = this._parseLineAddress(ctx.hash);
- if (address) {
- params.leftSide = address.leftSide;
- params.lineNum = address.lineNum;
- }
- this.$.reporting.setRepoName(params.project);
- this._redirectOrNavigate(params);
- }
-
- _handleChangeLegacyRoute(ctx) {
- // Parameter order is based on the regex group number matched.
- const params = {
- changeNum: ctx.params[0],
- basePatchNum: ctx.params[3],
- patchNum: ctx.params[5],
- view: Gerrit.Nav.View.CHANGE,
- querystring: ctx.querystring,
- };
-
- this._normalizeLegacyRouteParams(params);
- }
-
- _handleLegacyLinenum(ctx) {
- this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1'));
- }
-
- _handleDiffLegacyRoute(ctx) {
- // Parameter order is based on the regex group number matched.
- const params = {
- changeNum: ctx.params[0],
- basePatchNum: ctx.params[2],
- patchNum: ctx.params[4],
- path: ctx.params[5],
- view: Gerrit.Nav.View.DIFF,
- };
-
- const address = this._parseLineAddress(ctx.hash);
- if (address) {
- params.leftSide = address.leftSide;
- params.lineNum = address.lineNum;
- }
-
- this._normalizeLegacyRouteParams(params);
- }
-
- _handleDiffEditRoute(ctx) {
- // Parameter order is based on the regex group number matched.
- const project = ctx.params[0];
- this._redirectOrNavigate({
- project,
- changeNum: ctx.params[1],
- patchNum: ctx.params[2],
- path: ctx.params[3],
- lineNum: ctx.hash,
- view: Gerrit.Nav.View.EDIT,
- });
- this.$.reporting.setRepoName(project);
- }
-
- _handleChangeEditRoute(ctx) {
- // Parameter order is based on the regex group number matched.
- const project = ctx.params[0];
- this._redirectOrNavigate({
- project,
- changeNum: ctx.params[1],
- patchNum: ctx.params[3],
- view: Gerrit.Nav.View.CHANGE,
- edit: true,
- });
- this.$.reporting.setRepoName(project);
- }
-
- /**
- * Normalize the patch range params for a the change or diff view and
- * redirect if URL upgrade is needed.
- */
- _redirectOrNavigate(params) {
- const needsRedirect = this._normalizePatchRangeParams(params);
- if (needsRedirect) {
- this._redirect(this._generateUrl(params));
- } else {
- this._setParams(params);
+ _handleProjectsOldRoute(data) {
+ let params = '';
+ if (data.params[1]) {
+ params = encodeURIComponent(data.params[1]);
+ if (data.params[1].includes(',')) {
+ params =
+ encodeURIComponent(data.params[1]).replace('%2C', ',');
}
}
- _handleAgreementsRoute() {
- this._redirect('/settings/#Agreements');
+ this._redirect(`/admin/repos/${params}`);
+ }
+
+ _handleRepoCommandsRoute(data) {
+ const repo = data.params[0];
+ this._setParams({
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.COMMANDS,
+ repo,
+ });
+ this.$.reporting.setRepoName(repo);
+ }
+
+ _handleRepoAccessRoute(data) {
+ const repo = data.params[0];
+ this._setParams({
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.ACCESS,
+ repo,
+ });
+ this.$.reporting.setRepoName(repo);
+ }
+
+ _handleRepoDashboardsRoute(data) {
+ const repo = data.params[0];
+ this._setParams({
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
+ repo,
+ });
+ this.$.reporting.setRepoName(repo);
+ }
+
+ _handleBranchListOffsetRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+ repo: data.params[0],
+ offset: data.params[2] || 0,
+ filter: null,
+ });
+ }
+
+ _handleBranchListFilterOffsetRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+ repo: data.params.repo,
+ offset: data.params.offset,
+ filter: data.params.filter,
+ });
+ }
+
+ _handleBranchListFilterRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+ repo: data.params.repo,
+ filter: data.params.filter || null,
+ });
+ }
+
+ _handleTagListOffsetRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.TAGS,
+ repo: data.params[0],
+ offset: data.params[2] || 0,
+ filter: null,
+ });
+ }
+
+ _handleTagListFilterOffsetRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.TAGS,
+ repo: data.params.repo,
+ offset: data.params.offset,
+ filter: data.params.filter,
+ });
+ }
+
+ _handleTagListFilterRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.TAGS,
+ repo: data.params.repo,
+ filter: data.params.filter || null,
+ });
+ }
+
+ _handleRepoListOffsetRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-repo-list',
+ offset: data.params[1] || 0,
+ filter: null,
+ openCreateModal: data.hash === 'create',
+ });
+ }
+
+ _handleRepoListFilterOffsetRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-repo-list',
+ offset: data.params.offset,
+ filter: data.params.filter,
+ });
+ }
+
+ _handleRepoListFilterRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-repo-list',
+ filter: data.params.filter || null,
+ });
+ }
+
+ _handleCreateProjectRoute(data) {
+ // Redirects the legacy route to the new route, which displays the project
+ // list with a hash 'create'.
+ this._redirect('/admin/repos#create');
+ }
+
+ _handleCreateGroupRoute(data) {
+ // Redirects the legacy route to the new route, which displays the group
+ // list with a hash 'create'.
+ this._redirect('/admin/groups#create');
+ }
+
+ _handleRepoRoute(data) {
+ const repo = data.params[0];
+ this._setParams({
+ view: Gerrit.Nav.View.REPO,
+ repo,
+ });
+ this.$.reporting.setRepoName(repo);
+ }
+
+ _handlePluginListOffsetRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-plugin-list',
+ offset: data.params[1] || 0,
+ filter: null,
+ });
+ }
+
+ _handlePluginListFilterOffsetRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-plugin-list',
+ offset: data.params.offset,
+ filter: data.params.filter,
+ });
+ }
+
+ _handlePluginListFilterRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-plugin-list',
+ filter: data.params.filter || null,
+ });
+ }
+
+ _handlePluginListRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-plugin-list',
+ });
+ }
+
+ _handleQueryRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.SEARCH,
+ query: data.params[0],
+ offset: data.params[2],
+ });
+ }
+
+ _handleQueryLegacySuffixRoute(ctx) {
+ this._redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, ''));
+ }
+
+ _handleChangeNumberLegacyRoute(ctx) {
+ this._redirect('/c/' + encodeURIComponent(ctx.params[0]));
+ }
+
+ _handleChangeRoute(ctx) {
+ // Parameter order is based on the regex group number matched.
+ const params = {
+ project: ctx.params[0],
+ changeNum: ctx.params[1],
+ basePatchNum: ctx.params[4],
+ patchNum: ctx.params[6],
+ view: Gerrit.Nav.View.CHANGE,
+ };
+
+ this.$.reporting.setRepoName(params.project);
+ this._redirectOrNavigate(params);
+ }
+
+ _handleDiffRoute(ctx) {
+ // Parameter order is based on the regex group number matched.
+ const params = {
+ project: ctx.params[0],
+ changeNum: ctx.params[1],
+ basePatchNum: ctx.params[4],
+ patchNum: ctx.params[6],
+ path: ctx.params[8],
+ view: Gerrit.Nav.View.DIFF,
+ };
+
+ const address = this._parseLineAddress(ctx.hash);
+ if (address) {
+ params.leftSide = address.leftSide;
+ params.lineNum = address.lineNum;
+ }
+ this.$.reporting.setRepoName(params.project);
+ this._redirectOrNavigate(params);
+ }
+
+ _handleChangeLegacyRoute(ctx) {
+ // Parameter order is based on the regex group number matched.
+ const params = {
+ changeNum: ctx.params[0],
+ basePatchNum: ctx.params[3],
+ patchNum: ctx.params[5],
+ view: Gerrit.Nav.View.CHANGE,
+ querystring: ctx.querystring,
+ };
+
+ this._normalizeLegacyRouteParams(params);
+ }
+
+ _handleLegacyLinenum(ctx) {
+ this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1'));
+ }
+
+ _handleDiffLegacyRoute(ctx) {
+ // Parameter order is based on the regex group number matched.
+ const params = {
+ changeNum: ctx.params[0],
+ basePatchNum: ctx.params[2],
+ patchNum: ctx.params[4],
+ path: ctx.params[5],
+ view: Gerrit.Nav.View.DIFF,
+ };
+
+ const address = this._parseLineAddress(ctx.hash);
+ if (address) {
+ params.leftSide = address.leftSide;
+ params.lineNum = address.lineNum;
}
- _handleNewAgreementsRoute(data) {
- data.params.view = Gerrit.Nav.View.AGREEMENTS;
- this._setParams(data.params);
- }
+ this._normalizeLegacyRouteParams(params);
+ }
- _handleSettingsLegacyRoute(data) {
- // email tokens may contain '+' but no space.
- // The parameter parsing replaces all '+' with a space,
- // undo that to have valid tokens.
- const token = data.params[0].replace(/ /g, '+');
- this._setParams({
- view: Gerrit.Nav.View.SETTINGS,
- emailToken: token,
- });
- }
+ _handleDiffEditRoute(ctx) {
+ // Parameter order is based on the regex group number matched.
+ const project = ctx.params[0];
+ this._redirectOrNavigate({
+ project,
+ changeNum: ctx.params[1],
+ patchNum: ctx.params[2],
+ path: ctx.params[3],
+ lineNum: ctx.hash,
+ view: Gerrit.Nav.View.EDIT,
+ });
+ this.$.reporting.setRepoName(project);
+ }
- _handleSettingsRoute(data) {
- this._setParams({view: Gerrit.Nav.View.SETTINGS});
- }
+ _handleChangeEditRoute(ctx) {
+ // Parameter order is based on the regex group number matched.
+ const project = ctx.params[0];
+ this._redirectOrNavigate({
+ project,
+ changeNum: ctx.params[1],
+ patchNum: ctx.params[3],
+ view: Gerrit.Nav.View.CHANGE,
+ edit: true,
+ });
+ this.$.reporting.setRepoName(project);
+ }
- _handleRegisterRoute(ctx) {
- this._setParams({justRegistered: true});
- let path = ctx.params[0] || '/';
-
- // Prevent redirect looping.
- if (path.startsWith('/register')) { path = '/'; }
-
- if (path[0] !== '/') { return; }
- this._redirect(this.getBaseUrl() + path);
- }
-
- /**
- * Handler for routes that should pass through the router and not be caught
- * by the catchall _handleDefaultRoute handler.
- */
- _handlePassThroughRoute() {
- location.reload();
- }
-
- /**
- * URL may sometimes have /+/ encoded to / /.
- * Context: Issue 6888, Issue 7100
- */
- _handleImproperlyEncodedPlusRoute(ctx) {
- let hash = this._getHashFromCanonicalPath(ctx.canonicalPath);
- if (hash.length) { hash = '#' + hash; }
- this._redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`);
- }
-
- _handlePluginScreen(ctx) {
- const view = Gerrit.Nav.View.PLUGIN_SCREEN;
- const plugin = ctx.params[0];
- const screen = ctx.params[1];
- this._setParams({view, plugin, screen});
- }
-
- _handleDocumentationSearchRoute(data) {
- this._setParams({
- view: Gerrit.Nav.View.DOCUMENTATION_SEARCH,
- filter: data.params.filter || null,
- });
- }
-
- _handleDocumentationSearchRedirectRoute(data) {
- this._redirect('/Documentation/q/filter:' +
- encodeURIComponent(data.params[0]));
- }
-
- _handleDocumentationRedirectRoute(data) {
- if (data.params[1]) {
- location.reload();
- } else {
- // Redirect /Documentation to /Documentation/index.html
- this._redirect('/Documentation/index.html');
- }
- }
-
- /**
- * Catchall route for when no other route is matched.
- */
- _handleDefaultRoute() {
- if (this._isInitialLoad) {
- // Server recognized this route as polygerrit, so we show 404.
- this._show404();
- } else {
- // Route can be recognized by server, so we pass it to server.
- this._handlePassThroughRoute();
- }
- }
-
- _show404() {
- // Note: the app's 404 display is tightly-coupled with catching 404
- // network responses, so we simulate a 404 response status to display it.
- // TODO: Decouple the gr-app error view from network responses.
- this._appElement().dispatchEvent(new CustomEvent('page-error',
- {detail: {response: {status: 404}}}));
+ /**
+ * Normalize the patch range params for a the change or diff view and
+ * redirect if URL upgrade is needed.
+ */
+ _redirectOrNavigate(params) {
+ const needsRedirect = this._normalizePatchRangeParams(params);
+ if (needsRedirect) {
+ this._redirect(this._generateUrl(params));
+ } else {
+ this._setParams(params);
}
}
- customElements.define(GrRouter.is, GrRouter);
-})();
+ _handleAgreementsRoute() {
+ this._redirect('/settings/#Agreements');
+ }
+
+ _handleNewAgreementsRoute(data) {
+ data.params.view = Gerrit.Nav.View.AGREEMENTS;
+ this._setParams(data.params);
+ }
+
+ _handleSettingsLegacyRoute(data) {
+ // email tokens may contain '+' but no space.
+ // The parameter parsing replaces all '+' with a space,
+ // undo that to have valid tokens.
+ const token = data.params[0].replace(/ /g, '+');
+ this._setParams({
+ view: Gerrit.Nav.View.SETTINGS,
+ emailToken: token,
+ });
+ }
+
+ _handleSettingsRoute(data) {
+ this._setParams({view: Gerrit.Nav.View.SETTINGS});
+ }
+
+ _handleRegisterRoute(ctx) {
+ this._setParams({justRegistered: true});
+ let path = ctx.params[0] || '/';
+
+ // Prevent redirect looping.
+ if (path.startsWith('/register')) { path = '/'; }
+
+ if (path[0] !== '/') { return; }
+ this._redirect(this.getBaseUrl() + path);
+ }
+
+ /**
+ * Handler for routes that should pass through the router and not be caught
+ * by the catchall _handleDefaultRoute handler.
+ */
+ _handlePassThroughRoute() {
+ location.reload();
+ }
+
+ /**
+ * URL may sometimes have /+/ encoded to / /.
+ * Context: Issue 6888, Issue 7100
+ */
+ _handleImproperlyEncodedPlusRoute(ctx) {
+ let hash = this._getHashFromCanonicalPath(ctx.canonicalPath);
+ if (hash.length) { hash = '#' + hash; }
+ this._redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`);
+ }
+
+ _handlePluginScreen(ctx) {
+ const view = Gerrit.Nav.View.PLUGIN_SCREEN;
+ const plugin = ctx.params[0];
+ const screen = ctx.params[1];
+ this._setParams({view, plugin, screen});
+ }
+
+ _handleDocumentationSearchRoute(data) {
+ this._setParams({
+ view: Gerrit.Nav.View.DOCUMENTATION_SEARCH,
+ filter: data.params.filter || null,
+ });
+ }
+
+ _handleDocumentationSearchRedirectRoute(data) {
+ this._redirect('/Documentation/q/filter:' +
+ encodeURIComponent(data.params[0]));
+ }
+
+ _handleDocumentationRedirectRoute(data) {
+ if (data.params[1]) {
+ location.reload();
+ } else {
+ // Redirect /Documentation to /Documentation/index.html
+ this._redirect('/Documentation/index.html');
+ }
+ }
+
+ /**
+ * Catchall route for when no other route is matched.
+ */
+ _handleDefaultRoute() {
+ if (this._isInitialLoad) {
+ // Server recognized this route as polygerrit, so we show 404.
+ this._show404();
+ } else {
+ // Route can be recognized by server, so we pass it to server.
+ this._handlePassThroughRoute();
+ }
+ }
+
+ _show404() {
+ // Note: the app's 404 display is tightly-coupled with catching 404
+ // network responses, so we simulate a 404 response status to display it.
+ // TODO: Decouple the gr-app error view from network responses.
+ this._appElement().dispatchEvent(new CustomEvent('page-error',
+ {detail: {response: {status: 404}}}));
+ }
+}
+
+customElements.define(GrRouter.is, GrRouter);
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js b/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js
new file mode 100644
index 0000000..01acaa3
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js
@@ -0,0 +1,22 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ <gr-reporting id="reporting"></gr-reporting>
+`;
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index f127a91..5e98011 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-router</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-router.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,1617 +30,1618 @@
</template>
</test-fixture>
-<script>
- suite('gr-router tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-router.js';
+suite('gr-router tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => { sandbox.restore(); });
+
+ test('_firstCodeBrowserWeblink', () => {
+ assert.deepEqual(element._firstCodeBrowserWeblink([
+ {name: 'gitweb'},
+ {name: 'gitiles'},
+ {name: 'browse'},
+ {name: 'test'}]), {name: 'gitiles'});
+
+ assert.deepEqual(element._firstCodeBrowserWeblink([
+ {name: 'gitweb'},
+ {name: 'test'}]), {name: 'gitweb'});
+ });
+
+ test('_getBrowseCommitWeblink', () => {
+ const browserLink = {name: 'browser', url: 'browser/url'};
+ const link = {name: 'test', url: 'test/url'};
+ const weblinks = [browserLink, link];
+ const config = {gerrit: {primary_weblink_name: browserLink.name}};
+ sandbox.stub(element, '_firstCodeBrowserWeblink').returns(link);
+
+ assert.deepEqual(element._getBrowseCommitWeblink(weblinks, config),
+ browserLink);
+
+ assert.deepEqual(element._getBrowseCommitWeblink(weblinks, {}), link);
+ });
+
+ test('_getChangeWeblinks', () => {
+ const link = {name: 'test', url: 'test/url'};
+ const browserLink = {name: 'browser', url: 'browser/url'};
+ const mapLinksToConfig = weblinks => { return {options: {weblinks}}; };
+ sandbox.stub(element, '_getBrowseCommitWeblink').returns(browserLink);
+
+ assert.deepEqual(
+ element._getChangeWeblinks(mapLinksToConfig([link, browserLink]))[0],
+ {name: 'test', url: 'test/url'});
+
+ assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0],
+ {name: 'test', url: 'test/url'});
+
+ link.url = 'https://' + link.url;
+ assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0],
+ {name: 'test', url: 'https://test/url'});
+ });
+
+ test('_getHashFromCanonicalPath', () => {
+ let url = '/foo/bar';
+ let hash = element._getHashFromCanonicalPath(url);
+ assert.equal(hash, '');
+
+ url = '';
+ hash = element._getHashFromCanonicalPath(url);
+ assert.equal(hash, '');
+
+ url = '/foo#bar';
+ hash = element._getHashFromCanonicalPath(url);
+ assert.equal(hash, 'bar');
+
+ url = '/foo#bar#baz';
+ hash = element._getHashFromCanonicalPath(url);
+ assert.equal(hash, 'bar#baz');
+
+ url = '#foo#bar#baz';
+ hash = element._getHashFromCanonicalPath(url);
+ assert.equal(hash, 'foo#bar#baz');
+ });
+
+ suite('_parseLineAddress', () => {
+ test('returns null for empty and invalid hashes', () => {
+ let actual = element._parseLineAddress('');
+ assert.isNull(actual);
+
+ actual = element._parseLineAddress('foobar');
+ assert.isNull(actual);
+
+ actual = element._parseLineAddress('foo123');
+ assert.isNull(actual);
+
+ actual = element._parseLineAddress('123bar');
+ assert.isNull(actual);
+ });
+
+ test('parses correctly', () => {
+ let actual = element._parseLineAddress('1234');
+ assert.isOk(actual);
+ assert.equal(actual.lineNum, 1234);
+ assert.isFalse(actual.leftSide);
+
+ actual = element._parseLineAddress('a4');
+ assert.isOk(actual);
+ assert.equal(actual.lineNum, 4);
+ assert.isTrue(actual.leftSide);
+
+ actual = element._parseLineAddress('b77');
+ assert.isOk(actual);
+ assert.equal(actual.lineNum, 77);
+ assert.isTrue(actual.leftSide);
+ });
+ });
+
+ test('_startRouter requires auth for the right handlers', () => {
+ // This test encodes the lists of route handler methods that gr-router
+ // automatically checks for authentication before triggering.
+
+ const requiresAuth = {};
+ const doesNotRequireAuth = {};
+ sandbox.stub(Gerrit.Nav, 'setup');
+ sandbox.stub(window.page, 'start');
+ sandbox.stub(window.page, 'base');
+ sandbox.stub(window, 'page');
+ sandbox.stub(element, '_mapRoute', (pattern, methodName, usesAuth) => {
+ if (usesAuth) {
+ requiresAuth[methodName] = true;
+ } else {
+ doesNotRequireAuth[methodName] = true;
+ }
+ });
+ element._startRouter();
+
+ const actualRequiresAuth = Object.keys(requiresAuth);
+ actualRequiresAuth.sort();
+ const actualDoesNotRequireAuth = Object.keys(doesNotRequireAuth);
+ actualDoesNotRequireAuth.sort();
+
+ const shouldRequireAutoAuth = [
+ '_handleAgreementsRoute',
+ '_handleChangeEditRoute',
+ '_handleCreateGroupRoute',
+ '_handleCreateProjectRoute',
+ '_handleDiffEditRoute',
+ '_handleGroupAuditLogRoute',
+ '_handleGroupInfoRoute',
+ '_handleGroupListFilterOffsetRoute',
+ '_handleGroupListFilterRoute',
+ '_handleGroupListOffsetRoute',
+ '_handleGroupMembersRoute',
+ '_handleGroupRoute',
+ '_handleGroupSelfRedirectRoute',
+ '_handleNewAgreementsRoute',
+ '_handlePluginListFilterOffsetRoute',
+ '_handlePluginListFilterRoute',
+ '_handlePluginListOffsetRoute',
+ '_handlePluginListRoute',
+ '_handleRepoCommandsRoute',
+ '_handleSettingsLegacyRoute',
+ '_handleSettingsRoute',
+ ];
+ assert.deepEqual(actualRequiresAuth, shouldRequireAutoAuth);
+
+ const unauthenticatedHandlers = [
+ '_handleBranchListFilterOffsetRoute',
+ '_handleBranchListFilterRoute',
+ '_handleBranchListOffsetRoute',
+ '_handleChangeNumberLegacyRoute',
+ '_handleChangeRoute',
+ '_handleDiffRoute',
+ '_handleDefaultRoute',
+ '_handleChangeLegacyRoute',
+ '_handleDiffLegacyRoute',
+ '_handleDocumentationRedirectRoute',
+ '_handleDocumentationSearchRoute',
+ '_handleDocumentationSearchRedirectRoute',
+ '_handleLegacyLinenum',
+ '_handleImproperlyEncodedPlusRoute',
+ '_handlePassThroughRoute',
+ '_handleProjectDashboardRoute',
+ '_handleProjectsOldRoute',
+ '_handleRepoAccessRoute',
+ '_handleRepoDashboardsRoute',
+ '_handleRepoListFilterOffsetRoute',
+ '_handleRepoListFilterRoute',
+ '_handleRepoListOffsetRoute',
+ '_handleRepoRoute',
+ '_handleQueryLegacySuffixRoute',
+ '_handleQueryRoute',
+ '_handleRegisterRoute',
+ '_handleTagListFilterOffsetRoute',
+ '_handleTagListFilterRoute',
+ '_handleTagListOffsetRoute',
+ '_handlePluginScreen',
+ ];
+
+ // Handler names that check authentication themselves, and thus don't need
+ // it performed for them.
+ const selfAuthenticatingHandlers = [
+ '_handleDashboardRoute',
+ '_handleCustomDashboardRoute',
+ '_handleRootRoute',
+ ];
+
+ const shouldNotRequireAuth = unauthenticatedHandlers
+ .concat(selfAuthenticatingHandlers);
+ shouldNotRequireAuth.sort();
+ assert.deepEqual(actualDoesNotRequireAuth, shouldNotRequireAuth);
+ });
+
+ test('_redirectIfNotLoggedIn while logged in', () => {
+ sandbox.stub(element.$.restAPI, 'getLoggedIn')
+ .returns(Promise.resolve(true));
+ const data = {canonicalPath: ''};
+ const redirectStub = sandbox.stub(element, '_redirectToLogin');
+ return element._redirectIfNotLoggedIn(data).then(() => {
+ assert.isFalse(redirectStub.called);
+ });
+ });
+
+ test('_redirectIfNotLoggedIn while logged out', () => {
+ sandbox.stub(element.$.restAPI, 'getLoggedIn')
+ .returns(Promise.resolve(false));
+ const redirectStub = sandbox.stub(element, '_redirectToLogin');
+ const data = {canonicalPath: ''};
+ return new Promise(resolve => {
+ element._redirectIfNotLoggedIn(data)
+ .then(() => {
+ assert.isTrue(false, 'Should never execute');
+ })
+ .catch(() => {
+ assert.isTrue(redirectStub.calledOnce);
+ resolve();
+ });
+ });
+ });
+
+ suite('generateUrl', () => {
+ test('search', () => {
+ let params = {
+ view: Gerrit.Nav.View.SEARCH,
+ owner: 'a%b',
+ project: 'c%d',
+ branch: 'e%f',
+ topic: 'g%h',
+ statuses: ['op%en'],
+ };
+ assert.equal(element._generateUrl(params),
+ '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
+ 'topic:"g%2525h"+status:op%2525en');
+
+ params.offset = 100;
+ assert.equal(element._generateUrl(params),
+ '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
+ 'topic:"g%2525h"+status:op%2525en,100');
+ delete params.offset;
+
+ // The presence of the query param overrides other params.
+ params.query = 'foo$bar';
+ assert.equal(element._generateUrl(params), '/q/foo%2524bar');
+
+ params.offset = 100;
+ assert.equal(element._generateUrl(params), '/q/foo%2524bar,100');
+
+ params = {
+ view: Gerrit.Nav.View.SEARCH,
+ statuses: ['a', 'b', 'c'],
+ };
+ assert.equal(element._generateUrl(params),
+ '/q/(status:a OR status:b OR status:c)');
+ });
+
+ test('change', () => {
+ const params = {
+ view: Gerrit.Nav.View.CHANGE,
+ changeNum: '1234',
+ project: 'test',
+ };
+ const paramsWithQuery = {
+ view: Gerrit.Nav.View.CHANGE,
+ changeNum: '1234',
+ project: 'test',
+ querystring: 'revert&foo=bar',
+ };
+
+ assert.equal(element._generateUrl(params), '/c/test/+/1234');
+ assert.equal(element._generateUrl(paramsWithQuery),
+ '/c/test/+/1234?revert&foo=bar');
+
+ params.patchNum = 10;
+ assert.equal(element._generateUrl(params), '/c/test/+/1234/10');
+ paramsWithQuery.patchNum = 10;
+ assert.equal(element._generateUrl(paramsWithQuery),
+ '/c/test/+/1234/10?revert&foo=bar');
+
+ params.basePatchNum = 5;
+ assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10');
+ paramsWithQuery.basePatchNum = 5;
+ assert.equal(element._generateUrl(paramsWithQuery),
+ '/c/test/+/1234/5..10?revert&foo=bar');
+
+ params.messageHash = '#123';
+ assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10#123');
+ });
+
+ test('change with repo name encoding', () => {
+ const params = {
+ view: Gerrit.Nav.View.CHANGE,
+ changeNum: '1234',
+ project: 'x+/y+/z+/w',
+ };
+ assert.equal(element._generateUrl(params),
+ '/c/x%252B/y%252B/z%252B/w/+/1234');
+ });
+
+ test('diff', () => {
+ const params = {
+ view: Gerrit.Nav.View.DIFF,
+ changeNum: '42',
+ path: 'x+y/path.cpp',
+ patchNum: 12,
+ };
+ assert.equal(element._generateUrl(params),
+ '/c/42/12/x%252By/path.cpp');
+
+ params.project = 'test';
+ assert.equal(element._generateUrl(params),
+ '/c/test/+/42/12/x%252By/path.cpp');
+
+ params.basePatchNum = 6;
+ assert.equal(element._generateUrl(params),
+ '/c/test/+/42/6..12/x%252By/path.cpp');
+
+ params.path = 'foo bar/my+file.txt%';
+ params.patchNum = 2;
+ delete params.basePatchNum;
+ assert.equal(element._generateUrl(params),
+ '/c/test/+/42/2/foo+bar/my%252Bfile.txt%2525');
+
+ params.path = 'file.cpp';
+ params.lineNum = 123;
+ assert.equal(element._generateUrl(params),
+ '/c/test/+/42/2/file.cpp#123');
+
+ params.leftSide = true;
+ assert.equal(element._generateUrl(params),
+ '/c/test/+/42/2/file.cpp#b123');
+ });
+
+ test('diff with repo name encoding', () => {
+ const params = {
+ view: Gerrit.Nav.View.DIFF,
+ changeNum: '42',
+ path: 'x+y/path.cpp',
+ patchNum: 12,
+ project: 'x+/y',
+ };
+ assert.equal(element._generateUrl(params),
+ '/c/x%252B/y/+/42/12/x%252By/path.cpp');
+ });
+
+ test('edit', () => {
+ const params = {
+ view: Gerrit.Nav.View.EDIT,
+ changeNum: '42',
+ project: 'test',
+ path: 'x+y/path.cpp',
+ };
+ assert.equal(element._generateUrl(params),
+ '/c/test/+/42/x%252By/path.cpp,edit');
+ });
+
+ test('_getPatchRangeExpression', () => {
+ const params = {};
+ let actual = element._getPatchRangeExpression(params);
+ assert.equal(actual, '');
+
+ params.patchNum = 4;
+ actual = element._getPatchRangeExpression(params);
+ assert.equal(actual, '4');
+
+ params.basePatchNum = 2;
+ actual = element._getPatchRangeExpression(params);
+ assert.equal(actual, '2..4');
+
+ delete params.patchNum;
+ actual = element._getPatchRangeExpression(params);
+ assert.equal(actual, '2..');
+ });
+
+ suite('dashboard', () => {
+ test('self dashboard', () => {
+ const params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ };
+ assert.equal(element._generateUrl(params), '/dashboard/self');
+ });
+
+ test('user dashboard', () => {
+ const params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ user: 'user',
+ };
+ assert.equal(element._generateUrl(params), '/dashboard/user');
+ });
+
+ test('custom self dashboard, no title', () => {
+ const params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ sections: [
+ {name: 'section 1', query: 'query 1'},
+ {name: 'section 2', query: 'query 2'},
+ ],
+ };
+ assert.equal(
+ element._generateUrl(params),
+ '/dashboard/?section%201=query%201§ion%202=query%202');
+ });
+
+ test('custom repo dashboard', () => {
+ const params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ sections: [
+ {name: 'section 1', query: 'query 1 ${project}'},
+ {name: 'section 2', query: 'query 2 ${repo}'},
+ ],
+ repo: 'repo-name',
+ };
+ assert.equal(
+ element._generateUrl(params),
+ '/dashboard/?section%201=query%201%20repo-name&' +
+ 'section%202=query%202%20repo-name');
+ });
+
+ test('custom user dashboard, with title', () => {
+ const params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ user: 'user',
+ sections: [{name: 'name', query: 'query'}],
+ title: 'custom dashboard',
+ };
+ assert.equal(
+ element._generateUrl(params),
+ '/dashboard/user?name=query&title=custom%20dashboard');
+ });
+
+ test('repo dashboard', () => {
+ const params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ repo: 'gerrit/repo',
+ dashboard: 'default:main',
+ };
+ assert.equal(
+ element._generateUrl(params),
+ '/p/gerrit/repo/+/dashboard/default:main');
+ });
+
+ test('project dashboard (legacy)', () => {
+ const params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ project: 'gerrit/project',
+ dashboard: 'default:main',
+ };
+ assert.equal(
+ element._generateUrl(params),
+ '/p/gerrit/project/+/dashboard/default:main');
+ });
+ });
+
+ suite('groups', () => {
+ test('group info', () => {
+ const params = {
+ view: Gerrit.Nav.View.GROUP,
+ groupId: 1234,
+ };
+ assert.equal(element._generateUrl(params), '/admin/groups/1234');
+ });
+
+ test('group members', () => {
+ const params = {
+ view: Gerrit.Nav.View.GROUP,
+ groupId: 1234,
+ detail: 'members',
+ };
+ assert.equal(element._generateUrl(params),
+ '/admin/groups/1234,members');
+ });
+
+ test('group audit log', () => {
+ const params = {
+ view: Gerrit.Nav.View.GROUP,
+ groupId: 1234,
+ detail: 'log',
+ };
+ assert.equal(element._generateUrl(params),
+ '/admin/groups/1234,audit-log');
+ });
+ });
+ });
+
+ suite('param normalization', () => {
+ let projectLookupStub;
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
+ projectLookupStub = sandbox
+ .stub(element.$.restAPI, 'getFromProjectLookup');
+ sandbox.stub(element, '_generateUrl');
});
- teardown(() => { sandbox.restore(); });
+ suite('_normalizeLegacyRouteParams', () => {
+ let rangeStub;
+ let redirectStub;
+ let show404Stub;
- test('_firstCodeBrowserWeblink', () => {
- assert.deepEqual(element._firstCodeBrowserWeblink([
- {name: 'gitweb'},
- {name: 'gitiles'},
- {name: 'browse'},
- {name: 'test'}]), {name: 'gitiles'});
-
- assert.deepEqual(element._firstCodeBrowserWeblink([
- {name: 'gitweb'},
- {name: 'test'}]), {name: 'gitweb'});
- });
-
- test('_getBrowseCommitWeblink', () => {
- const browserLink = {name: 'browser', url: 'browser/url'};
- const link = {name: 'test', url: 'test/url'};
- const weblinks = [browserLink, link];
- const config = {gerrit: {primary_weblink_name: browserLink.name}};
- sandbox.stub(element, '_firstCodeBrowserWeblink').returns(link);
-
- assert.deepEqual(element._getBrowseCommitWeblink(weblinks, config),
- browserLink);
-
- assert.deepEqual(element._getBrowseCommitWeblink(weblinks, {}), link);
- });
-
- test('_getChangeWeblinks', () => {
- const link = {name: 'test', url: 'test/url'};
- const browserLink = {name: 'browser', url: 'browser/url'};
- const mapLinksToConfig = weblinks => { return {options: {weblinks}}; };
- sandbox.stub(element, '_getBrowseCommitWeblink').returns(browserLink);
-
- assert.deepEqual(
- element._getChangeWeblinks(mapLinksToConfig([link, browserLink]))[0],
- {name: 'test', url: 'test/url'});
-
- assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0],
- {name: 'test', url: 'test/url'});
-
- link.url = 'https://' + link.url;
- assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0],
- {name: 'test', url: 'https://test/url'});
- });
-
- test('_getHashFromCanonicalPath', () => {
- let url = '/foo/bar';
- let hash = element._getHashFromCanonicalPath(url);
- assert.equal(hash, '');
-
- url = '';
- hash = element._getHashFromCanonicalPath(url);
- assert.equal(hash, '');
-
- url = '/foo#bar';
- hash = element._getHashFromCanonicalPath(url);
- assert.equal(hash, 'bar');
-
- url = '/foo#bar#baz';
- hash = element._getHashFromCanonicalPath(url);
- assert.equal(hash, 'bar#baz');
-
- url = '#foo#bar#baz';
- hash = element._getHashFromCanonicalPath(url);
- assert.equal(hash, 'foo#bar#baz');
- });
-
- suite('_parseLineAddress', () => {
- test('returns null for empty and invalid hashes', () => {
- let actual = element._parseLineAddress('');
- assert.isNull(actual);
-
- actual = element._parseLineAddress('foobar');
- assert.isNull(actual);
-
- actual = element._parseLineAddress('foo123');
- assert.isNull(actual);
-
- actual = element._parseLineAddress('123bar');
- assert.isNull(actual);
+ setup(() => {
+ rangeStub = sandbox.stub(element, '_normalizePatchRangeParams')
+ .returns(Promise.resolve());
+ redirectStub = sandbox.stub(element, '_redirect');
+ show404Stub = sandbox.stub(element, '_show404');
});
- test('parses correctly', () => {
- let actual = element._parseLineAddress('1234');
- assert.isOk(actual);
- assert.equal(actual.lineNum, 1234);
- assert.isFalse(actual.leftSide);
+ test('w/o changeNum', () => {
+ projectLookupStub.returns(Promise.resolve('foo/bar'));
+ const params = {};
+ return element._normalizeLegacyRouteParams(params).then(() => {
+ assert.isFalse(projectLookupStub.called);
+ assert.isFalse(rangeStub.called);
+ assert.isNotOk(params.project);
+ assert.isFalse(redirectStub.called);
+ assert.isFalse(show404Stub.called);
+ });
+ });
- actual = element._parseLineAddress('a4');
- assert.isOk(actual);
- assert.equal(actual.lineNum, 4);
- assert.isTrue(actual.leftSide);
+ test('w/ changeNum', () => {
+ projectLookupStub.returns(Promise.resolve('foo/bar'));
+ const params = {changeNum: 1234};
+ return element._normalizeLegacyRouteParams(params).then(() => {
+ assert.isTrue(projectLookupStub.called);
+ assert.isTrue(rangeStub.called);
+ assert.equal(params.project, 'foo/bar');
+ assert.isTrue(redirectStub.calledOnce);
+ assert.isFalse(show404Stub.called);
+ });
+ });
- actual = element._parseLineAddress('b77');
- assert.isOk(actual);
- assert.equal(actual.lineNum, 77);
- assert.isTrue(actual.leftSide);
+ test('halts on project lookup failure', () => {
+ projectLookupStub.returns(Promise.resolve(undefined));
+ const params = {changeNum: 1234};
+ return element._normalizeLegacyRouteParams(params).then(() => {
+ assert.isTrue(projectLookupStub.called);
+ assert.isFalse(rangeStub.called);
+ assert.isUndefined(params.project);
+ assert.isFalse(redirectStub.called);
+ assert.isTrue(show404Stub.calledOnce);
+ });
});
});
- test('_startRouter requires auth for the right handlers', () => {
- // This test encodes the lists of route handler methods that gr-router
- // automatically checks for authentication before triggering.
+ suite('_normalizePatchRangeParams', () => {
+ test('range n..n normalizes to n', () => {
+ const params = {basePatchNum: 4, patchNum: 4};
+ const needsRedirect = element._normalizePatchRangeParams(params);
+ assert.isTrue(needsRedirect);
+ assert.isNotOk(params.basePatchNum);
+ assert.equal(params.patchNum, 4);
+ });
- const requiresAuth = {};
- const doesNotRequireAuth = {};
+ test('range n.. normalizes to n', () => {
+ const params = {basePatchNum: 4};
+ const needsRedirect = element._normalizePatchRangeParams(params);
+ assert.isFalse(needsRedirect);
+ assert.isNotOk(params.basePatchNum);
+ assert.equal(params.patchNum, 4);
+ });
+ });
+ });
+
+ suite('route handlers', () => {
+ let redirectStub;
+ let setParamsStub;
+ let handlePassThroughRoute;
+
+ // Simple route handlers are direct mappings from parsed route data to a
+ // new set of app.params. This test helper asserts that passing `data`
+ // into `methodName` results in setting the params specified in `params`.
+ function assertDataToParams(data, methodName, params) {
+ element[methodName](data);
+ assert.deepEqual(setParamsStub.lastCall.args[0], params);
+ }
+
+ setup(() => {
+ redirectStub = sandbox.stub(element, '_redirect');
+ setParamsStub = sandbox.stub(element, '_setParams');
+ handlePassThroughRoute = sandbox.stub(element, '_handlePassThroughRoute');
+ });
+
+ test('_handleAgreementsRoute', () => {
+ const data = {params: {}};
+ element._handleAgreementsRoute(data);
+ assert.isTrue(redirectStub.calledOnce);
+ assert.equal(redirectStub.lastCall.args[0], '/settings/#Agreements');
+ });
+
+ test('_handleNewAgreementsRoute', () => {
+ element._handleNewAgreementsRoute({params: {}});
+ assert.isTrue(setParamsStub.calledOnce);
+ assert.equal(setParamsStub.lastCall.args[0].view,
+ Gerrit.Nav.View.AGREEMENTS);
+ });
+
+ test('_handleSettingsLegacyRoute', () => {
+ const data = {params: {0: 'my-token'}};
+ assertDataToParams(data, '_handleSettingsLegacyRoute', {
+ view: Gerrit.Nav.View.SETTINGS,
+ emailToken: 'my-token',
+ });
+ });
+
+ test('_handleSettingsLegacyRoute with +', () => {
+ const data = {params: {0: 'my-token test'}};
+ assertDataToParams(data, '_handleSettingsLegacyRoute', {
+ view: Gerrit.Nav.View.SETTINGS,
+ emailToken: 'my-token+test',
+ });
+ });
+
+ test('_handleSettingsRoute', () => {
+ const data = {};
+ assertDataToParams(data, '_handleSettingsRoute', {
+ view: Gerrit.Nav.View.SETTINGS,
+ });
+ });
+
+ test('_handleDefaultRoute on first load', () => {
+ const appElementStub = {dispatchEvent: sinon.stub()};
+ element._appElement = () => appElementStub;
+ element._handleDefaultRoute();
+ assert.isTrue(appElementStub.dispatchEvent.calledOnce);
+ assert.equal(
+ appElementStub.dispatchEvent.lastCall.args[0].detail.response.status,
+ 404);
+ });
+
+ test('_handleDefaultRoute after internal navigation', () => {
+ let onExit = null;
+ const onRegisteringExit = (match, _onExit) => {
+ onExit = _onExit;
+ };
+ sandbox.stub(window.page, 'exit', onRegisteringExit);
sandbox.stub(Gerrit.Nav, 'setup');
sandbox.stub(window.page, 'start');
sandbox.stub(window.page, 'base');
sandbox.stub(window, 'page');
- sandbox.stub(element, '_mapRoute', (pattern, methodName, usesAuth) => {
- if (usesAuth) {
- requiresAuth[methodName] = true;
- } else {
- doesNotRequireAuth[methodName] = true;
- }
- });
element._startRouter();
- const actualRequiresAuth = Object.keys(requiresAuth);
- actualRequiresAuth.sort();
- const actualDoesNotRequireAuth = Object.keys(doesNotRequireAuth);
- actualDoesNotRequireAuth.sort();
+ const appElementStub = {dispatchEvent: sinon.stub()};
+ element._appElement = () => appElementStub;
+ element._handleDefaultRoute();
- const shouldRequireAutoAuth = [
- '_handleAgreementsRoute',
- '_handleChangeEditRoute',
- '_handleCreateGroupRoute',
- '_handleCreateProjectRoute',
- '_handleDiffEditRoute',
- '_handleGroupAuditLogRoute',
- '_handleGroupInfoRoute',
- '_handleGroupListFilterOffsetRoute',
- '_handleGroupListFilterRoute',
- '_handleGroupListOffsetRoute',
- '_handleGroupMembersRoute',
- '_handleGroupRoute',
- '_handleGroupSelfRedirectRoute',
- '_handleNewAgreementsRoute',
- '_handlePluginListFilterOffsetRoute',
- '_handlePluginListFilterRoute',
- '_handlePluginListOffsetRoute',
- '_handlePluginListRoute',
- '_handleRepoCommandsRoute',
- '_handleSettingsLegacyRoute',
- '_handleSettingsRoute',
- ];
- assert.deepEqual(actualRequiresAuth, shouldRequireAutoAuth);
+ onExit('', () => {}); // we left page;
- const unauthenticatedHandlers = [
- '_handleBranchListFilterOffsetRoute',
- '_handleBranchListFilterRoute',
- '_handleBranchListOffsetRoute',
- '_handleChangeNumberLegacyRoute',
- '_handleChangeRoute',
- '_handleDiffRoute',
- '_handleDefaultRoute',
- '_handleChangeLegacyRoute',
- '_handleDiffLegacyRoute',
- '_handleDocumentationRedirectRoute',
- '_handleDocumentationSearchRoute',
- '_handleDocumentationSearchRedirectRoute',
- '_handleLegacyLinenum',
- '_handleImproperlyEncodedPlusRoute',
- '_handlePassThroughRoute',
- '_handleProjectDashboardRoute',
- '_handleProjectsOldRoute',
- '_handleRepoAccessRoute',
- '_handleRepoDashboardsRoute',
- '_handleRepoListFilterOffsetRoute',
- '_handleRepoListFilterRoute',
- '_handleRepoListOffsetRoute',
- '_handleRepoRoute',
- '_handleQueryLegacySuffixRoute',
- '_handleQueryRoute',
- '_handleRegisterRoute',
- '_handleTagListFilterOffsetRoute',
- '_handleTagListFilterRoute',
- '_handleTagListOffsetRoute',
- '_handlePluginScreen',
- ];
-
- // Handler names that check authentication themselves, and thus don't need
- // it performed for them.
- const selfAuthenticatingHandlers = [
- '_handleDashboardRoute',
- '_handleCustomDashboardRoute',
- '_handleRootRoute',
- ];
-
- const shouldNotRequireAuth = unauthenticatedHandlers
- .concat(selfAuthenticatingHandlers);
- shouldNotRequireAuth.sort();
- assert.deepEqual(actualDoesNotRequireAuth, shouldNotRequireAuth);
+ element._handleDefaultRoute();
+ assert.isTrue(handlePassThroughRoute.calledOnce);
});
- test('_redirectIfNotLoggedIn while logged in', () => {
- sandbox.stub(element.$.restAPI, 'getLoggedIn')
- .returns(Promise.resolve(true));
- const data = {canonicalPath: ''};
- const redirectStub = sandbox.stub(element, '_redirectToLogin');
- return element._redirectIfNotLoggedIn(data).then(() => {
+ test('_handleImproperlyEncodedPlusRoute', () => {
+ // Regression test for Issue 7100.
+ element._handleImproperlyEncodedPlusRoute(
+ {canonicalPath: '/c/test/%20/42', params: ['test', '42']});
+ assert.isTrue(redirectStub.calledOnce);
+ assert.equal(
+ redirectStub.lastCall.args[0],
+ '/c/test/+/42');
+
+ sandbox.stub(element, '_getHashFromCanonicalPath').returns('foo');
+ element._handleImproperlyEncodedPlusRoute(
+ {canonicalPath: '/c/test/%20/42', params: ['test', '42']});
+ assert.equal(
+ redirectStub.lastCall.args[0],
+ '/c/test/+/42#foo');
+ });
+
+ test('_handleQueryRoute', () => {
+ const data = {params: ['project:foo/bar/baz']};
+ assertDataToParams(data, '_handleQueryRoute', {
+ view: Gerrit.Nav.View.SEARCH,
+ query: 'project:foo/bar/baz',
+ offset: undefined,
+ });
+
+ data.params.push(',123', '123');
+ assertDataToParams(data, '_handleQueryRoute', {
+ view: Gerrit.Nav.View.SEARCH,
+ query: 'project:foo/bar/baz',
+ offset: '123',
+ });
+ });
+
+ test('_handleQueryLegacySuffixRoute', () => {
+ element._handleQueryLegacySuffixRoute({path: '/q/foo+bar,n,z'});
+ assert.isTrue(redirectStub.calledOnce);
+ assert.equal(redirectStub.lastCall.args[0], '/q/foo+bar');
+ });
+
+ test('_handleQueryRoute', () => {
+ const data = {params: ['project:foo/bar/baz']};
+ assertDataToParams(data, '_handleQueryRoute', {
+ view: Gerrit.Nav.View.SEARCH,
+ query: 'project:foo/bar/baz',
+ offset: undefined,
+ });
+
+ data.params.push(',123', '123');
+ assertDataToParams(data, '_handleQueryRoute', {
+ view: Gerrit.Nav.View.SEARCH,
+ query: 'project:foo/bar/baz',
+ offset: '123',
+ });
+ });
+
+ suite('_handleRegisterRoute', () => {
+ test('happy path', () => {
+ const ctx = {params: ['/foo/bar']};
+ element._handleRegisterRoute(ctx);
+ assert.isTrue(redirectStub.calledWithExactly('/foo/bar'));
+ assert.isTrue(setParamsStub.calledOnce);
+ assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
+ });
+
+ test('no param', () => {
+ const ctx = {params: ['']};
+ element._handleRegisterRoute(ctx);
+ assert.isTrue(redirectStub.calledWithExactly('/'));
+ assert.isTrue(setParamsStub.calledOnce);
+ assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
+ });
+
+ test('prevent redirect', () => {
+ const ctx = {params: ['/register']};
+ element._handleRegisterRoute(ctx);
+ assert.isTrue(redirectStub.calledWithExactly('/'));
+ assert.isTrue(setParamsStub.calledOnce);
+ assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
+ });
+ });
+
+ suite('_handleRootRoute', () => {
+ test('closes for closeAfterLogin', () => {
+ const data = {querystring: 'closeAfterLogin', canonicalPath: ''};
+ const closeStub = sandbox.stub(window, 'close');
+ const result = element._handleRootRoute(data);
+ assert.isNotOk(result);
+ assert.isTrue(closeStub.called);
assert.isFalse(redirectStub.called);
});
- });
- test('_redirectIfNotLoggedIn while logged out', () => {
- sandbox.stub(element.$.restAPI, 'getLoggedIn')
- .returns(Promise.resolve(false));
- const redirectStub = sandbox.stub(element, '_redirectToLogin');
- const data = {canonicalPath: ''};
- return new Promise(resolve => {
- element._redirectIfNotLoggedIn(data)
- .then(() => {
- assert.isTrue(false, 'Should never execute');
- })
- .catch(() => {
- assert.isTrue(redirectStub.calledOnce);
- resolve();
- });
- });
- });
-
- suite('generateUrl', () => {
- test('search', () => {
- let params = {
- view: Gerrit.Nav.View.SEARCH,
- owner: 'a%b',
- project: 'c%d',
- branch: 'e%f',
- topic: 'g%h',
- statuses: ['op%en'],
+ test('redirects to dashboard if logged in', () => {
+ sandbox.stub(element.$.restAPI, 'getLoggedIn')
+ .returns(Promise.resolve(true));
+ const data = {
+ canonicalPath: '/', path: '/', querystring: '', hash: '',
};
- assert.equal(element._generateUrl(params),
- '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
- 'topic:"g%2525h"+status:op%2525en');
+ const result = element._handleRootRoute(data);
+ assert.isOk(result);
+ return result.then(() => {
+ assert.isTrue(redirectStub.calledWithExactly('/dashboard/self'));
+ });
+ });
- params.offset = 100;
- assert.equal(element._generateUrl(params),
- '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
- 'topic:"g%2525h"+status:op%2525en,100');
- delete params.offset;
-
- // The presence of the query param overrides other params.
- params.query = 'foo$bar';
- assert.equal(element._generateUrl(params), '/q/foo%2524bar');
-
- params.offset = 100;
- assert.equal(element._generateUrl(params), '/q/foo%2524bar,100');
-
- params = {
- view: Gerrit.Nav.View.SEARCH,
- statuses: ['a', 'b', 'c'],
+ test('redirects to open changes if not logged in', () => {
+ sandbox.stub(element.$.restAPI, 'getLoggedIn')
+ .returns(Promise.resolve(false));
+ const data = {
+ canonicalPath: '/', path: '/', querystring: '', hash: '',
};
- assert.equal(element._generateUrl(params),
- '/q/(status:a OR status:b OR status:c)');
+ const result = element._handleRootRoute(data);
+ assert.isOk(result);
+ return result.then(() => {
+ assert.isTrue(redirectStub.calledWithExactly('/q/status:open'));
+ });
});
- test('change', () => {
- const params = {
- view: Gerrit.Nav.View.CHANGE,
- changeNum: '1234',
- project: 'test',
- };
- const paramsWithQuery = {
- view: Gerrit.Nav.View.CHANGE,
- changeNum: '1234',
- project: 'test',
- querystring: 'revert&foo=bar',
- };
-
- assert.equal(element._generateUrl(params), '/c/test/+/1234');
- assert.equal(element._generateUrl(paramsWithQuery),
- '/c/test/+/1234?revert&foo=bar');
-
- params.patchNum = 10;
- assert.equal(element._generateUrl(params), '/c/test/+/1234/10');
- paramsWithQuery.patchNum = 10;
- assert.equal(element._generateUrl(paramsWithQuery),
- '/c/test/+/1234/10?revert&foo=bar');
-
- params.basePatchNum = 5;
- assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10');
- paramsWithQuery.basePatchNum = 5;
- assert.equal(element._generateUrl(paramsWithQuery),
- '/c/test/+/1234/5..10?revert&foo=bar');
-
- params.messageHash = '#123';
- assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10#123');
- });
-
- test('change with repo name encoding', () => {
- const params = {
- view: Gerrit.Nav.View.CHANGE,
- changeNum: '1234',
- project: 'x+/y+/z+/w',
- };
- assert.equal(element._generateUrl(params),
- '/c/x%252B/y%252B/z%252B/w/+/1234');
- });
-
- test('diff', () => {
- const params = {
- view: Gerrit.Nav.View.DIFF,
- changeNum: '42',
- path: 'x+y/path.cpp',
- patchNum: 12,
- };
- assert.equal(element._generateUrl(params),
- '/c/42/12/x%252By/path.cpp');
-
- params.project = 'test';
- assert.equal(element._generateUrl(params),
- '/c/test/+/42/12/x%252By/path.cpp');
-
- params.basePatchNum = 6;
- assert.equal(element._generateUrl(params),
- '/c/test/+/42/6..12/x%252By/path.cpp');
-
- params.path = 'foo bar/my+file.txt%';
- params.patchNum = 2;
- delete params.basePatchNum;
- assert.equal(element._generateUrl(params),
- '/c/test/+/42/2/foo+bar/my%252Bfile.txt%2525');
-
- params.path = 'file.cpp';
- params.lineNum = 123;
- assert.equal(element._generateUrl(params),
- '/c/test/+/42/2/file.cpp#123');
-
- params.leftSide = true;
- assert.equal(element._generateUrl(params),
- '/c/test/+/42/2/file.cpp#b123');
- });
-
- test('diff with repo name encoding', () => {
- const params = {
- view: Gerrit.Nav.View.DIFF,
- changeNum: '42',
- path: 'x+y/path.cpp',
- patchNum: 12,
- project: 'x+/y',
- };
- assert.equal(element._generateUrl(params),
- '/c/x%252B/y/+/42/12/x%252By/path.cpp');
- });
-
- test('edit', () => {
- const params = {
- view: Gerrit.Nav.View.EDIT,
- changeNum: '42',
- project: 'test',
- path: 'x+y/path.cpp',
- };
- assert.equal(element._generateUrl(params),
- '/c/test/+/42/x%252By/path.cpp,edit');
- });
-
- test('_getPatchRangeExpression', () => {
- const params = {};
- let actual = element._getPatchRangeExpression(params);
- assert.equal(actual, '');
-
- params.patchNum = 4;
- actual = element._getPatchRangeExpression(params);
- assert.equal(actual, '4');
-
- params.basePatchNum = 2;
- actual = element._getPatchRangeExpression(params);
- assert.equal(actual, '2..4');
-
- delete params.patchNum;
- actual = element._getPatchRangeExpression(params);
- assert.equal(actual, '2..');
- });
-
- suite('dashboard', () => {
- test('self dashboard', () => {
- const params = {
- view: Gerrit.Nav.View.DASHBOARD,
+ suite('GWT hash-path URLs', () => {
+ test('redirects hash-path URLs', () => {
+ const data = {
+ canonicalPath: '/#/foo/bar/baz',
+ hash: '/foo/bar/baz',
+ querystring: '',
};
- assert.equal(element._generateUrl(params), '/dashboard/self');
- });
-
- test('user dashboard', () => {
- const params = {
- view: Gerrit.Nav.View.DASHBOARD,
- user: 'user',
- };
- assert.equal(element._generateUrl(params), '/dashboard/user');
- });
-
- test('custom self dashboard, no title', () => {
- const params = {
- view: Gerrit.Nav.View.DASHBOARD,
- sections: [
- {name: 'section 1', query: 'query 1'},
- {name: 'section 2', query: 'query 2'},
- ],
- };
- assert.equal(
- element._generateUrl(params),
- '/dashboard/?section%201=query%201§ion%202=query%202');
- });
-
- test('custom repo dashboard', () => {
- const params = {
- view: Gerrit.Nav.View.DASHBOARD,
- sections: [
- {name: 'section 1', query: 'query 1 ${project}'},
- {name: 'section 2', query: 'query 2 ${repo}'},
- ],
- repo: 'repo-name',
- };
- assert.equal(
- element._generateUrl(params),
- '/dashboard/?section%201=query%201%20repo-name&' +
- 'section%202=query%202%20repo-name');
- });
-
- test('custom user dashboard, with title', () => {
- const params = {
- view: Gerrit.Nav.View.DASHBOARD,
- user: 'user',
- sections: [{name: 'name', query: 'query'}],
- title: 'custom dashboard',
- };
- assert.equal(
- element._generateUrl(params),
- '/dashboard/user?name=query&title=custom%20dashboard');
- });
-
- test('repo dashboard', () => {
- const params = {
- view: Gerrit.Nav.View.DASHBOARD,
- repo: 'gerrit/repo',
- dashboard: 'default:main',
- };
- assert.equal(
- element._generateUrl(params),
- '/p/gerrit/repo/+/dashboard/default:main');
- });
-
- test('project dashboard (legacy)', () => {
- const params = {
- view: Gerrit.Nav.View.DASHBOARD,
- project: 'gerrit/project',
- dashboard: 'default:main',
- };
- assert.equal(
- element._generateUrl(params),
- '/p/gerrit/project/+/dashboard/default:main');
- });
- });
-
- suite('groups', () => {
- test('group info', () => {
- const params = {
- view: Gerrit.Nav.View.GROUP,
- groupId: 1234,
- };
- assert.equal(element._generateUrl(params), '/admin/groups/1234');
- });
-
- test('group members', () => {
- const params = {
- view: Gerrit.Nav.View.GROUP,
- groupId: 1234,
- detail: 'members',
- };
- assert.equal(element._generateUrl(params),
- '/admin/groups/1234,members');
- });
-
- test('group audit log', () => {
- const params = {
- view: Gerrit.Nav.View.GROUP,
- groupId: 1234,
- detail: 'log',
- };
- assert.equal(element._generateUrl(params),
- '/admin/groups/1234,audit-log');
- });
- });
- });
-
- suite('param normalization', () => {
- let projectLookupStub;
-
- setup(() => {
- projectLookupStub = sandbox
- .stub(element.$.restAPI, 'getFromProjectLookup');
- sandbox.stub(element, '_generateUrl');
- });
-
- suite('_normalizeLegacyRouteParams', () => {
- let rangeStub;
- let redirectStub;
- let show404Stub;
-
- setup(() => {
- rangeStub = sandbox.stub(element, '_normalizePatchRangeParams')
- .returns(Promise.resolve());
- redirectStub = sandbox.stub(element, '_redirect');
- show404Stub = sandbox.stub(element, '_show404');
- });
-
- test('w/o changeNum', () => {
- projectLookupStub.returns(Promise.resolve('foo/bar'));
- const params = {};
- return element._normalizeLegacyRouteParams(params).then(() => {
- assert.isFalse(projectLookupStub.called);
- assert.isFalse(rangeStub.called);
- assert.isNotOk(params.project);
- assert.isFalse(redirectStub.called);
- assert.isFalse(show404Stub.called);
- });
- });
-
- test('w/ changeNum', () => {
- projectLookupStub.returns(Promise.resolve('foo/bar'));
- const params = {changeNum: 1234};
- return element._normalizeLegacyRouteParams(params).then(() => {
- assert.isTrue(projectLookupStub.called);
- assert.isTrue(rangeStub.called);
- assert.equal(params.project, 'foo/bar');
- assert.isTrue(redirectStub.calledOnce);
- assert.isFalse(show404Stub.called);
- });
- });
-
- test('halts on project lookup failure', () => {
- projectLookupStub.returns(Promise.resolve(undefined));
- const params = {changeNum: 1234};
- return element._normalizeLegacyRouteParams(params).then(() => {
- assert.isTrue(projectLookupStub.called);
- assert.isFalse(rangeStub.called);
- assert.isUndefined(params.project);
- assert.isFalse(redirectStub.called);
- assert.isTrue(show404Stub.calledOnce);
- });
- });
- });
-
- suite('_normalizePatchRangeParams', () => {
- test('range n..n normalizes to n', () => {
- const params = {basePatchNum: 4, patchNum: 4};
- const needsRedirect = element._normalizePatchRangeParams(params);
- assert.isTrue(needsRedirect);
- assert.isNotOk(params.basePatchNum);
- assert.equal(params.patchNum, 4);
- });
-
- test('range n.. normalizes to n', () => {
- const params = {basePatchNum: 4};
- const needsRedirect = element._normalizePatchRangeParams(params);
- assert.isFalse(needsRedirect);
- assert.isNotOk(params.basePatchNum);
- assert.equal(params.patchNum, 4);
- });
- });
- });
-
- suite('route handlers', () => {
- let redirectStub;
- let setParamsStub;
- let handlePassThroughRoute;
-
- // Simple route handlers are direct mappings from parsed route data to a
- // new set of app.params. This test helper asserts that passing `data`
- // into `methodName` results in setting the params specified in `params`.
- function assertDataToParams(data, methodName, params) {
- element[methodName](data);
- assert.deepEqual(setParamsStub.lastCall.args[0], params);
- }
-
- setup(() => {
- redirectStub = sandbox.stub(element, '_redirect');
- setParamsStub = sandbox.stub(element, '_setParams');
- handlePassThroughRoute = sandbox.stub(element, '_handlePassThroughRoute');
- });
-
- test('_handleAgreementsRoute', () => {
- const data = {params: {}};
- element._handleAgreementsRoute(data);
- assert.isTrue(redirectStub.calledOnce);
- assert.equal(redirectStub.lastCall.args[0], '/settings/#Agreements');
- });
-
- test('_handleNewAgreementsRoute', () => {
- element._handleNewAgreementsRoute({params: {}});
- assert.isTrue(setParamsStub.calledOnce);
- assert.equal(setParamsStub.lastCall.args[0].view,
- Gerrit.Nav.View.AGREEMENTS);
- });
-
- test('_handleSettingsLegacyRoute', () => {
- const data = {params: {0: 'my-token'}};
- assertDataToParams(data, '_handleSettingsLegacyRoute', {
- view: Gerrit.Nav.View.SETTINGS,
- emailToken: 'my-token',
- });
- });
-
- test('_handleSettingsLegacyRoute with +', () => {
- const data = {params: {0: 'my-token test'}};
- assertDataToParams(data, '_handleSettingsLegacyRoute', {
- view: Gerrit.Nav.View.SETTINGS,
- emailToken: 'my-token+test',
- });
- });
-
- test('_handleSettingsRoute', () => {
- const data = {};
- assertDataToParams(data, '_handleSettingsRoute', {
- view: Gerrit.Nav.View.SETTINGS,
- });
- });
-
- test('_handleDefaultRoute on first load', () => {
- const appElementStub = {dispatchEvent: sinon.stub()};
- element._appElement = () => appElementStub;
- element._handleDefaultRoute();
- assert.isTrue(appElementStub.dispatchEvent.calledOnce);
- assert.equal(
- appElementStub.dispatchEvent.lastCall.args[0].detail.response.status,
- 404);
- });
-
- test('_handleDefaultRoute after internal navigation', () => {
- let onExit = null;
- const onRegisteringExit = (match, _onExit) => {
- onExit = _onExit;
- };
- sandbox.stub(window.page, 'exit', onRegisteringExit);
- sandbox.stub(Gerrit.Nav, 'setup');
- sandbox.stub(window.page, 'start');
- sandbox.stub(window.page, 'base');
- sandbox.stub(window, 'page');
- element._startRouter();
-
- const appElementStub = {dispatchEvent: sinon.stub()};
- element._appElement = () => appElementStub;
- element._handleDefaultRoute();
-
- onExit('', () => {}); // we left page;
-
- element._handleDefaultRoute();
- assert.isTrue(handlePassThroughRoute.calledOnce);
- });
-
- test('_handleImproperlyEncodedPlusRoute', () => {
- // Regression test for Issue 7100.
- element._handleImproperlyEncodedPlusRoute(
- {canonicalPath: '/c/test/%20/42', params: ['test', '42']});
- assert.isTrue(redirectStub.calledOnce);
- assert.equal(
- redirectStub.lastCall.args[0],
- '/c/test/+/42');
-
- sandbox.stub(element, '_getHashFromCanonicalPath').returns('foo');
- element._handleImproperlyEncodedPlusRoute(
- {canonicalPath: '/c/test/%20/42', params: ['test', '42']});
- assert.equal(
- redirectStub.lastCall.args[0],
- '/c/test/+/42#foo');
- });
-
- test('_handleQueryRoute', () => {
- const data = {params: ['project:foo/bar/baz']};
- assertDataToParams(data, '_handleQueryRoute', {
- view: Gerrit.Nav.View.SEARCH,
- query: 'project:foo/bar/baz',
- offset: undefined,
- });
-
- data.params.push(',123', '123');
- assertDataToParams(data, '_handleQueryRoute', {
- view: Gerrit.Nav.View.SEARCH,
- query: 'project:foo/bar/baz',
- offset: '123',
- });
- });
-
- test('_handleQueryLegacySuffixRoute', () => {
- element._handleQueryLegacySuffixRoute({path: '/q/foo+bar,n,z'});
- assert.isTrue(redirectStub.calledOnce);
- assert.equal(redirectStub.lastCall.args[0], '/q/foo+bar');
- });
-
- test('_handleQueryRoute', () => {
- const data = {params: ['project:foo/bar/baz']};
- assertDataToParams(data, '_handleQueryRoute', {
- view: Gerrit.Nav.View.SEARCH,
- query: 'project:foo/bar/baz',
- offset: undefined,
- });
-
- data.params.push(',123', '123');
- assertDataToParams(data, '_handleQueryRoute', {
- view: Gerrit.Nav.View.SEARCH,
- query: 'project:foo/bar/baz',
- offset: '123',
- });
- });
-
- suite('_handleRegisterRoute', () => {
- test('happy path', () => {
- const ctx = {params: ['/foo/bar']};
- element._handleRegisterRoute(ctx);
- assert.isTrue(redirectStub.calledWithExactly('/foo/bar'));
- assert.isTrue(setParamsStub.calledOnce);
- assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
- });
-
- test('no param', () => {
- const ctx = {params: ['']};
- element._handleRegisterRoute(ctx);
- assert.isTrue(redirectStub.calledWithExactly('/'));
- assert.isTrue(setParamsStub.calledOnce);
- assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
- });
-
- test('prevent redirect', () => {
- const ctx = {params: ['/register']};
- element._handleRegisterRoute(ctx);
- assert.isTrue(redirectStub.calledWithExactly('/'));
- assert.isTrue(setParamsStub.calledOnce);
- assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
- });
- });
-
- suite('_handleRootRoute', () => {
- test('closes for closeAfterLogin', () => {
- const data = {querystring: 'closeAfterLogin', canonicalPath: ''};
- const closeStub = sandbox.stub(window, 'close');
const result = element._handleRootRoute(data);
assert.isNotOk(result);
- assert.isTrue(closeStub.called);
+ assert.isTrue(redirectStub.called);
+ assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz'));
+ });
+
+ test('redirects hash-path URLs w/o leading slash', () => {
+ const data = {
+ canonicalPath: '/#foo/bar/baz',
+ querystring: '',
+ hash: 'foo/bar/baz',
+ };
+ const result = element._handleRootRoute(data);
+ assert.isNotOk(result);
+ assert.isTrue(redirectStub.called);
+ assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz'));
+ });
+
+ test('normalizes "/ /" in hash to "/+/"', () => {
+ const data = {
+ canonicalPath: '/#/foo/bar/+/123/4',
+ querystring: '',
+ hash: '/foo/bar/ /123/4',
+ };
+ const result = element._handleRootRoute(data);
+ assert.isNotOk(result);
+ assert.isTrue(redirectStub.called);
+ assert.isTrue(redirectStub.calledWithExactly('/foo/bar/+/123/4'));
+ });
+
+ test('prepends baseurl to hash-path', () => {
+ const data = {
+ canonicalPath: '/#/foo/bar',
+ querystring: '',
+ hash: '/foo/bar',
+ };
+ sandbox.stub(element, 'getBaseUrl').returns('/baz');
+ const result = element._handleRootRoute(data);
+ assert.isNotOk(result);
+ assert.isTrue(redirectStub.called);
+ assert.isTrue(redirectStub.calledWithExactly('/baz/foo/bar'));
+ });
+
+ test('normalizes /VE/ settings hash-paths', () => {
+ const data = {
+ canonicalPath: '/#/VE/foo/bar',
+ querystring: '',
+ hash: '/VE/foo/bar',
+ };
+ const result = element._handleRootRoute(data);
+ assert.isNotOk(result);
+ assert.isTrue(redirectStub.called);
+ assert.isTrue(redirectStub.calledWithExactly(
+ '/settings/VE/foo/bar'));
+ });
+
+ test('does not drop "inner hashes"', () => {
+ const data = {
+ canonicalPath: '/#/foo/bar#baz',
+ querystring: '',
+ hash: '/foo/bar',
+ };
+ const result = element._handleRootRoute(data);
+ assert.isNotOk(result);
+ assert.isTrue(redirectStub.called);
+ assert.isTrue(redirectStub.calledWithExactly('/foo/bar#baz'));
+ });
+ });
+ });
+
+ suite('_handleDashboardRoute', () => {
+ let redirectToLoginStub;
+
+ setup(() => {
+ redirectToLoginStub = sandbox.stub(element, '_redirectToLogin');
+ });
+
+ test('own dashboard but signed out redirects to login', () => {
+ sandbox.stub(element.$.restAPI, 'getLoggedIn')
+ .returns(Promise.resolve(false));
+ const data = {canonicalPath: '/dashboard/', params: {0: 'seLF'}};
+ return element._handleDashboardRoute(data, '').then(() => {
+ assert.isTrue(redirectToLoginStub.calledOnce);
assert.isFalse(redirectStub.called);
- });
-
- test('redirects to dashboard if logged in', () => {
- sandbox.stub(element.$.restAPI, 'getLoggedIn')
- .returns(Promise.resolve(true));
- const data = {
- canonicalPath: '/', path: '/', querystring: '', hash: '',
- };
- const result = element._handleRootRoute(data);
- assert.isOk(result);
- return result.then(() => {
- assert.isTrue(redirectStub.calledWithExactly('/dashboard/self'));
- });
- });
-
- test('redirects to open changes if not logged in', () => {
- sandbox.stub(element.$.restAPI, 'getLoggedIn')
- .returns(Promise.resolve(false));
- const data = {
- canonicalPath: '/', path: '/', querystring: '', hash: '',
- };
- const result = element._handleRootRoute(data);
- assert.isOk(result);
- return result.then(() => {
- assert.isTrue(redirectStub.calledWithExactly('/q/status:open'));
- });
- });
-
- suite('GWT hash-path URLs', () => {
- test('redirects hash-path URLs', () => {
- const data = {
- canonicalPath: '/#/foo/bar/baz',
- hash: '/foo/bar/baz',
- querystring: '',
- };
- const result = element._handleRootRoute(data);
- assert.isNotOk(result);
- assert.isTrue(redirectStub.called);
- assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz'));
- });
-
- test('redirects hash-path URLs w/o leading slash', () => {
- const data = {
- canonicalPath: '/#foo/bar/baz',
- querystring: '',
- hash: 'foo/bar/baz',
- };
- const result = element._handleRootRoute(data);
- assert.isNotOk(result);
- assert.isTrue(redirectStub.called);
- assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz'));
- });
-
- test('normalizes "/ /" in hash to "/+/"', () => {
- const data = {
- canonicalPath: '/#/foo/bar/+/123/4',
- querystring: '',
- hash: '/foo/bar/ /123/4',
- };
- const result = element._handleRootRoute(data);
- assert.isNotOk(result);
- assert.isTrue(redirectStub.called);
- assert.isTrue(redirectStub.calledWithExactly('/foo/bar/+/123/4'));
- });
-
- test('prepends baseurl to hash-path', () => {
- const data = {
- canonicalPath: '/#/foo/bar',
- querystring: '',
- hash: '/foo/bar',
- };
- sandbox.stub(element, 'getBaseUrl').returns('/baz');
- const result = element._handleRootRoute(data);
- assert.isNotOk(result);
- assert.isTrue(redirectStub.called);
- assert.isTrue(redirectStub.calledWithExactly('/baz/foo/bar'));
- });
-
- test('normalizes /VE/ settings hash-paths', () => {
- const data = {
- canonicalPath: '/#/VE/foo/bar',
- querystring: '',
- hash: '/VE/foo/bar',
- };
- const result = element._handleRootRoute(data);
- assert.isNotOk(result);
- assert.isTrue(redirectStub.called);
- assert.isTrue(redirectStub.calledWithExactly(
- '/settings/VE/foo/bar'));
- });
-
- test('does not drop "inner hashes"', () => {
- const data = {
- canonicalPath: '/#/foo/bar#baz',
- querystring: '',
- hash: '/foo/bar',
- };
- const result = element._handleRootRoute(data);
- assert.isNotOk(result);
- assert.isTrue(redirectStub.called);
- assert.isTrue(redirectStub.calledWithExactly('/foo/bar#baz'));
- });
+ assert.isFalse(setParamsStub.called);
});
});
- suite('_handleDashboardRoute', () => {
- let redirectToLoginStub;
-
- setup(() => {
- redirectToLoginStub = sandbox.stub(element, '_redirectToLogin');
- });
-
- test('own dashboard but signed out redirects to login', () => {
- sandbox.stub(element.$.restAPI, 'getLoggedIn')
- .returns(Promise.resolve(false));
- const data = {canonicalPath: '/dashboard/', params: {0: 'seLF'}};
- return element._handleDashboardRoute(data, '').then(() => {
- assert.isTrue(redirectToLoginStub.calledOnce);
- assert.isFalse(redirectStub.called);
- assert.isFalse(setParamsStub.called);
- });
- });
-
- test('non-self dashboard but signed out does not redirect', () => {
- sandbox.stub(element.$.restAPI, 'getLoggedIn')
- .returns(Promise.resolve(false));
- const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}};
- return element._handleDashboardRoute(data, '').then(() => {
- assert.isFalse(redirectToLoginStub.called);
- assert.isFalse(setParamsStub.called);
- assert.isTrue(redirectStub.calledOnce);
- assert.equal(redirectStub.lastCall.args[0], '/q/owner:foo');
- });
- });
-
- test('dashboard while signed in sets params', () => {
- sandbox.stub(element.$.restAPI, 'getLoggedIn')
- .returns(Promise.resolve(true));
- const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}};
- return element._handleDashboardRoute(data, '').then(() => {
- assert.isFalse(redirectToLoginStub.called);
- assert.isFalse(redirectStub.called);
- assert.isTrue(setParamsStub.calledOnce);
- assert.deepEqual(setParamsStub.lastCall.args[0], {
- view: Gerrit.Nav.View.DASHBOARD,
- user: 'foo',
- });
- });
- });
- });
-
- suite('_handleCustomDashboardRoute', () => {
- let redirectToLoginStub;
-
- setup(() => {
- redirectToLoginStub = sandbox.stub(element, '_redirectToLogin');
- });
-
- test('no user specified', () => {
- const data = {canonicalPath: '/dashboard/', params: {0: ''}};
- return element._handleCustomDashboardRoute(data, '').then(() => {
- assert.isFalse(setParamsStub.called);
- assert.isTrue(redirectStub.called);
- assert.equal(redirectStub.lastCall.args[0], '/dashboard/self');
- });
- });
-
- test('custom dashboard without title', () => {
- const data = {canonicalPath: '/dashboard/', params: {0: ''}};
- return element._handleCustomDashboardRoute(data, '?a=b&c&d=e')
- .then(() => {
- assert.isFalse(redirectStub.called);
- assert.isTrue(setParamsStub.calledOnce);
- assert.deepEqual(setParamsStub.lastCall.args[0], {
- view: Gerrit.Nav.View.DASHBOARD,
- user: 'self',
- sections: [
- {name: 'a', query: 'b'},
- {name: 'd', query: 'e'},
- ],
- title: 'Custom Dashboard',
- });
- });
- });
-
- test('custom dashboard with title', () => {
- const data = {canonicalPath: '/dashboard/', params: {0: ''}};
- return element._handleCustomDashboardRoute(data,
- '?a=b&c&d=&=e&title=t')
- .then(() => {
- assert.isFalse(redirectToLoginStub.called);
- assert.isFalse(redirectStub.called);
- assert.isTrue(setParamsStub.calledOnce);
- assert.deepEqual(setParamsStub.lastCall.args[0], {
- view: Gerrit.Nav.View.DASHBOARD,
- user: 'self',
- sections: [
- {name: 'a', query: 'b'},
- ],
- title: 't',
- });
- });
- });
-
- test('custom dashboard with foreach', () => {
- const data = {canonicalPath: '/dashboard/', params: {0: ''}};
- return element._handleCustomDashboardRoute(data,
- '?a=b&c&d=&=e&foreach=is:open')
- .then(() => {
- assert.isFalse(redirectToLoginStub.called);
- assert.isFalse(redirectStub.called);
- assert.isTrue(setParamsStub.calledOnce);
- assert.deepEqual(setParamsStub.lastCall.args[0], {
- view: Gerrit.Nav.View.DASHBOARD,
- user: 'self',
- sections: [
- {name: 'a', query: 'is:open b'},
- ],
- title: 'Custom Dashboard',
- });
- });
- });
- });
-
- suite('group routes', () => {
- test('_handleGroupInfoRoute', () => {
- const data = {params: {0: 1234}};
- element._handleGroupInfoRoute(data);
+ test('non-self dashboard but signed out does not redirect', () => {
+ sandbox.stub(element.$.restAPI, 'getLoggedIn')
+ .returns(Promise.resolve(false));
+ const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}};
+ return element._handleDashboardRoute(data, '').then(() => {
+ assert.isFalse(redirectToLoginStub.called);
+ assert.isFalse(setParamsStub.called);
assert.isTrue(redirectStub.calledOnce);
- assert.equal(redirectStub.lastCall.args[0], '/admin/groups/1234');
+ assert.equal(redirectStub.lastCall.args[0], '/q/owner:foo');
+ });
+ });
+
+ test('dashboard while signed in sets params', () => {
+ sandbox.stub(element.$.restAPI, 'getLoggedIn')
+ .returns(Promise.resolve(true));
+ const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}};
+ return element._handleDashboardRoute(data, '').then(() => {
+ assert.isFalse(redirectToLoginStub.called);
+ assert.isFalse(redirectStub.called);
+ assert.isTrue(setParamsStub.calledOnce);
+ assert.deepEqual(setParamsStub.lastCall.args[0], {
+ view: Gerrit.Nav.View.DASHBOARD,
+ user: 'foo',
+ });
+ });
+ });
+ });
+
+ suite('_handleCustomDashboardRoute', () => {
+ let redirectToLoginStub;
+
+ setup(() => {
+ redirectToLoginStub = sandbox.stub(element, '_redirectToLogin');
+ });
+
+ test('no user specified', () => {
+ const data = {canonicalPath: '/dashboard/', params: {0: ''}};
+ return element._handleCustomDashboardRoute(data, '').then(() => {
+ assert.isFalse(setParamsStub.called);
+ assert.isTrue(redirectStub.called);
+ assert.equal(redirectStub.lastCall.args[0], '/dashboard/self');
+ });
+ });
+
+ test('custom dashboard without title', () => {
+ const data = {canonicalPath: '/dashboard/', params: {0: ''}};
+ return element._handleCustomDashboardRoute(data, '?a=b&c&d=e')
+ .then(() => {
+ assert.isFalse(redirectStub.called);
+ assert.isTrue(setParamsStub.calledOnce);
+ assert.deepEqual(setParamsStub.lastCall.args[0], {
+ view: Gerrit.Nav.View.DASHBOARD,
+ user: 'self',
+ sections: [
+ {name: 'a', query: 'b'},
+ {name: 'd', query: 'e'},
+ ],
+ title: 'Custom Dashboard',
+ });
+ });
+ });
+
+ test('custom dashboard with title', () => {
+ const data = {canonicalPath: '/dashboard/', params: {0: ''}};
+ return element._handleCustomDashboardRoute(data,
+ '?a=b&c&d=&=e&title=t')
+ .then(() => {
+ assert.isFalse(redirectToLoginStub.called);
+ assert.isFalse(redirectStub.called);
+ assert.isTrue(setParamsStub.calledOnce);
+ assert.deepEqual(setParamsStub.lastCall.args[0], {
+ view: Gerrit.Nav.View.DASHBOARD,
+ user: 'self',
+ sections: [
+ {name: 'a', query: 'b'},
+ ],
+ title: 't',
+ });
+ });
+ });
+
+ test('custom dashboard with foreach', () => {
+ const data = {canonicalPath: '/dashboard/', params: {0: ''}};
+ return element._handleCustomDashboardRoute(data,
+ '?a=b&c&d=&=e&foreach=is:open')
+ .then(() => {
+ assert.isFalse(redirectToLoginStub.called);
+ assert.isFalse(redirectStub.called);
+ assert.isTrue(setParamsStub.calledOnce);
+ assert.deepEqual(setParamsStub.lastCall.args[0], {
+ view: Gerrit.Nav.View.DASHBOARD,
+ user: 'self',
+ sections: [
+ {name: 'a', query: 'is:open b'},
+ ],
+ title: 'Custom Dashboard',
+ });
+ });
+ });
+ });
+
+ suite('group routes', () => {
+ test('_handleGroupInfoRoute', () => {
+ const data = {params: {0: 1234}};
+ element._handleGroupInfoRoute(data);
+ assert.isTrue(redirectStub.calledOnce);
+ assert.equal(redirectStub.lastCall.args[0], '/admin/groups/1234');
+ });
+
+ test('_handleGroupAuditLogRoute', () => {
+ const data = {params: {0: 1234}};
+ assertDataToParams(data, '_handleGroupAuditLogRoute', {
+ view: Gerrit.Nav.View.GROUP,
+ detail: 'log',
+ groupId: 1234,
+ });
+ });
+
+ test('_handleGroupMembersRoute', () => {
+ const data = {params: {0: 1234}};
+ assertDataToParams(data, '_handleGroupMembersRoute', {
+ view: Gerrit.Nav.View.GROUP,
+ detail: 'members',
+ groupId: 1234,
+ });
+ });
+
+ test('_handleGroupListOffsetRoute', () => {
+ const data = {params: {}};
+ assertDataToParams(data, '_handleGroupListOffsetRoute', {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-admin-group-list',
+ offset: 0,
+ filter: null,
+ openCreateModal: false,
});
- test('_handleGroupAuditLogRoute', () => {
- const data = {params: {0: 1234}};
- assertDataToParams(data, '_handleGroupAuditLogRoute', {
- view: Gerrit.Nav.View.GROUP,
- detail: 'log',
- groupId: 1234,
+ data.params[1] = 42;
+ assertDataToParams(data, '_handleGroupListOffsetRoute', {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-admin-group-list',
+ offset: 42,
+ filter: null,
+ openCreateModal: false,
+ });
+
+ data.hash = 'create';
+ assertDataToParams(data, '_handleGroupListOffsetRoute', {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-admin-group-list',
+ offset: 42,
+ filter: null,
+ openCreateModal: true,
+ });
+ });
+
+ test('_handleGroupListFilterOffsetRoute', () => {
+ const data = {params: {filter: 'foo', offset: 42}};
+ assertDataToParams(data, '_handleGroupListFilterOffsetRoute', {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-admin-group-list',
+ offset: 42,
+ filter: 'foo',
+ });
+ });
+
+ test('_handleGroupListFilterRoute', () => {
+ const data = {params: {filter: 'foo'}};
+ assertDataToParams(data, '_handleGroupListFilterRoute', {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-admin-group-list',
+ filter: 'foo',
+ });
+ });
+
+ test('_handleGroupRoute', () => {
+ const data = {params: {0: 4321}};
+ assertDataToParams(data, '_handleGroupRoute', {
+ view: Gerrit.Nav.View.GROUP,
+ groupId: 4321,
+ });
+ });
+ });
+
+ suite('repo routes', () => {
+ test('_handleProjectsOldRoute', () => {
+ const data = {params: {}};
+ element._handleProjectsOldRoute(data);
+ assert.isTrue(redirectStub.calledOnce);
+ assert.equal(redirectStub.lastCall.args[0], '/admin/repos/');
+ });
+
+ test('_handleProjectsOldRoute test', () => {
+ const data = {params: {1: 'test'}};
+ element._handleProjectsOldRoute(data);
+ assert.isTrue(redirectStub.calledOnce);
+ assert.equal(redirectStub.lastCall.args[0], '/admin/repos/test');
+ });
+
+ test('_handleProjectsOldRoute test,branches', () => {
+ const data = {params: {1: 'test,branches'}};
+ element._handleProjectsOldRoute(data);
+ assert.isTrue(redirectStub.calledOnce);
+ assert.equal(
+ redirectStub.lastCall.args[0], '/admin/repos/test,branches');
+ });
+
+ test('_handleRepoRoute', () => {
+ const data = {params: {0: 4321}};
+ assertDataToParams(data, '_handleRepoRoute', {
+ view: Gerrit.Nav.View.REPO,
+ repo: 4321,
+ });
+ });
+
+ test('_handleRepoCommandsRoute', () => {
+ const data = {params: {0: 4321}};
+ assertDataToParams(data, '_handleRepoCommandsRoute', {
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.COMMANDS,
+ repo: 4321,
+ });
+ });
+
+ test('_handleRepoAccessRoute', () => {
+ const data = {params: {0: 4321}};
+ assertDataToParams(data, '_handleRepoAccessRoute', {
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.ACCESS,
+ repo: 4321,
+ });
+ });
+
+ suite('branch list routes', () => {
+ test('_handleBranchListOffsetRoute', () => {
+ const data = {params: {0: 4321}};
+ assertDataToParams(data, '_handleBranchListOffsetRoute', {
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+ repo: 4321,
+ offset: 0,
+ filter: null,
+ });
+
+ data.params[2] = 42;
+ assertDataToParams(data, '_handleBranchListOffsetRoute', {
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+ repo: 4321,
+ offset: 42,
+ filter: null,
});
});
- test('_handleGroupMembersRoute', () => {
- const data = {params: {0: 1234}};
- assertDataToParams(data, '_handleGroupMembersRoute', {
- view: Gerrit.Nav.View.GROUP,
- detail: 'members',
- groupId: 1234,
+ test('_handleBranchListFilterOffsetRoute', () => {
+ const data = {params: {repo: 4321, filter: 'foo', offset: 42}};
+ assertDataToParams(data, '_handleBranchListFilterOffsetRoute', {
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+ repo: 4321,
+ offset: 42,
+ filter: 'foo',
});
});
- test('_handleGroupListOffsetRoute', () => {
+ test('_handleBranchListFilterRoute', () => {
+ const data = {params: {repo: 4321, filter: 'foo'}};
+ assertDataToParams(data, '_handleBranchListFilterRoute', {
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+ repo: 4321,
+ filter: 'foo',
+ });
+ });
+ });
+
+ suite('tag list routes', () => {
+ test('_handleTagListOffsetRoute', () => {
+ const data = {params: {0: 4321}};
+ assertDataToParams(data, '_handleTagListOffsetRoute', {
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.TAGS,
+ repo: 4321,
+ offset: 0,
+ filter: null,
+ });
+ });
+
+ test('_handleTagListFilterOffsetRoute', () => {
+ const data = {params: {repo: 4321, filter: 'foo', offset: 42}};
+ assertDataToParams(data, '_handleTagListFilterOffsetRoute', {
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.TAGS,
+ repo: 4321,
+ offset: 42,
+ filter: 'foo',
+ });
+ });
+
+ test('_handleTagListFilterRoute', () => {
+ const data = {params: {repo: 4321}};
+ assertDataToParams(data, '_handleTagListFilterRoute', {
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.TAGS,
+ repo: 4321,
+ filter: null,
+ });
+
+ data.params.filter = 'foo';
+ assertDataToParams(data, '_handleTagListFilterRoute', {
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.TAGS,
+ repo: 4321,
+ filter: 'foo',
+ });
+ });
+ });
+
+ suite('repo list routes', () => {
+ test('_handleRepoListOffsetRoute', () => {
const data = {params: {}};
- assertDataToParams(data, '_handleGroupListOffsetRoute', {
+ assertDataToParams(data, '_handleRepoListOffsetRoute', {
view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-group-list',
+ adminView: 'gr-repo-list',
offset: 0,
filter: null,
openCreateModal: false,
});
data.params[1] = 42;
- assertDataToParams(data, '_handleGroupListOffsetRoute', {
+ assertDataToParams(data, '_handleRepoListOffsetRoute', {
view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-group-list',
+ adminView: 'gr-repo-list',
offset: 42,
filter: null,
openCreateModal: false,
});
data.hash = 'create';
- assertDataToParams(data, '_handleGroupListOffsetRoute', {
+ assertDataToParams(data, '_handleRepoListOffsetRoute', {
view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-group-list',
+ adminView: 'gr-repo-list',
offset: 42,
filter: null,
openCreateModal: true,
});
});
- test('_handleGroupListFilterOffsetRoute', () => {
+ test('_handleRepoListFilterOffsetRoute', () => {
const data = {params: {filter: 'foo', offset: 42}};
- assertDataToParams(data, '_handleGroupListFilterOffsetRoute', {
+ assertDataToParams(data, '_handleRepoListFilterOffsetRoute', {
view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-group-list',
+ adminView: 'gr-repo-list',
offset: 42,
filter: 'foo',
});
});
- test('_handleGroupListFilterRoute', () => {
- const data = {params: {filter: 'foo'}};
- assertDataToParams(data, '_handleGroupListFilterRoute', {
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-group-list',
- filter: 'foo',
- });
- });
-
- test('_handleGroupRoute', () => {
- const data = {params: {0: 4321}};
- assertDataToParams(data, '_handleGroupRoute', {
- view: Gerrit.Nav.View.GROUP,
- groupId: 4321,
- });
- });
- });
-
- suite('repo routes', () => {
- test('_handleProjectsOldRoute', () => {
+ test('_handleRepoListFilterRoute', () => {
const data = {params: {}};
- element._handleProjectsOldRoute(data);
- assert.isTrue(redirectStub.calledOnce);
- assert.equal(redirectStub.lastCall.args[0], '/admin/repos/');
- });
-
- test('_handleProjectsOldRoute test', () => {
- const data = {params: {1: 'test'}};
- element._handleProjectsOldRoute(data);
- assert.isTrue(redirectStub.calledOnce);
- assert.equal(redirectStub.lastCall.args[0], '/admin/repos/test');
- });
-
- test('_handleProjectsOldRoute test,branches', () => {
- const data = {params: {1: 'test,branches'}};
- element._handleProjectsOldRoute(data);
- assert.isTrue(redirectStub.calledOnce);
- assert.equal(
- redirectStub.lastCall.args[0], '/admin/repos/test,branches');
- });
-
- test('_handleRepoRoute', () => {
- const data = {params: {0: 4321}};
- assertDataToParams(data, '_handleRepoRoute', {
- view: Gerrit.Nav.View.REPO,
- repo: 4321,
- });
- });
-
- test('_handleRepoCommandsRoute', () => {
- const data = {params: {0: 4321}};
- assertDataToParams(data, '_handleRepoCommandsRoute', {
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.COMMANDS,
- repo: 4321,
- });
- });
-
- test('_handleRepoAccessRoute', () => {
- const data = {params: {0: 4321}};
- assertDataToParams(data, '_handleRepoAccessRoute', {
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.ACCESS,
- repo: 4321,
- });
- });
-
- suite('branch list routes', () => {
- test('_handleBranchListOffsetRoute', () => {
- const data = {params: {0: 4321}};
- assertDataToParams(data, '_handleBranchListOffsetRoute', {
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.BRANCHES,
- repo: 4321,
- offset: 0,
- filter: null,
- });
-
- data.params[2] = 42;
- assertDataToParams(data, '_handleBranchListOffsetRoute', {
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.BRANCHES,
- repo: 4321,
- offset: 42,
- filter: null,
- });
- });
-
- test('_handleBranchListFilterOffsetRoute', () => {
- const data = {params: {repo: 4321, filter: 'foo', offset: 42}};
- assertDataToParams(data, '_handleBranchListFilterOffsetRoute', {
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.BRANCHES,
- repo: 4321,
- offset: 42,
- filter: 'foo',
- });
- });
-
- test('_handleBranchListFilterRoute', () => {
- const data = {params: {repo: 4321, filter: 'foo'}};
- assertDataToParams(data, '_handleBranchListFilterRoute', {
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.BRANCHES,
- repo: 4321,
- filter: 'foo',
- });
- });
- });
-
- suite('tag list routes', () => {
- test('_handleTagListOffsetRoute', () => {
- const data = {params: {0: 4321}};
- assertDataToParams(data, '_handleTagListOffsetRoute', {
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.TAGS,
- repo: 4321,
- offset: 0,
- filter: null,
- });
- });
-
- test('_handleTagListFilterOffsetRoute', () => {
- const data = {params: {repo: 4321, filter: 'foo', offset: 42}};
- assertDataToParams(data, '_handleTagListFilterOffsetRoute', {
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.TAGS,
- repo: 4321,
- offset: 42,
- filter: 'foo',
- });
- });
-
- test('_handleTagListFilterRoute', () => {
- const data = {params: {repo: 4321}};
- assertDataToParams(data, '_handleTagListFilterRoute', {
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.TAGS,
- repo: 4321,
- filter: null,
- });
-
- data.params.filter = 'foo';
- assertDataToParams(data, '_handleTagListFilterRoute', {
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.TAGS,
- repo: 4321,
- filter: 'foo',
- });
- });
- });
-
- suite('repo list routes', () => {
- test('_handleRepoListOffsetRoute', () => {
- const data = {params: {}};
- assertDataToParams(data, '_handleRepoListOffsetRoute', {
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-repo-list',
- offset: 0,
- filter: null,
- openCreateModal: false,
- });
-
- data.params[1] = 42;
- assertDataToParams(data, '_handleRepoListOffsetRoute', {
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-repo-list',
- offset: 42,
- filter: null,
- openCreateModal: false,
- });
-
- data.hash = 'create';
- assertDataToParams(data, '_handleRepoListOffsetRoute', {
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-repo-list',
- offset: 42,
- filter: null,
- openCreateModal: true,
- });
- });
-
- test('_handleRepoListFilterOffsetRoute', () => {
- const data = {params: {filter: 'foo', offset: 42}};
- assertDataToParams(data, '_handleRepoListFilterOffsetRoute', {
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-repo-list',
- offset: 42,
- filter: 'foo',
- });
- });
-
- test('_handleRepoListFilterRoute', () => {
- const data = {params: {}};
- assertDataToParams(data, '_handleRepoListFilterRoute', {
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-repo-list',
- filter: null,
- });
-
- data.params.filter = 'foo';
- assertDataToParams(data, '_handleRepoListFilterRoute', {
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-repo-list',
- filter: 'foo',
- });
- });
- });
- });
-
- suite('plugin routes', () => {
- test('_handlePluginListOffsetRoute', () => {
- const data = {params: {}};
- assertDataToParams(data, '_handlePluginListOffsetRoute', {
+ assertDataToParams(data, '_handleRepoListFilterRoute', {
view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-plugin-list',
- offset: 0,
- filter: null,
- });
-
- data.params[1] = 42;
- assertDataToParams(data, '_handlePluginListOffsetRoute', {
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-plugin-list',
- offset: 42,
- filter: null,
- });
- });
-
- test('_handlePluginListFilterOffsetRoute', () => {
- const data = {params: {filter: 'foo', offset: 42}};
- assertDataToParams(data, '_handlePluginListFilterOffsetRoute', {
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-plugin-list',
- offset: 42,
- filter: 'foo',
- });
- });
-
- test('_handlePluginListFilterRoute', () => {
- const data = {params: {}};
- assertDataToParams(data, '_handlePluginListFilterRoute', {
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-plugin-list',
+ adminView: 'gr-repo-list',
filter: null,
});
data.params.filter = 'foo';
- assertDataToParams(data, '_handlePluginListFilterRoute', {
+ assertDataToParams(data, '_handleRepoListFilterRoute', {
view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-plugin-list',
+ adminView: 'gr-repo-list',
filter: 'foo',
});
});
-
- test('_handlePluginListRoute', () => {
- const data = {params: {}};
- assertDataToParams(data, '_handlePluginListRoute', {
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-plugin-list',
- });
- });
- });
-
- suite('change/diff routes', () => {
- test('_handleChangeNumberLegacyRoute', () => {
- const data = {params: {0: 12345}};
- element._handleChangeNumberLegacyRoute(data);
- assert.isTrue(redirectStub.calledOnce);
- assert.isTrue(redirectStub.calledWithExactly('/c/12345'));
- });
-
- test('_handleChangeLegacyRoute', () => {
- const normalizeRouteStub = sandbox.stub(element,
- '_normalizeLegacyRouteParams');
- const ctx = {
- params: [
- 1234, // 0 Change number
- null, // 1 Unused
- null, // 2 Unused
- 6, // 3 Base patch number
- null, // 4 Unused
- 9, // 5 Patch number
- ],
- querystring: '',
- };
- element._handleChangeLegacyRoute(ctx);
- assert.isTrue(normalizeRouteStub.calledOnce);
- assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
- changeNum: 1234,
- basePatchNum: 6,
- patchNum: 9,
- view: Gerrit.Nav.View.CHANGE,
- querystring: '',
- });
- });
-
- test('_handleDiffLegacyRoute', () => {
- const normalizeRouteStub = sandbox.stub(element,
- '_normalizeLegacyRouteParams');
- const ctx = {
- params: [
- 1234, // 0 Change number
- null, // 1 Unused
- 3, // 2 Base patch number
- null, // 3 Unused
- 8, // 4 Patch number
- 'foo/bar', // 5 Diff path
- ],
- path: '/c/1234/3..8/foo/bar',
- hash: 'b123',
- };
- element._handleDiffLegacyRoute(ctx);
- assert.isFalse(redirectStub.called);
- assert.isTrue(normalizeRouteStub.calledOnce);
- assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
- changeNum: 1234,
- basePatchNum: 3,
- patchNum: 8,
- view: Gerrit.Nav.View.DIFF,
- path: 'foo/bar',
- lineNum: 123,
- leftSide: true,
- });
- });
-
- test('_handleLegacyLinenum w/ @321', () => {
- const ctx = {path: '/c/1234/3..8/foo/bar@321'};
- element._handleLegacyLinenum(ctx);
- assert.isTrue(redirectStub.calledOnce);
- assert.isTrue(redirectStub.calledWithExactly(
- '/c/1234/3..8/foo/bar#321'));
- });
-
- test('_handleLegacyLinenum w/ @b123', () => {
- const ctx = {path: '/c/1234/3..8/foo/bar@b123'};
- element._handleLegacyLinenum(ctx);
- assert.isTrue(redirectStub.calledOnce);
- assert.isTrue(redirectStub.calledWithExactly(
- '/c/1234/3..8/foo/bar#b123'));
- });
-
- suite('_handleChangeRoute', () => {
- let normalizeRangeStub;
-
- function makeParams(path, hash) {
- return {
- params: [
- 'foo/bar', // 0 Project
- 1234, // 1 Change number
- null, // 2 Unused
- null, // 3 Unused
- 4, // 4 Base patch number
- null, // 5 Unused
- 7, // 6 Patch number
- ],
- };
- }
-
- setup(() => {
- normalizeRangeStub = sandbox.stub(element,
- '_normalizePatchRangeParams');
- sandbox.stub(element.$.restAPI, 'setInProjectLookup');
- });
-
- test('needs redirect', () => {
- normalizeRangeStub.returns(true);
- sandbox.stub(element, '_generateUrl').returns('foo');
- const ctx = makeParams(null, '');
- element._handleChangeRoute(ctx);
- assert.isTrue(normalizeRangeStub.called);
- assert.isFalse(setParamsStub.called);
- assert.isTrue(redirectStub.calledOnce);
- assert.isTrue(redirectStub.calledWithExactly('foo'));
- });
-
- test('change view', () => {
- normalizeRangeStub.returns(false);
- sandbox.stub(element, '_generateUrl').returns('foo');
- const ctx = makeParams(null, '');
- assertDataToParams(ctx, '_handleChangeRoute', {
- view: Gerrit.Nav.View.CHANGE,
- project: 'foo/bar',
- changeNum: 1234,
- basePatchNum: 4,
- patchNum: 7,
- });
- assert.isFalse(redirectStub.called);
- assert.isTrue(normalizeRangeStub.called);
- });
- });
-
- suite('_handleDiffRoute', () => {
- let normalizeRangeStub;
-
- function makeParams(path, hash) {
- return {
- params: [
- 'foo/bar', // 0 Project
- 1234, // 1 Change number
- null, // 2 Unused
- null, // 3 Unused
- 4, // 4 Base patch number
- null, // 5 Unused
- 7, // 6 Patch number
- null, // 7 Unused,
- path, // 8 Diff path
- ],
- hash,
- };
- }
-
- setup(() => {
- normalizeRangeStub = sandbox.stub(element,
- '_normalizePatchRangeParams');
- sandbox.stub(element.$.restAPI, 'setInProjectLookup');
- });
-
- test('needs redirect', () => {
- normalizeRangeStub.returns(true);
- sandbox.stub(element, '_generateUrl').returns('foo');
- const ctx = makeParams(null, '');
- element._handleDiffRoute(ctx);
- assert.isTrue(normalizeRangeStub.called);
- assert.isFalse(setParamsStub.called);
- assert.isTrue(redirectStub.calledOnce);
- assert.isTrue(redirectStub.calledWithExactly('foo'));
- });
-
- test('diff view', () => {
- normalizeRangeStub.returns(false);
- sandbox.stub(element, '_generateUrl').returns('foo');
- const ctx = makeParams('foo/bar/baz', 'b44');
- assertDataToParams(ctx, '_handleDiffRoute', {
- view: Gerrit.Nav.View.DIFF,
- project: 'foo/bar',
- changeNum: 1234,
- basePatchNum: 4,
- patchNum: 7,
- path: 'foo/bar/baz',
- leftSide: true,
- lineNum: 44,
- });
- assert.isFalse(redirectStub.called);
- assert.isTrue(normalizeRangeStub.called);
- });
- });
-
- test('_handleDiffEditRoute', () => {
- const normalizeRangeSpy =
- sandbox.spy(element, '_normalizePatchRangeParams');
- sandbox.stub(element.$.restAPI, 'setInProjectLookup');
- const ctx = {
- params: [
- 'foo/bar', // 0 Project
- 1234, // 1 Change number
- 3, // 2 Patch num
- 'foo/bar/baz', // 3 File path
- ],
- };
- const appParams = {
- project: 'foo/bar',
- changeNum: 1234,
- view: Gerrit.Nav.View.EDIT,
- path: 'foo/bar/baz',
- patchNum: 3,
- lineNum: undefined,
- };
-
- element._handleDiffEditRoute(ctx);
- assert.isFalse(redirectStub.called);
- assert.isTrue(normalizeRangeSpy.calledOnce);
- assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
- assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
- assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
- });
-
- test('_handleDiffEditRoute with lineNum', () => {
- const normalizeRangeSpy =
- sandbox.spy(element, '_normalizePatchRangeParams');
- sandbox.stub(element.$.restAPI, 'setInProjectLookup');
- const ctx = {
- params: [
- 'foo/bar', // 0 Project
- 1234, // 1 Change number
- 3, // 2 Patch num
- 'foo/bar/baz', // 3 File path
- ],
- hash: 4,
- };
- const appParams = {
- project: 'foo/bar',
- changeNum: 1234,
- view: Gerrit.Nav.View.EDIT,
- path: 'foo/bar/baz',
- patchNum: 3,
- lineNum: 4,
- };
-
- element._handleDiffEditRoute(ctx);
- assert.isFalse(redirectStub.called);
- assert.isTrue(normalizeRangeSpy.calledOnce);
- assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
- assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
- assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
- });
-
- test('_handleChangeEditRoute', () => {
- const normalizeRangeSpy =
- sandbox.spy(element, '_normalizePatchRangeParams');
- sandbox.stub(element.$.restAPI, 'setInProjectLookup');
- const ctx = {
- params: [
- 'foo/bar', // 0 Project
- 1234, // 1 Change number
- null,
- 3, // 3 Patch num
- ],
- };
- const appParams = {
- project: 'foo/bar',
- changeNum: 1234,
- view: Gerrit.Nav.View.CHANGE,
- patchNum: 3,
- edit: true,
- };
-
- element._handleChangeEditRoute(ctx);
- assert.isFalse(redirectStub.called);
- assert.isTrue(normalizeRangeSpy.calledOnce);
- assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
- assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
- assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
- });
- });
-
- test('_handlePluginScreen', () => {
- const ctx = {params: ['foo', 'bar']};
- assertDataToParams(ctx, '_handlePluginScreen', {
- view: Gerrit.Nav.View.PLUGIN_SCREEN,
- plugin: 'foo',
- screen: 'bar',
- });
- assert.isFalse(redirectStub.called);
});
});
- suite('_parseQueryString', () => {
- test('empty queries', () => {
- assert.deepEqual(element._parseQueryString(''), []);
- assert.deepEqual(element._parseQueryString('?'), []);
- assert.deepEqual(element._parseQueryString('??'), []);
- assert.deepEqual(element._parseQueryString('&&&'), []);
+ suite('plugin routes', () => {
+ test('_handlePluginListOffsetRoute', () => {
+ const data = {params: {}};
+ assertDataToParams(data, '_handlePluginListOffsetRoute', {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-plugin-list',
+ offset: 0,
+ filter: null,
+ });
+
+ data.params[1] = 42;
+ assertDataToParams(data, '_handlePluginListOffsetRoute', {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-plugin-list',
+ offset: 42,
+ filter: null,
+ });
});
- test('url decoding', () => {
- assert.deepEqual(element._parseQueryString('+'), [[' ', '']]);
- assert.deepEqual(element._parseQueryString('???+%3d+'), [[' = ', '']]);
- assert.deepEqual(
- element._parseQueryString('%6e%61%6d%65=%76%61%6c%75%65'),
- [['name', 'value']]);
+ test('_handlePluginListFilterOffsetRoute', () => {
+ const data = {params: {filter: 'foo', offset: 42}};
+ assertDataToParams(data, '_handlePluginListFilterOffsetRoute', {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-plugin-list',
+ offset: 42,
+ filter: 'foo',
+ });
});
- test('multiple parameters', () => {
- assert.deepEqual(
- element._parseQueryString('a=b&c=d&e=f'),
- [['a', 'b'], ['c', 'd'], ['e', 'f']]);
- assert.deepEqual(
- element._parseQueryString('&a=b&&&e=f&'),
- [['a', 'b'], ['e', 'f']]);
+ test('_handlePluginListFilterRoute', () => {
+ const data = {params: {}};
+ assertDataToParams(data, '_handlePluginListFilterRoute', {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-plugin-list',
+ filter: null,
+ });
+
+ data.params.filter = 'foo';
+ assertDataToParams(data, '_handlePluginListFilterRoute', {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-plugin-list',
+ filter: 'foo',
+ });
});
+
+ test('_handlePluginListRoute', () => {
+ const data = {params: {}};
+ assertDataToParams(data, '_handlePluginListRoute', {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-plugin-list',
+ });
+ });
+ });
+
+ suite('change/diff routes', () => {
+ test('_handleChangeNumberLegacyRoute', () => {
+ const data = {params: {0: 12345}};
+ element._handleChangeNumberLegacyRoute(data);
+ assert.isTrue(redirectStub.calledOnce);
+ assert.isTrue(redirectStub.calledWithExactly('/c/12345'));
+ });
+
+ test('_handleChangeLegacyRoute', () => {
+ const normalizeRouteStub = sandbox.stub(element,
+ '_normalizeLegacyRouteParams');
+ const ctx = {
+ params: [
+ 1234, // 0 Change number
+ null, // 1 Unused
+ null, // 2 Unused
+ 6, // 3 Base patch number
+ null, // 4 Unused
+ 9, // 5 Patch number
+ ],
+ querystring: '',
+ };
+ element._handleChangeLegacyRoute(ctx);
+ assert.isTrue(normalizeRouteStub.calledOnce);
+ assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
+ changeNum: 1234,
+ basePatchNum: 6,
+ patchNum: 9,
+ view: Gerrit.Nav.View.CHANGE,
+ querystring: '',
+ });
+ });
+
+ test('_handleDiffLegacyRoute', () => {
+ const normalizeRouteStub = sandbox.stub(element,
+ '_normalizeLegacyRouteParams');
+ const ctx = {
+ params: [
+ 1234, // 0 Change number
+ null, // 1 Unused
+ 3, // 2 Base patch number
+ null, // 3 Unused
+ 8, // 4 Patch number
+ 'foo/bar', // 5 Diff path
+ ],
+ path: '/c/1234/3..8/foo/bar',
+ hash: 'b123',
+ };
+ element._handleDiffLegacyRoute(ctx);
+ assert.isFalse(redirectStub.called);
+ assert.isTrue(normalizeRouteStub.calledOnce);
+ assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
+ changeNum: 1234,
+ basePatchNum: 3,
+ patchNum: 8,
+ view: Gerrit.Nav.View.DIFF,
+ path: 'foo/bar',
+ lineNum: 123,
+ leftSide: true,
+ });
+ });
+
+ test('_handleLegacyLinenum w/ @321', () => {
+ const ctx = {path: '/c/1234/3..8/foo/bar@321'};
+ element._handleLegacyLinenum(ctx);
+ assert.isTrue(redirectStub.calledOnce);
+ assert.isTrue(redirectStub.calledWithExactly(
+ '/c/1234/3..8/foo/bar#321'));
+ });
+
+ test('_handleLegacyLinenum w/ @b123', () => {
+ const ctx = {path: '/c/1234/3..8/foo/bar@b123'};
+ element._handleLegacyLinenum(ctx);
+ assert.isTrue(redirectStub.calledOnce);
+ assert.isTrue(redirectStub.calledWithExactly(
+ '/c/1234/3..8/foo/bar#b123'));
+ });
+
+ suite('_handleChangeRoute', () => {
+ let normalizeRangeStub;
+
+ function makeParams(path, hash) {
+ return {
+ params: [
+ 'foo/bar', // 0 Project
+ 1234, // 1 Change number
+ null, // 2 Unused
+ null, // 3 Unused
+ 4, // 4 Base patch number
+ null, // 5 Unused
+ 7, // 6 Patch number
+ ],
+ };
+ }
+
+ setup(() => {
+ normalizeRangeStub = sandbox.stub(element,
+ '_normalizePatchRangeParams');
+ sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+ });
+
+ test('needs redirect', () => {
+ normalizeRangeStub.returns(true);
+ sandbox.stub(element, '_generateUrl').returns('foo');
+ const ctx = makeParams(null, '');
+ element._handleChangeRoute(ctx);
+ assert.isTrue(normalizeRangeStub.called);
+ assert.isFalse(setParamsStub.called);
+ assert.isTrue(redirectStub.calledOnce);
+ assert.isTrue(redirectStub.calledWithExactly('foo'));
+ });
+
+ test('change view', () => {
+ normalizeRangeStub.returns(false);
+ sandbox.stub(element, '_generateUrl').returns('foo');
+ const ctx = makeParams(null, '');
+ assertDataToParams(ctx, '_handleChangeRoute', {
+ view: Gerrit.Nav.View.CHANGE,
+ project: 'foo/bar',
+ changeNum: 1234,
+ basePatchNum: 4,
+ patchNum: 7,
+ });
+ assert.isFalse(redirectStub.called);
+ assert.isTrue(normalizeRangeStub.called);
+ });
+ });
+
+ suite('_handleDiffRoute', () => {
+ let normalizeRangeStub;
+
+ function makeParams(path, hash) {
+ return {
+ params: [
+ 'foo/bar', // 0 Project
+ 1234, // 1 Change number
+ null, // 2 Unused
+ null, // 3 Unused
+ 4, // 4 Base patch number
+ null, // 5 Unused
+ 7, // 6 Patch number
+ null, // 7 Unused,
+ path, // 8 Diff path
+ ],
+ hash,
+ };
+ }
+
+ setup(() => {
+ normalizeRangeStub = sandbox.stub(element,
+ '_normalizePatchRangeParams');
+ sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+ });
+
+ test('needs redirect', () => {
+ normalizeRangeStub.returns(true);
+ sandbox.stub(element, '_generateUrl').returns('foo');
+ const ctx = makeParams(null, '');
+ element._handleDiffRoute(ctx);
+ assert.isTrue(normalizeRangeStub.called);
+ assert.isFalse(setParamsStub.called);
+ assert.isTrue(redirectStub.calledOnce);
+ assert.isTrue(redirectStub.calledWithExactly('foo'));
+ });
+
+ test('diff view', () => {
+ normalizeRangeStub.returns(false);
+ sandbox.stub(element, '_generateUrl').returns('foo');
+ const ctx = makeParams('foo/bar/baz', 'b44');
+ assertDataToParams(ctx, '_handleDiffRoute', {
+ view: Gerrit.Nav.View.DIFF,
+ project: 'foo/bar',
+ changeNum: 1234,
+ basePatchNum: 4,
+ patchNum: 7,
+ path: 'foo/bar/baz',
+ leftSide: true,
+ lineNum: 44,
+ });
+ assert.isFalse(redirectStub.called);
+ assert.isTrue(normalizeRangeStub.called);
+ });
+ });
+
+ test('_handleDiffEditRoute', () => {
+ const normalizeRangeSpy =
+ sandbox.spy(element, '_normalizePatchRangeParams');
+ sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+ const ctx = {
+ params: [
+ 'foo/bar', // 0 Project
+ 1234, // 1 Change number
+ 3, // 2 Patch num
+ 'foo/bar/baz', // 3 File path
+ ],
+ };
+ const appParams = {
+ project: 'foo/bar',
+ changeNum: 1234,
+ view: Gerrit.Nav.View.EDIT,
+ path: 'foo/bar/baz',
+ patchNum: 3,
+ lineNum: undefined,
+ };
+
+ element._handleDiffEditRoute(ctx);
+ assert.isFalse(redirectStub.called);
+ assert.isTrue(normalizeRangeSpy.calledOnce);
+ assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
+ assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
+ assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
+ });
+
+ test('_handleDiffEditRoute with lineNum', () => {
+ const normalizeRangeSpy =
+ sandbox.spy(element, '_normalizePatchRangeParams');
+ sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+ const ctx = {
+ params: [
+ 'foo/bar', // 0 Project
+ 1234, // 1 Change number
+ 3, // 2 Patch num
+ 'foo/bar/baz', // 3 File path
+ ],
+ hash: 4,
+ };
+ const appParams = {
+ project: 'foo/bar',
+ changeNum: 1234,
+ view: Gerrit.Nav.View.EDIT,
+ path: 'foo/bar/baz',
+ patchNum: 3,
+ lineNum: 4,
+ };
+
+ element._handleDiffEditRoute(ctx);
+ assert.isFalse(redirectStub.called);
+ assert.isTrue(normalizeRangeSpy.calledOnce);
+ assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
+ assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
+ assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
+ });
+
+ test('_handleChangeEditRoute', () => {
+ const normalizeRangeSpy =
+ sandbox.spy(element, '_normalizePatchRangeParams');
+ sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+ const ctx = {
+ params: [
+ 'foo/bar', // 0 Project
+ 1234, // 1 Change number
+ null,
+ 3, // 3 Patch num
+ ],
+ };
+ const appParams = {
+ project: 'foo/bar',
+ changeNum: 1234,
+ view: Gerrit.Nav.View.CHANGE,
+ patchNum: 3,
+ edit: true,
+ };
+
+ element._handleChangeEditRoute(ctx);
+ assert.isFalse(redirectStub.called);
+ assert.isTrue(normalizeRangeSpy.calledOnce);
+ assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams);
+ assert.isFalse(normalizeRangeSpy.lastCall.returnValue);
+ assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
+ });
+ });
+
+ test('_handlePluginScreen', () => {
+ const ctx = {params: ['foo', 'bar']};
+ assertDataToParams(ctx, '_handlePluginScreen', {
+ view: Gerrit.Nav.View.PLUGIN_SCREEN,
+ plugin: 'foo',
+ screen: 'bar',
+ });
+ assert.isFalse(redirectStub.called);
});
});
+
+ suite('_parseQueryString', () => {
+ test('empty queries', () => {
+ assert.deepEqual(element._parseQueryString(''), []);
+ assert.deepEqual(element._parseQueryString('?'), []);
+ assert.deepEqual(element._parseQueryString('??'), []);
+ assert.deepEqual(element._parseQueryString('&&&'), []);
+ });
+
+ test('url decoding', () => {
+ assert.deepEqual(element._parseQueryString('+'), [[' ', '']]);
+ assert.deepEqual(element._parseQueryString('???+%3d+'), [[' = ', '']]);
+ assert.deepEqual(
+ element._parseQueryString('%6e%61%6d%65=%76%61%6c%75%65'),
+ [['name', 'value']]);
+ });
+
+ test('multiple parameters', () => {
+ assert.deepEqual(
+ element._parseQueryString('a=b&c=d&e=f'),
+ [['a', 'b'], ['c', 'd'], ['e', 'f']]);
+ assert.deepEqual(
+ element._parseQueryString('&a=b&&&e=f&'),
+ [['a', 'b'], ['e', 'f']]);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
deleted file mode 100644
index cb8e142..0000000
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
+++ /dev/null
@@ -1,54 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-search-bar">
- <template>
- <style include="shared-styles">
- form {
- display: flex;
- }
- gr-autocomplete {
- background-color: var(--view-background-color);
- border-radius: var(--border-radius);
- flex: 1;
- outline: none;
- }
- </style>
- <form>
- <gr-autocomplete
- show-search-icon
- id="searchInput"
- text="{{_inputVal}}"
- query="[[query]]"
- on-commit="_handleInputCommit"
- allow-non-suggested-values
- multi
- threshold="[[_threshold]]"
- tab-complete
- vertical-offset="30"></gr-autocomplete>
- </form>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-search-bar.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index 41caab5..0ed5291 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,320 +14,332 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
- // Possible static search options for auto complete, without negations.
- const SEARCH_OPERATORS = [
- 'added:',
- 'age:',
- 'age:1week', // Give an example age
- 'assignee:',
- 'author:',
- 'branch:',
- 'bug:',
- 'cc:',
- 'cc:self',
- 'change:',
- 'cherrypickof:',
- 'comment:',
- 'commentby:',
- 'commit:',
- 'committer:',
- 'conflicts:',
- 'deleted:',
- 'delta:',
- 'dir:',
- 'directory:',
- 'ext:',
- 'extension:',
- 'file:',
- 'footer:',
- 'from:',
- 'has:',
- 'has:draft',
- 'has:edit',
- 'has:star',
- 'has:stars',
- 'has:unresolved',
- 'hashtag:',
- 'intopic:',
- 'is:',
- 'is:abandoned',
- 'is:assigned',
- 'is:closed',
- 'is:ignored',
- 'is:merged',
- 'is:open',
- 'is:owner',
- 'is:private',
- 'is:reviewed',
- 'is:reviewer',
- 'is:starred',
- 'is:submittable',
- 'is:watched',
- 'is:wip',
- 'label:',
- 'message:',
- 'onlyexts:',
- 'onlyextensions:',
- 'owner:',
- 'ownerin:',
- 'parentproject:',
- 'project:',
- 'projects:',
- 'query:',
- 'ref:',
- 'reviewedby:',
- 'reviewer:',
- 'reviewer:self',
- 'reviewerin:',
- 'size:',
- 'star:',
- 'status:',
- 'status:abandoned',
- 'status:closed',
- 'status:merged',
- 'status:open',
- 'status:reviewed',
- 'submissionid:',
- 'topic:',
- 'tr:',
- ];
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../../scripts/bundled-polymer.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-search-bar_html.js';
- // All of the ops, with corresponding negations.
- const SEARCH_OPERATORS_WITH_NEGATIONS_SET =
- new Set(SEARCH_OPERATORS.concat(SEARCH_OPERATORS.map(op => `-${op}`)));
+// Possible static search options for auto complete, without negations.
+const SEARCH_OPERATORS = [
+ 'added:',
+ 'age:',
+ 'age:1week', // Give an example age
+ 'assignee:',
+ 'author:',
+ 'branch:',
+ 'bug:',
+ 'cc:',
+ 'cc:self',
+ 'change:',
+ 'cherrypickof:',
+ 'comment:',
+ 'commentby:',
+ 'commit:',
+ 'committer:',
+ 'conflicts:',
+ 'deleted:',
+ 'delta:',
+ 'dir:',
+ 'directory:',
+ 'ext:',
+ 'extension:',
+ 'file:',
+ 'footer:',
+ 'from:',
+ 'has:',
+ 'has:draft',
+ 'has:edit',
+ 'has:star',
+ 'has:stars',
+ 'has:unresolved',
+ 'hashtag:',
+ 'intopic:',
+ 'is:',
+ 'is:abandoned',
+ 'is:assigned',
+ 'is:closed',
+ 'is:ignored',
+ 'is:merged',
+ 'is:open',
+ 'is:owner',
+ 'is:private',
+ 'is:reviewed',
+ 'is:reviewer',
+ 'is:starred',
+ 'is:submittable',
+ 'is:watched',
+ 'is:wip',
+ 'label:',
+ 'message:',
+ 'onlyexts:',
+ 'onlyextensions:',
+ 'owner:',
+ 'ownerin:',
+ 'parentproject:',
+ 'project:',
+ 'projects:',
+ 'query:',
+ 'ref:',
+ 'reviewedby:',
+ 'reviewer:',
+ 'reviewer:self',
+ 'reviewerin:',
+ 'size:',
+ 'star:',
+ 'status:',
+ 'status:abandoned',
+ 'status:closed',
+ 'status:merged',
+ 'status:open',
+ 'status:reviewed',
+ 'submissionid:',
+ 'topic:',
+ 'tr:',
+];
- const MAX_AUTOCOMPLETE_RESULTS = 10;
+// All of the ops, with corresponding negations.
+const SEARCH_OPERATORS_WITH_NEGATIONS_SET =
+ new Set(SEARCH_OPERATORS.concat(SEARCH_OPERATORS.map(op => `-${op}`)));
- const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g;
+const MAX_AUTOCOMPLETE_RESULTS = 10;
+const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g;
+
+/**
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrSearchBar extends mixinBehaviors( [
+ Gerrit.KeyboardShortcutBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-search-bar'; }
/**
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
+ * Fired when a search is committed
+ *
+ * @event handle-search
*/
- class GrSearchBar extends Polymer.mixinBehaviors( [
- Gerrit.KeyboardShortcutBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-search-bar'; }
- /**
- * Fired when a search is committed
- *
- * @event handle-search
- */
- static get properties() {
- return {
- value: {
- type: String,
- value: '',
- notify: true,
- observer: '_valueChanged',
+ static get properties() {
+ return {
+ value: {
+ type: String,
+ value: '',
+ notify: true,
+ observer: '_valueChanged',
+ },
+ keyEventTarget: {
+ type: Object,
+ value() { return document.body; },
+ },
+ query: {
+ type: Function,
+ value() {
+ return this._getSearchSuggestions.bind(this);
},
- keyEventTarget: {
- type: Object,
- value() { return document.body; },
+ },
+ projectSuggestions: {
+ type: Function,
+ value() {
+ return () => Promise.resolve([]);
},
- query: {
- type: Function,
- value() {
- return this._getSearchSuggestions.bind(this);
- },
+ },
+ groupSuggestions: {
+ type: Function,
+ value() {
+ return () => Promise.resolve([]);
},
- projectSuggestions: {
- type: Function,
- value() {
- return () => Promise.resolve([]);
- },
+ },
+ accountSuggestions: {
+ type: Function,
+ value() {
+ return () => Promise.resolve([]);
},
- groupSuggestions: {
- type: Function,
- value() {
- return () => Promise.resolve([]);
- },
- },
- accountSuggestions: {
- type: Function,
- value() {
- return () => Promise.resolve([]);
- },
- },
- _inputVal: String,
- _threshold: {
- type: Number,
- value: 1,
- },
- };
- }
+ },
+ _inputVal: String,
+ _threshold: {
+ type: Number,
+ value: 1,
+ },
+ };
+ }
- attached() {
- super.attached();
- this.$.restAPI.getConfig().then(serverConfig => {
- const mergeability = serverConfig
- && serverConfig.index
- && serverConfig.index.mergeabilityComputationBehavior;
- if (mergeability === 'API_REF_UPDATED_AND_CHANGE_REINDEX'
- || mergeability === 'REF_UPDATED_AND_CHANGE_REINDEX') {
- // add 'is:mergeable' to SEARCH_OPERATORS_WITH_NEGATIONS_SET
- this._addOperator('is:mergeable');
- }
- });
- }
-
- _addOperator(name, include_neg = true) {
- SEARCH_OPERATORS_WITH_NEGATIONS_SET.add(name);
- if (include_neg) {
- SEARCH_OPERATORS_WITH_NEGATIONS_SET.add(`-${name}`);
+ attached() {
+ super.attached();
+ this.$.restAPI.getConfig().then(serverConfig => {
+ const mergeability = serverConfig
+ && serverConfig.index
+ && serverConfig.index.mergeabilityComputationBehavior;
+ if (mergeability === 'API_REF_UPDATED_AND_CHANGE_REINDEX'
+ || mergeability === 'REF_UPDATED_AND_CHANGE_REINDEX') {
+ // add 'is:mergeable' to SEARCH_OPERATORS_WITH_NEGATIONS_SET
+ this._addOperator('is:mergeable');
}
- }
+ });
+ }
- keyboardShortcuts() {
- return {
- [this.Shortcut.SEARCH]: '_handleSearch',
- };
- }
-
- _valueChanged(value) {
- this._inputVal = value;
- }
-
- _handleInputCommit(e) {
- this._preventDefaultAndNavigateToInputVal(e);
- }
-
- /**
- * This function is called in a few different cases:
- * - e.target is the search button
- * - e.target is the gr-autocomplete widget (#searchInput)
- * - e.target is the input element wrapped within #searchInput
- *
- * @param {!Event} e
- */
- _preventDefaultAndNavigateToInputVal(e) {
- e.preventDefault();
- const target = Polymer.dom(e).rootTarget;
- // If the target is the #searchInput or has a sub-input component, that
- // is what holds the focus as opposed to the target from the DOM event.
- if (target.$.input) {
- target.$.input.blur();
- } else {
- target.blur();
- }
- const trimmedInput = this._inputVal && this._inputVal.trim();
- if (trimmedInput) {
- const predefinedOpOnlyQuery = [...SEARCH_OPERATORS_WITH_NEGATIONS_SET]
- .some(op => op.endsWith(':') && op === trimmedInput);
- if (predefinedOpOnlyQuery) {
- return;
- }
- this.dispatchEvent(new CustomEvent('handle-search', {
- detail: {inputVal: this._inputVal},
- }));
- }
- }
-
- /**
- * Determine what array of possible suggestions should be provided
- * to _getSearchSuggestions.
- *
- * @param {string} input - The full search term, in lowercase.
- * @return {!Promise} This returns a promise that resolves to an array of
- * suggestion objects.
- */
- _fetchSuggestions(input) {
- // Split the input on colon to get a two part predicate/expression.
- const splitInput = input.split(':');
- const predicate = splitInput[0];
- const expression = splitInput[1] || '';
- // Switch on the predicate to determine what to autocomplete.
- switch (predicate) {
- case 'ownerin':
- case 'reviewerin':
- // Fetch groups.
- return this.groupSuggestions(predicate, expression);
-
- case 'parentproject':
- case 'project':
- // Fetch projects.
- return this.projectSuggestions(predicate, expression);
-
- case 'author':
- case 'cc':
- case 'commentby':
- case 'committer':
- case 'from':
- case 'owner':
- case 'reviewedby':
- case 'reviewer':
- // Fetch accounts.
- return this.accountSuggestions(predicate, expression);
-
- default:
- return Promise.resolve([...SEARCH_OPERATORS_WITH_NEGATIONS_SET]
- .filter(operator => operator.includes(input))
- .map(operator => { return {text: operator}; }));
- }
- }
-
- /**
- * Get the sorted, pruned list of suggestions for the current search query.
- *
- * @param {string} input - The complete search query.
- * @return {!Promise} This returns a promise that resolves to an array of
- * suggestions.
- */
- _getSearchSuggestions(input) {
- // Allow spaces within quoted terms.
- const tokens = input.match(TOKENIZE_REGEX);
- const trimmedInput = tokens[tokens.length - 1].toLowerCase();
-
- return this._fetchSuggestions(trimmedInput)
- .then(suggestions => {
- if (!suggestions || !suggestions.length) { return []; }
- return suggestions
- // Prioritize results that start with the input.
- .sort((a, b) => {
- const aContains = a.text.toLowerCase().indexOf(trimmedInput);
- const bContains = b.text.toLowerCase().indexOf(trimmedInput);
- if (aContains === bContains) {
- return a.text.localeCompare(b.text);
- }
- if (aContains === -1) {
- return 1;
- }
- if (bContains === -1) {
- return -1;
- }
- return aContains - bContains;
- })
- // Return only the first {MAX_AUTOCOMPLETE_RESULTS} results.
- .slice(0, MAX_AUTOCOMPLETE_RESULTS - 1)
- // Map to an object to play nice with gr-autocomplete.
- .map(({text, label}) => {
- return {
- name: text,
- value: text,
- label,
- };
- });
- });
- }
-
- _handleSearch(e) {
- const keyboardEvent = this.getKeyboardEvent(e);
- if (this.shouldSuppressKeyboardShortcut(e) ||
- (this.modifierPressed(e) && !keyboardEvent.shiftKey)) { return; }
-
- e.preventDefault();
- this.$.searchInput.focus();
- this.$.searchInput.selectAll();
+ _addOperator(name, include_neg = true) {
+ SEARCH_OPERATORS_WITH_NEGATIONS_SET.add(name);
+ if (include_neg) {
+ SEARCH_OPERATORS_WITH_NEGATIONS_SET.add(`-${name}`);
}
}
- customElements.define(GrSearchBar.is, GrSearchBar);
-})();
+ keyboardShortcuts() {
+ return {
+ [this.Shortcut.SEARCH]: '_handleSearch',
+ };
+ }
+
+ _valueChanged(value) {
+ this._inputVal = value;
+ }
+
+ _handleInputCommit(e) {
+ this._preventDefaultAndNavigateToInputVal(e);
+ }
+
+ /**
+ * This function is called in a few different cases:
+ * - e.target is the search button
+ * - e.target is the gr-autocomplete widget (#searchInput)
+ * - e.target is the input element wrapped within #searchInput
+ *
+ * @param {!Event} e
+ */
+ _preventDefaultAndNavigateToInputVal(e) {
+ e.preventDefault();
+ const target = dom(e).rootTarget;
+ // If the target is the #searchInput or has a sub-input component, that
+ // is what holds the focus as opposed to the target from the DOM event.
+ if (target.$.input) {
+ target.$.input.blur();
+ } else {
+ target.blur();
+ }
+ const trimmedInput = this._inputVal && this._inputVal.trim();
+ if (trimmedInput) {
+ const predefinedOpOnlyQuery = [...SEARCH_OPERATORS_WITH_NEGATIONS_SET]
+ .some(op => op.endsWith(':') && op === trimmedInput);
+ if (predefinedOpOnlyQuery) {
+ return;
+ }
+ this.dispatchEvent(new CustomEvent('handle-search', {
+ detail: {inputVal: this._inputVal},
+ }));
+ }
+ }
+
+ /**
+ * Determine what array of possible suggestions should be provided
+ * to _getSearchSuggestions.
+ *
+ * @param {string} input - The full search term, in lowercase.
+ * @return {!Promise} This returns a promise that resolves to an array of
+ * suggestion objects.
+ */
+ _fetchSuggestions(input) {
+ // Split the input on colon to get a two part predicate/expression.
+ const splitInput = input.split(':');
+ const predicate = splitInput[0];
+ const expression = splitInput[1] || '';
+ // Switch on the predicate to determine what to autocomplete.
+ switch (predicate) {
+ case 'ownerin':
+ case 'reviewerin':
+ // Fetch groups.
+ return this.groupSuggestions(predicate, expression);
+
+ case 'parentproject':
+ case 'project':
+ // Fetch projects.
+ return this.projectSuggestions(predicate, expression);
+
+ case 'author':
+ case 'cc':
+ case 'commentby':
+ case 'committer':
+ case 'from':
+ case 'owner':
+ case 'reviewedby':
+ case 'reviewer':
+ // Fetch accounts.
+ return this.accountSuggestions(predicate, expression);
+
+ default:
+ return Promise.resolve([...SEARCH_OPERATORS_WITH_NEGATIONS_SET]
+ .filter(operator => operator.includes(input))
+ .map(operator => { return {text: operator}; }));
+ }
+ }
+
+ /**
+ * Get the sorted, pruned list of suggestions for the current search query.
+ *
+ * @param {string} input - The complete search query.
+ * @return {!Promise} This returns a promise that resolves to an array of
+ * suggestions.
+ */
+ _getSearchSuggestions(input) {
+ // Allow spaces within quoted terms.
+ const tokens = input.match(TOKENIZE_REGEX);
+ const trimmedInput = tokens[tokens.length - 1].toLowerCase();
+
+ return this._fetchSuggestions(trimmedInput)
+ .then(suggestions => {
+ if (!suggestions || !suggestions.length) { return []; }
+ return suggestions
+ // Prioritize results that start with the input.
+ .sort((a, b) => {
+ const aContains = a.text.toLowerCase().indexOf(trimmedInput);
+ const bContains = b.text.toLowerCase().indexOf(trimmedInput);
+ if (aContains === bContains) {
+ return a.text.localeCompare(b.text);
+ }
+ if (aContains === -1) {
+ return 1;
+ }
+ if (bContains === -1) {
+ return -1;
+ }
+ return aContains - bContains;
+ })
+ // Return only the first {MAX_AUTOCOMPLETE_RESULTS} results.
+ .slice(0, MAX_AUTOCOMPLETE_RESULTS - 1)
+ // Map to an object to play nice with gr-autocomplete.
+ .map(({text, label}) => {
+ return {
+ name: text,
+ value: text,
+ label,
+ };
+ });
+ });
+ }
+
+ _handleSearch(e) {
+ const keyboardEvent = this.getKeyboardEvent(e);
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ (this.modifierPressed(e) && !keyboardEvent.shiftKey)) { return; }
+
+ e.preventDefault();
+ this.$.searchInput.focus();
+ this.$.searchInput.selectAll();
+ }
+}
+
+customElements.define(GrSearchBar.is, GrSearchBar);
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js
new file mode 100644
index 0000000..831b080
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js
@@ -0,0 +1,35 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ form {
+ display: flex;
+ }
+ gr-autocomplete {
+ background-color: var(--view-background-color);
+ border-radius: var(--border-radius);
+ flex: 1;
+ outline: none;
+ }
+ </style>
+ <form>
+ <gr-autocomplete show-search-icon="" id="searchInput" text="{{_inputVal}}" query="[[query]]" on-commit="_handleInputCommit" allow-non-suggested-values="" multi="" threshold="[[_threshold]]" tab-complete="" vertical-offset="30"></gr-autocomplete>
+ </form>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
index 5b5dc02..c115946 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
@@ -19,18 +19,18 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-search-bar</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="/bower_components/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script src="/node_modules/page/page.js"></script>
-<link rel="import" href="gr-search-bar.html">
-<script src="../../../scripts/util.js"></script>
-
-<script>void (0);</script>
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-search-bar.js';
+import '../../../scripts/util.js';
+void (0);
+</script>
<test-fixture id="basic">
<template>
@@ -38,199 +38,201 @@
</template>
</test-fixture>
-<script>
- suite('gr-search-bar tests', async () => {
- await readyToTest();
- const kb = window.Gerrit.KeyboardShortcutBinder;
- kb.bindShortcut(kb.Shortcut.SEARCH, '/');
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-search-bar.js';
+import '../../../scripts/util.js';
+suite('gr-search-bar tests', () => {
+ const kb = window.Gerrit.KeyboardShortcutBinder;
+ kb.bindShortcut(kb.Shortcut.SEARCH, '/');
- let element;
- let sandbox;
+ let element;
+ let sandbox;
- setup(done => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- flush(done);
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ flush(done);
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('value is propagated to _inputVal', () => {
+ element.value = 'foo';
+ assert.equal(element._inputVal, 'foo');
+ });
+
+ const getActiveElement = () => (document.activeElement.shadowRoot ?
+ document.activeElement.shadowRoot.activeElement :
+ document.activeElement);
+
+ test('enter in search input fires event', done => {
+ element.addEventListener('handle-search', () => {
+ assert.notEqual(getActiveElement(), element.$.searchInput);
+ assert.notEqual(getActiveElement(), element.$.searchButton);
+ done();
+ });
+ element.value = 'test';
+ MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+ null, 'enter');
+ });
+
+ test('input blurred after commit', () => {
+ const blurSpy = sandbox.spy(element.$.searchInput.$.input, 'blur');
+ element.$.searchInput.text = 'fate/stay';
+ MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+ null, 'enter');
+ assert.isTrue(blurSpy.called);
+ });
+
+ test('empty search query does not trigger nav', () => {
+ const searchSpy = sandbox.spy();
+ element.addEventListener('handle-search', searchSpy);
+ element.value = '';
+ MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+ null, 'enter');
+ assert.isFalse(searchSpy.called);
+ });
+
+ test('Predefined query op with no predication doesnt trigger nav', () => {
+ const searchSpy = sandbox.spy();
+ element.addEventListener('handle-search', searchSpy);
+ element.value = 'added:';
+ MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+ null, 'enter');
+ assert.isFalse(searchSpy.called);
+ });
+
+ test('predefined predicate query triggers nav', () => {
+ const searchSpy = sandbox.spy();
+ element.addEventListener('handle-search', searchSpy);
+ element.value = 'age:1week';
+ MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+ null, 'enter');
+ assert.isTrue(searchSpy.called);
+ });
+
+ test('undefined predicate query triggers nav', () => {
+ const searchSpy = sandbox.spy();
+ element.addEventListener('handle-search', searchSpy);
+ element.value = 'random:1week';
+ MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+ null, 'enter');
+ assert.isTrue(searchSpy.called);
+ });
+
+ test('empty undefined predicate query triggers nav', () => {
+ const searchSpy = sandbox.spy();
+ element.addEventListener('handle-search', searchSpy);
+ element.value = 'random:';
+ MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+ null, 'enter');
+ assert.isTrue(searchSpy.called);
+ });
+
+ test('keyboard shortcuts', () => {
+ const focusSpy = sandbox.spy(element.$.searchInput, 'focus');
+ const selectAllSpy = sandbox.spy(element.$.searchInput, 'selectAll');
+ MockInteractions.pressAndReleaseKeyOn(document.body, 191, null, '/');
+ assert.isTrue(focusSpy.called);
+ assert.isTrue(selectAllSpy.called);
+ });
+
+ suite('_getSearchSuggestions', () => {
+ test('Autocompletes accounts', () => {
+ sandbox.stub(element, 'accountSuggestions', () =>
+ Promise.resolve([{text: 'owner:fred@goog.co'}])
+ );
+ return element._getSearchSuggestions('owner:fr').then(s => {
+ assert.equal(s[0].value, 'owner:fred@goog.co');
+ });
});
- teardown(() => {
- sandbox.restore();
- });
-
- test('value is propagated to _inputVal', () => {
- element.value = 'foo';
- assert.equal(element._inputVal, 'foo');
- });
-
- const getActiveElement = () => (document.activeElement.shadowRoot ?
- document.activeElement.shadowRoot.activeElement :
- document.activeElement);
-
- test('enter in search input fires event', done => {
- element.addEventListener('handle-search', () => {
- assert.notEqual(getActiveElement(), element.$.searchInput);
- assert.notEqual(getActiveElement(), element.$.searchButton);
+ test('Autocompletes groups', done => {
+ sandbox.stub(element, 'groupSuggestions', () =>
+ Promise.resolve([
+ {text: 'ownerin:Polygerrit'},
+ {text: 'ownerin:gerrit'},
+ ])
+ );
+ element._getSearchSuggestions('ownerin:pol').then(s => {
+ assert.equal(s[0].value, 'ownerin:Polygerrit');
done();
});
- element.value = 'test';
- MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
- null, 'enter');
});
- test('input blurred after commit', () => {
- const blurSpy = sandbox.spy(element.$.searchInput.$.input, 'blur');
- element.$.searchInput.text = 'fate/stay';
- MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
- null, 'enter');
- assert.isTrue(blurSpy.called);
+ test('Autocompletes projects', done => {
+ sandbox.stub(element, 'projectSuggestions', () =>
+ Promise.resolve([
+ {text: 'project:Polygerrit'},
+ {text: 'project:gerrit'},
+ {text: 'project:gerrittest'},
+ ])
+ );
+ element._getSearchSuggestions('project:pol').then(s => {
+ assert.equal(s[0].value, 'project:Polygerrit');
+ done();
+ });
});
- test('empty search query does not trigger nav', () => {
- const searchSpy = sandbox.spy();
- element.addEventListener('handle-search', searchSpy);
- element.value = '';
- MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
- null, 'enter');
- assert.isFalse(searchSpy.called);
+ test('Autocompletes simple searches', done => {
+ element._getSearchSuggestions('is:o').then(s => {
+ assert.equal(s[0].name, 'is:open');
+ assert.equal(s[0].value, 'is:open');
+ assert.equal(s[1].name, 'is:owner');
+ assert.equal(s[1].value, 'is:owner');
+ done();
+ });
});
- test('Predefined query op with no predication doesnt trigger nav', () => {
- const searchSpy = sandbox.spy();
- element.addEventListener('handle-search', searchSpy);
- element.value = 'added:';
- MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
- null, 'enter');
- assert.isFalse(searchSpy.called);
+ test('Does not autocomplete with no match', done => {
+ element._getSearchSuggestions('asdasdasdasd').then(s => {
+ assert.equal(s.length, 0);
+ done();
+ });
});
- test('predefined predicate query triggers nav', () => {
- const searchSpy = sandbox.spy();
- element.addEventListener('handle-search', searchSpy);
- element.value = 'age:1week';
- MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
- null, 'enter');
- assert.isTrue(searchSpy.called);
+ test('Autocompltes without is:mergable when disabled', done => {
+ element._getSearchSuggestions('is:mergeab').then(s => {
+ assert.equal(s.length, 0);
+ done();
+ });
});
+ });
- test('undefined predicate query triggers nav', () => {
- const searchSpy = sandbox.spy();
- element.addEventListener('handle-search', searchSpy);
- element.value = 'random:1week';
- MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
- null, 'enter');
- assert.isTrue(searchSpy.called);
- });
-
- test('empty undefined predicate query triggers nav', () => {
- const searchSpy = sandbox.spy();
- element.addEventListener('handle-search', searchSpy);
- element.value = 'random:';
- MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
- null, 'enter');
- assert.isTrue(searchSpy.called);
- });
-
- test('keyboard shortcuts', () => {
- const focusSpy = sandbox.spy(element.$.searchInput, 'focus');
- const selectAllSpy = sandbox.spy(element.$.searchInput, 'selectAll');
- MockInteractions.pressAndReleaseKeyOn(document.body, 191, null, '/');
- assert.isTrue(focusSpy.called);
- assert.isTrue(selectAllSpy.called);
- });
-
- suite('_getSearchSuggestions', () => {
- test('Autocompletes accounts', () => {
- sandbox.stub(element, 'accountSuggestions', () =>
- Promise.resolve([{text: 'owner:fred@goog.co'}])
- );
- return element._getSearchSuggestions('owner:fr').then(s => {
- assert.equal(s[0].value, 'owner:fred@goog.co');
+ [
+ 'API_REF_UPDATED_AND_CHANGE_REINDEX',
+ 'REF_UPDATED_AND_CHANGE_REINDEX',
+ ].forEach(mergeability => {
+ suite(`mergeability as ${mergeability}`, () => {
+ setup(done => {
+ stub('gr-rest-api-interface', {
+ getConfig() {
+ return Promise.resolve({
+ index: {
+ mergeabilityComputationBehavior: mergeability,
+ },
+ });
+ },
});
+
+ element = fixture('basic');
+ flush(done);
});
- test('Autocompletes groups', done => {
- sandbox.stub(element, 'groupSuggestions', () =>
- Promise.resolve([
- {text: 'ownerin:Polygerrit'},
- {text: 'ownerin:gerrit'},
- ])
- );
- element._getSearchSuggestions('ownerin:pol').then(s => {
- assert.equal(s[0].value, 'ownerin:Polygerrit');
- done();
- });
- });
-
- test('Autocompletes projects', done => {
- sandbox.stub(element, 'projectSuggestions', () =>
- Promise.resolve([
- {text: 'project:Polygerrit'},
- {text: 'project:gerrit'},
- {text: 'project:gerrittest'},
- ])
- );
- element._getSearchSuggestions('project:pol').then(s => {
- assert.equal(s[0].value, 'project:Polygerrit');
- done();
- });
- });
-
- test('Autocompletes simple searches', done => {
- element._getSearchSuggestions('is:o').then(s => {
- assert.equal(s[0].name, 'is:open');
- assert.equal(s[0].value, 'is:open');
- assert.equal(s[1].name, 'is:owner');
- assert.equal(s[1].value, 'is:owner');
- done();
- });
- });
-
- test('Does not autocomplete with no match', done => {
- element._getSearchSuggestions('asdasdasdasd').then(s => {
- assert.equal(s.length, 0);
- done();
- });
- });
-
- test('Autocompltes without is:mergable when disabled', done => {
+ test('Autocompltes with is:mergable when enabled', done => {
element._getSearchSuggestions('is:mergeab').then(s => {
- assert.equal(s.length, 0);
+ assert.equal(s.length, 2);
+ assert.equal(s[0].name, 'is:mergeable');
+ assert.equal(s[0].value, 'is:mergeable');
+ assert.equal(s[1].name, '-is:mergeable');
+ assert.equal(s[1].value, '-is:mergeable');
done();
});
});
});
-
- [
- 'API_REF_UPDATED_AND_CHANGE_REINDEX',
- 'REF_UPDATED_AND_CHANGE_REINDEX',
- ].forEach(mergeability => {
- suite(`mergeability as ${mergeability}`, () => {
- setup(done => {
- stub('gr-rest-api-interface', {
- getConfig() {
- return Promise.resolve({
- index: {
- mergeabilityComputationBehavior: mergeability,
- },
- });
- },
- });
-
- element = fixture('basic');
- flush(done);
- });
-
- test('Autocompltes with is:mergable when enabled', done => {
- element._getSearchSuggestions('is:mergeab').then(s => {
- assert.equal(s.length, 2);
- assert.equal(s[0].name, 'is:mergeable');
- assert.equal(s[0].value, 'is:mergeable');
- assert.equal(s[1].name, '-is:mergeable');
- assert.equal(s[1].value, '-is:mergeable');
- done();
- });
- });
- });
- });
});
-</script>
\ No newline at end of file
+});
+</script>
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html
deleted file mode 100644
index c4ae41b..0000000
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html
+++ /dev/null
@@ -1,38 +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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-search-bar/gr-search-bar.html">
-
-<dom-module id="gr-smart-search">
- <template>
- <style include="shared-styles">
-
- </style>
- <gr-search-bar id="search"
- value="{{searchQuery}}"
- on-handle-search="_handleSearch"
- project-suggestions="[[_projectSuggestions]]"
- group-suggestions="[[_groupSuggestions]]"
- account-suggestions="[[_accountSuggestions]]"></gr-search-bar>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-smart-search.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
index cfdd524..b27adf7 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
@@ -14,152 +14,162 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const MAX_AUTOCOMPLETE_RESULTS = 10;
- const SELF_EXPRESSION = 'self';
- const ME_EXPRESSION = 'me';
+import '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js';
+import '../gr-navigation/gr-navigation.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-search-bar/gr-search-bar.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-smart-search_html.js';
- /**
- * @appliesMixin Gerrit.DisplayNameMixin
- * @extends Polymer.Element
- */
- class GrSmartSearch extends Polymer.mixinBehaviors( [
- Gerrit.DisplayNameBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-smart-search'; }
+const MAX_AUTOCOMPLETE_RESULTS = 10;
+const SELF_EXPRESSION = 'self';
+const ME_EXPRESSION = 'me';
- static get properties() {
- return {
- searchQuery: String,
- _config: Object,
- _projectSuggestions: {
- type: Function,
- value() {
- return this._fetchProjects.bind(this);
- },
+/**
+ * @appliesMixin Gerrit.DisplayNameMixin
+ * @extends Polymer.Element
+ */
+class GrSmartSearch extends mixinBehaviors( [
+ Gerrit.DisplayNameBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-smart-search'; }
+
+ static get properties() {
+ return {
+ searchQuery: String,
+ _config: Object,
+ _projectSuggestions: {
+ type: Function,
+ value() {
+ return this._fetchProjects.bind(this);
},
- _groupSuggestions: {
- type: Function,
- value() {
- return this._fetchGroups.bind(this);
- },
+ },
+ _groupSuggestions: {
+ type: Function,
+ value() {
+ return this._fetchGroups.bind(this);
},
- _accountSuggestions: {
- type: Function,
- value() {
- return this._fetchAccounts.bind(this);
- },
+ },
+ _accountSuggestions: {
+ type: Function,
+ value() {
+ return this._fetchAccounts.bind(this);
},
- };
- }
+ },
+ };
+ }
- /** @override */
- attached() {
- super.attached();
- this.$.restAPI.getConfig().then(cfg => {
- this._config = cfg;
- });
- }
+ /** @override */
+ attached() {
+ super.attached();
+ this.$.restAPI.getConfig().then(cfg => {
+ this._config = cfg;
+ });
+ }
- _handleSearch(e) {
- const input = e.detail.inputVal;
- if (input) {
- Gerrit.Nav.navigateToSearchQuery(input);
- }
- }
-
- /**
- * Fetch from the API the predicted projects.
- *
- * @param {string} predicate - The first part of the search term, e.g.
- * 'project'
- * @param {string} expression - The second part of the search term, e.g.
- * 'gerr'
- * @return {!Promise} This returns a promise that resolves to an array of
- * strings.
- */
- _fetchProjects(predicate, expression) {
- return this.$.restAPI.getSuggestedProjects(
- expression,
- MAX_AUTOCOMPLETE_RESULTS)
- .then(projects => {
- if (!projects) { return []; }
- const keys = Object.keys(projects);
- return keys.map(key => { return {text: predicate + ':' + key}; });
- });
- }
-
- /**
- * Fetch from the API the predicted groups.
- *
- * @param {string} predicate - The first part of the search term, e.g.
- * 'ownerin'
- * @param {string} expression - The second part of the search term, e.g.
- * 'polyger'
- * @return {!Promise} This returns a promise that resolves to an array of
- * strings.
- */
- _fetchGroups(predicate, expression) {
- if (expression.length === 0) { return Promise.resolve([]); }
- return this.$.restAPI.getSuggestedGroups(
- expression,
- MAX_AUTOCOMPLETE_RESULTS)
- .then(groups => {
- if (!groups) { return []; }
- const keys = Object.keys(groups);
- return keys.map(key => { return {text: predicate + ':' + key}; });
- });
- }
-
- /**
- * Fetch from the API the predicted accounts.
- *
- * @param {string} predicate - The first part of the search term, e.g.
- * 'owner'
- * @param {string} expression - The second part of the search term, e.g.
- * 'kasp'
- * @return {!Promise} This returns a promise that resolves to an array of
- * strings.
- */
- _fetchAccounts(predicate, expression) {
- if (expression.length === 0) { return Promise.resolve([]); }
- return this.$.restAPI.getSuggestedAccounts(
- expression,
- MAX_AUTOCOMPLETE_RESULTS)
- .then(accounts => {
- if (!accounts) { return []; }
- return this._mapAccountsHelper(accounts, predicate);
- })
- .then(accounts => {
- // When the expression supplied is a beginning substring of 'self',
- // add it as an autocomplete option.
- if (SELF_EXPRESSION.startsWith(expression)) {
- return accounts.concat(
- [{text: predicate + ':' + SELF_EXPRESSION}]);
- } else if (ME_EXPRESSION.startsWith(expression)) {
- return accounts.concat([{text: predicate + ':' + ME_EXPRESSION}]);
- } else {
- return accounts;
- }
- });
- }
-
- _mapAccountsHelper(accounts, predicate) {
- return accounts.map(account => {
- const userName = this.getUserName(this._serverConfig, account, false);
- return {
- label: account.name || '',
- text: account.email ?
- `${predicate}:${account.email}` :
- `${predicate}:"${userName}"`,
- };
- });
+ _handleSearch(e) {
+ const input = e.detail.inputVal;
+ if (input) {
+ Gerrit.Nav.navigateToSearchQuery(input);
}
}
- customElements.define(GrSmartSearch.is, GrSmartSearch);
-})();
+ /**
+ * Fetch from the API the predicted projects.
+ *
+ * @param {string} predicate - The first part of the search term, e.g.
+ * 'project'
+ * @param {string} expression - The second part of the search term, e.g.
+ * 'gerr'
+ * @return {!Promise} This returns a promise that resolves to an array of
+ * strings.
+ */
+ _fetchProjects(predicate, expression) {
+ return this.$.restAPI.getSuggestedProjects(
+ expression,
+ MAX_AUTOCOMPLETE_RESULTS)
+ .then(projects => {
+ if (!projects) { return []; }
+ const keys = Object.keys(projects);
+ return keys.map(key => { return {text: predicate + ':' + key}; });
+ });
+ }
+
+ /**
+ * Fetch from the API the predicted groups.
+ *
+ * @param {string} predicate - The first part of the search term, e.g.
+ * 'ownerin'
+ * @param {string} expression - The second part of the search term, e.g.
+ * 'polyger'
+ * @return {!Promise} This returns a promise that resolves to an array of
+ * strings.
+ */
+ _fetchGroups(predicate, expression) {
+ if (expression.length === 0) { return Promise.resolve([]); }
+ return this.$.restAPI.getSuggestedGroups(
+ expression,
+ MAX_AUTOCOMPLETE_RESULTS)
+ .then(groups => {
+ if (!groups) { return []; }
+ const keys = Object.keys(groups);
+ return keys.map(key => { return {text: predicate + ':' + key}; });
+ });
+ }
+
+ /**
+ * Fetch from the API the predicted accounts.
+ *
+ * @param {string} predicate - The first part of the search term, e.g.
+ * 'owner'
+ * @param {string} expression - The second part of the search term, e.g.
+ * 'kasp'
+ * @return {!Promise} This returns a promise that resolves to an array of
+ * strings.
+ */
+ _fetchAccounts(predicate, expression) {
+ if (expression.length === 0) { return Promise.resolve([]); }
+ return this.$.restAPI.getSuggestedAccounts(
+ expression,
+ MAX_AUTOCOMPLETE_RESULTS)
+ .then(accounts => {
+ if (!accounts) { return []; }
+ return this._mapAccountsHelper(accounts, predicate);
+ })
+ .then(accounts => {
+ // When the expression supplied is a beginning substring of 'self',
+ // add it as an autocomplete option.
+ if (SELF_EXPRESSION.startsWith(expression)) {
+ return accounts.concat(
+ [{text: predicate + ':' + SELF_EXPRESSION}]);
+ } else if (ME_EXPRESSION.startsWith(expression)) {
+ return accounts.concat([{text: predicate + ':' + ME_EXPRESSION}]);
+ } else {
+ return accounts;
+ }
+ });
+ }
+
+ _mapAccountsHelper(accounts, predicate) {
+ return accounts.map(account => {
+ const userName = this.getUserName(this._serverConfig, account);
+ return {
+ label: account.name || '',
+ text: account.email ?
+ `${predicate}:${account.email}` :
+ `${predicate}:"${userName}"`,
+ };
+ });
+ }
+}
+
+customElements.define(GrSmartSearch.is, GrSmartSearch);
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js
new file mode 100644
index 0000000..78906a8
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js
@@ -0,0 +1,25 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+
+ </style>
+ <gr-search-bar id="search" value="{{searchQuery}}" on-handle-search="_handleSearch" project-suggestions="[[_projectSuggestions]]" group-suggestions="[[_groupSuggestions]]" account-suggestions="[[_accountSuggestions]]"></gr-search-bar>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html
index 6fd00c7..7cc75bb 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-smart-search</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-smart-search.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,125 +30,126 @@
</template>
</test-fixture>
-<script>
- suite('gr-smart-search tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-smart-search.js';
+suite('gr-smart-search tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('Autocompletes accounts', () => {
- sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
- Promise.resolve([
- {
- name: 'fred',
- email: 'fred@goog.co',
- },
- ])
- );
- return element._fetchAccounts('owner', 'fr').then(s => {
- assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
- });
- });
-
- test('Inserts self as option when valid', () => {
- sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
- Promise.resolve([
- {
- name: 'fred',
- email: 'fred@goog.co',
- },
- ])
- );
- element._fetchAccounts('owner', 's')
- .then(s => {
- assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
- assert.deepEqual(s[1], {text: 'owner:self'});
- })
- .then(() => element._fetchAccounts('owner', 'selfs'))
- .then(s => {
- assert.notEqual(s[0], {text: 'owner:self'});
- });
- });
-
- test('Inserts me as option when valid', () => {
- sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
- Promise.resolve([
- {
- name: 'fred',
- email: 'fred@goog.co',
- },
- ])
- );
- return element._fetchAccounts('owner', 'm')
- .then(s => {
- assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
- assert.deepEqual(s[1], {text: 'owner:me'});
- })
- .then(() => element._fetchAccounts('owner', 'meme'))
- .then(s => {
- assert.notEqual(s[0], {text: 'owner:me'});
- });
- });
-
- test('Autocompletes groups', () => {
- sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
- Promise.resolve({
- Polygerrit: 0,
- gerrit: 0,
- gerrittest: 0,
- })
- );
- return element._fetchGroups('ownerin', 'pol').then(s => {
- assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
- });
- });
-
- test('Autocompletes projects', () => {
- sandbox.stub(element.$.restAPI, 'getSuggestedProjects', () =>
- Promise.resolve({Polygerrit: 0}));
- return element._fetchProjects('project', 'pol').then(s => {
- assert.deepEqual(s[0], {text: 'project:Polygerrit'});
- });
- });
-
- test('Autocomplete doesnt override exact matches to input', () => {
- sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
- Promise.resolve({
- Polygerrit: 0,
- gerrit: 0,
- gerrittest: 0,
- })
- );
- return element._fetchGroups('ownerin', 'gerrit').then(s => {
- assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
- assert.deepEqual(s[1], {text: 'ownerin:gerrit'});
- assert.deepEqual(s[2], {text: 'ownerin:gerrittest'});
- });
- });
-
- test('Autocompletes accounts with no email', () => {
- sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
- Promise.resolve([{name: 'fred'}]));
- return element._fetchAccounts('owner', 'fr').then(s => {
- assert.deepEqual(s[0], {text: 'owner:"fred"', label: 'fred'});
- });
- });
-
- test('Autocompletes accounts with email', () => {
- sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
- Promise.resolve([{email: 'fred@goog.co'}]));
- return element._fetchAccounts('owner', 'fr').then(s => {
- assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: ''});
- });
+ test('Autocompletes accounts', () => {
+ sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+ Promise.resolve([
+ {
+ name: 'fred',
+ email: 'fred@goog.co',
+ },
+ ])
+ );
+ return element._fetchAccounts('owner', 'fr').then(s => {
+ assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
});
});
+
+ test('Inserts self as option when valid', () => {
+ sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+ Promise.resolve([
+ {
+ name: 'fred',
+ email: 'fred@goog.co',
+ },
+ ])
+ );
+ element._fetchAccounts('owner', 's')
+ .then(s => {
+ assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
+ assert.deepEqual(s[1], {text: 'owner:self'});
+ })
+ .then(() => element._fetchAccounts('owner', 'selfs'))
+ .then(s => {
+ assert.notEqual(s[0], {text: 'owner:self'});
+ });
+ });
+
+ test('Inserts me as option when valid', () => {
+ sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+ Promise.resolve([
+ {
+ name: 'fred',
+ email: 'fred@goog.co',
+ },
+ ])
+ );
+ return element._fetchAccounts('owner', 'm')
+ .then(s => {
+ assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
+ assert.deepEqual(s[1], {text: 'owner:me'});
+ })
+ .then(() => element._fetchAccounts('owner', 'meme'))
+ .then(s => {
+ assert.notEqual(s[0], {text: 'owner:me'});
+ });
+ });
+
+ test('Autocompletes groups', () => {
+ sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
+ Promise.resolve({
+ Polygerrit: 0,
+ gerrit: 0,
+ gerrittest: 0,
+ })
+ );
+ return element._fetchGroups('ownerin', 'pol').then(s => {
+ assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
+ });
+ });
+
+ test('Autocompletes projects', () => {
+ sandbox.stub(element.$.restAPI, 'getSuggestedProjects', () =>
+ Promise.resolve({Polygerrit: 0}));
+ return element._fetchProjects('project', 'pol').then(s => {
+ assert.deepEqual(s[0], {text: 'project:Polygerrit'});
+ });
+ });
+
+ test('Autocomplete doesnt override exact matches to input', () => {
+ sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
+ Promise.resolve({
+ Polygerrit: 0,
+ gerrit: 0,
+ gerrittest: 0,
+ })
+ );
+ return element._fetchGroups('ownerin', 'gerrit').then(s => {
+ assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
+ assert.deepEqual(s[1], {text: 'ownerin:gerrit'});
+ assert.deepEqual(s[2], {text: 'ownerin:gerrittest'});
+ });
+ });
+
+ test('Autocompletes accounts with no email', () => {
+ sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+ Promise.resolve([{name: 'fred'}]));
+ return element._fetchAccounts('owner', 'fr').then(s => {
+ assert.deepEqual(s[0], {text: 'owner:"fred"', label: 'fred'});
+ });
+ });
+
+ test('Autocompletes accounts with email', () => {
+ sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+ Promise.resolve([{email: 'fred@goog.co'}]));
+ return element._fetchAccounts('owner', 'fr').then(s => {
+ assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: ''});
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/custom-dark-theme_test.html b/polygerrit-ui/app/elements/custom-dark-theme_test.html
index cd07a67..24d9cdc 100644
--- a/polygerrit-ui/app/elements/custom-dark-theme_test.html
+++ b/polygerrit-ui/app/elements/custom-dark-theme_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-app-it_test</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../test/test-pre-setup.js"></script>
-<link rel="import" href="../test/common-test-setup.html"/>
-<link rel="import" href="./gr-app.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="element">
<template>
@@ -35,69 +30,70 @@
</template>
</test-fixture>
-<script>
- suite('gr-app custom dark theme tests', async () => {
- await readyToTest();
- let sandbox;
- let element;
+<script type="module">
+import '../test/common-test-setup.js';
+import './gr-app.js';
+suite('gr-app custom dark theme tests', () => {
+ let sandbox;
+ let element;
- setup(done => {
- sandbox = sinon.sandbox.create();
- stub('gr-reporting', {
- appStarted: sandbox.stub(),
- });
- stub('gr-account-dropdown', {
- _getTopContent: sinon.stub(),
- });
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve(null); },
- getAccountCapabilities() { return Promise.resolve({}); },
- getConfig() {
- return Promise.resolve({
- plugin: {
- js_resource_paths: [],
- html_resource_paths: [
- new URL('test/plugin.html', window.location.href).toString(),
- ],
- },
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-reporting', {
+ appStarted: sandbox.stub(),
+ });
+ stub('gr-account-dropdown', {
+ _getTopContent: sinon.stub(),
+ });
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve(null); },
+ getAccountCapabilities() { return Promise.resolve({}); },
+ getConfig() {
+ return Promise.resolve({
+ plugin: {
+ js_resource_paths: [],
+ html_resource_paths: [
+ new URL('test/plugin.html', window.location.href).toString(),
+ ],
+ },
+ });
+ },
+ getVersion() { return Promise.resolve(42); },
+ getLoggedIn() { return Promise.resolve(false); },
+ });
+
+ window.localStorage.setItem('dark-theme', 'true');
+
+ element = fixture('element');
+
+ const importSpy = sandbox.spy(
+ element.$['app-element'].$.externalStyleForAll,
+ '_import');
+ const importForThemeSpy = sandbox.spy(
+ element.$['app-element'].$.externalStyleForTheme,
+ '_import');
+ Gerrit.awaitPluginsLoaded().then(() => {
+ Promise.all(importSpy.returnValues.concat(importForThemeSpy.returnValues))
+ .then(() => {
+ flush(done);
});
- },
- getVersion() { return Promise.resolve(42); },
- getLoggedIn() { return Promise.resolve(false); },
- });
-
- window.localStorage.setItem('dark-theme', 'true');
-
- element = fixture('element');
-
- const importSpy = sandbox.spy(
- element.$['app-element'].$.externalStyleForAll,
- '_import');
- const importForThemeSpy = sandbox.spy(
- element.$['app-element'].$.externalStyleForTheme,
- '_import');
- Gerrit.awaitPluginsLoaded().then(() => {
- Promise.all(importSpy.returnValues.concat(importForThemeSpy.returnValues))
- .then(() => {
- flush(done);
- });
- });
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('applies the right theme', () => {
- assert.equal(
- util.getComputedStyleValue('--primary-text-color', element),
- 'red');
- assert.equal(
- util.getComputedStyleValue('--header-background-color', element),
- 'black');
- assert.equal(
- util.getComputedStyleValue('--footer-background-color', element),
- 'yellow');
});
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('applies the right theme', () => {
+ assert.equal(
+ util.getComputedStyleValue('--primary-text-color', element),
+ 'red');
+ assert.equal(
+ util.getComputedStyleValue('--header-background-color', element),
+ 'black');
+ assert.equal(
+ util.getComputedStyleValue('--footer-background-color', element),
+ 'yellow');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/custom-light-theme_test.html b/polygerrit-ui/app/elements/custom-light-theme_test.html
index 13b872e..eb442a6 100644
--- a/polygerrit-ui/app/elements/custom-light-theme_test.html
+++ b/polygerrit-ui/app/elements/custom-light-theme_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-app-it_test</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../test/test-pre-setup.js"></script>
-<link rel="import" href="../test/common-test-setup.html"/>
-<link rel="import" href="gr-app.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="element">
<template>
@@ -35,69 +30,70 @@
</template>
</test-fixture>
-<script>
- suite('gr-app custom light theme tests', async () => {
- await readyToTest();
- let sandbox;
- let element;
+<script type="module">
+import '../test/common-test-setup.js';
+import './gr-app.js';
+suite('gr-app custom light theme tests', () => {
+ let sandbox;
+ let element;
- setup(done => {
- sandbox = sinon.sandbox.create();
- stub('gr-reporting', {
- appStarted: sandbox.stub(),
- });
- stub('gr-account-dropdown', {
- _getTopContent: sinon.stub(),
- });
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve(null); },
- getAccountCapabilities() { return Promise.resolve({}); },
- getConfig() {
- return Promise.resolve({
- plugin: {
- js_resource_paths: [],
- html_resource_paths: [
- new URL('test/plugin.html', window.location.href).toString(),
- ],
- },
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-reporting', {
+ appStarted: sandbox.stub(),
+ });
+ stub('gr-account-dropdown', {
+ _getTopContent: sinon.stub(),
+ });
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve(null); },
+ getAccountCapabilities() { return Promise.resolve({}); },
+ getConfig() {
+ return Promise.resolve({
+ plugin: {
+ js_resource_paths: [],
+ html_resource_paths: [
+ new URL('test/plugin.html', window.location.href).toString(),
+ ],
+ },
+ });
+ },
+ getVersion() { return Promise.resolve(42); },
+ getLoggedIn() { return Promise.resolve(false); },
+ });
+
+ window.localStorage.removeItem('dark-theme');
+
+ element = fixture('element');
+
+ const importSpy = sandbox.spy(
+ element.$['app-element'].$.externalStyleForAll,
+ '_import');
+ const importForThemeSpy = sandbox.spy(
+ element.$['app-element'].$.externalStyleForTheme,
+ '_import');
+ Gerrit.awaitPluginsLoaded().then(() => {
+ Promise.all(importSpy.returnValues.concat(importForThemeSpy.returnValues))
+ .then(() => {
+ flush(done);
});
- },
- getVersion() { return Promise.resolve(42); },
- getLoggedIn() { return Promise.resolve(false); },
- });
-
- window.localStorage.removeItem('dark-theme');
-
- element = fixture('element');
-
- const importSpy = sandbox.spy(
- element.$['app-element'].$.externalStyleForAll,
- '_import');
- const importForThemeSpy = sandbox.spy(
- element.$['app-element'].$.externalStyleForTheme,
- '_import');
- Gerrit.awaitPluginsLoaded().then(() => {
- Promise.all(importSpy.returnValues.concat(importForThemeSpy.returnValues))
- .then(() => {
- flush(done);
- });
- });
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('applies the right theme', () => {
- assert.equal(
- util.getComputedStyleValue('--primary-text-color', element),
- '#F00BAA');
- assert.equal(
- util.getComputedStyleValue('--header-background-color', element),
- '#F01BAA');
- assert.equal(
- util.getComputedStyleValue('--footer-background-color', element),
- '#F02BAA');
});
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('applies the right theme', () => {
+ assert.equal(
+ util.getComputedStyleValue('--primary-text-color', element),
+ '#F00BAA');
+ assert.equal(
+ util.getComputedStyleValue('--header-background-color', element),
+ '#F01BAA');
+ assert.equal(
+ util.getComputedStyleValue('--footer-background-color', element),
+ '#F02BAA');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html
deleted file mode 100644
index c650bb5..0000000
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html
+++ /dev/null
@@ -1,91 +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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-diff/gr-diff.html">
-
-<dom-module id="gr-apply-fix-dialog">
- <template>
- <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);
- }
- .fixActions {
- display: flex;
- justify-content: flex-end;
- }
- 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="[[_isApplyFixLoading]]"
- 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)]]"
- change-num="[[changeNum]]"
- path="[[item.filepath]]"
- diff="[[item.preview]]"></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>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-apply-fix-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
index 9104b90..d5075d7 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
@@ -14,200 +14,213 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '@polymer/iron-icon/iron-icon.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-diff/gr-diff.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-apply-fix-dialog_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrApplyFixDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-apply-fix-dialog'; }
+
+ static get properties() {
+ return {
+ // Diff rendering preference API response.
+ prefs: Array,
+ // ChangeInfo API response object.
+ change: Object,
+ changeNum: String,
+ _patchNum: Number,
+ // robot ID associated with a robot comment.
+ _robotId: String,
+ // Selected FixSuggestionInfo entity from robot comment API response.
+ _currentFix: Object,
+ // Flattened /preview API response DiffInfo map object.
+ _currentPreviews: {type: Array, value: () => []},
+ // FixSuggestionInfo entities from robot comment API response.
+ _fixSuggestions: Array,
+ _isApplyFixLoading: {
+ type: Boolean,
+ value: false,
+ },
+ // Index of currently showing suggested fix.
+ _selectedFixIdx: Number,
+ };
+ }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Given robot comment CustomEvent objevt, fetch diffs associated
+ * with first robot comment suggested fix and open dialog.
+ *
+ * @param {*} e CustomEvent to be passed from gr-comment with
+ * robot comment detail.
+ * @return {Promise<undefined>} Promise that resolves either when all
+ * preview diffs are fetched or no fix suggestions in custom event detail.
*/
- class GrApplyFixDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-apply-fix-dialog'; }
-
- static get properties() {
- return {
- // Diff rendering preference API response.
- prefs: Array,
- // ChangeInfo API response object.
- change: Object,
- changeNum: String,
- _patchNum: Number,
- // robot ID associated with a robot comment.
- _robotId: String,
- // Selected FixSuggestionInfo entity from robot comment API response.
- _currentFix: Object,
- // Flattened /preview API response DiffInfo map object.
- _currentPreviews: {type: Array, value: () => []},
- // FixSuggestionInfo entities from robot comment API response.
- _fixSuggestions: Array,
- _isApplyFixLoading: {
- type: Boolean,
- value: false,
- },
- // Index of currently showing suggested fix.
- _selectedFixIdx: Number,
- };
+ open(e) {
+ this._patchNum = e.detail.patchNum;
+ this._fixSuggestions = e.detail.comment.fix_suggestions;
+ this._robotId = e.detail.comment.robot_id;
+ if (this._fixSuggestions == null || this._fixSuggestions.length == 0) {
+ return Promise.resolve();
}
+ this._selectedFixIdx = 0;
+ const promises = [];
+ promises.push(
+ this._showSelectedFixSuggestion(this._fixSuggestions[0]),
+ this.$.applyFixOverlay.open()
+ );
+ return Promise.all(promises)
+ .then(() => {
+ // ensures gr-overlay repositions overlay in center
+ this.$.applyFixOverlay.fire('iron-resize');
+ });
+ }
- /**
- * Given robot comment CustomEvent objevt, fetch diffs associated
- * with first robot comment suggested fix and open dialog.
- *
- * @param {*} e CustomEvent to be passed from gr-comment with
- * robot comment detail.
- * @return {Promise<undefined>} Promise that resolves either when all
- * preview diffs are fetched or no fix suggestions in custom event detail.
- */
- open(e) {
- this._patchNum = e.detail.patchNum;
- this._fixSuggestions = e.detail.comment.fix_suggestions;
- this._robotId = e.detail.comment.robot_id;
- if (this._fixSuggestions == null || this._fixSuggestions.length == 0) {
- return Promise.resolve();
- }
- this._selectedFixIdx = 0;
- const promises = [];
- promises.push(
- this._showSelectedFixSuggestion(this._fixSuggestions[0]),
- this.$.applyFixOverlay.open()
- );
- return Promise.all(promises)
- .then(() => {
- // ensures gr-overlay repositions overlay in center
- this.$.applyFixOverlay.fire('iron-resize');
- });
+ attached() {
+ super.attached();
+ this.refitOverlay = () => {
+ // re-center the dialog as content changed
+ this.$.applyFixOverlay.fire('iron-resize');
+ };
+ this.addEventListener('diff-context-expanded', this.refitOverlay);
+ }
+
+ detached() {
+ super.detached();
+ this.removeEventListener('diff-context-expanded', this.refitOverlay);
+ }
+
+ _showSelectedFixSuggestion(fixSuggestion) {
+ this._currentFix = fixSuggestion;
+ return this._fetchFixPreview(fixSuggestion.fix_id);
+ }
+
+ _fetchFixPreview(fixId) {
+ return this.$.restAPI
+ .getRobotCommentFixPreview(this.changeNum, this._patchNum, fixId)
+ .then(res => {
+ if (res != null) {
+ const previews = Object.keys(res).map(key => {
+ return {filepath: key, preview: res[key]};
+ });
+ this._currentPreviews = previews;
+ }
+ })
+ .catch(err => {
+ this._close();
+ throw err;
+ });
+ }
+
+ hasSingleFix(_fixSuggestions) {
+ return (_fixSuggestions || {}).length === 1;
+ }
+
+ overridePartialPrefs(prefs) {
+ // generate a smaller gr-diff than fullscreen for dialog
+ return Object.assign({}, prefs, {line_length: 50});
+ }
+
+ onCancel(e) {
+ if (e) {
+ e.stopPropagation();
}
+ this._close();
+ }
- attached() {
- super.attached();
- this.refitOverlay = () => {
- // re-center the dialog as content changed
- this.$.applyFixOverlay.fire('iron-resize');
- };
- this.addEventListener('diff-context-expanded', this.refitOverlay);
- }
+ addOneTo(_selectedFixIdx) {
+ return _selectedFixIdx + 1;
+ }
- detached() {
- super.detached();
- this.removeEventListener('diff-context-expanded', this.refitOverlay);
- }
-
- _showSelectedFixSuggestion(fixSuggestion) {
- this._currentFix = fixSuggestion;
- return this._fetchFixPreview(fixSuggestion.fix_id);
- }
-
- _fetchFixPreview(fixId) {
- return this.$.restAPI
- .getRobotCommentFixPreview(this.changeNum, this._patchNum, fixId)
- .then(res => {
- if (res != null) {
- const previews = Object.keys(res).map(key => {
- return {filepath: key, preview: res[key]};
- });
- this._currentPreviews = previews;
- }
- })
- .catch(err => {
- this._close();
- throw err;
- });
- }
-
- hasSingleFix(_fixSuggestions) {
- return (_fixSuggestions || {}).length === 1;
- }
-
- overridePartialPrefs(prefs) {
- // generate a smaller gr-diff than fullscreen for dialog
- return Object.assign({}, prefs, {line_length: 50});
- }
-
- onCancel(e) {
- if (e) {
- e.stopPropagation();
- }
- this._close();
- }
-
- addOneTo(_selectedFixIdx) {
- return _selectedFixIdx + 1;
- }
-
- _onPrevFixClick(e) {
- if (e) e.stopPropagation();
- if (this._selectedFixIdx >= 1 && this._fixSuggestions != null) {
- this._selectedFixIdx -= 1;
- return this._showSelectedFixSuggestion(
- this._fixSuggestions[this._selectedFixIdx]);
- }
- }
-
- _onNextFixClick(e) {
- if (e) e.stopPropagation();
- if (this._fixSuggestions &&
- this._selectedFixIdx < this._fixSuggestions.length) {
- this._selectedFixIdx += 1;
- return this._showSelectedFixSuggestion(
- this._fixSuggestions[this._selectedFixIdx]);
- }
- }
-
- _noPrevFix(_selectedFixIdx) {
- return _selectedFixIdx === 0;
- }
-
- _noNextFix(_selectedFixIdx, fixSuggestions) {
- if (fixSuggestions == null) return true;
- return _selectedFixIdx === fixSuggestions.length - 1;
- }
-
- _close() {
- this._currentFix = {};
- this._currentPreviews = [];
- this._isApplyFixLoading = false;
-
- this.dispatchEvent(new CustomEvent('close-fix-preview', {
- bubbles: true,
- composed: true,
- }));
- this.$.applyFixOverlay.close();
- }
-
- _getApplyFixButtonLabel(isLoading) {
- return isLoading ? 'Saving...' : 'Apply Fix';
- }
-
- _handleApplyFix(e) {
- if (e) {
- e.stopPropagation();
- }
- if (this._currentFix == null || this._currentFix.fix_id == null) {
- return;
- }
- this._isApplyFixLoading = true;
- return this.$.restAPI
- .applyFixSuggestion(
- this.changeNum, this._patchNum, this._currentFix.fix_id
- )
- .then(res => {
- if (res && res.ok) {
- Gerrit.Nav.navigateToChange(this.change, 'edit', this._patchNum);
- this._close();
- }
- this._isApplyFixLoading = false;
- });
- }
-
- getFixDescription(currentFix) {
- return currentFix != null && currentFix.description ?
- currentFix.description : '';
+ _onPrevFixClick(e) {
+ if (e) e.stopPropagation();
+ if (this._selectedFixIdx >= 1 && this._fixSuggestions != null) {
+ this._selectedFixIdx -= 1;
+ return this._showSelectedFixSuggestion(
+ this._fixSuggestions[this._selectedFixIdx]);
}
}
- customElements.define(GrApplyFixDialog.is, GrApplyFixDialog);
-})();
+ _onNextFixClick(e) {
+ if (e) e.stopPropagation();
+ if (this._fixSuggestions &&
+ this._selectedFixIdx < this._fixSuggestions.length) {
+ this._selectedFixIdx += 1;
+ return this._showSelectedFixSuggestion(
+ this._fixSuggestions[this._selectedFixIdx]);
+ }
+ }
+
+ _noPrevFix(_selectedFixIdx) {
+ return _selectedFixIdx === 0;
+ }
+
+ _noNextFix(_selectedFixIdx, fixSuggestions) {
+ if (fixSuggestions == null) return true;
+ return _selectedFixIdx === fixSuggestions.length - 1;
+ }
+
+ _close() {
+ this._currentFix = {};
+ this._currentPreviews = [];
+ this._isApplyFixLoading = false;
+
+ this.dispatchEvent(new CustomEvent('close-fix-preview', {
+ bubbles: true,
+ composed: true,
+ }));
+ this.$.applyFixOverlay.close();
+ }
+
+ _getApplyFixButtonLabel(isLoading) {
+ return isLoading ? 'Saving...' : 'Apply Fix';
+ }
+
+ _handleApplyFix(e) {
+ if (e) {
+ e.stopPropagation();
+ }
+ if (this._currentFix == null || this._currentFix.fix_id == null) {
+ return;
+ }
+ this._isApplyFixLoading = true;
+ return this.$.restAPI
+ .applyFixSuggestion(
+ this.changeNum, this._patchNum, this._currentFix.fix_id
+ )
+ .then(res => {
+ if (res && res.ok) {
+ Gerrit.Nav.navigateToChange(this.change, 'edit', this._patchNum);
+ this._close();
+ }
+ this._isApplyFixLoading = false;
+ });
+ }
+
+ getFixDescription(currentFix) {
+ return currentFix != null && currentFix.description ?
+ currentFix.description : '';
+ }
+}
+
+customElements.define(GrApplyFixDialog.is, GrApplyFixDialog);
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.js b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.js
new file mode 100644
index 0000000..f6cd1ec
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.js
@@ -0,0 +1,72 @@
+/**
+ * @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.js';
+
+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);
+ }
+ .fixActions {
+ display: flex;
+ justify-content: flex-end;
+ }
+ 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="[[_isApplyFixLoading]]" 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)]]" change-num="[[changeNum]]" path="[[item.filepath]]" diff="[[item.preview]]"></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>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
index f22ab57..2ad35b3 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
@@ -17,17 +17,16 @@
-->
<meta name='viewport' content='width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes'>
<title>gr-apply-fix-dialog</title>
-<link rel='import' href='../../../test/common-test-setup.html'>
-<script src='/bower_components/webcomponentsjs/custom-elements-es5-adapter.js'></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src='/bower_components/webcomponentsjs/webcomponents-lite.js'></script>
-<script src='/bower_components/web-component-tester/browser.js'></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel='import' href='../../../test/common-test-setup.html' />
-
-<link rel='import' href='./gr-apply-fix-dialog.html'>
-
-<script>void (0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-apply-fix-dialog.js';
+void (0);
+</script>
<test-fixture id='basic'>
<template>
@@ -35,229 +34,231 @@
</template>
</test-fixture>
-<script>
- suite('gr-apply-fix-dialog tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- const ROBOT_COMMENT_WITH_TWO_FIXES = {
- robot_id: 'robot_1',
- fix_suggestions: [{fix_id: 'fix_1'}, {fix_id: 'fix_2'}],
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-apply-fix-dialog.js';
+suite('gr-apply-fix-dialog tests', () => {
+ let element;
+ let sandbox;
+ const ROBOT_COMMENT_WITH_TWO_FIXES = {
+ robot_id: 'robot_1',
+ fix_suggestions: [{fix_id: 'fix_1'}, {fix_id: 'fix_2'}],
+ };
+
+ const ROBOT_COMMENT_WITH_ONE_FIX = {
+ robot_id: 'robot_1',
+ fix_suggestions: [{fix_id: 'fix_1'}],
+ };
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.changeNum = '1';
+ element._patchNum = 2;
+ element.change = {
+ _number: '1',
+ project: 'project',
};
-
- const ROBOT_COMMENT_WITH_ONE_FIX = {
- robot_id: 'robot_1',
- fix_suggestions: [{fix_id: 'fix_1'}],
+ element.prefs = {
+ font_size: 12,
+ line_length: 100,
+ tab_size: 4,
};
+ });
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.changeNum = '1';
- element._patchNum = 2;
- element.change = {
- _number: '1',
- project: 'project',
- };
- element.prefs = {
- font_size: 12,
- line_length: 100,
- tab_size: 4,
- };
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- teardown(() => {
- sandbox.restore();
- });
+ test('dialog opens fetch and sets previews', done => {
+ sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview')
+ .returns(Promise.resolve({
+ f1: {
+ meta_a: {},
+ meta_b: {},
+ content: [
+ {
+ ab: ['loqlwkqll'],
+ },
+ {
+ b: ['qwqqsqw'],
+ },
+ {
+ ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'],
+ },
+ ],
+ },
+ f2: {
+ meta_a: {},
+ meta_b: {},
+ content: [
+ {
+ ab: ['eqweqweqwex'],
+ },
+ {
+ b: ['zassdasd'],
+ },
+ {
+ ab: ['zassdasd', 'dasdasda', 'asdasdad'],
+ },
+ ],
+ },
+ }));
+ sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
- test('dialog opens fetch and sets previews', done => {
- sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview')
- .returns(Promise.resolve({
- f1: {
- meta_a: {},
- meta_b: {},
- content: [
- {
- ab: ['loqlwkqll'],
- },
- {
- b: ['qwqqsqw'],
- },
- {
- ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'],
- },
- ],
- },
- f2: {
- meta_a: {},
- meta_b: {},
- content: [
- {
- ab: ['eqweqweqwex'],
- },
- {
- b: ['zassdasd'],
- },
- {
- ab: ['zassdasd', 'dasdasda', 'asdasdad'],
- },
- ],
- },
- }));
- sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
-
- element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_TWO_FIXES}})
- .then(() => {
- assert.equal(element._currentFix.fix_id, 'fix_1');
- assert.equal(element._currentPreviews.length, 2);
- assert.equal(element._robotId, 'robot_1');
- done();
- });
- });
-
- test('next button state updated when suggestions changed', done => {
- sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview')
- .returns(Promise.resolve({}));
- sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
-
- element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_ONE_FIX}})
- .then(() => assert.isTrue(element.$.nextFix.disabled))
- .then(() =>
- element.open({detail: {patchNum: 2,
- comment: ROBOT_COMMENT_WITH_TWO_FIXES}}))
- .then(() => {
- assert.isFalse(element.$.nextFix.disabled);
- done();
- });
- });
-
- test('preview endpoint throws error should reset dialog', done => {
- sandbox.stub(window, 'fetch', (url => {
- if (url.endsWith('/preview')) {
- return Promise.reject(new Error('backend error'));
- }
- return Promise.resolve({
- ok: true,
- text() { return Promise.resolve(''); },
- status: 200,
+ element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_TWO_FIXES}})
+ .then(() => {
+ assert.equal(element._currentFix.fix_id, 'fix_1');
+ assert.equal(element._currentPreviews.length, 2);
+ assert.equal(element._robotId, 'robot_1');
+ done();
});
- }));
- const errorStub = sinon.stub();
- document.addEventListener('network-error', errorStub);
- element.open({detail: {patchNum: 2,
- comment: ROBOT_COMMENT_WITH_TWO_FIXES}});
- flush(() => {
- assert.isTrue(errorStub.called);
- assert.deepEqual(element._currentFix, {});
- done();
- });
- });
+ });
- test('apply fix button should call apply ' +
- 'and navigate to change view', done => {
- sandbox.stub(element.$.restAPI, 'applyFixSuggestion')
- .returns(Promise.resolve({ok: true}));
- sandbox.stub(Gerrit.Nav, 'navigateToChange');
- element._currentFix = {fix_id: '123'};
+ test('next button state updated when suggestions changed', done => {
+ sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview')
+ .returns(Promise.resolve({}));
+ sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
- element._handleApplyFix().then(() => {
- assert.isTrue(element.$.restAPI.applyFixSuggestion
- .calledWithExactly('1', 2, '123'));
- assert.isTrue(Gerrit.Nav.navigateToChange.calledWithExactly({
- _number: '1',
- project: 'project',
- }, 'edit', 2));
-
- // reset gr-apply-fix-dialog and close
- assert.deepEqual(element._currentFix, {});
- assert.equal(element._currentPreviews.length, 0);
- done();
- });
- });
-
- test('should not navigate to change view if incorect reponse', done => {
- sandbox.stub(element.$.restAPI, 'applyFixSuggestion')
- .returns(Promise.resolve({}));
- sandbox.stub(Gerrit.Nav, 'navigateToChange');
- element._currentFix = {fix_id: '123'};
-
- element._handleApplyFix().then(() => {
- assert.isTrue(element.$.restAPI.applyFixSuggestion
- .calledWithExactly('1', 2, '123'));
- assert.isTrue(Gerrit.Nav.navigateToChange.notCalled);
-
- assert.equal(element._isApplyFixLoading, false);
- done();
- });
- });
-
- test('select fix forward and back of multiple suggested fixes', done => {
- sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview')
- .returns(Promise.resolve({
- f1: {
- meta_a: {},
- meta_b: {},
- content: [
- {
- ab: ['loqlwkqll'],
- },
- {
- b: ['qwqqsqw'],
- },
- {
- ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'],
- },
- ],
- },
- f2: {
- meta_a: {},
- meta_b: {},
- content: [
- {
- ab: ['eqweqweqwex'],
- },
- {
- b: ['zassdasd'],
- },
- {
- ab: ['zassdasd', 'dasdasda', 'asdasdad'],
- },
- ],
- },
- }));
- sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
-
- element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_TWO_FIXES}})
- .then(() => {
- element._onNextFixClick();
- assert.equal(element._currentFix.fix_id, 'fix_2');
- element._onPrevFixClick();
- assert.equal(element._currentFix.fix_id, 'fix_1');
- done();
- });
- });
-
- test('server-error should throw for failed apply call', done => {
- sandbox.stub(window, 'fetch', (url => {
- if (url.endsWith('/apply')) {
- return Promise.reject(new Error('backend error'));
- }
- return Promise.resolve({
- ok: true,
- text() { return Promise.resolve(''); },
- status: 200,
+ element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_ONE_FIX}})
+ .then(() => assert.isTrue(element.$.nextFix.disabled))
+ .then(() =>
+ element.open({detail: {patchNum: 2,
+ comment: ROBOT_COMMENT_WITH_TWO_FIXES}}))
+ .then(() => {
+ assert.isFalse(element.$.nextFix.disabled);
+ done();
});
- }));
- const errorStub = sinon.stub();
- document.addEventListener('network-error', errorStub);
- sandbox.stub(Gerrit.Nav, 'navigateToChange');
- element._currentFix = {fix_id: '123'};
- element._handleApplyFix();
- flush(() => {
- assert.isFalse(Gerrit.Nav.navigateToChange.called);
- assert.isTrue(errorStub.called);
- done();
+ });
+
+ test('preview endpoint throws error should reset dialog', done => {
+ sandbox.stub(window, 'fetch', (url => {
+ if (url.endsWith('/preview')) {
+ return Promise.reject(new Error('backend error'));
+ }
+ return Promise.resolve({
+ ok: true,
+ text() { return Promise.resolve(''); },
+ status: 200,
});
+ }));
+ const errorStub = sinon.stub();
+ document.addEventListener('network-error', errorStub);
+ element.open({detail: {patchNum: 2,
+ comment: ROBOT_COMMENT_WITH_TWO_FIXES}});
+ flush(() => {
+ assert.isTrue(errorStub.called);
+ assert.deepEqual(element._currentFix, {});
+ done();
});
});
+
+ test('apply fix button should call apply ' +
+ 'and navigate to change view', done => {
+ sandbox.stub(element.$.restAPI, 'applyFixSuggestion')
+ .returns(Promise.resolve({ok: true}));
+ sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ element._currentFix = {fix_id: '123'};
+
+ element._handleApplyFix().then(() => {
+ assert.isTrue(element.$.restAPI.applyFixSuggestion
+ .calledWithExactly('1', 2, '123'));
+ assert.isTrue(Gerrit.Nav.navigateToChange.calledWithExactly({
+ _number: '1',
+ project: 'project',
+ }, 'edit', 2));
+
+ // reset gr-apply-fix-dialog and close
+ assert.deepEqual(element._currentFix, {});
+ assert.equal(element._currentPreviews.length, 0);
+ done();
+ });
+ });
+
+ test('should not navigate to change view if incorect reponse', done => {
+ sandbox.stub(element.$.restAPI, 'applyFixSuggestion')
+ .returns(Promise.resolve({}));
+ sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ element._currentFix = {fix_id: '123'};
+
+ element._handleApplyFix().then(() => {
+ assert.isTrue(element.$.restAPI.applyFixSuggestion
+ .calledWithExactly('1', 2, '123'));
+ assert.isTrue(Gerrit.Nav.navigateToChange.notCalled);
+
+ assert.equal(element._isApplyFixLoading, false);
+ done();
+ });
+ });
+
+ test('select fix forward and back of multiple suggested fixes', done => {
+ sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview')
+ .returns(Promise.resolve({
+ f1: {
+ meta_a: {},
+ meta_b: {},
+ content: [
+ {
+ ab: ['loqlwkqll'],
+ },
+ {
+ b: ['qwqqsqw'],
+ },
+ {
+ ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'],
+ },
+ ],
+ },
+ f2: {
+ meta_a: {},
+ meta_b: {},
+ content: [
+ {
+ ab: ['eqweqweqwex'],
+ },
+ {
+ b: ['zassdasd'],
+ },
+ {
+ ab: ['zassdasd', 'dasdasda', 'asdasdad'],
+ },
+ ],
+ },
+ }));
+ sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
+
+ element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_TWO_FIXES}})
+ .then(() => {
+ element._onNextFixClick();
+ assert.equal(element._currentFix.fix_id, 'fix_2');
+ element._onPrevFixClick();
+ assert.equal(element._currentFix.fix_id, 'fix_1');
+ done();
+ });
+ });
+
+ test('server-error should throw for failed apply call', done => {
+ sandbox.stub(window, 'fetch', (url => {
+ if (url.endsWith('/apply')) {
+ return Promise.reject(new Error('backend error'));
+ }
+ return Promise.resolve({
+ ok: true,
+ text() { return Promise.resolve(''); },
+ status: 200,
+ });
+ }));
+ const errorStub = sinon.stub();
+ document.addEventListener('network-error', errorStub);
+ sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ element._currentFix = {fix_id: '123'};
+ element._handleApplyFix();
+ flush(() => {
+ assert.isFalse(Gerrit.Nav.navigateToChange.called);
+ assert.isTrue(errorStub.called);
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock_test.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock_test.js
index 34abe76..4abdb615 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock_test.js
@@ -17,40 +17,38 @@
(function() {
'use strict';
- window.addEventListener('HTMLImportsLoaded', () => {
- class CommentApiMock extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'comment-api-mock'; }
+ class CommentApiMock extends Polymer.GestureEventListeners(
+ Polymer.LegacyElementMixin(
+ Polymer.Element)) {
+ static get is() { return 'comment-api-mock'; }
- static get properties() {
- return {
- _changeComments: Object,
- };
- }
-
- loadComments() {
- return this._reloadComments();
- }
-
- /**
- * For the purposes of the mock, _reloadDrafts is not included because its
- * response is the same type as reloadComments, just makes less API
- * requests. Since this is for test purposes/mocked data anyway, keep this
- * file simpler by just using _reloadComments here instead.
- */
- _reloadDraftsWithCallback(e) {
- return this._reloadComments().then(() => e.detail.resolve());
- }
-
- _reloadComments() {
- return this.$.commentAPI.loadAll(this._changeNum)
- .then(comments => {
- this._changeComments = this.$.commentAPI._changeComments;
- });
- }
+ static get properties() {
+ return {
+ _changeComments: Object,
+ };
}
- customElements.define(CommentApiMock.is, CommentApiMock);
- });
+ loadComments() {
+ return this._reloadComments();
+ }
+
+ /**
+ * For the purposes of the mock, _reloadDrafts is not included because its
+ * response is the same type as reloadComments, just makes less API
+ * requests. Since this is for test purposes/mocked data anyway, keep this
+ * file simpler by just using _reloadComments here instead.
+ */
+ _reloadDraftsWithCallback(e) {
+ return this._reloadComments().then(() => e.detail.resolve());
+ }
+
+ _reloadComments() {
+ return this.$.commentAPI.loadAll(this._changeNum)
+ .then(comments => {
+ this._changeComments = this.$.commentAPI._changeComments;
+ });
+ }
+ }
+
+ customElements.define(CommentApiMock.is, CommentApiMock);
})();
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.html b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.html
deleted file mode 100644
index 317e9e5..0000000
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.html
+++ /dev/null
@@ -1,27 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-comment-api">
- <template>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-comment-api.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
index 490367a..ac43679 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
@@ -14,46 +14,55 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const PARENT = 'PARENT';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-comment-api_html.js';
- /**
- * Construct a change comments object, which can be data-bound to child
- * elements of that which uses the gr-comment-api.
- *
- * @constructor
- * @param {!Object} comments
- * @param {!Object} robotComments
- * @param {!Object} drafts
- * @param {number} changeNum
- */
- function ChangeComments(comments, robotComments, drafts, changeNum) {
+const PARENT = 'PARENT';
+
+/**
+ * Construct a change comments object, which can be data-bound to child
+ * elements of that which uses the gr-comment-api.
+ *
+ * @constructor
+ * @param {!Object} comments
+ * @param {!Object} robotComments
+ * @param {!Object} drafts
+ * @param {number} changeNum
+ */
+class ChangeComments {
+ constructor(comments, robotComments, drafts, changeNum) {
+ // TODO(taoalpha): replace these with exported methods from patchset behavior
+ this._patchNumEquals =
+ Gerrit.PatchSetBehavior.patchNumEquals;
+ this._isMergeParent =
+ Gerrit.PatchSetBehavior.isMergeParent;
+ this._getParentIndex =
+ Gerrit.PatchSetBehavior.getParentIndex;
+
this._comments = comments;
this._robotComments = robotComments;
this._drafts = drafts;
this._changeNum = changeNum;
}
- ChangeComments.prototype = {
- get comments() {
- return this._comments;
- },
- get drafts() {
- return this._drafts;
- },
- get robotComments() {
- return this._robotComments;
- },
- };
+ get comments() {
+ return this._comments;
+ }
- ChangeComments.prototype._patchNumEquals =
- Gerrit.PatchSetBehavior.patchNumEquals;
- ChangeComments.prototype._isMergeParent =
- Gerrit.PatchSetBehavior.isMergeParent;
- ChangeComments.prototype._getParentIndex =
- Gerrit.PatchSetBehavior.getParentIndex;
+ get drafts() {
+ return this._drafts;
+ }
+
+ get robotComments() {
+ return this._robotComments;
+ }
/**
* Get an object mapping file paths to a boolean representing whether that
@@ -67,23 +76,23 @@
* patchNum and basePatchNum properties to represent the range.
* @return {!Object}
*/
- ChangeComments.prototype.getPaths = function(opt_patchRange) {
+ getPaths(opt_patchRange) {
const responses = [this.comments, this.drafts, this.robotComments];
const commentMap = {};
for (const response of responses) {
for (const path in response) {
if (response.hasOwnProperty(path) &&
- response[path].some(c => {
- // If don't care about patch range, we know that the path exists.
- if (!opt_patchRange) { return true; }
- return this._isInPatchRange(c, opt_patchRange);
- })) {
+ response[path].some(c => {
+ // If don't care about patch range, we know that the path exists.
+ if (!opt_patchRange) { return true; }
+ return this._isInPatchRange(c, opt_patchRange);
+ })) {
commentMap[path] = true;
}
}
}
return commentMap;
- };
+ }
/**
* Gets all the comments and robot comments for the given change.
@@ -91,9 +100,9 @@
* @param {number=} opt_patchNum
* @return {!Object}
*/
- ChangeComments.prototype.getAllPublishedComments = function(opt_patchNum) {
+ getAllPublishedComments(opt_patchNum) {
return this.getAllComments(false, opt_patchNum);
- };
+ }
/**
* Gets all the comments for a particular thread group. Used for refreshing
@@ -102,7 +111,7 @@
* @param {string} rootId
* @return {!Array} an array of comments
*/
- ChangeComments.prototype.getCommentsForThread = function(rootId) {
+ getCommentsForThread(rootId) {
const allThreads = this.getAllThreadsForChange();
const threadMatch = allThreads.find(t => t.rootId === rootId);
@@ -110,7 +119,7 @@
// and the diff view is updating comments, there will no longer be a thread
// found. In this case, return null.
return threadMatch ? threadMatch.comments : null;
- };
+ }
/**
* Filters an array of comments by line and side
@@ -123,10 +132,10 @@
* @param {number=} opt_line line number, can be undefined if file comment
* @return {!Array} an array of comments
*/
- ChangeComments.prototype._filterCommentsBySideAndLine = function(comments,
+ _filterCommentsBySideAndLine(comments,
parentOnly, commentSide, opt_line) {
return comments.filter(c => {
- // if parentOnly, only match comments with PARENT for the side.
+ // if parentOnly, only match comments with PARENT for the side.
let sideMatch = parentOnly ? c.side === PARENT : c.side !== PARENT;
if (parentOnly) {
sideMatch = sideMatch && c.side === PARENT;
@@ -136,7 +145,7 @@
c.__commentSide = commentSide;
return c;
});
- };
+ }
/**
* Gets all the comments and robot comments for the given change.
@@ -145,7 +154,7 @@
* @param {number=} opt_patchNum
* @return {!Object}
*/
- ChangeComments.prototype.getAllComments = function(opt_includeDrafts,
+ getAllComments(opt_includeDrafts,
opt_patchNum) {
const paths = this.getPaths();
const publishedComments = {};
@@ -159,7 +168,7 @@
publishedComments[path] = commentsToAdd;
}
return publishedComments;
- };
+ }
/**
* Gets all the comments and robot comments for the given change.
@@ -167,14 +176,14 @@
* @param {number=} opt_patchNum
* @return {!Object}
*/
- ChangeComments.prototype.getAllDrafts = function(opt_patchNum) {
+ getAllDrafts(opt_patchNum) {
const paths = this.getPaths();
const drafts = {};
for (const path of Object.keys(paths)) {
drafts[path] = this.getAllDraftsForPath(path, opt_patchNum);
}
return drafts;
- };
+ }
/**
* Get the comments (robot comments) for a path and optional patch num.
@@ -184,7 +193,7 @@
* @param {boolean=} opt_includeDrafts
* @return {!Array}
*/
- ChangeComments.prototype.getAllCommentsForPath = function(path,
+ getAllCommentsForPath(path,
opt_patchNum, opt_includeDrafts) {
const comments = this._comments[path] || [];
const robotComments = this._robotComments[path] || [];
@@ -198,7 +207,32 @@
return (allComments || []).filter(c =>
this._patchNumEquals(c.patch_set, opt_patchNum)
);
- };
+ }
+
+ /**
+ * Get the comments (robot comments) for a file.
+ *
+ * // TODO(taoalpha): maybe merge in *ForPath
+ *
+ * @param {!{path: string, oldPath?: string, patchNum?: number}} file
+ * @param {boolean=} opt_includeDrafts
+ * @return {!Array}
+ */
+ getAllCommentsForFile(file, opt_includeDrafts) {
+ let allComments = this.getAllCommentsForPath(
+ file.path, file.patchNum, opt_includeDrafts
+ );
+
+ if (file.oldPath) {
+ allComments = allComments.concat(
+ this.getAllCommentsForPath(
+ file.oldPath, file.patchNum, opt_includeDrafts
+ )
+ );
+ }
+
+ return allComments;
+ }
/**
* Get the drafts for a path and optional patch num.
@@ -207,14 +241,32 @@
* @param {number=} opt_patchNum
* @return {!Array}
*/
- ChangeComments.prototype.getAllDraftsForPath = function(path,
+ getAllDraftsForPath(path,
opt_patchNum) {
const comments = this._drafts[path] || [];
if (!opt_patchNum) { return comments; }
return (comments || []).filter(c =>
this._patchNumEquals(c.patch_set, opt_patchNum)
);
- };
+ }
+
+ /**
+ * Get the drafts for a file.
+ *
+ * // TODO(taoalpha): maybe merge in *ForPath
+ *
+ * @param {!{path: string, oldPath?: string, patchNum?: number}} file
+ * @return {!Array}
+ */
+ getAllDraftsForFile(file) {
+ let allDrafts = this.getAllDraftsForPath(file.path, file.patchNum);
+ if (file.oldPath) {
+ allDrafts = allDrafts.concat(
+ this.getAllDraftsForPath(file.oldPath, file.patchNum)
+ );
+ }
+ return allDrafts;
+ }
/**
* Get the comments (with drafts and robot comments) for a path and
@@ -228,7 +280,7 @@
* include in the meta sub-object.
* @return {!Gerrit.CommentsBySide}
*/
- ChangeComments.prototype.getCommentsBySideForPath = function(path,
+ getCommentsBySideForPath(path,
patchRange, opt_projectConfig) {
let comments = [];
let drafts = [];
@@ -262,7 +314,35 @@
left: baseComments,
right: revisionComments,
};
- };
+ }
+
+ /**
+ * Get the comments (with drafts and robot comments) for a file and
+ * patch-range. Returns an object with left and right properties mapping to
+ * arrays of comments in on either side of the patch range for that path.
+ *
+ * // TODO(taoalpha): maybe merge *ForPath so find all comments in one pass
+ *
+ * @param {!{path: string, oldPath?: string, patchNum?: number}} file
+ * @param {!Gerrit.PatchRange} patchRange The patch-range object containing patchNum
+ * and basePatchNum properties to represent the range.
+ * @param {Object=} opt_projectConfig Optional project config object to
+ * include in the meta sub-object.
+ * @return {!Gerrit.CommentsBySide}
+ */
+ getCommentsBySideForFile(file, patchRange, opt_projectConfig) {
+ const comments = this.getCommentsBySideForPath(
+ file.path, patchRange, opt_projectConfig
+ );
+ if (file.oldPath) {
+ const commentsForOldPath = this.getCommentsBySideForPath(
+ file.oldPath, patchRange, opt_projectConfig
+ );
+ // merge in the left and right
+ comments.left = comments.left.concat(commentsForOldPath.left);
+ comments.right = comments.right.concat(commentsForOldPath.right);
+ }
+ }
/**
* @param {!Object} comments Object keyed by file, with a value of an array
@@ -271,7 +351,7 @@
* also includes the file that it was left on, which was the key of the
* originall object.
*/
- ChangeComments.prototype._commentObjToArrayWithFile = function(comments) {
+ _commentObjToArrayWithFile(comments) {
let commentArr = [];
for (const file of Object.keys(comments)) {
const commentsForFile = [];
@@ -281,66 +361,61 @@
commentArr = commentArr.concat(commentsForFile);
}
return commentArr;
- };
+ }
- ChangeComments.prototype._commentObjToArray = function(comments) {
+ _commentObjToArray(comments) {
let commentArr = [];
for (const file of Object.keys(comments)) {
commentArr = commentArr.concat(comments[file]);
}
return commentArr;
- };
+ }
/**
- * Computes a string counting the number of commens in a given file and path.
+ * Computes a string counting the number of commens in a given file.
*
- * @param {number} patchNum
- * @param {string=} opt_path
+ * @param {{path: string, oldPath?: string, patchNum?: number}} file
* @return {number}
*/
- ChangeComments.prototype.computeCommentCount = function(patchNum, opt_path) {
- if (opt_path) {
- return this.getAllCommentsForPath(opt_path, patchNum).length;
+ computeCommentCount(file) {
+ if (file.path) {
+ return this.getAllCommentsForFile(file).length;
}
- const allComments = this.getAllPublishedComments(patchNum);
+ const allComments = this.getAllPublishedComments(file.patchNum);
return this._commentObjToArray(allComments).length;
- };
+ }
/**
* Computes a string counting the number of draft comments in the entire
* change, optionally filtered by path and/or patchNum.
*
- * @param {number=} opt_patchNum
- * @param {string=} opt_path
+ * @param {?{path: string, oldPath?: string, patchNum?: number}} file
* @return {number}
*/
- ChangeComments.prototype.computeDraftCount = function(opt_patchNum,
- opt_path) {
- if (opt_path) {
- return this.getAllDraftsForPath(opt_path, opt_patchNum).length;
+ computeDraftCount(file) {
+ if (file && file.path) {
+ return this.getAllDraftsForFile(file).length;
}
- const allDrafts = this.getAllDrafts(opt_patchNum);
+ const allDrafts = this.getAllDrafts(file && file.patchNum);
return this._commentObjToArray(allDrafts).length;
- };
+ }
/**
* Computes a number of unresolved comment threads in a given file and path.
*
- * @param {number} patchNum
- * @param {string=} opt_path
+ * @param {{path: string, oldPath?: string, patchNum?: number}} file
* @return {number}
*/
- ChangeComments.prototype.computeUnresolvedNum = function(patchNum,
- opt_path) {
+ computeUnresolvedNum(file) {
let comments = [];
let drafts = [];
- if (opt_path) {
- comments = this.getAllCommentsForPath(opt_path, patchNum);
- drafts = this.getAllDraftsForPath(opt_path, patchNum);
+ if (file.path) {
+ comments = this.getAllCommentsForFile(file);
+ drafts = this.getAllDraftsForFile(file);
} else {
comments = this._commentObjToArray(
- this.getAllPublishedComments(patchNum));
+ this.getAllPublishedComments(file.patchNum));
}
comments = comments.concat(drafts);
@@ -350,23 +425,23 @@
const unresolvedThreads = threads
.filter(thread =>
thread.comments.length &&
- thread.comments[thread.comments.length - 1].unresolved);
+ thread.comments[thread.comments.length - 1].unresolved);
return unresolvedThreads.length;
- };
+ }
- ChangeComments.prototype.getAllThreadsForChange = function() {
+ getAllThreadsForChange() {
const comments = this._commentObjToArrayWithFile(this.getAllComments(true));
const sortedComments = this._sortComments(comments);
return this.getCommentThreads(sortedComments);
- };
+ }
- ChangeComments.prototype._sortComments = function(comments) {
+ _sortComments(comments) {
return comments.slice(0)
.sort(
(c1, c2) => util.parseDate(c1.updated) - util.parseDate(c2.updated)
);
- };
+ }
/**
* Computes all of the comments in thread format.
@@ -374,12 +449,12 @@
* @param {!Array} comments sorted by updated timestamp.
* @return {!Array}
*/
- ChangeComments.prototype.getCommentThreads = function(comments) {
+ getCommentThreads(comments) {
const threads = [];
const idThreadMap = {};
for (const comment of comments) {
- // If the comment is in reply to another comment, find that comment's
- // thread and append to it.
+ // If the comment is in reply to another comment, find that comment's
+ // thread and append to it.
if (comment.in_reply_to) {
const thread = idThreadMap[comment.in_reply_to];
if (thread) {
@@ -404,7 +479,7 @@
idThreadMap[comment.id] = newThread;
}
return threads;
- };
+ }
/**
* Whether the given comment should be included in the base side of the
@@ -414,29 +489,29 @@
* @param {!Gerrit.PatchRange} range
* @return {boolean}
*/
- ChangeComments.prototype._isInBaseOfPatchRange = function(comment, range) {
- // If the base of the patch range is a parent of a merge, and the comment
- // appears on a specific parent then only show the comment if the parent
- // index of the comment matches that of the range.
+ _isInBaseOfPatchRange(comment, range) {
+ // If the base of the patch range is a parent of a merge, and the comment
+ // appears on a specific parent then only show the comment if the parent
+ // index of the comment matches that of the range.
if (comment.parent && comment.side === PARENT) {
return this._isMergeParent(range.basePatchNum) &&
- comment.parent === this._getParentIndex(range.basePatchNum);
+ comment.parent === this._getParentIndex(range.basePatchNum);
}
// If the base of the range is the parent of the patch:
if (range.basePatchNum === PARENT &&
- comment.side === PARENT &&
- this._patchNumEquals(comment.patch_set, range.patchNum)) {
+ comment.side === PARENT &&
+ this._patchNumEquals(comment.patch_set, range.patchNum)) {
return true;
}
// If the base of the range is not the parent of the patch:
if (range.basePatchNum !== PARENT &&
- comment.side !== PARENT &&
- this._patchNumEquals(comment.patch_set, range.basePatchNum)) {
+ comment.side !== PARENT &&
+ this._patchNumEquals(comment.patch_set, range.basePatchNum)) {
return true;
}
return false;
- };
+ }
/**
* Whether the given comment should be included in the revision side of the
@@ -446,11 +521,11 @@
* @param {!Gerrit.PatchRange} range
* @return {boolean}
*/
- ChangeComments.prototype._isInRevisionOfPatchRange = function(comment,
+ _isInRevisionOfPatchRange(comment,
range) {
return comment.side !== PARENT &&
- this._patchNumEquals(comment.patch_set, range.patchNum);
- };
+ this._patchNumEquals(comment.patch_set, range.patchNum);
+ }
/**
* Whether the given comment should be included in the given patch range.
@@ -459,75 +534,77 @@
* @param {!Gerrit.PatchRange} range
* @return {boolean|undefined}
*/
- ChangeComments.prototype._isInPatchRange = function(comment, range) {
+ _isInPatchRange(comment, range) {
return this._isInBaseOfPatchRange(comment, range) ||
- this._isInRevisionOfPatchRange(comment, range);
- };
+ this._isInRevisionOfPatchRange(comment, range);
+ }
+}
- /**
- * @appliesMixin Gerrit.PatchSetMixin
- * @extends Polymer.Element
- */
- class GrCommentApi extends Polymer.mixinBehaviors( [
- Gerrit.PatchSetBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-comment-api'; }
+/**
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @extends Polymer.Element
+ */
+class GrCommentApi extends mixinBehaviors( [
+ Gerrit.PatchSetBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- static get properties() {
- return {
- _changeComments: Object,
- };
- }
+ static get is() { return 'gr-comment-api'; }
- /** @override */
- created() {
- super.created();
- this.addEventListener('reload-drafts',
- changeNum => this.reloadDrafts(changeNum));
- }
-
- /**
- * Load all comments (with drafts and robot comments) for the given change
- * number. The returned promise resolves when the comments have loaded, but
- * does not yield the comment data.
- *
- * @param {number} changeNum
- * @return {!Promise<!Object>}
- */
- loadAll(changeNum) {
- const promises = [];
- promises.push(this.$.restAPI.getDiffComments(changeNum));
- promises.push(this.$.restAPI.getDiffRobotComments(changeNum));
- promises.push(this.$.restAPI.getDiffDrafts(changeNum));
-
- return Promise.all(promises).then(([comments, robotComments, drafts]) => {
- this._changeComments = new ChangeComments(comments,
- robotComments, drafts, changeNum);
- return this._changeComments;
- });
- }
-
- /**
- * Re-initialize _changeComments with a new ChangeComments object, that
- * uses the previous values for comments and robot comments, but fetches
- * updated draft comments.
- *
- * @param {number} changeNum
- * @return {!Promise<!Object>}
- */
- reloadDrafts(changeNum) {
- if (!this._changeComments) {
- return this.loadAll(changeNum);
- }
- return this.$.restAPI.getDiffDrafts(changeNum).then(drafts => {
- this._changeComments = new ChangeComments(this._changeComments.comments,
- this._changeComments.robotComments, drafts, changeNum);
- return this._changeComments;
- });
- }
+ static get properties() {
+ return {
+ _changeComments: Object,
+ };
}
- customElements.define(GrCommentApi.is, GrCommentApi);
-})();
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('reload-drafts',
+ changeNum => this.reloadDrafts(changeNum));
+ }
+
+ /**
+ * Load all comments (with drafts and robot comments) for the given change
+ * number. The returned promise resolves when the comments have loaded, but
+ * does not yield the comment data.
+ *
+ * @param {number} changeNum
+ * @return {!Promise<!Object>}
+ */
+ loadAll(changeNum) {
+ const promises = [];
+ promises.push(this.$.restAPI.getDiffComments(changeNum));
+ promises.push(this.$.restAPI.getDiffRobotComments(changeNum));
+ promises.push(this.$.restAPI.getDiffDrafts(changeNum));
+
+ return Promise.all(promises).then(([comments, robotComments, drafts]) => {
+ this._changeComments = new ChangeComments(comments,
+ robotComments, drafts, changeNum);
+ return this._changeComments;
+ });
+ }
+
+ /**
+ * Re-initialize _changeComments with a new ChangeComments object, that
+ * uses the previous values for comments and robot comments, but fetches
+ * updated draft comments.
+ *
+ * @param {number} changeNum
+ * @return {!Promise<!Object>}
+ */
+ reloadDrafts(changeNum) {
+ if (!this._changeComments) {
+ return this.loadAll(changeNum);
+ }
+ return this.$.restAPI.getDiffDrafts(changeNum).then(drafts => {
+ this._changeComments = new ChangeComments(this._changeComments.comments,
+ this._changeComments.robotComments, drafts, changeNum);
+ return this._changeComments;
+ });
+ }
+}
+
+customElements.define(GrCommentApi.is, GrCommentApi);
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.js
new file mode 100644
index 0000000..215bfac
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.js
@@ -0,0 +1,21 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
index f2f7c0f..e1539ea 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
@@ -19,16 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-comment-api</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-
-<link rel="import" href="./gr-comment-api.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,697 +30,725 @@
</template>
</test-fixture>
-<script>
- suite('gr-comment-api tests', async () => {
- await readyToTest();
- const PARENT = 'PARENT';
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-comment-api.js';
+suite('gr-comment-api tests', () => {
+ const PARENT = 'PARENT';
- let element;
- let sandbox;
+ let element;
+ let sandbox;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => { sandbox.restore(); });
+
+ test('loads logged-out', () => {
+ const changeNum = 1234;
+
+ sandbox.stub(element.$.restAPI, 'getLoggedIn')
+ .returns(Promise.resolve(false));
+ sandbox.stub(element.$.restAPI, 'getDiffComments')
+ .returns(Promise.resolve({
+ 'foo.c': [{id: '123', message: 'foo bar', in_reply_to: '321'}],
+ }));
+ sandbox.stub(element.$.restAPI, 'getDiffRobotComments')
+ .returns(Promise.resolve({'foo.c': [{id: '321', message: 'done'}]}));
+ sandbox.stub(element.$.restAPI, 'getDiffDrafts')
+ .returns(Promise.resolve({}));
+
+ return element.loadAll(changeNum).then(() => {
+ assert.isTrue(element.$.restAPI.getDiffComments.calledWithExactly(
+ changeNum));
+ assert.isTrue(element.$.restAPI.getDiffRobotComments.calledWithExactly(
+ changeNum));
+ assert.isTrue(element.$.restAPI.getDiffDrafts.calledWithExactly(
+ changeNum));
+ assert.isOk(element._changeComments._comments);
+ assert.isOk(element._changeComments._robotComments);
+ assert.deepEqual(element._changeComments._drafts, {});
+ });
+ });
+
+ test('loads logged-in', () => {
+ const changeNum = 1234;
+
+ sandbox.stub(element.$.restAPI, 'getLoggedIn')
+ .returns(Promise.resolve(true));
+ sandbox.stub(element.$.restAPI, 'getDiffComments')
+ .returns(Promise.resolve({
+ 'foo.c': [{id: '123', message: 'foo bar', in_reply_to: '321'}],
+ }));
+ sandbox.stub(element.$.restAPI, 'getDiffRobotComments')
+ .returns(Promise.resolve({'foo.c': [{id: '321', message: 'done'}]}));
+ sandbox.stub(element.$.restAPI, 'getDiffDrafts')
+ .returns(Promise.resolve({'foo.c': [{id: '555', message: 'ack'}]}));
+
+ return element.loadAll(changeNum).then(() => {
+ assert.isTrue(element.$.restAPI.getDiffComments.calledWithExactly(
+ changeNum));
+ assert.isTrue(element.$.restAPI.getDiffRobotComments.calledWithExactly(
+ changeNum));
+ assert.isTrue(element.$.restAPI.getDiffDrafts.calledWithExactly(
+ changeNum));
+ assert.isOk(element._changeComments._comments);
+ assert.isOk(element._changeComments._robotComments);
+ assert.notDeepEqual(element._changeComments._drafts, {});
+ });
+ });
+
+ suite('reloadDrafts', () => {
+ let commentStub;
+ let robotCommentStub;
+ let draftStub;
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('loads logged-out', () => {
- const changeNum = 1234;
-
- sandbox.stub(element.$.restAPI, 'getLoggedIn')
- .returns(Promise.resolve(false));
- sandbox.stub(element.$.restAPI, 'getDiffComments')
- .returns(Promise.resolve({
- 'foo.c': [{id: '123', message: 'foo bar', in_reply_to: '321'}],
- }));
- sandbox.stub(element.$.restAPI, 'getDiffRobotComments')
- .returns(Promise.resolve({'foo.c': [{id: '321', message: 'done'}]}));
- sandbox.stub(element.$.restAPI, 'getDiffDrafts')
+ commentStub = sandbox.stub(element.$.restAPI, 'getDiffComments')
.returns(Promise.resolve({}));
+ robotCommentStub = sandbox.stub(element.$.restAPI,
+ 'getDiffRobotComments').returns(Promise.resolve({}));
+ draftStub = sandbox.stub(element.$.restAPI, 'getDiffDrafts')
+ .returns(Promise.resolve({}));
+ });
- return element.loadAll(changeNum).then(() => {
- assert.isTrue(element.$.restAPI.getDiffComments.calledWithExactly(
- changeNum));
- assert.isTrue(element.$.restAPI.getDiffRobotComments.calledWithExactly(
- changeNum));
- assert.isTrue(element.$.restAPI.getDiffDrafts.calledWithExactly(
- changeNum));
- assert.isOk(element._changeComments._comments);
- assert.isOk(element._changeComments._robotComments);
- assert.deepEqual(element._changeComments._drafts, {});
+ test('without loadAll first', done => {
+ assert.isNotOk(element._changeComments);
+ sandbox.spy(element, 'loadAll');
+ element.reloadDrafts().then(() => {
+ assert.isTrue(element.loadAll.called);
+ assert.isOk(element._changeComments);
+ assert.equal(commentStub.callCount, 1);
+ assert.equal(robotCommentStub.callCount, 1);
+ assert.equal(draftStub.callCount, 1);
+ done();
});
});
- test('loads logged-in', () => {
+ test('with loadAll first', done => {
+ assert.isNotOk(element._changeComments);
+ element.loadAll()
+ .then(() => {
+ assert.isOk(element._changeComments);
+ assert.equal(commentStub.callCount, 1);
+ assert.equal(robotCommentStub.callCount, 1);
+ assert.equal(draftStub.callCount, 1);
+ return element.reloadDrafts();
+ })
+ .then(() => {
+ assert.isOk(element._changeComments);
+ assert.equal(commentStub.callCount, 1);
+ assert.equal(robotCommentStub.callCount, 1);
+ assert.equal(draftStub.callCount, 2);
+ done();
+ });
+ });
+ });
+
+ suite('_changeComment methods', () => {
+ setup(done => {
const changeNum = 1234;
-
- sandbox.stub(element.$.restAPI, 'getLoggedIn')
- .returns(Promise.resolve(true));
- sandbox.stub(element.$.restAPI, 'getDiffComments')
- .returns(Promise.resolve({
- 'foo.c': [{id: '123', message: 'foo bar', in_reply_to: '321'}],
- }));
- sandbox.stub(element.$.restAPI, 'getDiffRobotComments')
- .returns(Promise.resolve({'foo.c': [{id: '321', message: 'done'}]}));
- sandbox.stub(element.$.restAPI, 'getDiffDrafts')
- .returns(Promise.resolve({'foo.c': [{id: '555', message: 'ack'}]}));
-
- return element.loadAll(changeNum).then(() => {
- assert.isTrue(element.$.restAPI.getDiffComments.calledWithExactly(
- changeNum));
- assert.isTrue(element.$.restAPI.getDiffRobotComments.calledWithExactly(
- changeNum));
- assert.isTrue(element.$.restAPI.getDiffDrafts.calledWithExactly(
- changeNum));
- assert.isOk(element._changeComments._comments);
- assert.isOk(element._changeComments._robotComments);
- assert.notDeepEqual(element._changeComments._drafts, {});
+ stub('gr-rest-api-interface', {
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ });
+ element.loadAll(changeNum).then(() => {
+ done();
});
});
- suite('reloadDrafts', () => {
- let commentStub;
- let robotCommentStub;
- let draftStub;
+ test('_isInBaseOfPatchRange', () => {
+ const comment = {patch_set: 1};
+ const patchRange = {basePatchNum: 1, patchNum: 2};
+ assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment,
+ patchRange));
+
+ patchRange.basePatchNum = PARENT;
+ assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment,
+ patchRange));
+
+ comment.side = PARENT;
+ assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment,
+ patchRange));
+
+ comment.patch_set = 2;
+ assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment,
+ patchRange));
+
+ patchRange.basePatchNum = -2;
+ comment.side = PARENT;
+ comment.parent = 1;
+ assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment,
+ patchRange));
+
+ comment.parent = 2;
+ assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment,
+ patchRange));
+ });
+
+ test('_isInRevisionOfPatchRange', () => {
+ const comment = {patch_set: 123};
+ const patchRange = {basePatchNum: 122, patchNum: 124};
+ assert.isFalse(element._changeComments._isInRevisionOfPatchRange(
+ comment, patchRange));
+
+ patchRange.patchNum = 123;
+ assert.isTrue(element._changeComments._isInRevisionOfPatchRange(
+ comment, patchRange));
+
+ comment.side = PARENT;
+ assert.isFalse(element._changeComments._isInRevisionOfPatchRange(
+ comment, patchRange));
+ });
+
+ test('_isInPatchRange', () => {
+ const patchRange1 = {basePatchNum: 122, patchNum: 124};
+ const patchRange2 = {basePatchNum: 123, patchNum: 125};
+ const patchRange3 = {basePatchNum: 124, patchNum: 125};
+
+ const isInBasePatchStub = sandbox.stub(element._changeComments,
+ '_isInBaseOfPatchRange');
+ const isInRevisionPatchStub = sandbox.stub(element._changeComments,
+ '_isInRevisionOfPatchRange');
+
+ isInBasePatchStub.withArgs({}, patchRange1).returns(true);
+ isInBasePatchStub.withArgs({}, patchRange2).returns(false);
+ isInBasePatchStub.withArgs({}, patchRange3).returns(false);
+
+ isInRevisionPatchStub.withArgs({}, patchRange1).returns(false);
+ isInRevisionPatchStub.withArgs({}, patchRange2).returns(true);
+ isInRevisionPatchStub.withArgs({}, patchRange3).returns(false);
+
+ assert.isTrue(element._changeComments._isInPatchRange({}, patchRange1));
+ assert.isTrue(element._changeComments._isInPatchRange({}, patchRange2));
+ assert.isFalse(element._changeComments._isInPatchRange({},
+ patchRange3));
+ });
+
+ suite('comment ranges and paths', () => {
+ function makeTime(mins) {
+ return `2013-02-26 15:0${mins}:43.986000000`;
+ }
+
setup(() => {
- commentStub = sandbox.stub(element.$.restAPI, 'getDiffComments')
- .returns(Promise.resolve({}));
- robotCommentStub = sandbox.stub(element.$.restAPI,
- 'getDiffRobotComments').returns(Promise.resolve({}));
- draftStub = sandbox.stub(element.$.restAPI, 'getDiffDrafts')
- .returns(Promise.resolve({}));
+ element._changeComments._drafts = {
+ 'file/one': [
+ {
+ id: 11,
+ patch_set: 2,
+ side: PARENT,
+ line: 1,
+ updated: makeTime(3),
+ },
+ {
+ id: 12,
+ in_reply_to: 2,
+ patch_set: 2,
+ line: 1,
+ updated: makeTime(3),
+ },
+ ],
+ 'file/two': [
+ {
+ id: 5,
+ patch_set: 3,
+ line: 1,
+ updated: makeTime(3),
+ },
+ ],
+ };
+ element._changeComments._robotComments = {
+ 'file/one': [
+ {
+ id: 1,
+ patch_set: 2,
+ side: PARENT,
+ line: 1,
+ updated: makeTime(1),
+ range: {
+ start_line: 1,
+ start_character: 2,
+ end_line: 2,
+ end_character: 2,
+ },
+ }, {
+ id: 2,
+ in_reply_to: 4,
+ patch_set: 2,
+ unresolved: true,
+ line: 1,
+ updated: makeTime(2),
+ },
+ ],
+ };
+ element._changeComments._comments = {
+ 'file/one': [
+ {id: 3, patch_set: 2, side: PARENT, line: 2, updated: makeTime(1)},
+ {id: 4, patch_set: 2, line: 1, updated: makeTime(1)},
+ ],
+ 'file/two': [
+ {id: 5, patch_set: 2, line: 2, updated: makeTime(1)},
+ {id: 6, patch_set: 3, line: 2, updated: makeTime(1)},
+ ],
+ 'file/three': [
+ {
+ id: 7,
+ patch_set: 2,
+ side: PARENT,
+ unresolved: true,
+ line: 1,
+ updated: makeTime(1),
+ },
+ {id: 8, patch_set: 3, line: 1, updated: makeTime(1)},
+ ],
+ 'file/four': [
+ {id: 9, patch_set: 5, side: PARENT, line: 1, updated: makeTime(1)},
+ {id: 10, patch_set: 5, line: 1, updated: makeTime(1)},
+ ],
+ };
});
- test('without loadAll first', done => {
- assert.isNotOk(element._changeComments);
- sandbox.spy(element, 'loadAll');
- element.reloadDrafts().then(() => {
- assert.isTrue(element.loadAll.called);
- assert.isOk(element._changeComments);
- assert.equal(commentStub.callCount, 1);
- assert.equal(robotCommentStub.callCount, 1);
- assert.equal(draftStub.callCount, 1);
- done();
- });
- });
-
- test('with loadAll first', done => {
- assert.isNotOk(element._changeComments);
- element.loadAll()
- .then(() => {
- assert.isOk(element._changeComments);
- assert.equal(commentStub.callCount, 1);
- assert.equal(robotCommentStub.callCount, 1);
- assert.equal(draftStub.callCount, 1);
- return element.reloadDrafts();
- })
- .then(() => {
- assert.isOk(element._changeComments);
- assert.equal(commentStub.callCount, 1);
- assert.equal(robotCommentStub.callCount, 1);
- assert.equal(draftStub.callCount, 2);
- done();
- });
- });
- });
-
- suite('_changeComment methods', () => {
- setup(done => {
- const changeNum = 1234;
- stub('gr-rest-api-interface', {
- getDiffComments() { return Promise.resolve({}); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
- });
- element.loadAll(changeNum).then(() => {
- done();
- });
- });
-
- test('_isInBaseOfPatchRange', () => {
- const comment = {patch_set: 1};
- const patchRange = {basePatchNum: 1, patchNum: 2};
- assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment,
- patchRange));
+ test('getPaths', () => {
+ const patchRange = {basePatchNum: 1, patchNum: 4};
+ let paths = element._changeComments.getPaths(patchRange);
+ assert.equal(Object.keys(paths).length, 0);
patchRange.basePatchNum = PARENT;
- assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment,
- patchRange));
+ patchRange.patchNum = 3;
+ paths = element._changeComments.getPaths(patchRange);
+ assert.notProperty(paths, 'file/one');
+ assert.property(paths, 'file/two');
+ assert.property(paths, 'file/three');
+ assert.notProperty(paths, 'file/four');
- comment.side = PARENT;
- assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment,
- patchRange));
+ patchRange.patchNum = 2;
+ paths = element._changeComments.getPaths(patchRange);
+ assert.property(paths, 'file/one');
+ assert.property(paths, 'file/two');
+ assert.property(paths, 'file/three');
+ assert.notProperty(paths, 'file/four');
- comment.patch_set = 2;
- assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment,
- patchRange));
-
- patchRange.basePatchNum = -2;
- comment.side = PARENT;
- comment.parent = 1;
- assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment,
- patchRange));
-
- comment.parent = 2;
- assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment,
- patchRange));
+ paths = element._changeComments.getPaths();
+ assert.property(paths, 'file/one');
+ assert.property(paths, 'file/two');
+ assert.property(paths, 'file/three');
+ assert.property(paths, 'file/four');
});
- test('_isInRevisionOfPatchRange', () => {
- const comment = {patch_set: 123};
- const patchRange = {basePatchNum: 122, patchNum: 124};
- assert.isFalse(element._changeComments._isInRevisionOfPatchRange(
- comment, patchRange));
+ test('getCommentsBySideForPath', () => {
+ const patchRange = {basePatchNum: 1, patchNum: 3};
+ let path = 'file/one';
+ let comments = element._changeComments.getCommentsBySideForPath(path,
+ patchRange);
+ assert.equal(comments.meta.changeNum, 1234);
+ assert.equal(comments.left.length, 0);
+ assert.equal(comments.right.length, 0);
- patchRange.patchNum = 123;
- assert.isTrue(element._changeComments._isInRevisionOfPatchRange(
- comment, patchRange));
+ path = 'file/two';
+ comments = element._changeComments.getCommentsBySideForPath(path,
+ patchRange);
+ assert.equal(comments.left.length, 0);
+ assert.equal(comments.right.length, 2);
- comment.side = PARENT;
- assert.isFalse(element._changeComments._isInRevisionOfPatchRange(
- comment, patchRange));
+ patchRange.basePatchNum = 2;
+ comments = element._changeComments.getCommentsBySideForPath(path,
+ patchRange);
+ assert.equal(comments.left.length, 1);
+ assert.equal(comments.right.length, 2);
+
+ patchRange.basePatchNum = PARENT;
+ path = 'file/three';
+ comments = element._changeComments.getCommentsBySideForPath(path,
+ patchRange);
+ assert.equal(comments.left.length, 0);
+ assert.equal(comments.right.length, 1);
});
- test('_isInPatchRange', () => {
- const patchRange1 = {basePatchNum: 122, patchNum: 124};
- const patchRange2 = {basePatchNum: 123, patchNum: 125};
- const patchRange3 = {basePatchNum: 124, patchNum: 125};
-
- const isInBasePatchStub = sandbox.stub(element._changeComments,
- '_isInBaseOfPatchRange');
- const isInRevisionPatchStub = sandbox.stub(element._changeComments,
- '_isInRevisionOfPatchRange');
-
- isInBasePatchStub.withArgs({}, patchRange1).returns(true);
- isInBasePatchStub.withArgs({}, patchRange2).returns(false);
- isInBasePatchStub.withArgs({}, patchRange3).returns(false);
-
- isInRevisionPatchStub.withArgs({}, patchRange1).returns(false);
- isInRevisionPatchStub.withArgs({}, patchRange2).returns(true);
- isInRevisionPatchStub.withArgs({}, patchRange3).returns(false);
-
- assert.isTrue(element._changeComments._isInPatchRange({}, patchRange1));
- assert.isTrue(element._changeComments._isInPatchRange({}, patchRange2));
- assert.isFalse(element._changeComments._isInPatchRange({},
- patchRange3));
+ test('getAllCommentsForPath', () => {
+ let path = 'file/one';
+ let comments = element._changeComments.getAllCommentsForPath(path);
+ assert.deepEqual(comments.length, 4);
+ path = 'file/two';
+ comments = element._changeComments.getAllCommentsForPath(path, 2);
+ assert.deepEqual(comments.length, 1);
});
- suite('comment ranges and paths', () => {
- function makeTime(mins) {
- return `2013-02-26 15:0${mins}:43.986000000`;
- }
+ test('getAllDraftsForPath', () => {
+ const path = 'file/one';
+ const drafts = element._changeComments.getAllDraftsForPath(path);
+ assert.deepEqual(drafts.length, 2);
+ });
- setup(() => {
- element._changeComments._drafts = {
- 'file/one': [
- {
- id: 11,
- patch_set: 2,
- side: PARENT,
- line: 1,
- updated: makeTime(3),
- },
- {
- id: 12,
- in_reply_to: 2,
- patch_set: 2,
- line: 1,
- updated: makeTime(3),
- },
- ],
- 'file/two': [
+ test('computeUnresolvedNum', () => {
+ assert.equal(element._changeComments
+ .computeUnresolvedNum({
+ patchNum: 2,
+ path: 'file/one',
+ }), 0);
+ assert.equal(element._changeComments
+ .computeUnresolvedNum({
+ patchNum: 1,
+ path: 'file/one',
+ }), 0);
+ assert.equal(element._changeComments
+ .computeUnresolvedNum({
+ patchNum: 2,
+ path: 'file/three',
+ }), 1);
+ });
+
+ test('computeUnresolvedNum w/ non-linear thread', () => {
+ element._changeComments._drafts = {};
+ element._changeComments._robotComments = {};
+ element._changeComments._comments = {
+ path: [{
+ id: '9c6ba3c6_28b7d467',
+ patch_set: 1,
+ updated: '2018-02-28 14:41:13.000000000',
+ unresolved: true,
+ }, {
+ id: '3df7b331_0bead405',
+ patch_set: 1,
+ in_reply_to: '1c346623_ab85d14a',
+ updated: '2018-02-28 23:07:55.000000000',
+ unresolved: false,
+ }, {
+ id: '6153dce6_69958d1e',
+ patch_set: 1,
+ in_reply_to: '9c6ba3c6_28b7d467',
+ updated: '2018-02-28 17:11:31.000000000',
+ unresolved: true,
+ }, {
+ id: '1c346623_ab85d14a',
+ patch_set: 1,
+ in_reply_to: '9c6ba3c6_28b7d467',
+ updated: '2018-02-28 23:01:39.000000000',
+ unresolved: false,
+ }],
+ };
+ assert.equal(
+ element._changeComments.computeUnresolvedNum(1, 'path'), 0);
+ });
+
+ test('computeCommentCount', () => {
+ assert.equal(element._changeComments
+ .computeCommentCount({
+ patchNum: 2,
+ path: 'file/one',
+ }), 4);
+ assert.equal(element._changeComments
+ .computeCommentCount({
+ patchNum: 1,
+ path: 'file/one',
+ }), 0);
+ assert.equal(element._changeComments
+ .computeCommentCount({
+ patchNum: 2,
+ path: 'file/three',
+ }), 1);
+ });
+
+ test('computeDraftCount', () => {
+ assert.equal(element._changeComments
+ .computeDraftCount({
+ patchNum: 2,
+ path: 'file/one',
+ }), 2);
+ assert.equal(element._changeComments
+ .computeDraftCount({
+ patchNum: 1,
+ path: 'file/one',
+ }), 0);
+ assert.equal(element._changeComments
+ .computeDraftCount({
+ patchNum: 2,
+ path: 'file/three',
+ }), 0);
+ assert.equal(element._changeComments
+ .computeDraftCount(), 3);
+ });
+
+ test('getAllPublishedComments', () => {
+ let publishedComments = element._changeComments
+ .getAllPublishedComments();
+ assert.equal(Object.keys(publishedComments).length, 4);
+ assert.equal(Object.keys(publishedComments[['file/one']]).length, 4);
+ assert.equal(Object.keys(publishedComments[['file/two']]).length, 2);
+ publishedComments = element._changeComments
+ .getAllPublishedComments(2);
+ assert.equal(Object.keys(publishedComments[['file/one']]).length, 4);
+ assert.equal(Object.keys(publishedComments[['file/two']]).length, 1);
+ });
+
+ test('getAllComments', () => {
+ let comments = element._changeComments.getAllComments();
+ assert.equal(Object.keys(comments).length, 4);
+ assert.equal(Object.keys(comments[['file/one']]).length, 4);
+ assert.equal(Object.keys(comments[['file/two']]).length, 2);
+ comments = element._changeComments.getAllComments(false, 2);
+ assert.equal(Object.keys(comments).length, 4);
+ assert.equal(Object.keys(comments[['file/one']]).length, 4);
+ assert.equal(Object.keys(comments[['file/two']]).length, 1);
+ // Include drafts
+ comments = element._changeComments.getAllComments(true);
+ assert.equal(Object.keys(comments).length, 4);
+ assert.equal(Object.keys(comments[['file/one']]).length, 6);
+ assert.equal(Object.keys(comments[['file/two']]).length, 3);
+ comments = element._changeComments.getAllComments(true, 2);
+ assert.equal(Object.keys(comments).length, 4);
+ assert.equal(Object.keys(comments[['file/one']]).length, 6);
+ assert.equal(Object.keys(comments[['file/two']]).length, 1);
+ });
+
+ test('computeAllThreads', () => {
+ const expectedThreads = [
+ {
+ comments: [
{
id: 5,
- patch_set: 3,
- line: 1,
- updated: makeTime(3),
+ patch_set: 2,
+ line: 2,
+ __path: 'file/two',
+ updated: '2013-02-26 15:01:43.986000000',
},
],
- };
- element._changeComments._robotComments = {
- 'file/one': [
+ patchNum: 2,
+ path: 'file/two',
+ line: 2,
+ rootId: 5,
+ }, {
+ comments: [
+ {
+ id: 3,
+ patch_set: 2,
+ side: 'PARENT',
+ line: 2,
+ __path: 'file/one',
+ updated: '2013-02-26 15:01:43.986000000',
+ },
+ ],
+ commentSide: 'PARENT',
+ patchNum: 2,
+ path: 'file/one',
+ line: 2,
+ rootId: 3,
+ }, {
+ comments: [
{
id: 1,
patch_set: 2,
- side: PARENT,
+ side: 'PARENT',
line: 1,
- updated: makeTime(1),
+ updated: '2013-02-26 15:01:43.986000000',
range: {
start_line: 1,
start_character: 2,
end_line: 2,
end_character: 2,
},
- }, {
+ __path: 'file/one',
+ },
+ ],
+ commentSide: 'PARENT',
+ patchNum: 2,
+ path: 'file/one',
+ line: 1,
+ rootId: 1,
+ }, {
+ comments: [
+ {
+ id: 9,
+ patch_set: 5,
+ side: 'PARENT',
+ line: 1,
+ __path: 'file/four',
+ updated: '2013-02-26 15:01:43.986000000',
+ },
+ ],
+ commentSide: 'PARENT',
+ patchNum: 5,
+ path: 'file/four',
+ line: 1,
+ rootId: 9,
+ }, {
+ comments: [
+ {
+ id: 8,
+ patch_set: 3,
+ line: 1,
+ __path: 'file/three',
+ updated: '2013-02-26 15:01:43.986000000',
+ },
+ ],
+ patchNum: 3,
+ path: 'file/three',
+ line: 1,
+ rootId: 8,
+ }, {
+ comments: [
+ {
+ id: 7,
+ patch_set: 2,
+ side: 'PARENT',
+ unresolved: true,
+ line: 1,
+ __path: 'file/three',
+ updated: '2013-02-26 15:01:43.986000000',
+ },
+ ],
+ commentSide: 'PARENT',
+ patchNum: 2,
+ path: 'file/three',
+ line: 1,
+ rootId: 7,
+ }, {
+ comments: [
+ {
+ id: 4,
+ patch_set: 2,
+ line: 1,
+ __path: 'file/one',
+ updated: '2013-02-26 15:01:43.986000000',
+ },
+ {
id: 2,
in_reply_to: 4,
patch_set: 2,
unresolved: true,
line: 1,
- updated: makeTime(2),
+ __path: 'file/one',
+ updated: '2013-02-26 15:02:43.986000000',
},
- ],
- };
- element._changeComments._comments = {
- 'file/one': [
- {id: 3, patch_set: 2, side: PARENT, line: 2, updated: makeTime(1)},
- {id: 4, patch_set: 2, line: 1, updated: makeTime(1)},
- ],
- 'file/two': [
- {id: 5, patch_set: 2, line: 2, updated: makeTime(1)},
- {id: 6, patch_set: 3, line: 2, updated: makeTime(1)},
- ],
- 'file/three': [
{
- id: 7,
+ id: 12,
+ in_reply_to: 2,
patch_set: 2,
- side: PARENT,
- unresolved: true,
line: 1,
- updated: makeTime(1),
+ __path: 'file/one',
+ __draft: true,
+ updated: '2013-02-26 15:03:43.986000000',
},
- {id: 8, patch_set: 3, line: 1, updated: makeTime(1)},
],
- 'file/four': [
- {id: 9, patch_set: 5, side: PARENT, line: 1, updated: makeTime(1)},
- {id: 10, patch_set: 5, line: 1, updated: makeTime(1)},
- ],
- };
- });
-
- test('getPaths', () => {
- const patchRange = {basePatchNum: 1, patchNum: 4};
- let paths = element._changeComments.getPaths(patchRange);
- assert.equal(Object.keys(paths).length, 0);
-
- patchRange.basePatchNum = PARENT;
- patchRange.patchNum = 3;
- paths = element._changeComments.getPaths(patchRange);
- assert.notProperty(paths, 'file/one');
- assert.property(paths, 'file/two');
- assert.property(paths, 'file/three');
- assert.notProperty(paths, 'file/four');
-
- patchRange.patchNum = 2;
- paths = element._changeComments.getPaths(patchRange);
- assert.property(paths, 'file/one');
- assert.property(paths, 'file/two');
- assert.property(paths, 'file/three');
- assert.notProperty(paths, 'file/four');
-
- paths = element._changeComments.getPaths();
- assert.property(paths, 'file/one');
- assert.property(paths, 'file/two');
- assert.property(paths, 'file/three');
- assert.property(paths, 'file/four');
- });
-
- test('getCommentsBySideForPath', () => {
- const patchRange = {basePatchNum: 1, patchNum: 3};
- let path = 'file/one';
- let comments = element._changeComments.getCommentsBySideForPath(path,
- patchRange);
- assert.equal(comments.meta.changeNum, 1234);
- assert.equal(comments.left.length, 0);
- assert.equal(comments.right.length, 0);
-
- path = 'file/two';
- comments = element._changeComments.getCommentsBySideForPath(path,
- patchRange);
- assert.equal(comments.left.length, 0);
- assert.equal(comments.right.length, 2);
-
- patchRange.basePatchNum = 2;
- comments = element._changeComments.getCommentsBySideForPath(path,
- patchRange);
- assert.equal(comments.left.length, 1);
- assert.equal(comments.right.length, 2);
-
- patchRange.basePatchNum = PARENT;
- path = 'file/three';
- comments = element._changeComments.getCommentsBySideForPath(path,
- patchRange);
- assert.equal(comments.left.length, 0);
- assert.equal(comments.right.length, 1);
- });
-
- test('getAllCommentsForPath', () => {
- let path = 'file/one';
- let comments = element._changeComments.getAllCommentsForPath(path);
- assert.deepEqual(comments.length, 4);
- path = 'file/two';
- comments = element._changeComments.getAllCommentsForPath(path, 2);
- assert.deepEqual(comments.length, 1);
- });
-
- test('getAllDraftsForPath', () => {
- const path = 'file/one';
- const drafts = element._changeComments.getAllDraftsForPath(path);
- assert.deepEqual(drafts.length, 2);
- });
-
- test('computeUnresolvedNum', () => {
- assert.equal(element._changeComments
- .computeUnresolvedNum(2, 'file/one'), 0);
- assert.equal(element._changeComments
- .computeUnresolvedNum(1, 'file/one'), 0);
- assert.equal(element._changeComments
- .computeUnresolvedNum(2, 'file/three'), 1);
- });
-
- test('computeUnresolvedNum w/ non-linear thread', () => {
- element._changeComments._drafts = {};
- element._changeComments._robotComments = {};
- element._changeComments._comments = {
- path: [{
- id: '9c6ba3c6_28b7d467',
- patch_set: 1,
- updated: '2018-02-28 14:41:13.000000000',
- unresolved: true,
- }, {
- id: '3df7b331_0bead405',
- patch_set: 1,
- in_reply_to: '1c346623_ab85d14a',
- updated: '2018-02-28 23:07:55.000000000',
- unresolved: false,
- }, {
- id: '6153dce6_69958d1e',
- patch_set: 1,
- in_reply_to: '9c6ba3c6_28b7d467',
- updated: '2018-02-28 17:11:31.000000000',
- unresolved: true,
- }, {
- id: '1c346623_ab85d14a',
- patch_set: 1,
- in_reply_to: '9c6ba3c6_28b7d467',
- updated: '2018-02-28 23:01:39.000000000',
- unresolved: false,
- }],
- };
- assert.equal(
- element._changeComments.computeUnresolvedNum(1, 'path'), 0);
- });
-
- test('computeCommentCount', () => {
- assert.equal(element._changeComments
- .computeCommentCount(2, 'file/one'), 4);
- assert.equal(element._changeComments
- .computeCommentCount(1, 'file/one'), 0);
- assert.equal(element._changeComments
- .computeCommentCount(2, 'file/three'), 1);
- });
-
- test('computeDraftCount', () => {
- assert.equal(element._changeComments
- .computeDraftCount(2, 'file/one'), 2);
- assert.equal(element._changeComments
- .computeDraftCount(1, 'file/one'), 0);
- assert.equal(element._changeComments
- .computeDraftCount(2, 'file/three'), 0);
- assert.equal(element._changeComments
- .computeDraftCount(), 3);
- });
-
- test('getAllPublishedComments', () => {
- let publishedComments = element._changeComments
- .getAllPublishedComments();
- assert.equal(Object.keys(publishedComments).length, 4);
- assert.equal(Object.keys(publishedComments[['file/one']]).length, 4);
- assert.equal(Object.keys(publishedComments[['file/two']]).length, 2);
- publishedComments = element._changeComments
- .getAllPublishedComments(2);
- assert.equal(Object.keys(publishedComments[['file/one']]).length, 4);
- assert.equal(Object.keys(publishedComments[['file/two']]).length, 1);
- });
-
- test('getAllComments', () => {
- let comments = element._changeComments.getAllComments();
- assert.equal(Object.keys(comments).length, 4);
- assert.equal(Object.keys(comments[['file/one']]).length, 4);
- assert.equal(Object.keys(comments[['file/two']]).length, 2);
- comments = element._changeComments.getAllComments(false, 2);
- assert.equal(Object.keys(comments).length, 4);
- assert.equal(Object.keys(comments[['file/one']]).length, 4);
- assert.equal(Object.keys(comments[['file/two']]).length, 1);
- // Include drafts
- comments = element._changeComments.getAllComments(true);
- assert.equal(Object.keys(comments).length, 4);
- assert.equal(Object.keys(comments[['file/one']]).length, 6);
- assert.equal(Object.keys(comments[['file/two']]).length, 3);
- comments = element._changeComments.getAllComments(true, 2);
- assert.equal(Object.keys(comments).length, 4);
- assert.equal(Object.keys(comments[['file/one']]).length, 6);
- assert.equal(Object.keys(comments[['file/two']]).length, 1);
- });
-
- test('computeAllThreads', () => {
- const expectedThreads = [
- {
- comments: [
- {
- id: 5,
- patch_set: 2,
- line: 2,
- __path: 'file/two',
- updated: '2013-02-26 15:01:43.986000000',
- },
- ],
- patchNum: 2,
- path: 'file/two',
- line: 2,
- rootId: 5,
- }, {
- comments: [
- {
- id: 3,
- patch_set: 2,
- side: 'PARENT',
- line: 2,
- __path: 'file/one',
- updated: '2013-02-26 15:01:43.986000000',
- },
- ],
- commentSide: 'PARENT',
- patchNum: 2,
- path: 'file/one',
- line: 2,
- rootId: 3,
- }, {
- comments: [
- {
- id: 1,
- patch_set: 2,
- side: 'PARENT',
- line: 1,
- updated: '2013-02-26 15:01:43.986000000',
- range: {
- start_line: 1,
- start_character: 2,
- end_line: 2,
- end_character: 2,
- },
- __path: 'file/one',
- },
- ],
- commentSide: 'PARENT',
- patchNum: 2,
- path: 'file/one',
- line: 1,
- rootId: 1,
- }, {
- comments: [
- {
- id: 9,
- patch_set: 5,
- side: 'PARENT',
- line: 1,
- __path: 'file/four',
- updated: '2013-02-26 15:01:43.986000000',
- },
- ],
- commentSide: 'PARENT',
- patchNum: 5,
- path: 'file/four',
- line: 1,
- rootId: 9,
- }, {
- comments: [
- {
- id: 8,
- patch_set: 3,
- line: 1,
- __path: 'file/three',
- updated: '2013-02-26 15:01:43.986000000',
- },
- ],
- patchNum: 3,
- path: 'file/three',
- line: 1,
- rootId: 8,
- }, {
- comments: [
- {
- id: 7,
- patch_set: 2,
- side: 'PARENT',
- unresolved: true,
- line: 1,
- __path: 'file/three',
- updated: '2013-02-26 15:01:43.986000000',
- },
- ],
- commentSide: 'PARENT',
- patchNum: 2,
- path: 'file/three',
- line: 1,
- rootId: 7,
- }, {
- comments: [
- {
- id: 4,
- patch_set: 2,
- line: 1,
- __path: 'file/one',
- updated: '2013-02-26 15:01:43.986000000',
- },
- {
- id: 2,
- in_reply_to: 4,
- patch_set: 2,
- unresolved: true,
- line: 1,
- __path: 'file/one',
- updated: '2013-02-26 15:02:43.986000000',
- },
- {
- id: 12,
- in_reply_to: 2,
- patch_set: 2,
- line: 1,
- __path: 'file/one',
- __draft: true,
- updated: '2013-02-26 15:03:43.986000000',
- },
- ],
- patchNum: 2,
- path: 'file/one',
- line: 1,
- rootId: 4,
- }, {
- comments: [
- {
- id: 6,
- patch_set: 3,
- line: 2,
- __path: 'file/two',
- updated: '2013-02-26 15:01:43.986000000',
- },
- ],
- patchNum: 3,
- path: 'file/two',
- line: 2,
- rootId: 6,
- }, {
- comments: [
- {
- id: 10,
- patch_set: 5,
- line: 1,
- __path: 'file/four',
- updated: '2013-02-26 15:01:43.986000000',
- },
- ],
- rootId: 10,
- patchNum: 5,
- path: 'file/four',
- line: 1,
- }, {
- comments: [
- {
- id: 5,
- patch_set: 3,
- line: 1,
- __path: 'file/two',
- __draft: true,
- updated: '2013-02-26 15:03:43.986000000',
- },
- ],
- rootId: 5,
- patchNum: 3,
- path: 'file/two',
- line: 1,
- }, {
- comments: [
- {
- id: 11,
- patch_set: 2,
- side: 'PARENT',
- line: 1,
- __path: 'file/one',
- __draft: true,
- updated: '2013-02-26 15:03:43.986000000',
- },
- ],
- rootId: 11,
- commentSide: 'PARENT',
- patchNum: 2,
- path: 'file/one',
- line: 1,
- },
- ];
- const threads = element._changeComments.getAllThreadsForChange();
- assert.deepEqual(threads, expectedThreads);
- });
-
- test('getCommentsForThreadGroup', () => {
- let expectedComments = [
- {
- __path: 'file/one',
- id: 4,
- patch_set: 2,
- line: 1,
- updated: '2013-02-26 15:01:43.986000000',
- },
- {
- __path: 'file/one',
- id: 2,
- in_reply_to: 4,
- patch_set: 2,
- unresolved: true,
- line: 1,
- updated: '2013-02-26 15:02:43.986000000',
- },
- {
- __path: 'file/one',
- __draft: true,
- id: 12,
- in_reply_to: 2,
- patch_set: 2,
- line: 1,
- updated: '2013-02-26 15:03:43.986000000',
- },
- ];
- assert.deepEqual(element._changeComments.getCommentsForThread(4),
- expectedComments);
-
- expectedComments = [{
- id: 11,
- patch_set: 2,
- side: 'PARENT',
+ patchNum: 2,
+ path: 'file/one',
line: 1,
+ rootId: 4,
+ }, {
+ comments: [
+ {
+ id: 6,
+ patch_set: 3,
+ line: 2,
+ __path: 'file/two',
+ updated: '2013-02-26 15:01:43.986000000',
+ },
+ ],
+ patchNum: 3,
+ path: 'file/two',
+ line: 2,
+ rootId: 6,
+ }, {
+ comments: [
+ {
+ id: 10,
+ patch_set: 5,
+ line: 1,
+ __path: 'file/four',
+ updated: '2013-02-26 15:01:43.986000000',
+ },
+ ],
+ rootId: 10,
+ patchNum: 5,
+ path: 'file/four',
+ line: 1,
+ }, {
+ comments: [
+ {
+ id: 5,
+ patch_set: 3,
+ line: 1,
+ __path: 'file/two',
+ __draft: true,
+ updated: '2013-02-26 15:03:43.986000000',
+ },
+ ],
+ rootId: 5,
+ patchNum: 3,
+ path: 'file/two',
+ line: 1,
+ }, {
+ comments: [
+ {
+ id: 11,
+ patch_set: 2,
+ side: 'PARENT',
+ line: 1,
+ __path: 'file/one',
+ __draft: true,
+ updated: '2013-02-26 15:03:43.986000000',
+ },
+ ],
+ rootId: 11,
+ commentSide: 'PARENT',
+ patchNum: 2,
+ path: 'file/one',
+ line: 1,
+ },
+ ];
+ const threads = element._changeComments.getAllThreadsForChange();
+ assert.deepEqual(threads, expectedThreads);
+ });
+
+ test('getCommentsForThreadGroup', () => {
+ let expectedComments = [
+ {
+ __path: 'file/one',
+ id: 4,
+ patch_set: 2,
+ line: 1,
+ updated: '2013-02-26 15:01:43.986000000',
+ },
+ {
+ __path: 'file/one',
+ id: 2,
+ in_reply_to: 4,
+ patch_set: 2,
+ unresolved: true,
+ line: 1,
+ updated: '2013-02-26 15:02:43.986000000',
+ },
+ {
__path: 'file/one',
__draft: true,
+ id: 12,
+ in_reply_to: 2,
+ patch_set: 2,
+ line: 1,
updated: '2013-02-26 15:03:43.986000000',
- }];
+ },
+ ];
+ assert.deepEqual(element._changeComments.getCommentsForThread(4),
+ expectedComments);
- assert.deepEqual(element._changeComments.getCommentsForThread(11),
- expectedComments);
+ expectedComments = [{
+ id: 11,
+ patch_set: 2,
+ side: 'PARENT',
+ line: 1,
+ __path: 'file/one',
+ __draft: true,
+ updated: '2013-02-26 15:03:43.986000000',
+ }];
- assert.deepEqual(element._changeComments.getCommentsForThread(1000),
- null);
- });
+ assert.deepEqual(element._changeComments.getCommentsForThread(11),
+ expectedComments);
+
+ assert.deepEqual(element._changeComments.getCommentsForThread(1000),
+ null);
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.html b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.html
deleted file mode 100644
index 549bf43..0000000
--- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.html
+++ /dev/null
@@ -1,25 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<dom-module id="gr-coverage-layer">
- <template>
- </template>
- <script src="../../../types/types.js"></script>
- <script src="../gr-diff-highlight/gr-annotation.js"></script>
- <script src="gr-coverage-layer.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
index 1bc4674..529c559 100644
--- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
@@ -14,101 +14,108 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const TOOLTIP_MAP = new Map([
- [Gerrit.CoverageType.COVERED, 'Covered by tests.'],
- [Gerrit.CoverageType.NOT_COVERED, 'Not covered by tests.'],
- [Gerrit.CoverageType.PARTIALLY_COVERED, 'Partially covered by tests.'],
- [Gerrit.CoverageType.NOT_INSTRUMENTED, 'Not instrumented by any tests.'],
- ]);
+import '../../../types/types.js';
+import '../gr-diff-highlight/gr-annotation.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-coverage-layer_html.js';
- /** @extends Polymer.Element */
- class GrCoverageLayer extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-coverage-layer'; }
+const TOOLTIP_MAP = new Map([
+ [Gerrit.CoverageType.COVERED, 'Covered by tests.'],
+ [Gerrit.CoverageType.NOT_COVERED, 'Not covered by tests.'],
+ [Gerrit.CoverageType.PARTIALLY_COVERED, 'Partially covered by tests.'],
+ [Gerrit.CoverageType.NOT_INSTRUMENTED, 'Not instrumented by any tests.'],
+]);
- static get properties() {
- return {
- /**
- * Must be sorted by code_range.start_line.
- * Must only contain ranges that match the side.
- *
- * @type {!Array<!Gerrit.CoverageRange>}
- */
- coverageRanges: Array,
- side: String,
+/** @extends Polymer.Element */
+class GrCoverageLayer extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /**
- * We keep track of the line number from the previous annotate() call,
- * and also of the index of the coverage range that had matched.
- * annotate() calls are coming in with increasing line numbers and
- * coverage ranges are sorted by line number. So this is a very simple
- * and efficient way for finding the coverage range that matches a given
- * line number.
- */
- _lineNumber: {
- type: Number,
- value: 0,
- },
- _index: {
- type: Number,
- value: 0,
- },
- };
- }
+ static get is() { return 'gr-coverage-layer'; }
+ static get properties() {
+ return {
/**
- * Layer method to add annotations to a line.
+ * Must be sorted by code_range.start_line.
+ * Must only contain ranges that match the side.
*
- * @param {!HTMLElement} el Not used for this layer.
- * @param {!HTMLElement} lineNumberEl The <td> element with the line number.
- * @param {!Object} line Not used for this layer.
+ * @type {!Array<!Gerrit.CoverageRange>}
*/
- annotate(el, lineNumberEl, line) {
- if (!lineNumberEl || !lineNumberEl.classList.contains(this.side)) {
- return;
- }
- const elementLineNumber = parseInt(
- lineNumberEl.getAttribute('data-value'), 10);
- if (!elementLineNumber || elementLineNumber < 1) return;
+ coverageRanges: Array,
+ side: String,
- // 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;
- }
- this._lineNumber = 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];
-
- // 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++;
- 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) {
- return;
- }
-
- // The line number is within the current coverage range. Style it!
- lineNumberEl.classList.add(coverageRange.type);
- lineNumberEl.title = TOOLTIP_MAP.get(coverageRange.type);
- return;
- }
- }
+ /**
+ * We keep track of the line number from the previous annotate() call,
+ * and also of the index of the coverage range that had matched.
+ * annotate() calls are coming in with increasing line numbers and
+ * coverage ranges are sorted by line number. So this is a very simple
+ * and efficient way for finding the coverage range that matches a given
+ * line number.
+ */
+ _lineNumber: {
+ type: Number,
+ value: 0,
+ },
+ _index: {
+ type: Number,
+ value: 0,
+ },
+ };
}
- customElements.define(GrCoverageLayer.is, GrCoverageLayer);
-})();
+ /**
+ * Layer method to add annotations to a line.
+ *
+ * @param {!HTMLElement} el Not used for this layer.
+ * @param {!HTMLElement} lineNumberEl The <td> element with the line number.
+ * @param {!Object} line Not used for this layer.
+ */
+ annotate(el, lineNumberEl, line) {
+ if (!lineNumberEl || !lineNumberEl.classList.contains(this.side)) {
+ return;
+ }
+ const elementLineNumber = parseInt(
+ lineNumberEl.getAttribute('data-value'), 10);
+ if (!elementLineNumber || elementLineNumber < 1) return;
+
+ // 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;
+ }
+ this._lineNumber = 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];
+
+ // 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++;
+ 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) {
+ return;
+ }
+
+ // The line number is within the current coverage range. Style it!
+ lineNumberEl.classList.add(coverageRange.type);
+ lineNumberEl.title = TOOLTIP_MAP.get(coverageRange.type);
+ return;
+ }
+ }
+}
+
+customElements.define(GrCoverageLayer.is, GrCoverageLayer);
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_html.js b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_html.js
new file mode 100644
index 0000000..29757e5
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_html.js
@@ -0,0 +1,21 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
index 8439a22..cec75fd 100644
--- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
@@ -19,17 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-coverage-layer</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../gr-diff/gr-diff-line.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-
-<link rel="import" href="gr-coverage-layer.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -37,106 +30,108 @@
</template>
</test-fixture>
-<script>
- suite('gr-coverage-layer', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../gr-diff/gr-diff-line.js';
+import '../../../test/common-test-setup.js';
+import './gr-coverage-layer.js';
+suite('gr-coverage-layer', () => {
+ let element;
- setup(() => {
- const initialCoverageRanges = [
- {
- type: 'COVERED',
- side: 'right',
- code_range: {
- start_line: 1,
- end_line: 2,
- },
+ 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: '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: '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,
- },
+ },
+ {
+ type: 'NOT_INSTRUMENTED',
+ side: 'right',
+ code_range: {
+ start_line: 8,
+ end_line: 9,
},
- ];
+ },
+ ];
- element = fixture('basic');
- element.coverageRanges = initialCoverageRanges;
- element.side = 'right';
+ element = fixture('basic');
+ 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');
});
- 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;
- }
+ test('line 3-4 are not covered', () => {
+ checkLine(3, 'NOT_COVERED');
+ checkLine(4, 'NOT_COVERED');
+ });
- 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 5-6 are partially covered', () => {
+ checkLine(5, 'PARTIALLY_COVERED');
+ checkLine(6, 'PARTIALLY_COVERED');
+ });
- test('line 1-2 are covered', () => {
- checkLine(1, 'COVERED');
- checkLine(2, '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 3-4 are not covered', () => {
- checkLine(3, 'NOT_COVERED');
- checkLine(4, 'NOT_COVERED');
- });
+ test('line 8-9 are not instrumented', () => {
+ checkLine(8, 'NOT_INSTRUMENTED');
+ checkLine(9, 'NOT_INSTRUMENTED');
+ });
- 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');
- });
+ 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');
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.html
deleted file mode 100644
index e8e60c4..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.html
+++ /dev/null
@@ -1,54 +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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../gr-coverage-layer/gr-coverage-layer.html">
-<link rel="import" href="../gr-diff-processor/gr-diff-processor.html">
-<link rel="import" href="../../../elements/shared/gr-hovercard/gr-hovercard.html">
-<link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html">
-
-<dom-module id="gr-diff-builder">
- <template>
- <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>
- </template>
- <script src="../../../scripts/util.js"></script>
- <script src="../gr-diff/gr-diff-line.js"></script>
- <script src="../gr-diff/gr-diff-group.js"></script>
- <script src="../gr-diff-highlight/gr-annotation.js"></script>
- <script src="gr-diff-builder.js"></script>
- <script src="gr-diff-builder-side-by-side.js"></script>
- <script src="gr-diff-builder-unified.js"></script>
- <script src="gr-diff-builder-image.js"></script>
- <script src="gr-diff-builder-binary.js"></script>
- <script src="gr-diff-builder-element.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
index 9e1ec9e..637c8f7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
@@ -1,417 +1,438 @@
/**
* @license
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2016 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the 'License');
+ * 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,
+ * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const DiffViewMode = {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- };
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../gr-coverage-layer/gr-coverage-layer.js';
+import '../gr-diff-processor/gr-diff-processor.js';
+import '../../shared/gr-hovercard/gr-hovercard.js';
+import '../gr-ranged-comment-layer/gr-ranged-comment-layer.js';
+import '../../../scripts/util.js';
+import '../gr-diff/gr-diff-line.js';
+import '../gr-diff/gr-diff-group.js';
+import '../gr-diff-highlight/gr-annotation.js';
+import './gr-diff-builder.js';
+import './gr-diff-builder-side-by-side.js';
+import './gr-diff-builder-unified.js';
+import './gr-diff-builder-image.js';
+import './gr-diff-builder-binary.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-diff-builder-element_html.js';
- const TRAILING_WHITESPACE_PATTERN = /\s+$/;
+const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+};
- // https://gerrit.googlesource.com/gerrit/+/234616a8627334686769f1de989d286039f4d6a5/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js#740
- const COMMIT_MSG_PATH = '/COMMIT_MSG';
- const COMMIT_MSG_LINE_LENGTH = 72;
+const TRAILING_WHITESPACE_PATTERN = /\s+$/;
+
+// https://gerrit.googlesource.com/gerrit/+/234616a8627334686769f1de989d286039f4d6a5/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js#740
+const COMMIT_MSG_PATH = '/COMMIT_MSG';
+const COMMIT_MSG_LINE_LENGTH = 72;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ */
+class GrDiffBuilderElement extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-diff-builder'; }
+ /**
+ * Fired when the diff begins rendering.
+ *
+ * @event render-start
+ */
/**
- * @appliesMixin Gerrit.FireMixin
+ * Fired when the diff finishes rendering text content.
+ *
+ * @event render-content
*/
- class GrDiffBuilderElement extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-diff-builder'; }
- /**
- * Fired when the diff begins rendering.
- *
- * @event render-start
- */
- /**
- * Fired when the diff finishes rendering text content.
- *
- * @event render-content
- */
+ static get properties() {
+ return {
+ diff: Object,
+ changeNum: String,
+ patchNum: String,
+ viewMode: String,
+ isImageDiff: Boolean,
+ baseImage: Object,
+ revisionImage: Object,
+ parentIndex: Number,
+ path: String,
+ projectName: String,
- static get properties() {
- return {
- diff: Object,
- changeNum: String,
- patchNum: String,
- viewMode: String,
- isImageDiff: Boolean,
- baseImage: Object,
- revisionImage: Object,
- parentIndex: Number,
- path: String,
- projectName: String,
+ _builder: Object,
+ _groups: Array,
+ _layers: Array,
+ _showTabs: Boolean,
+ /** @type {!Array<!Gerrit.HoveredRange>} */
+ commentRanges: {
+ type: Array,
+ value: () => [],
+ },
+ /** @type {!Array<!Gerrit.CoverageRange>} */
+ coverageRanges: {
+ type: Array,
+ value: () => [],
+ },
+ _leftCoverageRanges: {
+ type: Array,
+ computed: '_computeLeftCoverageRanges(coverageRanges)',
+ },
+ _rightCoverageRanges: {
+ type: Array,
+ computed: '_computeRightCoverageRanges(coverageRanges)',
+ },
+ /**
+ * The promise last returned from `render()` while the asynchronous
+ * rendering is running - `null` otherwise. Provides a `cancel()`
+ * method that rejects it with `{isCancelled: true}`.
+ *
+ * @type {?Object}
+ */
+ _cancelableRenderPromise: Object,
+ layers: {
+ type: Array,
+ value: [],
+ },
+ };
+ }
- _builder: Object,
- _groups: Array,
- _layers: Array,
- _showTabs: Boolean,
- /** @type {!Array<!Gerrit.HoveredRange>} */
- commentRanges: {
- type: Array,
- value: () => [],
- },
- /** @type {!Array<!Gerrit.CoverageRange>} */
- coverageRanges: {
- type: Array,
- value: () => [],
- },
- _leftCoverageRanges: {
- type: Array,
- computed: '_computeLeftCoverageRanges(coverageRanges)',
- },
- _rightCoverageRanges: {
- type: Array,
- computed: '_computeRightCoverageRanges(coverageRanges)',
- },
- /**
- * The promise last returned from `render()` while the asynchronous
- * rendering is running - `null` otherwise. Provides a `cancel()`
- * method that rejects it with `{isCancelled: true}`.
- *
- * @type {?Object}
- */
- _cancelableRenderPromise: Object,
- layers: {
- type: Array,
- value: [],
- },
- };
+ get diffElement() {
+ return this.queryEffectiveChildren('#diffTable');
+ }
+
+ static get observers() {
+ return [
+ '_groupsChanged(_groups.splices)',
+ ];
+ }
+
+ _computeLeftCoverageRanges(coverageRanges) {
+ return coverageRanges.filter(range => range && range.side === 'left');
+ }
+
+ _computeRightCoverageRanges(coverageRanges) {
+ return coverageRanges.filter(range => range && range.side === 'right');
+ }
+
+ render(keyLocations, prefs) {
+ // Setting up annotation layers must happen after plugins are
+ // installed, and |render| satisfies the requirement, however,
+ // |attached| doesn't because in the diff view page, the element is
+ // attached before plugins are installed.
+ this._setupAnnotationLayers();
+
+ this._showTabs = !!prefs.show_tabs;
+ this._showTrailingWhitespace = !!prefs.show_whitespace_errors;
+
+ // Stop the processor if it's running.
+ this.cancel();
+
+ this._builder = this._getDiffBuilder(this.diff, prefs);
+
+ this.$.processor.context = prefs.context;
+ this.$.processor.keyLocations = keyLocations;
+
+ this._clearDiffContent();
+ this._builder.addColumns(this.diffElement, prefs.font_size);
+
+ const isBinary = !!(this.isImageDiff || this.diff.binary);
+
+ this.dispatchEvent(new CustomEvent(
+ 'render-start', {bubbles: true, composed: true}));
+ this._cancelableRenderPromise = util.makeCancelable(
+ this.$.processor.process(this.diff.content, isBinary)
+ .then(() => {
+ if (this.isImageDiff) {
+ this._builder.renderDiff();
+ }
+ this.dispatchEvent(new CustomEvent('render-content',
+ {bubbles: true, composed: true}));
+ }));
+ return this._cancelableRenderPromise
+ .finally(() => { this._cancelableRenderPromise = null; })
+ // Mocca testing does not like uncaught rejections, so we catch
+ // the cancels which are expected and should not throw errors in
+ // tests.
+ .catch(e => { if (!e.isCanceled) return Promise.reject(e); });
+ }
+
+ _setupAnnotationLayers() {
+ const layers = [
+ this._createTrailingWhitespaceLayer(),
+ this._createIntralineLayer(),
+ this._createTabIndicatorLayer(),
+ this.$.rangeLayer,
+ this.$.coverageLayerLeft,
+ this.$.coverageLayerRight,
+ ];
+
+ if (this.layers) {
+ layers.push(...this.layers);
}
+ this._layers = layers;
+ }
- get diffElement() {
- return this.queryEffectiveChildren('#diffTable');
- }
-
- static get observers() {
- return [
- '_groupsChanged(_groups.splices)',
- ];
- }
-
- _computeLeftCoverageRanges(coverageRanges) {
- return coverageRanges.filter(range => range && range.side === 'left');
- }
-
- _computeRightCoverageRanges(coverageRanges) {
- return coverageRanges.filter(range => range && range.side === 'right');
- }
-
- render(keyLocations, prefs) {
- // Setting up annotation layers must happen after plugins are
- // installed, and |render| satisfies the requirement, however,
- // |attached| doesn't because in the diff view page, the element is
- // attached before plugins are installed.
- this._setupAnnotationLayers();
-
- this._showTabs = !!prefs.show_tabs;
- this._showTrailingWhitespace = !!prefs.show_whitespace_errors;
-
- // Stop the processor if it's running.
- this.cancel();
-
- this._builder = this._getDiffBuilder(this.diff, prefs);
-
- this.$.processor.context = prefs.context;
- this.$.processor.keyLocations = keyLocations;
-
- this._clearDiffContent();
- this._builder.addColumns(this.diffElement, prefs.font_size);
-
- const isBinary = !!(this.isImageDiff || this.diff.binary);
-
- this.dispatchEvent(new CustomEvent(
- 'render-start', {bubbles: true, composed: true}));
- this._cancelableRenderPromise = util.makeCancelable(
- this.$.processor.process(this.diff.content, isBinary)
- .then(() => {
- if (this.isImageDiff) {
- this._builder.renderDiff();
- }
- this.dispatchEvent(new CustomEvent('render-content',
- {bubbles: true, composed: true}));
- }));
- return this._cancelableRenderPromise
- .finally(() => { this._cancelableRenderPromise = null; })
- // Mocca testing does not like uncaught rejections, so we catch
- // the cancels which are expected and should not throw errors in
- // tests.
- .catch(e => { if (!e.isCanceled) return Promise.reject(e); });
- }
-
- _setupAnnotationLayers() {
- const layers = [
- this._createTrailingWhitespaceLayer(),
- this._createIntralineLayer(),
- this._createTabIndicatorLayer(),
- this.$.rangeLayer,
- this.$.coverageLayerLeft,
- this.$.coverageLayerRight,
- ];
-
- if (this.layers) {
- layers.push(...this.layers);
- }
- this._layers = layers;
- }
-
- getLineElByChild(node) {
- while (node) {
- if (node instanceof Element) {
- if (node.classList.contains('lineNum')) {
- return node;
- }
- if (node.classList.contains('section')) {
- return null;
- }
+ getLineElByChild(node) {
+ while (node) {
+ if (node instanceof Element) {
+ if (node.classList.contains('lineNum')) {
+ return node;
}
- node = node.previousSibling || node.parentElement;
- }
- return null;
- }
-
- getLineNumberByChild(node) {
- const lineEl = this.getLineElByChild(node);
- return lineEl ?
- parseInt(lineEl.getAttribute('data-value'), 10) :
- null;
- }
-
- getContentByLine(lineNumber, opt_side, opt_root) {
- return this._builder.getContentByLine(lineNumber, opt_side, opt_root);
- }
-
- getContentByLineEl(lineEl) {
- const root = Polymer.dom(lineEl.parentElement);
- const side = this.getSideByLineEl(lineEl);
- const line = lineEl.getAttribute('data-value');
- return this.getContentByLine(line, side, root);
- }
-
- getLineElByNumber(lineNumber, opt_side) {
- const sideSelector = opt_side ? ('.' + opt_side) : '';
- return this.diffElement.querySelector(
- '.lineNum[data-value="' + lineNumber + '"]' + sideSelector);
- }
-
- getContentsByLineRange(startLine, endLine, opt_side) {
- const result = [];
- this._builder.findLinesByRange(startLine, endLine, opt_side, null,
- result);
- return result;
- }
-
- getSideByLineEl(lineEl) {
- return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT) ?
- GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT;
- }
-
- emitGroup(group, sectionEl) {
- this._builder.emitGroup(group, sectionEl);
- }
-
- showContext(newGroups, sectionEl) {
- const groups = this._builder.groups;
-
- const contextIndex = groups.findIndex(group =>
- group.element === sectionEl
- );
- groups.splice(contextIndex, 1, ...newGroups);
-
- for (const newGroup of newGroups) {
- this._builder.emitGroup(newGroup, sectionEl);
- }
- sectionEl.parentNode.removeChild(sectionEl);
-
- this.async(() => this.fire('render-content'), 1);
- }
-
- cancel() {
- this.$.processor.cancel();
- if (this._cancelableRenderPromise) {
- this._cancelableRenderPromise.cancel();
- this._cancelableRenderPromise = null;
- }
- }
-
- _handlePreferenceError(pref) {
- const message = `The value of the '${pref}' user preference is ` +
- `invalid. Fix in diff preferences`;
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {
- message,
- }, bubbles: true, composed: true}));
- throw Error(`Invalid preference value: ${pref}`);
- }
-
- _getDiffBuilder(diff, prefs) {
- if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) {
- this._handlePreferenceError('tab size');
- return;
- }
-
- if (isNaN(prefs.line_length) || prefs.line_length <= 0) {
- this._handlePreferenceError('diff width');
- return;
- }
-
- const localPrefs = Object.assign({}, prefs);
- if (this.path === COMMIT_MSG_PATH) {
- // override line_length for commit msg the same way as
- // in gr-diff
- localPrefs.line_length = COMMIT_MSG_LINE_LENGTH;
- }
-
- let builder = null;
- if (this.isImageDiff) {
- builder = new GrDiffBuilderImage(
- diff,
- localPrefs,
- this.diffElement,
- this.baseImage,
- this.revisionImage);
- } else if (diff.binary) {
- // If the diff is binary, but not an image.
- return new GrDiffBuilderBinary(
- diff,
- localPrefs,
- this.diffElement);
- } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
- builder = new GrDiffBuilderSideBySide(
- diff,
- localPrefs,
- this.diffElement,
- this._layers
- );
- } else if (this.viewMode === DiffViewMode.UNIFIED) {
- builder = new GrDiffBuilderUnified(
- diff,
- localPrefs,
- this.diffElement,
- this._layers);
- }
- if (!builder) {
- throw Error('Unsupported diff view mode: ' + this.viewMode);
- }
- return builder;
- }
-
- _clearDiffContent() {
- this.diffElement.innerHTML = null;
- }
-
- _groupsChanged(changeRecord) {
- if (!changeRecord) { return; }
- for (const splice of changeRecord.indexSplices) {
- let group;
- for (let i = 0; i < splice.addedCount; i++) {
- group = splice.object[splice.index + i];
- this._builder.groups.push(group);
- this._builder.emitGroup(group);
+ if (node.classList.contains('section')) {
+ return null;
}
}
+ node = node.previousSibling || node.parentElement;
}
+ return null;
+ }
- _createIntralineLayer() {
- return {
- // Take a DIV.contentText element and a line object with intraline
- // differences to highlight and apply them to the element as
- // annotations.
- annotate(contentEl, lineNumberEl, line) {
- const HL_CLASS = 'style-scope gr-diff intraline';
- for (const highlight of line.highlights) {
- // The start and end indices could be the same if a highlight is
- // meant to start at the end of a line and continue onto the
- // next one. Ignore it.
- if (highlight.startIndex === highlight.endIndex) { continue; }
+ getLineNumberByChild(node) {
+ const lineEl = this.getLineElByChild(node);
+ return lineEl ?
+ parseInt(lineEl.getAttribute('data-value'), 10) :
+ null;
+ }
- // If endIndex isn't present, continue to the end of the line.
- const endIndex = highlight.endIndex === undefined ?
- line.text.length :
- highlight.endIndex;
+ getContentByLine(lineNumber, opt_side, opt_root) {
+ return this._builder.getContentByLine(lineNumber, opt_side, opt_root);
+ }
- GrAnnotation.annotateElement(
- contentEl,
- highlight.startIndex,
- endIndex - highlight.startIndex,
- HL_CLASS);
- }
- },
- };
+ getContentByLineEl(lineEl) {
+ const root = dom(lineEl.parentElement);
+ const side = this.getSideByLineEl(lineEl);
+ const line = lineEl.getAttribute('data-value');
+ return this.getContentByLine(line, side, root);
+ }
+
+ getLineElByNumber(lineNumber, opt_side) {
+ const sideSelector = opt_side ? ('.' + opt_side) : '';
+ return this.diffElement.querySelector(
+ '.lineNum[data-value="' + lineNumber + '"]' + sideSelector);
+ }
+
+ getContentsByLineRange(startLine, endLine, opt_side) {
+ const result = [];
+ this._builder.findLinesByRange(startLine, endLine, opt_side, null,
+ result);
+ return result;
+ }
+
+ getSideByLineEl(lineEl) {
+ return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT) ?
+ GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT;
+ }
+
+ emitGroup(group, sectionEl) {
+ this._builder.emitGroup(group, sectionEl);
+ }
+
+ showContext(newGroups, sectionEl) {
+ const groups = this._builder.groups;
+
+ const contextIndex = groups.findIndex(group =>
+ group.element === sectionEl
+ );
+ groups.splice(contextIndex, 1, ...newGroups);
+
+ for (const newGroup of newGroups) {
+ this._builder.emitGroup(newGroup, sectionEl);
}
+ sectionEl.parentNode.removeChild(sectionEl);
- _createTabIndicatorLayer() {
- const show = () => this._showTabs;
- return {
- annotate(contentEl, lineNumberEl, line) {
- // If visible tabs are disabled, do nothing.
- if (!show()) { return; }
+ this.async(() => this.fire('render-content'), 1);
+ }
- // Find and annotate the locations of tabs.
- const split = line.text.split('\t');
- if (!split) { return; }
- for (let i = 0, pos = 0; i < split.length - 1; i++) {
- // Skip forward by the length of the content
- pos += split[i].length;
-
- GrAnnotation.annotateElement(contentEl, pos, 1,
- 'style-scope gr-diff tab-indicator');
-
- // Skip forward by one tab character.
- pos++;
- }
- },
- };
- }
-
- _createTrailingWhitespaceLayer() {
- const show = function() {
- return this._showTrailingWhitespace;
- }.bind(this);
-
- return {
- annotate(contentEl, lineNumberEl, line) {
- if (!show()) { return; }
-
- const match = line.text.match(TRAILING_WHITESPACE_PATTERN);
- if (match) {
- // Normalize string positions in case there is unicode before or
- // within the match.
- const index = GrAnnotation.getStringLength(
- line.text.substr(0, match.index));
- const length = GrAnnotation.getStringLength(match[0]);
- GrAnnotation.annotateElement(contentEl, index, length,
- 'style-scope gr-diff trailing-whitespace');
- }
- },
- };
- }
-
- setBlame(blame) {
- if (!this._builder || !blame) { return; }
- this._builder.setBlame(blame);
+ cancel() {
+ this.$.processor.cancel();
+ if (this._cancelableRenderPromise) {
+ this._cancelableRenderPromise.cancel();
+ this._cancelableRenderPromise = null;
}
}
- customElements.define(GrDiffBuilderElement.is, GrDiffBuilderElement);
-})();
+ _handlePreferenceError(pref) {
+ const message = `The value of the '${pref}' user preference is ` +
+ `invalid. Fix in diff preferences`;
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {
+ message,
+ }, bubbles: true, composed: true}));
+ throw Error(`Invalid preference value: ${pref}`);
+ }
+
+ _getDiffBuilder(diff, prefs) {
+ if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) {
+ this._handlePreferenceError('tab size');
+ return;
+ }
+
+ if (isNaN(prefs.line_length) || prefs.line_length <= 0) {
+ this._handlePreferenceError('diff width');
+ return;
+ }
+
+ const localPrefs = Object.assign({}, prefs);
+ if (this.path === COMMIT_MSG_PATH) {
+ // override line_length for commit msg the same way as
+ // in gr-diff
+ localPrefs.line_length = COMMIT_MSG_LINE_LENGTH;
+ }
+
+ let builder = null;
+ if (this.isImageDiff) {
+ builder = new GrDiffBuilderImage(
+ diff,
+ localPrefs,
+ this.diffElement,
+ this.baseImage,
+ this.revisionImage);
+ } else if (diff.binary) {
+ // If the diff is binary, but not an image.
+ return new GrDiffBuilderBinary(
+ diff,
+ localPrefs,
+ this.diffElement);
+ } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
+ builder = new GrDiffBuilderSideBySide(
+ diff,
+ localPrefs,
+ this.diffElement,
+ this._layers
+ );
+ } else if (this.viewMode === DiffViewMode.UNIFIED) {
+ builder = new GrDiffBuilderUnified(
+ diff,
+ localPrefs,
+ this.diffElement,
+ this._layers);
+ }
+ if (!builder) {
+ throw Error('Unsupported diff view mode: ' + this.viewMode);
+ }
+ return builder;
+ }
+
+ _clearDiffContent() {
+ this.diffElement.innerHTML = null;
+ }
+
+ _groupsChanged(changeRecord) {
+ if (!changeRecord) { return; }
+ for (const splice of changeRecord.indexSplices) {
+ let group;
+ for (let i = 0; i < splice.addedCount; i++) {
+ group = splice.object[splice.index + i];
+ this._builder.groups.push(group);
+ this._builder.emitGroup(group);
+ }
+ }
+ }
+
+ _createIntralineLayer() {
+ return {
+ // Take a DIV.contentText element and a line object with intraline
+ // differences to highlight and apply them to the element as
+ // annotations.
+ annotate(contentEl, lineNumberEl, line) {
+ const HL_CLASS = 'style-scope gr-diff intraline';
+ for (const highlight of line.highlights) {
+ // The start and end indices could be the same if a highlight is
+ // meant to start at the end of a line and continue onto the
+ // next one. Ignore it.
+ if (highlight.startIndex === highlight.endIndex) { continue; }
+
+ // If endIndex isn't present, continue to the end of the line.
+ const endIndex = highlight.endIndex === undefined ?
+ line.text.length :
+ highlight.endIndex;
+
+ GrAnnotation.annotateElement(
+ contentEl,
+ highlight.startIndex,
+ endIndex - highlight.startIndex,
+ HL_CLASS);
+ }
+ },
+ };
+ }
+
+ _createTabIndicatorLayer() {
+ const show = () => this._showTabs;
+ return {
+ annotate(contentEl, lineNumberEl, line) {
+ // If visible tabs are disabled, do nothing.
+ if (!show()) { return; }
+
+ // Find and annotate the locations of tabs.
+ const split = line.text.split('\t');
+ if (!split) { return; }
+ for (let i = 0, pos = 0; i < split.length - 1; i++) {
+ // Skip forward by the length of the content
+ pos += split[i].length;
+
+ GrAnnotation.annotateElement(contentEl, pos, 1,
+ 'style-scope gr-diff tab-indicator');
+
+ // Skip forward by one tab character.
+ pos++;
+ }
+ },
+ };
+ }
+
+ _createTrailingWhitespaceLayer() {
+ const show = function() {
+ return this._showTrailingWhitespace;
+ }.bind(this);
+
+ return {
+ annotate(contentEl, lineNumberEl, line) {
+ if (!show()) { return; }
+
+ const match = line.text.match(TRAILING_WHITESPACE_PATTERN);
+ if (match) {
+ // Normalize string positions in case there is unicode before or
+ // within the match.
+ const index = GrAnnotation.getStringLength(
+ line.text.substr(0, match.index));
+ const length = GrAnnotation.getStringLength(match[0]);
+ GrAnnotation.annotateElement(contentEl, index, length,
+ 'style-scope gr-diff trailing-whitespace');
+ }
+ },
+ };
+ }
+
+ setBlame(blame) {
+ if (!this._builder || !blame) { return; }
+ this._builder.setBlame(blame);
+ }
+}
+
+customElements.define(GrDiffBuilderElement.is, GrDiffBuilderElement);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_html.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_html.js
new file mode 100644
index 0000000..c8df78f
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_html.js
@@ -0,0 +1,27 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <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/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
index 3af5522..d9f8443 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
@@ -19,23 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-builder</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
-<script src="../gr-diff/gr-diff-line.js"></script>
-<script src="../gr-diff/gr-diff-group.js"></script>
-<script src="../gr-diff-highlight/gr-annotation.js"></script>
-<script src="gr-diff-builder.js"></script>
-
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
-<link rel="import" href="gr-diff-builder-element.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template is="dom-template">
@@ -59,985 +46,1023 @@
</template>
</test-fixture>
-<script>
- const DiffViewMode = {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- };
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import '../gr-diff/gr-diff-line.js';
+import '../gr-diff/gr-diff-group.js';
+import '../gr-diff-highlight/gr-annotation.js';
+import './gr-diff-builder.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import './gr-diff-builder-element.js';
+import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+};
- suite('gr-diff-builder tests', async () => {
- await readyToTest();
- let prefs;
- let element;
- let builder;
- let sandbox;
- const LINE_FEED_HTML = '<span class="style-scope gr-diff br"></span>';
+suite('gr-diff-builder tests', () => {
+ let prefs;
+ let element;
+ let builder;
+ let sandbox;
+ const LINE_FEED_HTML = '<span class="style-scope gr-diff br"></span>';
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(false); },
- getProjectConfig() { return Promise.resolve({}); },
- });
- prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- };
- builder = new GrDiffBuilder({content: []}, prefs);
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(false); },
+ getProjectConfig() { return Promise.resolve({}); },
});
+ prefs = {
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ };
+ builder = new GrDiffBuilder({content: []}, prefs);
+ });
- teardown(() => { sandbox.restore(); });
+ teardown(() => { sandbox.restore(); });
- test('_createElement classStr applies all classes', () => {
- const node = builder._createElement('div', 'test classes');
- assert.isTrue(node.classList.contains('gr-diff'));
- assert.isTrue(node.classList.contains('test'));
- assert.isTrue(node.classList.contains('classes'));
- });
+ test('_createElement classStr applies all classes', () => {
+ const node = builder._createElement('div', 'test classes');
+ assert.isTrue(node.classList.contains('gr-diff'));
+ assert.isTrue(node.classList.contains('test'));
+ assert.isTrue(node.classList.contains('classes'));
+ });
- test('context control buttons', () => {
- // Create 10 lines.
- const lines = [];
- for (let i = 0; i < 10; i++) {
- const line = new GrDiffLine(GrDiffLine.Type.BOTH);
- line.beforeNumber = i + 1;
- line.afterNumber = i + 1;
- line.text = 'lorem upsum';
- lines.push(line);
- }
-
- const contextLine = {
- contextGroups: [new GrDiffGroup(GrDiffGroup.Type.BOTH, lines)],
- };
-
- const section = {};
- // Does not include +10 buttons when there are fewer than 11 lines.
- let td = builder._createContextControl(section, contextLine);
- let buttons = td.querySelectorAll('gr-button.showContext');
-
- assert.equal(buttons.length, 1);
- assert.equal(Polymer.dom(buttons[0]).textContent, 'Show 10 common lines');
-
- // Add another line.
+ test('context control buttons', () => {
+ // Create 10 lines.
+ const lines = [];
+ for (let i = 0; i < 10; i++) {
const line = new GrDiffLine(GrDiffLine.Type.BOTH);
+ line.beforeNumber = i + 1;
+ line.afterNumber = i + 1;
line.text = 'lorem upsum';
- line.beforeNumber = 11;
- line.afterNumber = 11;
- contextLine.contextGroups[0].addLine(line);
+ lines.push(line);
+ }
- // Includes +10 buttons when there are at least 11 lines.
- td = builder._createContextControl(section, contextLine);
- buttons = td.querySelectorAll('gr-button.showContext');
+ const contextLine = {
+ contextGroups: [new GrDiffGroup(GrDiffGroup.Type.BOTH, lines)],
+ };
- assert.equal(buttons.length, 3);
- assert.equal(Polymer.dom(buttons[0]).textContent, '+10 above');
- assert.equal(Polymer.dom(buttons[1]).textContent, 'Show 11 common lines');
- assert.equal(Polymer.dom(buttons[2]).textContent, '+10 below');
- });
+ const section = {};
+ // Does not include +10 buttons when there are fewer than 11 lines.
+ let td = builder._createContextControl(section, contextLine);
+ let buttons = td.querySelectorAll('gr-button.showContext');
- test('newlines 1', () => {
- let text = 'abcdef';
+ assert.equal(buttons.length, 1);
+ assert.equal(dom(buttons[0]).textContent, 'Show 10 common lines');
- assert.equal(builder._formatText(text, 4, 10).innerHTML, text);
- text = 'a'.repeat(20);
- assert.equal(builder._formatText(text, 4, 10).innerHTML,
- 'a'.repeat(10) +
- LINE_FEED_HTML +
- 'a'.repeat(10));
- });
+ // Add another line.
+ const line = new GrDiffLine(GrDiffLine.Type.BOTH);
+ line.text = 'lorem upsum';
+ line.beforeNumber = 11;
+ line.afterNumber = 11;
+ contextLine.contextGroups[0].addLine(line);
- test('newlines 2', () => {
- const text = '<span class="thumbsup">👍</span>';
- assert.equal(builder._formatText(text, 4, 10).innerHTML,
- '<span clas' +
- LINE_FEED_HTML +
- 's="thumbsu' +
- LINE_FEED_HTML +
- 'p">👍</span' +
- LINE_FEED_HTML +
- '>');
- });
+ // Includes +10 buttons when there are at least 11 lines.
+ td = builder._createContextControl(section, contextLine);
+ buttons = td.querySelectorAll('gr-button.showContext');
- test('newlines 3', () => {
- const text = '01234\t56789';
- assert.equal(builder._formatText(text, 4, 10).innerHTML,
- '01234' + builder._getTabWrapper(3).outerHTML + '56' +
- LINE_FEED_HTML +
- '789');
- });
+ assert.equal(buttons.length, 3);
+ assert.equal(dom(buttons[0]).textContent, '+10 above');
+ assert.equal(dom(buttons[1]).textContent, 'Show 11 common lines');
+ assert.equal(dom(buttons[2]).textContent, '+10 below');
+ });
- test('newlines 4', () => {
- const text = '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍';
- assert.equal(builder._formatText(text, 4, 20).innerHTML,
- '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' +
- LINE_FEED_HTML +
- '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' +
- LINE_FEED_HTML +
- '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍');
- });
+ test('newlines 1', () => {
+ let text = 'abcdef';
- test('line_length ignored if line_wrapping is true', () => {
- builder._prefs = {line_wrapping: true, tab_size: 4, line_length: 50};
- const text = 'a'.repeat(51);
+ assert.equal(builder._formatText(text, 4, 10).innerHTML, text);
+ text = 'a'.repeat(20);
+ assert.equal(builder._formatText(text, 4, 10).innerHTML,
+ 'a'.repeat(10) +
+ LINE_FEED_HTML +
+ 'a'.repeat(10));
+ });
- const line = {text, highlights: []};
- const result = builder._createTextEl(undefined, line).firstChild.innerHTML;
- assert.equal(result, text);
- });
+ test('newlines 2', () => {
+ const text = '<span class="thumbsup">👍</span>';
+ assert.equal(builder._formatText(text, 4, 10).innerHTML,
+ '<span clas' +
+ LINE_FEED_HTML +
+ 's="thumbsu' +
+ LINE_FEED_HTML +
+ 'p">👍</span' +
+ LINE_FEED_HTML +
+ '>');
+ });
- test('line_length applied if line_wrapping is false', () => {
- builder._prefs = {line_wrapping: false, tab_size: 4, line_length: 50};
- const text = 'a'.repeat(51);
+ test('newlines 3', () => {
+ const text = '01234\t56789';
+ assert.equal(builder._formatText(text, 4, 10).innerHTML,
+ '01234' + builder._getTabWrapper(3).outerHTML + '56' +
+ LINE_FEED_HTML +
+ '789');
+ });
- const line = {text, highlights: []};
- const expected = 'a'.repeat(50) + LINE_FEED_HTML + 'a';
- const result = builder._createTextEl(undefined, line).firstChild.innerHTML;
- assert.equal(result, expected);
- });
+ test('newlines 4', () => {
+ const text = '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍';
+ assert.equal(builder._formatText(text, 4, 20).innerHTML,
+ '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' +
+ LINE_FEED_HTML +
+ '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' +
+ LINE_FEED_HTML +
+ '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍');
+ });
- [DiffViewMode.UNIFIED, DiffViewMode.SIDE_BY_SIDE]
- .forEach(mode => {
- test(`line_length used for regular files under ${mode}`, () => {
- element.path = '/a.txt';
- element.viewMode = mode;
- builder = element._getDiffBuilder(
- {}, {tab_size: 4, line_length: 50}
- );
- assert.equal(builder._prefs.line_length, 50);
- });
+ test('line_length ignored if line_wrapping is true', () => {
+ builder._prefs = {line_wrapping: true, tab_size: 4, line_length: 50};
+ const text = 'a'.repeat(51);
- test(`line_length ignored for commit msg under ${mode}`, () => {
- element.path = '/COMMIT_MSG';
- element.viewMode = mode;
- builder = element._getDiffBuilder(
- {}, {tab_size: 4, line_length: 50}
- );
- assert.equal(builder._prefs.line_length, 72);
- });
+ const line = {text, highlights: []};
+ const result = builder._createTextEl(undefined, line).firstChild.innerHTML;
+ assert.equal(result, text);
+ });
+
+ test('line_length applied if line_wrapping is false', () => {
+ builder._prefs = {line_wrapping: false, tab_size: 4, line_length: 50};
+ const text = 'a'.repeat(51);
+
+ const line = {text, highlights: []};
+ const expected = 'a'.repeat(50) + LINE_FEED_HTML + 'a';
+ const result = builder._createTextEl(undefined, line).firstChild.innerHTML;
+ assert.equal(result, expected);
+ });
+
+ [DiffViewMode.UNIFIED, DiffViewMode.SIDE_BY_SIDE]
+ .forEach(mode => {
+ test(`line_length used for regular files under ${mode}`, () => {
+ element.path = '/a.txt';
+ element.viewMode = mode;
+ builder = element._getDiffBuilder(
+ {}, {tab_size: 4, line_length: 50}
+ );
+ assert.equal(builder._prefs.line_length, 50);
});
- test('_createTextEl linewrap with tabs', () => {
- const text = '\t'.repeat(7) + '!';
- const line = {text, highlights: []};
- const el = builder._createTextEl(undefined, line);
- assert.equal(el.innerText, text);
- // With line length 10 and tab size 2, there should be a line break
- // after every two tabs.
- const newlineEl = el.querySelector('.contentText > .br');
- assert.isOk(newlineEl);
- assert.equal(
- el.querySelector('.contentText .tab:nth-child(2)').nextSibling,
- newlineEl);
- });
+ test(`line_length ignored for commit msg under ${mode}`, () => {
+ element.path = '/COMMIT_MSG';
+ element.viewMode = mode;
+ builder = element._getDiffBuilder(
+ {}, {tab_size: 4, line_length: 50}
+ );
+ assert.equal(builder._prefs.line_length, 72);
+ });
+ });
- test('text length with tabs and unicode', () => {
- function expectTextLength(text, tabSize, expected) {
- // Formatting to |expected| columns should not introduce line breaks.
- const result = builder._formatText(text, tabSize, expected);
- assert.isNotOk(result.querySelector('.contentText > .br'),
+ test('_createTextEl linewrap with tabs', () => {
+ const text = '\t'.repeat(7) + '!';
+ const line = {text, highlights: []};
+ const el = builder._createTextEl(undefined, line);
+ assert.equal(el.innerText, text);
+ // With line length 10 and tab size 2, there should be a line break
+ // after every two tabs.
+ const newlineEl = el.querySelector('.contentText > .br');
+ assert.isOk(newlineEl);
+ assert.equal(
+ el.querySelector('.contentText .tab:nth-child(2)').nextSibling,
+ newlineEl);
+ });
+
+ test('text length with tabs and unicode', () => {
+ function expectTextLength(text, tabSize, expected) {
+ // Formatting to |expected| columns should not introduce line breaks.
+ const result = builder._formatText(text, tabSize, expected);
+ assert.isNotOk(result.querySelector('.contentText > .br'),
+ ` Expected the result of: \n` +
+ ` _formatText(${text}', ${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(builder._formatText(text, tabSize, Infinity).innerHTML,
+ result.innerHTML);
+ assert.equal(builder._formatText(text, tabSize, expected + 1).innerHTML,
+ result.innerHTML);
+
+ // Decreasing the line limit should introduce line breaks.
+ if (expected > 0) {
+ const tooSmall = builder._formatText(text, tabSize, expected - 1);
+ assert.isOk(tooSmall.querySelector('.contentText > .br'),
` Expected the result of: \n` +
- ` _formatText(${text}', ${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(builder._formatText(text, tabSize, Infinity).innerHTML,
- result.innerHTML);
- assert.equal(builder._formatText(text, tabSize, expected + 1).innerHTML,
- result.innerHTML);
-
- // Decreasing the line limit should introduce line breaks.
- if (expected > 0) {
- const tooSmall = builder._formatText(text, 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`);
- }
+ ` _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);
- 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);
- });
+ }
+ 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);
+ 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 = builder._prefs.tab_size;
- const wrapper = builder._getTabWrapper(tabSize - 3);
- assert.ok(wrapper);
- assert.equal(wrapper.innerText, '\t');
- assert.equal(
- builder._formatText(html, tabSize, Infinity).innerHTML,
- 'abc' + wrapper.outerHTML + 'def');
- });
+ test('tab wrapper insertion', () => {
+ const html = 'abc\tdef';
+ const tabSize = builder._prefs.tab_size;
+ const wrapper = builder._getTabWrapper(tabSize - 3);
+ assert.ok(wrapper);
+ assert.equal(wrapper.innerText, '\t');
+ assert.equal(
+ builder._formatText(html, tabSize, Infinity).innerHTML,
+ 'abc' + wrapper.outerHTML + 'def');
+ });
- test('tab wrapper style', () => {
- const pattern = new RegExp('^<span class="style-scope gr-diff tab" ' +
- 'style="(?:-moz-)?tab-size: (\\d+);">\\t<\\/span>$');
+ 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 = builder._getTabWrapper(size).outerHTML;
- expect(html).to.match(pattern);
- assert.equal(html.match(pattern)[1], size);
+ for (const size of [1, 3, 8, 55]) {
+ const html = builder._getTabWrapper(size).outerHTML;
+ expect(html).to.match(pattern);
+ assert.equal(html.match(pattern)[1], size);
+ }
+ });
+
+ test('_handlePreferenceError called with invalid preference', () => {
+ sandbox.stub(element, '_handlePreferenceError');
+ const prefs = {tab_size: 0};
+ element._getDiffBuilder(element.diff, prefs);
+ assert.isTrue(element._handlePreferenceError.lastCall
+ .calledWithExactly('tab size'));
+ });
+
+ test('_handlePreferenceError triggers alert and javascript error', () => {
+ const errorStub = sinon.stub();
+ element.addEventListener('show-alert', errorStub);
+ assert.throws(element._handlePreferenceError.bind(element, 'tab size'));
+ assert.equal(errorStub.lastCall.args[0].detail.message,
+ `The value of the 'tab size' user preference is invalid. ` +
+ `Fix in diff preferences`);
+ });
+
+ suite('_isTotal', () => {
+ test('is total for add', () => {
+ const group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
+ for (let idx = 0; idx < 10; idx++) {
+ group.addLine(new GrDiffLine(GrDiffLine.Type.ADD));
}
+ assert.isTrue(GrDiffBuilder.prototype._isTotal(group));
});
- test('_handlePreferenceError called with invalid preference', () => {
- sandbox.stub(element, '_handlePreferenceError');
- const prefs = {tab_size: 0};
- element._getDiffBuilder(element.diff, prefs);
- assert.isTrue(element._handlePreferenceError.lastCall
- .calledWithExactly('tab size'));
- });
-
- test('_handlePreferenceError triggers alert and javascript error', () => {
- const errorStub = sinon.stub();
- element.addEventListener('show-alert', errorStub);
- assert.throws(element._handlePreferenceError.bind(element, 'tab size'));
- assert.equal(errorStub.lastCall.args[0].detail.message,
- `The value of the 'tab size' user preference is invalid. ` +
- `Fix in diff preferences`);
- });
-
- suite('_isTotal', () => {
- test('is total for add', () => {
- const group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
- for (let idx = 0; idx < 10; idx++) {
- group.addLine(new GrDiffLine(GrDiffLine.Type.ADD));
- }
- assert.isTrue(GrDiffBuilder.prototype._isTotal(group));
- });
-
- test('is total for remove', () => {
- const group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
- for (let idx = 0; idx < 10; idx++) {
- group.addLine(new GrDiffLine(GrDiffLine.Type.REMOVE));
- }
- assert.isTrue(GrDiffBuilder.prototype._isTotal(group));
- });
-
- test('not total for empty', () => {
- const group = new GrDiffGroup(GrDiffGroup.Type.BOTH);
- assert.isFalse(GrDiffBuilder.prototype._isTotal(group));
- });
-
- test('not total for non-delta', () => {
- const group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
- for (let idx = 0; idx < 10; idx++) {
- group.addLine(new GrDiffLine(GrDiffLine.Type.BOTH));
- }
- assert.isFalse(GrDiffBuilder.prototype._isTotal(group));
- });
- });
-
- suite('intraline differences', () => {
- let el;
- let str;
- let annotateElementSpy;
- let layer;
- const lineNumberEl = document.createElement('td');
-
- function slice(str, start, end) {
- return Array.from(str).slice(start, end)
- .join('');
+ test('is total for remove', () => {
+ const group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
+ for (let idx = 0; idx < 10; idx++) {
+ group.addLine(new GrDiffLine(GrDiffLine.Type.REMOVE));
}
-
- setup(() => {
- el = fixture('div-with-text');
- str = el.textContent;
- annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement');
- layer = document.createElement('gr-diff-builder')
- ._createIntralineLayer();
- });
-
- test('annotate no highlights', () => {
- const line = {
- text: str,
- highlights: [],
- };
-
- layer.annotate(el, lineNumberEl, line);
-
- // The content is unchanged.
- assert.isFalse(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 1);
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(str, el.childNodes[0].textContent);
- });
-
- test('annotate with highlights', () => {
- const line = {
- text: str,
- highlights: [
- {startIndex: 6, endIndex: 12},
- {startIndex: 18, endIndex: 22},
- ],
- };
- const str0 = slice(str, 0, 6);
- const str1 = slice(str, 6, 12);
- const str2 = slice(str, 12, 18);
- const str3 = slice(str, 18, 22);
- const str4 = slice(str, 22);
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 5);
-
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(el.childNodes[0].textContent, str0);
-
- assert.notInstanceOf(el.childNodes[1], Text);
- assert.equal(el.childNodes[1].textContent, str1);
-
- assert.instanceOf(el.childNodes[2], Text);
- assert.equal(el.childNodes[2].textContent, str2);
-
- assert.notInstanceOf(el.childNodes[3], Text);
- assert.equal(el.childNodes[3].textContent, str3);
-
- assert.instanceOf(el.childNodes[4], Text);
- assert.equal(el.childNodes[4].textContent, str4);
- });
-
- test('annotate without endIndex', () => {
- const line = {
- text: str,
- highlights: [
- {startIndex: 28},
- ],
- };
-
- const str0 = slice(str, 0, 28);
- const str1 = slice(str, 28);
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 2);
-
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(el.childNodes[0].textContent, str0);
-
- assert.notInstanceOf(el.childNodes[1], Text);
- assert.equal(el.childNodes[1].textContent, str1);
- });
-
- test('annotate ignores empty highlights', () => {
- const line = {
- text: str,
- highlights: [
- {startIndex: 28, endIndex: 28},
- ],
- };
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 1);
- });
-
- test('annotate handles unicode', () => {
- // Put some unicode into the string:
- str = str.replace(/\s/g, '💢');
- el.textContent = str;
- const line = {
- text: str,
- highlights: [
- {startIndex: 6, endIndex: 12},
- ],
- };
-
- const str0 = slice(str, 0, 6);
- const str1 = slice(str, 6, 12);
- const str2 = slice(str, 12);
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 3);
-
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(el.childNodes[0].textContent, str0);
-
- assert.notInstanceOf(el.childNodes[1], Text);
- assert.equal(el.childNodes[1].textContent, str1);
-
- assert.instanceOf(el.childNodes[2], Text);
- assert.equal(el.childNodes[2].textContent, str2);
- });
-
- test('annotate handles unicode w/o endIndex', () => {
- // Put some unicode into the string:
- str = str.replace(/\s/g, '💢');
- el.textContent = str;
-
- const line = {
- text: str,
- highlights: [
- {startIndex: 6},
- ],
- };
-
- const str0 = slice(str, 0, 6);
- const str1 = slice(str, 6);
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 2);
-
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(el.childNodes[0].textContent, str0);
-
- assert.notInstanceOf(el.childNodes[1], Text);
- assert.equal(el.childNodes[1].textContent, str1);
- });
+ assert.isTrue(GrDiffBuilder.prototype._isTotal(group));
});
- suite('tab indicators', () => {
- let element;
- let layer;
- const lineNumberEl = document.createElement('td');
-
- setup(() => {
- element = fixture('basic');
- element._showTabs = true;
- layer = element._createTabIndicatorLayer();
- });
-
- test('does nothing with empty line', () => {
- const line = {text: ''};
- const el = document.createElement('div');
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementStub.called);
- });
-
- test('does nothing with no tabs', () => {
- const str = 'lorem ipsum no tabs';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementStub.called);
- });
-
- test('annotates tab at beginning', () => {
- const str = '\tlorem upsum';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.equal(annotateElementStub.callCount, 1);
- const args = annotateElementStub.getCalls()[0].args;
- assert.equal(args[0], el);
- assert.equal(args[1], 0, 'offset of tab indicator');
- assert.equal(args[2], 1, 'length of tab indicator');
- assert.include(args[3], 'tab-indicator');
- });
-
- test('does not annotate when disabled', () => {
- element._showTabs = false;
-
- const str = '\tlorem upsum';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementStub.called);
- });
-
- test('annotates multiple in beginning', () => {
- const str = '\t\tlorem upsum';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.equal(annotateElementStub.callCount, 2);
-
- let args = annotateElementStub.getCalls()[0].args;
- assert.equal(args[0], el);
- assert.equal(args[1], 0, 'offset of tab indicator');
- assert.equal(args[2], 1, 'length of tab indicator');
- assert.include(args[3], 'tab-indicator');
-
- args = annotateElementStub.getCalls()[1].args;
- assert.equal(args[0], el);
- assert.equal(args[1], 1, 'offset of tab indicator');
- assert.equal(args[2], 1, 'length of tab indicator');
- assert.include(args[3], 'tab-indicator');
- });
-
- test('annotates intermediate tabs', () => {
- const str = 'lorem\tupsum';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.equal(annotateElementStub.callCount, 1);
- const args = annotateElementStub.getCalls()[0].args;
- assert.equal(args[0], el);
- assert.equal(args[1], 5, 'offset of tab indicator');
- assert.equal(args[2], 1, 'length of tab indicator');
- assert.include(args[3], 'tab-indicator');
- });
+ test('not total for empty', () => {
+ const group = new GrDiffGroup(GrDiffGroup.Type.BOTH);
+ assert.isFalse(GrDiffBuilder.prototype._isTotal(group));
});
- suite('layers', () => {
- let element;
- let initialLayersCount;
- let withLayerCount;
+ test('not total for non-delta', () => {
+ const group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
+ for (let idx = 0; idx < 10; idx++) {
+ group.addLine(new GrDiffLine(GrDiffLine.Type.BOTH));
+ }
+ assert.isFalse(GrDiffBuilder.prototype._isTotal(group));
+ });
+ });
+
+ suite('intraline differences', () => {
+ let el;
+ let str;
+ let annotateElementSpy;
+ let layer;
+ const lineNumberEl = document.createElement('td');
+
+ function slice(str, start, end) {
+ return Array.from(str).slice(start, end)
+ .join('');
+ }
+
+ setup(() => {
+ el = fixture('div-with-text');
+ str = el.textContent;
+ annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+ layer = document.createElement('gr-diff-builder')
+ ._createIntralineLayer();
+ });
+
+ test('annotate no highlights', () => {
+ const line = {
+ text: str,
+ highlights: [],
+ };
+
+ layer.annotate(el, lineNumberEl, line);
+
+ // The content is unchanged.
+ assert.isFalse(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 1);
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(str, el.childNodes[0].textContent);
+ });
+
+ test('annotate with highlights', () => {
+ const line = {
+ text: str,
+ highlights: [
+ {startIndex: 6, endIndex: 12},
+ {startIndex: 18, endIndex: 22},
+ ],
+ };
+ const str0 = slice(str, 0, 6);
+ const str1 = slice(str, 6, 12);
+ const str2 = slice(str, 12, 18);
+ const str3 = slice(str, 18, 22);
+ const str4 = slice(str, 22);
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.isTrue(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 5);
+
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(el.childNodes[0].textContent, str0);
+
+ assert.notInstanceOf(el.childNodes[1], Text);
+ assert.equal(el.childNodes[1].textContent, str1);
+
+ assert.instanceOf(el.childNodes[2], Text);
+ assert.equal(el.childNodes[2].textContent, str2);
+
+ assert.notInstanceOf(el.childNodes[3], Text);
+ assert.equal(el.childNodes[3].textContent, str3);
+
+ assert.instanceOf(el.childNodes[4], Text);
+ assert.equal(el.childNodes[4].textContent, str4);
+ });
+
+ test('annotate without endIndex', () => {
+ const line = {
+ text: str,
+ highlights: [
+ {startIndex: 28},
+ ],
+ };
+
+ const str0 = slice(str, 0, 28);
+ const str1 = slice(str, 28);
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.isTrue(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 2);
+
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(el.childNodes[0].textContent, str0);
+
+ assert.notInstanceOf(el.childNodes[1], Text);
+ assert.equal(el.childNodes[1].textContent, str1);
+ });
+
+ test('annotate ignores empty highlights', () => {
+ const line = {
+ text: str,
+ highlights: [
+ {startIndex: 28, endIndex: 28},
+ ],
+ };
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.isFalse(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 1);
+ });
+
+ test('annotate handles unicode', () => {
+ // Put some unicode into the string:
+ str = str.replace(/\s/g, '💢');
+ el.textContent = str;
+ const line = {
+ text: str,
+ highlights: [
+ {startIndex: 6, endIndex: 12},
+ ],
+ };
+
+ const str0 = slice(str, 0, 6);
+ const str1 = slice(str, 6, 12);
+ const str2 = slice(str, 12);
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.isTrue(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 3);
+
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(el.childNodes[0].textContent, str0);
+
+ assert.notInstanceOf(el.childNodes[1], Text);
+ assert.equal(el.childNodes[1].textContent, str1);
+
+ assert.instanceOf(el.childNodes[2], Text);
+ assert.equal(el.childNodes[2].textContent, str2);
+ });
+
+ test('annotate handles unicode w/o endIndex', () => {
+ // Put some unicode into the string:
+ str = str.replace(/\s/g, '💢');
+ el.textContent = str;
+
+ const line = {
+ text: str,
+ highlights: [
+ {startIndex: 6},
+ ],
+ };
+
+ const str0 = slice(str, 0, 6);
+ const str1 = slice(str, 6);
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.isTrue(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 2);
+
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(el.childNodes[0].textContent, str0);
+
+ assert.notInstanceOf(el.childNodes[1], Text);
+ assert.equal(el.childNodes[1].textContent, str1);
+ });
+ });
+
+ suite('tab indicators', () => {
+ let element;
+ let layer;
+ const lineNumberEl = document.createElement('td');
+
+ setup(() => {
+ element = fixture('basic');
+ element._showTabs = true;
+ layer = element._createTabIndicatorLayer();
+ });
+
+ test('does nothing with empty line', () => {
+ const line = {text: ''};
+ const el = document.createElement('div');
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('does nothing with no tabs', () => {
+ const str = 'lorem ipsum no tabs';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('annotates tab at beginning', () => {
+ const str = '\tlorem upsum';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.equal(annotateElementStub.callCount, 1);
+ const args = annotateElementStub.getCalls()[0].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 0, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+ });
+
+ test('does not annotate when disabled', () => {
+ element._showTabs = false;
+
+ const str = '\tlorem upsum';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('annotates multiple in beginning', () => {
+ const str = '\t\tlorem upsum';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.equal(annotateElementStub.callCount, 2);
+
+ let args = annotateElementStub.getCalls()[0].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 0, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+
+ args = annotateElementStub.getCalls()[1].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 1, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+ });
+
+ test('annotates intermediate tabs', () => {
+ const str = 'lorem\tupsum';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.equal(annotateElementStub.callCount, 1);
+ const args = annotateElementStub.getCalls()[0].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 5, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+ });
+ });
+
+ suite('layers', () => {
+ let element;
+ let initialLayersCount;
+ let withLayerCount;
+ setup(() => {
+ const layers = [];
+ element = fixture('basic');
+ element.layers = layers;
+ element._showTrailingWhitespace = true;
+ element._setupAnnotationLayers();
+ initialLayersCount = element._layers.length;
+ });
+
+ test('no layers', () => {
+ element._setupAnnotationLayers();
+ assert.equal(element._layers.length, initialLayersCount);
+ });
+
+ suite('with layers', () => {
+ const layers = [{}, {}];
setup(() => {
- const layers = [];
element = fixture('basic');
element.layers = layers;
element._showTrailingWhitespace = true;
element._setupAnnotationLayers();
- initialLayersCount = element._layers.length;
+ withLayerCount = element._layers.length;
});
-
- test('no layers', () => {
+ test('with layers', () => {
element._setupAnnotationLayers();
- assert.equal(element._layers.length, initialLayersCount);
+ assert.equal(element._layers.length, withLayerCount);
+ assert.equal(initialLayersCount + layers.length,
+ withLayerCount);
});
+ });
+ });
- suite('with layers', () => {
- const layers = [{}, {}];
- setup(() => {
- element = fixture('basic');
- element.layers = layers;
- element._showTrailingWhitespace = true;
- element._setupAnnotationLayers();
- withLayerCount = element._layers.length;
- });
- test('with layers', () => {
- element._setupAnnotationLayers();
- assert.equal(element._layers.length, withLayerCount);
- assert.equal(initialLayersCount + layers.length,
- withLayerCount);
- });
+ suite('trailing whitespace', () => {
+ let element;
+ let layer;
+ const lineNumberEl = document.createElement('td');
+
+ setup(() => {
+ element = fixture('basic');
+ element._showTrailingWhitespace = true;
+ layer = element._createTrailingWhitespaceLayer();
+ });
+
+ test('does nothing with empty line', () => {
+ const line = {text: ''};
+ const el = document.createElement('div');
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, line);
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('does nothing with no trailing whitespace', () => {
+ const str = 'lorem ipsum blah blah';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, line);
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('annotates trailing spaces', () => {
+ const str = 'lorem ipsum ';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, line);
+ assert.isTrue(annotateElementStub.called);
+ assert.equal(annotateElementStub.lastCall.args[1], 11);
+ assert.equal(annotateElementStub.lastCall.args[2], 3);
+ });
+
+ test('annotates trailing tabs', () => {
+ const str = 'lorem ipsum\t\t\t';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, line);
+ assert.isTrue(annotateElementStub.called);
+ assert.equal(annotateElementStub.lastCall.args[1], 11);
+ assert.equal(annotateElementStub.lastCall.args[2], 3);
+ });
+
+ test('annotates mixed trailing whitespace', () => {
+ const str = 'lorem ipsum\t \t';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, line);
+ assert.isTrue(annotateElementStub.called);
+ assert.equal(annotateElementStub.lastCall.args[1], 11);
+ assert.equal(annotateElementStub.lastCall.args[2], 3);
+ });
+
+ test('unicode preceding trailing whitespace', () => {
+ const str = '💢\t';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, line);
+ assert.isTrue(annotateElementStub.called);
+ assert.equal(annotateElementStub.lastCall.args[1], 1);
+ assert.equal(annotateElementStub.lastCall.args[2], 1);
+ });
+
+ test('does not annotate when disabled', () => {
+ element._showTrailingWhitespace = false;
+ const str = 'lorem upsum\t \t ';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, line);
+ assert.isFalse(annotateElementStub.called);
+ });
+ });
+
+ suite('rendering text, images and binary files', () => {
+ let processStub;
+ let keyLocations;
+ let prefs;
+ let content;
+
+ setup(() => {
+ element = fixture('basic');
+ element.viewMode = 'SIDE_BY_SIDE';
+ processStub = sandbox.stub(element.$.processor, 'process')
+ .returns(Promise.resolve());
+ keyLocations = {left: {}, right: {}};
+ prefs = {
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ context: -1,
+ syntax_highlighting: true,
+ };
+ content = [{
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ }, {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ }];
+ });
+
+ test('text', () => {
+ element.diff = {content};
+ return element.render(keyLocations, prefs).then(() => {
+ assert.isTrue(processStub.calledOnce);
+ assert.isFalse(processStub.lastCall.args[1]);
});
});
- suite('trailing whitespace', () => {
- let element;
- let layer;
- const lineNumberEl = document.createElement('td');
-
- setup(() => {
- element = fixture('basic');
- element._showTrailingWhitespace = true;
- layer = element._createTrailingWhitespaceLayer();
- });
-
- test('does nothing with empty line', () => {
- const line = {text: ''};
- const el = document.createElement('div');
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isFalse(annotateElementStub.called);
- });
-
- test('does nothing with no trailing whitespace', () => {
- const str = 'lorem ipsum blah blah';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isFalse(annotateElementStub.called);
- });
-
- test('annotates trailing spaces', () => {
- const str = 'lorem ipsum ';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isTrue(annotateElementStub.called);
- assert.equal(annotateElementStub.lastCall.args[1], 11);
- assert.equal(annotateElementStub.lastCall.args[2], 3);
- });
-
- test('annotates trailing tabs', () => {
- const str = 'lorem ipsum\t\t\t';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isTrue(annotateElementStub.called);
- assert.equal(annotateElementStub.lastCall.args[1], 11);
- assert.equal(annotateElementStub.lastCall.args[2], 3);
- });
-
- test('annotates mixed trailing whitespace', () => {
- const str = 'lorem ipsum\t \t';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isTrue(annotateElementStub.called);
- assert.equal(annotateElementStub.lastCall.args[1], 11);
- assert.equal(annotateElementStub.lastCall.args[2], 3);
- });
-
- test('unicode preceding trailing whitespace', () => {
- const str = '💢\t';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isTrue(annotateElementStub.called);
- assert.equal(annotateElementStub.lastCall.args[1], 1);
- assert.equal(annotateElementStub.lastCall.args[2], 1);
- });
-
- test('does not annotate when disabled', () => {
- element._showTrailingWhitespace = false;
- const str = 'lorem upsum\t \t ';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isFalse(annotateElementStub.called);
+ test('image', () => {
+ element.diff = {content, binary: true};
+ element.isImageDiff = true;
+ return element.render(keyLocations, prefs).then(() => {
+ assert.isTrue(processStub.calledOnce);
+ assert.isTrue(processStub.lastCall.args[1]);
});
});
- suite('rendering text, images and binary files', () => {
- let processStub;
- let keyLocations;
- let prefs;
- let content;
+ test('binary', () => {
+ element.diff = {content, binary: true};
+ return element.render(keyLocations, prefs).then(() => {
+ assert.isTrue(processStub.calledOnce);
+ assert.isTrue(processStub.lastCall.args[1]);
+ });
+ });
+ });
- setup(() => {
- element = fixture('basic');
- element.viewMode = 'SIDE_BY_SIDE';
- processStub = sandbox.stub(element.$.processor, 'process')
- .returns(Promise.resolve());
- keyLocations = {left: {}, right: {}};
- prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- context: -1,
- syntax_highlighting: true,
- };
- content = [{
+ suite('rendering', () => {
+ let content;
+ let outputEl;
+ let keyLocations;
+
+ setup(done => {
+ const prefs = {
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ context: -1,
+ syntax_highlighting: true,
+ };
+ content = [
+ {
a: ['all work and no play make andybons a dull boy'],
b: ['elgoog elgoog elgoog'],
- }, {
+ },
+ {
ab: [
'Non eram nescius, Brute, cum, quae summis ingeniis ',
'exquisitaque doctrina philosophi Graeco sermone tractavissent',
],
- }];
+ },
+ ];
+ element = fixture('basic');
+ outputEl = element.queryEffectiveChildren('#diffTable');
+ keyLocations = {left: {}, right: {}};
+ sandbox.stub(element, '_getDiffBuilder', () => {
+ const builder = new GrDiffBuilder({content}, prefs, outputEl);
+ sandbox.stub(builder, 'addColumns');
+ builder.buildSectionElement = function(group) {
+ const section = document.createElement('stub');
+ section.textContent = group.lines
+ .reduce((acc, line) => acc + line.text, '');
+ return section;
+ };
+ return builder;
});
+ element.diff = {content};
+ element.render(keyLocations, prefs).then(done);
+ });
- test('text', () => {
- element.diff = {content};
- return element.render(keyLocations, prefs).then(() => {
- assert.isTrue(processStub.calledOnce);
- assert.isFalse(processStub.lastCall.args[1]);
- });
- });
+ test('addColumns is called', done => {
+ element.render(keyLocations, {}).then(done);
+ assert.isTrue(element._builder.addColumns.called);
+ });
- test('image', () => {
- element.diff = {content, binary: true};
- element.isImageDiff = true;
- return element.render(keyLocations, prefs).then(() => {
- assert.isTrue(processStub.calledOnce);
- assert.isTrue(processStub.lastCall.args[1]);
- });
- });
+ test('getSectionsByLineRange one line', () => {
+ const section = outputEl.querySelector('stub:nth-of-type(2)');
+ const sections = element._builder.getSectionsByLineRange(1, 1, 'left');
+ assert.equal(sections.length, 1);
+ assert.strictEqual(sections[0], section);
+ });
- test('binary', () => {
- element.diff = {content, binary: true};
- return element.render(keyLocations, prefs).then(() => {
- assert.isTrue(processStub.calledOnce);
- assert.isTrue(processStub.lastCall.args[1]);
- });
+ test('getSectionsByLineRange over diff', () => {
+ const section = [
+ outputEl.querySelector('stub:nth-of-type(2)'),
+ outputEl.querySelector('stub:nth-of-type(3)'),
+ ];
+ const sections = element._builder.getSectionsByLineRange(1, 2, 'left');
+ assert.equal(sections.length, 2);
+ assert.strictEqual(sections[0], section[0]);
+ assert.strictEqual(sections[1], section[1]);
+ });
+
+ test('render-start and render-content are fired', done => {
+ const dispatchEventStub = sandbox.stub(element, 'dispatchEvent');
+ element.render(keyLocations, {}).then(() => {
+ const firedEventTypes = dispatchEventStub.getCalls()
+ .map(c => c.args[0].type);
+ assert.include(firedEventTypes, 'render-start');
+ assert.include(firedEventTypes, 'render-content');
+ done();
});
});
- suite('rendering', () => {
- let content;
- let outputEl;
- let keyLocations;
+ test('cancel', () => {
+ const processorCancelStub = sandbox.stub(element.$.processor, 'cancel');
+ element.cancel();
+ assert.isTrue(processorCancelStub.called);
+ });
+ });
- setup(done => {
- const prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- context: -1,
- syntax_highlighting: true,
- };
- content = [
- {
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- },
- {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- },
- ];
- element = fixture('basic');
- outputEl = element.queryEffectiveChildren('#diffTable');
- keyLocations = {left: {}, right: {}};
- sandbox.stub(element, '_getDiffBuilder', () => {
- const builder = new GrDiffBuilder({content}, prefs, outputEl);
- sandbox.stub(builder, 'addColumns');
- builder.buildSectionElement = function(group) {
- const section = document.createElement('stub');
- section.textContent = group.lines
- .reduce((acc, line) => acc + line.text, '');
- return section;
- };
- return builder;
- });
- element.diff = {content};
- element.render(keyLocations, prefs).then(done);
- });
+ suite('mock-diff', () => {
+ let element;
+ let builder;
+ let diff;
+ let prefs;
+ let keyLocations;
- test('addColumns is called', done => {
- element.render(keyLocations, {}).then(done);
- assert.isTrue(element._builder.addColumns.called);
- });
+ setup(done => {
+ element = fixture('mock-diff');
+ diff = document.createElement('mock-diff-response').diffResponse;
+ element.diff = diff;
- test('getSectionsByLineRange one line', () => {
- const section = outputEl.querySelector('stub:nth-of-type(2)');
- const sections = element._builder.getSectionsByLineRange(1, 1, 'left');
- assert.equal(sections.length, 1);
- assert.strictEqual(sections[0], section);
- });
+ prefs = {
+ line_length: 80,
+ show_tabs: true,
+ tab_size: 4,
+ };
+ keyLocations = {left: {}, right: {}};
- test('getSectionsByLineRange over diff', () => {
- const section = [
- outputEl.querySelector('stub:nth-of-type(2)'),
- outputEl.querySelector('stub:nth-of-type(3)'),
- ];
- const sections = element._builder.getSectionsByLineRange(1, 2, 'left');
- assert.equal(sections.length, 2);
- assert.strictEqual(sections[0], section[0]);
- assert.strictEqual(sections[1], section[1]);
- });
-
- test('render-start and render-content are fired', done => {
- const dispatchEventStub = sandbox.stub(element, 'dispatchEvent');
- element.render(keyLocations, {}).then(() => {
- const firedEventTypes = dispatchEventStub.getCalls()
- .map(c => c.args[0].type);
- assert.include(firedEventTypes, 'render-start');
- assert.include(firedEventTypes, 'render-content');
- done();
- });
- });
-
- test('cancel', () => {
- const processorCancelStub = sandbox.stub(element.$.processor, 'cancel');
- element.cancel();
- assert.isTrue(processorCancelStub.called);
+ element.render(keyLocations, prefs).then(() => {
+ builder = element._builder;
+ done();
});
});
- suite('mock-diff', () => {
- let element;
- let builder;
- let diff;
- let prefs;
- let keyLocations;
+ test('getContentByLine', () => {
+ let actual;
- setup(done => {
- element = fixture('mock-diff');
- diff = document.createElement('mock-diff-response').diffResponse;
- element.diff = diff;
+ actual = builder.getContentByLine(2, 'left');
+ assert.equal(actual.textContent, diff.content[0].ab[1]);
- prefs = {
- line_length: 80,
- show_tabs: true,
- tab_size: 4,
- };
- keyLocations = {left: {}, right: {}};
+ actual = builder.getContentByLine(2, 'right');
+ assert.equal(actual.textContent, diff.content[0].ab[1]);
- element.render(keyLocations, prefs).then(() => {
- builder = element._builder;
- done();
- });
+ actual = builder.getContentByLine(5, 'left');
+ assert.equal(actual.textContent, diff.content[2].ab[0]);
+
+ actual = builder.getContentByLine(5, 'right');
+ assert.equal(actual.textContent, diff.content[1].b[0]);
+ });
+
+ test('findLinesByRange', () => {
+ const lines = [];
+ const elems = [];
+ const start = 6;
+ const end = 10;
+ const count = end - start + 1;
+
+ builder.findLinesByRange(start, end, 'right', lines, elems);
+
+ assert.equal(lines.length, count);
+ assert.equal(elems.length, count);
+
+ for (let i = 0; i < 5; i++) {
+ assert.instanceOf(lines[i], GrDiffLine);
+ assert.equal(lines[i].afterNumber, start + i);
+ assert.instanceOf(elems[i], HTMLElement);
+ assert.equal(lines[i].text, elems[i].textContent);
+ }
+ });
+
+ test('_renderContentByRange', () => {
+ const spy = sandbox.spy(builder, '_createTextEl');
+ const start = 9;
+ const end = 14;
+ const count = end - start + 1;
+
+ builder._renderContentByRange(start, end, 'left');
+
+ assert.equal(spy.callCount, count);
+ spy.getCalls().forEach((call, i) => {
+ assert.equal(call.args[1].beforeNumber, start + i);
});
+ });
- test('getContentByLine', () => {
- let actual;
+ test('_renderContentByRange notexistent elements', () => {
+ const spy = sandbox.spy(builder, '_createTextEl');
- actual = builder.getContentByLine(2, 'left');
- assert.equal(actual.textContent, diff.content[0].ab[1]);
+ sandbox.stub(builder, 'findLinesByRange',
+ (s, e, d, lines, elements) => {
+ // Add a line and a corresponding element.
+ lines.push(new GrDiffLine(GrDiffLine.Type.BOTH));
+ const tr = document.createElement('tr');
+ const td = document.createElement('td');
+ const el = document.createElement('div');
+ tr.appendChild(td);
+ td.appendChild(el);
+ elements.push(el);
- actual = builder.getContentByLine(2, 'right');
- assert.equal(actual.textContent, diff.content[0].ab[1]);
+ // Add 2 lines without corresponding elements.
+ lines.push(new GrDiffLine(GrDiffLine.Type.BOTH));
+ lines.push(new GrDiffLine(GrDiffLine.Type.BOTH));
+ });
- actual = builder.getContentByLine(5, 'left');
- assert.equal(actual.textContent, diff.content[2].ab[0]);
+ builder._renderContentByRange(1, 10, 'left');
+ // Should be called only once because only one line had a corresponding
+ // element.
+ assert.equal(spy.callCount, 1);
+ });
- actual = builder.getContentByLine(5, 'right');
- assert.equal(actual.textContent, diff.content[1].b[0]);
- });
+ test('_getLineNumberEl side-by-side left', () => {
+ const contentEl = builder.getContentByLine(5, 'left',
+ element.$.diffTable);
+ const lineNumberEl = builder._getLineNumberEl(contentEl, 'left');
+ assert.isTrue(lineNumberEl.classList.contains('lineNum'));
+ assert.isTrue(lineNumberEl.classList.contains('left'));
+ });
- test('findLinesByRange', () => {
- const lines = [];
- const elems = [];
- const start = 6;
- const end = 10;
- const count = end - start + 1;
+ test('_getLineNumberEl side-by-side right', () => {
+ const contentEl = builder.getContentByLine(5, 'right',
+ element.$.diffTable);
+ const lineNumberEl = builder._getLineNumberEl(contentEl, 'right');
+ assert.isTrue(lineNumberEl.classList.contains('lineNum'));
+ assert.isTrue(lineNumberEl.classList.contains('right'));
+ });
- builder.findLinesByRange(start, end, 'right', lines, elems);
+ test('_getLineNumberEl unified left', done => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations, prefs).then(() => {
+ builder = element._builder;
- assert.equal(lines.length, count);
- assert.equal(elems.length, count);
-
- for (let i = 0; i < 5; i++) {
- assert.instanceOf(lines[i], GrDiffLine);
- assert.equal(lines[i].afterNumber, start + i);
- assert.instanceOf(elems[i], HTMLElement);
- assert.equal(lines[i].text, elems[i].textContent);
- }
- });
-
- test('_renderContentByRange', () => {
- const spy = sandbox.spy(builder, '_createTextEl');
- const start = 9;
- const end = 14;
- const count = end - start + 1;
-
- builder._renderContentByRange(start, end, 'left');
-
- assert.equal(spy.callCount, count);
- spy.getCalls().forEach((call, i) => {
- assert.equal(call.args[1].beforeNumber, start + i);
- });
- });
-
- test('_renderContentByRange notexistent elements', () => {
- const spy = sandbox.spy(builder, '_createTextEl');
-
- sandbox.stub(builder, 'findLinesByRange',
- (s, e, d, lines, elements) => {
- // Add a line and a corresponding element.
- lines.push(new GrDiffLine(GrDiffLine.Type.BOTH));
- const tr = document.createElement('tr');
- const td = document.createElement('td');
- const el = document.createElement('div');
- tr.appendChild(td);
- td.appendChild(el);
- elements.push(el);
-
- // Add 2 lines without corresponding elements.
- lines.push(new GrDiffLine(GrDiffLine.Type.BOTH));
- lines.push(new GrDiffLine(GrDiffLine.Type.BOTH));
- });
-
- builder._renderContentByRange(1, 10, 'left');
- // Should be called only once because only one line had a corresponding
- // element.
- assert.equal(spy.callCount, 1);
- });
-
- test('_getLineNumberEl side-by-side left', () => {
const contentEl = builder.getContentByLine(5, 'left',
element.$.diffTable);
const lineNumberEl = builder._getLineNumberEl(contentEl, 'left');
assert.isTrue(lineNumberEl.classList.contains('lineNum'));
assert.isTrue(lineNumberEl.classList.contains('left'));
+ done();
});
+ });
- test('_getLineNumberEl side-by-side right', () => {
+ test('_getLineNumberEl unified right', done => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations, prefs).then(() => {
+ builder = element._builder;
+
const contentEl = builder.getContentByLine(5, 'right',
element.$.diffTable);
const lineNumberEl = builder._getLineNumberEl(contentEl, 'right');
assert.isTrue(lineNumberEl.classList.contains('lineNum'));
assert.isTrue(lineNumberEl.classList.contains('right'));
+ done();
});
+ });
- test('_getLineNumberEl unified left', done => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- element.render(keyLocations, prefs).then(() => {
- builder = element._builder;
+ test('_getNextContentOnSide side-by-side left', () => {
+ const startElem = builder.getContentByLine(5, 'left',
+ element.$.diffTable);
+ const expectedStartString = diff.content[2].ab[0];
+ const expectedNextString = diff.content[2].ab[1];
+ assert.equal(startElem.textContent, expectedStartString);
- const contentEl = builder.getContentByLine(5, 'left',
- element.$.diffTable);
- const lineNumberEl = builder._getLineNumberEl(contentEl, 'left');
- assert.isTrue(lineNumberEl.classList.contains('lineNum'));
- assert.isTrue(lineNumberEl.classList.contains('left'));
- done();
- });
- });
+ const nextElem = builder._getNextContentOnSide(startElem,
+ 'left');
+ assert.equal(nextElem.textContent, expectedNextString);
+ });
- test('_getLineNumberEl unified right', done => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- element.render(keyLocations, prefs).then(() => {
- builder = element._builder;
+ test('_getNextContentOnSide side-by-side right', () => {
+ const startElem = builder.getContentByLine(5, 'right',
+ element.$.diffTable);
+ const expectedStartString = diff.content[1].b[0];
+ const expectedNextString = diff.content[1].b[1];
+ assert.equal(startElem.textContent, expectedStartString);
- const contentEl = builder.getContentByLine(5, 'right',
- element.$.diffTable);
- const lineNumberEl = builder._getLineNumberEl(contentEl, 'right');
- assert.isTrue(lineNumberEl.classList.contains('lineNum'));
- assert.isTrue(lineNumberEl.classList.contains('right'));
- done();
- });
- });
+ const nextElem = builder._getNextContentOnSide(startElem,
+ 'right');
+ assert.equal(nextElem.textContent, expectedNextString);
+ });
- test('_getNextContentOnSide side-by-side left', () => {
+ test('_getNextContentOnSide unified left', done => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations, prefs).then(() => {
+ builder = element._builder;
+
const startElem = builder.getContentByLine(5, 'left',
element.$.diffTable);
const expectedStartString = diff.content[2].ab[0];
@@ -1047,9 +1072,17 @@
const nextElem = builder._getNextContentOnSide(startElem,
'left');
assert.equal(nextElem.textContent, expectedNextString);
- });
- test('_getNextContentOnSide side-by-side right', () => {
+ done();
+ });
+ });
+
+ test('_getNextContentOnSide unified right', done => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations, prefs).then(() => {
+ builder = element._builder;
+
const startElem = builder.getContentByLine(5, 'right',
element.$.diffTable);
const expectedStartString = diff.content[1].b[0];
@@ -1059,136 +1092,99 @@
const nextElem = builder._getNextContentOnSide(startElem,
'right');
assert.equal(nextElem.textContent, expectedNextString);
- });
- test('_getNextContentOnSide unified left', done => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- element.render(keyLocations, prefs).then(() => {
- builder = element._builder;
-
- const startElem = builder.getContentByLine(5, 'left',
- element.$.diffTable);
- const expectedStartString = diff.content[2].ab[0];
- const expectedNextString = diff.content[2].ab[1];
- assert.equal(startElem.textContent, expectedStartString);
-
- const nextElem = builder._getNextContentOnSide(startElem,
- 'left');
- assert.equal(nextElem.textContent, expectedNextString);
-
- done();
- });
- });
-
- test('_getNextContentOnSide unified right', done => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- element.render(keyLocations, prefs).then(() => {
- builder = element._builder;
-
- const startElem = builder.getContentByLine(5, 'right',
- element.$.diffTable);
- const expectedStartString = diff.content[1].b[0];
- const expectedNextString = diff.content[1].b[1];
- assert.equal(startElem.textContent, expectedStartString);
-
- const nextElem = builder._getNextContentOnSide(startElem,
- 'right');
- assert.equal(nextElem.textContent, expectedNextString);
-
- done();
- });
- });
-
- test('escaping HTML', () => {
- let input = '<script>alert("XSS");<' + '/script>';
- let expected = '<script>alert("XSS");</script>';
- let result = builder._formatText(input, 1, Infinity).innerHTML;
- assert.equal(result, expected);
-
- input = '& < > " \' / `';
- expected = '& < > " \' / `';
- result = builder._formatText(input, 1, Infinity).innerHTML;
- assert.equal(result, expected);
+ done();
});
});
- suite('blame', () => {
- let mockBlame;
+ test('escaping HTML', () => {
+ let input = '<script>alert("XSS");<' + '/script>';
+ let expected = '<script>alert("XSS");</script>';
+ let result = builder._formatText(input, 1, Infinity).innerHTML;
+ assert.equal(result, expected);
- setup(() => {
- mockBlame = [
- {id: 'commit 1', ranges: [{start: 1, end: 2}, {start: 10, end: 16}]},
- {id: 'commit 2', ranges: [{start: 4, end: 10}, {start: 17, end: 32}]},
- ];
- });
-
- test('setBlame attempts to render each blamed line', () => {
- const getBlameStub = sandbox.stub(builder, '_getBlameByLineNum')
- .returns(null);
- builder.setBlame(mockBlame);
- assert.equal(getBlameStub.callCount, 32);
- });
-
- test('_getBlameCommitForBaseLine', () => {
- builder.setBlame(mockBlame);
- assert.isOk(builder._getBlameCommitForBaseLine(1));
- assert.equal(builder._getBlameCommitForBaseLine(1).id, 'commit 1');
-
- assert.isOk(builder._getBlameCommitForBaseLine(11));
- assert.equal(builder._getBlameCommitForBaseLine(11).id, 'commit 1');
-
- assert.isOk(builder._getBlameCommitForBaseLine(32));
- assert.equal(builder._getBlameCommitForBaseLine(32).id, 'commit 2');
-
- assert.isNull(builder._getBlameCommitForBaseLine(33));
- });
-
- test('_getBlameCommitForBaseLine w/o blame returns null', () => {
- assert.isNull(builder._getBlameCommitForBaseLine(1));
- assert.isNull(builder._getBlameCommitForBaseLine(11));
- assert.isNull(builder._getBlameCommitForBaseLine(31));
- });
-
- test('_createBlameCell', () => {
- const mocbBlameCell = document.createElement('span');
- const getBlameStub = sinon.stub(builder, '_getBlameForBaseLine')
- .returns(mocbBlameCell);
- const line = new GrDiffLine(GrDiffLine.Type.BOTH);
- line.beforeNumber = 3;
- line.afterNumber = 5;
-
- const result = builder._createBlameCell(line);
-
- assert.isTrue(getBlameStub.calledWithExactly(3));
- assert.equal(result.getAttribute('data-line-number'), '3');
- assert.equal(result.firstChild, mocbBlameCell);
- });
-
- test('_getBlameForBaseLine', () => {
- const mockCommit = {
- time: 1576105200,
- id: 1234567890,
- author: 'Clark Kent',
- commit_msg: 'Testing Commit',
- ranges: [1],
- };
- const blameNode = builder._getBlameForBaseLine(1, mockCommit);
-
- const authors = blameNode.getElementsByClassName('blameAuthor');
- assert.equal(authors.length, 1);
- assert.equal(authors[0].innerText, ' Clark');
-
- const date = (new Date(mockCommit.time * 1000)).toLocaleDateString();
- Polymer.dom.flush();
- const cards = blameNode.getElementsByClassName('blameHoverCard');
- assert.equal(cards.length, 1);
- assert.equal(cards[0].innerHTML,
- `Commit 1234567890<br>Author: Clark Kent<br>Date: ${date}`
- + '<br><br>Testing Commit'
- );
- });
+ input = '& < > " \' / `';
+ expected = '& < > " \' / `';
+ result = builder._formatText(input, 1, Infinity).innerHTML;
+ assert.equal(result, expected);
});
});
+
+ suite('blame', () => {
+ let mockBlame;
+
+ setup(() => {
+ mockBlame = [
+ {id: 'commit 1', ranges: [{start: 1, end: 2}, {start: 10, end: 16}]},
+ {id: 'commit 2', ranges: [{start: 4, end: 10}, {start: 17, end: 32}]},
+ ];
+ });
+
+ test('setBlame attempts to render each blamed line', () => {
+ const getBlameStub = sandbox.stub(builder, '_getBlameByLineNum')
+ .returns(null);
+ builder.setBlame(mockBlame);
+ assert.equal(getBlameStub.callCount, 32);
+ });
+
+ test('_getBlameCommitForBaseLine', () => {
+ builder.setBlame(mockBlame);
+ assert.isOk(builder._getBlameCommitForBaseLine(1));
+ assert.equal(builder._getBlameCommitForBaseLine(1).id, 'commit 1');
+
+ assert.isOk(builder._getBlameCommitForBaseLine(11));
+ assert.equal(builder._getBlameCommitForBaseLine(11).id, 'commit 1');
+
+ assert.isOk(builder._getBlameCommitForBaseLine(32));
+ assert.equal(builder._getBlameCommitForBaseLine(32).id, 'commit 2');
+
+ assert.isNull(builder._getBlameCommitForBaseLine(33));
+ });
+
+ test('_getBlameCommitForBaseLine w/o blame returns null', () => {
+ assert.isNull(builder._getBlameCommitForBaseLine(1));
+ assert.isNull(builder._getBlameCommitForBaseLine(11));
+ assert.isNull(builder._getBlameCommitForBaseLine(31));
+ });
+
+ test('_createBlameCell', () => {
+ const mocbBlameCell = document.createElement('span');
+ const getBlameStub = sinon.stub(builder, '_getBlameForBaseLine')
+ .returns(mocbBlameCell);
+ const line = new GrDiffLine(GrDiffLine.Type.BOTH);
+ line.beforeNumber = 3;
+ line.afterNumber = 5;
+
+ const result = builder._createBlameCell(line);
+
+ assert.isTrue(getBlameStub.calledWithExactly(3));
+ assert.equal(result.getAttribute('data-line-number'), '3');
+ assert.equal(result.firstChild, mocbBlameCell);
+ });
+
+ test('_getBlameForBaseLine', () => {
+ const mockCommit = {
+ time: 1576105200,
+ id: 1234567890,
+ author: 'Clark Kent',
+ commit_msg: 'Testing Commit',
+ ranges: [1],
+ };
+ const blameNode = builder._getBlameForBaseLine(1, mockCommit);
+
+ const authors = blameNode.getElementsByClassName('blameAuthor');
+ assert.equal(authors.length, 1);
+ assert.equal(authors[0].innerText, ' Clark');
+
+ const date = (new Date(mockCommit.time * 1000)).toLocaleDateString();
+ flush();
+ const cards = blameNode.getElementsByClassName('blameHoverCard');
+ assert.equal(cards.length, 1);
+ assert.equal(cards[0].innerHTML,
+ `Commit 1234567890<br>Author: Clark Kent<br>Date: ${date}`
+ + '<br><br>Testing Commit'
+ );
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html
index 4f0a94f..88187fd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html
@@ -19,189 +19,185 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>GrDiffBuilderUnified</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
-<script src="../gr-diff/gr-diff-line.js"></script>
-<script src="../gr-diff/gr-diff-group.js"></script>
-<script src="../gr-diff-highlight/gr-annotation.js"></script>
-<script src="gr-diff-builder.js"></script>
-<script src="gr-diff-builder-unified.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import '../gr-diff/gr-diff-line.js';
+import '../gr-diff/gr-diff-group.js';
+import '../gr-diff-highlight/gr-annotation.js';
+import './gr-diff-builder.js';
+import './gr-diff-builder-unified.js';
+suite('GrDiffBuilderUnified tests', () => {
+ let prefs;
+ let outputEl;
+ let diffBuilder;
-<script>
- suite('GrDiffBuilderUnified tests', async () => {
- await readyToTest();
- let prefs;
- let outputEl;
- let diffBuilder;
+ setup(()=> {
+ prefs = {
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ };
+ outputEl = document.createElement('div');
+ diffBuilder = new GrDiffBuilderUnified({}, prefs, outputEl, []);
+ });
- setup(()=> {
- prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- };
- outputEl = document.createElement('div');
- diffBuilder = new GrDiffBuilderUnified({}, prefs, outputEl, []);
+ suite('buildSectionElement for BOTH group', () => {
+ let lines;
+ let group;
+
+ setup(() => {
+ lines = [
+ new GrDiffLine(GrDiffLine.Type.BOTH, 1, 2),
+ new GrDiffLine(GrDiffLine.Type.BOTH, 2, 3),
+ new GrDiffLine(GrDiffLine.Type.BOTH, 3, 4),
+ ];
+ lines[0].text = 'def hello_world():';
+ lines[1].text = ' print "Hello World";';
+ lines[2].text = ' return True';
+
+ group = new GrDiffGroup(GrDiffGroup.Type.BOTH, lines);
});
- suite('buildSectionElement for BOTH group', () => {
- let lines;
- let group;
-
- setup(() => {
- lines = [
- new GrDiffLine(GrDiffLine.Type.BOTH, 1, 2),
- new GrDiffLine(GrDiffLine.Type.BOTH, 2, 3),
- new GrDiffLine(GrDiffLine.Type.BOTH, 3, 4),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World";';
- lines[2].text = ' return True';
-
- group = new GrDiffGroup(GrDiffGroup.Type.BOTH, lines);
- });
-
- test('creates the section', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('section'));
- assert.isTrue(sectionEl.classList.contains('both'));
- });
-
- test('creates each unchanged row once', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 3);
-
- assert.equal(
- rowEls[0].querySelector('.lineNum.left').textContent,
- lines[0].beforeNumber);
- assert.equal(
- rowEls[0].querySelector('.lineNum.right').textContent,
- lines[0].afterNumber);
- assert.equal(
- rowEls[0].querySelector('.content').textContent, lines[0].text);
-
- assert.equal(
- rowEls[1].querySelector('.lineNum.left').textContent,
- lines[1].beforeNumber);
- assert.equal(
- rowEls[1].querySelector('.lineNum.right').textContent,
- lines[1].afterNumber);
- assert.equal(
- rowEls[1].querySelector('.content').textContent, lines[1].text);
-
- assert.equal(
- rowEls[2].querySelector('.lineNum.left').textContent,
- lines[2].beforeNumber);
- assert.equal(
- rowEls[2].querySelector('.lineNum.right').textContent,
- lines[2].afterNumber);
- assert.equal(
- rowEls[2].querySelector('.content').textContent, lines[2].text);
- });
+ test('creates the section', () => {
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('section'));
+ assert.isTrue(sectionEl.classList.contains('both'));
});
- suite('buildSectionElement for DELTA group', () => {
- let lines;
- let group;
+ test('creates each unchanged row once', () => {
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ const rowEls = sectionEl.querySelectorAll('.diff-row');
- setup(() => {
- lines = [
- new GrDiffLine(GrDiffLine.Type.REMOVE, 1),
- new GrDiffLine(GrDiffLine.Type.REMOVE, 2),
- new GrDiffLine(GrDiffLine.Type.ADD, 2),
- new GrDiffLine(GrDiffLine.Type.ADD, 3),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World"';
- lines[2].text = 'def hello_universe()';
- lines[3].text = ' print "Hello Universe"';
+ assert.equal(rowEls.length, 3);
- group = new GrDiffGroup(GrDiffGroup.Type.DELTA, lines);
- });
+ assert.equal(
+ rowEls[0].querySelector('.lineNum.left').textContent,
+ lines[0].beforeNumber);
+ assert.equal(
+ rowEls[0].querySelector('.lineNum.right').textContent,
+ lines[0].afterNumber);
+ assert.equal(
+ rowEls[0].querySelector('.content').textContent, lines[0].text);
- test('creates the section', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('section'));
- assert.isTrue(sectionEl.classList.contains('delta'));
- });
+ assert.equal(
+ rowEls[1].querySelector('.lineNum.left').textContent,
+ lines[1].beforeNumber);
+ assert.equal(
+ rowEls[1].querySelector('.lineNum.right').textContent,
+ lines[1].afterNumber);
+ assert.equal(
+ rowEls[1].querySelector('.content').textContent, lines[1].text);
- test('creates the section with class if ignoredWhitespaceOnly', () => {
- group.ignoredWhitespaceOnly = true;
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('ignoredWhitespaceOnly'));
- });
-
- test('creates the section with class if dueToRebase', () => {
- group.dueToRebase = true;
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('dueToRebase'));
- });
-
- test('creates first the removed and then the added rows', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 4);
-
- assert.equal(
- rowEls[0].querySelector('.lineNum.left').textContent,
- lines[0].beforeNumber);
- assert.isNotOk(rowEls[0].querySelector('.lineNum.right'));
- assert.equal(
- rowEls[0].querySelector('.content').textContent, lines[0].text);
-
- assert.equal(
- rowEls[1].querySelector('.lineNum.left').textContent,
- lines[1].beforeNumber);
- assert.isNotOk(rowEls[1].querySelector('.lineNum.right'));
- assert.equal(
- rowEls[1].querySelector('.content').textContent, lines[1].text);
-
- assert.isNotOk(rowEls[2].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[2].querySelector('.lineNum.right').textContent,
- lines[2].afterNumber);
- assert.equal(
- rowEls[2].querySelector('.content').textContent, lines[2].text);
-
- assert.isNotOk(rowEls[3].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[3].querySelector('.lineNum.right').textContent,
- lines[3].afterNumber);
- assert.equal(
- rowEls[3].querySelector('.content').textContent, lines[3].text);
- });
-
- test('creates only the added rows if only ignored whitespace', () => {
- group.ignoredWhitespaceOnly = true;
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 2);
-
- assert.isNotOk(rowEls[0].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[0].querySelector('.lineNum.right').textContent,
- lines[2].afterNumber);
- assert.equal(
- rowEls[0].querySelector('.content').textContent, lines[2].text);
-
- assert.isNotOk(rowEls[1].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[1].querySelector('.lineNum.right').textContent,
- lines[3].afterNumber);
- assert.equal(
- rowEls[1].querySelector('.content').textContent, lines[3].text);
- });
+ assert.equal(
+ rowEls[2].querySelector('.lineNum.left').textContent,
+ lines[2].beforeNumber);
+ assert.equal(
+ rowEls[2].querySelector('.lineNum.right').textContent,
+ lines[2].afterNumber);
+ assert.equal(
+ rowEls[2].querySelector('.content').textContent, lines[2].text);
});
});
+
+ suite('buildSectionElement for DELTA group', () => {
+ let lines;
+ let group;
+
+ setup(() => {
+ lines = [
+ new GrDiffLine(GrDiffLine.Type.REMOVE, 1),
+ new GrDiffLine(GrDiffLine.Type.REMOVE, 2),
+ new GrDiffLine(GrDiffLine.Type.ADD, 2),
+ new GrDiffLine(GrDiffLine.Type.ADD, 3),
+ ];
+ lines[0].text = 'def hello_world():';
+ lines[1].text = ' print "Hello World"';
+ lines[2].text = 'def hello_universe()';
+ lines[3].text = ' print "Hello Universe"';
+
+ group = new GrDiffGroup(GrDiffGroup.Type.DELTA, lines);
+ });
+
+ test('creates the section', () => {
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('section'));
+ assert.isTrue(sectionEl.classList.contains('delta'));
+ });
+
+ test('creates the section with class if ignoredWhitespaceOnly', () => {
+ group.ignoredWhitespaceOnly = true;
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('ignoredWhitespaceOnly'));
+ });
+
+ test('creates the section with class if dueToRebase', () => {
+ group.dueToRebase = true;
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('dueToRebase'));
+ });
+
+ test('creates first the removed and then the added rows', () => {
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+ assert.equal(rowEls.length, 4);
+
+ assert.equal(
+ rowEls[0].querySelector('.lineNum.left').textContent,
+ lines[0].beforeNumber);
+ assert.isNotOk(rowEls[0].querySelector('.lineNum.right'));
+ assert.equal(
+ rowEls[0].querySelector('.content').textContent, lines[0].text);
+
+ assert.equal(
+ rowEls[1].querySelector('.lineNum.left').textContent,
+ lines[1].beforeNumber);
+ assert.isNotOk(rowEls[1].querySelector('.lineNum.right'));
+ assert.equal(
+ rowEls[1].querySelector('.content').textContent, lines[1].text);
+
+ assert.isNotOk(rowEls[2].querySelector('.lineNum.left'));
+ assert.equal(
+ rowEls[2].querySelector('.lineNum.right').textContent,
+ lines[2].afterNumber);
+ assert.equal(
+ rowEls[2].querySelector('.content').textContent, lines[2].text);
+
+ assert.isNotOk(rowEls[3].querySelector('.lineNum.left'));
+ assert.equal(
+ rowEls[3].querySelector('.lineNum.right').textContent,
+ lines[3].afterNumber);
+ assert.equal(
+ rowEls[3].querySelector('.content').textContent, lines[3].text);
+ });
+
+ test('creates only the added rows if only ignored whitespace', () => {
+ group.ignoredWhitespaceOnly = true;
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+ assert.equal(rowEls.length, 2);
+
+ assert.isNotOk(rowEls[0].querySelector('.lineNum.left'));
+ assert.equal(
+ rowEls[0].querySelector('.lineNum.right').textContent,
+ lines[2].afterNumber);
+ assert.equal(
+ rowEls[0].querySelector('.content').textContent, lines[2].text);
+
+ assert.isNotOk(rowEls[1].querySelector('.lineNum.left'));
+ assert.equal(
+ rowEls[1].querySelector('.lineNum.right').textContent,
+ lines[3].afterNumber);
+ assert.equal(
+ rowEls[1].querySelector('.content').textContent, lines[3].text);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
deleted file mode 100644
index 1e2d963..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
+++ /dev/null
@@ -1,33 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
-
-<dom-module id="gr-diff-cursor">
- <template>
- <gr-cursor-manager
- id="cursorManager"
- scroll-behavior="[[_scrollBehavior]]"
- cursor-target-class="target-row"
- focus-on-move="[[_focusOnMove]]"
- target="{{diffRow}}"
- scroll-top-margin="[[scrollTopMargin]]"
- ></gr-cursor-manager>
- </template>
- <script src="gr-diff-cursor.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
index 87152d8..92ea310 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -14,485 +14,494 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const DiffSides = {
- LEFT: 'left',
- RIGHT: 'right',
- };
+import '../../shared/gr-cursor-manager/gr-cursor-manager.js';
+import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-diff-cursor_html.js';
- const DiffViewMode = {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- };
+const DiffSides = {
+ LEFT: 'left',
+ RIGHT: 'right',
+};
- const ScrollBehavior = {
- KEEP_VISIBLE: 'keep-visible',
- NEVER: 'never',
- };
+const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+};
- const LEFT_SIDE_CLASS = 'target-side-left';
- const RIGHT_SIDE_CLASS = 'target-side-right';
+const ScrollBehavior = {
+ KEEP_VISIBLE: 'keep-visible',
+ NEVER: 'never',
+};
- /** @extends Polymer.Element */
- class GrDiffCursor extends Polymer.mixinBehaviors([Gerrit.FireBehavior],
- Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(Polymer.Element))) {
- static get is() { return 'gr-diff-cursor'; }
+const LEFT_SIDE_CLASS = 'target-side-left';
+const RIGHT_SIDE_CLASS = 'target-side-right';
- static get properties() {
- return {
+/** @extends Polymer.Element */
+class GrDiffCursor extends mixinBehaviors([Gerrit.FireBehavior],
+ GestureEventListeners(
+ LegacyElementMixin(PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-diff-cursor'; }
+
+ static get properties() {
+ return {
+ /**
+ * Either DiffSides.LEFT or DiffSides.RIGHT.
+ */
+ side: {
+ type: String,
+ value: DiffSides.RIGHT,
+ },
+ /** @type {!HTMLElement|undefined} */
+ diffRow: {
+ type: Object,
+ notify: true,
+ observer: '_rowChanged',
+ },
+
/**
- * Either DiffSides.LEFT or DiffSides.RIGHT.
+ * The diff views to cursor through and listen to.
*/
- side: {
- type: String,
- value: DiffSides.RIGHT,
- },
- /** @type {!HTMLElement|undefined} */
- diffRow: {
- type: Object,
- notify: true,
- observer: '_rowChanged',
- },
+ diffs: {
+ type: Array,
+ value() { return []; },
+ },
- /**
- * The diff views to cursor through and listen to.
- */
- diffs: {
- type: Array,
- value() { return []; },
- },
+ /**
+ * If set, the cursor will attempt to move to the line number (instead of
+ * the first chunk) the next time the diff renders. It is set back to null
+ * when used. It should be only used if you want the line to be focused
+ * after initialization of the component and page should scroll
+ * to that position. This parameter should be set at most for one gr-diff
+ * element in the page.
+ *
+ * @type {?number}
+ */
+ initialLineNumber: {
+ type: Number,
+ value: null,
+ },
- /**
- * If set, the cursor will attempt to move to the line number (instead of
- * the first chunk) the next time the diff renders. It is set back to null
- * when used. It should be only used if you want the line to be focused
- * after initialization of the component and page should scroll
- * to that position. This parameter should be set at most for one gr-diff
- * element in the page.
- *
- * @type {?number}
- */
- initialLineNumber: {
- type: Number,
- value: null,
- },
+ /**
+ * The scroll behavior for the cursor. Values are 'never' and
+ * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
+ * the viewport.
+ */
+ _scrollBehavior: {
+ type: String,
+ value: ScrollBehavior.KEEP_VISIBLE,
+ },
- /**
- * The scroll behavior for the cursor. Values are 'never' and
- * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
- * the viewport.
- */
- _scrollBehavior: {
- type: String,
- value: ScrollBehavior.KEEP_VISIBLE,
- },
+ _focusOnMove: {
+ type: Boolean,
+ value: true,
+ },
- _focusOnMove: {
- type: Boolean,
- value: true,
- },
+ _listeningForScroll: Boolean,
- _listeningForScroll: Boolean,
+ /**
+ * gr-diff-view has gr-fixed-panel on top. The panel can
+ * intersect a main element and partially hides a content of
+ * the main element. To correctly calculates visibility of an
+ * element, the cursor must know how much height occuped by a fixed
+ * panel.
+ * The scrollTopMargin defines margin occuped by fixed panel.
+ */
+ scrollTopMargin: {
+ type: Number,
+ value: 0,
+ },
+ };
+ }
- /**
- * gr-diff-view has gr-fixed-panel on top. The panel can
- * intersect a main element and partially hides a content of
- * the main element. To correctly calculates visibility of an
- * element, the cursor must know how much height occuped by a fixed
- * panel.
- * The scrollTopMargin defines margin occuped by fixed panel.
- */
- scrollTopMargin: {
- type: Number,
- value: 0,
- },
- };
+ static get observers() {
+ return [
+ '_updateSideClass(side)',
+ '_diffsChanged(diffs.splices)',
+ ];
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ afterNextRender(this, () => {
+ /*
+ This represents the diff cursor is ready for interaction coming from
+ client components. It is more then Polymer "ready" lifecycle, as no
+ "ready" events are automatically fired by Polymer, it means
+ the cursor is completely interactable - in this case attached and
+ painted on the page. We name it "ready" instead of "rendered" as the
+ long-term goal is to make gr-diff-cursor a javascript class - not a DOM
+ element with an actual lifecycle. This will be triggered only once
+ per element.
+ */
+ this.fire('ready', null, {bubbles: false});
+ });
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ // Catch when users are scrolling as the view loads.
+ this.listen(window, 'scroll', '_handleWindowScroll');
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this.unlisten(window, 'scroll', '_handleWindowScroll');
+ }
+
+ moveLeft() {
+ this.side = DiffSides.LEFT;
+ if (this._isTargetBlank()) {
+ this.moveUp();
+ }
+ }
+
+ moveRight() {
+ this.side = DiffSides.RIGHT;
+ if (this._isTargetBlank()) {
+ this.moveUp();
+ }
+ }
+
+ moveDown() {
+ if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
+ this.$.cursorManager.next(this._rowHasSide.bind(this));
+ } else {
+ this.$.cursorManager.next();
+ }
+ }
+
+ moveUp() {
+ if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
+ this.$.cursorManager.previous(this._rowHasSide.bind(this));
+ } else {
+ this.$.cursorManager.previous();
+ }
+ }
+
+ moveToVisibleArea() {
+ if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
+ this.$.cursorManager.moveToVisibleArea(
+ this._rowHasSide.bind(this));
+ } else {
+ this.$.cursorManager.moveToVisibleArea();
+ }
+ }
+
+ moveToNextChunk(opt_clipToTop) {
+ this.$.cursorManager.next(this._isFirstRowOfChunk.bind(this),
+ target => target.parentNode.scrollHeight, opt_clipToTop);
+ this._fixSide();
+ }
+
+ moveToPreviousChunk() {
+ this.$.cursorManager.previous(this._isFirstRowOfChunk.bind(this));
+ this._fixSide();
+ }
+
+ moveToNextCommentThread() {
+ this.$.cursorManager.next(this._rowHasThread.bind(this));
+ this._fixSide();
+ }
+
+ moveToPreviousCommentThread() {
+ this.$.cursorManager.previous(this._rowHasThread.bind(this));
+ this._fixSide();
+ }
+
+ /**
+ * @param {number} number
+ * @param {string} side
+ * @param {string=} opt_path
+ */
+ moveToLineNumber(number, side, opt_path) {
+ const row = this._findRowByNumberAndFile(number, side, opt_path);
+ if (row) {
+ this.side = side;
+ this.$.cursorManager.setCursor(row);
+ }
+ }
+
+ /**
+ * Get the line number element targeted by the cursor row and side.
+ *
+ * @return {?Element|undefined}
+ */
+ getTargetLineElement() {
+ let lineElSelector = '.lineNum';
+
+ if (!this.diffRow) {
+ return;
}
- static get observers() {
- return [
- '_updateSideClass(side)',
- '_diffsChanged(diffs.splices)',
- ];
+ if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
+ lineElSelector += this.side === DiffSides.LEFT ? '.left' : '.right';
}
- /** @override */
- ready() {
- super.ready();
- Polymer.RenderStatus.afterNextRender(this, () => {
- /*
- This represents the diff cursor is ready for interaction coming from
- client components. It is more then Polymer "ready" lifecycle, as no
- "ready" events are automatically fired by Polymer, it means
- the cursor is completely interactable - in this case attached and
- painted on the page. We name it "ready" instead of "rendered" as the
- long-term goal is to make gr-diff-cursor a javascript class - not a DOM
- element with an actual lifecycle. This will be triggered only once
- per element.
- */
- this.fire('ready', null, {bubbles: false});
- });
+ return this.diffRow.querySelector(lineElSelector);
+ }
+
+ getTargetDiffElement() {
+ if (!this.diffRow) return null;
+
+ const hostOwner = dom( (this.diffRow))
+ .getOwnerRoot();
+ if (hostOwner && hostOwner.host &&
+ hostOwner.host.tagName === 'GR-DIFF') {
+ return hostOwner.host;
}
+ return null;
+ }
- /** @override */
- attached() {
- super.attached();
- // Catch when users are scrolling as the view loads.
- this.listen(window, 'scroll', '_handleWindowScroll');
+ moveToFirstChunk() {
+ this.$.cursorManager.moveToStart();
+ this.moveToNextChunk(true);
+ }
+
+ moveToLastChunk() {
+ this.$.cursorManager.moveToEnd();
+ this.moveToPreviousChunk();
+ }
+
+ reInitCursor() {
+ this._updateStops();
+ if (this.initialLineNumber) {
+ this.moveToLineNumber(this.initialLineNumber, this.side);
+ this.initialLineNumber = null;
+ } else {
+ this.moveToFirstChunk();
}
+ }
- /** @override */
- detached() {
- super.detached();
- this.unlisten(window, 'scroll', '_handleWindowScroll');
- }
-
- moveLeft() {
- this.side = DiffSides.LEFT;
- if (this._isTargetBlank()) {
- this.moveUp();
- }
- }
-
- moveRight() {
- this.side = DiffSides.RIGHT;
- if (this._isTargetBlank()) {
- this.moveUp();
- }
- }
-
- moveDown() {
- if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
- this.$.cursorManager.next(this._rowHasSide.bind(this));
- } else {
- this.$.cursorManager.next();
- }
- }
-
- moveUp() {
- if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
- this.$.cursorManager.previous(this._rowHasSide.bind(this));
- } else {
- this.$.cursorManager.previous();
- }
- }
-
- moveToVisibleArea() {
- if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
- this.$.cursorManager.moveToVisibleArea(
- this._rowHasSide.bind(this));
- } else {
- this.$.cursorManager.moveToVisibleArea();
- }
- }
-
- moveToNextChunk(opt_clipToTop) {
- this.$.cursorManager.next(this._isFirstRowOfChunk.bind(this),
- target => target.parentNode.scrollHeight, opt_clipToTop);
- this._fixSide();
- }
-
- moveToPreviousChunk() {
- this.$.cursorManager.previous(this._isFirstRowOfChunk.bind(this));
- this._fixSide();
- }
-
- moveToNextCommentThread() {
- this.$.cursorManager.next(this._rowHasThread.bind(this));
- this._fixSide();
- }
-
- moveToPreviousCommentThread() {
- this.$.cursorManager.previous(this._rowHasThread.bind(this));
- this._fixSide();
- }
-
- /**
- * @param {number} number
- * @param {string} side
- * @param {string=} opt_path
- */
- moveToLineNumber(number, side, opt_path) {
- const row = this._findRowByNumberAndFile(number, side, opt_path);
- if (row) {
- this.side = side;
- this.$.cursorManager.setCursor(row);
- }
- }
-
- /**
- * Get the line number element targeted by the cursor row and side.
- *
- * @return {?Element|undefined}
- */
- getTargetLineElement() {
- let lineElSelector = '.lineNum';
-
- if (!this.diffRow) {
- return;
- }
-
- if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
- lineElSelector += this.side === DiffSides.LEFT ? '.left' : '.right';
- }
-
- return this.diffRow.querySelector(lineElSelector);
- }
-
- getTargetDiffElement() {
- if (!this.diffRow) return null;
-
- const hostOwner = Polymer.dom(/** @type {Node} */ (this.diffRow))
- .getOwnerRoot();
- if (hostOwner && hostOwner.host &&
- hostOwner.host.tagName === 'GR-DIFF') {
- return hostOwner.host;
- }
- return null;
- }
-
- moveToFirstChunk() {
- this.$.cursorManager.moveToStart();
- this.moveToNextChunk(true);
- }
-
- moveToLastChunk() {
- this.$.cursorManager.moveToEnd();
- this.moveToPreviousChunk();
- }
-
- reInitCursor() {
- this._updateStops();
- if (this.initialLineNumber) {
- this.moveToLineNumber(this.initialLineNumber, this.side);
- this.initialLineNumber = null;
- } else {
- this.moveToFirstChunk();
- }
- }
-
- _handleWindowScroll() {
- if (this._listeningForScroll) {
- this._scrollBehavior = ScrollBehavior.NEVER;
- this._focusOnMove = false;
- this._listeningForScroll = false;
- }
- }
-
- handleDiffUpdate() {
- this._updateStops();
- if (!this.diffRow) {
- // does not scroll during init unless requested
- const scrollingBehaviorForInit = this.initialLineNumber ?
- ScrollBehavior.KEEP_VISIBLE :
- ScrollBehavior.NEVER;
- this._scrollBehavior = scrollingBehaviorForInit;
- this.reInitCursor();
- }
- this._scrollBehavior = ScrollBehavior.KEEP_VISIBLE;
- this._focusOnMove = true;
+ _handleWindowScroll() {
+ if (this._listeningForScroll) {
+ this._scrollBehavior = ScrollBehavior.NEVER;
+ this._focusOnMove = false;
this._listeningForScroll = false;
}
+ }
- _handleDiffRenderStart() {
- this._listeningForScroll = true;
+ handleDiffUpdate() {
+ this._updateStops();
+ if (!this.diffRow) {
+ // does not scroll during init unless requested
+ const scrollingBehaviorForInit = this.initialLineNumber ?
+ ScrollBehavior.KEEP_VISIBLE :
+ ScrollBehavior.NEVER;
+ this._scrollBehavior = scrollingBehaviorForInit;
+ this.reInitCursor();
}
+ this._scrollBehavior = ScrollBehavior.KEEP_VISIBLE;
+ this._focusOnMove = true;
+ this._listeningForScroll = false;
+ }
- createCommentInPlace() {
- const diffWithRangeSelected = this.diffs
- .find(diff => diff.isRangeSelected());
- if (diffWithRangeSelected) {
- diffWithRangeSelected.createRangeComment();
- } else {
- const line = this.getTargetLineElement();
- if (line) {
- this.getTargetDiffElement().addDraftAtLine(line);
- }
- }
- }
+ _handleDiffRenderStart() {
+ this._listeningForScroll = true;
+ }
- /**
- * Get an object describing the location of the cursor. Such as
- * {leftSide: false, number: 123} for line 123 of the revision, or
- * {leftSide: true, number: 321} for line 321 of the base patch.
- * Returns null if an address is not available.
- *
- * @return {?Object}
- */
- getAddress() {
- if (!this.diffRow) { return null; }
-
- // Get the line-number cell targeted by the cursor. If the mode is unified
- // then prefer the revision cell if available.
- let cell;
- if (this._getViewMode() === DiffViewMode.UNIFIED) {
- cell = this.diffRow.querySelector('.lineNum.right');
- if (!cell) {
- cell = this.diffRow.querySelector('.lineNum.left');
- }
- } else {
- cell = this.diffRow.querySelector('.lineNum.' + this.side);
- }
- if (!cell) { return null; }
-
- const number = cell.getAttribute('data-value');
- if (!number || number === 'FILE') { return null; }
-
- return {
- leftSide: cell.matches('.left'),
- number: parseInt(number, 10),
- };
- }
-
- _getViewMode() {
- if (!this.diffRow) {
- return null;
- }
-
- if (this.diffRow.classList.contains('side-by-side')) {
- return DiffViewMode.SIDE_BY_SIDE;
- } else {
- return DiffViewMode.UNIFIED;
- }
- }
-
- _rowHasSide(row) {
- const selector = (this.side === DiffSides.LEFT ? '.left' : '.right') +
- ' + .content';
- return !!row.querySelector(selector);
- }
-
- _isFirstRowOfChunk(row) {
- const parentClassList = row.parentNode.classList;
- return parentClassList.contains('section') &&
- parentClassList.contains('delta') &&
- !row.previousSibling;
- }
-
- _rowHasThread(row) {
- return row.querySelector('.thread-group');
- }
-
- /**
- * If we jumped to a row where there is no content on the current side then
- * switch to the alternate side.
- */
- _fixSide() {
- if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE &&
- this._isTargetBlank()) {
- this.side = this.side === DiffSides.LEFT ?
- DiffSides.RIGHT : DiffSides.LEFT;
- }
- }
-
- _isTargetBlank() {
- if (!this.diffRow) {
- return false;
- }
-
- const actions = this._getActionsForRow();
- return (this.side === DiffSides.LEFT && !actions.left) ||
- (this.side === DiffSides.RIGHT && !actions.right);
- }
-
- _rowChanged(newRow, oldRow) {
- if (oldRow) {
- oldRow.classList.remove(LEFT_SIDE_CLASS, RIGHT_SIDE_CLASS);
- }
- this._updateSideClass();
- }
-
- _updateSideClass() {
- if (!this.diffRow) {
- return;
- }
- this.toggleClass(LEFT_SIDE_CLASS, this.side === DiffSides.LEFT,
- this.diffRow);
- this.toggleClass(RIGHT_SIDE_CLASS, this.side === DiffSides.RIGHT,
- this.diffRow);
- }
-
- _isActionType(type) {
- return type !== 'blank' && type !== 'contextControl';
- }
-
- _getActionsForRow() {
- const actions = {left: false, right: false};
- if (this.diffRow) {
- actions.left = this._isActionType(
- this.diffRow.getAttribute('left-type'));
- actions.right = this._isActionType(
- this.diffRow.getAttribute('right-type'));
- }
- return actions;
- }
-
- _getStops() {
- return this.diffs.reduce(
- (stops, diff) => stops.concat(diff.getCursorStops()), []);
- }
-
- _updateStops() {
- this.$.cursorManager.stops = this._getStops();
- }
-
- /**
- * Setup and tear down on-render listeners for any diffs that are added or
- * removed from the cursor.
- *
- * @private
- */
- _diffsChanged(changeRecord) {
- if (!changeRecord) { return; }
-
- this._updateStops();
-
- let splice;
- let i;
- for (let spliceIdx = 0;
- changeRecord.indexSplices &&
- spliceIdx < changeRecord.indexSplices.length;
- spliceIdx++) {
- splice = changeRecord.indexSplices[spliceIdx];
-
- for (i = splice.index;
- i < splice.index + splice.addedCount;
- i++) {
- this.listen(this.diffs[i], 'render-start', '_handleDiffRenderStart');
- this.listen(this.diffs[i], 'render-content', 'handleDiffUpdate');
- }
-
- for (i = 0;
- i < splice.removed && splice.removed.length;
- i++) {
- this.unlisten(splice.removed[i],
- 'render-start', '_handleDiffRenderStart');
- this.unlisten(splice.removed[i],
- 'render-content', 'handleDiffUpdate');
- }
- }
- }
-
- _findRowByNumberAndFile(targetNumber, side, opt_path) {
- let stops;
- if (opt_path) {
- const diff = this.diffs.filter(diff => diff.path === opt_path)[0];
- stops = diff.getCursorStops();
- } else {
- stops = this.$.cursorManager.stops;
- }
- let selector;
- for (let i = 0; i < stops.length; i++) {
- selector = '.lineNum.' + side + '[data-value="' + targetNumber + '"]';
- if (stops[i].querySelector(selector)) {
- return stops[i];
- }
+ createCommentInPlace() {
+ const diffWithRangeSelected = this.diffs
+ .find(diff => diff.isRangeSelected());
+ if (diffWithRangeSelected) {
+ diffWithRangeSelected.createRangeComment();
+ } else {
+ const line = this.getTargetLineElement();
+ if (line) {
+ this.getTargetDiffElement().addDraftAtLine(line);
}
}
}
- customElements.define(GrDiffCursor.is, GrDiffCursor);
-})();
+ /**
+ * Get an object describing the location of the cursor. Such as
+ * {leftSide: false, number: 123} for line 123 of the revision, or
+ * {leftSide: true, number: 321} for line 321 of the base patch.
+ * Returns null if an address is not available.
+ *
+ * @return {?Object}
+ */
+ getAddress() {
+ if (!this.diffRow) { return null; }
+
+ // Get the line-number cell targeted by the cursor. If the mode is unified
+ // then prefer the revision cell if available.
+ let cell;
+ if (this._getViewMode() === DiffViewMode.UNIFIED) {
+ cell = this.diffRow.querySelector('.lineNum.right');
+ if (!cell) {
+ cell = this.diffRow.querySelector('.lineNum.left');
+ }
+ } else {
+ cell = this.diffRow.querySelector('.lineNum.' + this.side);
+ }
+ if (!cell) { return null; }
+
+ const number = cell.getAttribute('data-value');
+ if (!number || number === 'FILE') { return null; }
+
+ return {
+ leftSide: cell.matches('.left'),
+ number: parseInt(number, 10),
+ };
+ }
+
+ _getViewMode() {
+ if (!this.diffRow) {
+ return null;
+ }
+
+ if (this.diffRow.classList.contains('side-by-side')) {
+ return DiffViewMode.SIDE_BY_SIDE;
+ } else {
+ return DiffViewMode.UNIFIED;
+ }
+ }
+
+ _rowHasSide(row) {
+ const selector = (this.side === DiffSides.LEFT ? '.left' : '.right') +
+ ' + .content';
+ return !!row.querySelector(selector);
+ }
+
+ _isFirstRowOfChunk(row) {
+ const parentClassList = row.parentNode.classList;
+ return parentClassList.contains('section') &&
+ parentClassList.contains('delta') &&
+ !row.previousSibling;
+ }
+
+ _rowHasThread(row) {
+ return row.querySelector('.thread-group');
+ }
+
+ /**
+ * If we jumped to a row where there is no content on the current side then
+ * switch to the alternate side.
+ */
+ _fixSide() {
+ if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE &&
+ this._isTargetBlank()) {
+ this.side = this.side === DiffSides.LEFT ?
+ DiffSides.RIGHT : DiffSides.LEFT;
+ }
+ }
+
+ _isTargetBlank() {
+ if (!this.diffRow) {
+ return false;
+ }
+
+ const actions = this._getActionsForRow();
+ return (this.side === DiffSides.LEFT && !actions.left) ||
+ (this.side === DiffSides.RIGHT && !actions.right);
+ }
+
+ _rowChanged(newRow, oldRow) {
+ if (oldRow) {
+ oldRow.classList.remove(LEFT_SIDE_CLASS, RIGHT_SIDE_CLASS);
+ }
+ this._updateSideClass();
+ }
+
+ _updateSideClass() {
+ if (!this.diffRow) {
+ return;
+ }
+ this.toggleClass(LEFT_SIDE_CLASS, this.side === DiffSides.LEFT,
+ this.diffRow);
+ this.toggleClass(RIGHT_SIDE_CLASS, this.side === DiffSides.RIGHT,
+ this.diffRow);
+ }
+
+ _isActionType(type) {
+ return type !== 'blank' && type !== 'contextControl';
+ }
+
+ _getActionsForRow() {
+ const actions = {left: false, right: false};
+ if (this.diffRow) {
+ actions.left = this._isActionType(
+ this.diffRow.getAttribute('left-type'));
+ actions.right = this._isActionType(
+ this.diffRow.getAttribute('right-type'));
+ }
+ return actions;
+ }
+
+ _getStops() {
+ return this.diffs.reduce(
+ (stops, diff) => stops.concat(diff.getCursorStops()), []);
+ }
+
+ _updateStops() {
+ this.$.cursorManager.stops = this._getStops();
+ }
+
+ /**
+ * Setup and tear down on-render listeners for any diffs that are added or
+ * removed from the cursor.
+ *
+ * @private
+ */
+ _diffsChanged(changeRecord) {
+ if (!changeRecord) { return; }
+
+ this._updateStops();
+
+ let splice;
+ let i;
+ for (let spliceIdx = 0;
+ changeRecord.indexSplices &&
+ spliceIdx < changeRecord.indexSplices.length;
+ spliceIdx++) {
+ splice = changeRecord.indexSplices[spliceIdx];
+
+ for (i = splice.index;
+ i < splice.index + splice.addedCount;
+ i++) {
+ this.listen(this.diffs[i], 'render-start', '_handleDiffRenderStart');
+ this.listen(this.diffs[i], 'render-content', 'handleDiffUpdate');
+ }
+
+ for (i = 0;
+ i < splice.removed && splice.removed.length;
+ i++) {
+ this.unlisten(splice.removed[i],
+ 'render-start', '_handleDiffRenderStart');
+ this.unlisten(splice.removed[i],
+ 'render-content', 'handleDiffUpdate');
+ }
+ }
+ }
+
+ _findRowByNumberAndFile(targetNumber, side, opt_path) {
+ let stops;
+ if (opt_path) {
+ const diff = this.diffs.filter(diff => diff.path === opt_path)[0];
+ stops = diff.getCursorStops();
+ } else {
+ stops = this.$.cursorManager.stops;
+ }
+ let selector;
+ for (let i = 0; i < stops.length; i++) {
+ selector = '.lineNum.' + side + '[data-value="' + targetNumber + '"]';
+ if (stops[i].querySelector(selector)) {
+ return stops[i];
+ }
+ }
+ }
+}
+
+customElements.define(GrDiffCursor.is, GrDiffCursor);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js
new file mode 100644
index 0000000..81e0c9b
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js
@@ -0,0 +1,21 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <gr-cursor-manager id="cursorManager" scroll-behavior="[[_scrollBehavior]]" cursor-target-class="target-row" focus-on-move="[[_focusOnMove]]" target="{{diffRow}}" scroll-top-margin="[[scrollTopMargin]]"></gr-cursor-manager>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
index 02b1d57..6d9c828 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -19,20 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-cursor</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="../gr-diff/gr-diff.html">
-<link rel="import" href="./gr-diff-cursor.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -49,54 +39,121 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff-cursor tests', async () => {
- await readyToTest();
- let sandbox;
- let cursorElement;
- let diffElement;
- let mockDiffResponse;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import '../gr-diff/gr-diff.js';
+import './gr-diff-cursor.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-diff-cursor tests', () => {
+ let sandbox;
+ let cursorElement;
+ let diffElement;
+ let mockDiffResponse;
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+
+ const fixtureElems = fixture('basic');
+ mockDiffResponse = fixtureElems[0];
+ diffElement = fixtureElems[1];
+ cursorElement = fixtureElems[2];
+ const restAPI = fixtureElems[3];
+
+ // Register the diff with the cursor.
+ cursorElement.push('diffs', diffElement);
+
+ diffElement.loggedIn = false;
+ diffElement.patchRange = {basePatchNum: 1, patchNum: 2};
+ diffElement.comments = {
+ left: [],
+ right: [],
+ meta: {patchRange: undefined},
+ };
+ const setupDone = () => {
+ cursorElement._updateStops();
+ cursorElement.moveToFirstChunk();
+ diffElement.removeEventListener('render', setupDone);
+ done();
+ };
+ diffElement.addEventListener('render', setupDone);
+
+ restAPI.getDiffPreferences().then(prefs => {
+ diffElement.prefs = prefs;
+ diffElement.diff = mockDiffResponse.diffResponse;
+ });
+ });
+
+ teardown(() => sandbox.restore());
+
+ test('diff cursor functionality (side-by-side)', () => {
+ // The cursor has been initialized to the first delta.
+ assert.isOk(cursorElement.diffRow);
+
+ const firstDeltaRow = diffElement.shadowRoot
+ .querySelector('.section.delta .diff-row');
+ assert.equal(cursorElement.diffRow, firstDeltaRow);
+
+ cursorElement.moveDown();
+
+ assert.notEqual(cursorElement.diffRow, firstDeltaRow);
+ assert.equal(cursorElement.diffRow, firstDeltaRow.nextSibling);
+
+ cursorElement.moveUp();
+
+ assert.notEqual(cursorElement.diffRow, firstDeltaRow.nextSibling);
+ assert.equal(cursorElement.diffRow, firstDeltaRow);
+ });
+
+ test('moveToLastChunk', () => {
+ const chunks = Array.from(dom(diffElement.root).querySelectorAll(
+ '.section.delta'));
+ assert.isAbove(chunks.length, 1);
+ assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement), 0);
+
+ cursorElement.moveToLastChunk();
+
+ assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement),
+ chunks.length - 1);
+ });
+
+ test('cursor scroll behavior', () => {
+ cursorElement._handleDiffRenderStart();
+ assert.equal(cursorElement._scrollBehavior, 'keep-visible');
+ assert.isTrue(cursorElement._focusOnMove);
+
+ cursorElement._handleWindowScroll();
+ assert.equal(cursorElement._scrollBehavior, 'never');
+ assert.isFalse(cursorElement._focusOnMove);
+
+ cursorElement.handleDiffUpdate();
+ assert.equal(cursorElement._scrollBehavior, 'keep-visible');
+ assert.isTrue(cursorElement._focusOnMove);
+ });
+
+ suite('unified diff', () => {
setup(done => {
- sandbox = sinon.sandbox.create();
-
- const fixtureElems = fixture('basic');
- mockDiffResponse = fixtureElems[0];
- diffElement = fixtureElems[1];
- cursorElement = fixtureElems[2];
- const restAPI = fixtureElems[3];
-
- // Register the diff with the cursor.
- cursorElement.push('diffs', diffElement);
-
- diffElement.loggedIn = false;
- diffElement.patchRange = {basePatchNum: 1, patchNum: 2};
- diffElement.comments = {
- left: [],
- right: [],
- meta: {patchRange: undefined},
- };
- const setupDone = () => {
- cursorElement._updateStops();
- cursorElement.moveToFirstChunk();
- diffElement.removeEventListener('render', setupDone);
+ // We must allow the diff to re-render after setting the viewMode.
+ const renderHandler = function() {
+ diffElement.removeEventListener('render', renderHandler);
+ cursorElement.reInitCursor();
done();
};
- diffElement.addEventListener('render', setupDone);
-
- restAPI.getDiffPreferences().then(prefs => {
- diffElement.prefs = prefs;
- diffElement.diff = mockDiffResponse.diffResponse;
- });
+ diffElement.addEventListener('render', renderHandler);
+ diffElement.viewMode = 'UNIFIED_DIFF';
});
- teardown(() => sandbox.restore());
-
- test('diff cursor functionality (side-by-side)', () => {
+ test('diff cursor functionality (unified)', () => {
// The cursor has been initialized to the first delta.
assert.isOk(cursorElement.diffRow);
- const firstDeltaRow = diffElement.shadowRoot
+ let firstDeltaRow = diffElement.shadowRoot
+ .querySelector('.section.delta .diff-row');
+ assert.equal(cursorElement.diffRow, firstDeltaRow);
+
+ firstDeltaRow = diffElement.shadowRoot
.querySelector('.section.delta .diff-row');
assert.equal(cursorElement.diffRow, firstDeltaRow);
@@ -110,309 +167,248 @@
assert.notEqual(cursorElement.diffRow, firstDeltaRow.nextSibling);
assert.equal(cursorElement.diffRow, firstDeltaRow);
});
+ });
- test('moveToLastChunk', () => {
- const chunks = Array.from(Polymer.dom(diffElement.root).querySelectorAll(
- '.section.delta'));
- assert.isAbove(chunks.length, 1);
- assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement), 0);
+ test('cursor side functionality', () => {
+ // The side only applies to side-by-side mode, which should be the default
+ // mode.
+ assert.equal(diffElement.viewMode, 'SIDE_BY_SIDE');
- cursorElement.moveToLastChunk();
+ const firstDeltaSection = diffElement.shadowRoot
+ .querySelector('.section.delta');
+ const firstDeltaRow = firstDeltaSection.querySelector('.diff-row');
- assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement),
- chunks.length - 1);
- });
+ // Because the first delta in this diff is on the right, it should be set
+ // to the right side.
+ assert.equal(cursorElement.side, 'right');
+ assert.equal(cursorElement.diffRow, firstDeltaRow);
+ const firstIndex = cursorElement.$.cursorManager.index;
- test('cursor scroll behavior', () => {
- cursorElement._handleDiffRenderStart();
+ // Move the side to the left. Because this delta only has a right side, we
+ // should be moved up to the previous line where there is content on the
+ // right. The previous row is part of the previous section.
+ cursorElement.moveLeft();
+
+ assert.equal(cursorElement.side, 'left');
+ assert.notEqual(cursorElement.diffRow, firstDeltaRow);
+ assert.equal(cursorElement.$.cursorManager.index, firstIndex - 1);
+ assert.equal(cursorElement.diffRow.parentElement,
+ firstDeltaSection.previousSibling);
+
+ // If we move down, we should skip everything in the first delta because
+ // we are on the left side and the first delta has no content on the left.
+ cursorElement.moveDown();
+
+ assert.equal(cursorElement.side, 'left');
+ assert.notEqual(cursorElement.diffRow, firstDeltaRow);
+ assert.isTrue(cursorElement.$.cursorManager.index > firstIndex);
+ assert.equal(cursorElement.diffRow.parentElement,
+ firstDeltaSection.nextSibling);
+ });
+
+ test('chunk skip functionality', () => {
+ const chunks = dom(diffElement.root).querySelectorAll(
+ '.section.delta');
+ const indexOfChunk = function(chunk) {
+ return Array.prototype.indexOf.call(chunks, chunk);
+ };
+
+ // We should be initialized to the first chunk. Since this chunk only has
+ // content on the right side, our side should be right.
+ let currentIndex = indexOfChunk(cursorElement.diffRow.parentElement);
+ assert.equal(currentIndex, 0);
+ assert.equal(cursorElement.side, 'right');
+
+ // Move to the next chunk.
+ cursorElement.moveToNextChunk();
+
+ // Since this chunk only has content on the left side. we should have been
+ // automatically mvoed over.
+ const previousIndex = currentIndex;
+ currentIndex = indexOfChunk(cursorElement.diffRow.parentElement);
+ assert.equal(currentIndex, previousIndex + 1);
+ assert.equal(cursorElement.side, 'left');
+ });
+
+ test('initialLineNumber not provided', done => {
+ let scrollBehaviorDuringMove;
+ const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber');
+ const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk',
+ () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; });
+
+ function renderHandler() {
+ diffElement.removeEventListener('render', renderHandler);
+ assert.isFalse(moveToNumStub.called);
+ assert.isTrue(moveToChunkStub.called);
+ assert.equal(scrollBehaviorDuringMove, 'never');
assert.equal(cursorElement._scrollBehavior, 'keep-visible');
- assert.isTrue(cursorElement._focusOnMove);
+ done();
+ }
+ diffElement.addEventListener('render', renderHandler);
+ diffElement._diffChanged(mockDiffResponse.diffResponse);
+ });
- cursorElement._handleWindowScroll();
- assert.equal(cursorElement._scrollBehavior, 'never');
- assert.isFalse(cursorElement._focusOnMove);
-
- cursorElement.handleDiffUpdate();
+ test('initialLineNumber provided', done => {
+ let scrollBehaviorDuringMove;
+ const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber',
+ () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; });
+ const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk');
+ function renderHandler() {
+ diffElement.removeEventListener('render', renderHandler);
+ assert.isFalse(moveToChunkStub.called);
+ assert.isTrue(moveToNumStub.called);
+ assert.equal(moveToNumStub.lastCall.args[0], 10);
+ assert.equal(moveToNumStub.lastCall.args[1], 'right');
+ assert.equal(scrollBehaviorDuringMove, 'keep-visible');
assert.equal(cursorElement._scrollBehavior, 'keep-visible');
- assert.isTrue(cursorElement._focusOnMove);
+ done();
+ }
+ diffElement.addEventListener('render', renderHandler);
+ cursorElement.initialLineNumber = 10;
+ cursorElement.side = 'right';
+
+ diffElement._diffChanged(mockDiffResponse.diffResponse);
+ });
+
+ test('getTargetDiffElement', () => {
+ cursorElement.initialLineNumber = 1;
+ assert.isTrue(!!cursorElement.diffRow);
+ assert.equal(
+ cursorElement.getTargetDiffElement(),
+ diffElement
+ );
+ });
+
+ suite('createCommentInPlace', () => {
+ setup(() => {
+ diffElement.loggedIn = true;
});
- suite('unified diff', () => {
- setup(done => {
- // We must allow the diff to re-render after setting the viewMode.
- const renderHandler = function() {
- diffElement.removeEventListener('render', renderHandler);
- cursorElement.reInitCursor();
- done();
- };
- diffElement.addEventListener('render', renderHandler);
- diffElement.viewMode = 'UNIFIED_DIFF';
+ test('adds new draft for selected line on the left', done => {
+ cursorElement.moveToLineNumber(2, 'left');
+ diffElement.addEventListener('create-comment', e => {
+ const {lineNum, range, side, patchNum} = e.detail;
+ assert.equal(lineNum, 2);
+ assert.equal(range, undefined);
+ assert.equal(patchNum, 1);
+ assert.equal(side, 'left');
+ done();
});
+ cursorElement.createCommentInPlace();
+ });
- test('diff cursor functionality (unified)', () => {
- // The cursor has been initialized to the first delta.
- assert.isOk(cursorElement.diffRow);
-
- let firstDeltaRow = diffElement.shadowRoot
- .querySelector('.section.delta .diff-row');
- assert.equal(cursorElement.diffRow, firstDeltaRow);
-
- firstDeltaRow = diffElement.shadowRoot
- .querySelector('.section.delta .diff-row');
- assert.equal(cursorElement.diffRow, firstDeltaRow);
-
- cursorElement.moveDown();
-
- assert.notEqual(cursorElement.diffRow, firstDeltaRow);
- assert.equal(cursorElement.diffRow, firstDeltaRow.nextSibling);
-
- cursorElement.moveUp();
-
- assert.notEqual(cursorElement.diffRow, firstDeltaRow.nextSibling);
- assert.equal(cursorElement.diffRow, firstDeltaRow);
+ test('adds draft for selected line on the right', done => {
+ cursorElement.moveToLineNumber(4, 'right');
+ diffElement.addEventListener('create-comment', e => {
+ const {lineNum, range, side, patchNum} = e.detail;
+ assert.equal(lineNum, 4);
+ assert.equal(range, undefined);
+ assert.equal(patchNum, 2);
+ assert.equal(side, 'right');
+ done();
});
+ cursorElement.createCommentInPlace();
});
- test('cursor side functionality', () => {
- // The side only applies to side-by-side mode, which should be the default
- // mode.
- assert.equal(diffElement.viewMode, 'SIDE_BY_SIDE');
-
- const firstDeltaSection = diffElement.shadowRoot
- .querySelector('.section.delta');
- const firstDeltaRow = firstDeltaSection.querySelector('.diff-row');
-
- // Because the first delta in this diff is on the right, it should be set
- // to the right side.
- assert.equal(cursorElement.side, 'right');
- assert.equal(cursorElement.diffRow, firstDeltaRow);
- const firstIndex = cursorElement.$.cursorManager.index;
-
- // Move the side to the left. Because this delta only has a right side, we
- // should be moved up to the previous line where there is content on the
- // right. The previous row is part of the previous section.
- cursorElement.moveLeft();
-
- assert.equal(cursorElement.side, 'left');
- assert.notEqual(cursorElement.diffRow, firstDeltaRow);
- assert.equal(cursorElement.$.cursorManager.index, firstIndex - 1);
- assert.equal(cursorElement.diffRow.parentElement,
- firstDeltaSection.previousSibling);
-
- // If we move down, we should skip everything in the first delta because
- // we are on the left side and the first delta has no content on the left.
- cursorElement.moveDown();
-
- assert.equal(cursorElement.side, 'left');
- assert.notEqual(cursorElement.diffRow, firstDeltaRow);
- assert.isTrue(cursorElement.$.cursorManager.index > firstIndex);
- assert.equal(cursorElement.diffRow.parentElement,
- firstDeltaSection.nextSibling);
- });
-
- test('chunk skip functionality', () => {
- const chunks = Polymer.dom(diffElement.root).querySelectorAll(
- '.section.delta');
- const indexOfChunk = function(chunk) {
- return Array.prototype.indexOf.call(chunks, chunk);
+ test('createCommentInPlace creates comment for range if selected', done => {
+ const someRange = {
+ start_line: 2,
+ start_character: 3,
+ end_line: 6,
+ end_character: 1,
};
-
- // We should be initialized to the first chunk. Since this chunk only has
- // content on the right side, our side should be right.
- let currentIndex = indexOfChunk(cursorElement.diffRow.parentElement);
- assert.equal(currentIndex, 0);
- assert.equal(cursorElement.side, 'right');
-
- // Move to the next chunk.
- cursorElement.moveToNextChunk();
-
- // Since this chunk only has content on the left side. we should have been
- // automatically mvoed over.
- const previousIndex = currentIndex;
- currentIndex = indexOfChunk(cursorElement.diffRow.parentElement);
- assert.equal(currentIndex, previousIndex + 1);
- assert.equal(cursorElement.side, 'left');
- });
-
- test('initialLineNumber not provided', done => {
- let scrollBehaviorDuringMove;
- const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber');
- const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk',
- () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; });
-
- function renderHandler() {
- diffElement.removeEventListener('render', renderHandler);
- assert.isFalse(moveToNumStub.called);
- assert.isTrue(moveToChunkStub.called);
- assert.equal(scrollBehaviorDuringMove, 'never');
- assert.equal(cursorElement._scrollBehavior, 'keep-visible');
- done();
- }
- diffElement.addEventListener('render', renderHandler);
- diffElement._diffChanged(mockDiffResponse.diffResponse);
- });
-
- test('initialLineNumber provided', done => {
- let scrollBehaviorDuringMove;
- const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber',
- () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; });
- const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk');
- function renderHandler() {
- diffElement.removeEventListener('render', renderHandler);
- assert.isFalse(moveToChunkStub.called);
- assert.isTrue(moveToNumStub.called);
- assert.equal(moveToNumStub.lastCall.args[0], 10);
- assert.equal(moveToNumStub.lastCall.args[1], 'right');
- assert.equal(scrollBehaviorDuringMove, 'keep-visible');
- assert.equal(cursorElement._scrollBehavior, 'keep-visible');
- done();
- }
- diffElement.addEventListener('render', renderHandler);
- cursorElement.initialLineNumber = 10;
- cursorElement.side = 'right';
-
- diffElement._diffChanged(mockDiffResponse.diffResponse);
- });
-
- test('getTargetDiffElement', () => {
- cursorElement.initialLineNumber = 1;
- assert.isTrue(!!cursorElement.diffRow);
- assert.equal(
- cursorElement.getTargetDiffElement(),
- diffElement
- );
- });
-
- suite('createCommentInPlace', () => {
- setup(() => {
- diffElement.loggedIn = true;
- });
-
- test('adds new draft for selected line on the left', done => {
- cursorElement.moveToLineNumber(2, 'left');
- diffElement.addEventListener('create-comment', e => {
- const {lineNum, range, side, patchNum} = e.detail;
- assert.equal(lineNum, 2);
- assert.equal(range, undefined);
- assert.equal(patchNum, 1);
- assert.equal(side, 'left');
- done();
- });
- cursorElement.createCommentInPlace();
- });
-
- test('adds draft for selected line on the right', done => {
- cursorElement.moveToLineNumber(4, 'right');
- diffElement.addEventListener('create-comment', e => {
- const {lineNum, range, side, patchNum} = e.detail;
- assert.equal(lineNum, 4);
- assert.equal(range, undefined);
- assert.equal(patchNum, 2);
- assert.equal(side, 'right');
- done();
- });
- cursorElement.createCommentInPlace();
- });
-
- test('createCommentInPlace creates comment for range if selected', done => {
- const someRange = {
- start_line: 2,
- start_character: 3,
- end_line: 6,
- end_character: 1,
- };
- diffElement.$.highlights.selectedRange = {
- side: 'right',
- range: someRange,
- };
- diffElement.addEventListener('create-comment', e => {
- const {lineNum, range, side, patchNum} = e.detail;
- assert.equal(lineNum, 6);
- assert.equal(range, someRange);
- assert.equal(patchNum, 2);
- assert.equal(side, 'right');
- done();
- });
- cursorElement.createCommentInPlace();
- });
-
- test('createCommentInPlace ignores call if nothing is selected', () => {
- const createRangeCommentStub = sandbox.stub(diffElement,
- 'createRangeComment');
- const addDraftAtLineStub = sandbox.stub(diffElement, 'addDraftAtLine');
- cursorElement.diffRow = undefined;
- cursorElement.createCommentInPlace();
- assert.isFalse(createRangeCommentStub.called);
- assert.isFalse(addDraftAtLineStub.called);
- });
- });
-
- test('getAddress', () => {
- // It should initialize to the first chunk: line 5 of the revision.
- assert.deepEqual(cursorElement.getAddress(),
- {leftSide: false, number: 5});
-
- // Revision line 4 is up.
- cursorElement.moveUp();
- assert.deepEqual(cursorElement.getAddress(),
- {leftSide: false, number: 4});
-
- // Base line 4 is left.
- cursorElement.moveLeft();
- assert.deepEqual(cursorElement.getAddress(), {leftSide: true, number: 4});
-
- // Moving to the next chunk takes it back to the start.
- cursorElement.moveToNextChunk();
- assert.deepEqual(cursorElement.getAddress(),
- {leftSide: false, number: 5});
-
- // The following chunk is a removal starting on line 10 of the base.
- cursorElement.moveToNextChunk();
- assert.deepEqual(cursorElement.getAddress(),
- {leftSide: true, number: 10});
-
- // Should be null if there is no selection.
- cursorElement.$.cursorManager.unsetCursor();
- assert.isNotOk(cursorElement.getAddress());
- });
-
- test('_findRowByNumberAndFile', () => {
- // Get the first ab row after the first chunk.
- const row = Polymer.dom(diffElement.root).querySelectorAll('tr')[8];
-
- // It should be line 8 on the right, but line 5 on the left.
- assert.equal(cursorElement._findRowByNumberAndFile(8, 'right'), row);
- assert.equal(cursorElement._findRowByNumberAndFile(5, 'left'), row);
- });
-
- test('expand context updates stops', done => {
- sandbox.spy(cursorElement, 'handleDiffUpdate');
- MockInteractions.tap(diffElement.shadowRoot
- .querySelector('.showContext'));
- flush(() => {
- assert.isTrue(cursorElement.handleDiffUpdate.called);
+ diffElement.$.highlights.selectedRange = {
+ side: 'right',
+ range: someRange,
+ };
+ diffElement.addEventListener('create-comment', e => {
+ const {lineNum, range, side, patchNum} = e.detail;
+ assert.equal(lineNum, 6);
+ assert.equal(range, someRange);
+ assert.equal(patchNum, 2);
+ assert.equal(side, 'right');
done();
});
+ cursorElement.createCommentInPlace();
});
- suite('gr-diff-cursor event tests', () => {
- let sandbox;
- let someEmptyDiv;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- someEmptyDiv = fixture('empty');
- });
-
- teardown(() => sandbox.restore());
-
- test('ready is fired after component is rendered', done => {
- const cursorElement = document.createElement('gr-diff-cursor');
- cursorElement.addEventListener('ready', () => {
- done();
- });
- someEmptyDiv.appendChild(cursorElement);
- });
+ test('createCommentInPlace ignores call if nothing is selected', () => {
+ const createRangeCommentStub = sandbox.stub(diffElement,
+ 'createRangeComment');
+ const addDraftAtLineStub = sandbox.stub(diffElement, 'addDraftAtLine');
+ cursorElement.diffRow = undefined;
+ cursorElement.createCommentInPlace();
+ assert.isFalse(createRangeCommentStub.called);
+ assert.isFalse(addDraftAtLineStub.called);
});
});
+
+ test('getAddress', () => {
+ // It should initialize to the first chunk: line 5 of the revision.
+ assert.deepEqual(cursorElement.getAddress(),
+ {leftSide: false, number: 5});
+
+ // Revision line 4 is up.
+ cursorElement.moveUp();
+ assert.deepEqual(cursorElement.getAddress(),
+ {leftSide: false, number: 4});
+
+ // Base line 4 is left.
+ cursorElement.moveLeft();
+ assert.deepEqual(cursorElement.getAddress(), {leftSide: true, number: 4});
+
+ // Moving to the next chunk takes it back to the start.
+ cursorElement.moveToNextChunk();
+ assert.deepEqual(cursorElement.getAddress(),
+ {leftSide: false, number: 5});
+
+ // The following chunk is a removal starting on line 10 of the base.
+ cursorElement.moveToNextChunk();
+ assert.deepEqual(cursorElement.getAddress(),
+ {leftSide: true, number: 10});
+
+ // Should be null if there is no selection.
+ cursorElement.$.cursorManager.unsetCursor();
+ assert.isNotOk(cursorElement.getAddress());
+ });
+
+ test('_findRowByNumberAndFile', () => {
+ // Get the first ab row after the first chunk.
+ const row = dom(diffElement.root).querySelectorAll('tr')[8];
+
+ // It should be line 8 on the right, but line 5 on the left.
+ assert.equal(cursorElement._findRowByNumberAndFile(8, 'right'), row);
+ assert.equal(cursorElement._findRowByNumberAndFile(5, 'left'), row);
+ });
+
+ test('expand context updates stops', done => {
+ sandbox.spy(cursorElement, 'handleDiffUpdate');
+ MockInteractions.tap(diffElement.shadowRoot
+ .querySelector('.showContext'));
+ flush(() => {
+ assert.isTrue(cursorElement.handleDiffUpdate.called);
+ done();
+ });
+ });
+
+ suite('gr-diff-cursor event tests', () => {
+ let sandbox;
+ let someEmptyDiv;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ someEmptyDiv = fixture('empty');
+ });
+
+ teardown(() => sandbox.restore());
+
+ test('ready is fired after component is rendered', done => {
+ const cursorElement = document.createElement('gr-diff-cursor');
+ cursorElement.addEventListener('ready', () => {
+ done();
+ });
+ someEmptyDiv.appendChild(cursorElement);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
index 79e4036..357c831 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
@@ -19,16 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-annotation</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="gr-annotation.js"></script>
-
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,266 +30,268 @@
</template>
</test-fixture>
-<script>
- suite('annotation', async () => {
- await readyToTest();
- let str;
- let parent;
- let textNode;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-annotation.js';
+import {sanitizeDOMValue, setSanitizeDOMValue} from '@polymer/polymer/lib/utils/settings.js';
+suite('annotation', () => {
+ let str;
+ let parent;
+ let textNode;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ parent = fixture('basic');
+ textNode = parent.childNodes[0];
+ str = textNode.textContent;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_annotateText Case 1', () => {
+ GrAnnotation._annotateText(textNode, 0, str.length, 'foobar');
+
+ assert.equal(parent.childNodes.length, 1);
+ assert.instanceOf(parent.childNodes[0], HTMLElement);
+ assert.equal(parent.childNodes[0].className, 'foobar');
+ assert.instanceOf(parent.childNodes[0].childNodes[0], Text);
+ assert.equal(parent.childNodes[0].childNodes[0].textContent, str);
+ });
+
+ test('_annotateText Case 2', () => {
+ const length = 12;
+ const substr = str.substr(0, length);
+ const remainder = str.substr(length);
+
+ GrAnnotation._annotateText(textNode, 0, length, 'foobar');
+
+ assert.equal(parent.childNodes.length, 2);
+
+ assert.instanceOf(parent.childNodes[0], HTMLElement);
+ assert.equal(parent.childNodes[0].className, 'foobar');
+ assert.instanceOf(parent.childNodes[0].childNodes[0], Text);
+ assert.equal(parent.childNodes[0].childNodes[0].textContent, substr);
+
+ assert.instanceOf(parent.childNodes[1], Text);
+ assert.equal(parent.childNodes[1].textContent, remainder);
+ });
+
+ test('_annotateText Case 3', () => {
+ const index = 12;
+ const length = str.length - index;
+ const remainder = str.substr(0, index);
+ const substr = str.substr(index);
+
+ GrAnnotation._annotateText(textNode, index, length, 'foobar');
+
+ assert.equal(parent.childNodes.length, 2);
+
+ assert.instanceOf(parent.childNodes[0], Text);
+ assert.equal(parent.childNodes[0].textContent, remainder);
+
+ assert.instanceOf(parent.childNodes[1], HTMLElement);
+ assert.equal(parent.childNodes[1].className, 'foobar');
+ assert.instanceOf(parent.childNodes[1].childNodes[0], Text);
+ assert.equal(parent.childNodes[1].childNodes[0].textContent, substr);
+ });
+
+ test('_annotateText Case 4', () => {
+ const index = str.indexOf('dolor');
+ const length = 'dolor '.length;
+
+ const remainderPre = str.substr(0, index);
+ const substr = str.substr(index, length);
+ const remainderPost = str.substr(index + length);
+
+ GrAnnotation._annotateText(textNode, index, length, 'foobar');
+
+ assert.equal(parent.childNodes.length, 3);
+
+ assert.instanceOf(parent.childNodes[0], Text);
+ assert.equal(parent.childNodes[0].textContent, remainderPre);
+
+ assert.instanceOf(parent.childNodes[1], HTMLElement);
+ assert.equal(parent.childNodes[1].className, 'foobar');
+ assert.instanceOf(parent.childNodes[1].childNodes[0], Text);
+ assert.equal(parent.childNodes[1].childNodes[0].textContent, substr);
+
+ assert.instanceOf(parent.childNodes[2], Text);
+ assert.equal(parent.childNodes[2].textContent, remainderPost);
+ });
+
+ test('_annotateElement design doc example', () => {
+ const layers = [
+ 'amet, ',
+ 'inceptos ',
+ 'amet, ',
+ 'et, suspendisse ince',
+ ];
+
+ // Apply the layers successively.
+ layers.forEach((layer, i) => {
+ GrAnnotation.annotateElement(
+ parent, str.indexOf(layer), layer.length, `layer-${i + 1}`);
+ });
+
+ assert.equal(parent.textContent, str);
+
+ // Layer 1:
+ const layer1 = parent.querySelectorAll('.layer-1');
+ assert.equal(layer1.length, 1);
+ assert.equal(layer1[0].textContent, layers[0]);
+ assert.equal(layer1[0].parentElement, parent);
+
+ // Layer 2:
+ const layer2 = parent.querySelectorAll('.layer-2');
+ assert.equal(layer2.length, 1);
+ assert.equal(layer2[0].textContent, layers[1]);
+ assert.equal(layer2[0].parentElement, parent);
+
+ // Layer 3:
+ const layer3 = parent.querySelectorAll('.layer-3');
+ assert.equal(layer3.length, 1);
+ assert.equal(layer3[0].textContent, layers[2]);
+ assert.equal(layer3[0].parentElement, layer1[0]);
+
+ // Layer 4:
+ const layer4 = parent.querySelectorAll('.layer-4');
+ assert.equal(layer4.length, 3);
+
+ assert.equal(layer4[0].textContent, 'et, ');
+ assert.equal(layer4[0].parentElement, layer3[0]);
+
+ assert.equal(layer4[1].textContent, 'suspendisse ');
+ assert.equal(layer4[1].parentElement, parent);
+
+ assert.equal(layer4[2].textContent, 'ince');
+ assert.equal(layer4[2].parentElement, layer2[0]);
+
+ assert.equal(layer4[0].textContent +
+ layer4[1].textContent +
+ layer4[2].textContent,
+ layers[3]);
+ });
+
+ test('splitTextNode', () => {
+ const helloString = 'hello';
+ const asciiString = 'ASCII';
+ const unicodeString = 'Unic💢de';
+
+ let node;
+ let tail;
+
+ // Non-unicode path:
+ node = document.createTextNode(helloString + asciiString);
+ tail = GrAnnotation.splitTextNode(node, helloString.length);
+ assert(node.textContent, helloString);
+ assert(tail.textContent, asciiString);
+
+ // Unicdoe path:
+ node = document.createTextNode(helloString + unicodeString);
+ tail = GrAnnotation.splitTextNode(node, helloString.length);
+ assert(node.textContent, helloString);
+ assert(tail.textContent, unicodeString);
+ });
+
+ suite('annotateWithElement', () => {
+ const fullText = '01234567890123456789';
+ let mockSanitize;
+ let originalSanitizeDOMValue;
setup(() => {
- sandbox = sinon.sandbox.create();
- parent = fixture('basic');
- textNode = parent.childNodes[0];
- str = textNode.textContent;
+ originalSanitizeDOMValue = sanitizeDOMValue;
+ assert.isDefined(originalSanitizeDOMValue);
+ mockSanitize = sandbox.spy(originalSanitizeDOMValue);
+ setSanitizeDOMValue(mockSanitize);
});
teardown(() => {
- sandbox.restore();
+ setSanitizeDOMValue(originalSanitizeDOMValue);
});
- test('_annotateText Case 1', () => {
- GrAnnotation._annotateText(textNode, 0, str.length, 'foobar');
+ test('annotates when fully contained', () => {
+ const length = 10;
+ const container = document.createElement('div');
+ container.textContent = fullText;
+ GrAnnotation.annotateWithElement(
+ container, 1, length, {tagName: 'test-wrapper'});
- assert.equal(parent.childNodes.length, 1);
- assert.instanceOf(parent.childNodes[0], HTMLElement);
- assert.equal(parent.childNodes[0].className, 'foobar');
- assert.instanceOf(parent.childNodes[0].childNodes[0], Text);
- assert.equal(parent.childNodes[0].childNodes[0].textContent, str);
+ assert.equal(
+ container.innerHTML,
+ '0<test-wrapper>1234567890</test-wrapper>123456789');
});
- test('_annotateText Case 2', () => {
- const length = 12;
- const substr = str.substr(0, length);
- const remainder = str.substr(length);
+ test('annotates when spanning multiple nodes', () => {
+ const length = 10;
+ const container = document.createElement('div');
+ container.textContent = fullText;
+ GrAnnotation.annotateElement(container, 5, length, 'testclass');
+ GrAnnotation.annotateWithElement(
+ container, 1, length, {tagName: 'test-wrapper'});
- GrAnnotation._annotateText(textNode, 0, length, 'foobar');
-
- assert.equal(parent.childNodes.length, 2);
-
- assert.instanceOf(parent.childNodes[0], HTMLElement);
- assert.equal(parent.childNodes[0].className, 'foobar');
- assert.instanceOf(parent.childNodes[0].childNodes[0], Text);
- assert.equal(parent.childNodes[0].childNodes[0].textContent, substr);
-
- assert.instanceOf(parent.childNodes[1], Text);
- assert.equal(parent.childNodes[1].textContent, remainder);
+ assert.equal(
+ container.innerHTML,
+ '0' +
+ '<test-wrapper>' +
+ '1234' +
+ '<hl class="testclass">567890</hl>' +
+ '</test-wrapper>' +
+ '<hl class="testclass">1234</hl>' +
+ '56789');
});
- test('_annotateText Case 3', () => {
- const index = 12;
- const length = str.length - index;
- const remainder = str.substr(0, index);
- const substr = str.substr(index);
+ test('annotates text node', () => {
+ const length = 10;
+ const container = document.createElement('div');
+ container.textContent = fullText;
+ GrAnnotation.annotateWithElement(
+ container.childNodes[0], 1, length, {tagName: 'test-wrapper'});
- GrAnnotation._annotateText(textNode, index, length, 'foobar');
-
- assert.equal(parent.childNodes.length, 2);
-
- assert.instanceOf(parent.childNodes[0], Text);
- assert.equal(parent.childNodes[0].textContent, remainder);
-
- assert.instanceOf(parent.childNodes[1], HTMLElement);
- assert.equal(parent.childNodes[1].className, 'foobar');
- assert.instanceOf(parent.childNodes[1].childNodes[0], Text);
- assert.equal(parent.childNodes[1].childNodes[0].textContent, substr);
+ assert.equal(
+ container.innerHTML,
+ '0<test-wrapper>1234567890</test-wrapper>123456789');
});
- test('_annotateText Case 4', () => {
- const index = str.indexOf('dolor');
- const length = 'dolor '.length;
+ test('handles zero-length nodes', () => {
+ const container = document.createElement('div');
+ container.appendChild(document.createTextNode('0123456789'));
+ container.appendChild(document.createElement('span'));
+ container.appendChild(document.createTextNode('0123456789'));
+ GrAnnotation.annotateWithElement(
+ container, 1, 10, {tagName: 'test-wrapper'});
- const remainderPre = str.substr(0, index);
- const substr = str.substr(index, length);
- const remainderPost = str.substr(index + length);
-
- GrAnnotation._annotateText(textNode, index, length, 'foobar');
-
- assert.equal(parent.childNodes.length, 3);
-
- assert.instanceOf(parent.childNodes[0], Text);
- assert.equal(parent.childNodes[0].textContent, remainderPre);
-
- assert.instanceOf(parent.childNodes[1], HTMLElement);
- assert.equal(parent.childNodes[1].className, 'foobar');
- assert.instanceOf(parent.childNodes[1].childNodes[0], Text);
- assert.equal(parent.childNodes[1].childNodes[0].textContent, substr);
-
- assert.instanceOf(parent.childNodes[2], Text);
- assert.equal(parent.childNodes[2].textContent, remainderPost);
+ assert.equal(
+ container.innerHTML,
+ '0<test-wrapper>123456789<span></span>0</test-wrapper>123456789');
});
- test('_annotateElement design doc example', () => {
- const layers = [
- 'amet, ',
- 'inceptos ',
- 'amet, ',
- 'et, suspendisse ince',
- ];
-
- // Apply the layers successively.
- layers.forEach((layer, i) => {
- GrAnnotation.annotateElement(
- parent, str.indexOf(layer), layer.length, `layer-${i + 1}`);
- });
-
- assert.equal(parent.textContent, str);
-
- // Layer 1:
- const layer1 = parent.querySelectorAll('.layer-1');
- assert.equal(layer1.length, 1);
- assert.equal(layer1[0].textContent, layers[0]);
- assert.equal(layer1[0].parentElement, parent);
-
- // Layer 2:
- const layer2 = parent.querySelectorAll('.layer-2');
- assert.equal(layer2.length, 1);
- assert.equal(layer2[0].textContent, layers[1]);
- assert.equal(layer2[0].parentElement, parent);
-
- // Layer 3:
- const layer3 = parent.querySelectorAll('.layer-3');
- assert.equal(layer3.length, 1);
- assert.equal(layer3[0].textContent, layers[2]);
- assert.equal(layer3[0].parentElement, layer1[0]);
-
- // Layer 4:
- const layer4 = parent.querySelectorAll('.layer-4');
- assert.equal(layer4.length, 3);
-
- assert.equal(layer4[0].textContent, 'et, ');
- assert.equal(layer4[0].parentElement, layer3[0]);
-
- assert.equal(layer4[1].textContent, 'suspendisse ');
- assert.equal(layer4[1].parentElement, parent);
-
- assert.equal(layer4[2].textContent, 'ince');
- assert.equal(layer4[2].parentElement, layer2[0]);
-
- assert.equal(layer4[0].textContent +
- layer4[1].textContent +
- layer4[2].textContent,
- layers[3]);
- });
-
- test('splitTextNode', () => {
- const helloString = 'hello';
- const asciiString = 'ASCII';
- const unicodeString = 'Unic💢de';
-
- let node;
- let tail;
-
- // Non-unicode path:
- node = document.createTextNode(helloString + asciiString);
- tail = GrAnnotation.splitTextNode(node, helloString.length);
- assert(node.textContent, helloString);
- assert(tail.textContent, asciiString);
-
- // Unicdoe path:
- node = document.createTextNode(helloString + unicodeString);
- tail = GrAnnotation.splitTextNode(node, helloString.length);
- assert(node.textContent, helloString);
- assert(tail.textContent, unicodeString);
- });
-
- suite('annotateWithElement', () => {
- const fullText = '01234567890123456789';
- let mockSanitize;
- let originalSanitizeDOMValue;
-
- setup(() => {
- originalSanitizeDOMValue = window.Polymer.sanitizeDOMValue;
- assert.isDefined(originalSanitizeDOMValue);
- mockSanitize = sandbox.spy(originalSanitizeDOMValue);
- window.Polymer.sanitizeDOMValue = mockSanitize;
- });
-
- teardown(() => {
- window.Polymer.sanitizeDOMValue = originalSanitizeDOMValue;
- });
-
- test('annotates when fully contained', () => {
- const length = 10;
- const container = document.createElement('div');
- container.textContent = fullText;
- GrAnnotation.annotateWithElement(
- container, 1, length, {tagName: 'test-wrapper'});
-
- assert.equal(
- container.innerHTML,
- '0<test-wrapper>1234567890</test-wrapper>123456789');
- });
-
- test('annotates when spanning multiple nodes', () => {
- const length = 10;
- const container = document.createElement('div');
- container.textContent = fullText;
- GrAnnotation.annotateElement(container, 5, length, 'testclass');
- GrAnnotation.annotateWithElement(
- container, 1, length, {tagName: 'test-wrapper'});
-
- assert.equal(
- container.innerHTML,
- '0' +
- '<test-wrapper>' +
- '1234' +
- '<hl class="testclass">567890</hl>' +
- '</test-wrapper>' +
- '<hl class="testclass">1234</hl>' +
- '56789');
- });
-
- test('annotates text node', () => {
- const length = 10;
- const container = document.createElement('div');
- container.textContent = fullText;
- GrAnnotation.annotateWithElement(
- container.childNodes[0], 1, length, {tagName: 'test-wrapper'});
-
- assert.equal(
- container.innerHTML,
- '0<test-wrapper>1234567890</test-wrapper>123456789');
- });
-
- test('handles zero-length nodes', () => {
- const container = document.createElement('div');
- container.appendChild(document.createTextNode('0123456789'));
- container.appendChild(document.createElement('span'));
- container.appendChild(document.createTextNode('0123456789'));
- GrAnnotation.annotateWithElement(
- container, 1, 10, {tagName: 'test-wrapper'});
-
- assert.equal(
- container.innerHTML,
- '0<test-wrapper>123456789<span></span>0</test-wrapper>123456789');
- });
-
- test('sets sanitized attributes', () => {
- const container = document.createElement('div');
- container.textContent = fullText;
- const attributes = {
- 'href': 'foo',
- 'data-foo': 'bar',
- 'class': 'hello world',
- };
- GrAnnotation.annotateWithElement(
- container, 1, length, {tagName: 'test-wrapper', attributes});
- assert(mockSanitize.calledWith(
- 'foo', 'href', 'attribute', sinon.match.instanceOf(Element)));
- assert(mockSanitize.calledWith(
- 'bar', 'data-foo', 'attribute', sinon.match.instanceOf(Element)));
- assert(mockSanitize.calledWith(
- 'hello world',
- 'class',
- 'attribute',
- sinon.match.instanceOf(Element)));
- const el = container.querySelector('test-wrapper');
- assert.equal(el.getAttribute('href'), 'foo');
- assert.equal(el.getAttribute('data-foo'), 'bar');
- assert.equal(el.getAttribute('class'), 'hello world');
- });
+ test('sets sanitized attributes', () => {
+ const container = document.createElement('div');
+ container.textContent = fullText;
+ const attributes = {
+ 'href': 'foo',
+ 'data-foo': 'bar',
+ 'class': 'hello world',
+ };
+ GrAnnotation.annotateWithElement(
+ container, 1, length, {tagName: 'test-wrapper', attributes});
+ assert(mockSanitize.calledWith(
+ 'foo', 'href', 'attribute', sinon.match.instanceOf(Element)));
+ assert(mockSanitize.calledWith(
+ 'bar', 'data-foo', 'attribute', sinon.match.instanceOf(Element)));
+ assert(mockSanitize.calledWith(
+ 'hello world',
+ 'class',
+ 'attribute',
+ sinon.match.instanceOf(Element)));
+ const el = container.querySelector('test-wrapper');
+ assert.equal(el.getAttribute('href'), 'foo');
+ assert.equal(el.getAttribute('data-foo'), 'bar');
+ assert.equal(el.getAttribute('class'), 'hello world');
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
deleted file mode 100644
index 3b17190..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
+++ /dev/null
@@ -1,44 +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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../gr-selection-action-box/gr-selection-action-box.html">
-
-<dom-module id="gr-diff-highlight">
- <template>
- <style include="shared-styles">
- :host {
- position: relative;
- }
- gr-selection-action-box {
- /**
- * Needs z-index to apear above wrapped content, since it's inseted
- * into DOM before it.
- */
- z-index: 10;
- }
- </style>
- <div class="contentWrapper">
- <slot></slot>
- </div>
- </template>
- <script src="gr-annotation.js"></script>
- <script src="gr-range-normalizer.js"></script>
- <script src="gr-diff-highlight.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index 99bf1c8..4567c9e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -14,484 +14,496 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../gr-selection-action-box/gr-selection-action-box.js';
+import './gr-annotation.js';
+import './gr-range-normalizer.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-diff-highlight_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrDiffHighlight extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-diff-highlight'; }
+
+ static get properties() {
+ return {
+ /** @type {!Array<!Gerrit.HoveredRange>} */
+ commentRanges: {
+ type: Array,
+ notify: true,
+ },
+ loggedIn: Boolean,
+ /**
+ * querySelector can return null, so needs to be nullable.
+ *
+ * @type {?HTMLElement}
+ * */
+ _cachedDiffBuilder: Object,
+
+ /**
+ * Which range is currently selected by the user.
+ * Stored in order to add a range-based comment
+ * later.
+ * undefined if no range is selected.
+ *
+ * @type {{side: string, range: Gerrit.Range}|undefined}
+ */
+ selectedRange: {
+ type: Object,
+ notify: true,
+ },
+ };
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('comment-thread-mouseleave',
+ e => this._handleCommentThreadMouseleave(e));
+ this.addEventListener('comment-thread-mouseenter',
+ e => this._handleCommentThreadMouseenter(e));
+ this.addEventListener('create-comment-requested',
+ e => this._handleRangeCommentRequest(e));
+ }
+
+ get diffBuilder() {
+ if (!this._cachedDiffBuilder) {
+ this._cachedDiffBuilder =
+ dom(this).querySelector('gr-diff-builder');
+ }
+ return this._cachedDiffBuilder;
+ }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Determines side/line/range for a DOM selection and shows a tooltip.
+ *
+ * With native shadow DOM, gr-diff-highlight cannot access a selection that
+ * references the DOM elements making up the diff because they are in the
+ * shadow DOM the gr-diff element. For this reason, we listen to the
+ * selectionchange event and retrieve the selection in gr-diff, and then
+ * call this method to process the Selection.
+ *
+ * @param {Selection} selection A DOM Selection living in the shadow DOM of
+ * the diff element.
+ * @param {boolean} isMouseUp If true, this is called due to a mouseup
+ * event, in which case we might want to immediately create a comment,
+ * because isMouseUp === true combined with an existing selection must
+ * mean that this is the end of a double-click.
*/
- class GrDiffHighlight extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-diff-highlight'; }
+ handleSelectionChange(selection, isMouseUp) {
+ // Debounce is not just nice for waiting until the selection has settled,
+ // it is also vital for being able to click on the action box before it is
+ // removed.
+ // If you wait longer than 50 ms, then you don't properly catch a very
+ // quick 'c' press after the selection change. If you wait less than 10
+ // ms, then you will have about 50 _handleSelection calls when doing a
+ // simple drag for select.
+ this.debounce(
+ 'selectionChange', () => this._handleSelection(selection, isMouseUp),
+ 10);
+ }
- static get properties() {
- return {
- /** @type {!Array<!Gerrit.HoveredRange>} */
- commentRanges: {
- type: Array,
- notify: true,
- },
- loggedIn: Boolean,
- /**
- * querySelector can return null, so needs to be nullable.
- *
- * @type {?HTMLElement}
- * */
- _cachedDiffBuilder: Object,
-
- /**
- * Which range is currently selected by the user.
- * Stored in order to add a range-based comment
- * later.
- * undefined if no range is selected.
- *
- * @type {{side: string, range: Gerrit.Range}|undefined}
- */
- selectedRange: {
- type: Object,
- notify: true,
- },
- };
+ _getThreadEl(e) {
+ const path = dom(e).path || [];
+ for (const pathEl of path) {
+ if (pathEl.classList.contains('comment-thread')) return pathEl;
}
+ return null;
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('comment-thread-mouseleave',
- e => this._handleCommentThreadMouseleave(e));
- this.addEventListener('comment-thread-mouseenter',
- e => this._handleCommentThreadMouseenter(e));
- this.addEventListener('create-comment-requested',
- e => this._handleRangeCommentRequest(e));
- }
+ _handleCommentThreadMouseenter(e) {
+ const threadEl = this._getThreadEl(e);
+ const index = this._indexForThreadEl(threadEl);
- get diffBuilder() {
- if (!this._cachedDiffBuilder) {
- this._cachedDiffBuilder =
- Polymer.dom(this).querySelector('gr-diff-builder');
- }
- return this._cachedDiffBuilder;
- }
-
- /**
- * Determines side/line/range for a DOM selection and shows a tooltip.
- *
- * With native shadow DOM, gr-diff-highlight cannot access a selection that
- * references the DOM elements making up the diff because they are in the
- * shadow DOM the gr-diff element. For this reason, we listen to the
- * selectionchange event and retrieve the selection in gr-diff, and then
- * call this method to process the Selection.
- *
- * @param {Selection} selection A DOM Selection living in the shadow DOM of
- * the diff element.
- * @param {boolean} isMouseUp If true, this is called due to a mouseup
- * event, in which case we might want to immediately create a comment,
- * because isMouseUp === true combined with an existing selection must
- * mean that this is the end of a double-click.
- */
- handleSelectionChange(selection, isMouseUp) {
- // Debounce is not just nice for waiting until the selection has settled,
- // it is also vital for being able to click on the action box before it is
- // removed.
- // If you wait longer than 50 ms, then you don't properly catch a very
- // quick 'c' press after the selection change. If you wait less than 10
- // ms, then you will have about 50 _handleSelection calls when doing a
- // simple drag for select.
- this.debounce(
- 'selectionChange', () => this._handleSelection(selection, isMouseUp),
- 10);
- }
-
- _getThreadEl(e) {
- const path = Polymer.dom(e).path || [];
- for (const pathEl of path) {
- if (pathEl.classList.contains('comment-thread')) return pathEl;
- }
- return null;
- }
-
- _handleCommentThreadMouseenter(e) {
- const threadEl = this._getThreadEl(e);
- const index = this._indexForThreadEl(threadEl);
-
- if (index !== undefined) {
- this.set(['commentRanges', index, 'hovering'], true);
- }
- }
-
- _handleCommentThreadMouseleave(e) {
- const threadEl = this._getThreadEl(e);
- const index = this._indexForThreadEl(threadEl);
-
- if (index !== undefined) {
- this.set(['commentRanges', index, 'hovering'], false);
- }
- }
-
- _indexForThreadEl(threadEl) {
- const side = threadEl.getAttribute('comment-side');
- const range = JSON.parse(threadEl.getAttribute('range'));
-
- if (!range) return undefined;
-
- return this._indexOfCommentRange(side, range);
- }
-
- _indexOfCommentRange(side, range) {
- function rangesEqual(a, b) {
- if (!a && !b) {
- return true;
- }
- if (!a || !b) {
- return false;
- }
- return a.start_line === b.start_line &&
- a.start_character === b.start_character &&
- a.end_line === b.end_line &&
- a.end_character === b.end_character;
- }
-
- return this.commentRanges.findIndex(commentRange =>
- commentRange.side === side && rangesEqual(commentRange.range, range));
- }
-
- /**
- * Get current normalized selection.
- * Merges multiple ranges, accounts for triple click, accounts for
- * syntax highligh, convert native DOM Range objects to Gerrit concepts
- * (line, side, etc).
- *
- * @param {Selection} selection
- * @return {({
- * start: {
- * node: Node,
- * side: string,
- * line: Number,
- * column: Number
- * },
- * end: {
- * node: Node,
- * side: string,
- * line: Number,
- * column: Number
- * }
- * })|null|!Object}
- */
- _getNormalizedRange(selection) {
- const rangeCount = selection.rangeCount;
- if (rangeCount === 0) {
- return null;
- } else if (rangeCount === 1) {
- return this._normalizeRange(selection.getRangeAt(0));
- } else {
- const startRange = this._normalizeRange(selection.getRangeAt(0));
- const endRange = this._normalizeRange(
- selection.getRangeAt(rangeCount - 1));
- return {
- start: startRange.start,
- end: endRange.end,
- };
- }
- }
-
- /**
- * Normalize a specific DOM Range.
- *
- * @return {!Object} fixed normalized range
- */
- _normalizeRange(domRange) {
- const range = GrRangeNormalizer.normalize(domRange);
- return this._fixTripleClickSelection({
- start: this._normalizeSelectionSide(
- range.startContainer, range.startOffset),
- end: this._normalizeSelectionSide(
- range.endContainer, range.endOffset),
- }, domRange);
- }
-
- /**
- * Adjust triple click selection for the whole line.
- * A triple click always results in:
- * - start.column == end.column == 0
- * - end.line == start.line + 1
- *
- * @param {!Object} range Normalized range, ie column/line numbers
- * @param {!Range} domRange DOM Range object
- * @return {!Object} fixed normalized range
- */
- _fixTripleClickSelection(range, domRange) {
- if (!range.start) {
- // Selection outside of current diff.
- return range;
- }
- const start = range.start;
- const end = range.end;
- // Happens when triple click in side-by-side mode with other side empty.
- const endsAtOtherEmptySide = !end &&
- domRange.endOffset === 0 &&
- domRange.endContainer.nodeName === 'TD' &&
- (domRange.endContainer.classList.contains('left') ||
- domRange.endContainer.classList.contains('right'));
- const endsAtBeginningOfNextLine = end &&
- start.column === 0 &&
- end.column === 0 &&
- end.line === start.line + 1;
- const content = domRange.cloneContents().querySelector('.contentText');
- const lineLength = content && this._getLength(content) || 0;
- if (lineLength && (endsAtBeginningOfNextLine || endsAtOtherEmptySide)) {
- // Move the selection to the end of the previous line.
- range.end = {
- node: start.node,
- column: lineLength,
- side: start.side,
- line: start.line,
- };
- }
- return range;
- }
-
- /**
- * Convert DOM Range selection to concrete numbers (line, column, side).
- * Moves range end if it's not inside td.content.
- * Returns null if selection end is not valid (outside of diff).
- *
- * @param {Node} node td.content child
- * @param {number} offset offset within node
- * @return {({
- * node: Node,
- * side: string,
- * line: Number,
- * column: Number
- * }|undefined)}
- */
- _normalizeSelectionSide(node, offset) {
- let column;
- if (!this.contains(node)) {
- return;
- }
- const lineEl = this.diffBuilder.getLineElByChild(node);
- if (!lineEl) {
- return;
- }
- const side = this.diffBuilder.getSideByLineEl(lineEl);
- if (!side) {
- return;
- }
- const line = this.diffBuilder.getLineNumberByChild(lineEl);
- if (!line) {
- return;
- }
- const contentText = this.diffBuilder.getContentByLineEl(lineEl);
- if (!contentText) {
- return;
- }
- const contentTd = contentText.parentElement;
- if (!contentTd.contains(node)) {
- node = contentText;
- column = 0;
- } else {
- const thread = contentTd.querySelector('.comment-thread');
- if (thread && thread.contains(node)) {
- column = this._getLength(contentText);
- node = contentText;
- } else {
- column = this._convertOffsetToColumn(node, offset);
- }
- }
-
- return {
- node,
- side,
- line,
- column,
- };
- }
-
- /**
- * The only line in which add a comment tooltip is cut off is the first
- * line. Even if there is a collapsed section, The first visible line is
- * in the position where the second line would have been, if not for the
- * collapsed section, so don't need to worry about this case for
- * positioning the tooltip.
- */
- _positionActionBox(actionBox, startLine, range) {
- if (startLine > 1) {
- actionBox.placeAbove(range);
- return;
- }
- actionBox.positionBelow = true;
- actionBox.placeBelow(range);
- }
-
- _isRangeValid(range) {
- if (!range || !range.start || !range.end) {
- return false;
- }
- const start = range.start;
- const end = range.end;
- if (start.side !== end.side ||
- end.line < start.line ||
- (start.line === end.line && start.column === end.column)) {
- return false;
- }
- return true;
- }
-
- _handleSelection(selection, isMouseUp) {
- const normalizedRange = this._getNormalizedRange(selection);
- if (!this._isRangeValid(normalizedRange)) {
- this._removeActionBox();
- return;
- }
- const domRange = selection.getRangeAt(0);
- const start = normalizedRange.start;
- const end = normalizedRange.end;
-
- // TODO (viktard): Drop empty first and last lines from selection.
-
- // If the selection is from the end of one line to the start of the next
- // line, then this must have been a double-click, or you have started
- // dragging. Showing the action box is bad in the former case and not very
- // useful in the latter, so never do that.
- // If this was a mouse-up event, we create a comment immediately if
- // the selection is from the end of a line to the start of the next line.
- // In a perfect world we would only do this for double-click, but it is
- // extremely rare that a user would drag from the end of one line to the
- // start of the next and release the mouse, so we don't bother.
- // TODO(brohlfs): This does not work, if the double-click is before a new
- // diff chunk (start will be equal to end), and neither before an "expand
- // the diff context" block (end line will match the first line of the new
- // section and thus be greater than start line + 1).
- if (start.line === end.line - 1 && end.column === 0) {
- // Rather than trying to find the line contents (for comparing
- // start.column with the content length), we just check if the selection
- // is empty to see that it's at the end of a line.
- const content = domRange.cloneContents().querySelector('.contentText');
- if (isMouseUp && this._getLength(content) === 0) {
- this._fireCreateRangeComment(start.side, {
- start_line: start.line,
- start_character: 0,
- end_line: start.line,
- end_character: start.column,
- });
- }
- return;
- }
-
- let actionBox = this.shadowRoot.querySelector('gr-selection-action-box');
- if (!actionBox) {
- actionBox = document.createElement('gr-selection-action-box');
- const root = Polymer.dom(this.root);
- root.insertBefore(actionBox, root.firstElementChild);
- }
- this.selectedRange = {
- range: {
- start_line: start.line,
- start_character: start.column,
- end_line: end.line,
- end_character: end.column,
- },
- side: start.side,
- };
- if (start.line === end.line) {
- this._positionActionBox(actionBox, start.line, domRange);
- } else if (start.node instanceof Text) {
- if (start.column) {
- this._positionActionBox(actionBox, start.line,
- start.node.splitText(start.column));
- }
- start.node.parentElement.normalize(); // Undo splitText from above.
- } else if (start.node.classList.contains('content') &&
- start.node.firstChild) {
- this._positionActionBox(actionBox, start.line, start.node.firstChild);
- } else {
- this._positionActionBox(actionBox, start.line, start.node);
- }
- }
-
- _fireCreateRangeComment(side, range) {
- this.fire('create-range-comment', {side, range});
- this._removeActionBox();
- }
-
- _handleRangeCommentRequest(e) {
- e.stopPropagation();
- if (!this.selectedRange) {
- throw Error('Selected Range is needed for new range comment!');
- }
- const {side, range} = this.selectedRange;
- this._fireCreateRangeComment(side, range);
- }
-
- _removeActionBox() {
- this.selectedRange = undefined;
- const actionBox = this.shadowRoot
- .querySelector('gr-selection-action-box');
- if (actionBox) {
- Polymer.dom(this.root).removeChild(actionBox);
- }
- }
-
- _convertOffsetToColumn(el, offset) {
- if (el instanceof Element && el.classList.contains('content')) {
- return offset;
- }
- while (el.previousSibling ||
- !el.parentElement.classList.contains('content')) {
- if (el.previousSibling) {
- el = el.previousSibling;
- offset += this._getLength(el);
- } else {
- el = el.parentElement;
- }
- }
- return offset;
- }
-
- /**
- * Traverse Element from right to left, call callback for each node.
- * Stops if callback returns true.
- *
- * @param {!Element} startNode
- * @param {function(Node):boolean} callback
- * @param {Object=} opt_flags If flags.left is true, traverse left.
- */
- _traverseContentSiblings(startNode, callback, opt_flags) {
- const travelLeft = opt_flags && opt_flags.left;
- let node = startNode;
- while (node) {
- if (node instanceof Element &&
- node.tagName !== 'HL' &&
- node.tagName !== 'SPAN') {
- break;
- }
- const nextNode = travelLeft ? node.previousSibling : node.nextSibling;
- if (callback(node)) {
- break;
- }
- node = nextNode;
- }
- }
-
- /**
- * Get length of a node. If the node is a content node, then only give the
- * length of its .contentText child.
- *
- * @param {?Element} node this is sometimes passed as null.
- * @return {number}
- */
- _getLength(node) {
- if (node instanceof Element && node.classList.contains('content')) {
- return this._getLength(node.querySelector('.contentText'));
- } else {
- return GrAnnotation.getLength(node);
- }
+ if (index !== undefined) {
+ this.set(['commentRanges', index, 'hovering'], true);
}
}
- customElements.define(GrDiffHighlight.is, GrDiffHighlight);
-})();
+ _handleCommentThreadMouseleave(e) {
+ const threadEl = this._getThreadEl(e);
+ const index = this._indexForThreadEl(threadEl);
+
+ if (index !== undefined) {
+ this.set(['commentRanges', index, 'hovering'], false);
+ }
+ }
+
+ _indexForThreadEl(threadEl) {
+ const side = threadEl.getAttribute('comment-side');
+ const range = JSON.parse(threadEl.getAttribute('range'));
+
+ if (!range) return undefined;
+
+ return this._indexOfCommentRange(side, range);
+ }
+
+ _indexOfCommentRange(side, range) {
+ function rangesEqual(a, b) {
+ if (!a && !b) {
+ return true;
+ }
+ if (!a || !b) {
+ return false;
+ }
+ return a.start_line === b.start_line &&
+ a.start_character === b.start_character &&
+ a.end_line === b.end_line &&
+ a.end_character === b.end_character;
+ }
+
+ return this.commentRanges.findIndex(commentRange =>
+ commentRange.side === side && rangesEqual(commentRange.range, range));
+ }
+
+ /**
+ * Get current normalized selection.
+ * Merges multiple ranges, accounts for triple click, accounts for
+ * syntax highligh, convert native DOM Range objects to Gerrit concepts
+ * (line, side, etc).
+ *
+ * @param {Selection} selection
+ * @return {({
+ * start: {
+ * node: Node,
+ * side: string,
+ * line: Number,
+ * column: Number
+ * },
+ * end: {
+ * node: Node,
+ * side: string,
+ * line: Number,
+ * column: Number
+ * }
+ * })|null|!Object}
+ */
+ _getNormalizedRange(selection) {
+ const rangeCount = selection.rangeCount;
+ if (rangeCount === 0) {
+ return null;
+ } else if (rangeCount === 1) {
+ return this._normalizeRange(selection.getRangeAt(0));
+ } else {
+ const startRange = this._normalizeRange(selection.getRangeAt(0));
+ const endRange = this._normalizeRange(
+ selection.getRangeAt(rangeCount - 1));
+ return {
+ start: startRange.start,
+ end: endRange.end,
+ };
+ }
+ }
+
+ /**
+ * Normalize a specific DOM Range.
+ *
+ * @return {!Object} fixed normalized range
+ */
+ _normalizeRange(domRange) {
+ const range = GrRangeNormalizer.normalize(domRange);
+ return this._fixTripleClickSelection({
+ start: this._normalizeSelectionSide(
+ range.startContainer, range.startOffset),
+ end: this._normalizeSelectionSide(
+ range.endContainer, range.endOffset),
+ }, domRange);
+ }
+
+ /**
+ * Adjust triple click selection for the whole line.
+ * A triple click always results in:
+ * - start.column == end.column == 0
+ * - end.line == start.line + 1
+ *
+ * @param {!Object} range Normalized range, ie column/line numbers
+ * @param {!Range} domRange DOM Range object
+ * @return {!Object} fixed normalized range
+ */
+ _fixTripleClickSelection(range, domRange) {
+ if (!range.start) {
+ // Selection outside of current diff.
+ return range;
+ }
+ const start = range.start;
+ const end = range.end;
+ // Happens when triple click in side-by-side mode with other side empty.
+ const endsAtOtherEmptySide = !end &&
+ domRange.endOffset === 0 &&
+ domRange.endContainer.nodeName === 'TD' &&
+ (domRange.endContainer.classList.contains('left') ||
+ domRange.endContainer.classList.contains('right'));
+ const endsAtBeginningOfNextLine = end &&
+ start.column === 0 &&
+ end.column === 0 &&
+ end.line === start.line + 1;
+ const content = domRange.cloneContents().querySelector('.contentText');
+ const lineLength = content && this._getLength(content) || 0;
+ if (lineLength && (endsAtBeginningOfNextLine || endsAtOtherEmptySide)) {
+ // Move the selection to the end of the previous line.
+ range.end = {
+ node: start.node,
+ column: lineLength,
+ side: start.side,
+ line: start.line,
+ };
+ }
+ return range;
+ }
+
+ /**
+ * Convert DOM Range selection to concrete numbers (line, column, side).
+ * Moves range end if it's not inside td.content.
+ * Returns null if selection end is not valid (outside of diff).
+ *
+ * @param {Node} node td.content child
+ * @param {number} offset offset within node
+ * @return {({
+ * node: Node,
+ * side: string,
+ * line: Number,
+ * column: Number
+ * }|undefined)}
+ */
+ _normalizeSelectionSide(node, offset) {
+ let column;
+ if (!this.contains(node)) {
+ return;
+ }
+ const lineEl = this.diffBuilder.getLineElByChild(node);
+ if (!lineEl) {
+ return;
+ }
+ const side = this.diffBuilder.getSideByLineEl(lineEl);
+ if (!side) {
+ return;
+ }
+ const line = this.diffBuilder.getLineNumberByChild(lineEl);
+ if (!line) {
+ return;
+ }
+ const contentText = this.diffBuilder.getContentByLineEl(lineEl);
+ if (!contentText) {
+ return;
+ }
+ const contentTd = contentText.parentElement;
+ if (!contentTd.contains(node)) {
+ node = contentText;
+ column = 0;
+ } else {
+ const thread = contentTd.querySelector('.comment-thread');
+ if (thread && thread.contains(node)) {
+ column = this._getLength(contentText);
+ node = contentText;
+ } else {
+ column = this._convertOffsetToColumn(node, offset);
+ }
+ }
+
+ return {
+ node,
+ side,
+ line,
+ column,
+ };
+ }
+
+ /**
+ * The only line in which add a comment tooltip is cut off is the first
+ * line. Even if there is a collapsed section, The first visible line is
+ * in the position where the second line would have been, if not for the
+ * collapsed section, so don't need to worry about this case for
+ * positioning the tooltip.
+ */
+ _positionActionBox(actionBox, startLine, range) {
+ if (startLine > 1) {
+ actionBox.placeAbove(range);
+ return;
+ }
+ actionBox.positionBelow = true;
+ actionBox.placeBelow(range);
+ }
+
+ _isRangeValid(range) {
+ if (!range || !range.start || !range.end) {
+ return false;
+ }
+ const start = range.start;
+ const end = range.end;
+ if (start.side !== end.side ||
+ end.line < start.line ||
+ (start.line === end.line && start.column === end.column)) {
+ return false;
+ }
+ return true;
+ }
+
+ _handleSelection(selection, isMouseUp) {
+ const normalizedRange = this._getNormalizedRange(selection);
+ if (!this._isRangeValid(normalizedRange)) {
+ this._removeActionBox();
+ return;
+ }
+ const domRange = selection.getRangeAt(0);
+ const start = normalizedRange.start;
+ const end = normalizedRange.end;
+
+ // TODO (viktard): Drop empty first and last lines from selection.
+
+ // If the selection is from the end of one line to the start of the next
+ // line, then this must have been a double-click, or you have started
+ // dragging. Showing the action box is bad in the former case and not very
+ // useful in the latter, so never do that.
+ // If this was a mouse-up event, we create a comment immediately if
+ // the selection is from the end of a line to the start of the next line.
+ // In a perfect world we would only do this for double-click, but it is
+ // extremely rare that a user would drag from the end of one line to the
+ // start of the next and release the mouse, so we don't bother.
+ // TODO(brohlfs): This does not work, if the double-click is before a new
+ // diff chunk (start will be equal to end), and neither before an "expand
+ // the diff context" block (end line will match the first line of the new
+ // section and thus be greater than start line + 1).
+ if (start.line === end.line - 1 && end.column === 0) {
+ // Rather than trying to find the line contents (for comparing
+ // start.column with the content length), we just check if the selection
+ // is empty to see that it's at the end of a line.
+ const content = domRange.cloneContents().querySelector('.contentText');
+ if (isMouseUp && this._getLength(content) === 0) {
+ this._fireCreateRangeComment(start.side, {
+ start_line: start.line,
+ start_character: 0,
+ end_line: start.line,
+ end_character: start.column,
+ });
+ }
+ return;
+ }
+
+ let actionBox = this.shadowRoot.querySelector('gr-selection-action-box');
+ if (!actionBox) {
+ actionBox = document.createElement('gr-selection-action-box');
+ const root = dom(this.root);
+ root.insertBefore(actionBox, root.firstElementChild);
+ }
+ this.selectedRange = {
+ range: {
+ start_line: start.line,
+ start_character: start.column,
+ end_line: end.line,
+ end_character: end.column,
+ },
+ side: start.side,
+ };
+ if (start.line === end.line) {
+ this._positionActionBox(actionBox, start.line, domRange);
+ } else if (start.node instanceof Text) {
+ if (start.column) {
+ this._positionActionBox(actionBox, start.line,
+ start.node.splitText(start.column));
+ }
+ start.node.parentElement.normalize(); // Undo splitText from above.
+ } else if (start.node.classList.contains('content') &&
+ start.node.firstChild) {
+ this._positionActionBox(actionBox, start.line, start.node.firstChild);
+ } else {
+ this._positionActionBox(actionBox, start.line, start.node);
+ }
+ }
+
+ _fireCreateRangeComment(side, range) {
+ this.fire('create-range-comment', {side, range});
+ this._removeActionBox();
+ }
+
+ _handleRangeCommentRequest(e) {
+ e.stopPropagation();
+ if (!this.selectedRange) {
+ throw Error('Selected Range is needed for new range comment!');
+ }
+ const {side, range} = this.selectedRange;
+ this._fireCreateRangeComment(side, range);
+ }
+
+ _removeActionBox() {
+ this.selectedRange = undefined;
+ const actionBox = this.shadowRoot
+ .querySelector('gr-selection-action-box');
+ if (actionBox) {
+ dom(this.root).removeChild(actionBox);
+ }
+ }
+
+ _convertOffsetToColumn(el, offset) {
+ if (el instanceof Element && el.classList.contains('content')) {
+ return offset;
+ }
+ while (el.previousSibling ||
+ !el.parentElement.classList.contains('content')) {
+ if (el.previousSibling) {
+ el = el.previousSibling;
+ offset += this._getLength(el);
+ } else {
+ el = el.parentElement;
+ }
+ }
+ return offset;
+ }
+
+ /**
+ * Traverse Element from right to left, call callback for each node.
+ * Stops if callback returns true.
+ *
+ * @param {!Element} startNode
+ * @param {function(Node):boolean} callback
+ * @param {Object=} opt_flags If flags.left is true, traverse left.
+ */
+ _traverseContentSiblings(startNode, callback, opt_flags) {
+ const travelLeft = opt_flags && opt_flags.left;
+ let node = startNode;
+ while (node) {
+ if (node instanceof Element &&
+ node.tagName !== 'HL' &&
+ node.tagName !== 'SPAN') {
+ break;
+ }
+ const nextNode = travelLeft ? node.previousSibling : node.nextSibling;
+ if (callback(node)) {
+ break;
+ }
+ node = nextNode;
+ }
+ }
+
+ /**
+ * Get length of a node. If the node is a content node, then only give the
+ * length of its .contentText child.
+ *
+ * @param {?Element} node this is sometimes passed as null.
+ * @return {number}
+ */
+ _getLength(node) {
+ if (node instanceof Element && node.classList.contains('content')) {
+ return this._getLength(node.querySelector('.contentText'));
+ } else {
+ return GrAnnotation.getLength(node);
+ }
+ }
+}
+
+customElements.define(GrDiffHighlight.is, GrDiffHighlight);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js
new file mode 100644
index 0000000..10b4f2d
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js
@@ -0,0 +1,35 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ position: relative;
+ }
+ gr-selection-action-box {
+ /**
+ * Needs z-index to apear above wrapped content, since it's inseted
+ * into DOM before it.
+ */
+ z-index: 10;
+ }
+ </style>
+ <div class="contentWrapper">
+ <slot></slot>
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index 02c2033..c4f7152 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-highlight</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-diff-highlight.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -61,8 +56,7 @@
</tr>
</tbody>
-
- <tbody class="section both">
+<tbody class="section both">
<tr class="diff-row side-by-side" left-type="both" right-type="both">
<td class="left lineNum" data-value="138"></td>
<td class="content both"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
@@ -147,486 +141,487 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff-highlight', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-diff-highlight.js';
+suite('gr-diff-highlight', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic')[1];
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('comment events', () => {
+ let builder;
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic')[1];
+ builder = {
+ getContentsByLineRange: sandbox.stub().returns([]),
+ getLineElByChild: sandbox.stub().returns({}),
+ getSideByLineEl: sandbox.stub().returns('other-side'),
+ };
+ element._cachedDiffBuilder = builder;
+ });
+
+ test('comment-thread-mouseenter from line comments is ignored', () => {
+ const threadEl = document.createElement('div');
+ threadEl.className = 'comment-thread';
+ threadEl.setAttribute('comment-side', 'right');
+ threadEl.setAttribute('line-num', 3);
+ element.appendChild(threadEl);
+ element.commentRanges = [{side: 'right'}];
+
+ sandbox.stub(element, 'set');
+ threadEl.dispatchEvent(new CustomEvent(
+ 'comment-thread-mouseenter', {bubbles: true, composed: true}));
+ assert.isFalse(element.set.called);
+ });
+
+ test('comment-thread-mouseenter from ranged comment causes set', () => {
+ const threadEl = document.createElement('div');
+ threadEl.className = 'comment-thread';
+ threadEl.setAttribute('comment-side', 'right');
+ threadEl.setAttribute('line-num', 3);
+ threadEl.setAttribute('range', JSON.stringify({
+ start_line: 3,
+ start_character: 4,
+ end_line: 5,
+ end_character: 6,
+ }));
+ element.appendChild(threadEl);
+ element.commentRanges = [{side: 'right', range: {
+ start_line: 3,
+ start_character: 4,
+ end_line: 5,
+ end_character: 6,
+ }}];
+
+ sandbox.stub(element, 'set');
+ threadEl.dispatchEvent(new CustomEvent(
+ 'comment-thread-mouseenter', {bubbles: true, composed: true}));
+ assert.isTrue(element.set.called);
+ const args = element.set.lastCall.args;
+ assert.deepEqual(args[0], ['commentRanges', 0, 'hovering']);
+ assert.deepEqual(args[1], true);
+ });
+
+ test('comment-thread-mouseleave from line comments is ignored', () => {
+ const threadEl = document.createElement('div');
+ threadEl.className = 'comment-thread';
+ threadEl.setAttribute('comment-side', 'right');
+ threadEl.setAttribute('line-num', 3);
+ element.appendChild(threadEl);
+ element.commentRanges = [{side: 'right'}];
+
+ sandbox.stub(element, 'set');
+ threadEl.dispatchEvent(new CustomEvent(
+ 'comment-thread-mouseleave', {bubbles: true, composed: true}));
+ assert.isFalse(element.set.called);
+ });
+
+ test(`create-range-comment for range when create-comment-requested
+ is fired`, () => {
+ sandbox.stub(element, '_removeActionBox');
+ element.selectedRange = {
+ side: 'left',
+ range: {
+ start_line: 7,
+ start_character: 11,
+ end_line: 24,
+ end_character: 42,
+ },
+ };
+ const requestEvent = new CustomEvent('create-comment-requested');
+ let createRangeEvent;
+ element.addEventListener('create-range-comment', e => {
+ createRangeEvent = e;
+ });
+ element.dispatchEvent(requestEvent);
+ assert.deepEqual(element.selectedRange, createRangeEvent.detail);
+ assert.isTrue(element._removeActionBox.called);
+ });
+ });
+
+ suite('selection', () => {
+ let diff;
+ let builder;
+ let contentStubs;
+
+ const stubContent = (line, side, opt_child) => {
+ const contentTd = diff.querySelector(
+ `.${side}.lineNum[data-value="${line}"] ~ .content`);
+ const contentText = contentTd.querySelector('.contentText');
+ const lineEl = diff.querySelector(
+ `.${side}.lineNum[data-value="${line}"]`);
+ contentStubs.push({
+ lineEl,
+ contentTd,
+ contentText,
+ });
+ builder.getContentByLineEl.withArgs(lineEl).returns(contentText);
+ builder.getLineNumberByChild.withArgs(lineEl).returns(line);
+ builder.getContentByLine.withArgs(line, side).returns(contentText);
+ builder.getSideByLineEl.withArgs(lineEl).returns(side);
+ return contentText;
+ };
+
+ const emulateSelection = (startNode, startOffset, endNode, endOffset) => {
+ const selection = window.getSelection();
+ const range = document.createRange();
+ range.setStart(startNode, startOffset);
+ range.setEnd(endNode, endOffset);
+ selection.addRange(range);
+ element._handleSelection(selection);
+ };
+
+ const getLineElByChild = node => {
+ const stubs = contentStubs.find(stub => stub.contentTd.contains(node));
+ return stubs && stubs.lineEl;
+ };
+
+ setup(() => {
+ contentStubs = [];
+ stub('gr-selection-action-box', {
+ placeAbove: sandbox.stub(),
+ placeBelow: sandbox.stub(),
+ });
+ diff = element.querySelector('#diffTable');
+ builder = {
+ getContentByLine: sandbox.stub(),
+ getContentByLineEl: sandbox.stub(),
+ getLineElByChild,
+ getLineNumberByChild: sandbox.stub(),
+ getSideByLineEl: sandbox.stub(),
+ };
+ element._cachedDiffBuilder = builder;
});
teardown(() => {
- sandbox.restore();
+ contentStubs = null;
+ window.getSelection().removeAllRanges();
});
- suite('comment events', () => {
- let builder;
+ test('single first line', () => {
+ const content = stubContent(1, 'right');
+ sandbox.spy(element, '_positionActionBox');
+ emulateSelection(content.firstChild, 5, content.firstChild, 12);
+ const actionBox = element.shadowRoot
+ .querySelector('gr-selection-action-box');
+ assert.isTrue(actionBox.positionBelow);
+ });
- setup(() => {
- builder = {
- getContentsByLineRange: sandbox.stub().returns([]),
- getLineElByChild: sandbox.stub().returns({}),
- getSideByLineEl: sandbox.stub().returns('other-side'),
- };
- element._cachedDiffBuilder = builder;
+ test('multiline starting on first line', () => {
+ const startContent = stubContent(1, 'right');
+ const endContent = stubContent(2, 'right');
+ sandbox.spy(element, '_positionActionBox');
+ emulateSelection(
+ startContent.firstChild, 10, endContent.lastChild, 7);
+ const actionBox = element.shadowRoot
+ .querySelector('gr-selection-action-box');
+ assert.isTrue(actionBox.positionBelow);
+ });
+
+ test('single line', () => {
+ const content = stubContent(138, 'left');
+ sandbox.spy(element, '_positionActionBox');
+ emulateSelection(content.firstChild, 5, content.firstChild, 12);
+ const actionBox = element.shadowRoot
+ .querySelector('gr-selection-action-box');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 138,
+ start_character: 5,
+ end_line: 138,
+ end_character: 12,
});
+ assert.equal(side, 'left');
+ assert.notOk(actionBox.positionBelow);
+ });
- test('comment-thread-mouseenter from line comments is ignored', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('comment-side', 'right');
- threadEl.setAttribute('line-num', 3);
- element.appendChild(threadEl);
- element.commentRanges = [{side: 'right'}];
-
- sandbox.stub(element, 'set');
- threadEl.dispatchEvent(new CustomEvent(
- 'comment-thread-mouseenter', {bubbles: true, composed: true}));
- assert.isFalse(element.set.called);
+ test('multiline', () => {
+ const startContent = stubContent(119, 'right');
+ const endContent = stubContent(120, 'right');
+ sandbox.spy(element, '_positionActionBox');
+ emulateSelection(
+ startContent.firstChild, 10, endContent.lastChild, 7);
+ const actionBox = element.shadowRoot
+ .querySelector('gr-selection-action-box');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 119,
+ start_character: 10,
+ end_line: 120,
+ end_character: 36,
});
+ assert.equal(side, 'right');
+ assert.notOk(actionBox.positionBelow);
+ });
- test('comment-thread-mouseenter from ranged comment causes set', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('comment-side', 'right');
- threadEl.setAttribute('line-num', 3);
- threadEl.setAttribute('range', JSON.stringify({
- start_line: 3,
- start_character: 4,
- end_line: 5,
- end_character: 6,
- }));
- element.appendChild(threadEl);
- element.commentRanges = [{side: 'right', range: {
- start_line: 3,
- start_character: 4,
- end_line: 5,
- end_character: 6,
- }}];
+ test('multiple ranges aka firefox implementation', () => {
+ const startContent = stubContent(119, 'right');
+ const endContent = stubContent(120, 'right');
- sandbox.stub(element, 'set');
- threadEl.dispatchEvent(new CustomEvent(
- 'comment-thread-mouseenter', {bubbles: true, composed: true}));
- assert.isTrue(element.set.called);
- const args = element.set.lastCall.args;
- assert.deepEqual(args[0], ['commentRanges', 0, 'hovering']);
- assert.deepEqual(args[1], true);
- });
+ const startRange = document.createRange();
+ startRange.setStart(startContent.firstChild, 10);
+ startRange.setEnd(startContent.firstChild, 11);
- test('comment-thread-mouseleave from line comments is ignored', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('comment-side', 'right');
- threadEl.setAttribute('line-num', 3);
- element.appendChild(threadEl);
- element.commentRanges = [{side: 'right'}];
+ const endRange = document.createRange();
+ endRange.setStart(endContent.lastChild, 6);
+ endRange.setEnd(endContent.lastChild, 7);
- sandbox.stub(element, 'set');
- threadEl.dispatchEvent(new CustomEvent(
- 'comment-thread-mouseleave', {bubbles: true, composed: true}));
- assert.isFalse(element.set.called);
- });
-
- test(`create-range-comment for range when create-comment-requested
- is fired`, () => {
- sandbox.stub(element, '_removeActionBox');
- element.selectedRange = {
- side: 'left',
- range: {
- start_line: 7,
- start_character: 11,
- end_line: 24,
- end_character: 42,
- },
- };
- const requestEvent = new CustomEvent('create-comment-requested');
- let createRangeEvent;
- element.addEventListener('create-range-comment', e => {
- createRangeEvent = e;
- });
- element.dispatchEvent(requestEvent);
- assert.deepEqual(element.selectedRange, createRangeEvent.detail);
- assert.isTrue(element._removeActionBox.called);
+ const getRangeAtStub = sandbox.stub();
+ getRangeAtStub
+ .onFirstCall().returns(startRange)
+ .onSecondCall()
+ .returns(endRange);
+ const selection = {
+ rangeCount: 2,
+ getRangeAt: getRangeAtStub,
+ removeAllRanges: sandbox.stub(),
+ };
+ element._handleSelection(selection);
+ const {range} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 119,
+ start_character: 10,
+ end_line: 120,
+ end_character: 36,
});
});
- suite('selection', () => {
- let diff;
- let builder;
- let contentStubs;
-
- const stubContent = (line, side, opt_child) => {
- const contentTd = diff.querySelector(
- `.${side}.lineNum[data-value="${line}"] ~ .content`);
- const contentText = contentTd.querySelector('.contentText');
- const lineEl = diff.querySelector(
- `.${side}.lineNum[data-value="${line}"]`);
- contentStubs.push({
- lineEl,
- contentTd,
- contentText,
- });
- builder.getContentByLineEl.withArgs(lineEl).returns(contentText);
- builder.getLineNumberByChild.withArgs(lineEl).returns(line);
- builder.getContentByLine.withArgs(line, side).returns(contentText);
- builder.getSideByLineEl.withArgs(lineEl).returns(side);
- return contentText;
- };
-
- const emulateSelection = (startNode, startOffset, endNode, endOffset) => {
- const selection = window.getSelection();
- const range = document.createRange();
- range.setStart(startNode, startOffset);
- range.setEnd(endNode, endOffset);
- selection.addRange(range);
- element._handleSelection(selection);
- };
-
- const getLineElByChild = node => {
- const stubs = contentStubs.find(stub => stub.contentTd.contains(node));
- return stubs && stubs.lineEl;
- };
-
- setup(() => {
- contentStubs = [];
- stub('gr-selection-action-box', {
- placeAbove: sandbox.stub(),
- placeBelow: sandbox.stub(),
- });
- diff = element.querySelector('#diffTable');
- builder = {
- getContentByLine: sandbox.stub(),
- getContentByLineEl: sandbox.stub(),
- getLineElByChild,
- getLineNumberByChild: sandbox.stub(),
- getSideByLineEl: sandbox.stub(),
- };
- element._cachedDiffBuilder = builder;
+ test('multiline grow end highlight over tabs', () => {
+ const startContent = stubContent(119, 'right');
+ const endContent = stubContent(120, 'right');
+ emulateSelection(startContent.firstChild, 10, endContent.firstChild, 2);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 119,
+ start_character: 10,
+ end_line: 120,
+ end_character: 2,
});
+ assert.equal(side, 'right');
+ });
- teardown(() => {
- contentStubs = null;
- window.getSelection().removeAllRanges();
+ test('collapsed', () => {
+ const content = stubContent(138, 'left');
+ emulateSelection(content.firstChild, 5, content.firstChild, 5);
+ assert.isOk(window.getSelection().getRangeAt(0).startContainer);
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('starts inside hl', () => {
+ const content = stubContent(140, 'left');
+ const hl = content.querySelector('.foo');
+ emulateSelection(hl.firstChild, 2, hl.nextSibling, 7);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 8,
+ end_line: 140,
+ end_character: 23,
});
+ assert.equal(side, 'left');
+ });
- test('single first line', () => {
- const content = stubContent(1, 'right');
- sandbox.spy(element, '_positionActionBox');
- emulateSelection(content.firstChild, 5, content.firstChild, 12);
- const actionBox = element.shadowRoot
- .querySelector('gr-selection-action-box');
- assert.isTrue(actionBox.positionBelow);
+ test('ends inside hl', () => {
+ const content = stubContent(140, 'left');
+ const hl = content.querySelector('.bar');
+ emulateSelection(hl.previousSibling, 2, hl.firstChild, 3);
+ const {range} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 18,
+ end_line: 140,
+ end_character: 27,
});
+ });
- test('multiline starting on first line', () => {
- const startContent = stubContent(1, 'right');
- const endContent = stubContent(2, 'right');
- sandbox.spy(element, '_positionActionBox');
- emulateSelection(
- startContent.firstChild, 10, endContent.lastChild, 7);
- const actionBox = element.shadowRoot
- .querySelector('gr-selection-action-box');
- assert.isTrue(actionBox.positionBelow);
+ test('multiple hl', () => {
+ const content = stubContent(140, 'left');
+ const hl = content.querySelectorAll('hl')[4];
+ emulateSelection(content.firstChild, 2, hl.firstChild, 2);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 2,
+ end_line: 140,
+ end_character: 61,
});
+ assert.equal(side, 'left');
+ });
- test('single line', () => {
- const content = stubContent(138, 'left');
- sandbox.spy(element, '_positionActionBox');
- emulateSelection(content.firstChild, 5, content.firstChild, 12);
- const actionBox = element.shadowRoot
- .querySelector('gr-selection-action-box');
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 138,
- start_character: 5,
- end_line: 138,
- end_character: 12,
- });
- assert.equal(side, 'left');
- assert.notOk(actionBox.positionBelow);
+ test('starts outside of diff', () => {
+ const contentText = stubContent(140, 'left');
+ const contentTd = contentText.parentElement;
+
+ emulateSelection(contentTd.previousElementSibling, 0,
+ contentText.firstChild, 2);
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('ends outside of diff', () => {
+ const content = stubContent(140, 'left');
+ emulateSelection(content.nextElementSibling.firstChild, 2,
+ content.firstChild, 2);
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('starts and ends on different sides', () => {
+ const startContent = stubContent(140, 'left');
+ const endContent = stubContent(130, 'right');
+ emulateSelection(startContent.firstChild, 2, endContent.firstChild, 2);
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('starts in comment thread element', () => {
+ const startContent = stubContent(140, 'left');
+ const comment = startContent.parentElement.querySelector(
+ '.comment-thread');
+ const endContent = stubContent(141, 'left');
+ emulateSelection(comment.firstChild, 2, endContent.firstChild, 4);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 83,
+ end_line: 141,
+ end_character: 4,
});
+ assert.equal(side, 'left');
+ });
- test('multiline', () => {
- const startContent = stubContent(119, 'right');
- const endContent = stubContent(120, 'right');
- sandbox.spy(element, '_positionActionBox');
- emulateSelection(
- startContent.firstChild, 10, endContent.lastChild, 7);
- const actionBox = element.shadowRoot
- .querySelector('gr-selection-action-box');
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 119,
- start_character: 10,
- end_line: 120,
- end_character: 36,
- });
- assert.equal(side, 'right');
- assert.notOk(actionBox.positionBelow);
+ test('ends in comment thread element', () => {
+ const content = stubContent(140, 'left');
+ const comment = content.parentElement.querySelector(
+ '.comment-thread');
+ emulateSelection(content.firstChild, 4, comment.firstChild, 1);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 4,
+ end_line: 140,
+ end_character: 83,
});
+ assert.equal(side, 'left');
+ });
- test('multiple ranges aka firefox implementation', () => {
- const startContent = stubContent(119, 'right');
- const endContent = stubContent(120, 'right');
+ test('starts in context element', () => {
+ const contextControl =
+ diff.querySelector('.contextControl').querySelector('gr-button');
+ const content = stubContent(146, 'right');
+ emulateSelection(contextControl, 0, content.firstChild, 7);
+ // TODO (viktard): Select nearest line.
+ assert.isFalse(!!element.selectedRange);
+ });
- const startRange = document.createRange();
- startRange.setStart(startContent.firstChild, 10);
- startRange.setEnd(startContent.firstChild, 11);
+ test('ends in context element', () => {
+ const contextControl =
+ diff.querySelector('.contextControl').querySelector('gr-button');
+ const content = stubContent(141, 'left');
+ emulateSelection(content.firstChild, 2, contextControl, 1);
+ // TODO (viktard): Select nearest line.
+ assert.isFalse(!!element.selectedRange);
+ });
- const endRange = document.createRange();
- endRange.setStart(endContent.lastChild, 6);
- endRange.setEnd(endContent.lastChild, 7);
-
- const getRangeAtStub = sandbox.stub();
- getRangeAtStub
- .onFirstCall().returns(startRange)
- .onSecondCall()
- .returns(endRange);
- const selection = {
- rangeCount: 2,
- getRangeAt: getRangeAtStub,
- removeAllRanges: sandbox.stub(),
- };
- element._handleSelection(selection);
- const {range} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 119,
- start_character: 10,
- end_line: 120,
- end_character: 36,
- });
+ test('selection containing context element', () => {
+ const startContent = stubContent(130, 'right');
+ const endContent = stubContent(146, 'right');
+ emulateSelection(startContent.firstChild, 3, endContent.firstChild, 14);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 130,
+ start_character: 3,
+ end_line: 146,
+ end_character: 14,
});
+ assert.equal(side, 'right');
+ });
- test('multiline grow end highlight over tabs', () => {
- const startContent = stubContent(119, 'right');
- const endContent = stubContent(120, 'right');
- emulateSelection(startContent.firstChild, 10, endContent.firstChild, 2);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 119,
- start_character: 10,
- end_line: 120,
- end_character: 2,
- });
- assert.equal(side, 'right');
+ test('ends at a tab', () => {
+ const content = stubContent(140, 'left');
+ emulateSelection(
+ content.firstChild, 1, content.querySelector('span'), 0);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 1,
+ end_line: 140,
+ end_character: 51,
});
+ assert.equal(side, 'left');
+ });
- test('collapsed', () => {
- const content = stubContent(138, 'left');
- emulateSelection(content.firstChild, 5, content.firstChild, 5);
- assert.isOk(window.getSelection().getRangeAt(0).startContainer);
- assert.isFalse(!!element.selectedRange);
+ test('starts at a tab', () => {
+ const content = stubContent(140, 'left');
+ emulateSelection(
+ content.querySelectorAll('hl')[3], 0,
+ content.querySelectorAll('span')[1].nextSibling, 1);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 51,
+ end_line: 140,
+ end_character: 71,
});
+ assert.equal(side, 'left');
+ });
- test('starts inside hl', () => {
- const content = stubContent(140, 'left');
- const hl = content.querySelector('.foo');
- emulateSelection(hl.firstChild, 2, hl.nextSibling, 7);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 8,
- end_line: 140,
- end_character: 23,
- });
- assert.equal(side, 'left');
+ test('properly accounts for syntax highlighting', () => {
+ const content = stubContent(140, 'left');
+ const spy = sinon.spy(element, '_normalizeRange');
+ emulateSelection(
+ content.querySelectorAll('hl')[3], 0,
+ content.querySelectorAll('span')[1], 0);
+ const spyCall = spy.getCall(0);
+ const range = window.getSelection().getRangeAt(0);
+ assert.notDeepEqual(spyCall.returnValue, range);
+ });
+
+ test('GrRangeNormalizer._getTextOffset computes text offset', () => {
+ let content = stubContent(140, 'left');
+ let child = content.lastChild.lastChild;
+ let result = GrRangeNormalizer._getTextOffset(content, child);
+ assert.equal(result, 75);
+ content = stubContent(146, 'right');
+ child = content.lastChild;
+ result = GrRangeNormalizer._getTextOffset(content, child);
+ assert.equal(result, 0);
+ });
+
+ test('_fixTripleClickSelection', () => {
+ const startContent = stubContent(119, 'right');
+ const endContent = stubContent(120, 'right');
+ emulateSelection(startContent.firstChild, 0, endContent.firstChild, 0);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 119,
+ start_character: 0,
+ end_line: 119,
+ end_character: element._getLength(startContent),
});
+ assert.equal(side, 'right');
+ });
- test('ends inside hl', () => {
- const content = stubContent(140, 'left');
- const hl = content.querySelector('.bar');
- emulateSelection(hl.previousSibling, 2, hl.firstChild, 3);
- const {range} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 18,
- end_line: 140,
- end_character: 27,
- });
+ test('_fixTripleClickSelection empty line', () => {
+ const startContent = stubContent(146, 'right');
+ const endContent = stubContent(165, 'left');
+ emulateSelection(startContent.firstChild, 0,
+ endContent.parentElement.previousElementSibling, 0);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 146,
+ start_character: 0,
+ end_line: 146,
+ end_character: 84,
});
-
- test('multiple hl', () => {
- const content = stubContent(140, 'left');
- const hl = content.querySelectorAll('hl')[4];
- emulateSelection(content.firstChild, 2, hl.firstChild, 2);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 2,
- end_line: 140,
- end_character: 61,
- });
- assert.equal(side, 'left');
- });
-
- test('starts outside of diff', () => {
- const contentText = stubContent(140, 'left');
- const contentTd = contentText.parentElement;
-
- emulateSelection(contentTd.previousElementSibling, 0,
- contentText.firstChild, 2);
- assert.isFalse(!!element.selectedRange);
- });
-
- test('ends outside of diff', () => {
- const content = stubContent(140, 'left');
- emulateSelection(content.nextElementSibling.firstChild, 2,
- content.firstChild, 2);
- assert.isFalse(!!element.selectedRange);
- });
-
- test('starts and ends on different sides', () => {
- const startContent = stubContent(140, 'left');
- const endContent = stubContent(130, 'right');
- emulateSelection(startContent.firstChild, 2, endContent.firstChild, 2);
- assert.isFalse(!!element.selectedRange);
- });
-
- test('starts in comment thread element', () => {
- const startContent = stubContent(140, 'left');
- const comment = startContent.parentElement.querySelector(
- '.comment-thread');
- const endContent = stubContent(141, 'left');
- emulateSelection(comment.firstChild, 2, endContent.firstChild, 4);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 83,
- end_line: 141,
- end_character: 4,
- });
- assert.equal(side, 'left');
- });
-
- test('ends in comment thread element', () => {
- const content = stubContent(140, 'left');
- const comment = content.parentElement.querySelector(
- '.comment-thread');
- emulateSelection(content.firstChild, 4, comment.firstChild, 1);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 4,
- end_line: 140,
- end_character: 83,
- });
- assert.equal(side, 'left');
- });
-
- test('starts in context element', () => {
- const contextControl =
- diff.querySelector('.contextControl').querySelector('gr-button');
- const content = stubContent(146, 'right');
- emulateSelection(contextControl, 0, content.firstChild, 7);
- // TODO (viktard): Select nearest line.
- assert.isFalse(!!element.selectedRange);
- });
-
- test('ends in context element', () => {
- const contextControl =
- diff.querySelector('.contextControl').querySelector('gr-button');
- const content = stubContent(141, 'left');
- emulateSelection(content.firstChild, 2, contextControl, 1);
- // TODO (viktard): Select nearest line.
- assert.isFalse(!!element.selectedRange);
- });
-
- test('selection containing context element', () => {
- const startContent = stubContent(130, 'right');
- const endContent = stubContent(146, 'right');
- emulateSelection(startContent.firstChild, 3, endContent.firstChild, 14);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 130,
- start_character: 3,
- end_line: 146,
- end_character: 14,
- });
- assert.equal(side, 'right');
- });
-
- test('ends at a tab', () => {
- const content = stubContent(140, 'left');
- emulateSelection(
- content.firstChild, 1, content.querySelector('span'), 0);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 1,
- end_line: 140,
- end_character: 51,
- });
- assert.equal(side, 'left');
- });
-
- test('starts at a tab', () => {
- const content = stubContent(140, 'left');
- emulateSelection(
- content.querySelectorAll('hl')[3], 0,
- content.querySelectorAll('span')[1].nextSibling, 1);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 51,
- end_line: 140,
- end_character: 71,
- });
- assert.equal(side, 'left');
- });
-
- test('properly accounts for syntax highlighting', () => {
- const content = stubContent(140, 'left');
- const spy = sinon.spy(element, '_normalizeRange');
- emulateSelection(
- content.querySelectorAll('hl')[3], 0,
- content.querySelectorAll('span')[1], 0);
- const spyCall = spy.getCall(0);
- const range = window.getSelection().getRangeAt(0);
- assert.notDeepEqual(spyCall.returnValue, range);
- });
-
- test('GrRangeNormalizer._getTextOffset computes text offset', () => {
- let content = stubContent(140, 'left');
- let child = content.lastChild.lastChild;
- let result = GrRangeNormalizer._getTextOffset(content, child);
- assert.equal(result, 75);
- content = stubContent(146, 'right');
- child = content.lastChild;
- result = GrRangeNormalizer._getTextOffset(content, child);
- assert.equal(result, 0);
- });
-
- test('_fixTripleClickSelection', () => {
- const startContent = stubContent(119, 'right');
- const endContent = stubContent(120, 'right');
- emulateSelection(startContent.firstChild, 0, endContent.firstChild, 0);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 119,
- start_character: 0,
- end_line: 119,
- end_character: element._getLength(startContent),
- });
- assert.equal(side, 'right');
- });
-
- test('_fixTripleClickSelection empty line', () => {
- const startContent = stubContent(146, 'right');
- const endContent = stubContent(165, 'left');
- emulateSelection(startContent.firstChild, 0,
- endContent.parentElement.previousElementSibling, 0);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 146,
- start_character: 0,
- end_line: 146,
- end_character: 84,
- });
- assert.equal(side, 'right');
- });
+ assert.equal(side, 'right');
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html
deleted file mode 100644
index 2d9369f..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html
+++ /dev/null
@@ -1,67 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-comment-thread/gr-comment-thread.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-<link rel="import" href="../gr-diff/gr-diff.html">
-<link rel="import" href="../gr-syntax-layer/gr-syntax-layer.html">
-
-<dom-module id="gr-diff-host">
- <template>
- <gr-diff
- id="diff"
- change-num="[[changeNum]]"
- no-auto-render=[[noAutoRender]]
- patch-range="[[patchRange]]"
- path="[[path]]"
- prefs="[[prefs]]"
- project-name="[[projectName]]"
- display-line="[[displayLine]]"
- is-image-diff="[[isImageDiff]]"
- commit-range="[[commitRange]]"
- hidden$="[[hidden]]"
- no-render-on-prefs-change="[[noRenderOnPrefsChange]]"
- line-wrapping="[[lineWrapping]]"
- view-mode="[[viewMode]]"
- line-of-interest="[[lineOfInterest]]"
- logged-in="[[_loggedIn]]"
- loading="[[_loading]]"
- error-message="[[_errorMessage]]"
- base-image="[[_baseImage]]"
- revision-image=[[_revisionImage]]
- coverage-ranges="[[_coverageRanges]]"
- blame="[[_blame]]"
- layers="[[_layers]]"
- diff="[[diff]]"
- show-newline-warning-left="[[_showNewlineWarningLeft(diff)]]"
- show-newline-warning-right="[[_showNewlineWarningRight(diff)]]">
- </gr-diff>
- <gr-syntax-layer
- id="syntaxLayer"
- enabled="[[_syntaxHighlightingEnabled]]"
- diff="[[diff]]"></gr-syntax-layer>
- <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- <gr-reporting id="reporting" category="diff"></gr-reporting>
- </template>
- <script src="gr-diff-host.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
index a44e366..26a3a40 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
@@ -14,1097 +14,1112 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const MSG_EMPTY_BLAME = 'No blame information for this diff.';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-comment-thread/gr-comment-thread.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import '../gr-diff/gr-diff.js';
+import '../gr-syntax-layer/gr-syntax-layer.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-diff-host_html.js';
- const EVENT_AGAINST_PARENT = 'diff-against-parent';
- const EVENT_ZERO_REBASE = 'rebase-percent-zero';
- const EVENT_NONZERO_REBASE = 'rebase-percent-nonzero';
+const MSG_EMPTY_BLAME = 'No blame information for this diff.';
- const DiffViewMode = {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- };
+const EVENT_AGAINST_PARENT = 'diff-against-parent';
+const EVENT_ZERO_REBASE = 'rebase-percent-zero';
+const EVENT_NONZERO_REBASE = 'rebase-percent-nonzero';
- /** @enum {string} */
- const TimingLabel = {
- TOTAL: 'Diff Total Render',
- CONTENT: 'Diff Content Render',
- SYNTAX: 'Diff Syntax Render',
- };
+const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+};
- // Disable syntax highlighting if the overall diff is too large.
- const SYNTAX_MAX_DIFF_LENGTH = 20000;
+/** @enum {string} */
+const TimingLabel = {
+ TOTAL: 'Diff Total Render',
+ CONTENT: 'Diff Content Render',
+ SYNTAX: 'Diff Syntax Render',
+};
- // If any line of the diff is more than the character limit, then disable
- // syntax highlighting for the entire file.
- const SYNTAX_MAX_LINE_LENGTH = 500;
+// Disable syntax highlighting if the overall diff is too large.
+const SYNTAX_MAX_DIFF_LENGTH = 20000;
- // 120 lines is good enough threshold for full-sized window viewport
- const NUM_OF_LINES_THRESHOLD_FOR_VIEWPORT = 120;
+// If any line of the diff is more than the character limit, then disable
+// syntax highlighting for the entire file.
+const SYNTAX_MAX_LINE_LENGTH = 500;
- const WHITESPACE_IGNORE_NONE = 'IGNORE_NONE';
+// 120 lines is good enough threshold for full-sized window viewport
+const NUM_OF_LINES_THRESHOLD_FOR_VIEWPORT = 120;
+
+const WHITESPACE_IGNORE_NONE = 'IGNORE_NONE';
+
+/**
+ * @param {Object} diff
+ * @return {boolean}
+ */
+function isImageDiff(diff) {
+ if (!diff) { return false; }
+
+ const isA = diff.meta_a &&
+ diff.meta_a.content_type.startsWith('image/');
+ const isB = diff.meta_b &&
+ diff.meta_b.content_type.startsWith('image/');
+
+ return !!(diff.binary && (isA || isB));
+}
+
+/** @enum {string} */
+Gerrit.DiffSide = {
+ LEFT: 'left',
+ RIGHT: 'right',
+};
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ */
+/**
+ * Wrapper around gr-diff.
+ *
+ * Webcomponent fetching diffs and related data from restAPI and passing them
+ * to the presentational gr-diff for rendering.
+ *
+ * @extends Polymer.Element
+ */
+class GrDiffHost extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.PatchSetBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-diff-host'; }
+ /**
+ * Fired when the user selects a line.
+ *
+ * @event line-selected
+ */
+
+ /**
+ * Fired if being logged in is required.
+ *
+ * @event show-auth-required
+ */
+
+ /**
+ * Fired when a comment is saved or discarded
+ *
+ * @event diff-comments-modified
+ */
+
+ static get properties() {
+ return {
+ changeNum: String,
+ noAutoRender: {
+ type: Boolean,
+ value: false,
+ },
+ /** @type {?} */
+ patchRange: Object,
+ path: String,
+ prefs: {
+ type: Object,
+ },
+ projectName: String,
+ displayLine: {
+ type: Boolean,
+ value: false,
+ },
+ isImageDiff: {
+ type: Boolean,
+ computed: '_computeIsImageDiff(diff)',
+ notify: true,
+ },
+ commitRange: Object,
+ filesWeblinks: {
+ type: Object,
+ value() {
+ return {};
+ },
+ notify: true,
+ },
+ hidden: {
+ type: Boolean,
+ reflectToAttribute: true,
+ },
+ noRenderOnPrefsChange: {
+ type: Boolean,
+ value: false,
+ },
+ comments: {
+ type: Object,
+ observer: '_commentsChanged',
+ },
+ lineWrapping: {
+ type: Boolean,
+ value: false,
+ },
+ viewMode: {
+ type: String,
+ value: DiffViewMode.SIDE_BY_SIDE,
+ },
+
+ /**
+ * Special line number which should not be collapsed into a shared region.
+ *
+ * @type {{
+ * number: number,
+ * leftSide: {boolean}
+ * }|null}
+ */
+ lineOfInterest: Object,
+
+ /**
+ * If the diff fails to load, show the failure message in the diff rather
+ * than bubbling the error up to the whole page. This is useful for when
+ * loading inline diffs because one diff failing need not mark the whole
+ * page with a failure.
+ */
+ showLoadFailure: Boolean,
+
+ isBlameLoaded: {
+ type: Boolean,
+ notify: true,
+ computed: '_computeIsBlameLoaded(_blame)',
+ },
+
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+
+ _loading: {
+ type: Boolean,
+ value: false,
+ },
+
+ /** @type {?string} */
+ _errorMessage: {
+ type: String,
+ value: null,
+ },
+
+ /** @type {?Object} */
+ _baseImage: Object,
+ /** @type {?Object} */
+ _revisionImage: Object,
+ /**
+ * This is a DiffInfo object.
+ */
+ diff: {
+ type: Object,
+ notify: true,
+ },
+
+ /** @type {?Object} */
+ _blame: {
+ type: Object,
+ value: null,
+ },
+
+ /**
+ * @type {!Array<!Gerrit.CoverageRange>}
+ */
+ _coverageRanges: {
+ type: Array,
+ value: () => [],
+ },
+
+ _loadedWhitespaceLevel: String,
+
+ _parentIndex: {
+ type: Number,
+ computed: '_computeParentIndex(patchRange.*)',
+ },
+
+ _syntaxHighlightingEnabled: {
+ type: Boolean,
+ computed:
+ '_isSyntaxHighlightingEnabled(prefs.*, diff)',
+ },
+
+ _layers: {
+ type: Array,
+ value: [],
+ },
+ };
+ }
+
+ static get observers() {
+ return [
+ '_whitespaceChanged(prefs.ignore_whitespace, _loadedWhitespaceLevel,' +
+ ' noRenderOnPrefsChange)',
+ '_syntaxHighlightingChanged(noRenderOnPrefsChange, prefs.*)',
+ ];
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener(
+ // These are named inconsistently for a reason:
+ // The create-comment event is fired to indicate that we should
+ // create a comment.
+ // The comment-* events are just notifying that the comments did already
+ // change in some way, and that we should update any models we may want
+ // to keep in sync.
+ 'create-comment',
+ e => this._handleCreateComment(e));
+ this.addEventListener('comment-discard',
+ e => this._handleCommentDiscard(e));
+ this.addEventListener('comment-update',
+ e => this._handleCommentUpdate(e));
+ this.addEventListener('comment-save',
+ e => this._handleCommentSave(e));
+ this.addEventListener('render-start',
+ () => this._handleRenderStart());
+ this.addEventListener('render-content',
+ () => this._handleRenderContent());
+ this.addEventListener('normalize-range',
+ event => this._handleNormalizeRange(event));
+ this.addEventListener('diff-context-expanded',
+ event => this._handleDiffContextExpanded(event));
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ if (this._canReload()) {
+ this.reload();
+ }
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ });
+ }
+
+ /**
+ * @param {boolean=} shouldReportMetric indicate a new Diff Page. This is a
+ * signal to report metrics event that started on location change.
+ * @return {!Promise}
+ **/
+ reload(shouldReportMetric) {
+ this._loading = true;
+ this._errorMessage = null;
+ const whitespaceLevel = this._getIgnoreWhitespace();
+
+ const layers = [this.$.syntaxLayer];
+ // Get layers from plugins (if any).
+ for (const pluginLayer of this.$.jsAPI.getDiffLayers(
+ this.path, this.changeNum, this.patchNum)) {
+ layers.push(pluginLayer);
+ }
+ this._layers = layers;
+
+ if (shouldReportMetric) {
+ // We listen on render viewport only on DiffPage (on paramsChanged)
+ this._listenToViewportRender();
+ }
+
+ this._coverageRanges = [];
+ this._getCoverageData();
+ const diffRequest = this._getDiff()
+ .then(diff => {
+ this._loadedWhitespaceLevel = whitespaceLevel;
+ this._reportDiff(diff);
+ return diff;
+ })
+ .catch(e => {
+ this._handleGetDiffError(e);
+ return null;
+ });
+
+ const assetRequest = diffRequest.then(diff => {
+ // If the diff is null, then it's failed to load.
+ if (!diff) { return null; }
+
+ return this._loadDiffAssets(diff);
+ });
+
+ // Not waiting for coverage ranges intentionally as
+ // plugin loading should not block the content rendering
+ return Promise.all([diffRequest, assetRequest])
+ .then(results => {
+ const diff = results[0];
+ if (!diff) {
+ return Promise.resolve();
+ }
+ this.filesWeblinks = this._getFilesWeblinks(diff);
+ return new Promise(resolve => {
+ const callback = event => {
+ const needsSyntaxHighlighting = event.detail &&
+ event.detail.contentRendered;
+ if (needsSyntaxHighlighting) {
+ this.$.reporting.time(TimingLabel.SYNTAX);
+ this.$.syntaxLayer.process().then(() => {
+ this.$.reporting.timeEnd(TimingLabel.SYNTAX);
+ this.$.reporting.timeEnd(TimingLabel.TOTAL);
+ resolve();
+ });
+ } else {
+ this.$.reporting.timeEnd(TimingLabel.TOTAL);
+ resolve();
+ }
+ this.removeEventListener('render', callback);
+ if (shouldReportMetric) {
+ // We report diffViewContentDisplayed only on reload caused
+ // by params changed - expected only on Diff Page.
+ this.$.reporting.diffViewContentDisplayed();
+ }
+ };
+ this.addEventListener('render', callback);
+ this.diff = diff;
+ });
+ })
+ .catch(err => {
+ console.warn('Error encountered loading diff:', err);
+ })
+ .then(() => { this._loading = false; });
+ }
+
+ _getCoverageData() {
+ const {changeNum, path, patchRange: {basePatchNum, patchNum}} = this;
+ this.$.jsAPI.getCoverageAnnotationApi().
+ then(coverageAnnotationApi => {
+ if (!coverageAnnotationApi) return;
+ const provider = coverageAnnotationApi.getCoverageProvider();
+ return provider(changeNum, path, basePatchNum, patchNum)
+ .then(coverageRanges => {
+ if (!coverageRanges ||
+ changeNum !== this.changeNum ||
+ path !== this.path ||
+ basePatchNum !== this.patchRange.basePatchNum ||
+ patchNum !== this.patchRange.patchNum) {
+ return;
+ }
+
+ const existingCoverageRanges = this._coverageRanges;
+ this._coverageRanges = coverageRanges;
+
+ // Notify with existing coverage ranges
+ // in case there is some existing coverage data that needs to be removed
+ existingCoverageRanges.forEach(range => {
+ coverageAnnotationApi.notify(
+ path,
+ range.code_range.start_line,
+ range.code_range.end_line,
+ range.side);
+ });
+
+ // Notify with new coverage data
+ coverageRanges.forEach(range => {
+ coverageAnnotationApi.notify(
+ path,
+ range.code_range.start_line,
+ range.code_range.end_line,
+ range.side);
+ });
+ });
+ })
+ .catch(err => {
+ console.warn('Loading coverage ranges failed: ', err);
+ });
+ }
+
+ _getFilesWeblinks(diff) {
+ if (!this.commitRange) {
+ return {};
+ }
+ return {
+ meta_a: Gerrit.Nav.getFileWebLinks(
+ this.projectName, this.commitRange.baseCommit, this.path,
+ {weblinks: diff && diff.meta_a && diff.meta_a.web_links}),
+ meta_b: Gerrit.Nav.getFileWebLinks(
+ this.projectName, this.commitRange.commit, this.path,
+ {weblinks: diff && diff.meta_b && diff.meta_b.web_links}),
+ };
+ }
+
+ /** Cancel any remaining diff builder rendering work. */
+ cancel() {
+ this.$.diff.cancel();
+ }
+
+ /** @return {!Array<!HTMLElement>} */
+ getCursorStops() {
+ return this.$.diff.getCursorStops();
+ }
+
+ /** @return {boolean} */
+ isRangeSelected() {
+ return this.$.diff.isRangeSelected();
+ }
+
+ createRangeComment() {
+ return this.$.diff.createRangeComment();
+ }
+
+ toggleLeftDiff() {
+ this.$.diff.toggleLeftDiff();
+ }
+
+ /**
+ * Load and display blame information for the base of the diff.
+ *
+ * @return {Promise} A promise that resolves when blame finishes rendering.
+ */
+ loadBlame() {
+ return this.$.restAPI.getBlame(this.changeNum, this.patchRange.patchNum,
+ this.path, true)
+ .then(blame => {
+ if (!blame.length) {
+ this.fire('show-alert', {message: MSG_EMPTY_BLAME});
+ return Promise.reject(MSG_EMPTY_BLAME);
+ }
+
+ this._blame = blame;
+ });
+ }
+
+ /** Unload blame information for the diff. */
+ clearBlame() {
+ this._blame = null;
+ }
+
+ /**
+ * The thread elements in this diff, in no particular order.
+ *
+ * @return {!Array<!HTMLElement>}
+ */
+ getThreadEls() {
+ return Array.from(
+ dom(this.$.diff).querySelectorAll('.comment-thread'));
+ }
+
+ /** @param {HTMLElement} el */
+ addDraftAtLine(el) {
+ this.$.diff.addDraftAtLine(el);
+ }
+
+ clearDiffContent() {
+ this.$.diff.clearDiffContent();
+ }
+
+ expandAllContext() {
+ this.$.diff.expandAllContext();
+ }
+
+ /** @return {!Promise} */
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ /** @return {boolean}} */
+ _canReload() {
+ return !!this.changeNum && !!this.patchRange && !!this.path &&
+ !this.noAutoRender;
+ }
+
+ /** @return {!Promise<!Object>} */
+ _getDiff() {
+ // Wrap the diff request in a new promise so that the error handler
+ // rejects the promise, allowing the error to be handled in the .catch.
+ return new Promise((resolve, reject) => {
+ this.$.restAPI.getDiff(
+ this.changeNum,
+ this.patchRange.basePatchNum,
+ this.patchRange.patchNum,
+ this.path,
+ this._getIgnoreWhitespace(),
+ reject)
+ .then(resolve);
+ });
+ }
+
+ _handleGetDiffError(response) {
+ // Loading the diff may respond with 409 if the file is too large. In this
+ // case, use a toast error..
+ if (response.status === 409) {
+ this.fire('server-error', {response});
+ return;
+ }
+
+ if (this.showLoadFailure) {
+ this._errorMessage = [
+ 'Encountered error when loading the diff:',
+ response.status,
+ response.statusText,
+ ].join(' ');
+ return;
+ }
+
+ this.fire('page-error', {response});
+ }
+
+ /**
+ * Report info about the diff response.
+ */
+ _reportDiff(diff) {
+ if (!diff || !diff.content) {
+ return;
+ }
+
+ // Count the delta lines stemming from normal deltas, and from
+ // due_to_rebase deltas.
+ let nonRebaseDelta = 0;
+ let rebaseDelta = 0;
+ diff.content.forEach(chunk => {
+ if (chunk.ab) { return; }
+ const deltaSize = Math.max(
+ chunk.a ? chunk.a.length : 0, chunk.b ? chunk.b.length : 0);
+ if (chunk.due_to_rebase) {
+ rebaseDelta += deltaSize;
+ } else {
+ nonRebaseDelta += deltaSize;
+ }
+ });
+
+ // Find the percent of the delta from due_to_rebase chunks rounded to two
+ // digits. Diffs with no delta are considered 0%.
+ const totalDelta = rebaseDelta + nonRebaseDelta;
+ const percentRebaseDelta = !totalDelta ? 0 :
+ Math.round(100 * rebaseDelta / totalDelta);
+
+ // Report the due_to_rebase percentage in the "diff" category when
+ // applicable.
+ if (this.patchRange.basePatchNum === 'PARENT') {
+ this.$.reporting.reportInteraction(EVENT_AGAINST_PARENT);
+ } else if (percentRebaseDelta === 0) {
+ this.$.reporting.reportInteraction(EVENT_ZERO_REBASE);
+ } else {
+ this.$.reporting.reportInteraction(EVENT_NONZERO_REBASE,
+ {percentRebaseDelta});
+ }
+ }
+
+ /**
+ * @param {Object} diff
+ * @return {!Promise}
+ */
+ _loadDiffAssets(diff) {
+ if (isImageDiff(diff)) {
+ return this._getImages(diff).then(images => {
+ this._baseImage = images.baseImage;
+ this._revisionImage = images.revisionImage;
+ });
+ } else {
+ this._baseImage = null;
+ this._revisionImage = null;
+ return Promise.resolve();
+ }
+ }
/**
* @param {Object} diff
* @return {boolean}
*/
- function isImageDiff(diff) {
- if (!diff) { return false; }
-
- const isA = diff.meta_a &&
- diff.meta_a.content_type.startsWith('image/');
- const isB = diff.meta_b &&
- diff.meta_b.content_type.startsWith('image/');
-
- return !!(diff.binary && (isA || isB));
+ _computeIsImageDiff(diff) {
+ return isImageDiff(diff);
}
- /** @enum {string} */
- Gerrit.DiffSide = {
- LEFT: 'left',
- RIGHT: 'right',
- };
+ _commentsChanged(newComments) {
+ const allComments = [];
+ for (const side of [GrDiffBuilder.Side.LEFT, GrDiffBuilder.Side.RIGHT]) {
+ // This is needed by the threading.
+ for (const comment of newComments[side]) {
+ comment.__commentSide = side;
+ }
+ allComments.push(...newComments[side]);
+ }
+ // Currently, the only way this is ever changed here is when the initial
+ // comments are loaded, so it's okay performance wise to clear the threads
+ // and recreate them. If this changes in future, we might want to reuse
+ // some DOM nodes here.
+ this._clearThreads();
+ const threads = this._createThreads(allComments);
+ for (const thread of threads) {
+ const threadEl = this._createThreadElement(thread);
+ this._attachThreadElement(threadEl);
+ }
+ }
+
+ _sortComments(comments) {
+ return comments.slice(0).sort((a, b) => {
+ if (b.__draft && !a.__draft ) { return -1; }
+ if (a.__draft && !b.__draft ) { return 1; }
+ return util.parseDate(a.updated) - util.parseDate(b.updated);
+ });
+ }
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.PatchSetMixin
+ * @param {!Array<!Object>} comments
+ * @return {!Array<!Object>} Threads for the given comments.
*/
+ _createThreads(comments) {
+ const sortedComments = this._sortComments(comments);
+ const threads = [];
+ for (const comment of sortedComments) {
+ // If the comment is in reply to another comment, find that comment's
+ // thread and append to it.
+ if (comment.in_reply_to) {
+ const thread = threads.find(thread =>
+ thread.comments.some(c => c.id === comment.in_reply_to));
+ if (thread) {
+ thread.comments.push(comment);
+ continue;
+ }
+ }
+
+ // Otherwise, this comment starts its own thread.
+ const newThread = {
+ start_datetime: comment.updated,
+ comments: [comment],
+ commentSide: comment.__commentSide,
+ patchNum: comment.patch_set,
+ rootId: comment.id || comment.__draftID,
+ lineNum: comment.line,
+ isOnParent: comment.side === 'PARENT',
+ };
+ if (comment.range) {
+ newThread.range = Object.assign({}, comment.range);
+ }
+ threads.push(newThread);
+ }
+ return threads;
+ }
+
/**
- * Wrapper around gr-diff.
- *
- * Webcomponent fetching diffs and related data from restAPI and passing them
- * to the presentational gr-diff for rendering.
- *
- * @extends Polymer.Element
+ * @param {Object} blame
+ * @return {boolean}
*/
- class GrDiffHost extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.PatchSetBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-diff-host'; }
- /**
- * Fired when the user selects a line.
- *
- * @event line-selected
- */
+ _computeIsBlameLoaded(blame) {
+ return !!blame;
+ }
- /**
- * Fired if being logged in is required.
- *
- * @event show-auth-required
- */
+ /**
+ * @param {Object} diff
+ * @return {!Promise}
+ */
+ _getImages(diff) {
+ return this.$.restAPI.getImagesForDiff(this.changeNum, diff,
+ this.patchRange);
+ }
- /**
- * Fired when a comment is saved or discarded
- *
- * @event diff-comments-modified
- */
+ /** @param {CustomEvent} e */
+ _handleCreateComment(e) {
+ const {lineNum, side, patchNum, isOnParent, range} = e.detail;
+ const threadEl = this._getOrCreateThread(patchNum, lineNum, side, range,
+ isOnParent);
+ threadEl.addOrEditDraft(lineNum, range);
- static get properties() {
- return {
- changeNum: String,
- noAutoRender: {
- type: Boolean,
- value: false,
- },
- /** @type {?} */
- patchRange: Object,
- path: String,
- prefs: {
- type: Object,
- },
- projectName: String,
- displayLine: {
- type: Boolean,
- value: false,
- },
- isImageDiff: {
- type: Boolean,
- computed: '_computeIsImageDiff(diff)',
- notify: true,
- },
- commitRange: Object,
- filesWeblinks: {
- type: Object,
- value() {
- return {};
- },
- notify: true,
- },
- hidden: {
- type: Boolean,
- reflectToAttribute: true,
- },
- noRenderOnPrefsChange: {
- type: Boolean,
- value: false,
- },
- comments: {
- type: Object,
- observer: '_commentsChanged',
- },
- lineWrapping: {
- type: Boolean,
- value: false,
- },
- viewMode: {
- type: String,
- value: DiffViewMode.SIDE_BY_SIDE,
- },
+ this.$.reporting.recordDraftInteraction();
+ }
- /**
- * Special line number which should not be collapsed into a shared region.
- *
- * @type {{
- * number: number,
- * leftSide: {boolean}
- * }|null}
- */
- lineOfInterest: Object,
-
- /**
- * If the diff fails to load, show the failure message in the diff rather
- * than bubbling the error up to the whole page. This is useful for when
- * loading inline diffs because one diff failing need not mark the whole
- * page with a failure.
- */
- showLoadFailure: Boolean,
-
- isBlameLoaded: {
- type: Boolean,
- notify: true,
- computed: '_computeIsBlameLoaded(_blame)',
- },
-
- _loggedIn: {
- type: Boolean,
- value: false,
- },
-
- _loading: {
- type: Boolean,
- value: false,
- },
-
- /** @type {?string} */
- _errorMessage: {
- type: String,
- value: null,
- },
-
- /** @type {?Object} */
- _baseImage: Object,
- /** @type {?Object} */
- _revisionImage: Object,
- /**
- * This is a DiffInfo object.
- */
- diff: {
- type: Object,
- notify: true,
- },
-
- /** @type {?Object} */
- _blame: {
- type: Object,
- value: null,
- },
-
- /**
- * @type {!Array<!Gerrit.CoverageRange>}
- */
- _coverageRanges: {
- type: Array,
- value: () => [],
- },
-
- _loadedWhitespaceLevel: String,
-
- _parentIndex: {
- type: Number,
- computed: '_computeParentIndex(patchRange.*)',
- },
-
- _syntaxHighlightingEnabled: {
- type: Boolean,
- computed:
- '_isSyntaxHighlightingEnabled(prefs.*, diff)',
- },
-
- _layers: {
- type: Array,
- value: [],
- },
- };
- }
-
- static get observers() {
- return [
- '_whitespaceChanged(prefs.ignore_whitespace, _loadedWhitespaceLevel,' +
- ' noRenderOnPrefsChange)',
- '_syntaxHighlightingChanged(noRenderOnPrefsChange, prefs.*)',
- ];
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener(
- // These are named inconsistently for a reason:
- // The create-comment event is fired to indicate that we should
- // create a comment.
- // The comment-* events are just notifying that the comments did already
- // change in some way, and that we should update any models we may want
- // to keep in sync.
- 'create-comment',
- e => this._handleCreateComment(e));
- this.addEventListener('comment-discard',
- e => this._handleCommentDiscard(e));
- this.addEventListener('comment-update',
- e => this._handleCommentUpdate(e));
- this.addEventListener('comment-save',
- e => this._handleCommentSave(e));
- this.addEventListener('render-start',
- () => this._handleRenderStart());
- this.addEventListener('render-content',
- () => this._handleRenderContent());
- this.addEventListener('normalize-range',
- event => this._handleNormalizeRange(event));
- this.addEventListener('diff-context-expanded',
- event => this._handleDiffContextExpanded(event));
- }
-
- /** @override */
- ready() {
- super.ready();
- if (this._canReload()) {
- this.reload();
- }
- }
-
- /** @override */
- attached() {
- super.attached();
- this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
+ /**
+ * Gets or creates a comment thread at a given location.
+ * May provide a range, to get/create a range comment.
+ *
+ * @param {string} patchNum
+ * @param {?number} lineNum
+ * @param {string} commentSide
+ * @param {Gerrit.Range|undefined} range
+ * @param {boolean} isOnParent
+ * @return {!Object}
+ */
+ _getOrCreateThread(patchNum, lineNum, commentSide, range, isOnParent) {
+ let threadEl = this._getThreadEl(lineNum, commentSide, range);
+ if (!threadEl) {
+ threadEl = this._createThreadElement({
+ comments: [],
+ commentSide,
+ patchNum,
+ lineNum,
+ range,
+ isOnParent,
});
+ this._attachThreadElement(threadEl);
+ }
+ return threadEl;
+ }
+
+ _attachThreadElement(threadEl) {
+ dom(this.$.diff).appendChild(threadEl);
+ }
+
+ _clearThreads() {
+ for (const threadEl of this.getThreadEls()) {
+ const parent = dom(threadEl).parentNode;
+ dom(parent).removeChild(threadEl);
+ }
+ }
+
+ _createThreadElement(thread) {
+ const threadEl = document.createElement('gr-comment-thread');
+ threadEl.className = 'comment-thread';
+ threadEl.setAttribute('slot', `${thread.commentSide}-${thread.lineNum}`);
+ threadEl.comments = thread.comments;
+ threadEl.commentSide = thread.commentSide;
+ threadEl.isOnParent = !!thread.isOnParent;
+ threadEl.parentIndex = this._parentIndex;
+ threadEl.changeNum = this.changeNum;
+ threadEl.patchNum = thread.patchNum;
+ threadEl.lineNum = thread.lineNum;
+ const rootIdChangedListener = changeEvent => {
+ thread.rootId = changeEvent.detail.value;
+ };
+ threadEl.addEventListener('root-id-changed', rootIdChangedListener);
+ threadEl.path = this.path;
+ threadEl.projectName = this.projectName;
+ threadEl.range = thread.range;
+ const threadDiscardListener = e => {
+ const threadEl = /** @type {!Node} */ (e.currentTarget);
+
+ const parent = dom(threadEl).parentNode;
+ dom(parent).removeChild(threadEl);
+
+ threadEl.removeEventListener('root-id-changed', rootIdChangedListener);
+ threadEl.removeEventListener('thread-discard', threadDiscardListener);
+ };
+ threadEl.addEventListener('thread-discard', threadDiscardListener);
+ return threadEl;
+ }
+
+ /**
+ * Gets a comment thread element at a given location.
+ * May provide a range, to get a range comment.
+ *
+ * @param {?number} lineNum
+ * @param {string} commentSide
+ * @param {!Gerrit.Range=} range
+ * @return {?Node}
+ */
+ _getThreadEl(lineNum, commentSide, range = undefined) {
+ let line;
+ if (commentSide === GrDiffBuilder.Side.LEFT) {
+ line = {beforeNumber: lineNum};
+ } else if (commentSide === GrDiffBuilder.Side.RIGHT) {
+ line = {afterNumber: lineNum};
+ } else {
+ throw new Error(`Unknown side: ${commentSide}`);
+ }
+ function matchesRange(threadEl) {
+ const threadRange = /** @type {!Gerrit.Range} */(
+ JSON.parse(threadEl.getAttribute('range')));
+ return Gerrit.rangesEqual(threadRange, range);
}
- /**
- * @param {boolean=} shouldReportMetric indicate a new Diff Page. This is a
- * signal to report metrics event that started on location change.
- * @return {!Promise}
- **/
- reload(shouldReportMetric) {
- this._loading = true;
- this._errorMessage = null;
- const whitespaceLevel = this._getIgnoreWhitespace();
+ const filteredThreadEls = this._filterThreadElsForLocation(
+ this.getThreadEls(), line, commentSide).filter(matchesRange);
+ return filteredThreadEls.length ? filteredThreadEls[0] : null;
+ }
- const layers = [this.$.syntaxLayer];
- // Get layers from plugins (if any).
- for (const pluginLayer of this.$.jsAPI.getDiffLayers(
- this.path, this.changeNum, this.patchNum)) {
- layers.push(pluginLayer);
- }
- this._layers = layers;
-
- if (shouldReportMetric) {
- // We listen on render viewport only on DiffPage (on paramsChanged)
- this._listenToViewportRender();
- }
-
- this._coverageRanges = [];
- this._getCoverageData();
- const diffRequest = this._getDiff()
- .then(diff => {
- this._loadedWhitespaceLevel = whitespaceLevel;
- this._reportDiff(diff);
- return diff;
- })
- .catch(e => {
- this._handleGetDiffError(e);
- return null;
- });
-
- const assetRequest = diffRequest.then(diff => {
- // If the diff is null, then it's failed to load.
- if (!diff) { return null; }
-
- return this._loadDiffAssets(diff);
- });
-
- // Not waiting for coverage ranges intentionally as
- // plugin loading should not block the content rendering
- return Promise.all([diffRequest, assetRequest])
- .then(results => {
- const diff = results[0];
- if (!diff) {
- return Promise.resolve();
- }
- this.filesWeblinks = this._getFilesWeblinks(diff);
- return new Promise(resolve => {
- const callback = event => {
- const needsSyntaxHighlighting = event.detail &&
- event.detail.contentRendered;
- if (needsSyntaxHighlighting) {
- this.$.reporting.time(TimingLabel.SYNTAX);
- this.$.syntaxLayer.process().then(() => {
- this.$.reporting.timeEnd(TimingLabel.SYNTAX);
- this.$.reporting.timeEnd(TimingLabel.TOTAL);
- resolve();
- });
- } else {
- this.$.reporting.timeEnd(TimingLabel.TOTAL);
- resolve();
- }
- this.removeEventListener('render', callback);
- if (shouldReportMetric) {
- // We report diffViewContentDisplayed only on reload caused
- // by params changed - expected only on Diff Page.
- this.$.reporting.diffViewContentDisplayed();
- }
- };
- this.addEventListener('render', callback);
- this.diff = diff;
- });
- })
- .catch(err => {
- console.warn('Error encountered loading diff:', err);
- })
- .then(() => { this._loading = false; });
+ /**
+ * @param {!Array<!HTMLElement>} threadEls
+ * @param {!{beforeNumber: (number|string|undefined|null),
+ * afterNumber: (number|string|undefined|null)}}
+ * lineInfo
+ * @param {!Gerrit.DiffSide=} side The side (LEFT, RIGHT) for
+ * which to return the threads.
+ * @return {!Array<!HTMLElement>} The thread elements matching the given
+ * location.
+ */
+ _filterThreadElsForLocation(threadEls, lineInfo, side) {
+ function matchesLeftLine(threadEl) {
+ return threadEl.getAttribute('comment-side') ==
+ Gerrit.DiffSide.LEFT &&
+ threadEl.getAttribute('line-num') == lineInfo.beforeNumber;
+ }
+ function matchesRightLine(threadEl) {
+ return threadEl.getAttribute('comment-side') ==
+ Gerrit.DiffSide.RIGHT &&
+ threadEl.getAttribute('line-num') == lineInfo.afterNumber;
+ }
+ function matchesFileComment(threadEl) {
+ return threadEl.getAttribute('comment-side') == side &&
+ // line/range comments have 1-based line set, if line is falsy it's
+ // a file comment
+ !threadEl.getAttribute('line-num');
}
- _getCoverageData() {
- const {changeNum, path, patchRange: {basePatchNum, patchNum}} = this;
- this.$.jsAPI.getCoverageAnnotationApi().
- then(coverageAnnotationApi => {
- if (!coverageAnnotationApi) return;
- const provider = coverageAnnotationApi.getCoverageProvider();
- return provider(changeNum, path, basePatchNum, patchNum)
- .then(coverageRanges => {
- if (!coverageRanges ||
- changeNum !== this.changeNum ||
- path !== this.path ||
- basePatchNum !== this.patchRange.basePatchNum ||
- patchNum !== this.patchRange.patchNum) {
- return;
- }
+ // Select the appropriate matchers for the desired side and line
+ // If side is BOTH, we want both the left and right matcher.
+ const matchers = [];
+ if (side !== Gerrit.DiffSide.RIGHT) {
+ matchers.push(matchesLeftLine);
+ }
+ if (side !== Gerrit.DiffSide.LEFT) {
+ matchers.push(matchesRightLine);
+ }
+ if (lineInfo.afterNumber === 'FILE' ||
+ lineInfo.beforeNumber === 'FILE') {
+ matchers.push(matchesFileComment);
+ }
+ return threadEls.filter(threadEl =>
+ matchers.some(matcher => matcher(threadEl)));
+ }
- const existingCoverageRanges = this._coverageRanges;
- this._coverageRanges = coverageRanges;
+ _getIgnoreWhitespace() {
+ if (!this.prefs || !this.prefs.ignore_whitespace) {
+ return WHITESPACE_IGNORE_NONE;
+ }
+ return this.prefs.ignore_whitespace;
+ }
- // Notify with existing coverage ranges
- // in case there is some existing coverage data that needs to be removed
- existingCoverageRanges.forEach(range => {
- coverageAnnotationApi.notify(
- path,
- range.code_range.start_line,
- range.code_range.end_line,
- range.side);
- });
-
- // Notify with new coverage data
- coverageRanges.forEach(range => {
- coverageAnnotationApi.notify(
- path,
- range.code_range.start_line,
- range.code_range.end_line,
- range.side);
- });
- });
- })
- .catch(err => {
- console.warn('Loading coverage ranges failed: ', err);
- });
+ _whitespaceChanged(
+ preferredWhitespaceLevel, loadedWhitespaceLevel,
+ noRenderOnPrefsChange) {
+ // Polymer 2: check for undefined
+ if ([
+ preferredWhitespaceLevel,
+ loadedWhitespaceLevel,
+ noRenderOnPrefsChange,
+ ].some(arg => arg === undefined)) {
+ return;
}
- _getFilesWeblinks(diff) {
- if (!this.commitRange) {
- return {};
- }
- return {
- meta_a: Gerrit.Nav.getFileWebLinks(
- this.projectName, this.commitRange.baseCommit, this.path,
- {weblinks: diff && diff.meta_a && diff.meta_a.web_links}),
- meta_b: Gerrit.Nav.getFileWebLinks(
- this.projectName, this.commitRange.commit, this.path,
- {weblinks: diff && diff.meta_b && diff.meta_b.web_links}),
- };
+ if (preferredWhitespaceLevel !== loadedWhitespaceLevel &&
+ !noRenderOnPrefsChange) {
+ this.reload();
+ }
+ }
+
+ _syntaxHighlightingChanged(noRenderOnPrefsChange, prefsChangeRecord) {
+ // Polymer 2: check for undefined
+ if ([
+ noRenderOnPrefsChange,
+ prefsChangeRecord,
+ ].some(arg => arg === undefined)) {
+ return;
}
- /** Cancel any remaining diff builder rendering work. */
- cancel() {
- this.$.diff.cancel();
+ if (prefsChangeRecord.path !== 'prefs.syntax_highlighting') {
+ return;
}
- /** @return {!Array<!HTMLElement>} */
- getCursorStops() {
- return this.$.diff.getCursorStops();
+ if (!noRenderOnPrefsChange) {
+ this.reload();
}
+ }
- /** @return {boolean} */
- isRangeSelected() {
- return this.$.diff.isRangeSelected();
+ /**
+ * @param {Object} patchRangeRecord
+ * @return {number|null}
+ */
+ _computeParentIndex(patchRangeRecord) {
+ return this.isMergeParent(patchRangeRecord.base.basePatchNum) ?
+ this.getParentIndex(patchRangeRecord.base.basePatchNum) : null;
+ }
+
+ _handleCommentSave(e) {
+ const comment = e.detail.comment;
+ const side = e.detail.comment.__commentSide;
+ const idx = this._findDraftIndex(comment, side);
+ this.set(['comments', side, idx], comment);
+ this._handleCommentSaveOrDiscard();
+ }
+
+ _handleCommentDiscard(e) {
+ const comment = e.detail.comment;
+ this._removeComment(comment);
+ this._handleCommentSaveOrDiscard();
+ }
+
+ /**
+ * Closure annotation for Polymer.prototype.push is off. Submitted PR:
+ * https://github.com/Polymer/polymer/pull/4776
+ * but for not supressing annotations.
+ *
+ * @suppress {checkTypes}
+ */
+ _handleCommentUpdate(e) {
+ const comment = e.detail.comment;
+ const side = e.detail.comment.__commentSide;
+ let idx = this._findCommentIndex(comment, side);
+ if (idx === -1) {
+ idx = this._findDraftIndex(comment, side);
}
-
- createRangeComment() {
- return this.$.diff.createRangeComment();
- }
-
- toggleLeftDiff() {
- this.$.diff.toggleLeftDiff();
- }
-
- /**
- * Load and display blame information for the base of the diff.
- *
- * @return {Promise} A promise that resolves when blame finishes rendering.
- */
- loadBlame() {
- return this.$.restAPI.getBlame(this.changeNum, this.patchRange.patchNum,
- this.path, true)
- .then(blame => {
- if (!blame.length) {
- this.fire('show-alert', {message: MSG_EMPTY_BLAME});
- return Promise.reject(MSG_EMPTY_BLAME);
- }
-
- this._blame = blame;
- });
- }
-
- /** Unload blame information for the diff. */
- clearBlame() {
- this._blame = null;
- }
-
- /**
- * The thread elements in this diff, in no particular order.
- *
- * @return {!Array<!HTMLElement>}
- */
- getThreadEls() {
- return Array.from(
- Polymer.dom(this.$.diff).querySelectorAll('.comment-thread'));
- }
-
- /** @param {HTMLElement} el */
- addDraftAtLine(el) {
- this.$.diff.addDraftAtLine(el);
- }
-
- clearDiffContent() {
- this.$.diff.clearDiffContent();
- }
-
- expandAllContext() {
- this.$.diff.expandAllContext();
- }
-
- /** @return {!Promise} */
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- /** @return {boolean}} */
- _canReload() {
- return !!this.changeNum && !!this.patchRange && !!this.path &&
- !this.noAutoRender;
- }
-
- /** @return {!Promise<!Object>} */
- _getDiff() {
- // Wrap the diff request in a new promise so that the error handler
- // rejects the promise, allowing the error to be handled in the .catch.
- return new Promise((resolve, reject) => {
- this.$.restAPI.getDiff(
- this.changeNum,
- this.patchRange.basePatchNum,
- this.patchRange.patchNum,
- this.path,
- this._getIgnoreWhitespace(),
- reject)
- .then(resolve);
- });
- }
-
- _handleGetDiffError(response) {
- // Loading the diff may respond with 409 if the file is too large. In this
- // case, use a toast error..
- if (response.status === 409) {
- this.fire('server-error', {response});
- return;
- }
-
- if (this.showLoadFailure) {
- this._errorMessage = [
- 'Encountered error when loading the diff:',
- response.status,
- response.statusText,
- ].join(' ');
- return;
- }
-
- this.fire('page-error', {response});
- }
-
- /**
- * Report info about the diff response.
- */
- _reportDiff(diff) {
- if (!diff || !diff.content) {
- return;
- }
-
- // Count the delta lines stemming from normal deltas, and from
- // due_to_rebase deltas.
- let nonRebaseDelta = 0;
- let rebaseDelta = 0;
- diff.content.forEach(chunk => {
- if (chunk.ab) { return; }
- const deltaSize = Math.max(
- chunk.a ? chunk.a.length : 0, chunk.b ? chunk.b.length : 0);
- if (chunk.due_to_rebase) {
- rebaseDelta += deltaSize;
- } else {
- nonRebaseDelta += deltaSize;
- }
- });
-
- // Find the percent of the delta from due_to_rebase chunks rounded to two
- // digits. Diffs with no delta are considered 0%.
- const totalDelta = rebaseDelta + nonRebaseDelta;
- const percentRebaseDelta = !totalDelta ? 0 :
- Math.round(100 * rebaseDelta / totalDelta);
-
- // Report the due_to_rebase percentage in the "diff" category when
- // applicable.
- if (this.patchRange.basePatchNum === 'PARENT') {
- this.$.reporting.reportInteraction(EVENT_AGAINST_PARENT);
- } else if (percentRebaseDelta === 0) {
- this.$.reporting.reportInteraction(EVENT_ZERO_REBASE);
- } else {
- this.$.reporting.reportInteraction(EVENT_NONZERO_REBASE,
- {percentRebaseDelta});
- }
- }
-
- /**
- * @param {Object} diff
- * @return {!Promise}
- */
- _loadDiffAssets(diff) {
- if (isImageDiff(diff)) {
- return this._getImages(diff).then(images => {
- this._baseImage = images.baseImage;
- this._revisionImage = images.revisionImage;
- });
- } else {
- this._baseImage = null;
- this._revisionImage = null;
- return Promise.resolve();
- }
- }
-
- /**
- * @param {Object} diff
- * @return {boolean}
- */
- _computeIsImageDiff(diff) {
- return isImageDiff(diff);
- }
-
- _commentsChanged(newComments) {
- const allComments = [];
- for (const side of [GrDiffBuilder.Side.LEFT, GrDiffBuilder.Side.RIGHT]) {
- // This is needed by the threading.
- for (const comment of newComments[side]) {
- comment.__commentSide = side;
- }
- allComments.push(...newComments[side]);
- }
- // Currently, the only way this is ever changed here is when the initial
- // comments are loaded, so it's okay performance wise to clear the threads
- // and recreate them. If this changes in future, we might want to reuse
- // some DOM nodes here.
- this._clearThreads();
- const threads = this._createThreads(allComments);
- for (const thread of threads) {
- const threadEl = this._createThreadElement(thread);
- this._attachThreadElement(threadEl);
- }
- }
-
- _sortComments(comments) {
- return comments.slice(0).sort((a, b) => {
- if (b.__draft && !a.__draft ) { return -1; }
- if (a.__draft && !b.__draft ) { return 1; }
- return util.parseDate(a.updated) - util.parseDate(b.updated);
- });
- }
-
- /**
- * @param {!Array<!Object>} comments
- * @return {!Array<!Object>} Threads for the given comments.
- */
- _createThreads(comments) {
- const sortedComments = this._sortComments(comments);
- const threads = [];
- for (const comment of sortedComments) {
- // If the comment is in reply to another comment, find that comment's
- // thread and append to it.
- if (comment.in_reply_to) {
- const thread = threads.find(thread =>
- thread.comments.some(c => c.id === comment.in_reply_to));
- if (thread) {
- thread.comments.push(comment);
- continue;
- }
- }
-
- // Otherwise, this comment starts its own thread.
- const newThread = {
- start_datetime: comment.updated,
- comments: [comment],
- commentSide: comment.__commentSide,
- patchNum: comment.patch_set,
- rootId: comment.id || comment.__draftID,
- lineNum: comment.line,
- isOnParent: comment.side === 'PARENT',
- };
- if (comment.range) {
- newThread.range = Object.assign({}, comment.range);
- }
- threads.push(newThread);
- }
- return threads;
- }
-
- /**
- * @param {Object} blame
- * @return {boolean}
- */
- _computeIsBlameLoaded(blame) {
- return !!blame;
- }
-
- /**
- * @param {Object} diff
- * @return {!Promise}
- */
- _getImages(diff) {
- return this.$.restAPI.getImagesForDiff(this.changeNum, diff,
- this.patchRange);
- }
-
- /** @param {CustomEvent} e */
- _handleCreateComment(e) {
- const {lineNum, side, patchNum, isOnParent, range} = e.detail;
- const threadEl = this._getOrCreateThread(patchNum, lineNum, side, range,
- isOnParent);
- threadEl.addOrEditDraft(lineNum, range);
-
- this.$.reporting.recordDraftInteraction();
- }
-
- /**
- * Gets or creates a comment thread at a given location.
- * May provide a range, to get/create a range comment.
- *
- * @param {string} patchNum
- * @param {?number} lineNum
- * @param {string} commentSide
- * @param {Gerrit.Range|undefined} range
- * @param {boolean} isOnParent
- * @return {!Object}
- */
- _getOrCreateThread(patchNum, lineNum, commentSide, range, isOnParent) {
- let threadEl = this._getThreadEl(lineNum, commentSide, range);
- if (!threadEl) {
- threadEl = this._createThreadElement({
- comments: [],
- commentSide,
- patchNum,
- lineNum,
- range,
- isOnParent,
- });
- this._attachThreadElement(threadEl);
- }
- return threadEl;
- }
-
- _attachThreadElement(threadEl) {
- Polymer.dom(this.$.diff).appendChild(threadEl);
- }
-
- _clearThreads() {
- for (const threadEl of this.getThreadEls()) {
- const parent = Polymer.dom(threadEl).parentNode;
- Polymer.dom(parent).removeChild(threadEl);
- }
- }
-
- _createThreadElement(thread) {
- const threadEl = document.createElement('gr-comment-thread');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('slot', `${thread.commentSide}-${thread.lineNum}`);
- threadEl.comments = thread.comments;
- threadEl.commentSide = thread.commentSide;
- threadEl.isOnParent = !!thread.isOnParent;
- threadEl.parentIndex = this._parentIndex;
- threadEl.changeNum = this.changeNum;
- threadEl.patchNum = thread.patchNum;
- threadEl.lineNum = thread.lineNum;
- const rootIdChangedListener = changeEvent => {
- thread.rootId = changeEvent.detail.value;
- };
- threadEl.addEventListener('root-id-changed', rootIdChangedListener);
- threadEl.path = this.path;
- threadEl.projectName = this.projectName;
- threadEl.range = thread.range;
- const threadDiscardListener = e => {
- const threadEl = /** @type {!Node} */ (e.currentTarget);
-
- const parent = Polymer.dom(threadEl).parentNode;
- Polymer.dom(parent).removeChild(threadEl);
-
- threadEl.removeEventListener('root-id-changed', rootIdChangedListener);
- threadEl.removeEventListener('thread-discard', threadDiscardListener);
- };
- threadEl.addEventListener('thread-discard', threadDiscardListener);
- return threadEl;
- }
-
- /**
- * Gets a comment thread element at a given location.
- * May provide a range, to get a range comment.
- *
- * @param {?number} lineNum
- * @param {string} commentSide
- * @param {!Gerrit.Range=} range
- * @return {?Node}
- */
- _getThreadEl(lineNum, commentSide, range = undefined) {
- let line;
- if (commentSide === GrDiffBuilder.Side.LEFT) {
- line = {beforeNumber: lineNum};
- } else if (commentSide === GrDiffBuilder.Side.RIGHT) {
- line = {afterNumber: lineNum};
- } else {
- throw new Error(`Unknown side: ${commentSide}`);
- }
- function matchesRange(threadEl) {
- const threadRange = /** @type {!Gerrit.Range} */(
- JSON.parse(threadEl.getAttribute('range')));
- return Gerrit.rangesEqual(threadRange, range);
- }
-
- const filteredThreadEls = this._filterThreadElsForLocation(
- this.getThreadEls(), line, commentSide).filter(matchesRange);
- return filteredThreadEls.length ? filteredThreadEls[0] : null;
- }
-
- /**
- * @param {!Array<!HTMLElement>} threadEls
- * @param {!{beforeNumber: (number|string|undefined|null),
- * afterNumber: (number|string|undefined|null)}}
- * lineInfo
- * @param {!Gerrit.DiffSide=} side The side (LEFT, RIGHT) for
- * which to return the threads.
- * @return {!Array<!HTMLElement>} The thread elements matching the given
- * location.
- */
- _filterThreadElsForLocation(threadEls, lineInfo, side) {
- function matchesLeftLine(threadEl) {
- return threadEl.getAttribute('comment-side') ==
- Gerrit.DiffSide.LEFT &&
- threadEl.getAttribute('line-num') == lineInfo.beforeNumber;
- }
- function matchesRightLine(threadEl) {
- return threadEl.getAttribute('comment-side') ==
- Gerrit.DiffSide.RIGHT &&
- threadEl.getAttribute('line-num') == lineInfo.afterNumber;
- }
- function matchesFileComment(threadEl) {
- return threadEl.getAttribute('comment-side') == side &&
- // line/range comments have 1-based line set, if line is falsy it's
- // a file comment
- !threadEl.getAttribute('line-num');
- }
-
- // Select the appropriate matchers for the desired side and line
- // If side is BOTH, we want both the left and right matcher.
- const matchers = [];
- if (side !== Gerrit.DiffSide.RIGHT) {
- matchers.push(matchesLeftLine);
- }
- if (side !== Gerrit.DiffSide.LEFT) {
- matchers.push(matchesRightLine);
- }
- if (lineInfo.afterNumber === 'FILE' ||
- lineInfo.beforeNumber === 'FILE') {
- matchers.push(matchesFileComment);
- }
- return threadEls.filter(threadEl =>
- matchers.some(matcher => matcher(threadEl)));
- }
-
- _getIgnoreWhitespace() {
- if (!this.prefs || !this.prefs.ignore_whitespace) {
- return WHITESPACE_IGNORE_NONE;
- }
- return this.prefs.ignore_whitespace;
- }
-
- _whitespaceChanged(
- preferredWhitespaceLevel, loadedWhitespaceLevel,
- noRenderOnPrefsChange) {
- // Polymer 2: check for undefined
- if ([
- preferredWhitespaceLevel,
- loadedWhitespaceLevel,
- noRenderOnPrefsChange,
- ].some(arg => arg === undefined)) {
- return;
- }
-
- if (preferredWhitespaceLevel !== loadedWhitespaceLevel &&
- !noRenderOnPrefsChange) {
- this.reload();
- }
- }
-
- _syntaxHighlightingChanged(noRenderOnPrefsChange, prefsChangeRecord) {
- // Polymer 2: check for undefined
- if ([
- noRenderOnPrefsChange,
- prefsChangeRecord,
- ].some(arg => arg === undefined)) {
- return;
- }
-
- if (prefsChangeRecord.path !== 'prefs.syntax_highlighting') {
- return;
- }
-
- if (!noRenderOnPrefsChange) {
- this.reload();
- }
- }
-
- /**
- * @param {Object} patchRangeRecord
- * @return {number|null}
- */
- _computeParentIndex(patchRangeRecord) {
- return this.isMergeParent(patchRangeRecord.base.basePatchNum) ?
- this.getParentIndex(patchRangeRecord.base.basePatchNum) : null;
- }
-
- _handleCommentSave(e) {
- const comment = e.detail.comment;
- const side = e.detail.comment.__commentSide;
- const idx = this._findDraftIndex(comment, side);
+ if (idx !== -1) { // Update draft or comment.
this.set(['comments', side, idx], comment);
- this._handleCommentSaveOrDiscard();
- }
-
- _handleCommentDiscard(e) {
- const comment = e.detail.comment;
- this._removeComment(comment);
- this._handleCommentSaveOrDiscard();
- }
-
- /**
- * Closure annotation for Polymer.prototype.push is off. Submitted PR:
- * https://github.com/Polymer/polymer/pull/4776
- * but for not supressing annotations.
- *
- * @suppress {checkTypes}
- */
- _handleCommentUpdate(e) {
- const comment = e.detail.comment;
- const side = e.detail.comment.__commentSide;
- let idx = this._findCommentIndex(comment, side);
- if (idx === -1) {
- idx = this._findDraftIndex(comment, side);
- }
- if (idx !== -1) { // Update draft or comment.
- this.set(['comments', side, idx], comment);
- } else { // Create new draft.
- this.push(['comments', side], comment);
- }
- }
-
- _handleCommentSaveOrDiscard() {
- this.dispatchEvent(new CustomEvent(
- 'diff-comments-modified', {bubbles: true, composed: true}));
- }
-
- _removeComment(comment) {
- const side = comment.__commentSide;
- this._removeCommentFromSide(comment, side);
- }
-
- _removeCommentFromSide(comment, side) {
- let idx = this._findCommentIndex(comment, side);
- if (idx === -1) {
- idx = this._findDraftIndex(comment, side);
- }
- if (idx !== -1) {
- this.splice('comments.' + side, idx, 1);
- }
- }
-
- /** @return {number} */
- _findCommentIndex(comment, side) {
- if (!comment.id || !this.comments[side]) {
- return -1;
- }
- return this.comments[side].findIndex(item => item.id === comment.id);
- }
-
- /** @return {number} */
- _findDraftIndex(comment, side) {
- if (!comment.__draftID || !this.comments[side]) {
- return -1;
- }
- return this.comments[side].findIndex(
- item => item.__draftID === comment.__draftID);
- }
-
- _isSyntaxHighlightingEnabled(preferenceChangeRecord, diff) {
- if (!preferenceChangeRecord ||
- !preferenceChangeRecord.base ||
- !preferenceChangeRecord.base.syntax_highlighting ||
- !diff) {
- return false;
- }
- return !this._anyLineTooLong(diff) &&
- this.$.diff.getDiffLength(diff) <= SYNTAX_MAX_DIFF_LENGTH;
- }
-
- /**
- * @return {boolean} whether any of the lines in diff are longer
- * than SYNTAX_MAX_LINE_LENGTH.
- */
- _anyLineTooLong(diff) {
- if (!diff) return false;
- return diff.content.some(section => {
- const lines = section.ab ?
- section.ab :
- (section.a || []).concat(section.b || []);
- return lines.some(line => line.length >= SYNTAX_MAX_LINE_LENGTH);
- });
- }
-
- _listenToViewportRender() {
- const renderUpdateListener = start => {
- if (start > NUM_OF_LINES_THRESHOLD_FOR_VIEWPORT) {
- this.$.reporting.diffViewDisplayed();
- this.$.syntaxLayer.removeListener(renderUpdateListener);
- }
- };
-
- this.$.syntaxLayer.addListener(renderUpdateListener);
- }
-
- _handleRenderStart() {
- this.$.reporting.time(TimingLabel.TOTAL);
- this.$.reporting.time(TimingLabel.CONTENT);
- }
-
- _handleRenderContent() {
- this.$.reporting.timeEnd(TimingLabel.CONTENT);
- }
-
- _handleNormalizeRange(event) {
- this.$.reporting.reportInteraction('normalize-range',
- {
- side: event.detail.side,
- lineNum: event.detail.lineNum,
- });
- }
-
- _handleDiffContextExpanded(event) {
- this.$.reporting.reportInteraction(
- 'diff-context-expanded', {numLines: event.detail.numLines}
- );
- }
-
- /**
- * Find the last chunk for the given side.
- *
- * @param {!Object} diff
- * @param {boolean} leftSide true if checking the base of the diff,
- * false if testing the revision.
- * @return {Object|null} returns the chunk object or null if there was
- * no chunk for that side.
- */
- _lastChunkForSide(diff, leftSide) {
- if (!diff.content.length) { return null; }
-
- let chunkIndex = diff.content.length;
- let chunk;
-
- // Walk backwards until we find a chunk for the given side.
- do {
- chunkIndex--;
- chunk = diff.content[chunkIndex];
- } while (
- // We haven't reached the beginning.
- chunkIndex >= 0 &&
-
- // The chunk doesn't have both sides.
- !chunk.ab &&
-
- // The chunk doesn't have the given side.
- ((leftSide && (!chunk.a || !chunk.a.length)) ||
- (!leftSide && (!chunk.b || !chunk.b.length))));
-
- // If we reached the beginning of the diff and failed to find a chunk
- // with the given side, return null.
- if (chunkIndex === -1) { return null; }
-
- return chunk;
- }
-
- /**
- * Check whether the specified side of the diff has a trailing newline.
- *
- * @param {!Object} diff
- * @param {boolean} leftSide true if checking the base of the diff,
- * false if testing the revision.
- * @return {boolean|null} Return true if the side has a trailing newline.
- * Return false if it doesn't. Return null if not applicable (for
- * example, if the diff has no content on the specified side).
- */
- _hasTrailingNewlines(diff, leftSide) {
- const chunk = this._lastChunkForSide(diff, leftSide);
- if (!chunk) { return null; }
- let lines;
- if (chunk.ab) {
- lines = chunk.ab;
- } else {
- lines = leftSide ? chunk.a : chunk.b;
- }
- return lines[lines.length - 1] === '';
- }
-
- _showNewlineWarningLeft(diff) {
- return this._hasTrailingNewlines(diff, true) === false;
- }
-
- _showNewlineWarningRight(diff) {
- return this._hasTrailingNewlines(diff, false) === false;
+ } else { // Create new draft.
+ this.push(['comments', side], comment);
}
}
- customElements.define(GrDiffHost.is, GrDiffHost);
-})();
+ _handleCommentSaveOrDiscard() {
+ this.dispatchEvent(new CustomEvent(
+ 'diff-comments-modified', {bubbles: true, composed: true}));
+ }
+
+ _removeComment(comment) {
+ const side = comment.__commentSide;
+ this._removeCommentFromSide(comment, side);
+ }
+
+ _removeCommentFromSide(comment, side) {
+ let idx = this._findCommentIndex(comment, side);
+ if (idx === -1) {
+ idx = this._findDraftIndex(comment, side);
+ }
+ if (idx !== -1) {
+ this.splice('comments.' + side, idx, 1);
+ }
+ }
+
+ /** @return {number} */
+ _findCommentIndex(comment, side) {
+ if (!comment.id || !this.comments[side]) {
+ return -1;
+ }
+ return this.comments[side].findIndex(item => item.id === comment.id);
+ }
+
+ /** @return {number} */
+ _findDraftIndex(comment, side) {
+ if (!comment.__draftID || !this.comments[side]) {
+ return -1;
+ }
+ return this.comments[side].findIndex(
+ item => item.__draftID === comment.__draftID);
+ }
+
+ _isSyntaxHighlightingEnabled(preferenceChangeRecord, diff) {
+ if (!preferenceChangeRecord ||
+ !preferenceChangeRecord.base ||
+ !preferenceChangeRecord.base.syntax_highlighting ||
+ !diff) {
+ return false;
+ }
+ return !this._anyLineTooLong(diff) &&
+ this.$.diff.getDiffLength(diff) <= SYNTAX_MAX_DIFF_LENGTH;
+ }
+
+ /**
+ * @return {boolean} whether any of the lines in diff are longer
+ * than SYNTAX_MAX_LINE_LENGTH.
+ */
+ _anyLineTooLong(diff) {
+ if (!diff) return false;
+ return diff.content.some(section => {
+ const lines = section.ab ?
+ section.ab :
+ (section.a || []).concat(section.b || []);
+ return lines.some(line => line.length >= SYNTAX_MAX_LINE_LENGTH);
+ });
+ }
+
+ _listenToViewportRender() {
+ const renderUpdateListener = start => {
+ if (start > NUM_OF_LINES_THRESHOLD_FOR_VIEWPORT) {
+ this.$.reporting.diffViewDisplayed();
+ this.$.syntaxLayer.removeListener(renderUpdateListener);
+ }
+ };
+
+ this.$.syntaxLayer.addListener(renderUpdateListener);
+ }
+
+ _handleRenderStart() {
+ this.$.reporting.time(TimingLabel.TOTAL);
+ this.$.reporting.time(TimingLabel.CONTENT);
+ }
+
+ _handleRenderContent() {
+ this.$.reporting.timeEnd(TimingLabel.CONTENT);
+ }
+
+ _handleNormalizeRange(event) {
+ this.$.reporting.reportInteraction('normalize-range',
+ {
+ side: event.detail.side,
+ lineNum: event.detail.lineNum,
+ });
+ }
+
+ _handleDiffContextExpanded(event) {
+ this.$.reporting.reportInteraction(
+ 'diff-context-expanded', {numLines: event.detail.numLines}
+ );
+ }
+
+ /**
+ * Find the last chunk for the given side.
+ *
+ * @param {!Object} diff
+ * @param {boolean} leftSide true if checking the base of the diff,
+ * false if testing the revision.
+ * @return {Object|null} returns the chunk object or null if there was
+ * no chunk for that side.
+ */
+ _lastChunkForSide(diff, leftSide) {
+ if (!diff.content.length) { return null; }
+
+ let chunkIndex = diff.content.length;
+ let chunk;
+
+ // Walk backwards until we find a chunk for the given side.
+ do {
+ chunkIndex--;
+ chunk = diff.content[chunkIndex];
+ } while (
+ // We haven't reached the beginning.
+ chunkIndex >= 0 &&
+
+ // The chunk doesn't have both sides.
+ !chunk.ab &&
+
+ // The chunk doesn't have the given side.
+ ((leftSide && (!chunk.a || !chunk.a.length)) ||
+ (!leftSide && (!chunk.b || !chunk.b.length))));
+
+ // If we reached the beginning of the diff and failed to find a chunk
+ // with the given side, return null.
+ if (chunkIndex === -1) { return null; }
+
+ return chunk;
+ }
+
+ /**
+ * Check whether the specified side of the diff has a trailing newline.
+ *
+ * @param {!Object} diff
+ * @param {boolean} leftSide true if checking the base of the diff,
+ * false if testing the revision.
+ * @return {boolean|null} Return true if the side has a trailing newline.
+ * Return false if it doesn't. Return null if not applicable (for
+ * example, if the diff has no content on the specified side).
+ */
+ _hasTrailingNewlines(diff, leftSide) {
+ const chunk = this._lastChunkForSide(diff, leftSide);
+ if (!chunk) { return null; }
+ let lines;
+ if (chunk.ab) {
+ lines = chunk.ab;
+ } else {
+ lines = leftSide ? chunk.a : chunk.b;
+ }
+ return lines[lines.length - 1] === '';
+ }
+
+ _showNewlineWarningLeft(diff) {
+ return this._hasTrailingNewlines(diff, true) === false;
+ }
+
+ _showNewlineWarningRight(diff) {
+ return this._hasTrailingNewlines(diff, false) === false;
+ }
+}
+
+customElements.define(GrDiffHost.is, GrDiffHost);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js
new file mode 100644
index 0000000..d48531b
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js
@@ -0,0 +1,26 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <gr-diff id="diff" change-num="[[changeNum]]" no-auto-render="[[noAutoRender]]" patch-range="[[patchRange]]" path="[[path]]" prefs="[[prefs]]" project-name="[[projectName]]" display-line="[[displayLine]]" is-image-diff="[[isImageDiff]]" commit-range="[[commitRange]]" hidden\$="[[hidden]]" no-render-on-prefs-change="[[noRenderOnPrefsChange]]" line-wrapping="[[lineWrapping]]" view-mode="[[viewMode]]" line-of-interest="[[lineOfInterest]]" logged-in="[[_loggedIn]]" loading="[[_loading]]" error-message="[[_errorMessage]]" base-image="[[_baseImage]]" revision-image="[[_revisionImage]]" coverage-ranges="[[_coverageRanges]]" blame="[[_blame]]" layers="[[_layers]]" diff="[[diff]]" show-newline-warning-left="[[_showNewlineWarningLeft(diff)]]" show-newline-warning-right="[[_showNewlineWarningRight(diff)]]">
+ </gr-diff>
+ <gr-syntax-layer id="syntaxLayer" enabled="[[_syntaxHighlightingEnabled]]" diff="[[diff]]"></gr-syntax-layer>
+ <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ <gr-reporting id="reporting" category="diff"></gr-reporting>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
index be2101c..bb4ff3f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
@@ -19,16 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-
-<link rel="import" href="gr-diff-host.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,125 +30,49 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff-host tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let getLoggedIn;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-diff-host.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-diff-host tests', () => {
+ let element;
+ let sandbox;
+ let getLoggedIn;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ getLoggedIn = false;
+ stub('gr-rest-api-interface', {
+ async getLoggedIn() { return getLoggedIn; },
+ });
+ stub('gr-reporting', {
+ time: sandbox.stub(),
+ timeEnd: sandbox.stub(),
+ });
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('plugin layers', () => {
+ const pluginLayers = [{annotate: () => {}}, {annotate: () => {}}];
setup(() => {
- sandbox = sinon.sandbox.create();
- getLoggedIn = false;
- stub('gr-rest-api-interface', {
- async getLoggedIn() { return getLoggedIn; },
- });
- stub('gr-reporting', {
- time: sandbox.stub(),
- timeEnd: sandbox.stub(),
+ stub('gr-js-api-interface', {
+ getDiffLayers() { return pluginLayers; },
});
element = fixture('basic');
});
-
- teardown(() => {
- sandbox.restore();
+ test('plugin layers requested', () => {
+ element.patchRange = {};
+ element.reload();
+ assert(element.$.jsAPI.getDiffLayers.called);
});
+ });
- suite('plugin layers', () => {
- const pluginLayers = [{annotate: () => {}}, {annotate: () => {}}];
- setup(() => {
- stub('gr-js-api-interface', {
- getDiffLayers() { return pluginLayers; },
- });
- element = fixture('basic');
- });
- test('plugin layers requested', () => {
- element.patchRange = {};
- element.reload();
- assert(element.$.jsAPI.getDiffLayers.called);
- });
- });
-
- suite('handle comment-update', () => {
- setup(() => {
- sandbox.stub(element, '_commentsChanged');
- element.comments = {
- meta: {
- changeNum: '42',
- patchRange: {
- basePatchNum: 'PARENT',
- patchNum: 3,
- },
- path: '/path/to/foo',
- projectConfig: {foo: 'bar'},
- },
- left: [
- {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
- {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
- {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
- {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
- ],
- right: [
- {id: 'c1', __commentSide: 'right'},
- {id: 'c2', __commentSide: 'right'},
- {id: 'd1', __draft: true, __commentSide: 'right'},
- {id: 'd2', __draft: true, __commentSide: 'right'},
- ],
- };
- });
-
- test('creating a draft', () => {
- const comment = {__draft: true, __draftID: 'tempID', side: 'PARENT',
- __commentSide: 'left'};
- element.fire('comment-update', {comment});
- assert.include(element.comments.left, comment);
- });
-
- test('discarding a draft', () => {
- const draftID = 'tempID';
- const id = 'savedID';
- const comment = {
- __draft: true,
- __draftID: draftID,
- side: 'PARENT',
- __commentSide: 'left',
- };
- const diffCommentsModifiedStub = sandbox.stub();
- element.addEventListener('diff-comments-modified',
- diffCommentsModifiedStub);
- element.comments.left.push(comment);
- comment.id = id;
- element.fire('comment-discard', {comment});
- const drafts = element.comments.left
- .filter(item => item.__draftID === draftID);
- assert.equal(drafts.length, 0);
- assert.isTrue(diffCommentsModifiedStub.called);
- });
-
- test('saving a draft', () => {
- const draftID = 'tempID';
- const id = 'savedID';
- const comment = {
- __draft: true,
- __draftID: draftID,
- side: 'PARENT',
- __commentSide: 'left',
- };
- const diffCommentsModifiedStub = sandbox.stub();
- element.addEventListener('diff-comments-modified',
- diffCommentsModifiedStub);
- element.comments.left.push(comment);
- comment.id = id;
- element.fire('comment-save', {comment});
- const drafts = element.comments.left
- .filter(item => item.__draftID === draftID);
- assert.equal(drafts.length, 1);
- assert.equal(drafts[0].id, id);
- assert.isTrue(diffCommentsModifiedStub.called);
- });
- });
-
- test('remove comment', () => {
+ suite('handle comment-update', () => {
+ setup(() => {
sandbox.stub(element, '_commentsChanged');
element.comments = {
meta: {
@@ -179,1453 +97,1531 @@
{id: 'd2', __draft: true, __commentSide: 'right'},
],
};
-
- element._removeComment({});
- // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem
- // to believe that one object deepEquals another even when they do :-/.
- assert.equal(JSON.stringify(element.comments), JSON.stringify({
- meta: {
- changeNum: '42',
- patchRange: {
- basePatchNum: 'PARENT',
- patchNum: 3,
- },
- path: '/path/to/foo',
- projectConfig: {foo: 'bar'},
- },
- left: [
- {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
- {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
- {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
- {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
- ],
- right: [
- {id: 'c1', __commentSide: 'right'},
- {id: 'c2', __commentSide: 'right'},
- {id: 'd1', __draft: true, __commentSide: 'right'},
- {id: 'd2', __draft: true, __commentSide: 'right'},
- ],
- }));
-
- element._removeComment({id: 'bc2', side: 'PARENT',
- __commentSide: 'left'});
- assert.deepEqual(element.comments, {
- meta: {
- changeNum: '42',
- patchRange: {
- basePatchNum: 'PARENT',
- patchNum: 3,
- },
- path: '/path/to/foo',
- projectConfig: {foo: 'bar'},
- },
- left: [
- {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
- {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
- {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
- ],
- right: [
- {id: 'c1', __commentSide: 'right'},
- {id: 'c2', __commentSide: 'right'},
- {id: 'd1', __draft: true, __commentSide: 'right'},
- {id: 'd2', __draft: true, __commentSide: 'right'},
- ],
- });
-
- element._removeComment({id: 'd2', __commentSide: 'right'});
- assert.deepEqual(element.comments, {
- meta: {
- changeNum: '42',
- patchRange: {
- basePatchNum: 'PARENT',
- patchNum: 3,
- },
- path: '/path/to/foo',
- projectConfig: {foo: 'bar'},
- },
- left: [
- {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
- {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
- {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
- ],
- right: [
- {id: 'c1', __commentSide: 'right'},
- {id: 'c2', __commentSide: 'right'},
- {id: 'd1', __draft: true, __commentSide: 'right'},
- ],
- });
});
- test('thread-discard handling', () => {
- const threads = [
- {comments: [{id: 4711}]},
- {comments: [{id: 42}]},
- ];
- element._parentIndex = 1;
- element.changeNum = '2';
- element.path = 'some/path';
- element.projectName = 'Some project';
- const threadEls = threads.map(
- thread => {
- const threadEl = element._createThreadElement(thread);
- // Polymer 2 doesn't fire ready events and doesn't execute
- // observers if element is not added to the Dom.
- // See https://github.com/Polymer/old-docs-site/issues/2322
- // and https://github.com/Polymer/polymer/issues/4526
- element._attachThreadElement(threadEl);
- return threadEl;
- });
- assert.equal(threadEls.length, 2);
- assert.equal(threadEls[0].rootId, 4711);
- assert.equal(threadEls[1].rootId, 42);
- for (const threadEl of threadEls) {
- Polymer.dom(element).appendChild(threadEl);
- }
-
- threadEls[0].dispatchEvent(
- new CustomEvent('thread-discard', {detail: {rootId: 4711}}));
- const attachedThreads = element.queryAllEffectiveChildren(
- 'gr-comment-thread');
- assert.equal(attachedThreads.length, 1);
- assert.equal(attachedThreads[0].rootId, 42);
+ test('creating a draft', () => {
+ const comment = {__draft: true, __draftID: 'tempID', side: 'PARENT',
+ __commentSide: 'left'};
+ element.fire('comment-update', {comment});
+ assert.include(element.comments.left, comment);
});
- suite('render reporting', () => {
- test('starts total and content timer on render-start', done => {
- element.dispatchEvent(
- new CustomEvent('render-start', {bubbles: true, composed: true}));
- assert.isTrue(element.$.reporting.time.calledWithExactly(
- 'Diff Total Render'));
- assert.isTrue(element.$.reporting.time.calledWithExactly(
- 'Diff Content Render'));
- done();
- });
+ test('discarding a draft', () => {
+ const draftID = 'tempID';
+ const id = 'savedID';
+ const comment = {
+ __draft: true,
+ __draftID: draftID,
+ side: 'PARENT',
+ __commentSide: 'left',
+ };
+ const diffCommentsModifiedStub = sandbox.stub();
+ element.addEventListener('diff-comments-modified',
+ diffCommentsModifiedStub);
+ element.comments.left.push(comment);
+ comment.id = id;
+ element.fire('comment-discard', {comment});
+ const drafts = element.comments.left
+ .filter(item => item.__draftID === draftID);
+ assert.equal(drafts.length, 0);
+ assert.isTrue(diffCommentsModifiedStub.called);
+ });
- test('ends content timer on render-content', () => {
- element.dispatchEvent(
- new CustomEvent('render-content', {bubbles: true, composed: true}));
- assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
- 'Diff Content Render'));
- });
+ test('saving a draft', () => {
+ const draftID = 'tempID';
+ const id = 'savedID';
+ const comment = {
+ __draft: true,
+ __draftID: draftID,
+ side: 'PARENT',
+ __commentSide: 'left',
+ };
+ const diffCommentsModifiedStub = sandbox.stub();
+ element.addEventListener('diff-comments-modified',
+ diffCommentsModifiedStub);
+ element.comments.left.push(comment);
+ comment.id = id;
+ element.fire('comment-save', {comment});
+ const drafts = element.comments.left
+ .filter(item => item.__draftID === draftID);
+ assert.equal(drafts.length, 1);
+ assert.equal(drafts[0].id, id);
+ assert.isTrue(diffCommentsModifiedStub.called);
+ });
+ });
- test('ends total and syntax timer after syntax layer processing', done => {
- let notifySyntaxProcessed;
- sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise(
- resolve => {
- notifySyntaxProcessed = resolve;
- }));
- sandbox.stub(element.$.restAPI, 'getDiff').returns(
- Promise.resolve({content: []}));
- element.patchRange = {};
- element.$.restAPI.getDiffPreferences().then(prefs => {
- element.prefs = prefs;
- return element.reload(true);
+ test('remove comment', () => {
+ sandbox.stub(element, '_commentsChanged');
+ element.comments = {
+ meta: {
+ changeNum: '42',
+ patchRange: {
+ basePatchNum: 'PARENT',
+ patchNum: 3,
+ },
+ path: '/path/to/foo',
+ projectConfig: {foo: 'bar'},
+ },
+ left: [
+ {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+ {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ ],
+ right: [
+ {id: 'c1', __commentSide: 'right'},
+ {id: 'c2', __commentSide: 'right'},
+ {id: 'd1', __draft: true, __commentSide: 'right'},
+ {id: 'd2', __draft: true, __commentSide: 'right'},
+ ],
+ };
+
+ element._removeComment({});
+ // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem
+ // to believe that one object deepEquals another even when they do :-/.
+ assert.equal(JSON.stringify(element.comments), JSON.stringify({
+ meta: {
+ changeNum: '42',
+ patchRange: {
+ basePatchNum: 'PARENT',
+ patchNum: 3,
+ },
+ path: '/path/to/foo',
+ projectConfig: {foo: 'bar'},
+ },
+ left: [
+ {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+ {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ ],
+ right: [
+ {id: 'c1', __commentSide: 'right'},
+ {id: 'c2', __commentSide: 'right'},
+ {id: 'd1', __draft: true, __commentSide: 'right'},
+ {id: 'd2', __draft: true, __commentSide: 'right'},
+ ],
+ }));
+
+ element._removeComment({id: 'bc2', side: 'PARENT',
+ __commentSide: 'left'});
+ assert.deepEqual(element.comments, {
+ meta: {
+ changeNum: '42',
+ patchRange: {
+ basePatchNum: 'PARENT',
+ patchNum: 3,
+ },
+ path: '/path/to/foo',
+ projectConfig: {foo: 'bar'},
+ },
+ left: [
+ {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ ],
+ right: [
+ {id: 'c1', __commentSide: 'right'},
+ {id: 'c2', __commentSide: 'right'},
+ {id: 'd1', __draft: true, __commentSide: 'right'},
+ {id: 'd2', __draft: true, __commentSide: 'right'},
+ ],
+ });
+
+ element._removeComment({id: 'd2', __commentSide: 'right'});
+ assert.deepEqual(element.comments, {
+ meta: {
+ changeNum: '42',
+ patchRange: {
+ basePatchNum: 'PARENT',
+ patchNum: 3,
+ },
+ path: '/path/to/foo',
+ projectConfig: {foo: 'bar'},
+ },
+ left: [
+ {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ ],
+ right: [
+ {id: 'c1', __commentSide: 'right'},
+ {id: 'c2', __commentSide: 'right'},
+ {id: 'd1', __draft: true, __commentSide: 'right'},
+ ],
+ });
+ });
+
+ test('thread-discard handling', () => {
+ const threads = [
+ {comments: [{id: 4711}]},
+ {comments: [{id: 42}]},
+ ];
+ element._parentIndex = 1;
+ element.changeNum = '2';
+ element.path = 'some/path';
+ element.projectName = 'Some project';
+ const threadEls = threads.map(
+ thread => {
+ const threadEl = element._createThreadElement(thread);
+ // Polymer 2 doesn't fire ready events and doesn't execute
+ // observers if element is not added to the Dom.
+ // See https://github.com/Polymer/old-docs-site/issues/2322
+ // and https://github.com/Polymer/polymer/issues/4526
+ element._attachThreadElement(threadEl);
+ return threadEl;
});
- // Multiple cascading microtasks are scheduled.
- setTimeout(() => {
- notifySyntaxProcessed();
- // Assert after the notification task is processed.
- Promise.resolve().then(() => {
- assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
- 'Diff Total Render'));
- assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
- 'Diff Syntax Render'));
- assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
- 'StartupDiffViewOnlyContent'));
- done();
- });
- });
- });
+ assert.equal(threadEls.length, 2);
+ assert.equal(threadEls[0].rootId, 4711);
+ assert.equal(threadEls[1].rootId, 42);
+ for (const threadEl of threadEls) {
+ dom(element).appendChild(threadEl);
+ }
- test('ends total timer w/ no syntax layer processing', done => {
- sandbox.stub(element.$.restAPI, 'getDiff').returns(
- Promise.resolve({content: []}));
- element.patchRange = {};
- element.reload();
- // Multiple cascading microtasks are scheduled.
- setTimeout(() => {
- assert.isTrue(element.$.reporting.timeEnd.calledOnce);
+ threadEls[0].dispatchEvent(
+ new CustomEvent('thread-discard', {detail: {rootId: 4711}}));
+ const attachedThreads = element.queryAllEffectiveChildren(
+ 'gr-comment-thread');
+ assert.equal(attachedThreads.length, 1);
+ assert.equal(attachedThreads[0].rootId, 42);
+ });
+
+ suite('render reporting', () => {
+ test('starts total and content timer on render-start', done => {
+ element.dispatchEvent(
+ new CustomEvent('render-start', {bubbles: true, composed: true}));
+ assert.isTrue(element.$.reporting.time.calledWithExactly(
+ 'Diff Total Render'));
+ assert.isTrue(element.$.reporting.time.calledWithExactly(
+ 'Diff Content Render'));
+ done();
+ });
+
+ test('ends content timer on render-content', () => {
+ element.dispatchEvent(
+ new CustomEvent('render-content', {bubbles: true, composed: true}));
+ assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+ 'Diff Content Render'));
+ });
+
+ test('ends total and syntax timer after syntax layer processing', done => {
+ let notifySyntaxProcessed;
+ sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise(
+ resolve => {
+ notifySyntaxProcessed = resolve;
+ }));
+ sandbox.stub(element.$.restAPI, 'getDiff').returns(
+ Promise.resolve({content: []}));
+ element.patchRange = {};
+ element.$.restAPI.getDiffPreferences().then(prefs => {
+ element.prefs = prefs;
+ return element.reload(true);
+ });
+ // Multiple cascading microtasks are scheduled.
+ setTimeout(() => {
+ notifySyntaxProcessed();
+ // Assert after the notification task is processed.
+ Promise.resolve().then(() => {
assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
'Diff Total Render'));
+ assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+ 'Diff Syntax Render'));
+ assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+ 'StartupDiffViewOnlyContent'));
done();
});
});
+ });
- test('completes reload promise after syntax layer processing', done => {
- let notifySyntaxProcessed;
- sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise(
- resolve => {
- notifySyntaxProcessed = resolve;
- }));
- sandbox.stub(element.$.restAPI, 'getDiff').returns(
- Promise.resolve({content: []}));
- element.patchRange = {};
- let reloadComplete = false;
- element.$.restAPI.getDiffPreferences()
- .then(prefs => {
- element.prefs = prefs;
- return element.reload();
- })
- .then(() => {
- reloadComplete = true;
- });
- // Multiple cascading microtasks are scheduled.
+ test('ends total timer w/ no syntax layer processing', done => {
+ sandbox.stub(element.$.restAPI, 'getDiff').returns(
+ Promise.resolve({content: []}));
+ element.patchRange = {};
+ element.reload();
+ // Multiple cascading microtasks are scheduled.
+ setTimeout(() => {
+ assert.isTrue(element.$.reporting.timeEnd.calledOnce);
+ assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+ 'Diff Total Render'));
+ done();
+ });
+ });
+
+ test('completes reload promise after syntax layer processing', done => {
+ let notifySyntaxProcessed;
+ sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise(
+ resolve => {
+ notifySyntaxProcessed = resolve;
+ }));
+ sandbox.stub(element.$.restAPI, 'getDiff').returns(
+ Promise.resolve({content: []}));
+ element.patchRange = {};
+ let reloadComplete = false;
+ element.$.restAPI.getDiffPreferences()
+ .then(prefs => {
+ element.prefs = prefs;
+ return element.reload();
+ })
+ .then(() => {
+ reloadComplete = true;
+ });
+ // Multiple cascading microtasks are scheduled.
+ setTimeout(() => {
+ assert.isFalse(reloadComplete);
+ notifySyntaxProcessed();
+ // Assert after the notification task is processed.
setTimeout(() => {
- assert.isFalse(reloadComplete);
- notifySyntaxProcessed();
- // Assert after the notification task is processed.
- setTimeout(() => {
- assert.isTrue(reloadComplete);
- done();
- });
+ assert.isTrue(reloadComplete);
+ done();
});
});
});
+ });
- test('reload() cancels before network resolves', () => {
- const cancelStub = sandbox.stub(element.$.diff, 'cancel');
+ test('reload() cancels before network resolves', () => {
+ const cancelStub = sandbox.stub(element.$.diff, 'cancel');
- // Stub the network calls into requests that never resolve.
- sandbox.stub(element, '_getDiff', () => new Promise(() => {}));
+ // Stub the network calls into requests that never resolve.
+ sandbox.stub(element, '_getDiff', () => new Promise(() => {}));
+ element.patchRange = {};
+
+ element.reload();
+ assert.isTrue(cancelStub.called);
+ });
+
+ suite('not logged in', () => {
+ setup(() => {
+ getLoggedIn = false;
+ element = fixture('basic');
+ });
+
+ test('reload() loads files weblinks', () => {
+ const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
+ .returns({name: 'stubb', url: '#s'});
+ sandbox.stub(element.$.restAPI, 'getDiff').returns(Promise.resolve({
+ content: [],
+ }));
+ element.projectName = 'test-project';
+ element.path = 'test-path';
+ element.commitRange = {baseCommit: 'test-base', commit: 'test-commit'};
element.patchRange = {};
-
- element.reload();
- assert.isTrue(cancelStub.called);
+ return element.reload().then(() => {
+ assert.isTrue(weblinksStub.calledTwice);
+ assert.isTrue(weblinksStub.firstCall.calledWith({
+ commit: 'test-base',
+ file: 'test-path',
+ options: {
+ weblinks: undefined,
+ },
+ repo: 'test-project',
+ type: Gerrit.Nav.WeblinkType.FILE}));
+ assert.isTrue(weblinksStub.secondCall.calledWith({
+ commit: 'test-commit',
+ file: 'test-path',
+ options: {
+ weblinks: undefined,
+ },
+ repo: 'test-project',
+ type: Gerrit.Nav.WeblinkType.FILE}));
+ assert.deepEqual(element.filesWeblinks, {
+ meta_a: [{name: 'stubb', url: '#s'}],
+ meta_b: [{name: 'stubb', url: '#s'}],
+ });
+ });
});
- suite('not logged in', () => {
+ test('_getDiff handles null diff responses', done => {
+ stub('gr-rest-api-interface', {
+ getDiff() { return Promise.resolve(null); },
+ });
+ element.changeNum = 123;
+ element.patchRange = {basePatchNum: 1, patchNum: 2};
+ element.path = 'file.txt';
+ element._getDiff().then(done);
+ });
+
+ test('reload resolves on error', () => {
+ const onErrStub = sandbox.stub(element, '_handleGetDiffError');
+ const error = {ok: false, status: 500};
+ sandbox.stub(element.$.restAPI, 'getDiff',
+ (changeNum, basePatchNum, patchNum, path, onErr) => {
+ onErr(error);
+ });
+ element.patchRange = {};
+ return element.reload().then(() => {
+ assert.isTrue(onErrStub.calledOnce);
+ });
+ });
+
+ suite('_handleGetDiffError', () => {
+ let serverErrorStub;
+ let pageErrorStub;
+
setup(() => {
- getLoggedIn = false;
- element = fixture('basic');
+ serverErrorStub = sinon.stub();
+ element.addEventListener('server-error', serverErrorStub);
+ pageErrorStub = sinon.stub();
+ element.addEventListener('page-error', pageErrorStub);
});
- test('reload() loads files weblinks', () => {
- const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
- .returns({name: 'stubb', url: '#s'});
- sandbox.stub(element.$.restAPI, 'getDiff').returns(Promise.resolve({
- content: [],
- }));
- element.projectName = 'test-project';
- element.path = 'test-path';
- element.commitRange = {baseCommit: 'test-base', commit: 'test-commit'};
- element.patchRange = {};
- return element.reload().then(() => {
- assert.isTrue(weblinksStub.calledTwice);
- assert.isTrue(weblinksStub.firstCall.calledWith({
- commit: 'test-base',
- file: 'test-path',
- options: {
- weblinks: undefined,
- },
- repo: 'test-project',
- type: Gerrit.Nav.WeblinkType.FILE}));
- assert.isTrue(weblinksStub.secondCall.calledWith({
- commit: 'test-commit',
- file: 'test-path',
- options: {
- weblinks: undefined,
- },
- repo: 'test-project',
- type: Gerrit.Nav.WeblinkType.FILE}));
- assert.deepEqual(element.filesWeblinks, {
- meta_a: [{name: 'stubb', url: '#s'}],
- meta_b: [{name: 'stubb', url: '#s'}],
- });
- });
+ test('page error on HTTP-409', () => {
+ element._handleGetDiffError({status: 409});
+ assert.isTrue(serverErrorStub.calledOnce);
+ assert.isFalse(pageErrorStub.called);
+ assert.isNotOk(element._errorMessage);
});
- test('_getDiff handles null diff responses', done => {
- stub('gr-rest-api-interface', {
- getDiff() { return Promise.resolve(null); },
- });
- element.changeNum = 123;
- element.patchRange = {basePatchNum: 1, patchNum: 2};
- element.path = 'file.txt';
- element._getDiff().then(done);
+ test('server error on non-HTTP-409', () => {
+ element._handleGetDiffError({status: 500});
+ assert.isFalse(serverErrorStub.called);
+ assert.isTrue(pageErrorStub.calledOnce);
+ assert.isNotOk(element._errorMessage);
});
- test('reload resolves on error', () => {
- const onErrStub = sandbox.stub(element, '_handleGetDiffError');
- const error = {ok: false, status: 500};
- sandbox.stub(element.$.restAPI, 'getDiff',
- (changeNum, basePatchNum, patchNum, path, onErr) => {
- onErr(error);
- });
- element.patchRange = {};
- return element.reload().then(() => {
- assert.isTrue(onErrStub.calledOnce);
- });
+ test('error message if showLoadFailure', () => {
+ element.showLoadFailure = true;
+ element._handleGetDiffError({status: 500, statusText: 'Failure!'});
+ assert.isFalse(serverErrorStub.called);
+ assert.isFalse(pageErrorStub.called);
+ assert.equal(element._errorMessage,
+ 'Encountered error when loading the diff: 500 Failure!');
+ });
+ });
+
+ suite('image diffs', () => {
+ let mockFile1;
+ let mockFile2;
+ setup(() => {
+ mockFile1 = {
+ body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAAAAAA/w==',
+ type: 'image/bmp',
+ };
+ mockFile2 = {
+ body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAA/////w==',
+ type: 'image/bmp',
+ };
+ sandbox.stub(element.$.restAPI,
+ 'getB64FileContents',
+ (changeId, patchNum, path, opt_parentIndex) => Promise.resolve(
+ opt_parentIndex === 1 ? mockFile1 :
+ mockFile2)
+ );
+
+ element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
+ element.comments = {
+ left: [],
+ right: [],
+ meta: {patchRange: element.patchRange},
+ };
});
- suite('_handleGetDiffError', () => {
- let serverErrorStub;
- let pageErrorStub;
+ test('renders image diffs with same file name', done => {
+ const mockDiff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index 2adc47d..f9c2f2c 100644',
+ '--- a/carrot.jpg',
+ '+++ b/carrot.jpg',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+ sandbox.stub(element.$.restAPI, 'getDiff')
+ .returns(Promise.resolve(mockDiff));
- setup(() => {
- serverErrorStub = sinon.stub();
- element.addEventListener('server-error', serverErrorStub);
- pageErrorStub = sinon.stub();
- element.addEventListener('page-error', pageErrorStub);
- });
+ const rendered = () => {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
- test('page error on HTTP-409', () => {
- element._handleGetDiffError({status: 409});
- assert.isTrue(serverErrorStub.calledOnce);
- assert.isFalse(pageErrorStub.called);
- assert.isNotOk(element._errorMessage);
- });
+ // Left image rendered with the parent commit's version of the file.
+ const leftImage =
+ element.$.diff.$.diffTable.querySelector('td.left img');
+ const leftLabel =
+ element.$.diff.$.diffTable.querySelector('td.left label');
+ const leftLabelContent = leftLabel.querySelector('.label');
+ const leftLabelName = leftLabel.querySelector('.name');
- test('server error on non-HTTP-409', () => {
- element._handleGetDiffError({status: 500});
- assert.isFalse(serverErrorStub.called);
- assert.isTrue(pageErrorStub.calledOnce);
- assert.isNotOk(element._errorMessage);
- });
+ const rightImage =
+ element.$.diff.$.diffTable.querySelector('td.right img');
+ const rightLabel = element.$.diff.$.diffTable.querySelector(
+ 'td.right label');
+ const rightLabelContent = rightLabel.querySelector('.label');
+ const rightLabelName = rightLabel.querySelector('.name');
- test('error message if showLoadFailure', () => {
- element.showLoadFailure = true;
- element._handleGetDiffError({status: 500, statusText: 'Failure!'});
- assert.isFalse(serverErrorStub.called);
- assert.isFalse(pageErrorStub.called);
- assert.equal(element._errorMessage,
- 'Encountered error when loading the diff: 500 Failure!');
- });
- });
+ assert.isNotOk(rightLabelName);
+ assert.isNotOk(leftLabelName);
- suite('image diffs', () => {
- let mockFile1;
- let mockFile2;
- setup(() => {
- mockFile1 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
- 'wsAAAAAAAAAAAAAAAAA/w==',
- type: 'image/bmp',
- };
- mockFile2 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
- 'wsAAAAAAAAAAAAA/////w==',
- type: 'image/bmp',
- };
- sandbox.stub(element.$.restAPI,
- 'getB64FileContents',
- (changeId, patchNum, path, opt_parentIndex) => Promise.resolve(
- opt_parentIndex === 1 ? mockFile1 :
- mockFile2)
- );
+ let leftLoaded = false;
+ let rightLoaded = false;
- element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
- element.comments = {
- left: [],
- right: [],
- meta: {patchRange: element.patchRange},
- };
- });
-
- test('renders image diffs with same file name', done => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index 2adc47d..f9c2f2c 100644',
- '--- a/carrot.jpg',
- '+++ b/carrot.jpg',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- sandbox.stub(element.$.restAPI, 'getDiff')
- .returns(Promise.resolve(mockDiff));
-
- const rendered = () => {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
-
- // Left image rendered with the parent commit's version of the file.
- const leftImage =
- element.$.diff.$.diffTable.querySelector('td.left img');
- const leftLabel =
- element.$.diff.$.diffTable.querySelector('td.left label');
- const leftLabelContent = leftLabel.querySelector('.label');
- const leftLabelName = leftLabel.querySelector('.name');
-
- const rightImage =
- element.$.diff.$.diffTable.querySelector('td.right img');
- const rightLabel = element.$.diff.$.diffTable.querySelector(
- 'td.right label');
- const rightLabelContent = rightLabel.querySelector('.label');
- const rightLabelName = rightLabel.querySelector('.name');
-
- assert.isNotOk(rightLabelName);
- assert.isNotOk(leftLabelName);
-
- let leftLoaded = false;
- let rightLoaded = false;
-
- leftImage.addEventListener('load', () => {
- assert.isOk(leftImage);
- assert.equal(leftImage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile1.body);
- assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
- leftLoaded = true;
- if (rightLoaded) {
- element.removeEventListener('render', rendered);
- done();
- }
- });
-
- rightImage.addEventListener('load', () => {
- assert.isOk(rightImage);
- assert.equal(rightImage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile2.body);
- assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
-
- rightLoaded = true;
- if (leftLoaded) {
- element.removeEventListener('render', rendered);
- done();
- }
- });
- };
-
- element.addEventListener('render', rendered);
-
- element.$.restAPI.getDiffPreferences().then(prefs => {
- element.prefs = prefs;
- element.reload();
- });
- });
-
- test('renders image diffs with a different file name', done => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot2.jpg',
- 'index 2adc47d..f9c2f2c 100644',
- '--- a/carrot.jpg',
- '+++ b/carrot2.jpg',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- sandbox.stub(element.$.restAPI, 'getDiff')
- .returns(Promise.resolve(mockDiff));
-
- const rendered = () => {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
-
- // Left image rendered with the parent commit's version of the file.
- const leftImage =
- element.$.diff.$.diffTable.querySelector('td.left img');
- const leftLabel =
- element.$.diff.$.diffTable.querySelector('td.left label');
- const leftLabelContent = leftLabel.querySelector('.label');
- const leftLabelName = leftLabel.querySelector('.name');
-
- const rightImage =
- element.$.diff.$.diffTable.querySelector('td.right img');
- const rightLabel = element.$.diff.$.diffTable.querySelector(
- 'td.right label');
- const rightLabelContent = rightLabel.querySelector('.label');
- const rightLabelName = rightLabel.querySelector('.name');
-
- assert.isOk(rightLabelName);
- assert.isOk(leftLabelName);
- assert.equal(leftLabelName.textContent, mockDiff.meta_a.name);
- assert.equal(rightLabelName.textContent, mockDiff.meta_b.name);
-
- let leftLoaded = false;
- let rightLoaded = false;
-
- leftImage.addEventListener('load', () => {
- assert.isOk(leftImage);
- assert.equal(leftImage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile1.body);
- assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
- leftLoaded = true;
- if (rightLoaded) {
- element.removeEventListener('render', rendered);
- done();
- }
- });
-
- rightImage.addEventListener('load', () => {
- assert.isOk(rightImage);
- assert.equal(rightImage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile2.body);
- assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
-
- rightLoaded = true;
- if (leftLoaded) {
- element.removeEventListener('render', rendered);
- done();
- }
- });
- };
-
- element.addEventListener('render', rendered);
-
- element.$.restAPI.getDiffPreferences().then(prefs => {
- element.prefs = prefs;
- element.reload();
- });
- });
-
- test('renders added image', done => {
- const mockDiff = {
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'ADDED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index 0000000..f9c2f2c 100644',
- '--- /dev/null',
- '+++ b/carrot.jpg',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- sandbox.stub(element.$.restAPI, 'getDiff')
- .returns(Promise.resolve(mockDiff));
-
- element.addEventListener('render', () => {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
-
- const leftImage =
- element.$.diff.$.diffTable.querySelector('td.left img');
- const rightImage =
- element.$.diff.$.diffTable.querySelector('td.right img');
-
- assert.isNotOk(leftImage);
- assert.isOk(rightImage);
- done();
- });
-
- element.$.restAPI.getDiffPreferences().then(prefs => {
- element.prefs = prefs;
- element.reload();
- });
- });
-
- test('renders removed image', done => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'DELETED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index f9c2f2c..0000000 100644',
- '--- a/carrot.jpg',
- '+++ /dev/null',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- sandbox.stub(element.$.restAPI, 'getDiff')
- .returns(Promise.resolve(mockDiff));
-
- element.addEventListener('render', () => {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
-
- const leftImage =
- element.$.diff.$.diffTable.querySelector('td.left img');
- const rightImage =
- element.$.diff.$.diffTable.querySelector('td.right img');
-
+ leftImage.addEventListener('load', () => {
assert.isOk(leftImage);
- assert.isNotOk(rightImage);
- done();
+ assert.equal(leftImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile1.body);
+ assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
+ leftLoaded = true;
+ if (rightLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
});
- element.$.restAPI.getDiffPreferences().then(prefs => {
- element.prefs = prefs;
- element.reload();
+ rightImage.addEventListener('load', () => {
+ assert.isOk(rightImage);
+ assert.equal(rightImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile2.body);
+ assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
+
+ rightLoaded = true;
+ if (leftLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
});
- });
+ };
- test('does not render disallowed image type', done => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'DELETED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index f9c2f2c..0000000 100644',
- '--- a/carrot.jpg',
- '+++ /dev/null',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- mockFile1.type = 'image/jpeg-evil';
+ element.addEventListener('render', rendered);
- sandbox.stub(element.$.restAPI, 'getDiff')
- .returns(Promise.resolve(mockDiff));
-
- element.addEventListener('render', () => {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
- const leftImage =
- element.$.diff.$.diffTable.querySelector('td.left img');
- assert.isNotOk(leftImage);
- done();
- });
-
- element.$.restAPI.getDiffPreferences().then(prefs => {
- element.prefs = prefs;
- element.reload();
- });
- });
- });
- });
-
- test('delegates cancel()', () => {
- const stub = sandbox.stub(element.$.diff, 'cancel');
- element.patchRange = {};
- element.reload();
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- test('delegates getCursorStops()', () => {
- const returnValue = [document.createElement('b')];
- const stub = sandbox.stub(element.$.diff, 'getCursorStops')
- .returns(returnValue);
- assert.equal(element.getCursorStops(), returnValue);
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- test('delegates isRangeSelected()', () => {
- const returnValue = true;
- const stub = sandbox.stub(element.$.diff, 'isRangeSelected')
- .returns(returnValue);
- assert.equal(element.isRangeSelected(), returnValue);
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- test('delegates toggleLeftDiff()', () => {
- const stub = sandbox.stub(element.$.diff, 'toggleLeftDiff');
- element.toggleLeftDiff();
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- suite('blame', () => {
- setup(() => {
- element = fixture('basic');
- });
-
- test('clearBlame', () => {
- element._blame = [];
- const setBlameSpy = sandbox.spy(element.$.diff.$.diffBuilder, 'setBlame');
- element.clearBlame();
- assert.isNull(element._blame);
- assert.isTrue(setBlameSpy.calledWithExactly(null));
- assert.equal(element.isBlameLoaded, false);
- });
-
- test('loadBlame', () => {
- const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
- const showAlertStub = sinon.stub();
- element.addEventListener('show-alert', showAlertStub);
- const getBlameStub = sandbox.stub(element.$.restAPI, 'getBlame')
- .returns(Promise.resolve(mockBlame));
- element.changeNum = 42;
- element.patchRange = {patchNum: 5, basePatchNum: 4};
- element.path = 'foo/bar.baz';
- return element.loadBlame().then(() => {
- assert.isTrue(getBlameStub.calledWithExactly(
- 42, 5, 'foo/bar.baz', true));
- assert.isFalse(showAlertStub.called);
- assert.equal(element._blame, mockBlame);
- assert.equal(element.isBlameLoaded, true);
+ element.$.restAPI.getDiffPreferences().then(prefs => {
+ element.prefs = prefs;
+ element.reload();
});
});
- test('loadBlame empty', () => {
- const mockBlame = [];
- const showAlertStub = sinon.stub();
- element.addEventListener('show-alert', showAlertStub);
- sandbox.stub(element.$.restAPI, 'getBlame')
- .returns(Promise.resolve(mockBlame));
- element.changeNum = 42;
- element.patchRange = {patchNum: 5, basePatchNum: 4};
- element.path = 'foo/bar.baz';
- return element.loadBlame()
- .then(() => {
- assert.isTrue(false, 'Promise should not resolve');
- })
- .catch(() => {
- assert.isTrue(showAlertStub.calledOnce);
- assert.isNull(element._blame);
- assert.equal(element.isBlameLoaded, false);
- });
- });
- });
-
- test('getThreadEls() returns .comment-threads', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- Polymer.dom(element.$.diff).appendChild(threadEl);
- assert.deepEqual(element.getThreadEls(), [threadEl]);
- });
-
- test('delegates addDraftAtLine(el)', () => {
- const param0 = document.createElement('b');
- const stub = sandbox.stub(element.$.diff, 'addDraftAtLine');
- element.addDraftAtLine(param0);
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 1);
- assert.equal(stub.lastCall.args[0], param0);
- });
-
- test('delegates clearDiffContent()', () => {
- const stub = sandbox.stub(element.$.diff, 'clearDiffContent');
- element.clearDiffContent();
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- test('delegates expandAllContext()', () => {
- const stub = sandbox.stub(element.$.diff, 'expandAllContext');
- element.expandAllContext();
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- test('passes in changeNum', () => {
- const value = '12345';
- element.changeNum = value;
- assert.equal(element.$.diff.changeNum, value);
- });
-
- test('passes in noAutoRender', () => {
- const value = true;
- element.noAutoRender = value;
- assert.equal(element.$.diff.noAutoRender, value);
- });
-
- test('passes in patchRange', () => {
- const value = {patchNum: 'foo', basePatchNum: 'bar'};
- element.patchRange = value;
- assert.equal(element.$.diff.patchRange, value);
- });
-
- test('passes in path', () => {
- const value = 'some/file/path';
- element.path = value;
- assert.equal(element.$.diff.path, value);
- });
-
- test('passes in prefs', () => {
- const value = {};
- element.prefs = value;
- assert.equal(element.$.diff.prefs, value);
- });
-
- test('passes in changeNum', () => {
- const value = '12345';
- element.changeNum = value;
- assert.equal(element.$.diff.changeNum, value);
- });
-
- test('passes in projectName', () => {
- const value = 'Gerrit';
- element.projectName = value;
- assert.equal(element.$.diff.projectName, value);
- });
-
- test('passes in displayLine', () => {
- const value = true;
- element.displayLine = value;
- assert.equal(element.$.diff.displayLine, value);
- });
-
- test('passes in commitRange', () => {
- const value = {};
- element.commitRange = value;
- assert.equal(element.$.diff.commitRange, value);
- });
-
- test('passes in hidden', () => {
- const value = true;
- element.hidden = value;
- assert.equal(element.$.diff.hidden, value);
- assert.isNotNull(element.getAttribute('hidden'));
- });
-
- test('passes in noRenderOnPrefsChange', () => {
- const value = true;
- element.noRenderOnPrefsChange = value;
- assert.equal(element.$.diff.noRenderOnPrefsChange, value);
- });
-
- test('passes in lineWrapping', () => {
- const value = true;
- element.lineWrapping = value;
- assert.equal(element.$.diff.lineWrapping, value);
- });
-
- test('passes in viewMode', () => {
- const value = 'SIDE_BY_SIDE';
- element.viewMode = value;
- assert.equal(element.$.diff.viewMode, value);
- });
-
- test('passes in lineOfInterest', () => {
- const value = {number: 123, leftSide: true};
- element.lineOfInterest = value;
- assert.equal(element.$.diff.lineOfInterest, value);
- });
-
- suite('_reportDiff', () => {
- let reportStub;
-
- setup(() => {
- element = fixture('basic');
- element.patchRange = {basePatchNum: 1};
- reportStub = sandbox.stub(element.$.reporting, 'reportInteraction');
- });
-
- test('null and content-less', () => {
- element._reportDiff(null);
- assert.isFalse(reportStub.called);
-
- element._reportDiff({});
- assert.isFalse(reportStub.called);
- });
-
- test('diff w/ no delta', () => {
- const diff = {
- content: [
- {ab: ['foo', 'bar']},
- {ab: ['baz', 'foo']},
+ test('renders image diffs with a different file name', done => {
+ const mockDiff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot2.jpg',
+ 'index 2adc47d..f9c2f2c 100644',
+ '--- a/carrot.jpg',
+ '+++ b/carrot2.jpg',
+ 'Binary files differ',
],
+ content: [{skip: 66}],
+ binary: true,
};
- element._reportDiff(diff);
- assert.isTrue(reportStub.calledOnce);
- assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
- assert.isUndefined(reportStub.lastCall.args[1]);
+ sandbox.stub(element.$.restAPI, 'getDiff')
+ .returns(Promise.resolve(mockDiff));
+
+ const rendered = () => {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+
+ // Left image rendered with the parent commit's version of the file.
+ const leftImage =
+ element.$.diff.$.diffTable.querySelector('td.left img');
+ const leftLabel =
+ element.$.diff.$.diffTable.querySelector('td.left label');
+ const leftLabelContent = leftLabel.querySelector('.label');
+ const leftLabelName = leftLabel.querySelector('.name');
+
+ const rightImage =
+ element.$.diff.$.diffTable.querySelector('td.right img');
+ const rightLabel = element.$.diff.$.diffTable.querySelector(
+ 'td.right label');
+ const rightLabelContent = rightLabel.querySelector('.label');
+ const rightLabelName = rightLabel.querySelector('.name');
+
+ assert.isOk(rightLabelName);
+ assert.isOk(leftLabelName);
+ assert.equal(leftLabelName.textContent, mockDiff.meta_a.name);
+ assert.equal(rightLabelName.textContent, mockDiff.meta_b.name);
+
+ let leftLoaded = false;
+ let rightLoaded = false;
+
+ leftImage.addEventListener('load', () => {
+ assert.isOk(leftImage);
+ assert.equal(leftImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile1.body);
+ assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
+ leftLoaded = true;
+ if (rightLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
+ });
+
+ rightImage.addEventListener('load', () => {
+ assert.isOk(rightImage);
+ assert.equal(rightImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile2.body);
+ assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
+
+ rightLoaded = true;
+ if (leftLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
+ });
+ };
+
+ element.addEventListener('render', rendered);
+
+ element.$.restAPI.getDiffPreferences().then(prefs => {
+ element.prefs = prefs;
+ element.reload();
+ });
});
- test('diff w/ no rebase delta', () => {
- const diff = {
- content: [
- {ab: ['foo', 'bar']},
- {a: ['baz', 'foo']},
- {ab: ['foo', 'bar']},
- {a: ['baz', 'foo'], b: ['bar', 'baz']},
- {ab: ['foo', 'bar']},
- {b: ['baz', 'foo']},
- {ab: ['foo', 'bar']},
+ test('renders added image', done => {
+ const mockDiff = {
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'ADDED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index 0000000..f9c2f2c 100644',
+ '--- /dev/null',
+ '+++ b/carrot.jpg',
+ 'Binary files differ',
],
+ content: [{skip: 66}],
+ binary: true,
};
- element._reportDiff(diff);
- assert.isTrue(reportStub.calledOnce);
- assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
- assert.isUndefined(reportStub.lastCall.args[1]);
+ sandbox.stub(element.$.restAPI, 'getDiff')
+ .returns(Promise.resolve(mockDiff));
+
+ element.addEventListener('render', () => {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+
+ const leftImage =
+ element.$.diff.$.diffTable.querySelector('td.left img');
+ const rightImage =
+ element.$.diff.$.diffTable.querySelector('td.right img');
+
+ assert.isNotOk(leftImage);
+ assert.isOk(rightImage);
+ done();
+ });
+
+ element.$.restAPI.getDiffPreferences().then(prefs => {
+ element.prefs = prefs;
+ element.reload();
+ });
});
- test('diff w/ some rebase delta', () => {
- const diff = {
- content: [
- {ab: ['foo', 'bar']},
- {a: ['baz', 'foo'], due_to_rebase: true},
- {ab: ['foo', 'bar']},
- {a: ['baz', 'foo'], b: ['bar', 'baz']},
- {ab: ['foo', 'bar']},
- {b: ['baz', 'foo'], due_to_rebase: true},
- {ab: ['foo', 'bar']},
- {a: ['baz', 'foo']},
+ test('renders removed image', done => {
+ const mockDiff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'DELETED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index f9c2f2c..0000000 100644',
+ '--- a/carrot.jpg',
+ '+++ /dev/null',
+ 'Binary files differ',
],
+ content: [{skip: 66}],
+ binary: true,
};
- element._reportDiff(diff);
- assert.isTrue(reportStub.calledOnce);
- assert.isTrue(reportStub.calledWith(
- 'rebase-percent-nonzero',
- {percentRebaseDelta: 50}
- ));
+ sandbox.stub(element.$.restAPI, 'getDiff')
+ .returns(Promise.resolve(mockDiff));
+
+ element.addEventListener('render', () => {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+
+ const leftImage =
+ element.$.diff.$.diffTable.querySelector('td.left img');
+ const rightImage =
+ element.$.diff.$.diffTable.querySelector('td.right img');
+
+ assert.isOk(leftImage);
+ assert.isNotOk(rightImage);
+ done();
+ });
+
+ element.$.restAPI.getDiffPreferences().then(prefs => {
+ element.prefs = prefs;
+ element.reload();
+ });
});
- test('diff w/ all rebase delta', () => {
- const diff = {content: [{
- a: ['foo', 'bar'],
- b: ['baz', 'foo'],
- due_to_rebase: true,
- }]};
- element._reportDiff(diff);
- assert.isTrue(reportStub.calledOnce);
- assert.isTrue(reportStub.calledWith(
- 'rebase-percent-nonzero',
- {percentRebaseDelta: 100}
- ));
- });
+ test('does not render disallowed image type', done => {
+ const mockDiff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'DELETED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index f9c2f2c..0000000 100644',
+ '--- a/carrot.jpg',
+ '+++ /dev/null',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+ mockFile1.type = 'image/jpeg-evil';
- test('diff against parent event', () => {
- element.patchRange.basePatchNum = 'PARENT';
- const diff = {content: [{
- a: ['foo', 'bar'],
- b: ['baz', 'foo'],
- }]};
- element._reportDiff(diff);
- assert.isTrue(reportStub.calledOnce);
- assert.equal(reportStub.lastCall.args[0], 'diff-against-parent');
- assert.isUndefined(reportStub.lastCall.args[1]);
+ sandbox.stub(element.$.restAPI, 'getDiff')
+ .returns(Promise.resolve(mockDiff));
+
+ element.addEventListener('render', () => {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+ const leftImage =
+ element.$.diff.$.diffTable.querySelector('td.left img');
+ assert.isNotOk(leftImage);
+ done();
+ });
+
+ element.$.restAPI.getDiffPreferences().then(prefs => {
+ element.prefs = prefs;
+ element.reload();
+ });
+ });
+ });
+ });
+
+ test('delegates cancel()', () => {
+ const stub = sandbox.stub(element.$.diff, 'cancel');
+ element.patchRange = {};
+ element.reload();
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ test('delegates getCursorStops()', () => {
+ const returnValue = [document.createElement('b')];
+ const stub = sandbox.stub(element.$.diff, 'getCursorStops')
+ .returns(returnValue);
+ assert.equal(element.getCursorStops(), returnValue);
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ test('delegates isRangeSelected()', () => {
+ const returnValue = true;
+ const stub = sandbox.stub(element.$.diff, 'isRangeSelected')
+ .returns(returnValue);
+ assert.equal(element.isRangeSelected(), returnValue);
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ test('delegates toggleLeftDiff()', () => {
+ const stub = sandbox.stub(element.$.diff, 'toggleLeftDiff');
+ element.toggleLeftDiff();
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ suite('blame', () => {
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ test('clearBlame', () => {
+ element._blame = [];
+ const setBlameSpy = sandbox.spy(element.$.diff.$.diffBuilder, 'setBlame');
+ element.clearBlame();
+ assert.isNull(element._blame);
+ assert.isTrue(setBlameSpy.calledWithExactly(null));
+ assert.equal(element.isBlameLoaded, false);
+ });
+
+ test('loadBlame', () => {
+ const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
+ const showAlertStub = sinon.stub();
+ element.addEventListener('show-alert', showAlertStub);
+ const getBlameStub = sandbox.stub(element.$.restAPI, 'getBlame')
+ .returns(Promise.resolve(mockBlame));
+ element.changeNum = 42;
+ element.patchRange = {patchNum: 5, basePatchNum: 4};
+ element.path = 'foo/bar.baz';
+ return element.loadBlame().then(() => {
+ assert.isTrue(getBlameStub.calledWithExactly(
+ 42, 5, 'foo/bar.baz', true));
+ assert.isFalse(showAlertStub.called);
+ assert.equal(element._blame, mockBlame);
+ assert.equal(element.isBlameLoaded, true);
});
});
- test('comments sorting', () => {
- const comments = [
- {
- id: 'new_draft',
- message: 'i do not like either of you',
- __commentSide: 'left',
- __draft: true,
- updated: '2015-12-20 15:01:20.396000000',
- },
- {
- id: 'sallys_confession',
- message: 'i like you, jack',
- updated: '2015-12-23 15:00:20.396000000',
- line: 1,
- __commentSide: 'left',
- }, {
- id: 'jacks_reply',
- message: 'i like you, too',
- updated: '2015-12-24 15:01:20.396000000',
- __commentSide: 'left',
- line: 1,
- in_reply_to: 'sallys_confession',
- },
- ];
- const sortedComments = element._sortComments(comments);
- assert.equal(sortedComments[0], comments[1]);
- assert.equal(sortedComments[1], comments[2]);
- assert.equal(sortedComments[2], comments[0]);
+ test('loadBlame empty', () => {
+ const mockBlame = [];
+ const showAlertStub = sinon.stub();
+ element.addEventListener('show-alert', showAlertStub);
+ sandbox.stub(element.$.restAPI, 'getBlame')
+ .returns(Promise.resolve(mockBlame));
+ element.changeNum = 42;
+ element.patchRange = {patchNum: 5, basePatchNum: 4};
+ element.path = 'foo/bar.baz';
+ return element.loadBlame()
+ .then(() => {
+ assert.isTrue(false, 'Promise should not resolve');
+ })
+ .catch(() => {
+ assert.isTrue(showAlertStub.calledOnce);
+ assert.isNull(element._blame);
+ assert.equal(element.isBlameLoaded, false);
+ });
+ });
+ });
+
+ test('getThreadEls() returns .comment-threads', () => {
+ const threadEl = document.createElement('div');
+ threadEl.className = 'comment-thread';
+ dom(element.$.diff).appendChild(threadEl);
+ assert.deepEqual(element.getThreadEls(), [threadEl]);
+ });
+
+ test('delegates addDraftAtLine(el)', () => {
+ const param0 = document.createElement('b');
+ const stub = sandbox.stub(element.$.diff, 'addDraftAtLine');
+ element.addDraftAtLine(param0);
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 1);
+ assert.equal(stub.lastCall.args[0], param0);
+ });
+
+ test('delegates clearDiffContent()', () => {
+ const stub = sandbox.stub(element.$.diff, 'clearDiffContent');
+ element.clearDiffContent();
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ test('delegates expandAllContext()', () => {
+ const stub = sandbox.stub(element.$.diff, 'expandAllContext');
+ element.expandAllContext();
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ test('passes in changeNum', () => {
+ const value = '12345';
+ element.changeNum = value;
+ assert.equal(element.$.diff.changeNum, value);
+ });
+
+ test('passes in noAutoRender', () => {
+ const value = true;
+ element.noAutoRender = value;
+ assert.equal(element.$.diff.noAutoRender, value);
+ });
+
+ test('passes in patchRange', () => {
+ const value = {patchNum: 'foo', basePatchNum: 'bar'};
+ element.patchRange = value;
+ assert.equal(element.$.diff.patchRange, value);
+ });
+
+ test('passes in path', () => {
+ const value = 'some/file/path';
+ element.path = value;
+ assert.equal(element.$.diff.path, value);
+ });
+
+ test('passes in prefs', () => {
+ const value = {};
+ element.prefs = value;
+ assert.equal(element.$.diff.prefs, value);
+ });
+
+ test('passes in changeNum', () => {
+ const value = '12345';
+ element.changeNum = value;
+ assert.equal(element.$.diff.changeNum, value);
+ });
+
+ test('passes in projectName', () => {
+ const value = 'Gerrit';
+ element.projectName = value;
+ assert.equal(element.$.diff.projectName, value);
+ });
+
+ test('passes in displayLine', () => {
+ const value = true;
+ element.displayLine = value;
+ assert.equal(element.$.diff.displayLine, value);
+ });
+
+ test('passes in commitRange', () => {
+ const value = {};
+ element.commitRange = value;
+ assert.equal(element.$.diff.commitRange, value);
+ });
+
+ test('passes in hidden', () => {
+ const value = true;
+ element.hidden = value;
+ assert.equal(element.$.diff.hidden, value);
+ assert.isNotNull(element.getAttribute('hidden'));
+ });
+
+ test('passes in noRenderOnPrefsChange', () => {
+ const value = true;
+ element.noRenderOnPrefsChange = value;
+ assert.equal(element.$.diff.noRenderOnPrefsChange, value);
+ });
+
+ test('passes in lineWrapping', () => {
+ const value = true;
+ element.lineWrapping = value;
+ assert.equal(element.$.diff.lineWrapping, value);
+ });
+
+ test('passes in viewMode', () => {
+ const value = 'SIDE_BY_SIDE';
+ element.viewMode = value;
+ assert.equal(element.$.diff.viewMode, value);
+ });
+
+ test('passes in lineOfInterest', () => {
+ const value = {number: 123, leftSide: true};
+ element.lineOfInterest = value;
+ assert.equal(element.$.diff.lineOfInterest, value);
+ });
+
+ suite('_reportDiff', () => {
+ let reportStub;
+
+ setup(() => {
+ element = fixture('basic');
+ element.patchRange = {basePatchNum: 1};
+ reportStub = sandbox.stub(element.$.reporting, 'reportInteraction');
});
- test('_createThreads', () => {
- const comments = [
- {
- id: 'sallys_confession',
- message: 'i like you, jack',
- updated: '2015-12-23 15:00:20.396000000',
- line: 1,
- __commentSide: 'left',
- }, {
- id: 'jacks_reply',
- message: 'i like you, too',
- updated: '2015-12-24 15:01:20.396000000',
- __commentSide: 'left',
- line: 1,
- in_reply_to: 'sallys_confession',
- },
- {
- id: 'new_draft',
- message: 'i do not like either of you',
- __commentSide: 'left',
- __draft: true,
- updated: '2015-12-20 15:01:20.396000000',
- },
- ];
+ test('null and content-less', () => {
+ element._reportDiff(null);
+ assert.isFalse(reportStub.called);
- const actualThreads = element._createThreads(comments);
-
- assert.equal(actualThreads.length, 2);
-
- assert.equal(
- actualThreads[0].start_datetime, '2015-12-23 15:00:20.396000000');
- assert.equal(actualThreads[0].commentSide, 'left');
- assert.equal(actualThreads[0].comments.length, 2);
- assert.deepEqual(actualThreads[0].comments[0], comments[0]);
- assert.deepEqual(actualThreads[0].comments[1], comments[1]);
- assert.equal(actualThreads[0].patchNum, undefined);
- assert.equal(actualThreads[0].rootId, 'sallys_confession');
- assert.equal(actualThreads[0].lineNum, 1);
-
- assert.equal(
- actualThreads[1].start_datetime, '2015-12-20 15:01:20.396000000');
- assert.equal(actualThreads[1].commentSide, 'left');
- assert.equal(actualThreads[1].comments.length, 1);
- assert.deepEqual(actualThreads[1].comments[0], comments[2]);
- assert.equal(actualThreads[1].patchNum, undefined);
- assert.equal(actualThreads[1].rootId, 'new_draft');
- assert.equal(actualThreads[1].lineNum, undefined);
+ element._reportDiff({});
+ assert.isFalse(reportStub.called);
});
- test('_createThreads inherits patchNum and range', () => {
- const comments = [{
- id: 'betsys_confession',
+ test('diff w/ no delta', () => {
+ const diff = {
+ content: [
+ {ab: ['foo', 'bar']},
+ {ab: ['baz', 'foo']},
+ ],
+ };
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
+ assert.isUndefined(reportStub.lastCall.args[1]);
+ });
+
+ test('diff w/ no rebase delta', () => {
+ const diff = {
+ content: [
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo']},
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo'], b: ['bar', 'baz']},
+ {ab: ['foo', 'bar']},
+ {b: ['baz', 'foo']},
+ {ab: ['foo', 'bar']},
+ ],
+ };
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
+ assert.isUndefined(reportStub.lastCall.args[1]);
+ });
+
+ test('diff w/ some rebase delta', () => {
+ const diff = {
+ content: [
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo'], due_to_rebase: true},
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo'], b: ['bar', 'baz']},
+ {ab: ['foo', 'bar']},
+ {b: ['baz', 'foo'], due_to_rebase: true},
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo']},
+ ],
+ };
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.isTrue(reportStub.calledWith(
+ 'rebase-percent-nonzero',
+ {percentRebaseDelta: 50}
+ ));
+ });
+
+ test('diff w/ all rebase delta', () => {
+ const diff = {content: [{
+ a: ['foo', 'bar'],
+ b: ['baz', 'foo'],
+ due_to_rebase: true,
+ }]};
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.isTrue(reportStub.calledWith(
+ 'rebase-percent-nonzero',
+ {percentRebaseDelta: 100}
+ ));
+ });
+
+ test('diff against parent event', () => {
+ element.patchRange.basePatchNum = 'PARENT';
+ const diff = {content: [{
+ a: ['foo', 'bar'],
+ b: ['baz', 'foo'],
+ }]};
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.equal(reportStub.lastCall.args[0], 'diff-against-parent');
+ assert.isUndefined(reportStub.lastCall.args[1]);
+ });
+ });
+
+ test('comments sorting', () => {
+ const comments = [
+ {
+ id: 'new_draft',
+ message: 'i do not like either of you',
+ __commentSide: 'left',
+ __draft: true,
+ updated: '2015-12-20 15:01:20.396000000',
+ },
+ {
+ id: 'sallys_confession',
message: 'i like you, jack',
- updated: '2015-12-24 15:00:10.396000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 1,
- end_character: 2,
- },
- patch_set: 5,
+ updated: '2015-12-23 15:00:20.396000000',
+ line: 1,
+ __commentSide: 'left',
+ }, {
+ id: 'jacks_reply',
+ message: 'i like you, too',
+ updated: '2015-12-24 15:01:20.396000000',
__commentSide: 'left',
line: 1,
- }];
+ in_reply_to: 'sallys_confession',
+ },
+ ];
+ const sortedComments = element._sortComments(comments);
+ assert.equal(sortedComments[0], comments[1]);
+ assert.equal(sortedComments[1], comments[2]);
+ assert.equal(sortedComments[2], comments[0]);
+ });
- const expectedThreads = [
- {
- start_datetime: '2015-12-24 15:00:10.396000000',
- commentSide: 'left',
- comments: [{
- id: 'betsys_confession',
- message: 'i like you, jack',
- updated: '2015-12-24 15:00:10.396000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 1,
- end_character: 2,
- },
- patch_set: 5,
- __commentSide: 'left',
- line: 1,
- }],
- patchNum: 5,
- rootId: 'betsys_confession',
+ test('_createThreads', () => {
+ const comments = [
+ {
+ id: 'sallys_confession',
+ message: 'i like you, jack',
+ updated: '2015-12-23 15:00:20.396000000',
+ line: 1,
+ __commentSide: 'left',
+ }, {
+ id: 'jacks_reply',
+ message: 'i like you, too',
+ updated: '2015-12-24 15:01:20.396000000',
+ __commentSide: 'left',
+ line: 1,
+ in_reply_to: 'sallys_confession',
+ },
+ {
+ id: 'new_draft',
+ message: 'i do not like either of you',
+ __commentSide: 'left',
+ __draft: true,
+ updated: '2015-12-20 15:01:20.396000000',
+ },
+ ];
+
+ const actualThreads = element._createThreads(comments);
+
+ assert.equal(actualThreads.length, 2);
+
+ assert.equal(
+ actualThreads[0].start_datetime, '2015-12-23 15:00:20.396000000');
+ assert.equal(actualThreads[0].commentSide, 'left');
+ assert.equal(actualThreads[0].comments.length, 2);
+ assert.deepEqual(actualThreads[0].comments[0], comments[0]);
+ assert.deepEqual(actualThreads[0].comments[1], comments[1]);
+ assert.equal(actualThreads[0].patchNum, undefined);
+ assert.equal(actualThreads[0].rootId, 'sallys_confession');
+ assert.equal(actualThreads[0].lineNum, 1);
+
+ assert.equal(
+ actualThreads[1].start_datetime, '2015-12-20 15:01:20.396000000');
+ assert.equal(actualThreads[1].commentSide, 'left');
+ assert.equal(actualThreads[1].comments.length, 1);
+ assert.deepEqual(actualThreads[1].comments[0], comments[2]);
+ assert.equal(actualThreads[1].patchNum, undefined);
+ assert.equal(actualThreads[1].rootId, 'new_draft');
+ assert.equal(actualThreads[1].lineNum, undefined);
+ });
+
+ test('_createThreads inherits patchNum and range', () => {
+ const comments = [{
+ id: 'betsys_confession',
+ message: 'i like you, jack',
+ updated: '2015-12-24 15:00:10.396000000',
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 1,
+ end_character: 2,
+ },
+ patch_set: 5,
+ __commentSide: 'left',
+ line: 1,
+ }];
+
+ const expectedThreads = [
+ {
+ start_datetime: '2015-12-24 15:00:10.396000000',
+ commentSide: 'left',
+ comments: [{
+ id: 'betsys_confession',
+ message: 'i like you, jack',
+ updated: '2015-12-24 15:00:10.396000000',
range: {
start_line: 1,
start_character: 1,
end_line: 1,
end_character: 2,
},
- lineNum: 1,
- isOnParent: false,
+ patch_set: 5,
+ __commentSide: 'left',
+ line: 1,
+ }],
+ patchNum: 5,
+ rootId: 'betsys_confession',
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 1,
+ end_character: 2,
},
- ];
+ lineNum: 1,
+ isOnParent: false,
+ },
+ ];
- assert.deepEqual(
- element._createThreads(comments),
- expectedThreads);
- });
+ assert.deepEqual(
+ element._createThreads(comments),
+ expectedThreads);
+ });
- test('_createThreads does not thread unrelated comments at same location',
- () => {
- const comments = [
- {
- id: 'sallys_confession',
- message: 'i like you, jack',
- updated: '2015-12-23 15:00:20.396000000',
- __commentSide: 'left',
- }, {
- id: 'jacks_reply',
- message: 'i like you, too',
- updated: '2015-12-24 15:01:20.396000000',
- __commentSide: 'left',
- },
- ];
- assert.equal(element._createThreads(comments).length, 2);
- });
-
- test('_createThreads derives isOnParent using side from first comment',
- () => {
- const comments = [
- {
- id: 'sallys_confession',
- message: 'i like you, jack',
- updated: '2015-12-23 15:00:20.396000000',
- // line: 1,
- // __commentSide: 'left',
- }, {
- id: 'jacks_reply',
- message: 'i like you, too',
- updated: '2015-12-24 15:01:20.396000000',
- // __commentSide: 'left',
- // line: 1,
- in_reply_to: 'sallys_confession',
- },
- ];
-
- assert.equal(element._createThreads(comments)[0].isOnParent, false);
-
- comments[0].side = 'REVISION';
- assert.equal(element._createThreads(comments)[0].isOnParent, false);
-
- comments[0].side = 'PARENT';
- assert.equal(element._createThreads(comments)[0].isOnParent, true);
- });
-
- test('_getOrCreateThread', () => {
- const commentSide = 'left';
-
- assert.isOk(element._getOrCreateThread('2', 3,
- commentSide, undefined, false));
-
- let threads = Polymer.dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread');
-
- assert.equal(threads.length, 1);
- assert.equal(threads[0].commentSide, commentSide);
- assert.equal(threads[0].range, undefined);
- assert.equal(threads[0].isOnParent, false);
- assert.equal(threads[0].patchNum, 2);
-
- // Try to fetch a thread with a different range.
- const range = {
- start_line: 1,
- start_character: 1,
- end_line: 1,
- end_character: 3,
- };
-
- assert.isOk(element._getOrCreateThread(
- '3', 1, commentSide, range, true));
-
- threads = Polymer.dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread');
-
- assert.equal(threads.length, 2);
- assert.equal(threads[1].commentSide, commentSide);
- assert.equal(threads[1].range, range);
- assert.equal(threads[1].isOnParent, true);
- assert.equal(threads[1].patchNum, 3);
- });
-
- test('_filterThreadElsForLocation with no threads', () => {
- const line = {beforeNumber: 3, afterNumber: 5};
-
- const threads = [];
- assert.deepEqual(element._filterThreadElsForLocation(threads, line), []);
- assert.deepEqual(element._filterThreadElsForLocation(threads, line,
- Gerrit.DiffSide.LEFT), []);
- assert.deepEqual(element._filterThreadElsForLocation(threads, line,
- Gerrit.DiffSide.RIGHT), []);
- });
-
- test('_filterThreadElsForLocation for line comments', () => {
- const line = {beforeNumber: 3, afterNumber: 5};
-
- const l3 = document.createElement('div');
- l3.setAttribute('line-num', 3);
- l3.setAttribute('comment-side', 'left');
-
- const l5 = document.createElement('div');
- l5.setAttribute('line-num', 5);
- l5.setAttribute('comment-side', 'left');
-
- const r3 = document.createElement('div');
- r3.setAttribute('line-num', 3);
- r3.setAttribute('comment-side', 'right');
-
- const r5 = document.createElement('div');
- r5.setAttribute('line-num', 5);
- r5.setAttribute('comment-side', 'right');
-
- const threadEls = [l3, l5, r3, r5];
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line),
- [l3, r5]);
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
- Gerrit.DiffSide.LEFT), [l3]);
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
- Gerrit.DiffSide.RIGHT), [r5]);
- });
-
- test('_filterThreadElsForLocation for file comments', () => {
- const line = {beforeNumber: 'FILE', afterNumber: 'FILE'};
-
- const l = document.createElement('div');
- l.setAttribute('comment-side', 'left');
- l.setAttribute('line-num', 'FILE');
-
- const r = document.createElement('div');
- r.setAttribute('comment-side', 'right');
- r.setAttribute('line-num', 'FILE');
-
- const threadEls = [l, r];
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line),
- [l, r]);
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
- Gerrit.DiffSide.BOTH), [l, r]);
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
- Gerrit.DiffSide.LEFT), [l]);
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
- Gerrit.DiffSide.RIGHT), [r]);
- });
-
- suite('syntax layer with syntax_highlighting on', () => {
- setup(() => {
- const prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- context: -1,
- syntax_highlighting: true,
- };
- element.patchRange = {};
- element.prefs = prefs;
- });
-
- test('gr-diff-host provides syntax highlighting layer to gr-diff', () => {
- element.reload();
- assert.equal(element.$.diff.layers[0], element.$.syntaxLayer);
- });
-
- test('rendering normal-sized diff does not disable syntax', () => {
- element.diff = {
- content: [{
- a: ['foo'],
- }],
- };
- assert.isTrue(element.$.syntaxLayer.enabled);
- });
-
- test('rendering large diff disables syntax', () => {
- // Before it renders, set the first diff line to 500 '*' characters.
- element.diff = {
- content: [{
- a: [new Array(501).join('*')],
- }],
- };
- assert.isFalse(element.$.syntaxLayer.enabled);
- });
-
- test('starts syntax layer processing on render event', done => {
- sandbox.stub(element.$.syntaxLayer, 'process')
- .returns(Promise.resolve());
- sandbox.stub(element.$.restAPI, 'getDiff').returns(
- Promise.resolve({content: []}));
- element.reload();
- setTimeout(() => {
- element.dispatchEvent(
- new CustomEvent('render', {bubbles: true, composed: true}));
- assert.isTrue(element.$.syntaxLayer.process.called);
- done();
- });
- });
- });
-
- suite('syntax layer with syntax_highlgihting off', () => {
- setup(() => {
- const prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- context: -1,
- };
- element.diff = {
- content: [{
- a: ['foo'],
- }],
- };
- element.patchRange = {};
- element.prefs = prefs;
- });
-
- test('gr-diff-host provides syntax highlighting layer', () => {
- element.reload();
- assert.equal(element.$.diff.layers[0], element.$.syntaxLayer);
- });
-
- test('syntax layer should be disabled', () => {
- assert.isFalse(element.$.syntaxLayer.enabled);
- });
-
- test('still disabled for large diff', () => {
- // Before it renders, set the first diff line to 500 '*' characters.
- element.diff = {
- content: [{
- a: [new Array(501).join('*')],
- }],
- };
- assert.isFalse(element.$.syntaxLayer.enabled);
- });
- });
-
- suite('coverage layer', () => {
- let notifyStub;
- setup(() => {
- notifyStub = sinon.stub();
- stub('gr-js-api-interface', {
- getCoverageAnnotationApi() {
- return Promise.resolve({
- notify: notifyStub,
- getCoverageProvider() {
- return () => Promise.resolve([
- {
- 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,
- },
- },
- ]);
- },
- });
+ test('_createThreads does not thread unrelated comments at same location',
+ () => {
+ const comments = [
+ {
+ id: 'sallys_confession',
+ message: 'i like you, jack',
+ updated: '2015-12-23 15:00:20.396000000',
+ __commentSide: 'left',
+ }, {
+ id: 'jacks_reply',
+ message: 'i like you, too',
+ updated: '2015-12-24 15:01:20.396000000',
+ __commentSide: 'left',
},
- });
- element = fixture('basic');
- const prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- context: -1,
- };
- element.diff = {
- content: [{
- a: ['foo'],
- }],
- };
- element.patchRange = {};
- element.prefs = prefs;
+ ];
+ assert.equal(element._createThreads(comments).length, 2);
});
- test('getCoverageAnnotationApi should be called', done => {
- element.reload();
- flush(() => {
- assert.isTrue(element.$.jsAPI.getCoverageAnnotationApi.calledOnce);
- done();
- });
+ test('_createThreads derives isOnParent using side from first comment',
+ () => {
+ const comments = [
+ {
+ id: 'sallys_confession',
+ message: 'i like you, jack',
+ updated: '2015-12-23 15:00:20.396000000',
+ // line: 1,
+ // __commentSide: 'left',
+ }, {
+ id: 'jacks_reply',
+ message: 'i like you, too',
+ updated: '2015-12-24 15:01:20.396000000',
+ // __commentSide: 'left',
+ // line: 1,
+ in_reply_to: 'sallys_confession',
+ },
+ ];
+
+ assert.equal(element._createThreads(comments)[0].isOnParent, false);
+
+ comments[0].side = 'REVISION';
+ assert.equal(element._createThreads(comments)[0].isOnParent, false);
+
+ comments[0].side = 'PARENT';
+ assert.equal(element._createThreads(comments)[0].isOnParent, true);
});
- test('coverageRangeChanged should be called', done => {
- element.reload();
- flush(() => {
- assert.equal(notifyStub.callCount, 2);
- done();
- });
- });
+ test('_getOrCreateThread', () => {
+ const commentSide = 'left';
+
+ assert.isOk(element._getOrCreateThread('2', 3,
+ commentSide, undefined, false));
+
+ let threads = dom(element.$.diff)
+ .queryDistributedElements('gr-comment-thread');
+
+ assert.equal(threads.length, 1);
+ assert.equal(threads[0].commentSide, commentSide);
+ assert.equal(threads[0].range, undefined);
+ assert.equal(threads[0].isOnParent, false);
+ assert.equal(threads[0].patchNum, 2);
+
+ // Try to fetch a thread with a different range.
+ const range = {
+ start_line: 1,
+ start_character: 1,
+ end_line: 1,
+ end_character: 3,
+ };
+
+ assert.isOk(element._getOrCreateThread(
+ '3', 1, commentSide, range, true));
+
+ threads = dom(element.$.diff)
+ .queryDistributedElements('gr-comment-thread');
+
+ assert.equal(threads.length, 2);
+ assert.equal(threads[1].commentSide, commentSide);
+ assert.equal(threads[1].range, range);
+ assert.equal(threads[1].isOnParent, true);
+ assert.equal(threads[1].patchNum, 3);
+ });
+
+ test('_filterThreadElsForLocation with no threads', () => {
+ const line = {beforeNumber: 3, afterNumber: 5};
+
+ const threads = [];
+ assert.deepEqual(element._filterThreadElsForLocation(threads, line), []);
+ assert.deepEqual(element._filterThreadElsForLocation(threads, line,
+ Gerrit.DiffSide.LEFT), []);
+ assert.deepEqual(element._filterThreadElsForLocation(threads, line,
+ Gerrit.DiffSide.RIGHT), []);
+ });
+
+ test('_filterThreadElsForLocation for line comments', () => {
+ const line = {beforeNumber: 3, afterNumber: 5};
+
+ const l3 = document.createElement('div');
+ l3.setAttribute('line-num', 3);
+ l3.setAttribute('comment-side', 'left');
+
+ const l5 = document.createElement('div');
+ l5.setAttribute('line-num', 5);
+ l5.setAttribute('comment-side', 'left');
+
+ const r3 = document.createElement('div');
+ r3.setAttribute('line-num', 3);
+ r3.setAttribute('comment-side', 'right');
+
+ const r5 = document.createElement('div');
+ r5.setAttribute('line-num', 5);
+ r5.setAttribute('comment-side', 'right');
+
+ const threadEls = [l3, l5, r3, r5];
+ assert.deepEqual(element._filterThreadElsForLocation(threadEls, line),
+ [l3, r5]);
+ assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
+ Gerrit.DiffSide.LEFT), [l3]);
+ assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
+ Gerrit.DiffSide.RIGHT), [r5]);
+ });
+
+ test('_filterThreadElsForLocation for file comments', () => {
+ const line = {beforeNumber: 'FILE', afterNumber: 'FILE'};
+
+ const l = document.createElement('div');
+ l.setAttribute('comment-side', 'left');
+ l.setAttribute('line-num', 'FILE');
+
+ const r = document.createElement('div');
+ r.setAttribute('comment-side', 'right');
+ r.setAttribute('line-num', 'FILE');
+
+ const threadEls = [l, r];
+ assert.deepEqual(element._filterThreadElsForLocation(threadEls, line),
+ [l, r]);
+ assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
+ Gerrit.DiffSide.BOTH), [l, r]);
+ assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
+ Gerrit.DiffSide.LEFT), [l]);
+ assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
+ Gerrit.DiffSide.RIGHT), [r]);
+ });
+
+ suite('syntax layer with syntax_highlighting on', () => {
+ setup(() => {
+ const prefs = {
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ context: -1,
+ syntax_highlighting: true,
+ };
+ element.patchRange = {};
+ element.prefs = prefs;
});
- suite('trailing newlines', () => {
- setup(() => {
- });
+ test('gr-diff-host provides syntax highlighting layer to gr-diff', () => {
+ element.reload();
+ assert.equal(element.$.diff.layers[0], element.$.syntaxLayer);
+ });
- suite('_lastChunkForSide', () => {
- test('deltas', () => {
- const diff = {content: [
- {a: ['foo', 'bar'], b: ['baz']},
- {ab: ['foo', 'bar', 'baz']},
- {b: ['foo']},
- ]};
- assert.equal(element._lastChunkForSide(diff, false), diff.content[2]);
- assert.equal(element._lastChunkForSide(diff, true), diff.content[1]);
+ test('rendering normal-sized diff does not disable syntax', () => {
+ element.diff = {
+ content: [{
+ a: ['foo'],
+ }],
+ };
+ assert.isTrue(element.$.syntaxLayer.enabled);
+ });
- diff.content.push({a: ['foo'], b: ['bar']});
- assert.equal(element._lastChunkForSide(diff, false), diff.content[3]);
- assert.equal(element._lastChunkForSide(diff, true), diff.content[3]);
- });
+ test('rendering large diff disables syntax', () => {
+ // Before it renders, set the first diff line to 500 '*' characters.
+ element.diff = {
+ content: [{
+ a: [new Array(501).join('*')],
+ }],
+ };
+ assert.isFalse(element.$.syntaxLayer.enabled);
+ });
- test('addition with a undefined', () => {
- const diff = {content: [
- {b: ['foo', 'bar', 'baz']},
- ]};
- assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
- assert.isNull(element._lastChunkForSide(diff, true));
- });
-
- test('addition with a empty', () => {
- const diff = {content: [
- {a: [], b: ['foo', 'bar', 'baz']},
- ]};
- assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
- assert.isNull(element._lastChunkForSide(diff, true));
- });
-
- test('deletion with b undefined', () => {
- const diff = {content: [
- {a: ['foo', 'bar', 'baz']},
- ]};
- assert.isNull(element._lastChunkForSide(diff, false));
- assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
- });
-
- test('deletion with b empty', () => {
- const diff = {content: [
- {a: ['foo', 'bar', 'baz'], b: []},
- ]};
- assert.isNull(element._lastChunkForSide(diff, false));
- assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
- });
-
- test('empty', () => {
- const diff = {content: []};
- assert.isNull(element._lastChunkForSide(diff, false));
- assert.isNull(element._lastChunkForSide(diff, true));
- });
- });
-
- suite('_hasTrailingNewlines', () => {
- test('shared no trailing', () => {
- const diff = undefined;
- sandbox.stub(element, '_lastChunkForSide')
- .returns({ab: ['foo', 'bar']});
- assert.isFalse(element._hasTrailingNewlines(diff, false));
- assert.isFalse(element._hasTrailingNewlines(diff, true));
- });
-
- test('delta trailing in right', () => {
- const diff = undefined;
- sandbox.stub(element, '_lastChunkForSide')
- .returns({a: ['foo', 'bar'], b: ['baz', '']});
- assert.isTrue(element._hasTrailingNewlines(diff, false));
- assert.isFalse(element._hasTrailingNewlines(diff, true));
- });
-
- test('addition', () => {
- const diff = undefined;
- sandbox.stub(element, '_lastChunkForSide', (diff, leftSide) => {
- if (leftSide) { return null; }
- return {b: ['foo', '']};
- });
- assert.isTrue(element._hasTrailingNewlines(diff, false));
- assert.isNull(element._hasTrailingNewlines(diff, true));
- });
-
- test('deletion', () => {
- const diff = undefined;
- sandbox.stub(element, '_lastChunkForSide', (diff, leftSide) => {
- if (!leftSide) { return null; }
- return {a: ['foo']};
- });
- assert.isNull(element._hasTrailingNewlines(diff, false));
- assert.isFalse(element._hasTrailingNewlines(diff, true));
- });
+ test('starts syntax layer processing on render event', done => {
+ sandbox.stub(element.$.syntaxLayer, 'process')
+ .returns(Promise.resolve());
+ sandbox.stub(element.$.restAPI, 'getDiff').returns(
+ Promise.resolve({content: []}));
+ element.reload();
+ setTimeout(() => {
+ element.dispatchEvent(
+ new CustomEvent('render', {bubbles: true, composed: true}));
+ assert.isTrue(element.$.syntaxLayer.process.called);
+ done();
});
});
});
+
+ suite('syntax layer with syntax_highlgihting off', () => {
+ setup(() => {
+ const prefs = {
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ context: -1,
+ };
+ element.diff = {
+ content: [{
+ a: ['foo'],
+ }],
+ };
+ element.patchRange = {};
+ element.prefs = prefs;
+ });
+
+ test('gr-diff-host provides syntax highlighting layer', () => {
+ element.reload();
+ assert.equal(element.$.diff.layers[0], element.$.syntaxLayer);
+ });
+
+ test('syntax layer should be disabled', () => {
+ assert.isFalse(element.$.syntaxLayer.enabled);
+ });
+
+ test('still disabled for large diff', () => {
+ // Before it renders, set the first diff line to 500 '*' characters.
+ element.diff = {
+ content: [{
+ a: [new Array(501).join('*')],
+ }],
+ };
+ assert.isFalse(element.$.syntaxLayer.enabled);
+ });
+ });
+
+ suite('coverage layer', () => {
+ let notifyStub;
+ setup(() => {
+ notifyStub = sinon.stub();
+ stub('gr-js-api-interface', {
+ getCoverageAnnotationApi() {
+ return Promise.resolve({
+ notify: notifyStub,
+ getCoverageProvider() {
+ return () => Promise.resolve([
+ {
+ 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,
+ },
+ },
+ ]);
+ },
+ });
+ },
+ });
+ element = fixture('basic');
+ const prefs = {
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ context: -1,
+ };
+ element.diff = {
+ content: [{
+ a: ['foo'],
+ }],
+ };
+ element.patchRange = {};
+ element.prefs = prefs;
+ });
+
+ test('getCoverageAnnotationApi should be called', done => {
+ element.reload();
+ flush(() => {
+ assert.isTrue(element.$.jsAPI.getCoverageAnnotationApi.calledOnce);
+ done();
+ });
+ });
+
+ test('coverageRangeChanged should be called', done => {
+ element.reload();
+ flush(() => {
+ assert.equal(notifyStub.callCount, 2);
+ done();
+ });
+ });
+ });
+
+ suite('trailing newlines', () => {
+ setup(() => {
+ });
+
+ suite('_lastChunkForSide', () => {
+ test('deltas', () => {
+ const diff = {content: [
+ {a: ['foo', 'bar'], b: ['baz']},
+ {ab: ['foo', 'bar', 'baz']},
+ {b: ['foo']},
+ ]};
+ assert.equal(element._lastChunkForSide(diff, false), diff.content[2]);
+ assert.equal(element._lastChunkForSide(diff, true), diff.content[1]);
+
+ diff.content.push({a: ['foo'], b: ['bar']});
+ assert.equal(element._lastChunkForSide(diff, false), diff.content[3]);
+ assert.equal(element._lastChunkForSide(diff, true), diff.content[3]);
+ });
+
+ test('addition with a undefined', () => {
+ const diff = {content: [
+ {b: ['foo', 'bar', 'baz']},
+ ]};
+ assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
+ assert.isNull(element._lastChunkForSide(diff, true));
+ });
+
+ test('addition with a empty', () => {
+ const diff = {content: [
+ {a: [], b: ['foo', 'bar', 'baz']},
+ ]};
+ assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
+ assert.isNull(element._lastChunkForSide(diff, true));
+ });
+
+ test('deletion with b undefined', () => {
+ const diff = {content: [
+ {a: ['foo', 'bar', 'baz']},
+ ]};
+ assert.isNull(element._lastChunkForSide(diff, false));
+ assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
+ });
+
+ test('deletion with b empty', () => {
+ const diff = {content: [
+ {a: ['foo', 'bar', 'baz'], b: []},
+ ]};
+ assert.isNull(element._lastChunkForSide(diff, false));
+ assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
+ });
+
+ test('empty', () => {
+ const diff = {content: []};
+ assert.isNull(element._lastChunkForSide(diff, false));
+ assert.isNull(element._lastChunkForSide(diff, true));
+ });
+ });
+
+ suite('_hasTrailingNewlines', () => {
+ test('shared no trailing', () => {
+ const diff = undefined;
+ sandbox.stub(element, '_lastChunkForSide')
+ .returns({ab: ['foo', 'bar']});
+ assert.isFalse(element._hasTrailingNewlines(diff, false));
+ assert.isFalse(element._hasTrailingNewlines(diff, true));
+ });
+
+ test('delta trailing in right', () => {
+ const diff = undefined;
+ sandbox.stub(element, '_lastChunkForSide')
+ .returns({a: ['foo', 'bar'], b: ['baz', '']});
+ assert.isTrue(element._hasTrailingNewlines(diff, false));
+ assert.isFalse(element._hasTrailingNewlines(diff, true));
+ });
+
+ test('addition', () => {
+ const diff = undefined;
+ sandbox.stub(element, '_lastChunkForSide', (diff, leftSide) => {
+ if (leftSide) { return null; }
+ return {b: ['foo', '']};
+ });
+ assert.isTrue(element._hasTrailingNewlines(diff, false));
+ assert.isNull(element._hasTrailingNewlines(diff, true));
+ });
+
+ test('deletion', () => {
+ const diff = undefined;
+ sandbox.stub(element, '_lastChunkForSide', (diff, leftSide) => {
+ if (!leftSide) { return null; }
+ return {a: ['foo']};
+ });
+ assert.isNull(element._hasTrailingNewlines(diff, false));
+ assert.isFalse(element._hasTrailingNewlines(diff, true));
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.html b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.html
deleted file mode 100644
index 47cf771..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.html
+++ /dev/null
@@ -1,60 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-diff-mode-selector">
- <template>
- <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-button
- id="sideBySideBtn"
- link
- has-tooltip
- class$="[[_computeSelectedClass(mode, _VIEW_MODES.SIDE_BY_SIDE)]]"
- title="Side-by-side diff"
- on-click="_handleSideBySideTap">
- <iron-icon icon="gr-icons:side-by-side"></iron-icon>
- </gr-button>
- <gr-button
- id="unifiedBtn"
- link
- has-tooltip
- title="Unified diff"
- class$="[[_computeSelectedClass(mode, _VIEW_MODES.UNIFIED)]]"
- on-click="_handleUnifiedTap">
- <iron-icon icon="gr-icons:unified"></iron-icon>
- </gr-button>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-diff-mode-selector.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
index 68bca23..acd9457 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
@@ -14,65 +14,74 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrDiffModeSelector extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-diff-mode-selector'; }
+import '@polymer/iron-icon/iron-icon.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-diff-mode-selector_html.js';
- static get properties() {
- return {
- mode: {
- type: String,
- notify: true,
+/** @extends Polymer.Element */
+class GrDiffModeSelector extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-diff-mode-selector'; }
+
+ static get properties() {
+ return {
+ mode: {
+ type: String,
+ notify: true,
+ },
+
+ /**
+ * 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.
+ */
+ saveOnChange: {
+ type: Boolean,
+ value: false,
+ },
+
+ /** @type {?} */
+ _VIEW_MODES: {
+ type: Object,
+ readOnly: true,
+ value: {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
},
-
- /**
- * 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.
- */
- saveOnChange: {
- type: Boolean,
- value: false,
- },
-
- /** @type {?} */
- _VIEW_MODES: {
- type: Object,
- readOnly: true,
- value: {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- },
- },
- };
- }
-
- /**
- * Set the mode. If save on change is enabled also update the preference.
- */
- setMode(newMode) {
- if (this.saveOnChange && this.mode && this.mode !== newMode) {
- this.$.restAPI.savePreferences({diff_view: newMode});
- }
- this.mode = newMode;
- }
-
- _computeSelectedClass(diffViewMode, buttonViewMode) {
- return buttonViewMode === diffViewMode ? 'selected' : '';
- }
-
- _handleSideBySideTap() {
- this.setMode(this._VIEW_MODES.SIDE_BY_SIDE);
- }
-
- _handleUnifiedTap() {
- this.setMode(this._VIEW_MODES.UNIFIED);
- }
+ },
+ };
}
- customElements.define(GrDiffModeSelector.is, GrDiffModeSelector);
-})();
+ /**
+ * Set the mode. If save on change is enabled also update the preference.
+ */
+ setMode(newMode) {
+ if (this.saveOnChange && this.mode && this.mode !== newMode) {
+ this.$.restAPI.savePreferences({diff_view: newMode});
+ }
+ this.mode = newMode;
+ }
+
+ _computeSelectedClass(diffViewMode, buttonViewMode) {
+ return buttonViewMode === diffViewMode ? 'selected' : '';
+ }
+
+ _handleSideBySideTap() {
+ this.setMode(this._VIEW_MODES.SIDE_BY_SIDE);
+ }
+
+ _handleUnifiedTap() {
+ this.setMode(this._VIEW_MODES.UNIFIED);
+ }
+}
+
+customElements.define(GrDiffModeSelector.is, GrDiffModeSelector);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.js b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.js
new file mode 100644
index 0000000..5fe516c
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.js
@@ -0,0 +1,40 @@
+/**
+ * @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.js';
+
+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-button id="sideBySideBtn" link="" has-tooltip="" class\$="[[_computeSelectedClass(mode, _VIEW_MODES.SIDE_BY_SIDE)]]" title="Side-by-side diff" on-click="_handleSideBySideTap">
+ <iron-icon icon="gr-icons:side-by-side"></iron-icon>
+ </gr-button>
+ <gr-button id="unifiedBtn" link="" has-tooltip="" title="Unified diff" class\$="[[_computeSelectedClass(mode, _VIEW_MODES.UNIFIED)]]" on-click="_handleUnifiedTap">
+ <iron-icon icon="gr-icons:unified"></iron-icon>
+ </gr-button>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html
index 2f3d262..5ffae8c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html
@@ -19,18 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-mode-selector</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="/bower_components/page/page.js"></script>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="gr-diff-mode-selector.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script src="/node_modules/page/page.js"></script>
<test-fixture id="basic">
<template>
@@ -38,52 +31,54 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff-mode-selector tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-diff-mode-selector.js';
+suite('gr-diff-mode-selector tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('_computeSelectedClass', () => {
- assert.equal(
- element._computeSelectedClass('SIDE_BY_SIDE', 'SIDE_BY_SIDE'),
- 'selected');
- assert.equal(
- element._computeSelectedClass('SIDE_BY_SIDE', 'UNIFIED_DIFF'), '');
- });
-
- test('setMode', () => {
- const saveStub = sandbox.stub(element.$.restAPI, 'savePreferences');
-
- // Setting the mode initially does not save prefs.
- element.saveOnChange = true;
- element.setMode('SIDE_BY_SIDE');
- assert.isFalse(saveStub.called);
-
- // Setting the mode to itself does not save prefs.
- element.setMode('SIDE_BY_SIDE');
- assert.isFalse(saveStub.called);
-
- // Setting the mode to something else does not save prefs if saveOnChange
- // is false.
- element.saveOnChange = false;
- element.setMode('UNIFIED_DIFF');
- assert.isFalse(saveStub.called);
-
- // Setting the mode to something else does not save prefs if saveOnChange
- // is false.
- element.saveOnChange = true;
- element.setMode('SIDE_BY_SIDE');
- assert.isTrue(saveStub.calledOnce);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_computeSelectedClass', () => {
+ assert.equal(
+ element._computeSelectedClass('SIDE_BY_SIDE', 'SIDE_BY_SIDE'),
+ 'selected');
+ assert.equal(
+ element._computeSelectedClass('SIDE_BY_SIDE', 'UNIFIED_DIFF'), '');
+ });
+
+ test('setMode', () => {
+ const saveStub = sandbox.stub(element.$.restAPI, 'savePreferences');
+
+ // Setting the mode initially does not save prefs.
+ element.saveOnChange = true;
+ element.setMode('SIDE_BY_SIDE');
+ assert.isFalse(saveStub.called);
+
+ // Setting the mode to itself does not save prefs.
+ element.setMode('SIDE_BY_SIDE');
+ assert.isFalse(saveStub.called);
+
+ // Setting the mode to something else does not save prefs if saveOnChange
+ // is false.
+ element.saveOnChange = false;
+ element.setMode('UNIFIED_DIFF');
+ assert.isFalse(saveStub.called);
+
+ // Setting the mode to something else does not save prefs if saveOnChange
+ // is false.
+ element.saveOnChange = true;
+ element.setMode('SIDE_BY_SIDE');
+ assert.isTrue(saveStub.calledOnce);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html
deleted file mode 100644
index 21f6282..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html
+++ /dev/null
@@ -1,81 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-diff-preferences/gr-diff-preferences.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-
-<dom-module id="gr-diff-preferences-dialog">
- <template>
- <style include="shared-styles">
- .diffHeader,
- .diffActions {
- padding: var(--spacing-l) var(--spacing-xl);
- }
- .diffHeader,
- .diffActions {
- background-color: var(--dialog-background-color);
- }
- .diffHeader {
- border-bottom: 1px solid var(--border-color);
- font-weight: var(--font-weight-bold);
- }
- .diffActions {
- border-top: 1px solid var(--border-color);
- display: flex;
- justify-content: flex-end;
- }
- .diffPrefsOverlay gr-button {
- margin-left: var(--spacing-l);
- }
- div.edited:after {
- color: var(--deemphasized-text-color);
- content: ' *';
- }
- #diffPreferences {
- display: flex;
- padding: var(--spacing-s) var(--spacing-xl);
- }
- </style>
- <gr-overlay id="diffPrefsOverlay" with-backdrop>
- <div class$="diffHeader [[_computeHeaderClass(_diffPrefsChanged)]]">Diff Preferences</div>
- <gr-diff-preferences
- id="diffPreferences"
- diff-prefs="{{diffPrefs}}"
- has-unsaved-changes="{{_diffPrefsChanged}}"></gr-diff-preferences>
- <div class="diffActions">
- <gr-button
- id="cancelButton"
- link
- on-click="_handleCancelDiff">
- Cancel
- </gr-button>
- <gr-button
- id="saveButton"
- link primary
- on-click="_handleSaveDiffPreferences"
- disabled$="[[!_diffPrefsChanged]]">
- Save
- </gr-button>
- </div>
- </gr-overlay>
- </template>
- <script src="gr-diff-preferences-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
index 6aad66c..fa79e49 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
@@ -14,65 +14,76 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
- */
- class GrDiffPreferencesDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-diff-preferences-dialog'; }
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-diff-preferences/gr-diff-preferences.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-diff-preferences-dialog_html.js';
- static get properties() {
- return {
- /** @type {?} */
- diffPrefs: Object,
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrDiffPreferencesDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _diffPrefsChanged: Boolean,
- };
- }
+ static get is() { return 'gr-diff-preferences-dialog'; }
- getFocusStops() {
- return {
- start: this.$.diffPreferences.$.contextSelect,
- end: this.$.saveButton,
- };
- }
+ static get properties() {
+ return {
+ /** @type {?} */
+ diffPrefs: Object,
- resetFocus() {
- this.$.diffPreferences.$.contextSelect.focus();
- }
-
- _computeHeaderClass(changed) {
- return changed ? 'edited' : '';
- }
-
- _handleCancelDiff(e) {
- e.stopPropagation();
- this.$.diffPrefsOverlay.close();
- }
-
- open() {
- this.$.diffPrefsOverlay.open().then(() => {
- const focusStops = this.getFocusStops();
- this.$.diffPrefsOverlay.setFocusStops(focusStops);
- this.resetFocus();
- });
- }
-
- _handleSaveDiffPreferences() {
- this.$.diffPreferences.save().then(() => {
- this.fire('reload-diff-preference', null, {bubbles: false});
-
- this.$.diffPrefsOverlay.close();
- });
- }
+ _diffPrefsChanged: Boolean,
+ };
}
- customElements.define(GrDiffPreferencesDialog.is, GrDiffPreferencesDialog);
-})();
+ getFocusStops() {
+ return {
+ start: this.$.diffPreferences.$.contextSelect,
+ end: this.$.saveButton,
+ };
+ }
+
+ resetFocus() {
+ this.$.diffPreferences.$.contextSelect.focus();
+ }
+
+ _computeHeaderClass(changed) {
+ return changed ? 'edited' : '';
+ }
+
+ _handleCancelDiff(e) {
+ e.stopPropagation();
+ this.$.diffPrefsOverlay.close();
+ }
+
+ open() {
+ this.$.diffPrefsOverlay.open().then(() => {
+ const focusStops = this.getFocusStops();
+ this.$.diffPrefsOverlay.setFocusStops(focusStops);
+ this.resetFocus();
+ });
+ }
+
+ _handleSaveDiffPreferences() {
+ this.$.diffPreferences.save().then(() => {
+ this.fire('reload-diff-preference', null, {bubbles: false});
+
+ this.$.diffPrefsOverlay.close();
+ });
+ }
+}
+
+customElements.define(GrDiffPreferencesDialog.is, GrDiffPreferencesDialog);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.js
new file mode 100644
index 0000000..cd26a0b
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.js
@@ -0,0 +1,62 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ .diffHeader,
+ .diffActions {
+ padding: var(--spacing-l) var(--spacing-xl);
+ }
+ .diffHeader,
+ .diffActions {
+ background-color: var(--dialog-background-color);
+ }
+ .diffHeader {
+ border-bottom: 1px solid var(--border-color);
+ font-weight: var(--font-weight-bold);
+ }
+ .diffActions {
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ justify-content: flex-end;
+ }
+ .diffPrefsOverlay gr-button {
+ margin-left: var(--spacing-l);
+ }
+ div.edited:after {
+ color: var(--deemphasized-text-color);
+ content: ' *';
+ }
+ #diffPreferences {
+ display: flex;
+ padding: var(--spacing-s) var(--spacing-xl);
+ }
+ </style>
+ <gr-overlay id="diffPrefsOverlay" with-backdrop="">
+ <div class\$="diffHeader [[_computeHeaderClass(_diffPrefsChanged)]]">Diff Preferences</div>
+ <gr-diff-preferences id="diffPreferences" diff-prefs="{{diffPrefs}}" has-unsaved-changes="{{_diffPrefsChanged}}"></gr-diff-preferences>
+ <div class="diffActions">
+ <gr-button id="cancelButton" link="" on-click="_handleCancelDiff">
+ Cancel
+ </gr-button>
+ <gr-button id="saveButton" link="" primary="" on-click="_handleSaveDiffPreferences" disabled\$="[[!_diffPrefsChanged]]">
+ Save
+ </gr-button>
+ </div>
+ </gr-overlay>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.html
deleted file mode 100644
index 922ac87..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.html
+++ /dev/null
@@ -1,26 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<dom-module id="gr-diff-processor">
- <script src="../gr-diff/gr-diff-line.js"></script>
- <script src="../gr-diff/gr-diff-group.js"></script>
-
- <script src="../../../scripts/util.js"></script>
- <script src="gr-diff-processor.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
index dcda64d..adcb375 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -14,653 +14,658 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const WHOLE_FILE = -1;
+import '../gr-diff/gr-diff-line.js';
+import '../gr-diff/gr-diff-group.js';
+import '../../../scripts/util.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
- const DiffSide = {
- LEFT: 'left',
- RIGHT: 'right',
- };
+const WHOLE_FILE = -1;
- const DiffHighlights = {
- ADDED: 'edit_b',
- REMOVED: 'edit_a',
- };
+const DiffSide = {
+ LEFT: 'left',
+ RIGHT: 'right',
+};
+
+const DiffHighlights = {
+ ADDED: 'edit_b',
+ REMOVED: 'edit_a',
+};
+
+/**
+ * The maximum size for an addition or removal chunk before it is broken down
+ * into a series of chunks that are this size at most.
+ *
+ * Note: The value of 120 is chosen so that it is larger than the default
+ * _asyncThreshold of 64, but feel free to tune this constant to your
+ * performance needs.
+ */
+const MAX_GROUP_SIZE = 120;
+
+/**
+ * Converts the API's `DiffContent`s to `GrDiffGroup`s for rendering.
+ *
+ * Glossary:
+ * - "chunk": A single `DiffContent` as returned by the API.
+ * - "group": A single `GrDiffGroup` as used for rendering.
+ * - "common" chunk/group: A chunk/group that should be considered unchanged
+ * for diffing purposes. This can mean its either actually unchanged, or it
+ * has only whitespace changes.
+ * - "key location": A line number and side of the diff that should not be
+ * collapsed e.g. because a comment is attached to it, or because it was
+ * provided in the URL and thus should be visible
+ * - "uncollapsible" chunk/group: A chunk/group that is either not "common",
+ * or cannot be collapsed because it contains a key location
+ *
+ * Here a a number of tasks this processor performs:
+ * - splitting large chunks to allow more granular async rendering
+ * - adding a group for the "File" pseudo line that file-level comments can
+ * be attached to
+ * - replacing common parts of the diff that are outside the user's
+ * context setting and do not have comments with a group representing the
+ * "expand context" widget. This may require splitting a chunk/group so
+ * that the part that is within the context or has comments is shown, while
+ * the rest is not.
+ *
+ * @extends Polymer.Element
+ */
+class GrDiffProcessor extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get is() { return 'gr-diff-processor'; }
+
+ static get properties() {
+ return {
+
+ /**
+ * The amount of context around collapsed groups.
+ */
+ context: Number,
+
+ /**
+ * The array of groups output by the processor.
+ */
+ groups: {
+ type: Array,
+ notify: true,
+ },
+
+ /**
+ * Locations that should not be collapsed, including the locations of
+ * comments.
+ */
+ keyLocations: {
+ type: Object,
+ value() { return {left: {}, right: {}}; },
+ },
+
+ /**
+ * The maximum number of lines to process synchronously.
+ */
+ _asyncThreshold: {
+ type: Number,
+ value: 64,
+ },
+
+ /** @type {?number} */
+ _nextStepHandle: Number,
+ /**
+ * The promise last returned from `process()` while the asynchronous
+ * processing is running - `null` otherwise. Provides a `cancel()`
+ * method that rejects it with `{isCancelled: true}`.
+ *
+ * @type {?Object}
+ */
+ _processPromise: {
+ type: Object,
+ value: null,
+ },
+ _isScrolling: Boolean,
+ };
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this.listen(window, 'scroll', '_handleWindowScroll');
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this.cancel();
+ this.unlisten(window, 'scroll', '_handleWindowScroll');
+ }
+
+ _handleWindowScroll() {
+ this._isScrolling = true;
+ this.debounce('resetIsScrolling', () => {
+ this._isScrolling = false;
+ }, 50);
+ }
/**
- * The maximum size for an addition or removal chunk before it is broken down
- * into a series of chunks that are this size at most.
+ * Asynchronously process the diff chunks into groups. As it processes, it
+ * will splice groups into the `groups` property of the component.
*
- * Note: The value of 120 is chosen so that it is larger than the default
- * _asyncThreshold of 64, but feel free to tune this constant to your
- * performance needs.
+ * @param {!Array<!Gerrit.DiffChunk>} chunks
+ * @param {boolean} isBinary
+ *
+ * @return {!Promise<!Array<!Object>>} A promise that resolves with an
+ * array of GrDiffGroups when the diff is completely processed.
*/
- const MAX_GROUP_SIZE = 120;
+ process(chunks, isBinary) {
+ // Cancel any still running process() calls, because they append to the
+ // same groups field.
+ this.cancel();
- /**
- * Converts the API's `DiffContent`s to `GrDiffGroup`s for rendering.
- *
- * Glossary:
- * - "chunk": A single `DiffContent` as returned by the API.
- * - "group": A single `GrDiffGroup` as used for rendering.
- * - "common" chunk/group: A chunk/group that should be considered unchanged
- * for diffing purposes. This can mean its either actually unchanged, or it
- * has only whitespace changes.
- * - "key location": A line number and side of the diff that should not be
- * collapsed e.g. because a comment is attached to it, or because it was
- * provided in the URL and thus should be visible
- * - "uncollapsible" chunk/group: A chunk/group that is either not "common",
- * or cannot be collapsed because it contains a key location
- *
- * Here a a number of tasks this processor performs:
- * - splitting large chunks to allow more granular async rendering
- * - adding a group for the "File" pseudo line that file-level comments can
- * be attached to
- * - replacing common parts of the diff that are outside the user's
- * context setting and do not have comments with a group representing the
- * "expand context" widget. This may require splitting a chunk/group so
- * that the part that is within the context or has comments is shown, while
- * the rest is not.
- *
- * @extends Polymer.Element
- */
- class GrDiffProcessor extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-diff-processor'; }
+ this.groups = [];
+ this.push('groups', this._makeFileComments());
- static get properties() {
- return {
+ // If it's a binary diff, we won't be rendering hunks of text differences
+ // so finish processing.
+ if (isBinary) { return Promise.resolve(); }
- /**
- * The amount of context around collapsed groups.
- */
- context: Number,
+ this._processPromise = util.makeCancelable(
+ new Promise(resolve => {
+ const state = {
+ lineNums: {left: 0, right: 0},
+ chunkIndex: 0,
+ };
- /**
- * The array of groups output by the processor.
- */
- groups: {
- type: Array,
- notify: true,
- },
+ chunks = this._splitLargeChunks(chunks);
+ chunks = this._splitCommonChunksWithKeyLocations(chunks);
- /**
- * Locations that should not be collapsed, including the locations of
- * comments.
- */
- keyLocations: {
- type: Object,
- value() { return {left: {}, right: {}}; },
- },
-
- /**
- * The maximum number of lines to process synchronously.
- */
- _asyncThreshold: {
- type: Number,
- value: 64,
- },
-
- /** @type {?number} */
- _nextStepHandle: Number,
- /**
- * The promise last returned from `process()` while the asynchronous
- * processing is running - `null` otherwise. Provides a `cancel()`
- * method that rejects it with `{isCancelled: true}`.
- *
- * @type {?Object}
- */
- _processPromise: {
- type: Object,
- value: null,
- },
- _isScrolling: Boolean,
- };
- }
-
- /** @override */
- attached() {
- super.attached();
- this.listen(window, 'scroll', '_handleWindowScroll');
- }
-
- /** @override */
- detached() {
- super.detached();
- this.cancel();
- this.unlisten(window, 'scroll', '_handleWindowScroll');
- }
-
- _handleWindowScroll() {
- this._isScrolling = true;
- this.debounce('resetIsScrolling', () => {
- this._isScrolling = false;
- }, 50);
- }
-
- /**
- * Asynchronously process the diff chunks into groups. As it processes, it
- * will splice groups into the `groups` property of the component.
- *
- * @param {!Array<!Gerrit.DiffChunk>} chunks
- * @param {boolean} isBinary
- *
- * @return {!Promise<!Array<!Object>>} A promise that resolves with an
- * array of GrDiffGroups when the diff is completely processed.
- */
- process(chunks, isBinary) {
- // Cancel any still running process() calls, because they append to the
- // same groups field.
- this.cancel();
-
- this.groups = [];
- this.push('groups', this._makeFileComments());
-
- // If it's a binary diff, we won't be rendering hunks of text differences
- // so finish processing.
- if (isBinary) { return Promise.resolve(); }
-
- this._processPromise = util.makeCancelable(
- new Promise(resolve => {
- const state = {
- lineNums: {left: 0, right: 0},
- chunkIndex: 0,
- };
-
- chunks = this._splitLargeChunks(chunks);
- chunks = this._splitCommonChunksWithKeyLocations(chunks);
-
- let currentBatch = 0;
- const nextStep = () => {
- if (this._isScrolling) {
- this._nextStepHandle = this.async(nextStep, 100);
- return;
- }
- // If we are done, resolve the promise.
- if (state.chunkIndex >= chunks.length) {
- resolve();
- this._nextStepHandle = null;
- return;
- }
-
- // Process the next chunk and incorporate the result.
- const stateUpdate = this._processNext(state, chunks);
- for (const group of stateUpdate.groups) {
- this.push('groups', group);
- currentBatch += group.lines.length;
- }
- state.lineNums.left += stateUpdate.lineDelta.left;
- state.lineNums.right += stateUpdate.lineDelta.right;
-
- // Increment the index and recurse.
- state.chunkIndex = stateUpdate.newChunkIndex;
- if (currentBatch >= this._asyncThreshold) {
- currentBatch = 0;
- this._nextStepHandle = this.async(nextStep, 1);
- } else {
- nextStep.call(this);
- }
- };
-
- nextStep.call(this);
- }));
- return this._processPromise
- .finally(() => { this._processPromise = null; });
- }
-
- /**
- * Cancel any jobs that are running.
- */
- cancel() {
- if (this._nextStepHandle != null) {
- this.cancelAsync(this._nextStepHandle);
- this._nextStepHandle = null;
- }
- if (this._processPromise) {
- this._processPromise.cancel();
- }
- }
-
- /**
- * Process the next uncollapsible chunk, or the next collapsible chunks.
- *
- * @param {!Object} state
- * @param {!Array<!Object>} chunks
- * @return {{lineDelta: {left: number, right: number}, groups: !Array<!Object>, newChunkIndex: number}}
- */
- _processNext(state, chunks) {
- const firstUncollapsibleChunkIndex =
- this._firstUncollapsibleChunkIndex(chunks, state.chunkIndex);
- if (firstUncollapsibleChunkIndex === state.chunkIndex) {
- const chunk = chunks[state.chunkIndex];
- return {
- lineDelta: {
- left: this._linesLeft(chunk).length,
- right: this._linesRight(chunk).length,
- },
- groups: [this._chunkToGroup(
- chunk, state.lineNums.left + 1, state.lineNums.right + 1)],
- newChunkIndex: state.chunkIndex + 1,
- };
- }
-
- return this._processCollapsibleChunks(
- state, chunks, firstUncollapsibleChunkIndex);
- }
-
- _linesLeft(chunk) {
- return chunk.ab || chunk.a || [];
- }
-
- _linesRight(chunk) {
- return chunk.ab || chunk.b || [];
- }
-
- _firstUncollapsibleChunkIndex(chunks, offset) {
- let chunkIndex = offset;
- while (chunkIndex < chunks.length &&
- this._isCollapsibleChunk(chunks[chunkIndex])) {
- chunkIndex++;
- }
- return chunkIndex;
- }
-
- _isCollapsibleChunk(chunk) {
- return (chunk.ab || chunk.common) && !chunk.keyLocation;
- }
-
- /**
- * Process a stretch of collapsible chunks.
- *
- * Outputs up to three groups:
- * 1) Visible context before the hidden common code, unless it's the
- * very beginning of the file.
- * 2) Context hidden behind a context bar, unless empty.
- * 3) Visible context after the hidden common code, unless it's the very
- * end of the file.
- *
- * @param {!Object} state
- * @param {!Array<Object>} chunks
- * @param {number} firstUncollapsibleChunkIndex
- * @return {{lineDelta: {left: number, right: number}, groups: !Array<!Object>, newChunkIndex: number}}
- */
- _processCollapsibleChunks(
- state, chunks, firstUncollapsibleChunkIndex) {
- const collapsibleChunks = chunks.slice(
- state.chunkIndex, firstUncollapsibleChunkIndex);
- const lineCount = collapsibleChunks.reduce(
- (sum, chunk) => sum + this._commonChunkLength(chunk), 0);
-
- let groups = this._chunksToGroups(
- collapsibleChunks,
- state.lineNums.left + 1,
- state.lineNums.right + 1);
-
- if (this.context !== WHOLE_FILE) {
- const hiddenStart = state.chunkIndex === 0 ? 0 : this.context;
- const hiddenEnd = lineCount - (
- firstUncollapsibleChunkIndex === chunks.length ?
- 0 : this.context);
- groups = GrDiffGroup.hideInContextControl(
- groups, hiddenStart, hiddenEnd);
- }
-
- return {
- lineDelta: {
- left: lineCount,
- right: lineCount,
- },
- groups,
- newChunkIndex: firstUncollapsibleChunkIndex,
- };
- }
-
- _commonChunkLength(chunk) {
- console.assert(chunk.ab || chunk.common);
- console.assert(
- !chunk.a || (chunk.b && chunk.a.length === chunk.b.length),
- `common chunk needs same number of a and b lines: `, chunk);
- return this._linesLeft(chunk).length;
- }
-
- /**
- * @param {!Array<!Object>} chunks
- * @param {number} offsetLeft
- * @param {number} offsetRight
- * @return {!Array<!Object>} (GrDiffGroup)
- */
- _chunksToGroups(chunks, offsetLeft, offsetRight) {
- return chunks.map(chunk => {
- const group = this._chunkToGroup(chunk, offsetLeft, offsetRight);
- const chunkLength = this._commonChunkLength(chunk);
- offsetLeft += chunkLength;
- offsetRight += chunkLength;
- return group;
- });
- }
-
- /**
- * @param {!Object} chunk
- * @param {number} offsetLeft
- * @param {number} offsetRight
- * @return {!Object} (GrDiffGroup)
- */
- _chunkToGroup(chunk, offsetLeft, offsetRight) {
- const type = chunk.ab ? GrDiffGroup.Type.BOTH : GrDiffGroup.Type.DELTA;
- const lines = this._linesFromChunk(chunk, offsetLeft, offsetRight);
- const group = new GrDiffGroup(type, lines);
- group.keyLocation = chunk.keyLocation;
- group.dueToRebase = chunk.due_to_rebase;
- group.ignoredWhitespaceOnly = chunk.common;
- return group;
- }
-
- _linesFromChunk(chunk, offsetLeft, offsetRight) {
- if (chunk.ab) {
- return chunk.ab.map((row, i) => this._lineFromRow(
- GrDiffLine.Type.BOTH, offsetLeft, offsetRight, row, i));
- }
- let lines = [];
- if (chunk.a) {
- // Avoiding a.push(...b) because that causes callstack overflows for
- // large b, which can occur when large files are added removed.
- lines = lines.concat(this._linesFromRows(
- GrDiffLine.Type.REMOVE, chunk.a, offsetLeft,
- chunk[DiffHighlights.REMOVED]));
- }
- if (chunk.b) {
- // Avoiding a.push(...b) because that causes callstack overflows for
- // large b, which can occur when large files are added removed.
- lines = lines.concat(this._linesFromRows(
- GrDiffLine.Type.ADD, chunk.b, offsetRight,
- chunk[DiffHighlights.ADDED]));
- }
- return lines;
- }
-
- /**
- * @param {string} lineType (GrDiffLine.Type)
- * @param {!Array<string>} rows
- * @param {number} offset
- * @param {?Array<!Gerrit.IntralineInfo>=} opt_intralineInfos
- * @return {!Array<!Object>} (GrDiffLine)
- */
- _linesFromRows(lineType, rows, offset, opt_intralineInfos) {
- const grDiffHighlights = opt_intralineInfos ?
- this._convertIntralineInfos(rows, opt_intralineInfos) : undefined;
- return rows.map((row, i) => this._lineFromRow(
- lineType, offset, offset, row, i, grDiffHighlights));
- }
-
- /**
- * @param {string} type (GrDiffLine.Type)
- * @param {number} offsetLeft
- * @param {number} offsetRight
- * @param {string} row
- * @param {number} i
- * @param {!Array<!Object>=} opt_highlights
- * @return {!Object} (GrDiffLine)
- */
- _lineFromRow(type, offsetLeft, offsetRight, row, i, opt_highlights) {
- const line = new GrDiffLine(type);
- line.text = row;
- if (type !== GrDiffLine.Type.ADD) line.beforeNumber = offsetLeft + i;
- if (type !== GrDiffLine.Type.REMOVE) line.afterNumber = offsetRight + i;
- if (opt_highlights) {
- line.hasIntralineInfo = true;
- line.highlights = opt_highlights.filter(hl => hl.contentIndex === i);
- } else {
- line.hasIntralineInfo = false;
- }
- return line;
- }
-
- _makeFileComments() {
- const line = new GrDiffLine(GrDiffLine.Type.BOTH);
- line.beforeNumber = GrDiffLine.FILE;
- line.afterNumber = GrDiffLine.FILE;
- return new GrDiffGroup(GrDiffGroup.Type.BOTH, [line]);
- }
-
- /**
- * Split chunks into smaller chunks of the same kind.
- *
- * This is done to prevent doing too much work on the main thread in one
- * uninterrupted rendering step, which would make the browser unresponsive.
- *
- * Note that in the case of unmodified chunks, we only split chunks if the
- * context is set to file (because otherwise they are split up further down
- * the processing into the visible and hidden context), and only split it
- * into 2 chunks, one max sized one and the rest (for reasons that are
- * unclear to me).
- *
- * @param {!Array<!Gerrit.DiffChunk>} chunks Chunks as returned from the server
- * @return {!Array<!Gerrit.DiffChunk>} Finer grained chunks.
- */
- _splitLargeChunks(chunks) {
- const newChunks = [];
-
- for (const chunk of chunks) {
- if (!chunk.ab) {
- for (const subChunk of this._breakdownChunk(chunk)) {
- newChunks.push(subChunk);
- }
- continue;
- }
-
- // If the context is set to "whole file", then break down the shared
- // chunks so they can be rendered incrementally. Note: this is not
- // enabled for any other context preference because manipulating the
- // chunks in this way violates assumptions by the context grouper logic.
- if (this.context === -1 && chunk.ab.length > MAX_GROUP_SIZE * 2) {
- // Split large shared chunks in two, where the first is the maximum
- // group size.
- newChunks.push({ab: chunk.ab.slice(0, MAX_GROUP_SIZE)});
- newChunks.push({ab: chunk.ab.slice(MAX_GROUP_SIZE)});
- } else {
- newChunks.push(chunk);
- }
- }
- return newChunks;
- }
-
- /**
- * In order to show key locations, such as comments, out of the bounds of
- * the selected context, treat them as separate chunks within the model so
- * that the content (and context surrounding it) renders correctly.
- *
- * @param {!Array<!Object>} chunks DiffContents as returned from server.
- * @return {!Array<!Object>} Finer grained DiffContents.
- */
- _splitCommonChunksWithKeyLocations(chunks) {
- const result = [];
- let leftLineNum = 1;
- let rightLineNum = 1;
-
- for (const chunk of chunks) {
- // If it isn't a common chunk, append it as-is and update line numbers.
- if (!chunk.ab && !chunk.common) {
- if (chunk.a) {
- leftLineNum += chunk.a.length;
- }
- if (chunk.b) {
- rightLineNum += chunk.b.length;
- }
- result.push(chunk);
- continue;
- }
-
- if (chunk.common && chunk.a.length != chunk.b.length) {
- throw new Error(
- 'DiffContent with common=true must always have equal length');
- }
- const numLines = this._commonChunkLength(chunk);
- const chunkEnds = this._findChunkEndsAtKeyLocations(
- numLines, leftLineNum, rightLineNum);
- leftLineNum += numLines;
- rightLineNum += numLines;
-
- if (chunk.ab) {
- result.push(...this._splitAtChunkEnds(chunk.ab, chunkEnds)
- .map(({lines, keyLocation}) =>
- Object.assign({}, chunk, {ab: lines, keyLocation})));
- } else if (chunk.common) {
- const aChunks = this._splitAtChunkEnds(chunk.a, chunkEnds);
- const bChunks = this._splitAtChunkEnds(chunk.b, chunkEnds);
- result.push(...aChunks.map(({lines, keyLocation}, i) =>
- Object.assign(
- {}, chunk, {a: lines, b: bChunks[i].lines, keyLocation})));
- }
- }
-
- return result;
- }
-
- /**
- * @return {!Array<{offset: number, keyLocation: boolean}>} Offsets of the
- * new chunk ends, including whether it's a key location.
- */
- _findChunkEndsAtKeyLocations(numLines, leftOffset, rightOffset) {
- const result = [];
- let lastChunkEnd = 0;
- for (let i=0; i<numLines; i++) {
- // If this line should not be collapsed.
- if (this.keyLocations[DiffSide.LEFT][leftOffset + i] ||
- this.keyLocations[DiffSide.RIGHT][rightOffset + i]) {
- // If any lines have been accumulated into the chunk leading up to
- // this non-collapse line, then add them as a chunk and start a new
- // one.
- if (i > lastChunkEnd) {
- result.push({offset: i, keyLocation: false});
- lastChunkEnd = i;
- }
-
- // Add the non-collapse line as its own chunk.
- result.push({offset: i + 1, keyLocation: true});
- }
- }
-
- if (numLines > lastChunkEnd) {
- result.push({offset: numLines, keyLocation: false});
- }
-
- return result;
- }
-
- _splitAtChunkEnds(lines, chunkEnds) {
- const result = [];
- let lastChunkEndOffset = 0;
- for (const {offset, keyLocation} of chunkEnds) {
- result.push(
- {lines: lines.slice(lastChunkEndOffset, offset), keyLocation});
- lastChunkEndOffset = offset;
- }
- return result;
- }
-
- /**
- * Converts `IntralineInfo`s return by the API to `GrLineHighlights` used
- * for rendering.
- *
- * @param {!Array<string>} rows
- * @param {!Array<!Gerrit.IntralineInfo>} intralineInfos
- * @return {!Array<!Object>} (GrDiffLine.Highlight)
- */
- _convertIntralineInfos(rows, intralineInfos) {
- let rowIndex = 0;
- let idx = 0;
- const normalized = [];
- for (const [skipLength, markLength] of intralineInfos) {
- let line = rows[rowIndex] + '\n';
- let j = 0;
- while (j < skipLength) {
- if (idx === line.length) {
- idx = 0;
- line = rows[++rowIndex] + '\n';
- continue;
- }
- idx++;
- j++;
- }
- let lineHighlight = {
- contentIndex: rowIndex,
- startIndex: idx,
- };
-
- j = 0;
- while (line && j < markLength) {
- if (idx === line.length) {
- idx = 0;
- line = rows[++rowIndex] + '\n';
- normalized.push(lineHighlight);
- lineHighlight = {
- contentIndex: rowIndex,
- startIndex: idx,
- };
- continue;
- }
- idx++;
- j++;
- }
- lineHighlight.endIndex = idx;
- normalized.push(lineHighlight);
- }
- return normalized;
- }
-
- /**
- * If a group is an addition or a removal, break it down into smaller groups
- * of that type using the MAX_GROUP_SIZE. If the group is a shared chunk
- * or a delta it is returned as the single element of the result array.
- *
- * @param {!Gerrit.DiffChunk} chunk A raw chunk from a diff response.
- * @return {!Array<!Array<!Object>>}
- */
- _breakdownChunk(chunk) {
- let key = null;
- if (chunk.a && !chunk.b) {
- key = 'a';
- } else if (chunk.b && !chunk.a) {
- key = 'b';
- } else if (chunk.ab) {
- key = 'ab';
- }
-
- if (!key) { return [chunk]; }
-
- return this._breakdown(chunk[key], MAX_GROUP_SIZE)
- .map(subChunkLines => {
- const subChunk = {};
- subChunk[key] = subChunkLines;
- if (chunk.due_to_rebase) {
- subChunk.due_to_rebase = true;
+ let currentBatch = 0;
+ const nextStep = () => {
+ if (this._isScrolling) {
+ this._nextStepHandle = this.async(nextStep, 100);
+ return;
}
- return subChunk;
- });
+ // If we are done, resolve the promise.
+ if (state.chunkIndex >= chunks.length) {
+ resolve();
+ this._nextStepHandle = null;
+ return;
+ }
+
+ // Process the next chunk and incorporate the result.
+ const stateUpdate = this._processNext(state, chunks);
+ for (const group of stateUpdate.groups) {
+ this.push('groups', group);
+ currentBatch += group.lines.length;
+ }
+ state.lineNums.left += stateUpdate.lineDelta.left;
+ state.lineNums.right += stateUpdate.lineDelta.right;
+
+ // Increment the index and recurse.
+ state.chunkIndex = stateUpdate.newChunkIndex;
+ if (currentBatch >= this._asyncThreshold) {
+ currentBatch = 0;
+ this._nextStepHandle = this.async(nextStep, 1);
+ } else {
+ nextStep.call(this);
+ }
+ };
+
+ nextStep.call(this);
+ }));
+ return this._processPromise
+ .finally(() => { this._processPromise = null; });
+ }
+
+ /**
+ * Cancel any jobs that are running.
+ */
+ cancel() {
+ if (this._nextStepHandle != null) {
+ this.cancelAsync(this._nextStepHandle);
+ this._nextStepHandle = null;
}
-
- /**
- * Given an array and a size, return an array of arrays where no inner array
- * is larger than that size, preserving the original order.
- *
- * @param {!Array<T>} array
- * @param {number} size
- * @return {!Array<!Array<T>>}
- * @template T
- */
- _breakdown(array, size) {
- if (!array.length) { return []; }
- if (array.length < size) { return [array]; }
-
- const head = array.slice(0, array.length - size);
- const tail = array.slice(array.length - size);
-
- return this._breakdown(head, size).concat([tail]);
+ if (this._processPromise) {
+ this._processPromise.cancel();
}
}
- customElements.define(GrDiffProcessor.is, GrDiffProcessor);
-})();
+ /**
+ * Process the next uncollapsible chunk, or the next collapsible chunks.
+ *
+ * @param {!Object} state
+ * @param {!Array<!Object>} chunks
+ * @return {{lineDelta: {left: number, right: number}, groups: !Array<!Object>, newChunkIndex: number}}
+ */
+ _processNext(state, chunks) {
+ const firstUncollapsibleChunkIndex =
+ this._firstUncollapsibleChunkIndex(chunks, state.chunkIndex);
+ if (firstUncollapsibleChunkIndex === state.chunkIndex) {
+ const chunk = chunks[state.chunkIndex];
+ return {
+ lineDelta: {
+ left: this._linesLeft(chunk).length,
+ right: this._linesRight(chunk).length,
+ },
+ groups: [this._chunkToGroup(
+ chunk, state.lineNums.left + 1, state.lineNums.right + 1)],
+ newChunkIndex: state.chunkIndex + 1,
+ };
+ }
+
+ return this._processCollapsibleChunks(
+ state, chunks, firstUncollapsibleChunkIndex);
+ }
+
+ _linesLeft(chunk) {
+ return chunk.ab || chunk.a || [];
+ }
+
+ _linesRight(chunk) {
+ return chunk.ab || chunk.b || [];
+ }
+
+ _firstUncollapsibleChunkIndex(chunks, offset) {
+ let chunkIndex = offset;
+ while (chunkIndex < chunks.length &&
+ this._isCollapsibleChunk(chunks[chunkIndex])) {
+ chunkIndex++;
+ }
+ return chunkIndex;
+ }
+
+ _isCollapsibleChunk(chunk) {
+ return (chunk.ab || chunk.common) && !chunk.keyLocation;
+ }
+
+ /**
+ * Process a stretch of collapsible chunks.
+ *
+ * Outputs up to three groups:
+ * 1) Visible context before the hidden common code, unless it's the
+ * very beginning of the file.
+ * 2) Context hidden behind a context bar, unless empty.
+ * 3) Visible context after the hidden common code, unless it's the very
+ * end of the file.
+ *
+ * @param {!Object} state
+ * @param {!Array<Object>} chunks
+ * @param {number} firstUncollapsibleChunkIndex
+ * @return {{lineDelta: {left: number, right: number}, groups: !Array<!Object>, newChunkIndex: number}}
+ */
+ _processCollapsibleChunks(
+ state, chunks, firstUncollapsibleChunkIndex) {
+ const collapsibleChunks = chunks.slice(
+ state.chunkIndex, firstUncollapsibleChunkIndex);
+ const lineCount = collapsibleChunks.reduce(
+ (sum, chunk) => sum + this._commonChunkLength(chunk), 0);
+
+ let groups = this._chunksToGroups(
+ collapsibleChunks,
+ state.lineNums.left + 1,
+ state.lineNums.right + 1);
+
+ if (this.context !== WHOLE_FILE) {
+ const hiddenStart = state.chunkIndex === 0 ? 0 : this.context;
+ const hiddenEnd = lineCount - (
+ firstUncollapsibleChunkIndex === chunks.length ?
+ 0 : this.context);
+ groups = GrDiffGroup.hideInContextControl(
+ groups, hiddenStart, hiddenEnd);
+ }
+
+ return {
+ lineDelta: {
+ left: lineCount,
+ right: lineCount,
+ },
+ groups,
+ newChunkIndex: firstUncollapsibleChunkIndex,
+ };
+ }
+
+ _commonChunkLength(chunk) {
+ console.assert(chunk.ab || chunk.common);
+ console.assert(
+ !chunk.a || (chunk.b && chunk.a.length === chunk.b.length),
+ `common chunk needs same number of a and b lines: `, chunk);
+ return this._linesLeft(chunk).length;
+ }
+
+ /**
+ * @param {!Array<!Object>} chunks
+ * @param {number} offsetLeft
+ * @param {number} offsetRight
+ * @return {!Array<!Object>} (GrDiffGroup)
+ */
+ _chunksToGroups(chunks, offsetLeft, offsetRight) {
+ return chunks.map(chunk => {
+ const group = this._chunkToGroup(chunk, offsetLeft, offsetRight);
+ const chunkLength = this._commonChunkLength(chunk);
+ offsetLeft += chunkLength;
+ offsetRight += chunkLength;
+ return group;
+ });
+ }
+
+ /**
+ * @param {!Object} chunk
+ * @param {number} offsetLeft
+ * @param {number} offsetRight
+ * @return {!Object} (GrDiffGroup)
+ */
+ _chunkToGroup(chunk, offsetLeft, offsetRight) {
+ const type = chunk.ab ? GrDiffGroup.Type.BOTH : GrDiffGroup.Type.DELTA;
+ const lines = this._linesFromChunk(chunk, offsetLeft, offsetRight);
+ const group = new GrDiffGroup(type, lines);
+ group.keyLocation = chunk.keyLocation;
+ group.dueToRebase = chunk.due_to_rebase;
+ group.ignoredWhitespaceOnly = chunk.common;
+ return group;
+ }
+
+ _linesFromChunk(chunk, offsetLeft, offsetRight) {
+ if (chunk.ab) {
+ return chunk.ab.map((row, i) => this._lineFromRow(
+ GrDiffLine.Type.BOTH, offsetLeft, offsetRight, row, i));
+ }
+ let lines = [];
+ if (chunk.a) {
+ // Avoiding a.push(...b) because that causes callstack overflows for
+ // large b, which can occur when large files are added removed.
+ lines = lines.concat(this._linesFromRows(
+ GrDiffLine.Type.REMOVE, chunk.a, offsetLeft,
+ chunk[DiffHighlights.REMOVED]));
+ }
+ if (chunk.b) {
+ // Avoiding a.push(...b) because that causes callstack overflows for
+ // large b, which can occur when large files are added removed.
+ lines = lines.concat(this._linesFromRows(
+ GrDiffLine.Type.ADD, chunk.b, offsetRight,
+ chunk[DiffHighlights.ADDED]));
+ }
+ return lines;
+ }
+
+ /**
+ * @param {string} lineType (GrDiffLine.Type)
+ * @param {!Array<string>} rows
+ * @param {number} offset
+ * @param {?Array<!Gerrit.IntralineInfo>=} opt_intralineInfos
+ * @return {!Array<!Object>} (GrDiffLine)
+ */
+ _linesFromRows(lineType, rows, offset, opt_intralineInfos) {
+ const grDiffHighlights = opt_intralineInfos ?
+ this._convertIntralineInfos(rows, opt_intralineInfos) : undefined;
+ return rows.map((row, i) => this._lineFromRow(
+ lineType, offset, offset, row, i, grDiffHighlights));
+ }
+
+ /**
+ * @param {string} type (GrDiffLine.Type)
+ * @param {number} offsetLeft
+ * @param {number} offsetRight
+ * @param {string} row
+ * @param {number} i
+ * @param {!Array<!Object>=} opt_highlights
+ * @return {!Object} (GrDiffLine)
+ */
+ _lineFromRow(type, offsetLeft, offsetRight, row, i, opt_highlights) {
+ const line = new GrDiffLine(type);
+ line.text = row;
+ if (type !== GrDiffLine.Type.ADD) line.beforeNumber = offsetLeft + i;
+ if (type !== GrDiffLine.Type.REMOVE) line.afterNumber = offsetRight + i;
+ if (opt_highlights) {
+ line.hasIntralineInfo = true;
+ line.highlights = opt_highlights.filter(hl => hl.contentIndex === i);
+ } else {
+ line.hasIntralineInfo = false;
+ }
+ return line;
+ }
+
+ _makeFileComments() {
+ const line = new GrDiffLine(GrDiffLine.Type.BOTH);
+ line.beforeNumber = GrDiffLine.FILE;
+ line.afterNumber = GrDiffLine.FILE;
+ return new GrDiffGroup(GrDiffGroup.Type.BOTH, [line]);
+ }
+
+ /**
+ * Split chunks into smaller chunks of the same kind.
+ *
+ * This is done to prevent doing too much work on the main thread in one
+ * uninterrupted rendering step, which would make the browser unresponsive.
+ *
+ * Note that in the case of unmodified chunks, we only split chunks if the
+ * context is set to file (because otherwise they are split up further down
+ * the processing into the visible and hidden context), and only split it
+ * into 2 chunks, one max sized one and the rest (for reasons that are
+ * unclear to me).
+ *
+ * @param {!Array<!Gerrit.DiffChunk>} chunks Chunks as returned from the server
+ * @return {!Array<!Gerrit.DiffChunk>} Finer grained chunks.
+ */
+ _splitLargeChunks(chunks) {
+ const newChunks = [];
+
+ for (const chunk of chunks) {
+ if (!chunk.ab) {
+ for (const subChunk of this._breakdownChunk(chunk)) {
+ newChunks.push(subChunk);
+ }
+ continue;
+ }
+
+ // If the context is set to "whole file", then break down the shared
+ // chunks so they can be rendered incrementally. Note: this is not
+ // enabled for any other context preference because manipulating the
+ // chunks in this way violates assumptions by the context grouper logic.
+ if (this.context === -1 && chunk.ab.length > MAX_GROUP_SIZE * 2) {
+ // Split large shared chunks in two, where the first is the maximum
+ // group size.
+ newChunks.push({ab: chunk.ab.slice(0, MAX_GROUP_SIZE)});
+ newChunks.push({ab: chunk.ab.slice(MAX_GROUP_SIZE)});
+ } else {
+ newChunks.push(chunk);
+ }
+ }
+ return newChunks;
+ }
+
+ /**
+ * In order to show key locations, such as comments, out of the bounds of
+ * the selected context, treat them as separate chunks within the model so
+ * that the content (and context surrounding it) renders correctly.
+ *
+ * @param {!Array<!Object>} chunks DiffContents as returned from server.
+ * @return {!Array<!Object>} Finer grained DiffContents.
+ */
+ _splitCommonChunksWithKeyLocations(chunks) {
+ const result = [];
+ let leftLineNum = 1;
+ let rightLineNum = 1;
+
+ for (const chunk of chunks) {
+ // If it isn't a common chunk, append it as-is and update line numbers.
+ if (!chunk.ab && !chunk.common) {
+ if (chunk.a) {
+ leftLineNum += chunk.a.length;
+ }
+ if (chunk.b) {
+ rightLineNum += chunk.b.length;
+ }
+ result.push(chunk);
+ continue;
+ }
+
+ if (chunk.common && chunk.a.length != chunk.b.length) {
+ throw new Error(
+ 'DiffContent with common=true must always have equal length');
+ }
+ const numLines = this._commonChunkLength(chunk);
+ const chunkEnds = this._findChunkEndsAtKeyLocations(
+ numLines, leftLineNum, rightLineNum);
+ leftLineNum += numLines;
+ rightLineNum += numLines;
+
+ if (chunk.ab) {
+ result.push(...this._splitAtChunkEnds(chunk.ab, chunkEnds)
+ .map(({lines, keyLocation}) =>
+ Object.assign({}, chunk, {ab: lines, keyLocation})));
+ } else if (chunk.common) {
+ const aChunks = this._splitAtChunkEnds(chunk.a, chunkEnds);
+ const bChunks = this._splitAtChunkEnds(chunk.b, chunkEnds);
+ result.push(...aChunks.map(({lines, keyLocation}, i) =>
+ Object.assign(
+ {}, chunk, {a: lines, b: bChunks[i].lines, keyLocation})));
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * @return {!Array<{offset: number, keyLocation: boolean}>} Offsets of the
+ * new chunk ends, including whether it's a key location.
+ */
+ _findChunkEndsAtKeyLocations(numLines, leftOffset, rightOffset) {
+ const result = [];
+ let lastChunkEnd = 0;
+ for (let i=0; i<numLines; i++) {
+ // If this line should not be collapsed.
+ if (this.keyLocations[DiffSide.LEFT][leftOffset + i] ||
+ this.keyLocations[DiffSide.RIGHT][rightOffset + i]) {
+ // If any lines have been accumulated into the chunk leading up to
+ // this non-collapse line, then add them as a chunk and start a new
+ // one.
+ if (i > lastChunkEnd) {
+ result.push({offset: i, keyLocation: false});
+ lastChunkEnd = i;
+ }
+
+ // Add the non-collapse line as its own chunk.
+ result.push({offset: i + 1, keyLocation: true});
+ }
+ }
+
+ if (numLines > lastChunkEnd) {
+ result.push({offset: numLines, keyLocation: false});
+ }
+
+ return result;
+ }
+
+ _splitAtChunkEnds(lines, chunkEnds) {
+ const result = [];
+ let lastChunkEndOffset = 0;
+ for (const {offset, keyLocation} of chunkEnds) {
+ result.push(
+ {lines: lines.slice(lastChunkEndOffset, offset), keyLocation});
+ lastChunkEndOffset = offset;
+ }
+ return result;
+ }
+
+ /**
+ * Converts `IntralineInfo`s return by the API to `GrLineHighlights` used
+ * for rendering.
+ *
+ * @param {!Array<string>} rows
+ * @param {!Array<!Gerrit.IntralineInfo>} intralineInfos
+ * @return {!Array<!Object>} (GrDiffLine.Highlight)
+ */
+ _convertIntralineInfos(rows, intralineInfos) {
+ let rowIndex = 0;
+ let idx = 0;
+ const normalized = [];
+ for (const [skipLength, markLength] of intralineInfos) {
+ let line = rows[rowIndex] + '\n';
+ let j = 0;
+ while (j < skipLength) {
+ if (idx === line.length) {
+ idx = 0;
+ line = rows[++rowIndex] + '\n';
+ continue;
+ }
+ idx++;
+ j++;
+ }
+ let lineHighlight = {
+ contentIndex: rowIndex,
+ startIndex: idx,
+ };
+
+ j = 0;
+ while (line && j < markLength) {
+ if (idx === line.length) {
+ idx = 0;
+ line = rows[++rowIndex] + '\n';
+ normalized.push(lineHighlight);
+ lineHighlight = {
+ contentIndex: rowIndex,
+ startIndex: idx,
+ };
+ continue;
+ }
+ idx++;
+ j++;
+ }
+ lineHighlight.endIndex = idx;
+ normalized.push(lineHighlight);
+ }
+ return normalized;
+ }
+
+ /**
+ * If a group is an addition or a removal, break it down into smaller groups
+ * of that type using the MAX_GROUP_SIZE. If the group is a shared chunk
+ * or a delta it is returned as the single element of the result array.
+ *
+ * @param {!Gerrit.DiffChunk} chunk A raw chunk from a diff response.
+ * @return {!Array<!Array<!Object>>}
+ */
+ _breakdownChunk(chunk) {
+ let key = null;
+ if (chunk.a && !chunk.b) {
+ key = 'a';
+ } else if (chunk.b && !chunk.a) {
+ key = 'b';
+ } else if (chunk.ab) {
+ key = 'ab';
+ }
+
+ if (!key) { return [chunk]; }
+
+ return this._breakdown(chunk[key], MAX_GROUP_SIZE)
+ .map(subChunkLines => {
+ const subChunk = {};
+ subChunk[key] = subChunkLines;
+ if (chunk.due_to_rebase) {
+ subChunk.due_to_rebase = true;
+ }
+ return subChunk;
+ });
+ }
+
+ /**
+ * Given an array and a size, return an array of arrays where no inner array
+ * is larger than that size, preserving the original order.
+ *
+ * @param {!Array<T>} array
+ * @param {number} size
+ * @return {!Array<!Array<T>>}
+ * @template T
+ */
+ _breakdown(array, size) {
+ if (!array.length) { return []; }
+ if (array.length < size) { return [array]; }
+
+ const head = array.slice(0, array.length - size);
+ const tail = array.slice(array.length - size);
+
+ return this._breakdown(head, size).concat([tail]);
+ }
+}
+
+customElements.define(GrDiffProcessor.is, GrDiffProcessor);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
index 9b3f3b2..63209c1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-processor test</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-diff-processor.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,902 +30,903 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff-processor tests', async () => {
- await readyToTest();
- const WHOLE_FILE = -1;
- const loremIpsum =
- 'Lorem ipsum dolor sit amet, ei nonumes vituperata ius. ' +
- 'Duo animal omnesque fabellas et. Id has phaedrum dignissim ' +
- 'deterruisset, pro ei petentium comprehensam, ut vis solum dicta. ' +
- 'Eos cu aliquam labores qualisque, usu postea inermis te, et solum ' +
- 'fugit assum per.';
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-diff-processor.js';
+suite('gr-diff-processor tests', () => {
+ const WHOLE_FILE = -1;
+ const loremIpsum =
+ 'Lorem ipsum dolor sit amet, ei nonumes vituperata ius. ' +
+ 'Duo animal omnesque fabellas et. Id has phaedrum dignissim ' +
+ 'deterruisset, pro ei petentium comprehensam, ut vis solum dicta. ' +
+ 'Eos cu aliquam labores qualisque, usu postea inermis te, et solum ' +
+ 'fugit assum per.';
- let element;
- let sandbox;
+ let element;
+ let sandbox;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('not logged in', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+
+ element.context = 4;
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('not logged in', () => {
- setup(() => {
- element = fixture('basic');
-
- element.context = 4;
- });
-
- test('process loaded content', () => {
- const content = [
- {
- ab: [
- '<!DOCTYPE html>',
- '<meta charset="utf-8">',
- ],
- },
- {
- a: [
- ' Welcome ',
- ' to the wooorld of tomorrow!',
- ],
- b: [
- ' Hello, world!',
- ],
- },
- {
- ab: [
- 'Leela: This is the only place the ship can’t hear us, so ',
- 'everyone pretend to shower.',
- 'Fry: Same as every day. Got it.',
- ],
- },
- ];
-
- return element.process(content).then(() => {
- const groups = element.groups;
-
- assert.equal(groups.length, 4);
-
- let group = groups[0];
- assert.equal(group.type, GrDiffGroup.Type.BOTH);
- assert.equal(group.lines.length, 1);
- assert.equal(group.lines[0].text, '');
- assert.equal(group.lines[0].beforeNumber, GrDiffLine.FILE);
- assert.equal(group.lines[0].afterNumber, GrDiffLine.FILE);
-
- group = groups[1];
- assert.equal(group.type, GrDiffGroup.Type.BOTH);
- assert.equal(group.lines.length, 2);
- assert.equal(group.lines.length, 2);
-
- function beforeNumberFn(l) { return l.beforeNumber; }
- function afterNumberFn(l) { return l.afterNumber; }
- function textFn(l) { return l.text; }
-
- assert.deepEqual(group.lines.map(beforeNumberFn), [1, 2]);
- assert.deepEqual(group.lines.map(afterNumberFn), [1, 2]);
- assert.deepEqual(group.lines.map(textFn), [
+ test('process loaded content', () => {
+ const content = [
+ {
+ ab: [
'<!DOCTYPE html>',
'<meta charset="utf-8">',
- ]);
-
- group = groups[2];
- assert.equal(group.type, GrDiffGroup.Type.DELTA);
- assert.equal(group.lines.length, 3);
- assert.equal(group.adds.length, 1);
- assert.equal(group.removes.length, 2);
- assert.deepEqual(group.removes.map(beforeNumberFn), [3, 4]);
- assert.deepEqual(group.adds.map(afterNumberFn), [3]);
- assert.deepEqual(group.removes.map(textFn), [
+ ],
+ },
+ {
+ a: [
' Welcome ',
' to the wooorld of tomorrow!',
- ]);
- assert.deepEqual(group.adds.map(textFn), [
+ ],
+ b: [
' Hello, world!',
- ]);
-
- group = groups[3];
- assert.equal(group.type, GrDiffGroup.Type.BOTH);
- assert.equal(group.lines.length, 3);
- assert.deepEqual(group.lines.map(beforeNumberFn), [5, 6, 7]);
- assert.deepEqual(group.lines.map(afterNumberFn), [4, 5, 6]);
- assert.deepEqual(group.lines.map(textFn), [
+ ],
+ },
+ {
+ ab: [
'Leela: This is the only place the ship can’t hear us, so ',
'everyone pretend to shower.',
'Fry: Same as every day. Got it.',
- ]);
- });
- });
+ ],
+ },
+ ];
- test('first group is for file', () => {
+ return element.process(content).then(() => {
+ const groups = element.groups;
+
+ assert.equal(groups.length, 4);
+
+ let group = groups[0];
+ assert.equal(group.type, GrDiffGroup.Type.BOTH);
+ assert.equal(group.lines.length, 1);
+ assert.equal(group.lines[0].text, '');
+ assert.equal(group.lines[0].beforeNumber, GrDiffLine.FILE);
+ assert.equal(group.lines[0].afterNumber, GrDiffLine.FILE);
+
+ group = groups[1];
+ assert.equal(group.type, GrDiffGroup.Type.BOTH);
+ assert.equal(group.lines.length, 2);
+ assert.equal(group.lines.length, 2);
+
+ function beforeNumberFn(l) { return l.beforeNumber; }
+ function afterNumberFn(l) { return l.afterNumber; }
+ function textFn(l) { return l.text; }
+
+ assert.deepEqual(group.lines.map(beforeNumberFn), [1, 2]);
+ assert.deepEqual(group.lines.map(afterNumberFn), [1, 2]);
+ assert.deepEqual(group.lines.map(textFn), [
+ '<!DOCTYPE html>',
+ '<meta charset="utf-8">',
+ ]);
+
+ group = groups[2];
+ assert.equal(group.type, GrDiffGroup.Type.DELTA);
+ assert.equal(group.lines.length, 3);
+ assert.equal(group.adds.length, 1);
+ assert.equal(group.removes.length, 2);
+ assert.deepEqual(group.removes.map(beforeNumberFn), [3, 4]);
+ assert.deepEqual(group.adds.map(afterNumberFn), [3]);
+ assert.deepEqual(group.removes.map(textFn), [
+ ' Welcome ',
+ ' to the wooorld of tomorrow!',
+ ]);
+ assert.deepEqual(group.adds.map(textFn), [
+ ' Hello, world!',
+ ]);
+
+ group = groups[3];
+ assert.equal(group.type, GrDiffGroup.Type.BOTH);
+ assert.equal(group.lines.length, 3);
+ assert.deepEqual(group.lines.map(beforeNumberFn), [5, 6, 7]);
+ assert.deepEqual(group.lines.map(afterNumberFn), [4, 5, 6]);
+ assert.deepEqual(group.lines.map(textFn), [
+ 'Leela: This is the only place the ship can’t hear us, so ',
+ 'everyone pretend to shower.',
+ 'Fry: Same as every day. Got it.',
+ ]);
+ });
+ });
+
+ test('first group is for file', () => {
+ const content = [
+ {b: ['foo']},
+ ];
+
+ return element.process(content).then(() => {
+ const groups = element.groups;
+
+ assert.equal(groups[0].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[0].lines.length, 1);
+ assert.equal(groups[0].lines[0].text, '');
+ assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE);
+ assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
+ });
+ });
+
+ suite('context groups', () => {
+ test('at the beginning, larger than context', () => {
+ element.context = 10;
const content = [
- {b: ['foo']},
+ {ab: new Array(100)
+ .fill('all work and no play make jack a dull boy')},
+ {a: ['all work and no play make andybons a dull boy']},
];
return element.process(content).then(() => {
const groups = element.groups;
- assert.equal(groups[0].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[0].lines.length, 1);
- assert.equal(groups[0].lines[0].text, '');
- assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE);
- assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
+ // group[0] is the file group
+
+ assert.equal(groups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.instanceOf(groups[1].lines[0].contextGroups[0], GrDiffGroup);
+ assert.equal(groups[1].lines[0].contextGroups[0].lines.length, 90);
+ for (const l of groups[1].lines[0].contextGroups[0].lines) {
+ assert.equal(l.text, 'all work and no play make jack a dull boy');
+ }
+
+ assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[2].lines.length, 10);
+ for (const l of groups[2].lines) {
+ assert.equal(l.text, 'all work and no play make jack a dull boy');
+ }
});
});
- suite('context groups', () => {
- test('at the beginning, larger than context', () => {
- element.context = 10;
- const content = [
- {ab: new Array(100)
- .fill('all work and no play make jack a dull boy')},
- {a: ['all work and no play make andybons a dull boy']},
- ];
-
- return element.process(content).then(() => {
- const groups = element.groups;
-
- // group[0] is the file group
-
- assert.equal(groups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
- assert.instanceOf(groups[1].lines[0].contextGroups[0], GrDiffGroup);
- assert.equal(groups[1].lines[0].contextGroups[0].lines.length, 90);
- for (const l of groups[1].lines[0].contextGroups[0].lines) {
- assert.equal(l.text, 'all work and no play make jack a dull boy');
- }
-
- assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[2].lines.length, 10);
- for (const l of groups[2].lines) {
- assert.equal(l.text, 'all work and no play make jack a dull boy');
- }
- });
- });
-
- test('at the beginning, smaller than context', () => {
- element.context = 10;
- const content = [
- {ab: new Array(5)
- .fill('all work and no play make jack a dull boy')},
- {a: ['all work and no play make andybons a dull boy']},
- ];
-
- return element.process(content).then(() => {
- const groups = element.groups;
-
- // group[0] is the file group
-
- assert.equal(groups[1].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[1].lines.length, 5);
- for (const l of groups[1].lines) {
- assert.equal(l.text, 'all work and no play make jack a dull boy');
- }
- });
- });
-
- test('at the end, larger than context', () => {
- element.context = 10;
- const content = [
- {a: ['all work and no play make andybons a dull boy']},
- {ab: new Array(100)
- .fill('all work and no play make jill a dull girl')},
- ];
-
- return element.process(content).then(() => {
- const groups = element.groups;
-
- // group[0] is the file group
- // group[1] is the "a" group
-
- assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[2].lines.length, 10);
- for (const l of groups[2].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
-
- assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
- assert.instanceOf(groups[3].lines[0].contextGroups[0], GrDiffGroup);
- assert.equal(groups[3].lines[0].contextGroups[0].lines.length, 90);
- for (const l of groups[3].lines[0].contextGroups[0].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
- });
- });
-
- test('at the end, smaller than context', () => {
- element.context = 10;
- const content = [
- {a: ['all work and no play make andybons a dull boy']},
- {ab: new Array(5)
- .fill('all work and no play make jill a dull girl')},
- ];
-
- return element.process(content).then(() => {
- const groups = element.groups;
-
- // group[0] is the file group
- // group[1] is the "a" group
-
- assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[2].lines.length, 5);
- for (const l of groups[2].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
- });
- });
-
- test('for interleaved ab and common: true chunks', () => {
- element.context = 10;
- const content = [
- {a: ['all work and no play make andybons a dull boy']},
- {ab: new Array(3)
- .fill('all work and no play make jill a dull girl')},
- {
- a: new Array(3).fill(
- 'all work and no play make jill a dull girl'),
- b: new Array(3).fill(
- ' all work and no play make jill a dull girl'),
- common: true,
- },
- {ab: new Array(3)
- .fill('all work and no play make jill a dull girl')},
- {
- a: new Array(3).fill(
- 'all work and no play make jill a dull girl'),
- b: new Array(3).fill(
- ' all work and no play make jill a dull girl'),
- common: true,
- },
- {ab: new Array(3)
- .fill('all work and no play make jill a dull girl')},
- ];
-
- return element.process(content).then(() => {
- const groups = element.groups;
-
- // group[0] is the file group
- // group[1] is the "a" group
-
- // The first three interleaved chunks are completely shown because
- // they are part of the context (3 * 3 <= 10)
-
- assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[2].lines.length, 3);
- for (const l of groups[2].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
-
- assert.equal(groups[3].type, GrDiffGroup.Type.DELTA);
- assert.equal(groups[3].lines.length, 6);
- assert.equal(groups[3].adds.length, 3);
- assert.equal(groups[3].removes.length, 3);
- for (const l of groups[3].removes) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
- for (const l of groups[3].adds) {
- assert.equal(
- l.text, ' all work and no play make jill a dull girl');
- }
-
- assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[4].lines.length, 3);
- for (const l of groups[4].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
-
- // The next chunk is partially shown, so it results in two groups
-
- assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
- assert.equal(groups[5].lines.length, 2);
- assert.equal(groups[5].adds.length, 1);
- assert.equal(groups[5].removes.length, 1);
- for (const l of groups[5].removes) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
- for (const l of groups[5].adds) {
- assert.equal(
- l.text, ' all work and no play make jill a dull girl');
- }
-
- assert.equal(groups[6].type, GrDiffGroup.Type.CONTEXT_CONTROL);
- assert.equal(groups[6].lines[0].contextGroups.length, 2);
-
- assert.equal(groups[6].lines[0].contextGroups[0].lines.length, 4);
- assert.equal(groups[6].lines[0].contextGroups[0].removes.length, 2);
- assert.equal(groups[6].lines[0].contextGroups[0].adds.length, 2);
- for (const l of groups[6].lines[0].contextGroups[0].removes) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
- for (const l of groups[6].lines[0].contextGroups[0].adds) {
- assert.equal(
- l.text, ' all work and no play make jill a dull girl');
- }
-
- // The final chunk is completely hidden
- assert.equal(
- groups[6].lines[0].contextGroups[1].type,
- GrDiffGroup.Type.BOTH);
- assert.equal(groups[6].lines[0].contextGroups[1].lines.length, 3);
- for (const l of groups[6].lines[0].contextGroups[1].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
- });
- });
-
- test('in the middle, larger than context', () => {
- element.context = 10;
- const content = [
- {a: ['all work and no play make andybons a dull boy']},
- {ab: new Array(100)
- .fill('all work and no play make jill a dull girl')},
- {a: ['all work and no play make andybons a dull boy']},
- ];
-
- return element.process(content).then(() => {
- const groups = element.groups;
-
- // group[0] is the file group
- // group[1] is the "a" group
-
- assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[2].lines.length, 10);
- for (const l of groups[2].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
-
- assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
- assert.instanceOf(groups[3].lines[0].contextGroups[0], GrDiffGroup);
- assert.equal(groups[3].lines[0].contextGroups[0].lines.length, 80);
- for (const l of groups[3].lines[0].contextGroups[0].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
-
- assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[4].lines.length, 10);
- for (const l of groups[4].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
- });
- });
-
- test('in the middle, smaller than context', () => {
- element.context = 10;
- const content = [
- {a: ['all work and no play make andybons a dull boy']},
- {ab: new Array(5)
- .fill('all work and no play make jill a dull girl')},
- {a: ['all work and no play make andybons a dull boy']},
- ];
-
- return element.process(content).then(() => {
- const groups = element.groups;
-
- // group[0] is the file group
- // group[1] is the "a" group
-
- assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[2].lines.length, 5);
- for (const l of groups[2].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
- });
- });
- });
-
- test('break up common diff chunks', () => {
- element.keyLocations = {
- left: {1: true},
- right: {10: true},
- };
-
+ test('at the beginning, smaller than context', () => {
+ element.context = 10;
const content = [
- {
- ab: [
- 'Copyright (C) 2015 The Android Open Source Project',
- '',
- 'Licensed under the Apache License, Version 2.0 (the "License");',
- 'you may not use this file except in compliance with the ' +
- 'License.',
- 'You may obtain a copy of the License at',
- '',
- 'http://www.apache.org/licenses/LICENSE-2.0',
- '',
- 'Unless required by applicable law or agreed to in writing, ',
- 'software distributed under the License is distributed on an ',
- '"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, ',
- 'either express or implied. See the License for the specific ',
- 'language governing permissions and limitations under the ' +
- 'License.',
- ],
- },
+ {ab: new Array(5)
+ .fill('all work and no play make jack a dull boy')},
+ {a: ['all work and no play make andybons a dull boy']},
];
- const result =
- element._splitCommonChunksWithKeyLocations(content);
- assert.deepEqual(result, [
- {
- ab: ['Copyright (C) 2015 The Android Open Source Project'],
- keyLocation: true,
- },
- {
- ab: [
- '',
- '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, ',
- ],
- keyLocation: false,
- },
- {
- ab: [
- 'software distributed under the License is distributed on an '],
- keyLocation: true,
- },
- {
- ab: [
- '"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.',
- ],
- keyLocation: false,
- },
- ]);
+
+ return element.process(content).then(() => {
+ const groups = element.groups;
+
+ // group[0] is the file group
+
+ assert.equal(groups[1].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[1].lines.length, 5);
+ for (const l of groups[1].lines) {
+ assert.equal(l.text, 'all work and no play make jack a dull boy');
+ }
+ });
});
- test('breaks down shared chunks w/ whole-file', () => {
- const size = 120 * 2 + 5;
- const content = [{
- ab: _.times(size, () => `${Math.random()}`),
- }];
- element.context = -1;
- const result = element._splitLargeChunks(content);
- assert.equal(result.length, 2);
- assert.deepEqual(result[0].ab, content[0].ab.slice(0, 120));
- assert.deepEqual(result[1].ab, content[0].ab.slice(120));
+ test('at the end, larger than context', () => {
+ element.context = 10;
+ const content = [
+ {a: ['all work and no play make andybons a dull boy']},
+ {ab: new Array(100)
+ .fill('all work and no play make jill a dull girl')},
+ ];
+
+ return element.process(content).then(() => {
+ const groups = element.groups;
+
+ // group[0] is the file group
+ // group[1] is the "a" group
+
+ assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[2].lines.length, 10);
+ for (const l of groups[2].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.instanceOf(groups[3].lines[0].contextGroups[0], GrDiffGroup);
+ assert.equal(groups[3].lines[0].contextGroups[0].lines.length, 90);
+ for (const l of groups[3].lines[0].contextGroups[0].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+ });
});
- test('does not break-down common chunks w/ context', () => {
- const content = [{
- ab: _.times(75, () => `${Math.random()}`),
- }];
- element.context = 4;
- const result =
- element._splitCommonChunksWithKeyLocations(content);
- assert.equal(result.length, 1);
- assert.deepEqual(result[0].ab, content[0].ab);
- assert.isFalse(result[0].keyLocation);
+ test('at the end, smaller than context', () => {
+ element.context = 10;
+ const content = [
+ {a: ['all work and no play make andybons a dull boy']},
+ {ab: new Array(5)
+ .fill('all work and no play make jill a dull girl')},
+ ];
+
+ return element.process(content).then(() => {
+ const groups = element.groups;
+
+ // group[0] is the file group
+ // group[1] is the "a" group
+
+ assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[2].lines.length, 5);
+ for (const l of groups[2].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+ });
});
- test('intraline normalization', () => {
- // The content and highlights are in the format returned by the Gerrit
- // REST API.
- let content = [
- ' <section class="summary">',
- ' <gr-linked-text content="' +
- '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>',
- ' </section>',
- ];
- let highlights = [
- [31, 34], [42, 26],
+ test('for interleaved ab and common: true chunks', () => {
+ element.context = 10;
+ const content = [
+ {a: ['all work and no play make andybons a dull boy']},
+ {ab: new Array(3)
+ .fill('all work and no play make jill a dull girl')},
+ {
+ a: new Array(3).fill(
+ 'all work and no play make jill a dull girl'),
+ b: new Array(3).fill(
+ ' all work and no play make jill a dull girl'),
+ common: true,
+ },
+ {ab: new Array(3)
+ .fill('all work and no play make jill a dull girl')},
+ {
+ a: new Array(3).fill(
+ 'all work and no play make jill a dull girl'),
+ b: new Array(3).fill(
+ ' all work and no play make jill a dull girl'),
+ common: true,
+ },
+ {ab: new Array(3)
+ .fill('all work and no play make jill a dull girl')},
];
- let results = element._convertIntralineInfos(content,
- highlights);
- assert.deepEqual(results, [
- {
- contentIndex: 0,
- startIndex: 31,
- },
- {
- contentIndex: 1,
- startIndex: 0,
- endIndex: 33,
- },
- {
- contentIndex: 1,
- startIndex: 75,
- },
- {
- contentIndex: 2,
- startIndex: 0,
- endIndex: 6,
- },
- ]);
+ return element.process(content).then(() => {
+ const groups = element.groups;
- content = [
- ' this._path = value.path;',
- '',
- ' // When navigating away from the page, there is a ' +
- 'possibility that the',
- ' // patch number is no longer a part of the URL ' +
- '(say when navigating to',
- ' // the top-level change info view) and therefore ' +
- 'undefined in `params`.',
- ' if (!this._patchRange.patchNum) {',
- ];
- highlights = [
- [14, 17],
- [11, 70],
- [12, 67],
- [12, 67],
- [14, 29],
- ];
- results = element._convertIntralineInfos(content, highlights);
- assert.deepEqual(results, [
- {
- contentIndex: 0,
- startIndex: 14,
- endIndex: 31,
- },
- {
- contentIndex: 2,
- startIndex: 8,
- endIndex: 78,
- },
- {
- contentIndex: 3,
- startIndex: 11,
- endIndex: 78,
- },
- {
- contentIndex: 4,
- startIndex: 11,
- endIndex: 78,
- },
- {
- contentIndex: 5,
- startIndex: 12,
- endIndex: 41,
- },
- ]);
+ // group[0] is the file group
+ // group[1] is the "a" group
+
+ // The first three interleaved chunks are completely shown because
+ // they are part of the context (3 * 3 <= 10)
+
+ assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[2].lines.length, 3);
+ for (const l of groups[2].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[3].type, GrDiffGroup.Type.DELTA);
+ assert.equal(groups[3].lines.length, 6);
+ assert.equal(groups[3].adds.length, 3);
+ assert.equal(groups[3].removes.length, 3);
+ for (const l of groups[3].removes) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+ for (const l of groups[3].adds) {
+ assert.equal(
+ l.text, ' all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[4].lines.length, 3);
+ for (const l of groups[4].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+
+ // The next chunk is partially shown, so it results in two groups
+
+ assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
+ assert.equal(groups[5].lines.length, 2);
+ assert.equal(groups[5].adds.length, 1);
+ assert.equal(groups[5].removes.length, 1);
+ for (const l of groups[5].removes) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+ for (const l of groups[5].adds) {
+ assert.equal(
+ l.text, ' all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[6].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.equal(groups[6].lines[0].contextGroups.length, 2);
+
+ assert.equal(groups[6].lines[0].contextGroups[0].lines.length, 4);
+ assert.equal(groups[6].lines[0].contextGroups[0].removes.length, 2);
+ assert.equal(groups[6].lines[0].contextGroups[0].adds.length, 2);
+ for (const l of groups[6].lines[0].contextGroups[0].removes) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+ for (const l of groups[6].lines[0].contextGroups[0].adds) {
+ assert.equal(
+ l.text, ' all work and no play make jill a dull girl');
+ }
+
+ // The final chunk is completely hidden
+ assert.equal(
+ groups[6].lines[0].contextGroups[1].type,
+ GrDiffGroup.Type.BOTH);
+ assert.equal(groups[6].lines[0].contextGroups[1].lines.length, 3);
+ for (const l of groups[6].lines[0].contextGroups[1].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+ });
});
- test('scrolling pauses rendering', () => {
- const contentRow = {
+ test('in the middle, larger than context', () => {
+ element.context = 10;
+ const content = [
+ {a: ['all work and no play make andybons a dull boy']},
+ {ab: new Array(100)
+ .fill('all work and no play make jill a dull girl')},
+ {a: ['all work and no play make andybons a dull boy']},
+ ];
+
+ return element.process(content).then(() => {
+ const groups = element.groups;
+
+ // group[0] is the file group
+ // group[1] is the "a" group
+
+ assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[2].lines.length, 10);
+ for (const l of groups[2].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.instanceOf(groups[3].lines[0].contextGroups[0], GrDiffGroup);
+ assert.equal(groups[3].lines[0].contextGroups[0].lines.length, 80);
+ for (const l of groups[3].lines[0].contextGroups[0].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[4].lines.length, 10);
+ for (const l of groups[4].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+ });
+ });
+
+ test('in the middle, smaller than context', () => {
+ element.context = 10;
+ const content = [
+ {a: ['all work and no play make andybons a dull boy']},
+ {ab: new Array(5)
+ .fill('all work and no play make jill a dull girl')},
+ {a: ['all work and no play make andybons a dull boy']},
+ ];
+
+ return element.process(content).then(() => {
+ const groups = element.groups;
+
+ // group[0] is the file group
+ // group[1] is the "a" group
+
+ assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[2].lines.length, 5);
+ for (const l of groups[2].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+ });
+ });
+ });
+
+ test('break up common diff chunks', () => {
+ element.keyLocations = {
+ left: {1: true},
+ right: {10: true},
+ };
+
+ const content = [
+ {
ab: [
- '<!DOCTYPE html>',
- '<meta charset="utf-8">',
+ 'Copyright (C) 2015 The Android Open Source Project',
+ '',
+ 'Licensed under the Apache License, Version 2.0 (the "License");',
+ 'you may not use this file except in compliance with the ' +
+ 'License.',
+ 'You may obtain a copy of the License at',
+ '',
+ 'http://www.apache.org/licenses/LICENSE-2.0',
+ '',
+ 'Unless required by applicable law or agreed to in writing, ',
+ 'software distributed under the License is distributed on an ',
+ '"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, ',
+ 'either express or implied. See the License for the specific ',
+ 'language governing permissions and limitations under the ' +
+ 'License.',
],
- };
- const content = _.times(200, _.constant(contentRow));
- sandbox.stub(element, 'async');
- element._isScrolling = true;
- element.process(content);
- // Just the files group - no more processing during scrolling.
- assert.equal(element.groups.length, 1);
-
- element._isScrolling = false;
- element.process(content);
- // More groups have been processed. How many does not matter here.
- assert.isAtLeast(element.groups.length, 2);
- });
-
- test('image diffs', () => {
- const contentRow = {
+ },
+ ];
+ const result =
+ element._splitCommonChunksWithKeyLocations(content);
+ assert.deepEqual(result, [
+ {
+ ab: ['Copyright (C) 2015 The Android Open Source Project'],
+ keyLocation: true,
+ },
+ {
ab: [
- '<!DOCTYPE html>',
- '<meta charset="utf-8">',
+ '',
+ '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, ',
],
- };
- const content = _.times(200, _.constant(contentRow));
- sandbox.stub(element, 'async');
- element.process(content, true);
- assert.equal(element.groups.length, 1);
+ keyLocation: false,
+ },
+ {
+ ab: [
+ 'software distributed under the License is distributed on an '],
+ keyLocation: true,
+ },
+ {
+ ab: [
+ '"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.',
+ ],
+ keyLocation: false,
+ },
+ ]);
+ });
- // Image diffs don't process content, just the 'FILE' line.
- assert.equal(element.groups[0].lines.length, 1);
+ test('breaks down shared chunks w/ whole-file', () => {
+ const size = 120 * 2 + 5;
+ const content = [{
+ ab: _.times(size, () => `${Math.random()}`),
+ }];
+ element.context = -1;
+ const result = element._splitLargeChunks(content);
+ assert.equal(result.length, 2);
+ assert.deepEqual(result[0].ab, content[0].ab.slice(0, 120));
+ assert.deepEqual(result[1].ab, content[0].ab.slice(120));
+ });
+
+ test('does not break-down common chunks w/ context', () => {
+ const content = [{
+ ab: _.times(75, () => `${Math.random()}`),
+ }];
+ element.context = 4;
+ const result =
+ element._splitCommonChunksWithKeyLocations(content);
+ assert.equal(result.length, 1);
+ assert.deepEqual(result[0].ab, content[0].ab);
+ assert.isFalse(result[0].keyLocation);
+ });
+
+ test('intraline normalization', () => {
+ // The content and highlights are in the format returned by the Gerrit
+ // REST API.
+ let content = [
+ ' <section class="summary">',
+ ' <gr-linked-text content="' +
+ '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>',
+ ' </section>',
+ ];
+ let highlights = [
+ [31, 34], [42, 26],
+ ];
+
+ let results = element._convertIntralineInfos(content,
+ highlights);
+ assert.deepEqual(results, [
+ {
+ contentIndex: 0,
+ startIndex: 31,
+ },
+ {
+ contentIndex: 1,
+ startIndex: 0,
+ endIndex: 33,
+ },
+ {
+ contentIndex: 1,
+ startIndex: 75,
+ },
+ {
+ contentIndex: 2,
+ startIndex: 0,
+ endIndex: 6,
+ },
+ ]);
+
+ content = [
+ ' this._path = value.path;',
+ '',
+ ' // When navigating away from the page, there is a ' +
+ 'possibility that the',
+ ' // patch number is no longer a part of the URL ' +
+ '(say when navigating to',
+ ' // the top-level change info view) and therefore ' +
+ 'undefined in `params`.',
+ ' if (!this._patchRange.patchNum) {',
+ ];
+ highlights = [
+ [14, 17],
+ [11, 70],
+ [12, 67],
+ [12, 67],
+ [14, 29],
+ ];
+ results = element._convertIntralineInfos(content, highlights);
+ assert.deepEqual(results, [
+ {
+ contentIndex: 0,
+ startIndex: 14,
+ endIndex: 31,
+ },
+ {
+ contentIndex: 2,
+ startIndex: 8,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 3,
+ startIndex: 11,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 4,
+ startIndex: 11,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 5,
+ startIndex: 12,
+ endIndex: 41,
+ },
+ ]);
+ });
+
+ test('scrolling pauses rendering', () => {
+ const contentRow = {
+ ab: [
+ '<!DOCTYPE html>',
+ '<meta charset="utf-8">',
+ ],
+ };
+ const content = _.times(200, _.constant(contentRow));
+ sandbox.stub(element, 'async');
+ element._isScrolling = true;
+ element.process(content);
+ // Just the files group - no more processing during scrolling.
+ assert.equal(element.groups.length, 1);
+
+ element._isScrolling = false;
+ element.process(content);
+ // More groups have been processed. How many does not matter here.
+ assert.isAtLeast(element.groups.length, 2);
+ });
+
+ test('image diffs', () => {
+ const contentRow = {
+ ab: [
+ '<!DOCTYPE html>',
+ '<meta charset="utf-8">',
+ ],
+ };
+ const content = _.times(200, _.constant(contentRow));
+ sandbox.stub(element, 'async');
+ element.process(content, true);
+ assert.equal(element.groups.length, 1);
+
+ // Image diffs don't process content, just the 'FILE' line.
+ assert.equal(element.groups[0].lines.length, 1);
+ });
+
+ suite('_processNext', () => {
+ let rows;
+
+ setup(() => {
+ rows = loremIpsum.split(' ');
});
- suite('_processNext', () => {
- let rows;
+ test('WHOLE_FILE', () => {
+ element.context = WHOLE_FILE;
+ const state = {
+ lineNums: {left: 10, right: 100},
+ chunkIndex: 1,
+ };
+ const chunks = [
+ {a: ['foo']},
+ {ab: rows},
+ {a: ['bar']},
+ ];
+ const result = element._processNext(state, chunks);
+
+ // Results in one, uncollapsed group with all rows.
+ assert.equal(result.groups.length, 1);
+ assert.equal(result.groups[0].type, GrDiffGroup.Type.BOTH);
+ assert.equal(result.groups[0].lines.length, rows.length);
+
+ // Line numbers are set correctly.
+ assert.equal(
+ result.groups[0].lines[0].beforeNumber,
+ state.lineNums.left + 1);
+ assert.equal(
+ result.groups[0].lines[0].afterNumber,
+ state.lineNums.right + 1);
+
+ assert.equal(result.groups[0].lines[rows.length - 1].beforeNumber,
+ state.lineNums.left + rows.length);
+ assert.equal(result.groups[0].lines[rows.length - 1].afterNumber,
+ state.lineNums.right + rows.length);
+ });
+
+ test('with context', () => {
+ element.context = 10;
+ const state = {
+ lineNums: {left: 10, right: 100},
+ chunkIndex: 1,
+ };
+ const chunks = [
+ {a: ['foo']},
+ {ab: rows},
+ {a: ['bar']},
+ ];
+ const result = element._processNext(state, chunks);
+ const expectedCollapseSize = rows.length - 2 * element.context;
+
+ assert.equal(result.groups.length, 3, 'Results in three groups');
+
+ // The first and last are uncollapsed context, whereas the middle has
+ // a single context-control line.
+ assert.equal(result.groups[0].lines.length, element.context);
+ assert.equal(result.groups[1].lines.length, 1);
+ assert.equal(result.groups[2].lines.length, element.context);
+
+ // The collapsed group has the hidden lines as its context group.
+ assert.equal(result.groups[1].lines[0].contextGroups[0].lines.length,
+ expectedCollapseSize);
+ });
+
+ test('first', () => {
+ element.context = 10;
+ const state = {
+ lineNums: {left: 10, right: 100},
+ chunkIndex: 0,
+ };
+ const chunks = [
+ {ab: rows},
+ {a: ['foo']},
+ {a: ['bar']},
+ ];
+ const result = element._processNext(state, chunks);
+ const expectedCollapseSize = rows.length - element.context;
+
+ assert.equal(result.groups.length, 2, 'Results in two groups');
+
+ // Only the first group is collapsed.
+ assert.equal(result.groups[0].lines.length, 1);
+ assert.equal(result.groups[1].lines.length, element.context);
+
+ // The collapsed group has the hidden lines as its context group.
+ assert.equal(result.groups[0].lines[0].contextGroups[0].lines.length,
+ expectedCollapseSize);
+ });
+
+ test('few-rows', () => {
+ // Only ten rows.
+ rows = rows.slice(0, 10);
+ element.context = 10;
+ const state = {
+ lineNums: {left: 10, right: 100},
+ chunkIndex: 0,
+ };
+ const chunks = [
+ {ab: rows},
+ {a: ['foo']},
+ {a: ['bar']},
+ ];
+ const result = element._processNext(state, chunks);
+
+ // Results in one uncollapsed group with all rows.
+ assert.equal(result.groups.length, 1, 'Results in one group');
+ assert.equal(result.groups[0].lines.length, rows.length);
+ });
+
+ test('no single line collapse', () => {
+ rows = rows.slice(0, 7);
+ element.context = 3;
+ const state = {
+ lineNums: {left: 10, right: 100},
+ chunkIndex: 1,
+ };
+ const chunks = [
+ {a: ['foo']},
+ {ab: rows},
+ {a: ['bar']},
+ ];
+ const result = element._processNext(state, chunks);
+
+ // Results in one uncollapsed group with all rows.
+ assert.equal(result.groups.length, 1, 'Results in one group');
+ assert.equal(result.groups[0].lines.length, rows.length);
+ });
+
+ suite('with key location', () => {
+ let state;
+ let chunks;
setup(() => {
- rows = loremIpsum.split(' ');
- });
-
- test('WHOLE_FILE', () => {
- element.context = WHOLE_FILE;
- const state = {
+ state = {
lineNums: {left: 10, right: 100},
- chunkIndex: 1,
};
- const chunks = [
- {a: ['foo']},
- {ab: rows},
- {a: ['bar']},
- ];
- const result = element._processNext(state, chunks);
-
- // Results in one, uncollapsed group with all rows.
- assert.equal(result.groups.length, 1);
- assert.equal(result.groups[0].type, GrDiffGroup.Type.BOTH);
- assert.equal(result.groups[0].lines.length, rows.length);
-
- // Line numbers are set correctly.
- assert.equal(
- result.groups[0].lines[0].beforeNumber,
- state.lineNums.left + 1);
- assert.equal(
- result.groups[0].lines[0].afterNumber,
- state.lineNums.right + 1);
-
- assert.equal(result.groups[0].lines[rows.length - 1].beforeNumber,
- state.lineNums.left + rows.length);
- assert.equal(result.groups[0].lines[rows.length - 1].afterNumber,
- state.lineNums.right + rows.length);
- });
-
- test('with context', () => {
element.context = 10;
- const state = {
- lineNums: {left: 10, right: 100},
- chunkIndex: 1,
- };
- const chunks = [
- {a: ['foo']},
+ chunks = [
{ab: rows},
- {a: ['bar']},
+ {ab: ['foo'], keyLocation: true},
+ {ab: rows},
];
- const result = element._processNext(state, chunks);
- const expectedCollapseSize = rows.length - 2 * element.context;
-
- assert.equal(result.groups.length, 3, 'Results in three groups');
-
- // The first and last are uncollapsed context, whereas the middle has
- // a single context-control line.
- assert.equal(result.groups[0].lines.length, element.context);
- assert.equal(result.groups[1].lines.length, 1);
- assert.equal(result.groups[2].lines.length, element.context);
-
- // The collapsed group has the hidden lines as its context group.
- assert.equal(result.groups[1].lines[0].contextGroups[0].lines.length,
- expectedCollapseSize);
});
- test('first', () => {
- element.context = 10;
- const state = {
- lineNums: {left: 10, right: 100},
- chunkIndex: 0,
- };
- const chunks = [
- {ab: rows},
- {a: ['foo']},
- {a: ['bar']},
- ];
+ test('context before', () => {
+ state.chunkIndex = 0;
const result = element._processNext(state, chunks);
- const expectedCollapseSize = rows.length - element.context;
- assert.equal(result.groups.length, 2, 'Results in two groups');
-
- // Only the first group is collapsed.
+ // The first chunk is split into two groups:
+ // 1) A context-control, hiding everything but the context before
+ // the key location.
+ // 2) The context before the key location.
+ // The key location is not processed in this call to _processNext
+ assert.equal(result.groups.length, 2);
assert.equal(result.groups[0].lines.length, 1);
- assert.equal(result.groups[1].lines.length, element.context);
-
// The collapsed group has the hidden lines as its context group.
assert.equal(result.groups[0].lines[0].contextGroups[0].lines.length,
- expectedCollapseSize);
+ rows.length - element.context);
+ assert.equal(result.groups[1].lines.length, element.context);
});
- test('few-rows', () => {
- // Only ten rows.
- rows = rows.slice(0, 10);
- element.context = 10;
- const state = {
- lineNums: {left: 10, right: 100},
- chunkIndex: 0,
- };
- const chunks = [
- {ab: rows},
- {a: ['foo']},
- {a: ['bar']},
- ];
+ test('key location itself', () => {
+ state.chunkIndex = 1;
const result = element._processNext(state, chunks);
- // Results in one uncollapsed group with all rows.
- assert.equal(result.groups.length, 1, 'Results in one group');
- assert.equal(result.groups[0].lines.length, rows.length);
+ // The second chunk results in a single group, that is just the
+ // line with the key location
+ assert.equal(result.groups.length, 1);
+ assert.equal(result.groups[0].lines.length, 1);
+ assert.equal(result.lineDelta.left, 1);
+ assert.equal(result.lineDelta.right, 1);
});
- test('no single line collapse', () => {
- rows = rows.slice(0, 7);
- element.context = 3;
- const state = {
- lineNums: {left: 10, right: 100},
- chunkIndex: 1,
- };
- const chunks = [
- {a: ['foo']},
- {ab: rows},
- {a: ['bar']},
- ];
+ test('context after', () => {
+ state.chunkIndex = 2;
const result = element._processNext(state, chunks);
- // Results in one uncollapsed group with all rows.
- assert.equal(result.groups.length, 1, 'Results in one group');
- assert.equal(result.groups[0].lines.length, rows.length);
- });
-
- suite('with key location', () => {
- let state;
- let chunks;
-
- setup(() => {
- state = {
- lineNums: {left: 10, right: 100},
- };
- element.context = 10;
- chunks = [
- {ab: rows},
- {ab: ['foo'], keyLocation: true},
- {ab: rows},
- ];
- });
-
- test('context before', () => {
- state.chunkIndex = 0;
- const result = element._processNext(state, chunks);
-
- // The first chunk is split into two groups:
- // 1) A context-control, hiding everything but the context before
- // the key location.
- // 2) The context before the key location.
- // The key location is not processed in this call to _processNext
- assert.equal(result.groups.length, 2);
- assert.equal(result.groups[0].lines.length, 1);
- // The collapsed group has the hidden lines as its context group.
- assert.equal(result.groups[0].lines[0].contextGroups[0].lines.length,
- rows.length - element.context);
- assert.equal(result.groups[1].lines.length, element.context);
- });
-
- test('key location itself', () => {
- state.chunkIndex = 1;
- const result = element._processNext(state, chunks);
-
- // The second chunk results in a single group, that is just the
- // line with the key location
- assert.equal(result.groups.length, 1);
- assert.equal(result.groups[0].lines.length, 1);
- assert.equal(result.lineDelta.left, 1);
- assert.equal(result.lineDelta.right, 1);
- });
-
- test('context after', () => {
- state.chunkIndex = 2;
- const result = element._processNext(state, chunks);
-
- // The last chunk is split into two groups:
- // 1) The context after the key location.
- // 1) A context-control, hiding everything but the context after the
- // key location.
- assert.equal(result.groups.length, 2);
- assert.equal(result.groups[0].lines.length, element.context);
- assert.equal(result.groups[1].lines.length, 1);
- // The collapsed group has the hidden lines as its context group.
- assert.equal(result.groups[1].lines[0].contextGroups[0].lines.length,
- rows.length - element.context);
- });
- });
- });
-
- suite('gr-diff-processor helpers', () => {
- let rows;
-
- setup(() => {
- rows = loremIpsum.split(' ');
- });
-
- test('_linesFromRows', () => {
- const startLineNum = 10;
- let result = element._linesFromRows(GrDiffLine.Type.ADD, rows,
- startLineNum + 1);
-
- assert.equal(result.length, rows.length);
- assert.equal(result[0].type, GrDiffLine.Type.ADD);
- assert.equal(result[0].afterNumber, startLineNum + 1);
- assert.notOk(result[0].beforeNumber);
- assert.equal(result[result.length - 1].afterNumber,
- startLineNum + rows.length);
- assert.notOk(result[result.length - 1].beforeNumber);
-
- result = element._linesFromRows(GrDiffLine.Type.REMOVE, rows,
- startLineNum + 1);
-
- assert.equal(result.length, rows.length);
- assert.equal(result[0].type, GrDiffLine.Type.REMOVE);
- assert.equal(result[0].beforeNumber, startLineNum + 1);
- assert.notOk(result[0].afterNumber);
- assert.equal(result[result.length - 1].beforeNumber,
- startLineNum + rows.length);
- assert.notOk(result[result.length - 1].afterNumber);
- });
- });
-
- suite('_breakdown*', () => {
- test('_breakdownChunk breaks down additions', () => {
- sandbox.spy(element, '_breakdown');
- const chunk = {b: ['blah', 'blah', 'blah']};
- const result = element._breakdownChunk(chunk);
- assert.deepEqual(result, [chunk]);
- assert.isTrue(element._breakdown.called);
- });
-
- test('_breakdownChunk keeps due_to_rebase for broken down additions',
- () => {
- sandbox.spy(element, '_breakdown');
- const chunk = {b: ['blah', 'blah', 'blah'], due_to_rebase: true};
- const result = element._breakdownChunk(chunk);
- for (const subResult of result) {
- assert.isTrue(subResult.due_to_rebase);
- }
- });
-
- test('_breakdown common case', () => {
- const array = 'Lorem ipsum dolor sit amet, suspendisse inceptos'
- .split(' ');
- const size = 3;
-
- const result = element._breakdown(array, size);
-
- for (const subResult of result) {
- assert.isAtMost(subResult.length, size);
- }
- const flattened = result
- .reduce((a, b) => a.concat(b), []);
- assert.deepEqual(flattened, array);
- });
-
- test('_breakdown smaller than size', () => {
- const array = 'Lorem ipsum dolor sit amet, suspendisse inceptos'
- .split(' ');
- const size = 10;
- const expected = [array];
-
- const result = element._breakdown(array, size);
-
- assert.deepEqual(result, expected);
- });
-
- test('_breakdown empty', () => {
- const array = [];
- const size = 10;
- const expected = [];
-
- const result = element._breakdown(array, size);
-
- assert.deepEqual(result, expected);
+ // The last chunk is split into two groups:
+ // 1) The context after the key location.
+ // 1) A context-control, hiding everything but the context after the
+ // key location.
+ assert.equal(result.groups.length, 2);
+ assert.equal(result.groups[0].lines.length, element.context);
+ assert.equal(result.groups[1].lines.length, 1);
+ // The collapsed group has the hidden lines as its context group.
+ assert.equal(result.groups[1].lines[0].contextGroups[0].lines.length,
+ rows.length - element.context);
});
});
});
- test('detaching cancels', () => {
- element = fixture('basic');
- sandbox.stub(element, 'cancel');
- element.detached();
- assert(element.cancel.called);
+ suite('gr-diff-processor helpers', () => {
+ let rows;
+
+ setup(() => {
+ rows = loremIpsum.split(' ');
+ });
+
+ test('_linesFromRows', () => {
+ const startLineNum = 10;
+ let result = element._linesFromRows(GrDiffLine.Type.ADD, rows,
+ startLineNum + 1);
+
+ assert.equal(result.length, rows.length);
+ assert.equal(result[0].type, GrDiffLine.Type.ADD);
+ assert.equal(result[0].afterNumber, startLineNum + 1);
+ assert.notOk(result[0].beforeNumber);
+ assert.equal(result[result.length - 1].afterNumber,
+ startLineNum + rows.length);
+ assert.notOk(result[result.length - 1].beforeNumber);
+
+ result = element._linesFromRows(GrDiffLine.Type.REMOVE, rows,
+ startLineNum + 1);
+
+ assert.equal(result.length, rows.length);
+ assert.equal(result[0].type, GrDiffLine.Type.REMOVE);
+ assert.equal(result[0].beforeNumber, startLineNum + 1);
+ assert.notOk(result[0].afterNumber);
+ assert.equal(result[result.length - 1].beforeNumber,
+ startLineNum + rows.length);
+ assert.notOk(result[result.length - 1].afterNumber);
+ });
+ });
+
+ suite('_breakdown*', () => {
+ test('_breakdownChunk breaks down additions', () => {
+ sandbox.spy(element, '_breakdown');
+ const chunk = {b: ['blah', 'blah', 'blah']};
+ const result = element._breakdownChunk(chunk);
+ assert.deepEqual(result, [chunk]);
+ assert.isTrue(element._breakdown.called);
+ });
+
+ test('_breakdownChunk keeps due_to_rebase for broken down additions',
+ () => {
+ sandbox.spy(element, '_breakdown');
+ const chunk = {b: ['blah', 'blah', 'blah'], due_to_rebase: true};
+ const result = element._breakdownChunk(chunk);
+ for (const subResult of result) {
+ assert.isTrue(subResult.due_to_rebase);
+ }
+ });
+
+ test('_breakdown common case', () => {
+ const array = 'Lorem ipsum dolor sit amet, suspendisse inceptos'
+ .split(' ');
+ const size = 3;
+
+ const result = element._breakdown(array, size);
+
+ for (const subResult of result) {
+ assert.isAtMost(subResult.length, size);
+ }
+ const flattened = result
+ .reduce((a, b) => a.concat(b), []);
+ assert.deepEqual(flattened, array);
+ });
+
+ test('_breakdown smaller than size', () => {
+ const array = 'Lorem ipsum dolor sit amet, suspendisse inceptos'
+ .split(' ');
+ const size = 10;
+ const expected = [array];
+
+ const result = element._breakdown(array, size);
+
+ assert.deepEqual(result, expected);
+ });
+
+ test('_breakdown empty', () => {
+ const array = [];
+ const size = 10;
+ const expected = [];
+
+ const result = element._breakdown(array, size);
+
+ assert.deepEqual(result, expected);
+ });
});
});
+
+ test('detaching cancels', () => {
+ element = fixture('basic');
+ sandbox.stub(element, 'cancel');
+ element.detached();
+ assert(element.cancel.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
deleted file mode 100644
index cfa46a0..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
+++ /dev/null
@@ -1,30 +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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/dom-util-behavior/dom-util-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<script src="../../../scripts/util.js"></script>
-
-<dom-module id="gr-diff-selection">
- <template>
- <div class="contentWrapper">
- <slot></slot>
- </div>
- </template>
- <script src="../gr-diff-highlight/gr-range-normalizer.js"></script>
- <script src="gr-diff-selection.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index e46f959..e59b0c2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -14,352 +14,364 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * Possible CSS classes indicating the state of selection. Dynamically added/
- * removed based on where the user clicks within the diff.
- */
- const SelectionClass = {
- COMMENT: 'selected-comment',
- LEFT: 'selected-left',
- RIGHT: 'selected-right',
- BLAME: 'selected-blame',
- };
+import '../../../behaviors/dom-util-behavior/dom-util-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../../scripts/util.js';
+import '../gr-diff-highlight/gr-range-normalizer.js';
+import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-diff-selection_html.js';
- const getNewCache = () => { return {left: null, right: null}; };
+/**
+ * Possible CSS classes indicating the state of selection. Dynamically added/
+ * removed based on where the user clicks within the diff.
+ */
+const SelectionClass = {
+ COMMENT: 'selected-comment',
+ LEFT: 'selected-left',
+ RIGHT: 'selected-right',
+ BLAME: 'selected-blame',
+};
- /**
- * @appliesMixin Gerrit.DomUtilMixin
- * @extends Polymer.Element
- */
- class GrDiffSelection extends Polymer.mixinBehaviors( [
- Gerrit.DomUtilBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-diff-selection'; }
+const getNewCache = () => { return {left: null, right: null}; };
- static get properties() {
- return {
- diff: Object,
- /** @type {?Object} */
- _cachedDiffBuilder: Object,
- _linesCache: {
- type: Object,
- value: getNewCache(),
- },
- };
+/**
+ * @appliesMixin Gerrit.DomUtilMixin
+ * @extends Polymer.Element
+ */
+class GrDiffSelection extends mixinBehaviors( [
+ Gerrit.DomUtilBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-diff-selection'; }
+
+ static get properties() {
+ return {
+ diff: Object,
+ /** @type {?Object} */
+ _cachedDiffBuilder: Object,
+ _linesCache: {
+ type: Object,
+ value: getNewCache(),
+ },
+ };
+ }
+
+ static get observers() {
+ return [
+ '_diffChanged(diff)',
+ ];
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('copy',
+ e => this._handleCopy(e));
+ addListener(this, 'down',
+ e => this._handleDown(e));
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this.classList.add(SelectionClass.RIGHT);
+ }
+
+ get diffBuilder() {
+ if (!this._cachedDiffBuilder) {
+ this._cachedDiffBuilder =
+ dom(this).querySelector('gr-diff-builder');
}
+ return this._cachedDiffBuilder;
+ }
- static get observers() {
- return [
- '_diffChanged(diff)',
- ];
- }
+ _diffChanged() {
+ this._linesCache = getNewCache();
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('copy',
- e => this._handleCopy(e));
- Polymer.Gestures.addListener(this, 'down',
- e => this._handleDown(e));
- }
-
- /** @override */
- attached() {
- super.attached();
- this.classList.add(SelectionClass.RIGHT);
- }
-
- get diffBuilder() {
- if (!this._cachedDiffBuilder) {
- this._cachedDiffBuilder =
- Polymer.dom(this).querySelector('gr-diff-builder');
- }
- return this._cachedDiffBuilder;
- }
-
- _diffChanged() {
- this._linesCache = getNewCache();
- }
-
- _handleDownOnRangeComment(node) {
- if (node &&
- node.nodeName &&
- node.nodeName.toLowerCase() === 'gr-comment-thread') {
- this._setClasses([
- SelectionClass.COMMENT,
- node.commentSide === 'left' ?
- SelectionClass.LEFT :
- SelectionClass.RIGHT,
- ]);
- return true;
- }
- return false;
- }
-
- _handleDown(e) {
- // Handle the down event on comment thread in Polymer 2
- const handled = this._handleDownOnRangeComment(e.target);
- if (handled) return;
-
- const lineEl = this.diffBuilder.getLineElByChild(e.target);
- const blameSelected = this._elementDescendedFromClass(e.target, 'blame');
- if (!lineEl && !blameSelected) { return; }
-
- const targetClasses = [];
-
- if (blameSelected) {
- targetClasses.push(SelectionClass.BLAME);
- } else {
- const commentSelected =
- this._elementDescendedFromClass(e.target, 'gr-comment');
- const side = this.diffBuilder.getSideByLineEl(lineEl);
-
- targetClasses.push(side === 'left' ?
+ _handleDownOnRangeComment(node) {
+ if (node &&
+ node.nodeName &&
+ node.nodeName.toLowerCase() === 'gr-comment-thread') {
+ this._setClasses([
+ SelectionClass.COMMENT,
+ node.commentSide === 'left' ?
SelectionClass.LEFT :
- SelectionClass.RIGHT);
-
- if (commentSelected) {
- targetClasses.push(SelectionClass.COMMENT);
- }
- }
-
- this._setClasses(targetClasses);
+ SelectionClass.RIGHT,
+ ]);
+ return true;
}
+ return false;
+ }
- /**
- * Set the provided list of classes on the element, to the exclusion of all
- * other SelectionClass values.
- *
- * @param {!Array<!string>} targetClasses
- */
- _setClasses(targetClasses) {
- // Remove any selection classes that do not belong.
- for (const key in SelectionClass) {
- if (SelectionClass.hasOwnProperty(key)) {
- const className = SelectionClass[key];
- if (!targetClasses.includes(className)) {
- this.classList.remove(SelectionClass[key]);
- }
- }
- }
- // Add new selection classes iff they are not already present.
- for (const _class of targetClasses) {
- if (!this.classList.contains(_class)) {
- this.classList.add(_class);
- }
- }
- }
+ _handleDown(e) {
+ // Handle the down event on comment thread in Polymer 2
+ const handled = this._handleDownOnRangeComment(e.target);
+ if (handled) return;
- _getCopyEventTarget(e) {
- return Polymer.dom(e).rootTarget;
- }
+ const lineEl = this.diffBuilder.getLineElByChild(e.target);
+ const blameSelected = this._elementDescendedFromClass(e.target, 'blame');
+ if (!lineEl && !blameSelected) { return; }
- /**
- * Utility function to determine whether an element is a descendant of
- * another element with the particular className.
- *
- * @param {!Element} element
- * @param {!string} className
- * @return {boolean}
- */
- _elementDescendedFromClass(element, className) {
- return this.descendedFromClass(element, className,
- this.diffBuilder.diffElement);
- }
+ const targetClasses = [];
- _handleCopy(e) {
- let commentSelected = false;
- const target = this._getCopyEventTarget(e);
- if (target.type === 'textarea') { return; }
- if (!this._elementDescendedFromClass(target, 'diff-row')) { return; }
- if (this.classList.contains(SelectionClass.COMMENT)) {
- commentSelected = true;
- }
- const lineEl = this.diffBuilder.getLineElByChild(target);
- if (!lineEl) {
- return;
- }
+ if (blameSelected) {
+ targetClasses.push(SelectionClass.BLAME);
+ } else {
+ const commentSelected =
+ this._elementDescendedFromClass(e.target, 'gr-comment');
const side = this.diffBuilder.getSideByLineEl(lineEl);
- const text = this._getSelectedText(side, commentSelected);
- if (text) {
- e.clipboardData.setData('Text', text);
- e.preventDefault();
- }
- }
- _getSelection() {
- const diffHosts = util.querySelectorAll(document.body, 'gr-diff');
- if (!diffHosts.length) return window.getSelection();
+ targetClasses.push(side === 'left' ?
+ SelectionClass.LEFT :
+ SelectionClass.RIGHT);
- const curDiffHost = diffHosts.find(diffHost => {
- if (!diffHost || !diffHost.shadowRoot) return false;
- const selection = diffHost.shadowRoot.getSelection();
- // Pick the one with valid selection:
- // https://developer.mozilla.org/en-US/docs/Web/API/Selection/type
- return selection && selection.type !== 'None';
- });
-
- return curDiffHost ?
- curDiffHost.shadowRoot.getSelection(): window.getSelection();
- }
-
- /**
- * Get the text of the current selection. If commentSelected is
- * true, it returns only the text of comments within the selection.
- * Otherwise it returns the text of the selected diff region.
- *
- * @param {!string} side The side that is selected.
- * @param {boolean} commentSelected Whether or not a comment is selected.
- * @return {string} The selected text.
- */
- _getSelectedText(side, commentSelected) {
- const sel = this._getSelection();
- if (sel.rangeCount != 1) {
- return ''; // No multi-select support yet.
- }
if (commentSelected) {
- return this._getCommentLines(sel, side);
+ targetClasses.push(SelectionClass.COMMENT);
}
- const range = GrRangeNormalizer.normalize(sel.getRangeAt(0));
- const startLineEl =
- this.diffBuilder.getLineElByChild(range.startContainer);
- const endLineEl = this.diffBuilder.getLineElByChild(range.endContainer);
- // Happens when triple click in side-by-side mode with other side empty.
- const endsAtOtherEmptySide = !endLineEl &&
- range.endOffset === 0 &&
- range.endContainer.nodeName === 'TD' &&
- (range.endContainer.classList.contains('left') ||
- range.endContainer.classList.contains('right'));
- const startLineNum = parseInt(startLineEl.getAttribute('data-value'), 10);
- let endLineNum;
- if (endsAtOtherEmptySide) {
- endLineNum = startLineNum + 1;
- } else if (endLineEl) {
- endLineNum = parseInt(endLineEl.getAttribute('data-value'), 10);
- }
-
- return this._getRangeFromDiff(startLineNum, range.startOffset, endLineNum,
- range.endOffset, side);
}
- /**
- * Query the diff object for the selected lines.
- *
- * @param {number} startLineNum
- * @param {number} startOffset
- * @param {number|undefined} endLineNum Use undefined to get the range
- * extending to the end of the file.
- * @param {number} endOffset
- * @param {!string} side The side that is currently selected.
- * @return {string} The selected diff text.
- */
- _getRangeFromDiff(startLineNum, startOffset, endLineNum, endOffset, side) {
- const lines =
- this._getDiffLines(side).slice(startLineNum - 1, endLineNum);
- if (lines.length) {
- lines[lines.length - 1] = lines[lines.length - 1]
- .substring(0, endOffset);
- lines[0] = lines[0].substring(startOffset);
+ this._setClasses(targetClasses);
+ }
+
+ /**
+ * Set the provided list of classes on the element, to the exclusion of all
+ * other SelectionClass values.
+ *
+ * @param {!Array<!string>} targetClasses
+ */
+ _setClasses(targetClasses) {
+ // Remove any selection classes that do not belong.
+ for (const key in SelectionClass) {
+ if (SelectionClass.hasOwnProperty(key)) {
+ const className = SelectionClass[key];
+ if (!targetClasses.includes(className)) {
+ this.classList.remove(SelectionClass[key]);
+ }
}
- return lines.join('\n');
}
-
- /**
- * Query the diff object for the lines from a particular side.
- *
- * @param {!string} side The side that is currently selected.
- * @return {!Array<string>} An array of strings indexed by line number.
- */
- _getDiffLines(side) {
- if (this._linesCache[side]) {
- return this._linesCache[side];
+ // Add new selection classes iff they are not already present.
+ for (const _class of targetClasses) {
+ if (!this.classList.contains(_class)) {
+ this.classList.add(_class);
}
- let lines = [];
- const key = side === 'left' ? 'a' : 'b';
- for (const chunk of this.diff.content) {
- if (chunk.ab) {
- lines = lines.concat(chunk.ab);
- } else if (chunk[key]) {
- lines = lines.concat(chunk[key]);
- }
- }
- this._linesCache[side] = lines;
- return lines;
- }
-
- /**
- * Query the diffElement for comments and check whether they lie inside the
- * selection range.
- *
- * @param {!Selection} sel The selection of the window.
- * @param {!string} side The side that is currently selected.
- * @return {string} The selected comment text.
- */
- _getCommentLines(sel, side) {
- const range = GrRangeNormalizer.normalize(sel.getRangeAt(0));
- const content = [];
- // Query the diffElement for comments.
- const messages = this.diffBuilder.diffElement.querySelectorAll(
- `.side-by-side [data-side="${side
- }"] .message *, .unified .message *`);
-
- for (let i = 0; i < messages.length; i++) {
- const el = messages[i];
- // Check if the comment element exists inside the selection.
- if (sel.containsNode(el, true)) {
- // Padded elements require newlines for accurate spacing.
- if (el.parentElement.id === 'container' ||
- el.parentElement.nodeName === 'BLOCKQUOTE') {
- if (content.length && content[content.length - 1] !== '') {
- content.push('');
- }
- }
-
- if (el.id === 'output' &&
- !this._elementDescendedFromClass(el, 'collapsed')) {
- content.push(this._getTextContentForRange(el, sel, range));
- }
- }
- }
-
- return content.join('\n');
- }
-
- /**
- * Given a DOM node, a selection, and a selection range, recursively get all
- * of the text content within that selection.
- * Using a domNode that isn't in the selection returns an empty string.
- *
- * @param {!Node} domNode The root DOM node.
- * @param {!Selection} sel The selection.
- * @param {!Range} range The normalized selection range.
- * @return {string} The text within the selection.
- */
- _getTextContentForRange(domNode, sel, range) {
- if (!sel.containsNode(domNode, true)) { return ''; }
-
- let text = '';
- if (domNode instanceof Text) {
- text = domNode.textContent;
- if (domNode === range.endContainer) {
- text = text.substring(0, range.endOffset);
- }
- if (domNode === range.startContainer) {
- text = text.substring(range.startOffset);
- }
- } else {
- for (const childNode of domNode.childNodes) {
- text += this._getTextContentForRange(childNode, sel, range);
- }
- }
- return text;
}
}
- customElements.define(GrDiffSelection.is, GrDiffSelection);
-})();
+ _getCopyEventTarget(e) {
+ return dom(e).rootTarget;
+ }
+
+ /**
+ * Utility function to determine whether an element is a descendant of
+ * another element with the particular className.
+ *
+ * @param {!Element} element
+ * @param {!string} className
+ * @return {boolean}
+ */
+ _elementDescendedFromClass(element, className) {
+ return this.descendedFromClass(element, className,
+ this.diffBuilder.diffElement);
+ }
+
+ _handleCopy(e) {
+ let commentSelected = false;
+ const target = this._getCopyEventTarget(e);
+ if (target.type === 'textarea') { return; }
+ if (!this._elementDescendedFromClass(target, 'diff-row')) { return; }
+ if (this.classList.contains(SelectionClass.COMMENT)) {
+ commentSelected = true;
+ }
+ const lineEl = this.diffBuilder.getLineElByChild(target);
+ if (!lineEl) {
+ return;
+ }
+ const side = this.diffBuilder.getSideByLineEl(lineEl);
+ const text = this._getSelectedText(side, commentSelected);
+ if (text) {
+ e.clipboardData.setData('Text', text);
+ e.preventDefault();
+ }
+ }
+
+ _getSelection() {
+ const diffHosts = util.querySelectorAll(document.body, 'gr-diff');
+ if (!diffHosts.length) return window.getSelection();
+
+ const curDiffHost = diffHosts.find(diffHost => {
+ if (!diffHost || !diffHost.shadowRoot) return false;
+ const selection = diffHost.shadowRoot.getSelection();
+ // Pick the one with valid selection:
+ // https://developer.mozilla.org/en-US/docs/Web/API/Selection/type
+ return selection && selection.type !== 'None';
+ });
+
+ return curDiffHost ?
+ curDiffHost.shadowRoot.getSelection(): window.getSelection();
+ }
+
+ /**
+ * Get the text of the current selection. If commentSelected is
+ * true, it returns only the text of comments within the selection.
+ * Otherwise it returns the text of the selected diff region.
+ *
+ * @param {!string} side The side that is selected.
+ * @param {boolean} commentSelected Whether or not a comment is selected.
+ * @return {string} The selected text.
+ */
+ _getSelectedText(side, commentSelected) {
+ const sel = this._getSelection();
+ if (sel.rangeCount != 1) {
+ return ''; // No multi-select support yet.
+ }
+ if (commentSelected) {
+ return this._getCommentLines(sel, side);
+ }
+ const range = GrRangeNormalizer.normalize(sel.getRangeAt(0));
+ const startLineEl =
+ this.diffBuilder.getLineElByChild(range.startContainer);
+ const endLineEl = this.diffBuilder.getLineElByChild(range.endContainer);
+ // Happens when triple click in side-by-side mode with other side empty.
+ const endsAtOtherEmptySide = !endLineEl &&
+ range.endOffset === 0 &&
+ range.endContainer.nodeName === 'TD' &&
+ (range.endContainer.classList.contains('left') ||
+ range.endContainer.classList.contains('right'));
+ const startLineNum = parseInt(startLineEl.getAttribute('data-value'), 10);
+ let endLineNum;
+ if (endsAtOtherEmptySide) {
+ endLineNum = startLineNum + 1;
+ } else if (endLineEl) {
+ endLineNum = parseInt(endLineEl.getAttribute('data-value'), 10);
+ }
+
+ return this._getRangeFromDiff(startLineNum, range.startOffset, endLineNum,
+ range.endOffset, side);
+ }
+
+ /**
+ * Query the diff object for the selected lines.
+ *
+ * @param {number} startLineNum
+ * @param {number} startOffset
+ * @param {number|undefined} endLineNum Use undefined to get the range
+ * extending to the end of the file.
+ * @param {number} endOffset
+ * @param {!string} side The side that is currently selected.
+ * @return {string} The selected diff text.
+ */
+ _getRangeFromDiff(startLineNum, startOffset, endLineNum, endOffset, side) {
+ const lines =
+ this._getDiffLines(side).slice(startLineNum - 1, endLineNum);
+ if (lines.length) {
+ lines[lines.length - 1] = lines[lines.length - 1]
+ .substring(0, endOffset);
+ lines[0] = lines[0].substring(startOffset);
+ }
+ return lines.join('\n');
+ }
+
+ /**
+ * Query the diff object for the lines from a particular side.
+ *
+ * @param {!string} side The side that is currently selected.
+ * @return {!Array<string>} An array of strings indexed by line number.
+ */
+ _getDiffLines(side) {
+ if (this._linesCache[side]) {
+ return this._linesCache[side];
+ }
+ let lines = [];
+ const key = side === 'left' ? 'a' : 'b';
+ for (const chunk of this.diff.content) {
+ if (chunk.ab) {
+ lines = lines.concat(chunk.ab);
+ } else if (chunk[key]) {
+ lines = lines.concat(chunk[key]);
+ }
+ }
+ this._linesCache[side] = lines;
+ return lines;
+ }
+
+ /**
+ * Query the diffElement for comments and check whether they lie inside the
+ * selection range.
+ *
+ * @param {!Selection} sel The selection of the window.
+ * @param {!string} side The side that is currently selected.
+ * @return {string} The selected comment text.
+ */
+ _getCommentLines(sel, side) {
+ const range = GrRangeNormalizer.normalize(sel.getRangeAt(0));
+ const content = [];
+ // Query the diffElement for comments.
+ const messages = this.diffBuilder.diffElement.querySelectorAll(
+ `.side-by-side [data-side="${side
+ }"] .message *, .unified .message *`);
+
+ for (let i = 0; i < messages.length; i++) {
+ const el = messages[i];
+ // Check if the comment element exists inside the selection.
+ if (sel.containsNode(el, true)) {
+ // Padded elements require newlines for accurate spacing.
+ if (el.parentElement.id === 'container' ||
+ el.parentElement.nodeName === 'BLOCKQUOTE') {
+ if (content.length && content[content.length - 1] !== '') {
+ content.push('');
+ }
+ }
+
+ if (el.id === 'output' &&
+ !this._elementDescendedFromClass(el, 'collapsed')) {
+ content.push(this._getTextContentForRange(el, sel, range));
+ }
+ }
+ }
+
+ return content.join('\n');
+ }
+
+ /**
+ * Given a DOM node, a selection, and a selection range, recursively get all
+ * of the text content within that selection.
+ * Using a domNode that isn't in the selection returns an empty string.
+ *
+ * @param {!Node} domNode The root DOM node.
+ * @param {!Selection} sel The selection.
+ * @param {!Range} range The normalized selection range.
+ * @return {string} The text within the selection.
+ */
+ _getTextContentForRange(domNode, sel, range) {
+ if (!sel.containsNode(domNode, true)) { return ''; }
+
+ let text = '';
+ if (domNode instanceof Text) {
+ text = domNode.textContent;
+ if (domNode === range.endContainer) {
+ text = text.substring(0, range.endOffset);
+ }
+ if (domNode === range.startContainer) {
+ text = text.substring(range.startOffset);
+ }
+ } else {
+ for (const childNode of domNode.childNodes) {
+ text += this._getTextContentForRange(childNode, sel, range);
+ }
+ }
+ return text;
+ }
+}
+
+customElements.define(GrDiffSelection.is, GrDiffSelection);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_html.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_html.js
new file mode 100644
index 0000000..ce6008e
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_html.js
@@ -0,0 +1,23 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <div class="contentWrapper">
+ <slot></slot>
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
index a8e85e2..e9405cb 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-selection</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-diff-selection.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -107,298 +102,299 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff-selection', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-diff-selection.js';
+suite('gr-diff-selection', () => {
+ let element;
+ let sandbox;
- const emulateCopyOn = function(target) {
- const fakeEvent = {
- target,
- preventDefault: sandbox.stub(),
- clipboardData: {
- setData: sandbox.stub(),
+ const emulateCopyOn = function(target) {
+ const fakeEvent = {
+ target,
+ preventDefault: sandbox.stub(),
+ clipboardData: {
+ setData: sandbox.stub(),
+ },
+ };
+ element._getCopyEventTarget.returns(target);
+ element._handleCopy(fakeEvent);
+ return fakeEvent;
+ };
+
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(element, '_getCopyEventTarget');
+ element._cachedDiffBuilder = {
+ getLineElByChild: sandbox.stub().returns({}),
+ getSideByLineEl: sandbox.stub(),
+ diffElement: element.querySelector('#diffTable'),
+ };
+ element.diff = {
+ content: [
+ {
+ a: ['ba ba'],
+ b: ['some other text'],
},
- };
- element._getCopyEventTarget.returns(target);
- element._handleCopy(fakeEvent);
- return fakeEvent;
+ {
+ a: ['zin'],
+ b: ['more more more'],
+ },
+ {
+ a: ['ga ga'],
+ b: ['some other text'],
+ },
+ ],
+ };
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('applies selected-left on left side click', () => {
+ element.classList.add('selected-right');
+ element._cachedDiffBuilder.getSideByLineEl.returns('left');
+ MockInteractions.down(element);
+ assert.isTrue(
+ element.classList.contains('selected-left'), 'adds selected-left');
+ assert.isFalse(
+ element.classList.contains('selected-right'),
+ 'removes selected-right');
+ });
+
+ test('applies selected-right on right side click', () => {
+ element.classList.add('selected-left');
+ element._cachedDiffBuilder.getSideByLineEl.returns('right');
+ MockInteractions.down(element);
+ assert.isTrue(
+ element.classList.contains('selected-right'), 'adds selected-right');
+ assert.isFalse(
+ element.classList.contains('selected-left'), 'removes selected-left');
+ });
+
+ test('applies selected-blame on blame click', () => {
+ element.classList.add('selected-left');
+ element.diffBuilder.getLineElByChild.returns(null);
+ sandbox.stub(element, '_elementDescendedFromClass',
+ (el, className) => className === 'blame');
+ MockInteractions.down(element);
+ assert.isTrue(
+ element.classList.contains('selected-blame'), 'adds selected-right');
+ assert.isFalse(
+ element.classList.contains('selected-left'), 'removes selected-left');
+ });
+
+ test('ignores copy for non-content Element', () => {
+ sandbox.stub(element, '_getSelectedText');
+ emulateCopyOn(element.querySelector('.not-diff-row'));
+ assert.isFalse(element._getSelectedText.called);
+ });
+
+ test('asks for text for left side Elements', () => {
+ element._cachedDiffBuilder.getSideByLineEl.returns('left');
+ sandbox.stub(element, '_getSelectedText');
+ emulateCopyOn(element.querySelector('div.contentText'));
+ assert.deepEqual(['left', false], element._getSelectedText.lastCall.args);
+ });
+
+ test('reacts to copy for content Elements', () => {
+ sandbox.stub(element, '_getSelectedText');
+ emulateCopyOn(element.querySelector('div.contentText'));
+ assert.isTrue(element._getSelectedText.called);
+ });
+
+ test('copy event is prevented for content Elements', () => {
+ sandbox.stub(element, '_getSelectedText');
+ element._cachedDiffBuilder.getSideByLineEl.returns('left');
+ element._getSelectedText.returns('test');
+ const event = emulateCopyOn(element.querySelector('div.contentText'));
+ assert.isTrue(event.preventDefault.called);
+ });
+
+ test('inserts text into clipboard on copy', () => {
+ sandbox.stub(element, '_getSelectedText').returns('the text');
+ const event = emulateCopyOn(element.querySelector('div.contentText'));
+ assert.deepEqual(
+ ['Text', 'the text'], event.clipboardData.setData.lastCall.args);
+ });
+
+ test('_setClasses adds given SelectionClass values, removes others', () => {
+ element.classList.add('selected-right');
+ element._setClasses(['selected-comment', 'selected-left']);
+ assert.isTrue(element.classList.contains('selected-comment'));
+ assert.isTrue(element.classList.contains('selected-left'));
+ assert.isFalse(element.classList.contains('selected-right'));
+ assert.isFalse(element.classList.contains('selected-blame'));
+
+ element._setClasses(['selected-blame']);
+ assert.isFalse(element.classList.contains('selected-comment'));
+ assert.isFalse(element.classList.contains('selected-left'));
+ assert.isFalse(element.classList.contains('selected-right'));
+ assert.isTrue(element.classList.contains('selected-blame'));
+ });
+
+ test('_setClasses removes before it ads', () => {
+ element.classList.add('selected-right');
+ const addStub = sandbox.stub(element.classList, 'add');
+ const removeStub = sandbox.stub(element.classList, 'remove', () => {
+ assert.isFalse(addStub.called);
+ });
+ element._setClasses(['selected-comment', 'selected-left']);
+ assert.isTrue(addStub.called);
+ assert.isTrue(removeStub.called);
+ });
+
+ test('copies content correctly', () => {
+ // Fetch the line number.
+ element._cachedDiffBuilder.getLineElByChild = function(child) {
+ while (!child.classList.contains('content') && child.parentElement) {
+ child = child.parentElement;
+ }
+ return child.previousElementSibling;
};
+ element.classList.add('selected-left');
+ element.classList.remove('selected-right');
+
+ const selection = window.getSelection();
+ selection.removeAllRanges();
+ const range = document.createRange();
+ range.setStart(element.querySelector('div.contentText').firstChild, 3);
+ range.setEnd(
+ element.querySelectorAll('div.contentText')[4].firstChild, 2);
+ selection.addRange(range);
+ assert.equal(element._getSelectedText('left'), 'ba\nzin\nga');
+ });
+
+ test('copies comments', () => {
+ element.classList.add('selected-left');
+ element.classList.add('selected-comment');
+ element.classList.remove('selected-right');
+ const selection = window.getSelection();
+ selection.removeAllRanges();
+ const range = document.createRange();
+ range.setStart(
+ element.querySelector('.gr-formatted-text *').firstChild, 3);
+ range.setEnd(
+ element.querySelectorAll('.gr-formatted-text *')[2].childNodes[2], 7);
+ selection.addRange(range);
+ assert.equal('s is a comment\nThis is a differ',
+ element._getSelectedText('left', true));
+ });
+
+ test('respects astral chars in comments', () => {
+ element.classList.add('selected-left');
+ element.classList.add('selected-comment');
+ element.classList.remove('selected-right');
+ const selection = window.getSelection();
+ selection.removeAllRanges();
+ const range = document.createRange();
+ const nodes = element.querySelectorAll('.gr-formatted-text *');
+ range.setStart(nodes[2].childNodes[2], 13);
+ range.setEnd(nodes[2].childNodes[2], 23);
+ selection.addRange(range);
+ assert.equal('mment 💩 u',
+ element._getSelectedText('left', true));
+ });
+
+ test('defers to default behavior for textarea', () => {
+ element.classList.add('selected-left');
+ element.classList.remove('selected-right');
+ const selectedTextSpy = sandbox.spy(element, '_getSelectedText');
+ emulateCopyOn(element.querySelector('textarea'));
+ assert.isFalse(selectedTextSpy.called);
+ });
+
+ test('regression test for 4794', () => {
+ element._cachedDiffBuilder.getLineElByChild = function(child) {
+ while (!child.classList.contains('content') && child.parentElement) {
+ child = child.parentElement;
+ }
+ return child.previousElementSibling;
+ };
+
+ element.classList.add('selected-right');
+ element.classList.remove('selected-left');
+
+ const selection = window.getSelection();
+ selection.removeAllRanges();
+ const range = document.createRange();
+ range.setStart(
+ element.querySelectorAll('div.contentText')[1].firstChild, 4);
+ range.setEnd(
+ element.querySelectorAll('div.contentText')[1].firstChild, 10);
+ selection.addRange(range);
+ assert.equal(element._getSelectedText('right'), ' other');
+ });
+
+ test('copies to end of side (issue 7895)', () => {
+ element._cachedDiffBuilder.getLineElByChild = function(child) {
+ // Return null for the end container.
+ if (child.textContent === 'ga ga') { return null; }
+ while (!child.classList.contains('content') && child.parentElement) {
+ child = child.parentElement;
+ }
+ return child.previousElementSibling;
+ };
+ element.classList.add('selected-left');
+ element.classList.remove('selected-right');
+ const selection = window.getSelection();
+ selection.removeAllRanges();
+ const range = document.createRange();
+ range.setStart(element.querySelector('div.contentText').firstChild, 3);
+ range.setEnd(
+ element.querySelectorAll('div.contentText')[4].firstChild, 2);
+ selection.addRange(range);
+ assert.equal(element._getSelectedText('left'), 'ba\nzin\nga');
+ });
+
+ suite('_getTextContentForRange', () => {
+ let selection;
+ let range;
+ let nodes;
+
setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- sandbox.stub(element, '_getCopyEventTarget');
- element._cachedDiffBuilder = {
- getLineElByChild: sandbox.stub().returns({}),
- getSideByLineEl: sandbox.stub(),
- diffElement: element.querySelector('#diffTable'),
- };
- element.diff = {
- content: [
- {
- a: ['ba ba'],
- b: ['some other text'],
- },
- {
- a: ['zin'],
- b: ['more more more'],
- },
- {
- a: ['ga ga'],
- b: ['some other text'],
- },
- ],
- };
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('applies selected-left on left side click', () => {
- element.classList.add('selected-right');
- element._cachedDiffBuilder.getSideByLineEl.returns('left');
- MockInteractions.down(element);
- assert.isTrue(
- element.classList.contains('selected-left'), 'adds selected-left');
- assert.isFalse(
- element.classList.contains('selected-right'),
- 'removes selected-right');
- });
-
- test('applies selected-right on right side click', () => {
- element.classList.add('selected-left');
- element._cachedDiffBuilder.getSideByLineEl.returns('right');
- MockInteractions.down(element);
- assert.isTrue(
- element.classList.contains('selected-right'), 'adds selected-right');
- assert.isFalse(
- element.classList.contains('selected-left'), 'removes selected-left');
- });
-
- test('applies selected-blame on blame click', () => {
- element.classList.add('selected-left');
- element.diffBuilder.getLineElByChild.returns(null);
- sandbox.stub(element, '_elementDescendedFromClass',
- (el, className) => className === 'blame');
- MockInteractions.down(element);
- assert.isTrue(
- element.classList.contains('selected-blame'), 'adds selected-right');
- assert.isFalse(
- element.classList.contains('selected-left'), 'removes selected-left');
- });
-
- test('ignores copy for non-content Element', () => {
- sandbox.stub(element, '_getSelectedText');
- emulateCopyOn(element.querySelector('.not-diff-row'));
- assert.isFalse(element._getSelectedText.called);
- });
-
- test('asks for text for left side Elements', () => {
- element._cachedDiffBuilder.getSideByLineEl.returns('left');
- sandbox.stub(element, '_getSelectedText');
- emulateCopyOn(element.querySelector('div.contentText'));
- assert.deepEqual(['left', false], element._getSelectedText.lastCall.args);
- });
-
- test('reacts to copy for content Elements', () => {
- sandbox.stub(element, '_getSelectedText');
- emulateCopyOn(element.querySelector('div.contentText'));
- assert.isTrue(element._getSelectedText.called);
- });
-
- test('copy event is prevented for content Elements', () => {
- sandbox.stub(element, '_getSelectedText');
- element._cachedDiffBuilder.getSideByLineEl.returns('left');
- element._getSelectedText.returns('test');
- const event = emulateCopyOn(element.querySelector('div.contentText'));
- assert.isTrue(event.preventDefault.called);
- });
-
- test('inserts text into clipboard on copy', () => {
- sandbox.stub(element, '_getSelectedText').returns('the text');
- const event = emulateCopyOn(element.querySelector('div.contentText'));
- assert.deepEqual(
- ['Text', 'the text'], event.clipboardData.setData.lastCall.args);
- });
-
- test('_setClasses adds given SelectionClass values, removes others', () => {
- element.classList.add('selected-right');
- element._setClasses(['selected-comment', 'selected-left']);
- assert.isTrue(element.classList.contains('selected-comment'));
- assert.isTrue(element.classList.contains('selected-left'));
- assert.isFalse(element.classList.contains('selected-right'));
- assert.isFalse(element.classList.contains('selected-blame'));
-
- element._setClasses(['selected-blame']);
- assert.isFalse(element.classList.contains('selected-comment'));
- assert.isFalse(element.classList.contains('selected-left'));
- assert.isFalse(element.classList.contains('selected-right'));
- assert.isTrue(element.classList.contains('selected-blame'));
- });
-
- test('_setClasses removes before it ads', () => {
- element.classList.add('selected-right');
- const addStub = sandbox.stub(element.classList, 'add');
- const removeStub = sandbox.stub(element.classList, 'remove', () => {
- assert.isFalse(addStub.called);
- });
- element._setClasses(['selected-comment', 'selected-left']);
- assert.isTrue(addStub.called);
- assert.isTrue(removeStub.called);
- });
-
- test('copies content correctly', () => {
- // Fetch the line number.
- element._cachedDiffBuilder.getLineElByChild = function(child) {
- while (!child.classList.contains('content') && child.parentElement) {
- child = child.parentElement;
- }
- return child.previousElementSibling;
- };
-
- element.classList.add('selected-left');
- element.classList.remove('selected-right');
-
- const selection = window.getSelection();
- selection.removeAllRanges();
- const range = document.createRange();
- range.setStart(element.querySelector('div.contentText').firstChild, 3);
- range.setEnd(
- element.querySelectorAll('div.contentText')[4].firstChild, 2);
- selection.addRange(range);
- assert.equal(element._getSelectedText('left'), 'ba\nzin\nga');
- });
-
- test('copies comments', () => {
element.classList.add('selected-left');
element.classList.add('selected-comment');
element.classList.remove('selected-right');
- const selection = window.getSelection();
+ selection = window.getSelection();
selection.removeAllRanges();
- const range = document.createRange();
- range.setStart(
- element.querySelector('.gr-formatted-text *').firstChild, 3);
- range.setEnd(
- element.querySelectorAll('.gr-formatted-text *')[2].childNodes[2], 7);
+ range = document.createRange();
+ nodes = element.querySelectorAll('.gr-formatted-text *');
+ });
+
+ test('multi level element contained in range', () => {
+ range.setStart(nodes[2].childNodes[0], 1);
+ range.setEnd(nodes[2].childNodes[2], 7);
selection.addRange(range);
- assert.equal('s is a comment\nThis is a differ',
- element._getSelectedText('left', true));
+ assert.equal(element._getTextContentForRange(element, selection, range),
+ 'his is a differ');
});
- test('respects astral chars in comments', () => {
- element.classList.add('selected-left');
- element.classList.add('selected-comment');
- element.classList.remove('selected-right');
- const selection = window.getSelection();
- selection.removeAllRanges();
- const range = document.createRange();
- const nodes = element.querySelectorAll('.gr-formatted-text *');
- range.setStart(nodes[2].childNodes[2], 13);
- range.setEnd(nodes[2].childNodes[2], 23);
+ test('multi level element as startContainer of range', () => {
+ range.setStart(nodes[2].childNodes[1], 0);
+ range.setEnd(nodes[2].childNodes[2], 7);
selection.addRange(range);
- assert.equal('mment 💩 u',
- element._getSelectedText('left', true));
+ assert.equal(element._getTextContentForRange(element, selection, range),
+ 'a differ');
});
- test('defers to default behavior for textarea', () => {
- element.classList.add('selected-left');
- element.classList.remove('selected-right');
- const selectedTextSpy = sandbox.spy(element, '_getSelectedText');
- emulateCopyOn(element.querySelector('textarea'));
- assert.isFalse(selectedTextSpy.called);
- });
-
- test('regression test for 4794', () => {
- element._cachedDiffBuilder.getLineElByChild = function(child) {
- while (!child.classList.contains('content') && child.parentElement) {
- child = child.parentElement;
- }
- return child.previousElementSibling;
- };
-
- element.classList.add('selected-right');
- element.classList.remove('selected-left');
-
- const selection = window.getSelection();
- selection.removeAllRanges();
- const range = document.createRange();
- range.setStart(
- element.querySelectorAll('div.contentText')[1].firstChild, 4);
- range.setEnd(
- element.querySelectorAll('div.contentText')[1].firstChild, 10);
+ test('startContainer === endContainer', () => {
+ range.setStart(nodes[0].firstChild, 2);
+ range.setEnd(nodes[0].firstChild, 12);
selection.addRange(range);
- assert.equal(element._getSelectedText('right'), ' other');
- });
-
- test('copies to end of side (issue 7895)', () => {
- element._cachedDiffBuilder.getLineElByChild = function(child) {
- // Return null for the end container.
- if (child.textContent === 'ga ga') { return null; }
- while (!child.classList.contains('content') && child.parentElement) {
- child = child.parentElement;
- }
- return child.previousElementSibling;
- };
- element.classList.add('selected-left');
- element.classList.remove('selected-right');
- const selection = window.getSelection();
- selection.removeAllRanges();
- const range = document.createRange();
- range.setStart(element.querySelector('div.contentText').firstChild, 3);
- range.setEnd(
- element.querySelectorAll('div.contentText')[4].firstChild, 2);
- selection.addRange(range);
- assert.equal(element._getSelectedText('left'), 'ba\nzin\nga');
- });
-
- suite('_getTextContentForRange', () => {
- let selection;
- let range;
- let nodes;
-
- setup(() => {
- element.classList.add('selected-left');
- element.classList.add('selected-comment');
- element.classList.remove('selected-right');
- selection = window.getSelection();
- selection.removeAllRanges();
- range = document.createRange();
- nodes = element.querySelectorAll('.gr-formatted-text *');
- });
-
- test('multi level element contained in range', () => {
- range.setStart(nodes[2].childNodes[0], 1);
- range.setEnd(nodes[2].childNodes[2], 7);
- selection.addRange(range);
- assert.equal(element._getTextContentForRange(element, selection, range),
- 'his is a differ');
- });
-
- test('multi level element as startContainer of range', () => {
- range.setStart(nodes[2].childNodes[1], 0);
- range.setEnd(nodes[2].childNodes[2], 7);
- selection.addRange(range);
- assert.equal(element._getTextContentForRange(element, selection, range),
- 'a differ');
- });
-
- test('startContainer === endContainer', () => {
- range.setStart(nodes[0].firstChild, 2);
- range.setEnd(nodes[0].firstChild, 12);
- selection.addRange(range);
- assert.equal(element._getTextContentForRange(element, selection, range),
- 'is is a co');
- });
- });
-
- test('cache is reset when diff changes', () => {
- element._linesCache = {left: 'test', right: 'test'};
- element.diff = {};
- flushAsynchronousOperations();
- assert.deepEqual(element._linesCache, {left: null, right: null});
+ assert.equal(element._getTextContentForRange(element, selection, range),
+ 'is is a co');
});
});
+
+ test('cache is reset when diff changes', () => {
+ element._linesCache = {left: 'test', right: 'test'};
+ element.diff = {};
+ flushAsynchronousOperations();
+ assert.deepEqual(element._linesCache, {left: null, right: null});
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
deleted file mode 100644
index 947ccbd..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ /dev/null
@@ -1,402 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-count-string-formatter/gr-count-string-formatter.html">
-<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
-<link rel="import" href="../../shared/gr-dropdown-list/gr-dropdown-list.html">
-<link rel="import" href="../../shared/gr-fixed-panel/gr-fixed-panel.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-<link rel="import" href="../../shared/revision-info/revision-info.html">
-<link rel="import" href="../gr-comment-api/gr-comment-api.html">
-<link rel="import" href="../gr-diff-cursor/gr-diff-cursor.html">
-<link rel="import" href="../gr-apply-fix-dialog/gr-apply-fix-dialog.html">
-<link rel="import" href="../gr-diff-host/gr-diff-host.html">
-<link rel="import" href="../gr-diff-mode-selector/gr-diff-mode-selector.html">
-<link rel="import" href="../gr-diff-preferences-dialog/gr-diff-preferences-dialog.html">
-<link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html">
-
-<dom-module id="gr-diff-view">
- <template>
- <style include="shared-styles">
- :host {
- background-color: var(--view-background-color);
- }
- .hidden {
- display: none;
- }
- gr-patch-range-select {
- display: block;
- }
- gr-diff {
- border: none;
- --diff-container-styles: {
- border-bottom: 1px solid var(--border-color);
- }
- }
- gr-fixed-panel {
- background-color: var(--view-background-color);
- border-bottom: 1px solid var(--border-color);
- z-index: 1;
- }
- header,
- .subHeader {
- align-items: center;
- display: flex;
- justify-content: space-between;
- }
- header {
- padding: var(--spacing-s) var(--spacing-xl);
- border-bottom: 1px solid var(--border-color);
- }
- .changeNumberColon {
- color: transparent;
- }
- .headerSubject {
- margin-right: var(--spacing-m);
- font-weight: var(--font-weight-bold);
- }
- .patchRangeLeft {
- align-items: center;
- display: flex;
- }
- .navLink:not([href]) {
- color: var(--deemphasized-text-color);
- }
- .navLinks {
- align-items: center;
- display: flex;
- white-space: nowrap;
- }
- .navLink {
- padding: 0 var(--spacing-xs);
- }
- .reviewed {
- display: inline-block;
- margin: 0 var(--spacing-xs);
- vertical-align: .15em;
- }
- .jumpToFileContainer {
- display: inline-block;
- }
- .mobile {
- display: none;
- }
- gr-button {
- padding: var(--spacing-s) 0;
- text-decoration: none;
- }
- .loading {
- color: var(--deemphasized-text-color);
- font-family: var(--header-font-family);
- font-size: var(--font-size-h1);
- font-weight: var(--font-weight-h1);
- line-height: var(--line-height-h1);
- height: 100%;
- padding: var(--spacing-l);
- text-align: center;
- }
- .subHeader {
- background-color: var(--background-color-secondary);
- flex-wrap: wrap;
- padding: 0 var(--spacing-l);
- }
- .prefsButton {
- text-align: right;
- }
- .noOverflow {
- display: block;
- overflow: auto;
- }
- .editMode .hideOnEdit {
- display: none;
- }
- .blameLoader,
- .fileNum {
- display: none;
- }
- .blameLoader.show,
- .fileNum.show ,
- .download,
- .preferences,
- .rightControls {
- align-items: center;
- display: flex;
- }
- .diffModeSelector,
- .editButton {
- align-items: center;
- display: flex;
- }
- .diffModeSelector span,
- .editButton span {
- margin-right: var(--spacing-xs);
- }
- .diffModeSelector.hide,
- .separator.hide {
- display: none;
- }
- gr-dropdown-list {
- --trigger-style: {
- text-transform: none;
- }
- }
- .editButtona a {
- text-decoration: none;
- }
- @media screen and (max-width: 50em) {
- header {
- padding: var(--spacing-s) var(--spacing-l);
- }
- .dash {
- display: none;
- }
- .desktop {
- display: none;
- }
- .fileNav {
- align-items: flex-start;
- display: flex;
- margin: 0 var(--spacing-xs);
- }
- .fullFileName {
- display: block;
- font-style: italic;
- min-width: 50%;
- padding: 0 var(--spacing-xxs);
- text-align: center;
- width: 100%;
- word-wrap: break-word;
- }
- .reviewed {
- vertical-align: -1px;
- }
- .mobileNavLink {
- color: var(--primary-text-color);
- font-family: var(--header-font-family);
- font-size: var(--font-size-h2);
- font-weight: var(--font-weight-h2);
- line-height: var(--line-height-h2);
- text-decoration: none;
- }
- .mobileNavLink:not([href]) {
- color: var(--deemphasized-text-color);
- }
- .jumpToFileContainer {
- display: block;
- width: 100%;
- }
- gr-dropdown-list {
- width: 100%;
- --gr-select-style: {
- display: block;
- width: 100%;
- }
- --native-select-style: {
- width: 100%;
- }
- }
- }
- </style>
- <gr-fixed-panel
- class$="[[_computeContainerClass(_editMode)]]"
- floating-disabled="[[_panelFloatingDisabled]]"
- keep-on-scroll
- ready-for-measure="[[!_loading]]"
- on-floating-height-changed="_onChangeHeaderPanelHeightChanged"
- >
- <header>
- <div>
- <a href$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]">[[_changeNum]]</a><!--
- --><span class="changeNumberColon">:</span>
- <span class="headerSubject">[[_change.subject]]</span>
- <input id="reviewed"
- class="reviewed hideOnEdit"
- type="checkbox"
- on-change="_handleReviewedChange"
- hidden$="[[!_loggedIn]]" hidden><!--
- --><div class="jumpToFileContainer">
- <gr-dropdown-list
- id="dropdown"
- value="[[_path]]"
- on-value-change="_handleFileChange"
- items="[[_formattedFiles]]"
- initial-count="75">
- </gr-dropdown-list>
- </div>
- </div>
- <div class="navLinks desktop">
- <span class$="fileNum [[_computeFileNumClass(_fileNum, _formattedFiles)]]">
- File [[_fileNum]] of [[_formattedFiles.length]]
- <span class="separator"></span>
- </span>
- <a class="navLink"
- title="[[createTitle(Shortcut.PREV_FILE,
- ShortcutSection.NAVIGATION)]]"
- href$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]">
- Prev</a>
- <span class="separator"></span>
- <a class="navLink"
- title="[[createTitle(Shortcut.UP_TO_CHANGE,
- ShortcutSection.NAVIGATION)]]"
- href$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]">
- Up</a>
- <span class="separator"></span>
- <a class="navLink"
- title="[[createTitle(Shortcut.NEXT_FILE,
- ShortcutSection.NAVIGATION)]]"
- href$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]">
- Next</a>
- </div>
- </header>
- <div class="subHeader">
- <div class="patchRangeLeft">
- <gr-patch-range-select
- id="rangeSelect"
- change-num="[[_changeNum]]"
- change-comments="[[_changeComments]]"
- patch-num="[[_patchRange.patchNum]]"
- base-patch-num="[[_patchRange.basePatchNum]]"
- files-weblinks="[[_filesWeblinks]]"
- available-patches="[[_allPatchSets]]"
- revisions="[[_change.revisions]]"
- revision-info="[[_revisionInfo]]"
- on-patch-range-change="_handlePatchChange">
- </gr-patch-range-select>
- <span class="download desktop">
- <span class="separator"></span>
- <gr-dropdown
- link
- down-arrow
- items="[[_computeDownloadDropdownLinks(_change.project, _changeNum, _patchRange, _path, _diff)]]"
- horizontal-align="left">
- <span class="downloadTitle">
- Download
- </span>
- </gr-dropdown>
- </span>
- </div>
- <div class="rightControls">
- <span class$="blameLoader [[_computeBlameLoaderClass(_isImageDiff, _path)]]">
- <gr-button
- link
- id='toggleBlame'
- title="[[createTitle(Shortcut.TOGGLE_BLAME, ShortcutSection.DIFFS)]]"
- disabled="[[_isBlameLoading]]"
- on-click="_toggleBlame">[[_computeBlameToggleLabel(_isBlameLoaded, _isBlameLoading)]]</gr-button>
- </span>
- <template is="dom-if" if="[[_computeIsLoggedIn(_loggedIn)]]">
- <span class="separator"></span>
- <span class="editButton">
- <gr-button
- link
- title="Edit current file"
- on-click="_goToEditFile">edit</gr-button>
- </span>
- </template>
- <span class="separator"></span>
- <div class$="diffModeSelector [[_computeModeSelectHideClass(_isImageDiff)]]">
- <span>Diff view:</span>
- <gr-diff-mode-selector
- id="modeSelect"
- save-on-change="[[!_diffPrefsDisabled]]"
- mode="{{changeViewState.diffMode}}"></gr-diff-mode-selector>
- </div>
- <span id="diffPrefsContainer"
- hidden$="[[_computePrefsButtonHidden(_prefs, _diffPrefsDisabled)]]" hidden>
- <span class="preferences desktop">
- <gr-button
- link
- class="prefsButton"
- has-tooltip
- title="Diff preferences"
- on-click="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button>
- </span>
- </span>
- <gr-endpoint-decorator name="annotation-toggler">
- <span hidden id="annotation-span">
- <label for="annotation-checkbox" id="annotation-label"></label>
- <iron-input type="checkbox" disabled>
- <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled>
- </iron-input>
- </span>
- </gr-endpoint-decorator>
- </div>
- </div>
- <div class="fileNav mobile">
- <a class="mobileNavLink"
- href$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]">
- <</a>
- <div class="fullFileName mobile">[[computeDisplayPath(_path)]]
- </div>
- <a class="mobileNavLink"
- href$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]">
- ></a>
- </div>
- </gr-fixed-panel>
- <div class="loading" hidden$="[[!_loading]]">Loading...</div>
- <gr-diff-host
- id="diffHost"
- hidden
- hidden$="[[_loading]]"
- class$="[[_computeDiffClass(_panelFloatingDisabled)]]"
- is-image-diff="{{_isImageDiff}}"
- files-weblinks="{{_filesWeblinks}}"
- diff="{{_diff}}"
- change-num="[[_changeNum]]"
- commit-range="[[_commitRange]]"
- patch-range="[[_patchRange]]"
- path="[[_path]]"
- prefs="[[_prefs]]"
- project-name="[[_change.project]]"
- view-mode="[[_diffMode]]"
- is-blame-loaded="{{_isBlameLoaded}}"
- on-comment-anchor-tap="_onLineSelected"
- on-line-selected="_onLineSelected">
- </gr-diff-host>
- <gr-apply-fix-dialog
- id="applyFixDialog"
- prefs="[[_prefs]]"
- change="[[_change]]"
- change-num="[[_changeNum]]">
- </gr-apply-fix-dialog>
- <gr-diff-preferences-dialog
- id="diffPreferencesDialog"
- diff-prefs="{{_prefs}}"
- on-reload-diff-preference="_handleReloadingDiffPreference">
- </gr-diff-preferences-dialog>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- <gr-storage id="storage"></gr-storage>
- <gr-diff-cursor id="cursor" scroll-top-margin="[[_scrollTopMargin]]"></gr-diff-cursor>
- <gr-comment-api id="commentAPI"></gr-comment-api>
- <gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="gr-diff-view.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index eb5ea017..94bc5b6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,1225 +14,1258 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
- const MSG_LOADING_BLAME = 'Loading blame...';
- const MSG_LOADED_BLAME = 'Blame loaded';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '@polymer/iron-dropdown/iron-dropdown.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
+import '../../shared/gr-dropdown/gr-dropdown.js';
+import '../../shared/gr-dropdown-list/gr-dropdown-list.js';
+import '../../shared/gr-fixed-panel/gr-fixed-panel.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.js';
+import '../../shared/revision-info/revision-info.js';
+import '../gr-comment-api/gr-comment-api.js';
+import '../gr-diff-cursor/gr-diff-cursor.js';
+import '../gr-apply-fix-dialog/gr-apply-fix-dialog.js';
+import '../gr-diff-host/gr-diff-host.js';
+import '../gr-diff-mode-selector/gr-diff-mode-selector.js';
+import '../gr-diff-preferences-dialog/gr-diff-preferences-dialog.js';
+import '../gr-patch-range-select/gr-patch-range-select.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-diff-view_html.js';
- const PARENT = 'PARENT';
+const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
+const MSG_LOADING_BLAME = 'Loading blame...';
+const MSG_LOADED_BLAME = 'Blame loaded';
- const DiffSides = {
- LEFT: 'left',
- RIGHT: 'right',
- };
+const PARENT = 'PARENT';
- const DiffViewMode = {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- };
+const DiffSides = {
+ LEFT: 'left',
+ RIGHT: 'right',
+};
+
+const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+};
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.PathListMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrDiffView extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.PathListBehavior,
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-diff-view'; }
+ /**
+ * Fired when the title of the page should change.
+ *
+ * @event title-change
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.PathListMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * Fired when user tries to navigate away while comments are pending save.
+ *
+ * @event show-alert
*/
- class GrDiffView extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.PathListBehavior,
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-diff-view'; }
- /**
- * Fired when the title of the page should change.
- *
- * @event title-change
- */
+ static get properties() {
+ return {
/**
- * Fired when user tries to navigate away while comments are pending save.
- *
- * @event show-alert
+ * URL params passed from the router.
*/
-
- static get properties() {
- return {
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
+ keyEventTarget: {
+ type: Object,
+ value() { return document.body; },
+ },
/**
- * URL params passed from the router.
+ * @type {{ diffMode: (string|undefined) }}
*/
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
- keyEventTarget: {
- type: Object,
- value() { return document.body; },
- },
- /**
- * @type {{ diffMode: (string|undefined) }}
- */
- changeViewState: {
- type: Object,
- notify: true,
- value() { return {}; },
- observer: '_changeViewStateChanged',
- },
- disableDiffPrefs: {
- type: Boolean,
- value: false,
- },
- _diffPrefsDisabled: {
- type: Boolean,
- computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)',
- },
- /** @type {?} */
- _patchRange: Object,
- /** @type {?} */
- _commitRange: Object,
- /**
- * @type {{
- * subject: string,
- * project: string,
- * revisions: string,
- * }}
- */
- _change: Object,
- /** @type {?} */
- _changeComments: Object,
- _changeNum: String,
- /**
- * This is a DiffInfo object.
- * This is retrieved and owned by a child component.
- */
- _diff: Object,
- // An array specifically formatted to be used in a gr-dropdown-list
- // element for selected a file to view.
- _formattedFiles: {
- type: Array,
- computed: '_formatFilesForDropdown(_files, ' +
- '_patchRange.patchNum, _changeComments)',
- },
- // An sorted array of files, as returned by the rest API.
- _fileList: {
- type: Array,
- computed: '_getSortedFileList(_files)',
- },
- /**
- * Contains information about files as returned by the rest API.
- *
- * @type {{ sortedFileList: Array<string>, changeFilesByPath: Object }}
- */
- _files: {
- type: Object,
- value() { return {sortedFileList: [], changeFilesByPath: {}}; },
- },
+ changeViewState: {
+ type: Object,
+ notify: true,
+ value() { return {}; },
+ observer: '_changeViewStateChanged',
+ },
+ disableDiffPrefs: {
+ type: Boolean,
+ value: false,
+ },
+ _diffPrefsDisabled: {
+ type: Boolean,
+ computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)',
+ },
+ /** @type {?} */
+ _patchRange: Object,
+ /** @type {?} */
+ _commitRange: Object,
+ /**
+ * @type {{
+ * subject: string,
+ * project: string,
+ * revisions: string,
+ * }}
+ */
+ _change: Object,
+ /** @type {?} */
+ _changeComments: Object,
+ _changeNum: String,
+ /**
+ * This is a DiffInfo object.
+ * This is retrieved and owned by a child component.
+ */
+ _diff: Object,
+ // An array specifically formatted to be used in a gr-dropdown-list
+ // element for selected a file to view.
+ _formattedFiles: {
+ type: Array,
+ computed: '_formatFilesForDropdown(_files, ' +
+ '_patchRange.patchNum, _changeComments)',
+ },
+ // An sorted array of files, as returned by the rest API.
+ _fileList: {
+ type: Array,
+ computed: '_getSortedFileList(_files)',
+ },
+ /**
+ * Contains information about files as returned by the rest API.
+ *
+ * @type {{ sortedFileList: Array<string>, changeFilesByPath: Object }}
+ */
+ _files: {
+ type: Object,
+ value() { return {sortedFileList: [], changeFilesByPath: {}}; },
+ },
- _path: {
- type: String,
- observer: '_pathChanged',
- },
- _fileNum: {
- type: Number,
- computed: '_computeFileNum(_path, _formattedFiles)',
- },
- _loggedIn: {
- type: Boolean,
- value: false,
- },
- _loading: {
- type: Boolean,
- value: true,
- },
- _prefs: Object,
- _localPrefs: Object,
- _projectConfig: Object,
- _userPrefs: Object,
- _diffMode: {
- type: String,
- computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)',
- },
- _isImageDiff: Boolean,
- _filesWeblinks: Object,
+ _path: {
+ type: String,
+ observer: '_pathChanged',
+ },
+ _fileNum: {
+ type: Number,
+ computed: '_computeFileNum(_path, _formattedFiles)',
+ },
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _prefs: Object,
+ _localPrefs: Object,
+ _projectConfig: Object,
+ _userPrefs: Object,
+ _diffMode: {
+ type: String,
+ computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)',
+ },
+ _isImageDiff: Boolean,
+ _filesWeblinks: Object,
- /**
- * Map of paths in the current change and patch range that have comments
- * or drafts or robot comments.
- */
- _commentMap: Object,
+ /**
+ * Map of paths in the current change and patch range that have comments
+ * or drafts or robot comments.
+ */
+ _commentMap: Object,
- _commentsForDiff: Object,
+ _commentsForDiff: Object,
- /**
- * Object to contain the path of the next and previous file in the current
- * change and patch range that has comments.
- */
- _commentSkips: {
- type: Object,
- computed: '_computeCommentSkips(_commentMap, _fileList, _path)',
- },
- _panelFloatingDisabled: {
- type: Boolean,
- value: () => window.PANEL_FLOATING_DISABLED,
- },
- _editMode: {
- type: Boolean,
- computed: '_computeEditMode(_patchRange.*)',
- },
- _isBlameLoaded: Boolean,
- _isBlameLoading: {
- type: Boolean,
- value: false,
- },
- _allPatchSets: {
- type: Array,
- computed: 'computeAllPatchSets(_change, _change.revisions.*)',
- },
- _revisionInfo: {
- type: Object,
- computed: '_getRevisionInfo(_change)',
- },
- _reviewedFiles: {
- type: Object,
- value: () => new Set(),
- },
+ /**
+ * Object to contain the path of the next and previous file in the current
+ * change and patch range that has comments.
+ */
+ _commentSkips: {
+ type: Object,
+ computed: '_computeCommentSkips(_commentMap, _fileList, _path)',
+ },
+ _panelFloatingDisabled: {
+ type: Boolean,
+ value: () => window.PANEL_FLOATING_DISABLED,
+ },
+ _editMode: {
+ type: Boolean,
+ computed: '_computeEditMode(_patchRange.*)',
+ },
+ _isBlameLoaded: Boolean,
+ _isBlameLoading: {
+ type: Boolean,
+ value: false,
+ },
+ _allPatchSets: {
+ type: Array,
+ computed: 'computeAllPatchSets(_change, _change.revisions.*)',
+ },
+ _revisionInfo: {
+ type: Object,
+ computed: '_getRevisionInfo(_change)',
+ },
+ _reviewedFiles: {
+ type: Object,
+ value: () => new Set(),
+ },
- /**
- * gr-diff-view has gr-fixed-panel on top. The panel can
- * intersect a main element and partially hides a content of
- * the main element. To correctly calculates visibility of an
- * element, the cursor must know how much height occuped by a fixed
- * panel.
- * The scrollTopMargin defines margin occuped by fixed panel.
- */
- _scrollTopMargin: {
- type: Number,
- value: 0,
- },
- };
- }
+ /**
+ * gr-diff-view has gr-fixed-panel on top. The panel can
+ * intersect a main element and partially hides a content of
+ * the main element. To correctly calculates visibility of an
+ * element, the cursor must know how much height occuped by a fixed
+ * panel.
+ * The scrollTopMargin defines margin occuped by fixed panel.
+ */
+ _scrollTopMargin: {
+ type: Number,
+ value: 0,
+ },
+ };
+ }
- static get observers() {
- return [
- '_getProjectConfig(_change.project)',
- '_getFiles(_changeNum, _patchRange.*, _changeComments)',
- '_setReviewedObserver(_loggedIn, params.*, _prefs)',
- ];
- }
+ static get observers() {
+ return [
+ '_getProjectConfig(_change.project)',
+ '_getFiles(_changeNum, _patchRange.*, _changeComments)',
+ '_setReviewedObserver(_loggedIn, params.*, _prefs)',
+ ];
+ }
- get keyBindings() {
- return {
- esc: '_handleEscKey',
- };
- }
+ get keyBindings() {
+ return {
+ esc: '_handleEscKey',
+ };
+ }
- keyboardShortcuts() {
- return {
- [this.Shortcut.LEFT_PANE]: '_handleLeftPane',
- [this.Shortcut.RIGHT_PANE]: '_handleRightPane',
- [this.Shortcut.NEXT_LINE]: '_handleNextLineOrFileWithComments',
- [this.Shortcut.PREV_LINE]: '_handlePrevLineOrFileWithComments',
- [this.Shortcut.VISIBLE_LINE]: '_handleVisibleLine',
- [this.Shortcut.NEXT_FILE_WITH_COMMENTS]:
- '_handleNextLineOrFileWithComments',
- [this.Shortcut.PREV_FILE_WITH_COMMENTS]:
- '_handlePrevLineOrFileWithComments',
- [this.Shortcut.NEW_COMMENT]: '_handleNewComment',
- [this.Shortcut.SAVE_COMMENT]: null, // DOC_ONLY binding
- [this.Shortcut.NEXT_FILE]: '_handleNextFile',
- [this.Shortcut.PREV_FILE]: '_handlePrevFile',
- [this.Shortcut.NEXT_CHUNK]: '_handleNextChunkOrCommentThread',
- [this.Shortcut.NEXT_COMMENT_THREAD]: '_handleNextChunkOrCommentThread',
- [this.Shortcut.PREV_CHUNK]: '_handlePrevChunkOrCommentThread',
- [this.Shortcut.PREV_COMMENT_THREAD]: '_handlePrevChunkOrCommentThread',
- [this.Shortcut.OPEN_REPLY_DIALOG]:
- '_handleOpenReplyDialogOrToggleLeftPane',
- [this.Shortcut.TOGGLE_LEFT_PANE]:
- '_handleOpenReplyDialogOrToggleLeftPane',
- [this.Shortcut.UP_TO_CHANGE]: '_handleUpToChange',
- [this.Shortcut.OPEN_DIFF_PREFS]: '_handleCommaKey',
- [this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
- [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
- [this.Shortcut.EXPAND_ALL_DIFF_CONTEXT]: '_handleExpandAllDiffContext',
- [this.Shortcut.NEXT_UNREVIEWED_FILE]: '_handleNextUnreviewedFile',
- [this.Shortcut.TOGGLE_BLAME]: '_handleToggleBlame',
+ keyboardShortcuts() {
+ return {
+ [this.Shortcut.LEFT_PANE]: '_handleLeftPane',
+ [this.Shortcut.RIGHT_PANE]: '_handleRightPane',
+ [this.Shortcut.NEXT_LINE]: '_handleNextLineOrFileWithComments',
+ [this.Shortcut.PREV_LINE]: '_handlePrevLineOrFileWithComments',
+ [this.Shortcut.VISIBLE_LINE]: '_handleVisibleLine',
+ [this.Shortcut.NEXT_FILE_WITH_COMMENTS]:
+ '_handleNextLineOrFileWithComments',
+ [this.Shortcut.PREV_FILE_WITH_COMMENTS]:
+ '_handlePrevLineOrFileWithComments',
+ [this.Shortcut.NEW_COMMENT]: '_handleNewComment',
+ [this.Shortcut.SAVE_COMMENT]: null, // DOC_ONLY binding
+ [this.Shortcut.NEXT_FILE]: '_handleNextFile',
+ [this.Shortcut.PREV_FILE]: '_handlePrevFile',
+ [this.Shortcut.NEXT_CHUNK]: '_handleNextChunkOrCommentThread',
+ [this.Shortcut.NEXT_COMMENT_THREAD]: '_handleNextChunkOrCommentThread',
+ [this.Shortcut.PREV_CHUNK]: '_handlePrevChunkOrCommentThread',
+ [this.Shortcut.PREV_COMMENT_THREAD]: '_handlePrevChunkOrCommentThread',
+ [this.Shortcut.OPEN_REPLY_DIALOG]:
+ '_handleOpenReplyDialogOrToggleLeftPane',
+ [this.Shortcut.TOGGLE_LEFT_PANE]:
+ '_handleOpenReplyDialogOrToggleLeftPane',
+ [this.Shortcut.UP_TO_CHANGE]: '_handleUpToChange',
+ [this.Shortcut.OPEN_DIFF_PREFS]: '_handleCommaKey',
+ [this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
+ [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
+ [this.Shortcut.EXPAND_ALL_DIFF_CONTEXT]: '_handleExpandAllDiffContext',
+ [this.Shortcut.NEXT_UNREVIEWED_FILE]: '_handleNextUnreviewedFile',
+ [this.Shortcut.TOGGLE_BLAME]: '_handleToggleBlame',
- // Final two are actually handled by gr-comment-thread.
- [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
- [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
- };
- }
+ // Final two are actually handled by gr-comment-thread.
+ [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
+ [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
+ };
+ }
- /** @override */
- attached() {
- super.attached();
- this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- });
+ /** @override */
+ attached() {
+ super.attached();
+ this._getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ });
- this.addEventListener('open-fix-preview',
- this._onOpenFixPreview.bind(this));
- this.$.cursor.push('diffs', this.$.diffHost);
- }
+ this.addEventListener('open-fix-preview',
+ this._onOpenFixPreview.bind(this));
+ this.$.cursor.push('diffs', this.$.diffHost);
+ }
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
- _getProjectConfig(project) {
- return this.$.restAPI.getProjectConfig(project).then(
- config => {
- this._projectConfig = config;
- });
- }
-
- _getChangeDetail(changeNum) {
- return this.$.restAPI.getDiffChangeDetail(changeNum).then(change => {
- this._change = change;
- return change;
- });
- }
-
- _getChangeEdit(changeNum) {
- return this.$.restAPI.getChangeEdit(this._changeNum);
- }
-
- _getSortedFileList(files) {
- return files.sortedFileList;
- }
-
- _getFiles(changeNum, patchRangeRecord, changeComments) {
- // Polymer 2: check for undefined
- if ([changeNum, patchRangeRecord, patchRangeRecord.base, changeComments]
- .some(arg => arg === undefined)) {
- return Promise.resolve();
- }
-
- const patchRange = patchRangeRecord.base;
- return this.$.restAPI.getChangeFiles(
- changeNum, patchRange).then(changeFiles => {
- if (!changeFiles) return;
- const commentedPaths = changeComments.getPaths(patchRange);
- const files = Object.assign({}, changeFiles);
- Object.keys(commentedPaths).forEach(commentedPath => {
- if (files.hasOwnProperty(commentedPath)) { return; }
- files[commentedPath] = {status: 'U'};
+ _getProjectConfig(project) {
+ return this.$.restAPI.getProjectConfig(project).then(
+ config => {
+ this._projectConfig = config;
});
- this._files = {
- sortedFileList: Object.keys(files).sort(this.specialFilePathCompare),
- changeFilesByPath: files,
- };
+ }
+
+ _getChangeDetail(changeNum) {
+ return this.$.restAPI.getDiffChangeDetail(changeNum).then(change => {
+ this._change = change;
+ return change;
+ });
+ }
+
+ _getChangeEdit(changeNum) {
+ return this.$.restAPI.getChangeEdit(this._changeNum);
+ }
+
+ _getSortedFileList(files) {
+ return files.sortedFileList;
+ }
+
+ _getFiles(changeNum, patchRangeRecord, changeComments) {
+ // Polymer 2: check for undefined
+ if ([changeNum, patchRangeRecord, patchRangeRecord.base, changeComments]
+ .some(arg => arg === undefined)) {
+ return Promise.resolve();
+ }
+
+ const patchRange = patchRangeRecord.base;
+ return this.$.restAPI.getChangeFiles(
+ changeNum, patchRange).then(changeFiles => {
+ if (!changeFiles) return;
+ const commentedPaths = changeComments.getPaths(patchRange);
+ const files = Object.assign({}, changeFiles);
+ Object.keys(commentedPaths).forEach(commentedPath => {
+ if (files.hasOwnProperty(commentedPath)) { return; }
+ files[commentedPath] = {status: 'U'};
});
+ this._files = {
+ sortedFileList: Object.keys(files).sort(this.specialFilePathCompare),
+ changeFilesByPath: files,
+ };
+ });
+ }
+
+ _getDiffPreferences() {
+ return this.$.restAPI.getDiffPreferences().then(prefs => {
+ this._prefs = prefs;
+ });
+ }
+
+ _getPreferences() {
+ return this.$.restAPI.getPreferences();
+ }
+
+ _getWindowWidth() {
+ return window.innerWidth;
+ }
+
+ _handleReviewedChange(e) {
+ this._setReviewed(dom(e).rootTarget.checked);
+ }
+
+ _setReviewed(reviewed) {
+ if (this._editMode) { return; }
+ this.$.reviewed.checked = reviewed;
+ this._saveReviewedState(reviewed).catch(err => {
+ this.fire('show-alert', {message: ERR_REVIEW_STATUS});
+ throw err;
+ });
+ }
+
+ _saveReviewedState(reviewed) {
+ return this.$.restAPI.saveFileReviewed(this._changeNum,
+ this._patchRange.patchNum, this._path, reviewed);
+ }
+
+ _handleToggleFileReviewed(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this._setReviewed(!this.$.reviewed.checked);
+ }
+
+ _handleEscKey(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this.$.diffHost.displayLine = false;
+ }
+
+ _handleLeftPane(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ this.$.cursor.moveLeft();
+ }
+
+ _handleRightPane(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ this.$.cursor.moveRight();
+ }
+
+ _handlePrevLineOrFileWithComments(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+ if (e.detail.keyboardEvent.shiftKey &&
+ e.detail.keyboardEvent.keyCode === 75) { // 'K'
+ this._moveToPreviousFileWithComment();
+ return;
}
+ if (this.modifierPressed(e)) { return; }
- _getDiffPreferences() {
- return this.$.restAPI.getDiffPreferences().then(prefs => {
- this._prefs = prefs;
- });
+ e.preventDefault();
+ this.$.diffHost.displayLine = true;
+ this.$.cursor.moveUp();
+ }
+
+ _handleVisibleLine(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ this.$.cursor.moveToVisibleArea();
+ }
+
+ _onOpenFixPreview(e) {
+ this.$.applyFixDialog.open(e);
+ }
+
+ _handleNextLineOrFileWithComments(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+ if (e.detail.keyboardEvent.shiftKey &&
+ e.detail.keyboardEvent.keyCode === 74) { // 'J'
+ this._moveToNextFileWithComment();
+ return;
}
+ if (this.modifierPressed(e)) { return; }
- _getPreferences() {
- return this.$.restAPI.getPreferences();
- }
+ e.preventDefault();
+ this.$.diffHost.displayLine = true;
+ this.$.cursor.moveDown();
+ }
- _getWindowWidth() {
- return window.innerWidth;
- }
+ _moveToPreviousFileWithComment() {
+ if (!this._commentSkips) { return; }
- _handleReviewedChange(e) {
- this._setReviewed(Polymer.dom(e).rootTarget.checked);
- }
-
- _setReviewed(reviewed) {
- if (this._editMode) { return; }
- this.$.reviewed.checked = reviewed;
- this._saveReviewedState(reviewed).catch(err => {
- this.fire('show-alert', {message: ERR_REVIEW_STATUS});
- throw err;
- });
- }
-
- _saveReviewedState(reviewed) {
- return this.$.restAPI.saveFileReviewed(this._changeNum,
- this._patchRange.patchNum, this._path, reviewed);
- }
-
- _handleToggleFileReviewed(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this._setReviewed(!this.$.reviewed.checked);
- }
-
- _handleEscKey(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.diffHost.displayLine = false;
- }
-
- _handleLeftPane(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- this.$.cursor.moveLeft();
- }
-
- _handleRightPane(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- this.$.cursor.moveRight();
- }
-
- _handlePrevLineOrFileWithComments(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
- if (e.detail.keyboardEvent.shiftKey &&
- e.detail.keyboardEvent.keyCode === 75) { // 'K'
- this._moveToPreviousFileWithComment();
- return;
- }
- if (this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.diffHost.displayLine = true;
- this.$.cursor.moveUp();
- }
-
- _handleVisibleLine(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- this.$.cursor.moveToVisibleArea();
- }
-
- _onOpenFixPreview(e) {
- this.$.applyFixDialog.open(e);
- }
-
- _handleNextLineOrFileWithComments(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
- if (e.detail.keyboardEvent.shiftKey &&
- e.detail.keyboardEvent.keyCode === 74) { // 'J'
- this._moveToNextFileWithComment();
- return;
- }
- if (this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.diffHost.displayLine = true;
- this.$.cursor.moveDown();
- }
-
- _moveToPreviousFileWithComment() {
- if (!this._commentSkips) { return; }
-
- // If there is no previous diff with comments, then return to the change
- // view.
- if (!this._commentSkips.previous) {
- this._navToChangeView();
- return;
- }
-
- Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.previous,
- this._patchRange.patchNum, this._patchRange.basePatchNum);
- }
-
- _moveToNextFileWithComment() {
- if (!this._commentSkips) { return; }
-
- // If there is no next diff with comments, then return to the change view.
- if (!this._commentSkips.next) {
- this._navToChangeView();
- return;
- }
-
- Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.next,
- this._patchRange.patchNum, this._patchRange.basePatchNum);
- }
-
- _handleNewComment(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
- e.preventDefault();
- this.$.cursor.createCommentInPlace();
- }
-
- _handlePrevFile(e) {
- // Check for meta key to avoid overriding native chrome shortcut.
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.getKeyboardEvent(e).metaKey) { return; }
-
- e.preventDefault();
- this._navToFile(this._path, this._fileList, -1);
- }
-
- _handleNextFile(e) {
- // Check for meta key to avoid overriding native chrome shortcut.
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.getKeyboardEvent(e).metaKey) { return; }
-
- e.preventDefault();
- this._navToFile(this._path, this._fileList, 1);
- }
-
- _handleNextChunkOrCommentThread(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- if (e.detail.keyboardEvent.shiftKey) {
- this.$.cursor.moveToNextCommentThread();
- } else {
- if (this.modifierPressed(e)) { return; }
- this.$.cursor.moveToNextChunk();
- }
- }
-
- _handlePrevChunkOrCommentThread(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- if (e.detail.keyboardEvent.shiftKey) {
- this.$.cursor.moveToPreviousCommentThread();
- } else {
- if (this.modifierPressed(e)) { return; }
- this.$.cursor.moveToPreviousChunk();
- }
- }
-
- _handleOpenReplyDialogOrToggleLeftPane(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- if (e.detail.keyboardEvent.shiftKey) { // Hide left diff.
- e.preventDefault();
- this.$.diffHost.toggleLeftDiff();
- return;
- }
-
- if (this.modifierPressed(e)) { return; }
-
- if (!this._loggedIn) { return; }
-
- this.set('changeViewState.showReplyDialog', true);
- e.preventDefault();
+ // If there is no previous diff with comments, then return to the change
+ // view.
+ if (!this._commentSkips.previous) {
this._navToChangeView();
+ return;
}
- _handleUpToChange(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
+ Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.previous,
+ this._patchRange.patchNum, this._patchRange.basePatchNum);
+ }
- e.preventDefault();
+ _moveToNextFileWithComment() {
+ if (!this._commentSkips) { return; }
+
+ // If there is no next diff with comments, then return to the change view.
+ if (!this._commentSkips.next) {
this._navToChangeView();
+ return;
}
- _handleCommaKey(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
- if (this._diffPrefsDisabled) { return; }
+ Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.next,
+ this._patchRange.patchNum, this._patchRange.basePatchNum);
+ }
+ _handleNewComment(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+ e.preventDefault();
+ this.$.cursor.createCommentInPlace();
+ }
+
+ _handlePrevFile(e) {
+ // Check for meta key to avoid overriding native chrome shortcut.
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.getKeyboardEvent(e).metaKey) { return; }
+
+ e.preventDefault();
+ this._navToFile(this._path, this._fileList, -1);
+ }
+
+ _handleNextFile(e) {
+ // Check for meta key to avoid overriding native chrome shortcut.
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.getKeyboardEvent(e).metaKey) { return; }
+
+ e.preventDefault();
+ this._navToFile(this._path, this._fileList, 1);
+ }
+
+ _handleNextChunkOrCommentThread(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ if (e.detail.keyboardEvent.shiftKey) {
+ this.$.cursor.moveToNextCommentThread();
+ } else {
+ if (this.modifierPressed(e)) { return; }
+ this.$.cursor.moveToNextChunk();
+ }
+ }
+
+ _handlePrevChunkOrCommentThread(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ if (e.detail.keyboardEvent.shiftKey) {
+ this.$.cursor.moveToPreviousCommentThread();
+ } else {
+ if (this.modifierPressed(e)) { return; }
+ this.$.cursor.moveToPreviousChunk();
+ }
+ }
+
+ _handleOpenReplyDialogOrToggleLeftPane(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ if (e.detail.keyboardEvent.shiftKey) { // Hide left diff.
e.preventDefault();
- this.$.diffPreferencesDialog.open();
+ this.$.diffHost.toggleLeftDiff();
+ return;
}
- _handleToggleDiffMode(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
+ if (this.modifierPressed(e)) { return; }
- e.preventDefault();
- if (this._getDiffViewMode() === DiffViewMode.SIDE_BY_SIDE) {
- this.$.modeSelect.setMode(DiffViewMode.UNIFIED);
- } else {
- this.$.modeSelect.setMode(DiffViewMode.SIDE_BY_SIDE);
- }
+ if (!this._loggedIn) { return; }
+
+ this.set('changeViewState.showReplyDialog', true);
+ e.preventDefault();
+ this._navToChangeView();
+ }
+
+ _handleUpToChange(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this._navToChangeView();
+ }
+
+ _handleCommaKey(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+ if (this._diffPrefsDisabled) { return; }
+
+ e.preventDefault();
+ this.$.diffPreferencesDialog.open();
+ }
+
+ _handleToggleDiffMode(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ if (this._getDiffViewMode() === DiffViewMode.SIDE_BY_SIDE) {
+ this.$.modeSelect.setMode(DiffViewMode.UNIFIED);
+ } else {
+ this.$.modeSelect.setMode(DiffViewMode.SIDE_BY_SIDE);
}
+ }
- _navToChangeView() {
- if (!this._changeNum || !this._patchRange.patchNum) { return; }
+ _navToChangeView() {
+ if (!this._changeNum || !this._patchRange.patchNum) { return; }
+ this._navigateToChange(
+ this._change,
+ this._patchRange,
+ this._change && this._change.revisions);
+ }
+
+ _navToFile(path, fileList, direction) {
+ const newPath = this._getNavLinkPath(path, fileList, direction);
+ if (!newPath) { return; }
+
+ if (newPath.up) {
this._navigateToChange(
this._change,
this._patchRange,
this._change && this._change.revisions);
+ return;
}
- _navToFile(path, fileList, direction) {
- const newPath = this._getNavLinkPath(path, fileList, direction);
- if (!newPath) { return; }
+ Gerrit.Nav.navigateToDiff(this._change, newPath.path,
+ this._patchRange.patchNum, this._patchRange.basePatchNum);
+ }
- if (newPath.up) {
- this._navigateToChange(
- this._change,
- this._patchRange,
- this._change && this._change.revisions);
- return;
- }
+ /**
+ * @param {?string} path The path of the current file being shown.
+ * @param {!Array<string>} fileList The list of files in this change and
+ * patch range.
+ * @param {number} direction Either 1 (next file) or -1 (prev file).
+ * @param {(number|boolean)} opt_noUp Whether to return to the change view
+ * when advancing the file goes outside the bounds of fileList.
+ *
+ * @return {?string} The next URL when proceeding in the specified
+ * direction.
+ */
+ _computeNavLinkURL(change, path, fileList, direction, opt_noUp) {
+ const newPath = this._getNavLinkPath(path, fileList, direction, opt_noUp);
+ if (!newPath) { return null; }
- Gerrit.Nav.navigateToDiff(this._change, newPath.path,
- this._patchRange.patchNum, this._patchRange.basePatchNum);
+ if (newPath.up) {
+ return this._getChangePath(
+ this._change,
+ this._patchRange,
+ this._change && this._change.revisions);
+ }
+ return this._getDiffUrl(this._change, this._patchRange, newPath.path);
+ }
+
+ _goToEditFile() {
+ // TODO(taoalpha): add a shortcut for editing
+ const editUrl = Gerrit.Nav.getEditUrlForDiff(
+ this._change, this._path, this._patchRange.patchNum);
+ return Gerrit.Nav.navigateToRelativeUrl(editUrl);
+ }
+
+ /**
+ * Gives an object representing the target of navigating either left or
+ * right through the change. The resulting object will have one of the
+ * following forms:
+ * * {path: "<target file path>"} - When another file path should be the
+ * result of the navigation.
+ * * {up: true} - When the result of navigating should go back to the
+ * change view.
+ * * null - When no navigation is possible for the given direction.
+ *
+ * @param {?string} path The path of the current file being shown.
+ * @param {!Array<string>} fileList The list of files in this change and
+ * patch range.
+ * @param {number} direction Either 1 (next file) or -1 (prev file).
+ * @param {?number|boolean=} opt_noUp Whether to return to the change view
+ * when advancing the file goes outside the bounds of fileList.
+ * @return {?Object}
+ */
+ _getNavLinkPath(path, fileList, direction, opt_noUp) {
+ if (!path || !fileList || fileList.length === 0) { return null; }
+
+ let idx = fileList.indexOf(path);
+ if (idx === -1) {
+ const file = direction > 0 ?
+ fileList[0] :
+ fileList[fileList.length - 1];
+ return {path: file};
}
- /**
- * @param {?string} path The path of the current file being shown.
- * @param {!Array<string>} fileList The list of files in this change and
- * patch range.
- * @param {number} direction Either 1 (next file) or -1 (prev file).
- * @param {(number|boolean)} opt_noUp Whether to return to the change view
- * when advancing the file goes outside the bounds of fileList.
- *
- * @return {?string} The next URL when proceeding in the specified
- * direction.
- */
- _computeNavLinkURL(change, path, fileList, direction, opt_noUp) {
- const newPath = this._getNavLinkPath(path, fileList, direction, opt_noUp);
- if (!newPath) { return null; }
-
- if (newPath.up) {
- return this._getChangePath(
- this._change,
- this._patchRange,
- this._change && this._change.revisions);
- }
- return this._getDiffUrl(this._change, this._patchRange, newPath.path);
+ idx += direction;
+ // Redirect to the change view if opt_noUp isn’t truthy and idx falls
+ // outside the bounds of [0, fileList.length).
+ if (idx < 0 || idx > fileList.length - 1) {
+ if (opt_noUp) { return null; }
+ return {up: true};
}
- _goToEditFile() {
- // TODO(taoalpha): add a shortcut for editing
- const editUrl = Gerrit.Nav.getEditUrlForDiff(
- this._change, this._path, this._patchRange.patchNum);
- return Gerrit.Nav.navigateToRelativeUrl(editUrl);
+ return {path: fileList[idx]};
+ }
+
+ _getReviewedFiles(changeNum, patchNum) {
+ return this.$.restAPI.getReviewedFiles(changeNum, patchNum)
+ .then(files => {
+ this._reviewedFiles = new Set(files);
+ return this._reviewedFiles;
+ });
+ }
+
+ _getReviewedStatus(editMode, changeNum, patchNum, path) {
+ if (editMode) { return Promise.resolve(false); }
+ return this._getReviewedFiles(changeNum, patchNum)
+ .then(files => files.has(path));
+ }
+
+ _paramsChanged(value) {
+ if (value.view !== Gerrit.Nav.View.DIFF) { return; }
+
+ if (value.changeNum && value.project) {
+ this.$.restAPI.setInProjectLookup(value.changeNum, value.project);
}
- /**
- * Gives an object representing the target of navigating either left or
- * right through the change. The resulting object will have one of the
- * following forms:
- * * {path: "<target file path>"} - When another file path should be the
- * result of the navigation.
- * * {up: true} - When the result of navigating should go back to the
- * change view.
- * * null - When no navigation is possible for the given direction.
- *
- * @param {?string} path The path of the current file being shown.
- * @param {!Array<string>} fileList The list of files in this change and
- * patch range.
- * @param {number} direction Either 1 (next file) or -1 (prev file).
- * @param {?number|boolean=} opt_noUp Whether to return to the change view
- * when advancing the file goes outside the bounds of fileList.
- * @return {?Object}
- */
- _getNavLinkPath(path, fileList, direction, opt_noUp) {
- if (!path || !fileList || fileList.length === 0) { return null; }
+ this.$.diffHost.lineOfInterest = this._getLineOfInterest(this.params);
+ this._initCursor(this.params);
- let idx = fileList.indexOf(path);
- if (idx === -1) {
- const file = direction > 0 ?
- fileList[0] :
- fileList[fileList.length - 1];
- return {path: file};
- }
+ this._changeNum = value.changeNum;
+ this._path = value.path;
+ this._patchRange = {
+ patchNum: value.patchNum,
+ basePatchNum: value.basePatchNum || PARENT,
+ };
- idx += direction;
- // Redirect to the change view if opt_noUp isn’t truthy and idx falls
- // outside the bounds of [0, fileList.length).
- if (idx < 0 || idx > fileList.length - 1) {
- if (opt_noUp) { return null; }
- return {up: true};
- }
+ // NOTE: This may be called before attachment (e.g. while parentElement is
+ // null). Fire title-change in an async so that, if attachment to the DOM
+ // has been queued, the event can bubble up to the handler in gr-app.
+ this.async(() => {
+ this.fire('title-change',
+ {title: this.computeTruncatedPath(this._path)});
+ });
- return {path: fileList[idx]};
+ // When navigating away from the page, there is a possibility that the
+ // patch number is no longer a part of the URL (say when navigating to
+ // the top-level change info view) and therefore undefined in `params`.
+ if (!this._patchRange.patchNum) {
+ return;
}
- _getReviewedFiles(changeNum, patchNum) {
- return this.$.restAPI.getReviewedFiles(changeNum, patchNum)
- .then(files => {
- this._reviewedFiles = new Set(files);
- return this._reviewedFiles;
- });
- }
+ const promises = [];
- _getReviewedStatus(editMode, changeNum, patchNum, path) {
- if (editMode) { return Promise.resolve(false); }
- return this._getReviewedFiles(changeNum, patchNum)
- .then(files => files.has(path));
- }
+ promises.push(this._getDiffPreferences());
- _paramsChanged(value) {
- if (value.view !== Gerrit.Nav.View.DIFF) { return; }
+ promises.push(this._getPreferences().then(prefs => {
+ this._userPrefs = prefs;
+ }));
- if (value.changeNum && value.project) {
- this.$.restAPI.setInProjectLookup(value.changeNum, value.project);
- }
-
- this.$.diffHost.lineOfInterest = this._getLineOfInterest(this.params);
- this._initCursor(this.params);
-
- this._changeNum = value.changeNum;
- this._path = value.path;
- this._patchRange = {
- patchNum: value.patchNum,
- basePatchNum: value.basePatchNum || PARENT,
- };
-
- // NOTE: This may be called before attachment (e.g. while parentElement is
- // null). Fire title-change in an async so that, if attachment to the DOM
- // has been queued, the event can bubble up to the handler in gr-app.
- this.async(() => {
- this.fire('title-change',
- {title: this.computeTruncatedPath(this._path)});
- });
-
- // When navigating away from the page, there is a possibility that the
- // patch number is no longer a part of the URL (say when navigating to
- // the top-level change info view) and therefore undefined in `params`.
- if (!this._patchRange.patchNum) {
- return;
- }
-
- const promises = [];
-
- promises.push(this._getDiffPreferences());
-
- promises.push(this._getPreferences().then(prefs => {
- this._userPrefs = prefs;
- }));
-
- promises.push(this._getChangeDetail(this._changeNum).then(change => {
- let commit;
- let baseCommit;
- if (change) {
- for (const commitSha in change.revisions) {
- if (!change.revisions.hasOwnProperty(commitSha)) continue;
- const revision = change.revisions[commitSha];
- const patchNum = revision._number.toString();
- if (patchNum === this._patchRange.patchNum) {
- commit = commitSha;
- const commitObj = revision.commit || {};
- const parents = commitObj.parents || [];
- if (this._patchRange.basePatchNum === PARENT && parents.length) {
- baseCommit = parents[parents.length - 1].commit;
- }
- } else if (patchNum === this._patchRange.basePatchNum) {
- baseCommit = commitSha;
+ promises.push(this._getChangeDetail(this._changeNum).then(change => {
+ let commit;
+ let baseCommit;
+ if (change) {
+ for (const commitSha in change.revisions) {
+ if (!change.revisions.hasOwnProperty(commitSha)) continue;
+ const revision = change.revisions[commitSha];
+ const patchNum = revision._number.toString();
+ if (patchNum === this._patchRange.patchNum) {
+ commit = commitSha;
+ const commitObj = revision.commit || {};
+ const parents = commitObj.parents || [];
+ if (this._patchRange.basePatchNum === PARENT && parents.length) {
+ baseCommit = parents[parents.length - 1].commit;
}
+ } else if (patchNum === this._patchRange.basePatchNum) {
+ baseCommit = commitSha;
}
- this._commitRange = {commit, baseCommit};
}
- }));
+ this._commitRange = {commit, baseCommit};
+ }
+ }));
- promises.push(this._loadComments());
+ promises.push(this._loadComments());
- promises.push(this._getChangeEdit(this._changeNum));
+ promises.push(this._getChangeEdit(this._changeNum));
- this._loading = true;
- return Promise.all(promises)
- .then(r => {
- const edit = r[4];
- if (edit) {
- this.set('_change.revisions.' + edit.commit.commit, {
- _number: this.EDIT_NAME,
- basePatchNum: edit.base_patch_set_number,
- commit: edit.commit,
- });
- }
- this._loading = false;
- this.$.diffHost.comments = this._commentsForDiff;
- return this.$.diffHost.reload(true);
- })
- .then(() => {
- this.$.reporting.diffViewFullyLoaded();
- // If diff view displayed has not ended yet, it ends here.
- this.$.reporting.diffViewDisplayed();
- });
- }
-
- _changeViewStateChanged(changeViewState) {
- if (changeViewState.diffMode === null) {
- // If screen size is small, always default to unified view.
- this.$.restAPI.getPreferences().then(prefs => {
- this.set('changeViewState.diffMode', prefs.default_diff_view);
+ this._loading = true;
+ return Promise.all(promises)
+ .then(r => {
+ const edit = r[4];
+ if (edit) {
+ this.set('_change.revisions.' + edit.commit.commit, {
+ _number: this.EDIT_NAME,
+ basePatchNum: edit.base_patch_set_number,
+ commit: edit.commit,
+ });
+ }
+ this._loading = false;
+ this.$.diffHost.comments = this._commentsForDiff;
+ return this.$.diffHost.reload(true);
+ })
+ .then(() => {
+ this.$.reporting.diffViewFullyLoaded();
+ // If diff view displayed has not ended yet, it ends here.
+ this.$.reporting.diffViewDisplayed();
});
- }
- }
+ }
- _setReviewedObserver(_loggedIn, paramsRecord, _prefs) {
- // Polymer 2: check for undefined
- if ([_loggedIn, paramsRecord, _prefs].some(arg => arg === undefined)) {
- return;
- }
-
- const params = paramsRecord.base || {};
- if (!_loggedIn) { return; }
-
- if (_prefs.manual_review) {
- // Checkbox state needs to be set explicitly only when manual_review
- // is specified.
- this._getReviewedStatus(this.editMode, this._changeNum,
- this._patchRange.patchNum, this._path).then(status => {
- this.$.reviewed.checked = status;
- });
- return;
- }
-
- if (params.view === Gerrit.Nav.View.DIFF) {
- this._setReviewed(true);
- }
- }
-
- /**
- * If the params specify a diff address then configure the diff cursor.
- */
- _initCursor(params) {
- if (params.lineNum === undefined) { return; }
- if (params.leftSide) {
- this.$.cursor.side = DiffSides.LEFT;
- } else {
- this.$.cursor.side = DiffSides.RIGHT;
- }
- this.$.cursor.initialLineNumber = params.lineNum;
- }
-
- _getLineOfInterest(params) {
- // If there is a line number specified, pass it along to the diff so that
- // it will not get collapsed.
- if (!params.lineNum) { return null; }
- return {number: params.lineNum, leftSide: params.leftSide};
- }
-
- _pathChanged(path) {
- if (path) {
- this.fire('title-change',
- {title: this.computeTruncatedPath(path)});
- }
-
- if (this._fileList.length == 0) { return; }
-
- this.set('changeViewState.selectedFileIndex',
- this._fileList.indexOf(path));
- }
-
- _getDiffUrl(change, patchRange, path) {
- if ([change, patchRange, path].some(arg => arg === undefined)) {
- return '';
- }
- return Gerrit.Nav.getUrlForDiff(change, path, patchRange.patchNum,
- patchRange.basePatchNum);
- }
-
- _patchRangeStr(patchRange) {
- let patchStr = patchRange.patchNum;
- if (patchRange.basePatchNum != null &&
- patchRange.basePatchNum != PARENT) {
- patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum;
- }
- return patchStr;
- }
-
- /**
- * When the latest patch of the change is selected (and there is no base
- * patch) then the patch range need not appear in the URL. Return a patch
- * range object with undefined values when a range is not needed.
- *
- * @param {!Object} patchRange
- * @param {!Object} revisions
- * @return {!Object}
- */
- _getChangeUrlRange(patchRange, revisions) {
- let patchNum = undefined;
- let basePatchNum = undefined;
- let latestPatchNum = -1;
- for (const rev of Object.values(revisions || {})) {
- latestPatchNum = Math.max(latestPatchNum, rev._number);
- }
- if (patchRange.basePatchNum !== PARENT ||
- parseInt(patchRange.patchNum, 10) !== latestPatchNum) {
- patchNum = patchRange.patchNum;
- basePatchNum = patchRange.basePatchNum;
- }
- return {patchNum, basePatchNum};
- }
-
- _getChangePath(change, patchRange, revisions) {
- if ([change, patchRange].some(arg => arg === undefined)) {
- return '';
- }
- const range = this._getChangeUrlRange(patchRange, revisions);
- return Gerrit.Nav.getUrlForChange(change, range.patchNum,
- range.basePatchNum);
- }
-
- _navigateToChange(change, patchRange, revisions) {
- const range = this._getChangeUrlRange(patchRange, revisions);
- Gerrit.Nav.navigateToChange(change, range.patchNum, range.basePatchNum);
- }
-
- _computeChangePath(change, patchRangeRecord, revisions) {
- return this._getChangePath(change, patchRangeRecord.base, revisions);
- }
-
- _formatFilesForDropdown(files, patchNum, changeComments) {
- // Polymer 2: check for undefined
- if ([
- files,
- patchNum,
- changeComments,
- ].some(arg => arg === undefined)) {
- return;
- }
-
- if (!files) { return; }
- const dropdownContent = [];
- for (const path of files.sortedFileList) {
- dropdownContent.push({
- text: this.computeDisplayPath(path),
- mobileText: this.computeTruncatedPath(path),
- value: path,
- bottomText: this._computeCommentString(changeComments, patchNum,
- path, files.changeFilesByPath[path]),
- });
- }
- return dropdownContent;
- }
-
- _computeCommentString(changeComments, patchNum, path, changeFileInfo) {
- const unresolvedCount = changeComments.computeUnresolvedNum(patchNum,
- path);
- const commentCount = changeComments.computeCommentCount(patchNum, path);
- const commentString = GrCountStringFormatter.computePluralString(
- commentCount, 'comment');
- const unresolvedString = GrCountStringFormatter.computeString(
- unresolvedCount, 'unresolved');
-
- const unmodifiedString = changeFileInfo.status === 'U' ? 'no changes': '';
-
- return [
- unmodifiedString,
- commentString,
- unresolvedString]
- .filter(v => v && v.length > 0).join(', ');
- }
-
- _computePrefsButtonHidden(prefs, prefsDisabled) {
- return prefsDisabled || !prefs;
- }
-
- _handleFileChange(e) {
- // This is when it gets set initially.
- const path = e.detail.value;
- if (path === this._path) {
- return;
- }
-
- Gerrit.Nav.navigateToDiff(this._change, path, this._patchRange.patchNum,
- this._patchRange.basePatchNum);
- }
-
- _handleFileTap(e) {
- // async is needed so that that the click event is fired before the
- // dropdown closes (This was a bug for touch devices).
- this.async(() => {
- this.$.dropdown.close();
- }, 1);
- }
-
- _handlePatchChange(e) {
- const {basePatchNum, patchNum} = e.detail;
- if (this.patchNumEquals(basePatchNum, this._patchRange.basePatchNum) &&
- this.patchNumEquals(patchNum, this._patchRange.patchNum)) { return; }
- Gerrit.Nav.navigateToDiff(
- this._change, this._path, patchNum, basePatchNum);
- }
-
- _handlePrefsTap(e) {
- e.preventDefault();
- this.$.diffPreferencesDialog.open();
- }
-
- /**
- * _getDiffViewMode: Get the diff view (side-by-side or unified) based on
- * the current state.
- *
- * The expected behavior is to use the mode specified in the user's
- * preferences unless they have manually chosen the alternative view or they
- * are on a mobile device. If the user navigates up to the change view, it
- * should clear this choice and revert to the preference the next time a
- * diff is viewed.
- *
- * Use side-by-side if the user is not logged in.
- *
- * @return {string}
- */
- _getDiffViewMode() {
- if (this.changeViewState.diffMode) {
- return this.changeViewState.diffMode;
- } else if (this._userPrefs) {
- this.set('changeViewState.diffMode', this._userPrefs.default_diff_view);
- return this._userPrefs.default_diff_view;
- } else {
- return 'SIDE_BY_SIDE';
- }
- }
-
- _computeModeSelectHideClass(isImageDiff) {
- return isImageDiff ? 'hide' : '';
- }
-
- _onLineSelected(e, detail) {
- this.$.cursor.moveToLineNumber(detail.number, detail.side);
- if (!this._change) { return; }
- const cursorAddress = this.$.cursor.getAddress();
- const number = cursorAddress ? cursorAddress.number : undefined;
- const leftSide = cursorAddress ? cursorAddress.leftSide : undefined;
- const url = Gerrit.Nav.getUrlForDiffById(this._changeNum,
- this._change.project, this._path, this._patchRange.patchNum,
- this._patchRange.basePatchNum, number, leftSide);
- history.replaceState(null, '', url);
- }
-
- _computeDownloadDropdownLinks(
- project, changeNum, patchRange, path, diff) {
- if (!patchRange || !patchRange.patchNum) { return []; }
-
- const links = [
- {
- url: this._computeDownloadPatchLink(
- project, changeNum, patchRange, path),
- name: 'Patch',
- },
- ];
-
- if (diff && diff.meta_a) {
- let leftPath = path;
- if (diff.change_type === 'RENAMED') {
- leftPath = diff.meta_a.name;
- }
- links.push(
- {
- url: this._computeDownloadFileLink(
- project, changeNum, patchRange, leftPath, true),
- name: 'Left Content',
- }
- );
- }
-
- if (diff && diff.meta_b) {
- links.push(
- {
- url: this._computeDownloadFileLink(
- project, changeNum, patchRange, path, false),
- name: 'Right Content',
- }
- );
- }
-
- return links;
- }
-
- _computeDownloadFileLink(
- project, changeNum, patchRange, path, isBase) {
- let patchNum = patchRange.patchNum;
-
- const comparedAgainsParent = patchRange.basePatchNum === 'PARENT';
-
- if (isBase && !comparedAgainsParent) {
- patchNum = patchRange.basePatchNum;
- }
-
- let url = this.changeBaseURL(project, changeNum, patchNum) +
- `/files/${encodeURIComponent(path)}/download`;
-
- if (isBase && comparedAgainsParent) {
- url += '?parent=1';
- }
-
- return url;
- }
-
- _computeDownloadPatchLink(project, changeNum, patchRange, path) {
- let url = this.changeBaseURL(project, changeNum, patchRange.patchNum);
- url += '/patch?zip&path=' + encodeURIComponent(path);
- return url;
- }
-
- _loadComments() {
- return this.$.commentAPI.loadAll(this._changeNum).then(comments => {
- this._changeComments = comments;
- this._commentMap = this._getPaths(this._patchRange);
-
- this._commentsForDiff = this._getCommentsForPath(this._path,
- this._patchRange, this._projectConfig);
+ _changeViewStateChanged(changeViewState) {
+ if (changeViewState.diffMode === null) {
+ // If screen size is small, always default to unified view.
+ this.$.restAPI.getPreferences().then(prefs => {
+ this.set('changeViewState.diffMode', prefs.default_diff_view);
});
}
-
- _getPaths(patchRange) {
- return this._changeComments.getPaths(patchRange);
- }
-
- _getCommentsForPath(path, patchRange, projectConfig) {
- return this._changeComments.getCommentsBySideForPath(path, patchRange,
- projectConfig);
- }
-
- _getDiffDrafts() {
- return this.$.restAPI.getDiffDrafts(this._changeNum);
- }
-
- _computeCommentSkips(commentMap, fileList, path) {
- // Polymer 2: check for undefined
- if ([
- commentMap,
- fileList,
- path,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const skips = {previous: null, next: null};
- if (!fileList.length) { return skips; }
- const pathIndex = fileList.indexOf(path);
-
- // Scan backward for the previous file.
- for (let i = pathIndex - 1; i >= 0; i--) {
- if (commentMap[fileList[i]]) {
- skips.previous = fileList[i];
- break;
- }
- }
-
- // Scan forward for the next file.
- for (let i = pathIndex + 1; i < fileList.length; i++) {
- if (commentMap[fileList[i]]) {
- skips.next = fileList[i];
- break;
- }
- }
-
- return skips;
- }
-
- _computeDiffClass(panelFloatingDisabled) {
- if (panelFloatingDisabled) {
- return 'noOverflow';
- }
- }
-
- /**
- * @param {!Object} patchRangeRecord
- */
- _computeEditMode(patchRangeRecord) {
- const patchRange = patchRangeRecord.base || {};
- return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME);
- }
-
- /**
- * @param {boolean} editMode
- */
- _computeContainerClass(editMode) {
- return editMode ? 'editMode' : '';
- }
-
- _computeBlameToggleLabel(loaded, loading) {
- if (loaded) { return 'Hide blame'; }
- return 'Show blame';
- }
-
- /**
- * Load and display blame information if it has not already been loaded.
- * Otherwise hide it.
- */
- _toggleBlame() {
- if (this._isBlameLoaded) {
- this.$.diffHost.clearBlame();
- return;
- }
-
- this._isBlameLoading = true;
- this.fire('show-alert', {message: MSG_LOADING_BLAME});
- this.$.diffHost.loadBlame()
- .then(() => {
- this._isBlameLoading = false;
- this.fire('show-alert', {message: MSG_LOADED_BLAME});
- })
- .catch(() => {
- this._isBlameLoading = false;
- });
- }
-
- _handleToggleBlame(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
- this._toggleBlame();
- }
-
- _computeBlameLoaderClass(isImageDiff, path) {
- return !this.isMagicPath(path) && !isImageDiff ? 'show' : '';
- }
-
- _getRevisionInfo(change) {
- return new Gerrit.RevisionInfo(change);
- }
-
- _computeFileNum(file, files) {
- // Polymer 2: check for undefined
- if ([file, files].some(arg => arg === undefined)) {
- return undefined;
- }
-
- return files.findIndex(({value}) => value === file) + 1;
- }
-
- /**
- * @param {number} fileNum
- * @param {!Array<string>} files
- * @return {string}
- */
- _computeFileNumClass(fileNum, files) {
- if (files && fileNum > 0) {
- return 'show';
- }
- return '';
- }
-
- _handleExpandAllDiffContext(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
- this.$.diffHost.expandAllContext();
- }
-
- _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) {
- return disableDiffPrefs || !loggedIn;
- }
-
- _handleNextUnreviewedFile(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
- this._setReviewed(true);
- // Ensure that the currently viewed file always appears in unreviewedFiles
- // so we resolve the right "next" file.
- const unreviewedFiles = this._fileList
- .filter(file =>
- (file === this._path || !this._reviewedFiles.has(file)));
- this._navToFile(this._path, unreviewedFiles, 1);
- }
-
- _handleReloadingDiffPreference() {
- this._getDiffPreferences();
- }
-
- _onChangeHeaderPanelHeightChanged(e) {
- this._scrollTopMargin = e.detail.value;
- }
-
- _computeIsLoggedIn(loggedIn) {
- return loggedIn ? true : false;
- }
}
- customElements.define(GrDiffView.is, GrDiffView);
-})();
+ _setReviewedObserver(_loggedIn, paramsRecord, _prefs) {
+ // Polymer 2: check for undefined
+ if ([_loggedIn, paramsRecord, _prefs].some(arg => arg === undefined)) {
+ return;
+ }
+
+ const params = paramsRecord.base || {};
+ if (!_loggedIn) { return; }
+
+ if (_prefs.manual_review) {
+ // Checkbox state needs to be set explicitly only when manual_review
+ // is specified.
+ this._getReviewedStatus(this.editMode, this._changeNum,
+ this._patchRange.patchNum, this._path).then(status => {
+ this.$.reviewed.checked = status;
+ });
+ return;
+ }
+
+ if (params.view === Gerrit.Nav.View.DIFF) {
+ this._setReviewed(true);
+ }
+ }
+
+ /**
+ * If the params specify a diff address then configure the diff cursor.
+ */
+ _initCursor(params) {
+ if (params.lineNum === undefined) { return; }
+ if (params.leftSide) {
+ this.$.cursor.side = DiffSides.LEFT;
+ } else {
+ this.$.cursor.side = DiffSides.RIGHT;
+ }
+ this.$.cursor.initialLineNumber = params.lineNum;
+ }
+
+ _getLineOfInterest(params) {
+ // If there is a line number specified, pass it along to the diff so that
+ // it will not get collapsed.
+ if (!params.lineNum) { return null; }
+ return {number: params.lineNum, leftSide: params.leftSide};
+ }
+
+ _pathChanged(path) {
+ if (path) {
+ this.fire('title-change',
+ {title: this.computeTruncatedPath(path)});
+ }
+
+ if (this._fileList.length == 0) { return; }
+
+ this.set('changeViewState.selectedFileIndex',
+ this._fileList.indexOf(path));
+ }
+
+ _getDiffUrl(change, patchRange, path) {
+ if ([change, patchRange, path].some(arg => arg === undefined)) {
+ return '';
+ }
+ return Gerrit.Nav.getUrlForDiff(change, path, patchRange.patchNum,
+ patchRange.basePatchNum);
+ }
+
+ _patchRangeStr(patchRange) {
+ let patchStr = patchRange.patchNum;
+ if (patchRange.basePatchNum != null &&
+ patchRange.basePatchNum != PARENT) {
+ patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum;
+ }
+ return patchStr;
+ }
+
+ /**
+ * When the latest patch of the change is selected (and there is no base
+ * patch) then the patch range need not appear in the URL. Return a patch
+ * range object with undefined values when a range is not needed.
+ *
+ * @param {!Object} patchRange
+ * @param {!Object} revisions
+ * @return {!Object}
+ */
+ _getChangeUrlRange(patchRange, revisions) {
+ let patchNum = undefined;
+ let basePatchNum = undefined;
+ let latestPatchNum = -1;
+ for (const rev of Object.values(revisions || {})) {
+ latestPatchNum = Math.max(latestPatchNum, rev._number);
+ }
+ if (patchRange.basePatchNum !== PARENT ||
+ parseInt(patchRange.patchNum, 10) !== latestPatchNum) {
+ patchNum = patchRange.patchNum;
+ basePatchNum = patchRange.basePatchNum;
+ }
+ return {patchNum, basePatchNum};
+ }
+
+ _getChangePath(change, patchRange, revisions) {
+ if ([change, patchRange].some(arg => arg === undefined)) {
+ return '';
+ }
+ const range = this._getChangeUrlRange(patchRange, revisions);
+ return Gerrit.Nav.getUrlForChange(change, range.patchNum,
+ range.basePatchNum);
+ }
+
+ _navigateToChange(change, patchRange, revisions) {
+ const range = this._getChangeUrlRange(patchRange, revisions);
+ Gerrit.Nav.navigateToChange(change, range.patchNum, range.basePatchNum);
+ }
+
+ _computeChangePath(change, patchRangeRecord, revisions) {
+ return this._getChangePath(change, patchRangeRecord.base, revisions);
+ }
+
+ _formatFilesForDropdown(files, patchNum, changeComments) {
+ // Polymer 2: check for undefined
+ if ([
+ files,
+ patchNum,
+ changeComments,
+ ].some(arg => arg === undefined)) {
+ return;
+ }
+
+ if (!files) { return; }
+ const dropdownContent = [];
+ for (const path of files.sortedFileList) {
+ dropdownContent.push({
+ text: this.computeDisplayPath(path),
+ mobileText: this.computeTruncatedPath(path),
+ value: path,
+ bottomText: this._computeCommentString(changeComments, patchNum,
+ path, files.changeFilesByPath[path]),
+ });
+ }
+ return dropdownContent;
+ }
+
+ _computeCommentString(changeComments, patchNum, path, changeFileInfo) {
+ const unresolvedCount = changeComments.computeUnresolvedNum({patchNum,
+ path});
+ const commentCount = changeComments.computeCommentCount({patchNum, path});
+ const commentString = GrCountStringFormatter.computePluralString(
+ commentCount, 'comment');
+ const unresolvedString = GrCountStringFormatter.computeString(
+ unresolvedCount, 'unresolved');
+
+ const unmodifiedString = changeFileInfo.status === 'U' ? 'no changes': '';
+
+ return [
+ unmodifiedString,
+ commentString,
+ unresolvedString]
+ .filter(v => v && v.length > 0).join(', ');
+ }
+
+ _computePrefsButtonHidden(prefs, prefsDisabled) {
+ return prefsDisabled || !prefs;
+ }
+
+ _handleFileChange(e) {
+ // This is when it gets set initially.
+ const path = e.detail.value;
+ if (path === this._path) {
+ return;
+ }
+
+ Gerrit.Nav.navigateToDiff(this._change, path, this._patchRange.patchNum,
+ this._patchRange.basePatchNum);
+ }
+
+ _handleFileTap(e) {
+ // async is needed so that that the click event is fired before the
+ // dropdown closes (This was a bug for touch devices).
+ this.async(() => {
+ this.$.dropdown.close();
+ }, 1);
+ }
+
+ _handlePatchChange(e) {
+ const {basePatchNum, patchNum} = e.detail;
+ if (this.patchNumEquals(basePatchNum, this._patchRange.basePatchNum) &&
+ this.patchNumEquals(patchNum, this._patchRange.patchNum)) { return; }
+ Gerrit.Nav.navigateToDiff(
+ this._change, this._path, patchNum, basePatchNum);
+ }
+
+ _handlePrefsTap(e) {
+ e.preventDefault();
+ this.$.diffPreferencesDialog.open();
+ }
+
+ /**
+ * _getDiffViewMode: Get the diff view (side-by-side or unified) based on
+ * the current state.
+ *
+ * The expected behavior is to use the mode specified in the user's
+ * preferences unless they have manually chosen the alternative view or they
+ * are on a mobile device. If the user navigates up to the change view, it
+ * should clear this choice and revert to the preference the next time a
+ * diff is viewed.
+ *
+ * Use side-by-side if the user is not logged in.
+ *
+ * @return {string}
+ */
+ _getDiffViewMode() {
+ if (this.changeViewState.diffMode) {
+ return this.changeViewState.diffMode;
+ } else if (this._userPrefs) {
+ this.set('changeViewState.diffMode', this._userPrefs.default_diff_view);
+ return this._userPrefs.default_diff_view;
+ } else {
+ return 'SIDE_BY_SIDE';
+ }
+ }
+
+ _computeModeSelectHideClass(isImageDiff) {
+ return isImageDiff ? 'hide' : '';
+ }
+
+ _onLineSelected(e, detail) {
+ this.$.cursor.moveToLineNumber(detail.number, detail.side);
+ if (!this._change) { return; }
+ const cursorAddress = this.$.cursor.getAddress();
+ const number = cursorAddress ? cursorAddress.number : undefined;
+ const leftSide = cursorAddress ? cursorAddress.leftSide : undefined;
+ const url = Gerrit.Nav.getUrlForDiffById(this._changeNum,
+ this._change.project, this._path, this._patchRange.patchNum,
+ this._patchRange.basePatchNum, number, leftSide);
+ history.replaceState(null, '', url);
+ }
+
+ _computeDownloadDropdownLinks(
+ project, changeNum, patchRange, path, diff) {
+ if (!patchRange || !patchRange.patchNum) { return []; }
+
+ const links = [
+ {
+ url: this._computeDownloadPatchLink(
+ project, changeNum, patchRange, path),
+ name: 'Patch',
+ },
+ ];
+
+ if (diff && diff.meta_a) {
+ let leftPath = path;
+ if (diff.change_type === 'RENAMED') {
+ leftPath = diff.meta_a.name;
+ }
+ links.push(
+ {
+ url: this._computeDownloadFileLink(
+ project, changeNum, patchRange, leftPath, true),
+ name: 'Left Content',
+ }
+ );
+ }
+
+ if (diff && diff.meta_b) {
+ links.push(
+ {
+ url: this._computeDownloadFileLink(
+ project, changeNum, patchRange, path, false),
+ name: 'Right Content',
+ }
+ );
+ }
+
+ return links;
+ }
+
+ _computeDownloadFileLink(
+ project, changeNum, patchRange, path, isBase) {
+ let patchNum = patchRange.patchNum;
+
+ const comparedAgainsParent = patchRange.basePatchNum === 'PARENT';
+
+ if (isBase && !comparedAgainsParent) {
+ patchNum = patchRange.basePatchNum;
+ }
+
+ let url = this.changeBaseURL(project, changeNum, patchNum) +
+ `/files/${encodeURIComponent(path)}/download`;
+
+ if (isBase && comparedAgainsParent) {
+ url += '?parent=1';
+ }
+
+ return url;
+ }
+
+ _computeDownloadPatchLink(project, changeNum, patchRange, path) {
+ let url = this.changeBaseURL(project, changeNum, patchRange.patchNum);
+ url += '/patch?zip&path=' + encodeURIComponent(path);
+ return url;
+ }
+
+ _loadComments() {
+ return this.$.commentAPI.loadAll(this._changeNum).then(comments => {
+ this._changeComments = comments;
+ this._commentMap = this._getPaths(this._patchRange);
+
+ this._commentsForDiff = this._getCommentsForPath(this._path,
+ this._patchRange, this._projectConfig);
+ });
+ }
+
+ _getPaths(patchRange) {
+ return this._changeComments.getPaths(patchRange);
+ }
+
+ _getCommentsForPath(path, patchRange, projectConfig) {
+ return this._changeComments.getCommentsBySideForPath(path, patchRange,
+ projectConfig);
+ }
+
+ _getDiffDrafts() {
+ return this.$.restAPI.getDiffDrafts(this._changeNum);
+ }
+
+ _computeCommentSkips(commentMap, fileList, path) {
+ // Polymer 2: check for undefined
+ if ([
+ commentMap,
+ fileList,
+ path,
+ ].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const skips = {previous: null, next: null};
+ if (!fileList.length) { return skips; }
+ const pathIndex = fileList.indexOf(path);
+
+ // Scan backward for the previous file.
+ for (let i = pathIndex - 1; i >= 0; i--) {
+ if (commentMap[fileList[i]]) {
+ skips.previous = fileList[i];
+ break;
+ }
+ }
+
+ // Scan forward for the next file.
+ for (let i = pathIndex + 1; i < fileList.length; i++) {
+ if (commentMap[fileList[i]]) {
+ skips.next = fileList[i];
+ break;
+ }
+ }
+
+ return skips;
+ }
+
+ _computeDiffClass(panelFloatingDisabled) {
+ if (panelFloatingDisabled) {
+ return 'noOverflow';
+ }
+ }
+
+ /**
+ * @param {!Object} patchRangeRecord
+ */
+ _computeEditMode(patchRangeRecord) {
+ const patchRange = patchRangeRecord.base || {};
+ return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME);
+ }
+
+ /**
+ * @param {boolean} editMode
+ */
+ _computeContainerClass(editMode) {
+ return editMode ? 'editMode' : '';
+ }
+
+ _computeBlameToggleLabel(loaded, loading) {
+ if (loaded) { return 'Hide blame'; }
+ return 'Show blame';
+ }
+
+ /**
+ * Load and display blame information if it has not already been loaded.
+ * Otherwise hide it.
+ */
+ _toggleBlame() {
+ if (this._isBlameLoaded) {
+ this.$.diffHost.clearBlame();
+ return;
+ }
+
+ this._isBlameLoading = true;
+ this.fire('show-alert', {message: MSG_LOADING_BLAME});
+ this.$.diffHost.loadBlame()
+ .then(() => {
+ this._isBlameLoading = false;
+ this.fire('show-alert', {message: MSG_LOADED_BLAME});
+ })
+ .catch(() => {
+ this._isBlameLoading = false;
+ });
+ }
+
+ _handleToggleBlame(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+ this._toggleBlame();
+ }
+
+ _computeBlameLoaderClass(isImageDiff, path) {
+ return !this.isMagicPath(path) && !isImageDiff ? 'show' : '';
+ }
+
+ _getRevisionInfo(change) {
+ return new Gerrit.RevisionInfo(change);
+ }
+
+ _computeFileNum(file, files) {
+ // Polymer 2: check for undefined
+ if ([file, files].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ return files.findIndex(({value}) => value === file) + 1;
+ }
+
+ /**
+ * @param {number} fileNum
+ * @param {!Array<string>} files
+ * @return {string}
+ */
+ _computeFileNumClass(fileNum, files) {
+ if (files && fileNum > 0) {
+ return 'show';
+ }
+ return '';
+ }
+
+ _handleExpandAllDiffContext(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+ this.$.diffHost.expandAllContext();
+ }
+
+ _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) {
+ return disableDiffPrefs || !loggedIn;
+ }
+
+ _handleNextUnreviewedFile(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+ this._setReviewed(true);
+ // Ensure that the currently viewed file always appears in unreviewedFiles
+ // so we resolve the right "next" file.
+ const unreviewedFiles = this._fileList
+ .filter(file =>
+ (file === this._path || !this._reviewedFiles.has(file)));
+ this._navToFile(this._path, unreviewedFiles, 1);
+ }
+
+ _handleReloadingDiffPreference() {
+ this._getDiffPreferences();
+ }
+
+ _onChangeHeaderPanelHeightChanged(e) {
+ this._scrollTopMargin = e.detail.value;
+ }
+
+ _computeIsLoggedIn(loggedIn) {
+ return loggedIn ? true : false;
+ }
+}
+
+customElements.define(GrDiffView.is, GrDiffView);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
new file mode 100644
index 0000000..cf4cf92
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
@@ -0,0 +1,294 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ background-color: var(--view-background-color);
+ }
+ .hidden {
+ display: none;
+ }
+ gr-patch-range-select {
+ display: block;
+ }
+ gr-diff {
+ border: none;
+ --diff-container-styles: {
+ border-bottom: 1px solid var(--border-color);
+ }
+ }
+ gr-fixed-panel {
+ background-color: var(--view-background-color);
+ border-bottom: 1px solid var(--border-color);
+ z-index: 1;
+ }
+ header,
+ .subHeader {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+ }
+ header {
+ padding: var(--spacing-s) var(--spacing-xl);
+ border-bottom: 1px solid var(--border-color);
+ }
+ .changeNumberColon {
+ color: transparent;
+ }
+ .headerSubject {
+ margin-right: var(--spacing-m);
+ font-weight: var(--font-weight-bold);
+ }
+ .patchRangeLeft {
+ align-items: center;
+ display: flex;
+ }
+ .navLink:not([href]) {
+ color: var(--deemphasized-text-color);
+ }
+ .navLinks {
+ align-items: center;
+ display: flex;
+ white-space: nowrap;
+ }
+ .navLink {
+ padding: 0 var(--spacing-xs);
+ }
+ .reviewed {
+ display: inline-block;
+ margin: 0 var(--spacing-xs);
+ vertical-align: .15em;
+ }
+ .jumpToFileContainer {
+ display: inline-block;
+ }
+ .mobile {
+ display: none;
+ }
+ gr-button {
+ padding: var(--spacing-s) 0;
+ text-decoration: none;
+ }
+ .loading {
+ color: var(--deemphasized-text-color);
+ font-family: var(--header-font-family);
+ font-size: var(--font-size-h1);
+ font-weight: var(--font-weight-h1);
+ line-height: var(--line-height-h1);
+ height: 100%;
+ padding: var(--spacing-l);
+ text-align: center;
+ }
+ .subHeader {
+ background-color: var(--background-color-secondary);
+ flex-wrap: wrap;
+ padding: 0 var(--spacing-l);
+ }
+ .prefsButton {
+ text-align: right;
+ }
+ .noOverflow {
+ display: block;
+ overflow: auto;
+ }
+ .editMode .hideOnEdit {
+ display: none;
+ }
+ .blameLoader,
+ .fileNum {
+ display: none;
+ }
+ .blameLoader.show,
+ .fileNum.show ,
+ .download,
+ .preferences,
+ .rightControls {
+ align-items: center;
+ display: flex;
+ }
+ .diffModeSelector,
+ .editButton {
+ align-items: center;
+ display: flex;
+ }
+ .diffModeSelector span,
+ .editButton span {
+ margin-right: var(--spacing-xs);
+ }
+ .diffModeSelector.hide,
+ .separator.hide {
+ display: none;
+ }
+ gr-dropdown-list {
+ --trigger-style: {
+ text-transform: none;
+ }
+ }
+ .editButtona a {
+ text-decoration: none;
+ }
+ @media screen and (max-width: 50em) {
+ header {
+ padding: var(--spacing-s) var(--spacing-l);
+ }
+ .dash {
+ display: none;
+ }
+ .desktop {
+ display: none;
+ }
+ .fileNav {
+ align-items: flex-start;
+ display: flex;
+ margin: 0 var(--spacing-xs);
+ }
+ .fullFileName {
+ display: block;
+ font-style: italic;
+ min-width: 50%;
+ padding: 0 var(--spacing-xxs);
+ text-align: center;
+ width: 100%;
+ word-wrap: break-word;
+ }
+ .reviewed {
+ vertical-align: -1px;
+ }
+ .mobileNavLink {
+ color: var(--primary-text-color);
+ font-family: var(--header-font-family);
+ font-size: var(--font-size-h2);
+ font-weight: var(--font-weight-h2);
+ line-height: var(--line-height-h2);
+ text-decoration: none;
+ }
+ .mobileNavLink:not([href]) {
+ color: var(--deemphasized-text-color);
+ }
+ .jumpToFileContainer {
+ display: block;
+ width: 100%;
+ }
+ gr-dropdown-list {
+ width: 100%;
+ --gr-select-style: {
+ display: block;
+ width: 100%;
+ }
+ --native-select-style: {
+ width: 100%;
+ }
+ }
+ }
+ </style>
+ <gr-fixed-panel class\$="[[_computeContainerClass(_editMode)]]" floating-disabled="[[_panelFloatingDisabled]]" keep-on-scroll="" ready-for-measure="[[!_loading]]" on-floating-height-changed="_onChangeHeaderPanelHeightChanged">
+ <header>
+ <div>
+ <a href\$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]">[[_changeNum]]</a><!--
+ --><span class="changeNumberColon">:</span>
+ <span class="headerSubject">[[_change.subject]]</span>
+ <input id="reviewed" class="reviewed hideOnEdit" type="checkbox" on-change="_handleReviewedChange" hidden\$="[[!_loggedIn]]" hidden=""><!--
+ --><div class="jumpToFileContainer">
+ <gr-dropdown-list id="dropdown" value="[[_path]]" on-value-change="_handleFileChange" items="[[_formattedFiles]]" initial-count="75">
+ </gr-dropdown-list>
+ </div>
+ </div>
+ <div class="navLinks desktop">
+ <span class\$="fileNum [[_computeFileNumClass(_fileNum, _formattedFiles)]]">
+ File [[_fileNum]] of [[_formattedFiles.length]]
+ <span class="separator"></span>
+ </span>
+ <a class="navLink" title="[[createTitle(Shortcut.PREV_FILE,
+ ShortcutSection.NAVIGATION)]]" href\$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]">
+ Prev</a>
+ <span class="separator"></span>
+ <a class="navLink" title="[[createTitle(Shortcut.UP_TO_CHANGE,
+ ShortcutSection.NAVIGATION)]]" href\$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]">
+ Up</a>
+ <span class="separator"></span>
+ <a class="navLink" title="[[createTitle(Shortcut.NEXT_FILE,
+ ShortcutSection.NAVIGATION)]]" href\$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]">
+ Next</a>
+ </div>
+ </header>
+ <div class="subHeader">
+ <div class="patchRangeLeft">
+ <gr-patch-range-select id="rangeSelect" change-num="[[_changeNum]]" change-comments="[[_changeComments]]" patch-num="[[_patchRange.patchNum]]" base-patch-num="[[_patchRange.basePatchNum]]" files-weblinks="[[_filesWeblinks]]" available-patches="[[_allPatchSets]]" revisions="[[_change.revisions]]" revision-info="[[_revisionInfo]]" on-patch-range-change="_handlePatchChange">
+ </gr-patch-range-select>
+ <span class="download desktop">
+ <span class="separator"></span>
+ <gr-dropdown link="" down-arrow="" items="[[_computeDownloadDropdownLinks(_change.project, _changeNum, _patchRange, _path, _diff)]]" horizontal-align="left">
+ <span class="downloadTitle">
+ Download
+ </span>
+ </gr-dropdown>
+ </span>
+ </div>
+ <div class="rightControls">
+ <span class\$="blameLoader [[_computeBlameLoaderClass(_isImageDiff, _path)]]">
+ <gr-button link="" id="toggleBlame" title="[[createTitle(Shortcut.TOGGLE_BLAME, ShortcutSection.DIFFS)]]" disabled="[[_isBlameLoading]]" on-click="_toggleBlame">[[_computeBlameToggleLabel(_isBlameLoaded, _isBlameLoading)]]</gr-button>
+ </span>
+ <template is="dom-if" if="[[_computeIsLoggedIn(_loggedIn)]]">
+ <span class="separator"></span>
+ <span class="editButton">
+ <gr-button link="" title="Edit current file" on-click="_goToEditFile">edit</gr-button>
+ </span>
+ </template>
+ <span class="separator"></span>
+ <div class\$="diffModeSelector [[_computeModeSelectHideClass(_isImageDiff)]]">
+ <span>Diff view:</span>
+ <gr-diff-mode-selector id="modeSelect" save-on-change="[[!_diffPrefsDisabled]]" mode="{{changeViewState.diffMode}}"></gr-diff-mode-selector>
+ </div>
+ <span id="diffPrefsContainer" hidden\$="[[_computePrefsButtonHidden(_prefs, _diffPrefsDisabled)]]" hidden="">
+ <span class="preferences desktop">
+ <gr-button link="" class="prefsButton" has-tooltip="" title="Diff preferences" on-click="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button>
+ </span>
+ </span>
+ <gr-endpoint-decorator name="annotation-toggler">
+ <span hidden="" id="annotation-span">
+ <label for="annotation-checkbox" id="annotation-label"></label>
+ <iron-input type="checkbox" disabled="">
+ <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled="">
+ </iron-input>
+ </span>
+ </gr-endpoint-decorator>
+ </div>
+ </div>
+ <div class="fileNav mobile">
+ <a class="mobileNavLink" href\$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]">
+ <</a>
+ <div class="fullFileName mobile">[[computeDisplayPath(_path)]]
+ </div>
+ <a class="mobileNavLink" href\$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]">
+ ></a>
+ </div>
+ </gr-fixed-panel>
+ <div class="loading" hidden\$="[[!_loading]]">Loading...</div>
+ <gr-diff-host id="diffHost" hidden="" hidden\$="[[_loading]]" class\$="[[_computeDiffClass(_panelFloatingDisabled)]]" is-image-diff="{{_isImageDiff}}" files-weblinks="{{_filesWeblinks}}" diff="{{_diff}}" change-num="[[_changeNum]]" commit-range="[[_commitRange]]" patch-range="[[_patchRange]]" path="[[_path]]" prefs="[[_prefs]]" project-name="[[_change.project]]" view-mode="[[_diffMode]]" is-blame-loaded="{{_isBlameLoaded}}" on-comment-anchor-tap="_onLineSelected" on-line-selected="_onLineSelected">
+ </gr-diff-host>
+ <gr-apply-fix-dialog id="applyFixDialog" prefs="[[_prefs]]" change="[[_change]]" change-num="[[_changeNum]]">
+ </gr-apply-fix-dialog>
+ <gr-diff-preferences-dialog id="diffPreferencesDialog" diff-prefs="{{_prefs}}" on-reload-diff-preference="_handleReloadingDiffPreference">
+ </gr-diff-preferences-dialog>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ <gr-storage id="storage"></gr-storage>
+ <gr-diff-cursor id="cursor" scroll-top-margin="[[_scrollTopMargin]]"></gr-diff-cursor>
+ <gr-comment-api id="commentAPI"></gr-comment-api>
+ <gr-reporting id="reporting"></gr-reporting>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index a992a6e..5dccd5e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -19,18 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-view</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="/bower_components/page/page.js"></script>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="gr-diff-view.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script src="/node_modules/page/page.js"></script>
<test-fixture id="basic">
<template>
@@ -44,102 +37,532 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff-view tests', async () => {
- await readyToTest();
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-diff-view.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-diff-view tests', () => {
+ suite('basic tests', () => {
+ const kb = window.Gerrit.KeyboardShortcutBinder;
+ kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
+ kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
+ kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
+ kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
+ kb.bindShortcut(kb.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
+ kb.bindShortcut(kb.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
+ kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
+ kb.bindShortcut(kb.Shortcut.SAVE_COMMENT, 'ctrl+s');
+ kb.bindShortcut(kb.Shortcut.NEXT_FILE, ']');
+ kb.bindShortcut(kb.Shortcut.PREV_FILE, '[');
+ kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
+ kb.bindShortcut(kb.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
+ kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
+ kb.bindShortcut(kb.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
+ kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
+ kb.bindShortcut(kb.Shortcut.UP_TO_CHANGE, 'u');
+ kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
+ kb.bindShortcut(kb.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
+ kb.bindShortcut(kb.Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e');
+ kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e');
+ kb.bindShortcut(kb.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_BLAME, 'b');
- suite('basic tests', async () => {
- const kb = window.Gerrit.KeyboardShortcutBinder;
- kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
- kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
- kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
- kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
- kb.bindShortcut(kb.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
- kb.bindShortcut(kb.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
- kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
- kb.bindShortcut(kb.Shortcut.SAVE_COMMENT, 'ctrl+s');
- kb.bindShortcut(kb.Shortcut.NEXT_FILE, ']');
- kb.bindShortcut(kb.Shortcut.PREV_FILE, '[');
- kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
- kb.bindShortcut(kb.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
- kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
- kb.bindShortcut(kb.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
- kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
- kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
- kb.bindShortcut(kb.Shortcut.UP_TO_CHANGE, 'u');
- kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
- kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
- kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
- kb.bindShortcut(kb.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
- kb.bindShortcut(kb.Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e');
- kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e');
- kb.bindShortcut(kb.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
- kb.bindShortcut(kb.Shortcut.TOGGLE_BLAME, 'b');
+ let element;
+ let sandbox;
- let element;
- let sandbox;
+ const PARENT = 'PARENT';
- const PARENT = 'PARENT';
+ function getFilesFromFileList(fileList) {
+ const changeFilesByPath = fileList.reduce((files, path) => {
+ files[path] = {};
+ return files;
+ }, {});
+ return {
+ sortedFileList: fileList,
+ changeFilesByPath,
+ };
+ }
- function getFilesFromFileList(fileList) {
- const changeFilesByPath = fileList.reduce((files, path) => {
- files[path] = {};
- return files;
- }, {});
- return {
- sortedFileList: fileList,
- changeFilesByPath,
- };
- }
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({change: {}}); },
+ getLoggedIn() { return Promise.resolve(false); },
+ getProjectConfig() { return Promise.resolve({}); },
+ getDiffChangeDetail() { return Promise.resolve({}); },
+ getChangeFiles() { return Promise.resolve({}); },
+ saveFileReviewed() { return Promise.resolve(); },
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ getReviewedFiles() { return Promise.resolve([]); },
+ });
+ element = fixture('basic');
+ return element._loadComments();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('params change triggers diffViewDisplayed()', () => {
+ sandbox.stub(element.$.reporting, 'diffViewDisplayed');
+ sandbox.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
+ sandbox.spy(element, '_paramsChanged');
+ element.params = {
+ view: Gerrit.Nav.View.DIFF,
+ changeNum: '42',
+ patchNum: '2',
+ basePatchNum: '1',
+ path: '/COMMIT_MSG',
+ };
+
+ return element._paramsChanged.returnValues[0].then(() => {
+ assert.isTrue(element.$.reporting.diffViewDisplayed.calledOnce);
+ });
+ });
+
+ test('toggle left diff with a hotkey', () => {
+ const toggleLeftDiffStub = sandbox.stub(
+ element.$.diffHost, 'toggleLeftDiff');
+ MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
+ assert.isTrue(toggleLeftDiffStub.calledOnce);
+ });
+
+ test('keyboard shortcuts', () => {
+ element._changeNum = '42';
+ element._patchRange = {
+ basePatchNum: PARENT,
+ patchNum: '10',
+ };
+ element._change = {
+ _number: 42,
+ revisions: {
+ a: {_number: 10, commit: {parents: []}},
+ },
+ };
+ element._files = getFilesFromFileList(
+ ['chell.go', 'glados.txt', 'wheatley.md']);
+ element._path = 'glados.txt';
+ element.changeViewState.selectedFileIndex = 1;
+ element._loggedIn = true;
+
+ const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+ const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
+ assert(changeNavStub.lastCall.calledWith(element._change),
+ 'Should navigate to /c/42/');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
+ assert(diffNavStub.lastCall.calledWith(element._change, 'wheatley.md',
+ '10', PARENT), 'Should navigate to /c/42/10/wheatley.md');
+ element._path = 'wheatley.md';
+ assert.equal(element.changeViewState.selectedFileIndex, 2);
+ assert.isTrue(element._loading);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert(diffNavStub.lastCall.calledWith(element._change, 'glados.txt',
+ '10', PARENT), 'Should navigate to /c/42/10/glados.txt');
+ element._path = 'glados.txt';
+ assert.equal(element.changeViewState.selectedFileIndex, 1);
+ assert.isTrue(element._loading);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert(diffNavStub.lastCall.calledWith(element._change, 'chell.go', '10',
+ PARENT), 'Should navigate to /c/42/10/chell.go');
+ element._path = 'chell.go';
+ assert.equal(element.changeViewState.selectedFileIndex, 0);
+ assert.isTrue(element._loading);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert(changeNavStub.lastCall.calledWith(element._change),
+ 'Should navigate to /c/42/');
+ assert.equal(element.changeViewState.selectedFileIndex, 0);
+ assert.isTrue(element._loading);
+
+ const showPrefsStub =
+ sandbox.stub(element.$.diffPreferencesDialog, 'open',
+ () => Promise.resolve());
+
+ MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
+ assert(showPrefsStub.calledOnce);
+
+ element.disableDiffPrefs = true;
+ MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
+ assert(showPrefsStub.calledOnce);
+
+ let scrollStub = sandbox.stub(element.$.cursor, 'moveToNextChunk');
+ MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
+ assert(scrollStub.calledOnce);
+
+ scrollStub = sandbox.stub(element.$.cursor, 'moveToPreviousChunk');
+ MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
+ assert(scrollStub.calledOnce);
+
+ scrollStub = sandbox.stub(element.$.cursor, 'moveToNextCommentThread');
+ MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
+ assert(scrollStub.calledOnce);
+
+ scrollStub = sandbox.stub(element.$.cursor,
+ 'moveToPreviousCommentThread');
+ MockInteractions.pressAndReleaseKeyOn(element, 80, 'shift', 'p');
+ assert(scrollStub.calledOnce);
+
+ const computeContainerClassStub = sandbox.stub(element.$.diffHost.$.diff,
+ '_computeContainerClass');
+ MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
+ assert(computeContainerClassStub.lastCall.calledWithExactly(
+ false, 'SIDE_BY_SIDE', true));
+
+ MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc');
+ assert(computeContainerClassStub.lastCall.calledWithExactly(
+ false, 'SIDE_BY_SIDE', false));
+
+ sandbox.stub(element, '_setReviewed');
+ element.$.reviewed.checked = false;
+ MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
+ assert.isFalse(element._setReviewed.called);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ assert.isTrue(element._setReviewed.called);
+ assert.equal(element._setReviewed.lastCall.args[0], true);
+ });
+
+ test('shift+x shortcut expands all diff context', () => {
+ const expandStub = sandbox.stub(element.$.diffHost, 'expandAllContext');
+ MockInteractions.pressAndReleaseKeyOn(element, 88, 'shift', 'x');
+ flushAsynchronousOperations();
+ assert.isTrue(expandStub.called);
+ });
+
+ test('keyboard shortcuts with patch range', () => {
+ element._changeNum = '42';
+ element._patchRange = {
+ basePatchNum: '5',
+ patchNum: '10',
+ };
+ element._change = {
+ _number: 42,
+ revisions: {
+ a: {_number: 10, commit: {parents: []}},
+ b: {_number: 5, commit: {parents: []}},
+ },
+ };
+ element._files = getFilesFromFileList(
+ ['chell.go', 'glados.txt', 'wheatley.md']);
+ element._path = 'glados.txt';
+
+ const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+ const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' +
+ 'should only work when the user is logged in.');
+ assert.isNull(window.sessionStorage.getItem(
+ 'changeView.showReplyDialog'));
+
+ element._loggedIn = true;
+ MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ assert.isTrue(element.changeViewState.showReplyDialog);
+
+ assert(changeNavStub.lastCall.calledWithExactly(element._change, '10',
+ '5'), 'Should navigate to /c/42/5..10');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
+ assert(changeNavStub.lastCall.calledWithExactly(element._change, '10',
+ '5'), 'Should navigate to /c/42/5..10');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
+ assert.isTrue(element._loading);
+ assert(diffNavStub.lastCall.calledWithExactly(element._change,
+ 'wheatley.md', '10', '5'),
+ 'Should navigate to /c/42/5..10/wheatley.md');
+ element._path = 'wheatley.md';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert.isTrue(element._loading);
+ assert(diffNavStub.lastCall.calledWithExactly(element._change,
+ 'glados.txt', '10', '5'),
+ 'Should navigate to /c/42/5..10/glados.txt');
+ element._path = 'glados.txt';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert.isTrue(element._loading);
+ assert(diffNavStub.lastCall.calledWithExactly(
+ element._change,
+ 'chell.go',
+ '10',
+ '5'),
+ 'Should navigate to /c/42/5..10/chell.go');
+ element._path = 'chell.go';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert.isTrue(element._loading);
+ assert(changeNavStub.lastCall.calledWithExactly(element._change, '10',
+ '5'),
+ 'Should navigate to /c/42/5..10');
+ });
+
+ test('keyboard shortcuts with old patch number', () => {
+ element._changeNum = '42';
+ element._patchRange = {
+ basePatchNum: PARENT,
+ patchNum: '1',
+ };
+ element._change = {
+ _number: 42,
+ revisions: {
+ a: {_number: 1, commit: {parents: []}},
+ b: {_number: 2, commit: {parents: []}},
+ },
+ };
+ element._files = getFilesFromFileList(
+ ['chell.go', 'glados.txt', 'wheatley.md']);
+ element._path = 'glados.txt';
+
+ const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+ const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' +
+ 'should only work when the user is logged in.');
+ assert.isNull(window.sessionStorage.getItem(
+ 'changeView.showReplyDialog'));
+
+ element._loggedIn = true;
+ MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ assert.isTrue(element.changeViewState.showReplyDialog);
+
+ assert(changeNavStub.lastCall.calledWithExactly(element._change, '1',
+ PARENT), 'Should navigate to /c/42/1');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
+ assert(changeNavStub.lastCall.calledWithExactly(element._change, '1',
+ PARENT), 'Should navigate to /c/42/1');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
+ assert(diffNavStub.lastCall.calledWithExactly(element._change,
+ 'wheatley.md', '1', PARENT),
+ 'Should navigate to /c/42/1/wheatley.md');
+ element._path = 'wheatley.md';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert(diffNavStub.lastCall.calledWithExactly(element._change,
+ 'glados.txt', '1', PARENT),
+ 'Should navigate to /c/42/1/glados.txt');
+ element._path = 'glados.txt';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert(diffNavStub.lastCall.calledWithExactly(
+ element._change,
+ 'chell.go',
+ '1',
+ PARENT), 'Should navigate to /c/42/1/chell.go');
+ element._path = 'chell.go';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert(changeNavStub.lastCall.calledWithExactly(element._change, '1',
+ PARENT), 'Should navigate to /c/42/1');
+ });
+
+ test('edit should redirect to edit page', done => {
+ element._loggedIn = true;
+ element._path = 't.txt';
+ element._patchRange = {
+ basePatchNum: PARENT,
+ patchNum: '1',
+ };
+ element._change = {
+ _number: 42,
+ revisions: {
+ a: {_number: 1, commit: {parents: []}},
+ b: {_number: 2, commit: {parents: []}},
+ },
+ };
+ const redirectStub = sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
+ flush(() => {
+ const editBtn = element.shadowRoot
+ .querySelector('.editButton gr-button');
+ assert.isTrue(!!editBtn);
+ MockInteractions.tap(editBtn);
+ assert.isTrue(redirectStub.called);
+ done();
+ });
+ });
+
+ test('edit hidden when not logged in', done => {
+ element._loggedIn = false;
+ element._path = 't.txt';
+ element._patchRange = {
+ basePatchNum: PARENT,
+ patchNum: '1',
+ };
+ element._change = {
+ _number: 42,
+ revisions: {
+ a: {_number: 1, commit: {parents: []}},
+ b: {_number: 2, commit: {parents: []}},
+ },
+ };
+ flush(() => {
+ const editBtn = element.shadowRoot
+ .querySelector('.editButton gr-button');
+ assert.isFalse(!!editBtn);
+ done();
+ });
+ });
+
+ suite('diff prefs hidden', () => {
+ test('when no prefs or logged out', () => {
+ element.disableDiffPrefs = false;
+ element._loggedIn = false;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element._loggedIn = true;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element._loggedIn = false;
+ element._prefs = {font_size: '12'};
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element._loggedIn = true;
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.diffPrefsContainer.hidden);
+ });
+
+ test('when disableDiffPrefs is set', () => {
+ element._loggedIn = true;
+ element._prefs = {font_size: '12'};
+ element.disableDiffPrefs = false;
+ flushAsynchronousOperations();
+
+ assert.isFalse(element.$.diffPrefsContainer.hidden);
+ element.disableDiffPrefs = true;
+ flushAsynchronousOperations();
+
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+ });
+ });
+
+ test('prefsButton opens gr-diff-preferences', () => {
+ const handlePrefsTapSpy = sandbox.spy(element, '_handlePrefsTap');
+ const overlayOpenStub = sandbox.stub(element.$.diffPreferencesDialog,
+ 'open');
+ const prefsButton =
+ dom(element.root).querySelector('.prefsButton');
+
+ MockInteractions.tap(prefsButton);
+
+ assert.isTrue(handlePrefsTapSpy.called);
+ assert.isTrue(overlayOpenStub.called);
+ });
+
+ test('_computeCommentString', done => {
+ const path = '/test';
+ element.$.commentAPI.loadAll().then(comments => {
+ const commentCountStub =
+ sandbox.stub(comments, 'computeCommentCount');
+ const unresolvedCountStub =
+ sandbox.stub(comments, 'computeUnresolvedNum');
+ commentCountStub.withArgs({patchNum: 1, path}).returns(0);
+ commentCountStub.withArgs({patchNum: 2, path}).returns(1);
+ commentCountStub.withArgs({patchNum: 3, path}).returns(2);
+ commentCountStub.withArgs({patchNum: 4, path}).returns(0);
+ unresolvedCountStub.withArgs({patchNum: 1, path}).returns(1);
+ unresolvedCountStub.withArgs({patchNum: 2, path}).returns(0);
+ unresolvedCountStub.withArgs({patchNum: 3, path}).returns(2);
+ unresolvedCountStub.withArgs({patchNum: 4, path}).returns(0);
+
+ assert.equal(element._computeCommentString(comments, 1, path, {}),
+ '1 unresolved');
+ assert.equal(
+ element._computeCommentString(comments, 2, path, {status: 'M'}),
+ '1 comment');
+ assert.equal(
+ element._computeCommentString(comments, 2, path, {status: 'U'}),
+ 'no changes, 1 comment');
+ assert.equal(
+ element._computeCommentString(comments, 3, path, {status: 'A'}),
+ '2 comments, 2 unresolved');
+ assert.equal(
+ element._computeCommentString(
+ comments, 4, path, {status: 'M'}
+ ), '');
+ assert.equal(
+ element._computeCommentString(comments, 4, path, {status: 'U'}),
+ 'no changes');
+ done();
+ });
+ });
+
+ suite('url params', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
-
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({change: {}}); },
- getLoggedIn() { return Promise.resolve(false); },
- getProjectConfig() { return Promise.resolve({}); },
- getDiffChangeDetail() { return Promise.resolve({}); },
- getChangeFiles() { return Promise.resolve({}); },
- saveFileReviewed() { return Promise.resolve(); },
- getDiffComments() { return Promise.resolve({}); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
- getReviewedFiles() { return Promise.resolve([]); },
- });
- element = fixture('basic');
- return element._loadComments();
+ sandbox.stub(
+ Gerrit.Nav,
+ 'getUrlForDiff',
+ (c, p, pn, bpn) => `${c._number}-${p}-${pn}-${bpn}`);
+ sandbox.stub(
+ Gerrit.Nav
+ , 'getUrlForChange',
+ (c, pn, bpn) => `${c._number}-${pn}-${bpn}`);
});
- teardown(() => {
- sandbox.restore();
- });
-
- test('params change triggers diffViewDisplayed()', () => {
- sandbox.stub(element.$.reporting, 'diffViewDisplayed');
- sandbox.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
- sandbox.spy(element, '_paramsChanged');
- element.params = {
- view: Gerrit.Nav.View.DIFF,
- changeNum: '42',
- patchNum: '2',
- basePatchNum: '1',
- path: '/COMMIT_MSG',
+ test('_formattedFiles', () => {
+ element._changeNum = '42';
+ element._patchRange = {
+ basePatchNum: PARENT,
+ patchNum: '10',
};
+ element._change = {_number: 42};
+ element._files = getFilesFromFileList(
+ ['chell.go', 'glados.txt', 'wheatley.md',
+ '/COMMIT_MSG', '/MERGE_LIST']);
+ element._path = 'glados.txt';
+ const expectedFormattedFiles = [
+ {
+ text: 'chell.go',
+ mobileText: 'chell.go',
+ value: 'chell.go',
+ bottomText: '',
+ }, {
+ text: 'glados.txt',
+ mobileText: 'glados.txt',
+ value: 'glados.txt',
+ bottomText: '',
+ }, {
+ text: 'wheatley.md',
+ mobileText: 'wheatley.md',
+ value: 'wheatley.md',
+ bottomText: '',
+ },
+ {
+ text: 'Commit message',
+ mobileText: 'Commit message',
+ value: '/COMMIT_MSG',
+ bottomText: '',
+ },
+ {
+ text: 'Merge list',
+ mobileText: 'Merge list',
+ value: '/MERGE_LIST',
+ bottomText: '',
+ },
+ ];
- return element._paramsChanged.returnValues[0].then(() => {
- assert.isTrue(element.$.reporting.diffViewDisplayed.calledOnce);
- });
+ assert.deepEqual(element._formattedFiles, expectedFormattedFiles);
+ assert.equal(element._formattedFiles[1].value, element._path);
});
- test('toggle left diff with a hotkey', () => {
- const toggleLeftDiffStub = sandbox.stub(
- element.$.diffHost, 'toggleLeftDiff');
- MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
- assert.isTrue(toggleLeftDiffStub.calledOnce);
- });
-
- test('keyboard shortcuts', () => {
+ test('prev/up/next links', () => {
element._changeNum = '42';
element._patchRange = {
basePatchNum: PARENT,
@@ -154,99 +577,34 @@
element._files = getFilesFromFileList(
['chell.go', 'glados.txt', 'wheatley.md']);
element._path = 'glados.txt';
- element.changeViewState.selectedFileIndex = 1;
- element._loggedIn = true;
-
- const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
- const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
-
- MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
- assert(changeNavStub.lastCall.calledWith(element._change),
- 'Should navigate to /c/42/');
-
- MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
- assert(diffNavStub.lastCall.calledWith(element._change, 'wheatley.md',
- '10', PARENT), 'Should navigate to /c/42/10/wheatley.md');
- element._path = 'wheatley.md';
- assert.equal(element.changeViewState.selectedFileIndex, 2);
- assert.isTrue(element._loading);
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert(diffNavStub.lastCall.calledWith(element._change, 'glados.txt',
- '10', PARENT), 'Should navigate to /c/42/10/glados.txt');
- element._path = 'glados.txt';
- assert.equal(element.changeViewState.selectedFileIndex, 1);
- assert.isTrue(element._loading);
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert(diffNavStub.lastCall.calledWith(element._change, 'chell.go', '10',
- PARENT), 'Should navigate to /c/42/10/chell.go');
- element._path = 'chell.go';
- assert.equal(element.changeViewState.selectedFileIndex, 0);
- assert.isTrue(element._loading);
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert(changeNavStub.lastCall.calledWith(element._change),
- 'Should navigate to /c/42/');
- assert.equal(element.changeViewState.selectedFileIndex, 0);
- assert.isTrue(element._loading);
-
- const showPrefsStub =
- sandbox.stub(element.$.diffPreferencesDialog, 'open',
- () => Promise.resolve());
-
- MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
- assert(showPrefsStub.calledOnce);
-
- element.disableDiffPrefs = true;
- MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
- assert(showPrefsStub.calledOnce);
-
- let scrollStub = sandbox.stub(element.$.cursor, 'moveToNextChunk');
- MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
- assert(scrollStub.calledOnce);
-
- scrollStub = sandbox.stub(element.$.cursor, 'moveToPreviousChunk');
- MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
- assert(scrollStub.calledOnce);
-
- scrollStub = sandbox.stub(element.$.cursor, 'moveToNextCommentThread');
- MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
- assert(scrollStub.calledOnce);
-
- scrollStub = sandbox.stub(element.$.cursor,
- 'moveToPreviousCommentThread');
- MockInteractions.pressAndReleaseKeyOn(element, 80, 'shift', 'p');
- assert(scrollStub.calledOnce);
-
- const computeContainerClassStub = sandbox.stub(element.$.diffHost.$.diff,
- '_computeContainerClass');
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- assert(computeContainerClassStub.lastCall.calledWithExactly(
- false, 'SIDE_BY_SIDE', true));
-
- MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc');
- assert(computeContainerClassStub.lastCall.calledWithExactly(
- false, 'SIDE_BY_SIDE', false));
-
- sandbox.stub(element, '_setReviewed');
- element.$.reviewed.checked = false;
- MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
- assert.isFalse(element._setReviewed.called);
-
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
- assert.isTrue(element._setReviewed.called);
- assert.equal(element._setReviewed.lastCall.args[0], true);
- });
-
- test('shift+x shortcut expands all diff context', () => {
- const expandStub = sandbox.stub(element.$.diffHost, 'expandAllContext');
- MockInteractions.pressAndReleaseKeyOn(element, 88, 'shift', 'x');
flushAsynchronousOperations();
- assert.isTrue(expandStub.called);
+ const linkEls = dom(element.root).querySelectorAll('.navLink');
+ assert.equal(linkEls.length, 3);
+ assert.equal(linkEls[0].getAttribute('href'), '42-chell.go-10-PARENT');
+ assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
+ assert.equal(linkEls[2].getAttribute('href'),
+ '42-wheatley.md-10-PARENT');
+ element._path = 'wheatley.md';
+ flushAsynchronousOperations();
+ assert.equal(linkEls[0].getAttribute('href'),
+ '42-glados.txt-10-PARENT');
+ assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
+ assert.isFalse(linkEls[2].hasAttribute('href'));
+ element._path = 'chell.go';
+ flushAsynchronousOperations();
+ assert.isFalse(linkEls[0].hasAttribute('href'));
+ assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
+ assert.equal(linkEls[2].getAttribute('href'),
+ '42-glados.txt-10-PARENT');
+ element._path = 'not_a_real_file';
+ flushAsynchronousOperations();
+ assert.equal(linkEls[0].getAttribute('href'),
+ '42-wheatley.md-10-PARENT');
+ assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
+ assert.equal(linkEls[2].getAttribute('href'), '42-chell.go-10-PARENT');
});
- test('keyboard shortcuts with patch range', () => {
+ test('prev/up/next links with patch range', () => {
element._changeNum = '42';
element._patchRange = {
basePatchNum: '5',
@@ -255,1168 +613,805 @@
element._change = {
_number: 42,
revisions: {
- a: {_number: 10, commit: {parents: []}},
- b: {_number: 5, commit: {parents: []}},
+ a: {_number: 5, commit: {parents: []}},
+ b: {_number: 10, commit: {parents: []}},
},
};
element._files = getFilesFromFileList(
['chell.go', 'glados.txt', 'wheatley.md']);
element._path = 'glados.txt';
-
- const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
- const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
-
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
- assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' +
- 'should only work when the user is logged in.');
- assert.isNull(window.sessionStorage.getItem(
- 'changeView.showReplyDialog'));
-
- element._loggedIn = true;
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
- assert.isTrue(element.changeViewState.showReplyDialog);
-
- assert(changeNavStub.lastCall.calledWithExactly(element._change, '10',
- '5'), 'Should navigate to /c/42/5..10');
-
- MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
- assert(changeNavStub.lastCall.calledWithExactly(element._change, '10',
- '5'), 'Should navigate to /c/42/5..10');
-
- MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
- assert.isTrue(element._loading);
- assert(diffNavStub.lastCall.calledWithExactly(element._change,
- 'wheatley.md', '10', '5'),
- 'Should navigate to /c/42/5..10/wheatley.md');
+ flushAsynchronousOperations();
+ const linkEls = dom(element.root).querySelectorAll('.navLink');
+ assert.equal(linkEls.length, 3);
+ assert.equal(linkEls[0].getAttribute('href'), '42-chell.go-10-5');
+ assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
+ assert.equal(linkEls[2].getAttribute('href'), '42-wheatley.md-10-5');
element._path = 'wheatley.md';
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert.isTrue(element._loading);
- assert(diffNavStub.lastCall.calledWithExactly(element._change,
- 'glados.txt', '10', '5'),
- 'Should navigate to /c/42/5..10/glados.txt');
- element._path = 'glados.txt';
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert.isTrue(element._loading);
- assert(diffNavStub.lastCall.calledWithExactly(
- element._change,
- 'chell.go',
- '10',
- '5'),
- 'Should navigate to /c/42/5..10/chell.go');
+ flushAsynchronousOperations();
+ assert.equal(linkEls[0].getAttribute('href'), '42-glados.txt-10-5');
+ assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
+ assert.isFalse(linkEls[2].hasAttribute('href'));
element._path = 'chell.go';
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert.isTrue(element._loading);
- assert(changeNavStub.lastCall.calledWithExactly(element._change, '10',
- '5'),
- 'Should navigate to /c/42/5..10');
- });
-
- test('keyboard shortcuts with old patch number', () => {
- element._changeNum = '42';
- element._patchRange = {
- basePatchNum: PARENT,
- patchNum: '1',
- };
- element._change = {
- _number: 42,
- revisions: {
- a: {_number: 1, commit: {parents: []}},
- b: {_number: 2, commit: {parents: []}},
- },
- };
- element._files = getFilesFromFileList(
- ['chell.go', 'glados.txt', 'wheatley.md']);
- element._path = 'glados.txt';
-
- const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
- const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
-
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
- assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' +
- 'should only work when the user is logged in.');
- assert.isNull(window.sessionStorage.getItem(
- 'changeView.showReplyDialog'));
-
- element._loggedIn = true;
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
- assert.isTrue(element.changeViewState.showReplyDialog);
-
- assert(changeNavStub.lastCall.calledWithExactly(element._change, '1',
- PARENT), 'Should navigate to /c/42/1');
-
- MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
- assert(changeNavStub.lastCall.calledWithExactly(element._change, '1',
- PARENT), 'Should navigate to /c/42/1');
-
- MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
- assert(diffNavStub.lastCall.calledWithExactly(element._change,
- 'wheatley.md', '1', PARENT),
- 'Should navigate to /c/42/1/wheatley.md');
- element._path = 'wheatley.md';
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert(diffNavStub.lastCall.calledWithExactly(element._change,
- 'glados.txt', '1', PARENT),
- 'Should navigate to /c/42/1/glados.txt');
- element._path = 'glados.txt';
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert(diffNavStub.lastCall.calledWithExactly(
- element._change,
- 'chell.go',
- '1',
- PARENT), 'Should navigate to /c/42/1/chell.go');
- element._path = 'chell.go';
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert(changeNavStub.lastCall.calledWithExactly(element._change, '1',
- PARENT), 'Should navigate to /c/42/1');
- });
-
- test('edit should redirect to edit page', done => {
- element._loggedIn = true;
- element._path = 't.txt';
- element._patchRange = {
- basePatchNum: PARENT,
- patchNum: '1',
- };
- element._change = {
- _number: 42,
- revisions: {
- a: {_number: 1, commit: {parents: []}},
- b: {_number: 2, commit: {parents: []}},
- },
- };
- const redirectStub = sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
- flush(() => {
- const editBtn = element.shadowRoot
- .querySelector('.editButton gr-button');
- assert.isTrue(!!editBtn);
- MockInteractions.tap(editBtn);
- assert.isTrue(redirectStub.called);
- done();
- });
- });
-
- test('edit hidden when not logged in', done => {
- element._loggedIn = false;
- element._path = 't.txt';
- element._patchRange = {
- basePatchNum: PARENT,
- patchNum: '1',
- };
- element._change = {
- _number: 42,
- revisions: {
- a: {_number: 1, commit: {parents: []}},
- b: {_number: 2, commit: {parents: []}},
- },
- };
- flush(() => {
- const editBtn = element.shadowRoot
- .querySelector('.editButton gr-button');
- assert.isFalse(!!editBtn);
- done();
- });
- });
-
- suite('diff prefs hidden', () => {
- test('when no prefs or logged out', () => {
- element.disableDiffPrefs = false;
- element._loggedIn = false;
- flushAsynchronousOperations();
- assert.isTrue(element.$.diffPrefsContainer.hidden);
-
- element._loggedIn = true;
- flushAsynchronousOperations();
- assert.isTrue(element.$.diffPrefsContainer.hidden);
-
- element._loggedIn = false;
- element._prefs = {font_size: '12'};
- flushAsynchronousOperations();
- assert.isTrue(element.$.diffPrefsContainer.hidden);
-
- element._loggedIn = true;
- flushAsynchronousOperations();
- assert.isFalse(element.$.diffPrefsContainer.hidden);
- });
-
- test('when disableDiffPrefs is set', () => {
- element._loggedIn = true;
- element._prefs = {font_size: '12'};
- element.disableDiffPrefs = false;
- flushAsynchronousOperations();
-
- assert.isFalse(element.$.diffPrefsContainer.hidden);
- element.disableDiffPrefs = true;
- flushAsynchronousOperations();
-
- assert.isTrue(element.$.diffPrefsContainer.hidden);
- });
- });
-
- test('prefsButton opens gr-diff-preferences', () => {
- const handlePrefsTapSpy = sandbox.spy(element, '_handlePrefsTap');
- const overlayOpenStub = sandbox.stub(element.$.diffPreferencesDialog,
- 'open');
- const prefsButton =
- Polymer.dom(element.root).querySelector('.prefsButton');
-
- MockInteractions.tap(prefsButton);
-
- assert.isTrue(handlePrefsTapSpy.called);
- assert.isTrue(overlayOpenStub.called);
- });
-
- test('_computeCommentString', done => {
- const path = '/test';
- element.$.commentAPI.loadAll().then(comments => {
- const commentCountStub =
- sandbox.stub(comments, 'computeCommentCount');
- const unresolvedCountStub =
- sandbox.stub(comments, 'computeUnresolvedNum');
- commentCountStub.withArgs(1, path).returns(0);
- commentCountStub.withArgs(2, path).returns(1);
- commentCountStub.withArgs(3, path).returns(2);
- commentCountStub.withArgs(4, path).returns(0);
- unresolvedCountStub.withArgs(1, path).returns(1);
- unresolvedCountStub.withArgs(2, path).returns(0);
- unresolvedCountStub.withArgs(3, path).returns(2);
- unresolvedCountStub.withArgs(4, path).returns(0);
-
- assert.equal(element._computeCommentString(comments, 1, path, {}),
- '1 unresolved');
- assert.equal(
- element._computeCommentString(comments, 2, path, {status: 'M'}),
- '1 comment');
- assert.equal(
- element._computeCommentString(comments, 2, path, {status: 'U'}),
- 'no changes, 1 comment');
- assert.equal(
- element._computeCommentString(comments, 3, path, {status: 'A'}),
- '2 comments, 2 unresolved');
- assert.equal(
- element._computeCommentString(
- comments, 4, path, {status: 'M'}
- ), '');
- assert.equal(
- element._computeCommentString(comments, 4, path, {status: 'U'}),
- 'no changes');
- done();
- });
- });
-
- suite('url params', () => {
- setup(() => {
- sandbox.stub(
- Gerrit.Nav,
- 'getUrlForDiff',
- (c, p, pn, bpn) => `${c._number}-${p}-${pn}-${bpn}`);
- sandbox.stub(
- Gerrit.Nav
- , 'getUrlForChange',
- (c, pn, bpn) => `${c._number}-${pn}-${bpn}`);
- });
-
- test('_formattedFiles', () => {
- element._changeNum = '42';
- element._patchRange = {
- basePatchNum: PARENT,
- patchNum: '10',
- };
- element._change = {_number: 42};
- element._files = getFilesFromFileList(
- ['chell.go', 'glados.txt', 'wheatley.md',
- '/COMMIT_MSG', '/MERGE_LIST']);
- element._path = 'glados.txt';
- const expectedFormattedFiles = [
- {
- text: 'chell.go',
- mobileText: 'chell.go',
- value: 'chell.go',
- bottomText: '',
- }, {
- text: 'glados.txt',
- mobileText: 'glados.txt',
- value: 'glados.txt',
- bottomText: '',
- }, {
- text: 'wheatley.md',
- mobileText: 'wheatley.md',
- value: 'wheatley.md',
- bottomText: '',
- },
- {
- text: 'Commit message',
- mobileText: 'Commit message',
- value: '/COMMIT_MSG',
- bottomText: '',
- },
- {
- text: 'Merge list',
- mobileText: 'Merge list',
- value: '/MERGE_LIST',
- bottomText: '',
- },
- ];
-
- assert.deepEqual(element._formattedFiles, expectedFormattedFiles);
- assert.equal(element._formattedFiles[1].value, element._path);
- });
-
- test('prev/up/next links', () => {
- element._changeNum = '42';
- element._patchRange = {
- basePatchNum: PARENT,
- patchNum: '10',
- };
- element._change = {
- _number: 42,
- revisions: {
- a: {_number: 10, commit: {parents: []}},
- },
- };
- element._files = getFilesFromFileList(
- ['chell.go', 'glados.txt', 'wheatley.md']);
- element._path = 'glados.txt';
- flushAsynchronousOperations();
- const linkEls = Polymer.dom(element.root).querySelectorAll('.navLink');
- assert.equal(linkEls.length, 3);
- assert.equal(linkEls[0].getAttribute('href'), '42-chell.go-10-PARENT');
- assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
- assert.equal(linkEls[2].getAttribute('href'),
- '42-wheatley.md-10-PARENT');
- element._path = 'wheatley.md';
- flushAsynchronousOperations();
- assert.equal(linkEls[0].getAttribute('href'),
- '42-glados.txt-10-PARENT');
- assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
- assert.isFalse(linkEls[2].hasAttribute('href'));
- element._path = 'chell.go';
- flushAsynchronousOperations();
- assert.isFalse(linkEls[0].hasAttribute('href'));
- assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
- assert.equal(linkEls[2].getAttribute('href'),
- '42-glados.txt-10-PARENT');
- element._path = 'not_a_real_file';
- flushAsynchronousOperations();
- assert.equal(linkEls[0].getAttribute('href'),
- '42-wheatley.md-10-PARENT');
- assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
- assert.equal(linkEls[2].getAttribute('href'), '42-chell.go-10-PARENT');
- });
-
- test('prev/up/next links with patch range', () => {
- element._changeNum = '42';
- element._patchRange = {
- basePatchNum: '5',
- patchNum: '10',
- };
- element._change = {
- _number: 42,
- revisions: {
- a: {_number: 5, commit: {parents: []}},
- b: {_number: 10, commit: {parents: []}},
- },
- };
- element._files = getFilesFromFileList(
- ['chell.go', 'glados.txt', 'wheatley.md']);
- element._path = 'glados.txt';
- flushAsynchronousOperations();
- const linkEls = Polymer.dom(element.root).querySelectorAll('.navLink');
- assert.equal(linkEls.length, 3);
- assert.equal(linkEls[0].getAttribute('href'), '42-chell.go-10-5');
- assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
- assert.equal(linkEls[2].getAttribute('href'), '42-wheatley.md-10-5');
- element._path = 'wheatley.md';
- flushAsynchronousOperations();
- assert.equal(linkEls[0].getAttribute('href'), '42-glados.txt-10-5');
- assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
- assert.isFalse(linkEls[2].hasAttribute('href'));
- element._path = 'chell.go';
- flushAsynchronousOperations();
- assert.isFalse(linkEls[0].hasAttribute('href'));
- assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
- assert.equal(linkEls[2].getAttribute('href'), '42-glados.txt-10-5');
- });
- });
-
- test('_handlePatchChange calls navigateToDiff correctly', () => {
- const navigateStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
- element._change = {_number: 321, project: 'foo/bar'};
- element._path = 'path/to/file.txt';
-
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '3',
- };
-
- const detail = {
- basePatchNum: 'PARENT',
- patchNum: '1',
- };
-
- element.$.rangeSelect.dispatchEvent(
- new CustomEvent('patch-range-change', {detail, bubbles: false}));
-
- assert(navigateStub.lastCall.calledWithExactly(element._change,
- element._path, '1', 'PARENT'));
- });
-
- test('_prefs.manual_review is respected', () => {
- const saveReviewedStub = sandbox.stub(element, '_saveReviewedState',
- () => Promise.resolve());
- const getReviewedStub = sandbox.stub(element, '_getReviewedStatus',
- () => Promise.resolve());
-
- sandbox.stub(element.$.diffHost, 'reload');
- element._loggedIn = true;
- element.params = {
- view: Gerrit.Nav.View.DIFF,
- changeNum: '42',
- patchNum: '2',
- basePatchNum: '1',
- path: '/COMMIT_MSG',
- };
- element._prefs = {manual_review: true};
flushAsynchronousOperations();
-
- assert.isFalse(saveReviewedStub.called);
- assert.isTrue(getReviewedStub.called);
-
- element._prefs = {};
- flushAsynchronousOperations();
-
- assert.isTrue(saveReviewedStub.called);
- assert.isTrue(getReviewedStub.calledOnce);
+ assert.isFalse(linkEls[0].hasAttribute('href'));
+ assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
+ assert.equal(linkEls[2].getAttribute('href'), '42-glados.txt-10-5');
});
+ });
- test('file review status', () => {
- const saveReviewedStub = sandbox.stub(element, '_saveReviewedState',
- () => Promise.resolve());
- sandbox.stub(element.$.diffHost, 'reload');
+ test('_handlePatchChange calls navigateToDiff correctly', () => {
+ const navigateStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+ element._change = {_number: 321, project: 'foo/bar'};
+ element._path = 'path/to/file.txt';
- element._loggedIn = true;
- element.params = {
- view: Gerrit.Nav.View.DIFF,
- changeNum: '42',
- patchNum: '2',
- basePatchNum: '1',
- path: '/COMMIT_MSG',
- };
- element._prefs = {};
- flushAsynchronousOperations();
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '3',
+ };
- const commitMsg = Polymer.dom(element.root).querySelector(
- 'input[type="checkbox"]');
+ const detail = {
+ basePatchNum: 'PARENT',
+ patchNum: '1',
+ };
- assert.isTrue(commitMsg.checked);
- MockInteractions.tap(commitMsg);
- assert.isFalse(commitMsg.checked);
- assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(false));
+ element.$.rangeSelect.dispatchEvent(
+ new CustomEvent('patch-range-change', {detail, bubbles: false}));
- MockInteractions.tap(commitMsg);
- assert.isTrue(commitMsg.checked);
- assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(true));
- const callCount = saveReviewedStub.callCount;
+ assert(navigateStub.lastCall.calledWithExactly(element._change,
+ element._path, '1', 'PARENT'));
+ });
- element.set('params.view', Gerrit.Nav.View.CHANGE);
- flushAsynchronousOperations();
+ test('_prefs.manual_review is respected', () => {
+ const saveReviewedStub = sandbox.stub(element, '_saveReviewedState',
+ () => Promise.resolve());
+ const getReviewedStub = sandbox.stub(element, '_getReviewedStatus',
+ () => Promise.resolve());
- // saveReviewedState observer observes params, but should not fire when
- // view !== Gerrit.Nav.View.DIFF.
- assert.equal(saveReviewedStub.callCount, callCount);
+ sandbox.stub(element.$.diffHost, 'reload');
+ element._loggedIn = true;
+ element.params = {
+ view: Gerrit.Nav.View.DIFF,
+ changeNum: '42',
+ patchNum: '2',
+ basePatchNum: '1',
+ path: '/COMMIT_MSG',
+ };
+ element._prefs = {manual_review: true};
+ flushAsynchronousOperations();
+
+ assert.isFalse(saveReviewedStub.called);
+ assert.isTrue(getReviewedStub.called);
+
+ element._prefs = {};
+ flushAsynchronousOperations();
+
+ assert.isTrue(saveReviewedStub.called);
+ assert.isTrue(getReviewedStub.calledOnce);
+ });
+
+ test('file review status', () => {
+ const saveReviewedStub = sandbox.stub(element, '_saveReviewedState',
+ () => Promise.resolve());
+ sandbox.stub(element.$.diffHost, 'reload');
+
+ element._loggedIn = true;
+ element.params = {
+ view: Gerrit.Nav.View.DIFF,
+ changeNum: '42',
+ patchNum: '2',
+ basePatchNum: '1',
+ path: '/COMMIT_MSG',
+ };
+ element._prefs = {};
+ flushAsynchronousOperations();
+
+ const commitMsg = dom(element.root).querySelector(
+ 'input[type="checkbox"]');
+
+ assert.isTrue(commitMsg.checked);
+ MockInteractions.tap(commitMsg);
+ assert.isFalse(commitMsg.checked);
+ assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(false));
+
+ MockInteractions.tap(commitMsg);
+ assert.isTrue(commitMsg.checked);
+ assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(true));
+ const callCount = saveReviewedStub.callCount;
+
+ element.set('params.view', Gerrit.Nav.View.CHANGE);
+ flushAsynchronousOperations();
+
+ // saveReviewedState observer observes params, but should not fire when
+ // view !== Gerrit.Nav.View.DIFF.
+ assert.equal(saveReviewedStub.callCount, callCount);
+ });
+
+ test('file review status with edit loaded', () => {
+ const saveReviewedStub = sandbox.stub(element, '_saveReviewedState');
+
+ element._patchRange = {patchNum: element.EDIT_NAME};
+ flushAsynchronousOperations();
+
+ assert.isTrue(element._editMode);
+ element._setReviewed();
+ assert.isFalse(saveReviewedStub.called);
+ });
+
+ test('hash is determined from params', done => {
+ sandbox.stub(element.$.diffHost, 'reload');
+ sandbox.stub(element, '_initCursor');
+
+ element._loggedIn = true;
+ element.params = {
+ view: Gerrit.Nav.View.DIFF,
+ changeNum: '42',
+ patchNum: '2',
+ basePatchNum: '1',
+ path: '/COMMIT_MSG',
+ hash: 10,
+ };
+
+ flush(() => {
+ assert.isTrue(element._initCursor.calledOnce);
+ done();
});
+ });
- test('file review status with edit loaded', () => {
- const saveReviewedStub = sandbox.stub(element, '_saveReviewedState');
+ test('diff mode selector correctly toggles the diff', () => {
+ const select = element.$.modeSelect;
+ const diffDisplay = element.$.diffHost;
+ element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};
- element._patchRange = {patchNum: element.EDIT_NAME};
- flushAsynchronousOperations();
+ // The mode selected in the view state reflects the selected option.
+ assert.equal(element._getDiffViewMode(), select.mode);
- assert.isTrue(element._editMode);
- element._setReviewed();
- assert.isFalse(saveReviewedStub.called);
+ // The mode selected in the view state reflects the view rednered in the
+ // diff.
+ assert.equal(select.mode, diffDisplay.viewMode);
+
+ // We will simulate a user change of the selected mode.
+ const newMode = 'UNIFIED_DIFF';
+
+ // Set the mode, and simulate the change event.
+ element.set('changeViewState.diffMode', newMode);
+
+ // Make sure the handler was called and the state is still coherent.
+ assert.equal(element._getDiffViewMode(), newMode);
+ assert.equal(element._getDiffViewMode(), select.mode);
+ assert.equal(element._getDiffViewMode(), diffDisplay.viewMode);
+ });
+
+ test('diff mode selector initializes from preferences', () => {
+ let resolvePrefs;
+ const prefsPromise = new Promise(resolve => {
+ resolvePrefs = resolve;
});
+ sandbox.stub(element.$.restAPI, 'getPreferences', () => prefsPromise);
- test('hash is determined from params', done => {
+ // Attach a new gr-diff-view so we can intercept the preferences fetch.
+ const view = document.createElement('gr-diff-view');
+ fixture('blank').appendChild(view);
+ flushAsynchronousOperations();
+
+ // At this point the diff mode doesn't yet have the user's preference.
+ assert.equal(view._getDiffViewMode(), 'SIDE_BY_SIDE');
+
+ // Receive the overriding preference.
+ resolvePrefs({default_diff_view: 'UNIFIED'});
+ flushAsynchronousOperations();
+ assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+ });
+
+ suite('_commitRange', () => {
+ setup(() => {
sandbox.stub(element.$.diffHost, 'reload');
sandbox.stub(element, '_initCursor');
+ sandbox.stub(element, '_getChangeDetail').returns(Promise.resolve({
+ _number: 42,
+ revisions: {
+ 'commit-sha-1': {
+ _number: 1,
+ commit: {
+ parents: [{commit: 'sha-1-parent'}],
+ },
+ },
+ 'commit-sha-2': {_number: 2},
+ 'commit-sha-3': {_number: 3},
+ 'commit-sha-4': {_number: 4},
+ 'commit-sha-5': {
+ _number: 5,
+ commit: {
+ parents: [{commit: 'sha-5-parent'}],
+ },
+ },
+ },
+ }));
+ });
- element._loggedIn = true;
+ test('uses the patchNum and basePatchNum ', done => {
element.params = {
view: Gerrit.Nav.View.DIFF,
changeNum: '42',
- patchNum: '2',
- basePatchNum: '1',
+ patchNum: '4',
+ basePatchNum: '2',
path: '/COMMIT_MSG',
- hash: 10,
};
-
flush(() => {
- assert.isTrue(element._initCursor.calledOnce);
+ assert.deepEqual(element._commitRange, {
+ baseCommit: 'commit-sha-2',
+ commit: 'commit-sha-4',
+ });
done();
});
});
- test('diff mode selector correctly toggles the diff', () => {
- const select = element.$.modeSelect;
- const diffDisplay = element.$.diffHost;
- element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};
-
- // The mode selected in the view state reflects the selected option.
- assert.equal(element._getDiffViewMode(), select.mode);
-
- // The mode selected in the view state reflects the view rednered in the
- // diff.
- assert.equal(select.mode, diffDisplay.viewMode);
-
- // We will simulate a user change of the selected mode.
- const newMode = 'UNIFIED_DIFF';
-
- // Set the mode, and simulate the change event.
- element.set('changeViewState.diffMode', newMode);
-
- // Make sure the handler was called and the state is still coherent.
- assert.equal(element._getDiffViewMode(), newMode);
- assert.equal(element._getDiffViewMode(), select.mode);
- assert.equal(element._getDiffViewMode(), diffDisplay.viewMode);
- });
-
- test('diff mode selector initializes from preferences', () => {
- let resolvePrefs;
- const prefsPromise = new Promise(resolve => {
- resolvePrefs = resolve;
- });
- sandbox.stub(element.$.restAPI, 'getPreferences', () => prefsPromise);
-
- // Attach a new gr-diff-view so we can intercept the preferences fetch.
- const view = document.createElement('gr-diff-view');
- fixture('blank').appendChild(view);
- flushAsynchronousOperations();
-
- // At this point the diff mode doesn't yet have the user's preference.
- assert.equal(view._getDiffViewMode(), 'SIDE_BY_SIDE');
-
- // Receive the overriding preference.
- resolvePrefs({default_diff_view: 'UNIFIED'});
- flushAsynchronousOperations();
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
- });
-
- suite('_commitRange', () => {
- setup(() => {
- sandbox.stub(element.$.diffHost, 'reload');
- sandbox.stub(element, '_initCursor');
- sandbox.stub(element, '_getChangeDetail').returns(Promise.resolve({
- _number: 42,
- revisions: {
- 'commit-sha-1': {
- _number: 1,
- commit: {
- parents: [{commit: 'sha-1-parent'}],
- },
- },
- 'commit-sha-2': {_number: 2},
- 'commit-sha-3': {_number: 3},
- 'commit-sha-4': {_number: 4},
- 'commit-sha-5': {
- _number: 5,
- commit: {
- parents: [{commit: 'sha-5-parent'}],
- },
- },
- },
- }));
- });
-
- test('uses the patchNum and basePatchNum ', done => {
- element.params = {
- view: Gerrit.Nav.View.DIFF,
- changeNum: '42',
- patchNum: '4',
- basePatchNum: '2',
- path: '/COMMIT_MSG',
- };
- flush(() => {
- assert.deepEqual(element._commitRange, {
- baseCommit: 'commit-sha-2',
- commit: 'commit-sha-4',
- });
- done();
+ test('uses the parent when there is no base patch num ', done => {
+ element.params = {
+ view: Gerrit.Nav.View.DIFF,
+ changeNum: '42',
+ patchNum: '5',
+ path: '/COMMIT_MSG',
+ };
+ flush(() => {
+ assert.deepEqual(element._commitRange, {
+ commit: 'commit-sha-5',
+ baseCommit: 'sha-5-parent',
});
+ done();
});
+ });
+ });
- test('uses the parent when there is no base patch num ', done => {
- element.params = {
- view: Gerrit.Nav.View.DIFF,
- changeNum: '42',
- patchNum: '5',
- path: '/COMMIT_MSG',
- };
- flush(() => {
- assert.deepEqual(element._commitRange, {
- commit: 'commit-sha-5',
- baseCommit: 'sha-5-parent',
- });
- done();
- });
+ test('_initCursor', () => {
+ assert.isNotOk(element.$.cursor.initialLineNumber);
+
+ // Does nothing when params specify no cursor address:
+ element._initCursor({});
+ assert.isNotOk(element.$.cursor.initialLineNumber);
+
+ // Does nothing when params specify side but no number:
+ element._initCursor({leftSide: true});
+ assert.isNotOk(element.$.cursor.initialLineNumber);
+
+ // Revision hash: specifies lineNum but not side.
+ element._initCursor({lineNum: 234});
+ assert.equal(element.$.cursor.initialLineNumber, 234);
+ assert.equal(element.$.cursor.side, 'right');
+
+ // Base hash: specifies lineNum and side.
+ element._initCursor({leftSide: true, lineNum: 345});
+ assert.equal(element.$.cursor.initialLineNumber, 345);
+ assert.equal(element.$.cursor.side, 'left');
+
+ // Specifies right side:
+ element._initCursor({leftSide: false, lineNum: 123});
+ assert.equal(element.$.cursor.initialLineNumber, 123);
+ assert.equal(element.$.cursor.side, 'right');
+ });
+
+ test('_getLineOfInterest', () => {
+ assert.isNull(element._getLineOfInterest({}));
+
+ let result = element._getLineOfInterest({lineNum: 12});
+ assert.equal(result.number, 12);
+ assert.isNotOk(result.leftSide);
+
+ result = element._getLineOfInterest({lineNum: 12, leftSide: true});
+ assert.equal(result.number, 12);
+ assert.isOk(result.leftSide);
+ });
+
+ test('_onLineSelected', () => {
+ const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
+ const replaceStateStub = sandbox.stub(history, 'replaceState');
+ const moveStub = sandbox.stub(element.$.cursor, 'moveToLineNumber');
+ sandbox.stub(element.$.cursor, 'getAddress')
+ .returns({number: 123, isLeftSide: false});
+
+ element._changeNum = 321;
+ element._change = {_number: 321, project: 'foo/bar'};
+ element._patchRange = {
+ basePatchNum: '3',
+ patchNum: '5',
+ };
+ const e = {};
+ const detail = {number: 123, side: 'right'};
+
+ element._onLineSelected(e, detail);
+
+ assert.isTrue(moveStub.called);
+ assert.equal(moveStub.lastCall.args[0], detail.number);
+ assert.equal(moveStub.lastCall.args[1], detail.side);
+
+ assert.isTrue(replaceStateStub.called);
+ assert.isTrue(getUrlStub.called);
+ });
+
+ test('_onLineSelected w/o line address', () => {
+ const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
+ sandbox.stub(history, 'replaceState');
+ sandbox.stub(element.$.cursor, 'moveToLineNumber');
+ sandbox.stub(element.$.cursor, 'getAddress').returns(null);
+ element._changeNum = 321;
+ element._change = {_number: 321, project: 'foo/bar'};
+ element._patchRange = {basePatchNum: '3', patchNum: '5'};
+ element._onLineSelected({}, {number: 123, side: 'right'});
+ assert.isTrue(getUrlStub.calledOnce);
+ assert.isUndefined(getUrlStub.lastCall.args[5]);
+ assert.isUndefined(getUrlStub.lastCall.args[6]);
+ });
+
+ test('_getDiffViewMode', () => {
+ // No user prefs or change view state set.
+ assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+
+ // User prefs but no change view state set.
+ element._userPrefs = {default_diff_view: 'UNIFIED_DIFF'};
+ assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
+
+ // User prefs and change view state set.
+ element.changeViewState = {diffMode: 'SIDE_BY_SIDE'};
+ assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+ });
+
+ test('_handleToggleDiffMode', () => {
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ const e = {preventDefault: () => {}};
+ // Initial state.
+ assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+
+ element._handleToggleDiffMode(e);
+ assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
+
+ element._handleToggleDiffMode(e);
+ assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+ });
+
+ suite('_loadComments', () => {
+ test('empty', done => {
+ element._loadComments().then(() => {
+ assert.equal(Object.keys(element._commentMap).length, 0);
+ done();
});
});
- test('_initCursor', () => {
- assert.isNotOk(element.$.cursor.initialLineNumber);
-
- // Does nothing when params specify no cursor address:
- element._initCursor({});
- assert.isNotOk(element.$.cursor.initialLineNumber);
-
- // Does nothing when params specify side but no number:
- element._initCursor({leftSide: true});
- assert.isNotOk(element.$.cursor.initialLineNumber);
-
- // Revision hash: specifies lineNum but not side.
- element._initCursor({lineNum: 234});
- assert.equal(element.$.cursor.initialLineNumber, 234);
- assert.equal(element.$.cursor.side, 'right');
-
- // Base hash: specifies lineNum and side.
- element._initCursor({leftSide: true, lineNum: 345});
- assert.equal(element.$.cursor.initialLineNumber, 345);
- assert.equal(element.$.cursor.side, 'left');
-
- // Specifies right side:
- element._initCursor({leftSide: false, lineNum: 123});
- assert.equal(element.$.cursor.initialLineNumber, 123);
- assert.equal(element.$.cursor.side, 'right');
- });
-
- test('_getLineOfInterest', () => {
- assert.isNull(element._getLineOfInterest({}));
-
- let result = element._getLineOfInterest({lineNum: 12});
- assert.equal(result.number, 12);
- assert.isNotOk(result.leftSide);
-
- result = element._getLineOfInterest({lineNum: 12, leftSide: true});
- assert.equal(result.number, 12);
- assert.isOk(result.leftSide);
- });
-
- test('_onLineSelected', () => {
- const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
- const replaceStateStub = sandbox.stub(history, 'replaceState');
- const moveStub = sandbox.stub(element.$.cursor, 'moveToLineNumber');
- sandbox.stub(element.$.cursor, 'getAddress')
- .returns({number: 123, isLeftSide: false});
-
- element._changeNum = 321;
- element._change = {_number: 321, project: 'foo/bar'};
+ test('has paths', done => {
+ sandbox.stub(element, '_getPaths').returns({
+ 'path/to/file/one.cpp': [{patch_set: 3, message: 'lorem'}],
+ 'path-to/file/two.py': [{patch_set: 5, message: 'ipsum'}],
+ });
+ sandbox.stub(element, '_getCommentsForPath').returns({meta: {}});
+ element._changeNum = '42';
element._patchRange = {
basePatchNum: '3',
patchNum: '5',
};
- const e = {};
- const detail = {number: 123, side: 'right'};
-
- element._onLineSelected(e, detail);
-
- assert.isTrue(moveStub.called);
- assert.equal(moveStub.lastCall.args[0], detail.number);
- assert.equal(moveStub.lastCall.args[1], detail.side);
-
- assert.isTrue(replaceStateStub.called);
- assert.isTrue(getUrlStub.called);
- });
-
- test('_onLineSelected w/o line address', () => {
- const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
- sandbox.stub(history, 'replaceState');
- sandbox.stub(element.$.cursor, 'moveToLineNumber');
- sandbox.stub(element.$.cursor, 'getAddress').returns(null);
- element._changeNum = 321;
- element._change = {_number: 321, project: 'foo/bar'};
- element._patchRange = {basePatchNum: '3', patchNum: '5'};
- element._onLineSelected({}, {number: 123, side: 'right'});
- assert.isTrue(getUrlStub.calledOnce);
- assert.isUndefined(getUrlStub.lastCall.args[5]);
- assert.isUndefined(getUrlStub.lastCall.args[6]);
- });
-
- test('_getDiffViewMode', () => {
- // No user prefs or change view state set.
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
-
- // User prefs but no change view state set.
- element._userPrefs = {default_diff_view: 'UNIFIED_DIFF'};
- assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
-
- // User prefs and change view state set.
- element.changeViewState = {diffMode: 'SIDE_BY_SIDE'};
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
- });
-
- test('_handleToggleDiffMode', () => {
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- const e = {preventDefault: () => {}};
- // Initial state.
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
-
- element._handleToggleDiffMode(e);
- assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
-
- element._handleToggleDiffMode(e);
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
- });
-
- suite('_loadComments', () => {
- test('empty', done => {
- element._loadComments().then(() => {
- assert.equal(Object.keys(element._commentMap).length, 0);
- done();
- });
- });
-
- test('has paths', done => {
- sandbox.stub(element, '_getPaths').returns({
- 'path/to/file/one.cpp': [{patch_set: 3, message: 'lorem'}],
- 'path-to/file/two.py': [{patch_set: 5, message: 'ipsum'}],
- });
- sandbox.stub(element, '_getCommentsForPath').returns({meta: {}});
- element._changeNum = '42';
- element._patchRange = {
- basePatchNum: '3',
- patchNum: '5',
- };
- element._loadComments().then(() => {
- assert.deepEqual(Object.keys(element._commentMap),
- ['path/to/file/one.cpp', 'path-to/file/two.py']);
- done();
- });
+ element._loadComments().then(() => {
+ assert.deepEqual(Object.keys(element._commentMap),
+ ['path/to/file/one.cpp', 'path-to/file/two.py']);
+ done();
});
});
+ });
- suite('_computeCommentSkips', () => {
- test('empty file list', () => {
- const commentMap = {
- 'path/one.jpg': true,
- 'path/three.wav': true,
- };
- const path = 'path/two.m4v';
- const fileList = [];
- const result = element._computeCommentSkips(commentMap, fileList, path);
- assert.isNull(result.previous);
- assert.isNull(result.next);
- });
-
- test('finds skips', () => {
- const fileList = ['path/one.jpg', 'path/two.m4v', 'path/three.wav'];
- let path = fileList[1];
- const commentMap = {};
- commentMap[fileList[0]] = true;
- commentMap[fileList[1]] = false;
- commentMap[fileList[2]] = true;
-
- let result = element._computeCommentSkips(commentMap, fileList, path);
- assert.equal(result.previous, fileList[0]);
- assert.equal(result.next, fileList[2]);
-
- commentMap[fileList[1]] = true;
-
- result = element._computeCommentSkips(commentMap, fileList, path);
- assert.equal(result.previous, fileList[0]);
- assert.equal(result.next, fileList[2]);
-
- path = fileList[0];
-
- result = element._computeCommentSkips(commentMap, fileList, path);
- assert.isNull(result.previous);
- assert.equal(result.next, fileList[1]);
-
- path = fileList[2];
-
- result = element._computeCommentSkips(commentMap, fileList, path);
- assert.equal(result.previous, fileList[1]);
- assert.isNull(result.next);
- });
-
- suite('skip next/previous', () => {
- let navToChangeStub;
- let navToDiffStub;
-
- setup(() => {
- navToChangeStub = sandbox.stub(element, '_navToChangeView');
- navToDiffStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
- element._files = getFilesFromFileList([
- 'path/one.jpg', 'path/two.m4v', 'path/three.wav',
- ]);
- element._patchRange = {patchNum: '2', basePatchNum: '1'};
- });
-
- suite('_moveToPreviousFileWithComment', () => {
- test('no skips', () => {
- element._moveToPreviousFileWithComment();
- assert.isFalse(navToChangeStub.called);
- assert.isFalse(navToDiffStub.called);
- });
-
- test('no previous', () => {
- const commentMap = {};
- commentMap[element._fileList[0]] = false;
- commentMap[element._fileList[1]] = false;
- commentMap[element._fileList[2]] = true;
- element._commentMap = commentMap;
- element._path = element._fileList[1];
-
- element._moveToPreviousFileWithComment();
- assert.isTrue(navToChangeStub.calledOnce);
- assert.isFalse(navToDiffStub.called);
- });
-
- test('w/ previous', () => {
- const commentMap = {};
- commentMap[element._fileList[0]] = true;
- commentMap[element._fileList[1]] = false;
- commentMap[element._fileList[2]] = true;
- element._commentMap = commentMap;
- element._path = element._fileList[1];
-
- element._moveToPreviousFileWithComment();
- assert.isFalse(navToChangeStub.called);
- assert.isTrue(navToDiffStub.calledOnce);
- });
- });
-
- suite('_moveToNextFileWithComment', () => {
- test('no skips', () => {
- element._moveToNextFileWithComment();
- assert.isFalse(navToChangeStub.called);
- assert.isFalse(navToDiffStub.called);
- });
-
- test('no previous', () => {
- const commentMap = {};
- commentMap[element._fileList[0]] = true;
- commentMap[element._fileList[1]] = false;
- commentMap[element._fileList[2]] = false;
- element._commentMap = commentMap;
- element._path = element._fileList[1];
-
- element._moveToNextFileWithComment();
- assert.isTrue(navToChangeStub.calledOnce);
- assert.isFalse(navToDiffStub.called);
- });
-
- test('w/ previous', () => {
- const commentMap = {};
- commentMap[element._fileList[0]] = true;
- commentMap[element._fileList[1]] = false;
- commentMap[element._fileList[2]] = true;
- element._commentMap = commentMap;
- element._path = element._fileList[1];
-
- element._moveToNextFileWithComment();
- assert.isFalse(navToChangeStub.called);
- assert.isTrue(navToDiffStub.calledOnce);
- });
- });
- });
- });
-
- test('_computeEditMode', () => {
- const callCompute = range => element._computeEditMode({base: range});
- assert.isFalse(callCompute({}));
- assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1}));
- assert.isFalse(callCompute({basePatchNum: 'edit', patchNum: 1}));
- assert.isTrue(callCompute({basePatchNum: 1, patchNum: 'edit'}));
- });
-
- test('_computeFileNum', () => {
- assert.equal(element._computeFileNum('/foo',
- [{value: '/foo'}, {value: '/bar'}]), 1);
- assert.equal(element._computeFileNum('/bar',
- [{value: '/foo'}, {value: '/bar'}]), 2);
- });
-
- test('_computeFileNumClass', () => {
- assert.equal(element._computeFileNumClass(0, []), '');
- assert.equal(element._computeFileNumClass(1,
- [{value: '/foo'}, {value: '/bar'}]), 'show');
- });
-
- test('_getReviewedStatus', () => {
- const promises = [];
- element.$.restAPI.getReviewedFiles.restore();
-
- sandbox.stub(element.$.restAPI, 'getReviewedFiles')
- .returns(Promise.resolve(['path']));
-
- promises.push(element._getReviewedStatus(true, null, null, 'path')
- .then(reviewed => assert.isFalse(reviewed)));
-
- promises.push(element._getReviewedStatus(false, null, null, 'otherPath')
- .then(reviewed => assert.isFalse(reviewed)));
-
- promises.push(element._getReviewedStatus(false, null, null, 'path')
- .then(reviewed => assert.isTrue(reviewed)));
-
- return Promise.all(promises);
- });
-
- suite('blame', () => {
- test('toggle blame with button', () => {
- const toggleBlame = sandbox.stub(
- element.$.diffHost, 'loadBlame', () => Promise.resolve());
- MockInteractions.tap(element.$.toggleBlame);
- assert.isTrue(toggleBlame.calledOnce);
- });
- test('toggle blame with shortcut', () => {
- const toggleBlame = sandbox.stub(
- element.$.diffHost, 'loadBlame', () => Promise.resolve());
- MockInteractions.pressAndReleaseKeyOn(element, 66, null, 'b');
- assert.isTrue(toggleBlame.calledOnce);
- });
- });
-
- suite('editMode behavior', () => {
- setup(() => {
- element._loggedIn = true;
- });
-
- const isVisible = el => {
- assert.ok(el);
- return getComputedStyle(el).getPropertyValue('display') !== 'none';
+ suite('_computeCommentSkips', () => {
+ test('empty file list', () => {
+ const commentMap = {
+ 'path/one.jpg': true,
+ 'path/three.wav': true,
};
-
- test('reviewed checkbox', () => {
- sandbox.stub(element, '_handlePatchChange');
- element._patchRange = {patchNum: '1'};
- // Reviewed checkbox should be shown.
- assert.isTrue(isVisible(element.$.reviewed));
- element.set('_patchRange.patchNum', element.EDIT_NAME);
- flushAsynchronousOperations();
-
- assert.isFalse(isVisible(element.$.reviewed));
- });
+ const path = 'path/two.m4v';
+ const fileList = [];
+ const result = element._computeCommentSkips(commentMap, fileList, path);
+ assert.isNull(result.previous);
+ assert.isNull(result.next);
});
- test('_paramsChanged sets in projectLookup', () => {
- sandbox.stub(element, '_getLineOfInterest');
- sandbox.stub(element, '_initCursor');
- const setStub = sandbox.stub(element.$.restAPI, 'setInProjectLookup');
- element._paramsChanged({
- view: Gerrit.Nav.View.DIFF,
- changeNum: 101,
- project: 'test-project',
- path: '',
- });
- assert.isTrue(setStub.calledOnce);
- assert.isTrue(setStub.calledWith(101, 'test-project'));
+ test('finds skips', () => {
+ const fileList = ['path/one.jpg', 'path/two.m4v', 'path/three.wav'];
+ let path = fileList[1];
+ const commentMap = {};
+ commentMap[fileList[0]] = true;
+ commentMap[fileList[1]] = false;
+ commentMap[fileList[2]] = true;
+
+ let result = element._computeCommentSkips(commentMap, fileList, path);
+ assert.equal(result.previous, fileList[0]);
+ assert.equal(result.next, fileList[2]);
+
+ commentMap[fileList[1]] = true;
+
+ result = element._computeCommentSkips(commentMap, fileList, path);
+ assert.equal(result.previous, fileList[0]);
+ assert.equal(result.next, fileList[2]);
+
+ path = fileList[0];
+
+ result = element._computeCommentSkips(commentMap, fileList, path);
+ assert.isNull(result.previous);
+ assert.equal(result.next, fileList[1]);
+
+ path = fileList[2];
+
+ result = element._computeCommentSkips(commentMap, fileList, path);
+ assert.equal(result.previous, fileList[1]);
+ assert.isNull(result.next);
});
- test('shift+m navigates to next unreviewed file', () => {
- element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
- element._reviewedFiles = new Set(['file1', 'file2']);
- element._path = 'file1';
- const reviewedStub = sandbox.stub(element, '_setReviewed');
- const navStub = sandbox.stub(element, '_navToFile');
- MockInteractions.pressAndReleaseKeyOn(element, 77, 'shift', 'm');
+ suite('skip next/previous', () => {
+ let navToChangeStub;
+ let navToDiffStub;
+
+ setup(() => {
+ navToChangeStub = sandbox.stub(element, '_navToChangeView');
+ navToDiffStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+ element._files = getFilesFromFileList([
+ 'path/one.jpg', 'path/two.m4v', 'path/three.wav',
+ ]);
+ element._patchRange = {patchNum: '2', basePatchNum: '1'};
+ });
+
+ suite('_moveToPreviousFileWithComment', () => {
+ test('no skips', () => {
+ element._moveToPreviousFileWithComment();
+ assert.isFalse(navToChangeStub.called);
+ assert.isFalse(navToDiffStub.called);
+ });
+
+ test('no previous', () => {
+ const commentMap = {};
+ commentMap[element._fileList[0]] = false;
+ commentMap[element._fileList[1]] = false;
+ commentMap[element._fileList[2]] = true;
+ element._commentMap = commentMap;
+ element._path = element._fileList[1];
+
+ element._moveToPreviousFileWithComment();
+ assert.isTrue(navToChangeStub.calledOnce);
+ assert.isFalse(navToDiffStub.called);
+ });
+
+ test('w/ previous', () => {
+ const commentMap = {};
+ commentMap[element._fileList[0]] = true;
+ commentMap[element._fileList[1]] = false;
+ commentMap[element._fileList[2]] = true;
+ element._commentMap = commentMap;
+ element._path = element._fileList[1];
+
+ element._moveToPreviousFileWithComment();
+ assert.isFalse(navToChangeStub.called);
+ assert.isTrue(navToDiffStub.calledOnce);
+ });
+ });
+
+ suite('_moveToNextFileWithComment', () => {
+ test('no skips', () => {
+ element._moveToNextFileWithComment();
+ assert.isFalse(navToChangeStub.called);
+ assert.isFalse(navToDiffStub.called);
+ });
+
+ test('no previous', () => {
+ const commentMap = {};
+ commentMap[element._fileList[0]] = true;
+ commentMap[element._fileList[1]] = false;
+ commentMap[element._fileList[2]] = false;
+ element._commentMap = commentMap;
+ element._path = element._fileList[1];
+
+ element._moveToNextFileWithComment();
+ assert.isTrue(navToChangeStub.calledOnce);
+ assert.isFalse(navToDiffStub.called);
+ });
+
+ test('w/ previous', () => {
+ const commentMap = {};
+ commentMap[element._fileList[0]] = true;
+ commentMap[element._fileList[1]] = false;
+ commentMap[element._fileList[2]] = true;
+ element._commentMap = commentMap;
+ element._path = element._fileList[1];
+
+ element._moveToNextFileWithComment();
+ assert.isFalse(navToChangeStub.called);
+ assert.isTrue(navToDiffStub.calledOnce);
+ });
+ });
+ });
+ });
+
+ test('_computeEditMode', () => {
+ const callCompute = range => element._computeEditMode({base: range});
+ assert.isFalse(callCompute({}));
+ assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1}));
+ assert.isFalse(callCompute({basePatchNum: 'edit', patchNum: 1}));
+ assert.isTrue(callCompute({basePatchNum: 1, patchNum: 'edit'}));
+ });
+
+ test('_computeFileNum', () => {
+ assert.equal(element._computeFileNum('/foo',
+ [{value: '/foo'}, {value: '/bar'}]), 1);
+ assert.equal(element._computeFileNum('/bar',
+ [{value: '/foo'}, {value: '/bar'}]), 2);
+ });
+
+ test('_computeFileNumClass', () => {
+ assert.equal(element._computeFileNumClass(0, []), '');
+ assert.equal(element._computeFileNumClass(1,
+ [{value: '/foo'}, {value: '/bar'}]), 'show');
+ });
+
+ test('_getReviewedStatus', () => {
+ const promises = [];
+ element.$.restAPI.getReviewedFiles.restore();
+
+ sandbox.stub(element.$.restAPI, 'getReviewedFiles')
+ .returns(Promise.resolve(['path']));
+
+ promises.push(element._getReviewedStatus(true, null, null, 'path')
+ .then(reviewed => assert.isFalse(reviewed)));
+
+ promises.push(element._getReviewedStatus(false, null, null, 'otherPath')
+ .then(reviewed => assert.isFalse(reviewed)));
+
+ promises.push(element._getReviewedStatus(false, null, null, 'path')
+ .then(reviewed => assert.isTrue(reviewed)));
+
+ return Promise.all(promises);
+ });
+
+ suite('blame', () => {
+ test('toggle blame with button', () => {
+ const toggleBlame = sandbox.stub(
+ element.$.diffHost, 'loadBlame', () => Promise.resolve());
+ MockInteractions.tap(element.$.toggleBlame);
+ assert.isTrue(toggleBlame.calledOnce);
+ });
+ test('toggle blame with shortcut', () => {
+ const toggleBlame = sandbox.stub(
+ element.$.diffHost, 'loadBlame', () => Promise.resolve());
+ MockInteractions.pressAndReleaseKeyOn(element, 66, null, 'b');
+ assert.isTrue(toggleBlame.calledOnce);
+ });
+ });
+
+ suite('editMode behavior', () => {
+ setup(() => {
+ element._loggedIn = true;
+ });
+
+ const isVisible = el => {
+ assert.ok(el);
+ return getComputedStyle(el).getPropertyValue('display') !== 'none';
+ };
+
+ test('reviewed checkbox', () => {
+ sandbox.stub(element, '_handlePatchChange');
+ element._patchRange = {patchNum: '1'};
+ // Reviewed checkbox should be shown.
+ assert.isTrue(isVisible(element.$.reviewed));
+ element.set('_patchRange.patchNum', element.EDIT_NAME);
flushAsynchronousOperations();
- assert.isTrue(reviewedStub.lastCall.args[0]);
- assert.deepEqual(navStub.lastCall.args, [
- 'file1',
- ['file1', 'file3'],
- 1,
- ]);
- });
-
- test('File change should trigger navigateToDiff once', () => {
- element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
- sandbox.stub(element, '_getLineOfInterest');
- sandbox.stub(element, '_initCursor');
- sandbox.stub(Gerrit.Nav, 'navigateToDiff');
-
- // Load file1
- element._paramsChanged({
- view: Gerrit.Nav.View.DIFF,
- patchNum: 1,
- changeNum: 101,
- project: 'test-project',
- path: 'file1',
- });
- assert.isTrue(Gerrit.Nav.navigateToDiff.notCalled);
-
- // Switch to file2
- element.$.dropdown.value = 'file2';
- assert.isTrue(Gerrit.Nav.navigateToDiff.calledOnce);
-
- // This is to mock the param change triggered by above navigate
- element._paramsChanged({
- view: Gerrit.Nav.View.DIFF,
- patchNum: 1,
- changeNum: 101,
- project: 'test-project',
- path: 'file2',
- });
-
- // No extra call
- assert.isTrue(Gerrit.Nav.navigateToDiff.calledOnce);
- });
-
- test('_computeDownloadDropdownLinks', () => {
- const downloadLinks = [
- {
- url: '/changes/test~12/revisions/1/patch?zip&path=index.php',
- name: 'Patch',
- },
- {
- url: '/changes/test~12/revisions/1' +
- '/files/index.php/download?parent=1',
- name: 'Left Content',
- },
- {
- url: '/changes/test~12/revisions/1' +
- '/files/index.php/download',
- name: 'Right Content',
- },
- ];
-
- const side = {
- meta_a: true,
- meta_b: true,
- };
-
- const base = {
- patchNum: 1,
- basePatchNum: 'PARENT',
- };
-
- assert.deepEqual(
- element._computeDownloadDropdownLinks(
- 'test', 12, base, 'index.php', side),
- downloadLinks);
- });
-
- test('_computeDownloadDropdownLinks diff returns renamed', () => {
- const downloadLinks = [
- {
- url: '/changes/test~12/revisions/3/patch?zip&path=index.php',
- name: 'Patch',
- },
- {
- url: '/changes/test~12/revisions/2' +
- '/files/index2.php/download',
- name: 'Left Content',
- },
- {
- url: '/changes/test~12/revisions/3' +
- '/files/index.php/download',
- name: 'Right Content',
- },
- ];
-
- const side = {
- change_type: 'RENAMED',
- meta_a: {
- name: 'index2.php',
- },
- meta_b: true,
- };
-
- const base = {
- patchNum: 3,
- basePatchNum: 2,
- };
-
- assert.deepEqual(
- element._computeDownloadDropdownLinks(
- 'test', 12, base, 'index.php', side),
- downloadLinks);
- });
-
- test('_computeDownloadFileLink', () => {
- const base = {
- patchNum: 1,
- basePatchNum: 'PARENT',
- };
-
- assert.equal(
- element._computeDownloadFileLink(
- 'test', 12, base, 'index.php', true),
- '/changes/test~12/revisions/1/files/index.php/download?parent=1');
-
- assert.equal(
- element._computeDownloadFileLink(
- 'test', 12, base, 'index.php', false),
- '/changes/test~12/revisions/1/files/index.php/download');
- });
-
- test('_computeDownloadPatchLink', () => {
- assert.equal(
- element._computeDownloadPatchLink(
- 'test', 12, {patchNum: 1}, 'index.php'),
- '/changes/test~12/revisions/1/patch?zip&path=index.php');
+ assert.isFalse(isVisible(element.$.reviewed));
});
});
- suite('gr-diff-view tests unmodified files with comments', () => {
- let sandbox;
- let element;
- setup(() => {
- sandbox = sinon.sandbox.create();
- const changedFiles = {
- 'file1.txt': {},
- 'a/b/test.c': {},
- };
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({change: {}}); },
- getLoggedIn() { return Promise.resolve(false); },
- getProjectConfig() { return Promise.resolve({}); },
- getDiffChangeDetail() { return Promise.resolve({}); },
- getChangeFiles() { return Promise.resolve(changedFiles); },
- saveFileReviewed() { return Promise.resolve(); },
- getDiffComments() { return Promise.resolve({}); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
- getReviewedFiles() { return Promise.resolve([]); },
- });
- element = fixture('basic');
- return element._loadComments();
+ test('_paramsChanged sets in projectLookup', () => {
+ sandbox.stub(element, '_getLineOfInterest');
+ sandbox.stub(element, '_initCursor');
+ const setStub = sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+ element._paramsChanged({
+ view: Gerrit.Nav.View.DIFF,
+ changeNum: 101,
+ project: 'test-project',
+ path: '',
+ });
+ assert.isTrue(setStub.calledOnce);
+ assert.isTrue(setStub.calledWith(101, 'test-project'));
+ });
+
+ test('shift+m navigates to next unreviewed file', () => {
+ element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
+ element._reviewedFiles = new Set(['file1', 'file2']);
+ element._path = 'file1';
+ const reviewedStub = sandbox.stub(element, '_setReviewed');
+ const navStub = sandbox.stub(element, '_navToFile');
+ MockInteractions.pressAndReleaseKeyOn(element, 77, 'shift', 'm');
+ flushAsynchronousOperations();
+
+ assert.isTrue(reviewedStub.lastCall.args[0]);
+ assert.deepEqual(navStub.lastCall.args, [
+ 'file1',
+ ['file1', 'file3'],
+ 1,
+ ]);
+ });
+
+ test('File change should trigger navigateToDiff once', () => {
+ element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
+ sandbox.stub(element, '_getLineOfInterest');
+ sandbox.stub(element, '_initCursor');
+ sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+
+ // Load file1
+ element._paramsChanged({
+ view: Gerrit.Nav.View.DIFF,
+ patchNum: 1,
+ changeNum: 101,
+ project: 'test-project',
+ path: 'file1',
+ });
+ assert.isTrue(Gerrit.Nav.navigateToDiff.notCalled);
+
+ // Switch to file2
+ element.$.dropdown.value = 'file2';
+ assert.isTrue(Gerrit.Nav.navigateToDiff.calledOnce);
+
+ // This is to mock the param change triggered by above navigate
+ element._paramsChanged({
+ view: Gerrit.Nav.View.DIFF,
+ patchNum: 1,
+ changeNum: 101,
+ project: 'test-project',
+ path: 'file2',
});
- teardown(() => {
- sandbox.restore();
- });
+ // No extra call
+ assert.isTrue(Gerrit.Nav.navigateToDiff.calledOnce);
+ });
- test('_getFiles add files with comments without changes', () => {
- const patchChangeRecord = {
- base: {
- basePatchNum: '5',
- patchNum: '10',
- },
- };
- const changeComments = {
- getPaths: sandbox.stub().returns({
- 'file2.txt': {},
- 'file1.txt': {},
- }),
- };
- return element._getFiles(23, patchChangeRecord, changeComments)
- .then(() => {
- assert.deepEqual(element._files, {
- sortedFileList: ['a/b/test.c', 'file1.txt', 'file2.txt'],
- changeFilesByPath: {
- 'file1.txt': {},
- 'file2.txt': {status: 'U'},
- 'a/b/test.c': {},
- },
- });
- });
- });
+ test('_computeDownloadDropdownLinks', () => {
+ const downloadLinks = [
+ {
+ url: '/changes/test~12/revisions/1/patch?zip&path=index.php',
+ name: 'Patch',
+ },
+ {
+ url: '/changes/test~12/revisions/1' +
+ '/files/index.php/download?parent=1',
+ name: 'Left Content',
+ },
+ {
+ url: '/changes/test~12/revisions/1' +
+ '/files/index.php/download',
+ name: 'Right Content',
+ },
+ ];
+
+ const side = {
+ meta_a: true,
+ meta_b: true,
+ };
+
+ const base = {
+ patchNum: 1,
+ basePatchNum: 'PARENT',
+ };
+
+ assert.deepEqual(
+ element._computeDownloadDropdownLinks(
+ 'test', 12, base, 'index.php', side),
+ downloadLinks);
+ });
+
+ test('_computeDownloadDropdownLinks diff returns renamed', () => {
+ const downloadLinks = [
+ {
+ url: '/changes/test~12/revisions/3/patch?zip&path=index.php',
+ name: 'Patch',
+ },
+ {
+ url: '/changes/test~12/revisions/2' +
+ '/files/index2.php/download',
+ name: 'Left Content',
+ },
+ {
+ url: '/changes/test~12/revisions/3' +
+ '/files/index.php/download',
+ name: 'Right Content',
+ },
+ ];
+
+ const side = {
+ change_type: 'RENAMED',
+ meta_a: {
+ name: 'index2.php',
+ },
+ meta_b: true,
+ };
+
+ const base = {
+ patchNum: 3,
+ basePatchNum: 2,
+ };
+
+ assert.deepEqual(
+ element._computeDownloadDropdownLinks(
+ 'test', 12, base, 'index.php', side),
+ downloadLinks);
+ });
+
+ test('_computeDownloadFileLink', () => {
+ const base = {
+ patchNum: 1,
+ basePatchNum: 'PARENT',
+ };
+
+ assert.equal(
+ element._computeDownloadFileLink(
+ 'test', 12, base, 'index.php', true),
+ '/changes/test~12/revisions/1/files/index.php/download?parent=1');
+
+ assert.equal(
+ element._computeDownloadFileLink(
+ 'test', 12, base, 'index.php', false),
+ '/changes/test~12/revisions/1/files/index.php/download');
+ });
+
+ test('_computeDownloadPatchLink', () => {
+ assert.equal(
+ element._computeDownloadPatchLink(
+ 'test', 12, {patchNum: 1}, 'index.php'),
+ '/changes/test~12/revisions/1/patch?zip&path=index.php');
});
});
+
+ suite('gr-diff-view tests unmodified files with comments', () => {
+ let sandbox;
+ let element;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ const changedFiles = {
+ 'file1.txt': {},
+ 'a/b/test.c': {},
+ };
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({change: {}}); },
+ getLoggedIn() { return Promise.resolve(false); },
+ getProjectConfig() { return Promise.resolve({}); },
+ getDiffChangeDetail() { return Promise.resolve({}); },
+ getChangeFiles() { return Promise.resolve(changedFiles); },
+ saveFileReviewed() { return Promise.resolve(); },
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ getReviewedFiles() { return Promise.resolve([]); },
+ });
+ element = fixture('basic');
+ return element._loadComments();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_getFiles add files with comments without changes', () => {
+ const patchChangeRecord = {
+ base: {
+ basePatchNum: '5',
+ patchNum: '10',
+ },
+ };
+ const changeComments = {
+ getPaths: sandbox.stub().returns({
+ 'file2.txt': {},
+ 'file1.txt': {},
+ }),
+ };
+ return element._getFiles(23, patchChangeRecord, changeComments)
+ .then(() => {
+ assert.deepEqual(element._files, {
+ sortedFileList: ['a/b/test.c', 'file1.txt', 'file2.txt'],
+ changeFilesByPath: {
+ 'file1.txt': {},
+ 'file2.txt': {status: 'U'},
+ 'a/b/test.c': {},
+ },
+ });
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
index 00907d6..cd43803 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
@@ -19,193 +19,189 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-group</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="gr-diff-line.js"></script>
-<script src="gr-diff-group.js"></script>
-
-<script>
- suite('gr-diff-group tests', async () => {
- await readyToTest();
- test('delta line pairs', () => {
- let group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
- const l1 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 128);
- const l2 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 129);
- const l3 = new GrDiffLine(GrDiffLine.Type.REMOVE, 64, 0);
- group.addLine(l1);
- group.addLine(l2);
- group.addLine(l3);
- assert.deepEqual(group.lines, [l1, l2, l3]);
- assert.deepEqual(group.adds, [l1, l2]);
- assert.deepEqual(group.removes, [l3]);
- assert.deepEqual(group.lineRange, {
- left: {start: 64, end: 64},
- right: {start: 128, end: 129},
- });
-
- let pairs = group.getSideBySidePairs();
- assert.deepEqual(pairs, [
- {left: l3, right: l1},
- {left: GrDiffLine.BLANK_LINE, right: l2},
- ]);
-
- group = new GrDiffGroup(GrDiffGroup.Type.DELTA, [l1, l2, l3]);
- assert.deepEqual(group.lines, [l1, l2, l3]);
- assert.deepEqual(group.adds, [l1, l2]);
- assert.deepEqual(group.removes, [l3]);
-
- pairs = group.getSideBySidePairs();
- assert.deepEqual(pairs, [
- {left: l3, right: l1},
- {left: GrDiffLine.BLANK_LINE, right: l2},
- ]);
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-diff-line.js';
+import './gr-diff-group.js';
+suite('gr-diff-group tests', () => {
+ test('delta line pairs', () => {
+ let group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
+ const l1 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 128);
+ const l2 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 129);
+ const l3 = new GrDiffLine(GrDiffLine.Type.REMOVE, 64, 0);
+ group.addLine(l1);
+ group.addLine(l2);
+ group.addLine(l3);
+ assert.deepEqual(group.lines, [l1, l2, l3]);
+ assert.deepEqual(group.adds, [l1, l2]);
+ assert.deepEqual(group.removes, [l3]);
+ assert.deepEqual(group.lineRange, {
+ left: {start: 64, end: 64},
+ right: {start: 128, end: 129},
});
- test('group/header line pairs', () => {
- const l1 = new GrDiffLine(GrDiffLine.Type.BOTH, 64, 128);
- const l2 = new GrDiffLine(GrDiffLine.Type.BOTH, 65, 129);
- const l3 = new GrDiffLine(GrDiffLine.Type.BOTH, 66, 130);
+ let pairs = group.getSideBySidePairs();
+ assert.deepEqual(pairs, [
+ {left: l3, right: l1},
+ {left: GrDiffLine.BLANK_LINE, right: l2},
+ ]);
- let group = new GrDiffGroup(GrDiffGroup.Type.BOTH, [l1, l2, l3]);
+ group = new GrDiffGroup(GrDiffGroup.Type.DELTA, [l1, l2, l3]);
+ assert.deepEqual(group.lines, [l1, l2, l3]);
+ assert.deepEqual(group.adds, [l1, l2]);
+ assert.deepEqual(group.removes, [l3]);
- assert.deepEqual(group.lines, [l1, l2, l3]);
- assert.deepEqual(group.adds, []);
- assert.deepEqual(group.removes, []);
-
- assert.deepEqual(group.lineRange, {
- left: {start: 64, end: 66},
- right: {start: 128, end: 130},
- });
-
- let pairs = group.getSideBySidePairs();
- assert.deepEqual(pairs, [
- {left: l1, right: l1},
- {left: l2, right: l2},
- {left: l3, right: l3},
- ]);
-
- group = new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL, [l1, l2, l3]);
- assert.deepEqual(group.lines, [l1, l2, l3]);
- assert.deepEqual(group.adds, []);
- assert.deepEqual(group.removes, []);
-
- pairs = group.getSideBySidePairs();
- assert.deepEqual(pairs, [
- {left: l1, right: l1},
- {left: l2, right: l2},
- {left: l3, right: l3},
- ]);
- });
-
- test('adding delta lines to non-delta group', () => {
- const l1 = new GrDiffLine(GrDiffLine.Type.ADD);
- const l2 = new GrDiffLine(GrDiffLine.Type.REMOVE);
- const l3 = new GrDiffLine(GrDiffLine.Type.BOTH);
-
- let group = new GrDiffGroup(GrDiffGroup.Type.BOTH);
- assert.throws(group.addLine.bind(group, l1));
- assert.throws(group.addLine.bind(group, l2));
- assert.doesNotThrow(group.addLine.bind(group, l3));
-
- group = new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL);
- assert.throws(group.addLine.bind(group, l1));
- assert.throws(group.addLine.bind(group, l2));
- assert.doesNotThrow(group.addLine.bind(group, l3));
- });
-
- suite('hideInContextControl', () => {
- let groups;
- setup(() => {
- groups = [
- new GrDiffGroup(GrDiffGroup.Type.BOTH, [
- new GrDiffLine(GrDiffLine.Type.BOTH, 5, 7),
- new GrDiffLine(GrDiffLine.Type.BOTH, 6, 8),
- new GrDiffLine(GrDiffLine.Type.BOTH, 7, 9),
- ]),
- new GrDiffGroup(GrDiffGroup.Type.DELTA, [
- new GrDiffLine(GrDiffLine.Type.REMOVE, 8),
- new GrDiffLine(GrDiffLine.Type.ADD, 0, 10),
- new GrDiffLine(GrDiffLine.Type.REMOVE, 9),
- new GrDiffLine(GrDiffLine.Type.ADD, 0, 11),
- new GrDiffLine(GrDiffLine.Type.REMOVE, 10),
- new GrDiffLine(GrDiffLine.Type.ADD, 0, 12),
- ]),
- new GrDiffGroup(GrDiffGroup.Type.BOTH, [
- new GrDiffLine(GrDiffLine.Type.BOTH, 11, 13),
- new GrDiffLine(GrDiffLine.Type.BOTH, 12, 14),
- new GrDiffLine(GrDiffLine.Type.BOTH, 13, 15),
- ]),
- ];
- });
-
- test('hides hidden groups in context control', () => {
- const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 3, 6);
- assert.equal(collapsedGroups.length, 3);
-
- assert.equal(collapsedGroups[0], groups[0]);
-
- assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
- assert.equal(collapsedGroups[1].lines.length, 1);
- assert.equal(
- collapsedGroups[1].lines[0].type, GrDiffLine.Type.CONTEXT_CONTROL);
- assert.equal(
- collapsedGroups[1].lines[0].contextGroups.length, 1);
- assert.equal(
- collapsedGroups[1].lines[0].contextGroups[0], groups[1]);
-
- assert.equal(collapsedGroups[2], groups[2]);
- });
-
- test('splits partially hidden groups', () => {
- const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 4, 7);
- assert.equal(collapsedGroups.length, 4);
- assert.equal(collapsedGroups[0], groups[0]);
-
- assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.DELTA);
- assert.deepEqual(collapsedGroups[1].adds, [groups[1].adds[0]]);
- assert.deepEqual(collapsedGroups[1].removes, [groups[1].removes[0]]);
-
- assert.equal(collapsedGroups[2].type, GrDiffGroup.Type.CONTEXT_CONTROL);
- assert.equal(collapsedGroups[2].lines.length, 1);
- assert.equal(
- collapsedGroups[2].lines[0].type, GrDiffLine.Type.CONTEXT_CONTROL);
- assert.equal(
- collapsedGroups[2].lines[0].contextGroups.length, 2);
-
- assert.equal(
- collapsedGroups[2].lines[0].contextGroups[0].type,
- GrDiffGroup.Type.DELTA);
- assert.deepEqual(
- collapsedGroups[2].lines[0].contextGroups[0].adds,
- groups[1].adds.slice(1));
- assert.deepEqual(
- collapsedGroups[2].lines[0].contextGroups[0].removes,
- groups[1].removes.slice(1));
-
- assert.equal(
- collapsedGroups[2].lines[0].contextGroups[1].type,
- GrDiffGroup.Type.BOTH);
- assert.deepEqual(
- collapsedGroups[2].lines[0].contextGroups[1].lines,
- [groups[2].lines[0]]);
-
- assert.equal(collapsedGroups[3].type, GrDiffGroup.Type.BOTH);
- assert.deepEqual(collapsedGroups[3].lines, groups[2].lines.slice(1));
- });
-
- test('groups unchanged if the hidden range is empty', () => {
- assert.deepEqual(
- GrDiffGroup.hideInContextControl(groups, 0, 0), groups);
- });
-
- test('groups unchanged if there is only 1 line to hide', () => {
- assert.deepEqual(
- GrDiffGroup.hideInContextControl(groups, 3, 4), groups);
- });
- });
+ pairs = group.getSideBySidePairs();
+ assert.deepEqual(pairs, [
+ {left: l3, right: l1},
+ {left: GrDiffLine.BLANK_LINE, right: l2},
+ ]);
});
+ test('group/header line pairs', () => {
+ const l1 = new GrDiffLine(GrDiffLine.Type.BOTH, 64, 128);
+ const l2 = new GrDiffLine(GrDiffLine.Type.BOTH, 65, 129);
+ const l3 = new GrDiffLine(GrDiffLine.Type.BOTH, 66, 130);
+
+ let group = new GrDiffGroup(GrDiffGroup.Type.BOTH, [l1, l2, l3]);
+
+ assert.deepEqual(group.lines, [l1, l2, l3]);
+ assert.deepEqual(group.adds, []);
+ assert.deepEqual(group.removes, []);
+
+ assert.deepEqual(group.lineRange, {
+ left: {start: 64, end: 66},
+ right: {start: 128, end: 130},
+ });
+
+ let pairs = group.getSideBySidePairs();
+ assert.deepEqual(pairs, [
+ {left: l1, right: l1},
+ {left: l2, right: l2},
+ {left: l3, right: l3},
+ ]);
+
+ group = new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL, [l1, l2, l3]);
+ assert.deepEqual(group.lines, [l1, l2, l3]);
+ assert.deepEqual(group.adds, []);
+ assert.deepEqual(group.removes, []);
+
+ pairs = group.getSideBySidePairs();
+ assert.deepEqual(pairs, [
+ {left: l1, right: l1},
+ {left: l2, right: l2},
+ {left: l3, right: l3},
+ ]);
+ });
+
+ test('adding delta lines to non-delta group', () => {
+ const l1 = new GrDiffLine(GrDiffLine.Type.ADD);
+ const l2 = new GrDiffLine(GrDiffLine.Type.REMOVE);
+ const l3 = new GrDiffLine(GrDiffLine.Type.BOTH);
+
+ let group = new GrDiffGroup(GrDiffGroup.Type.BOTH);
+ assert.throws(group.addLine.bind(group, l1));
+ assert.throws(group.addLine.bind(group, l2));
+ assert.doesNotThrow(group.addLine.bind(group, l3));
+
+ group = new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.throws(group.addLine.bind(group, l1));
+ assert.throws(group.addLine.bind(group, l2));
+ assert.doesNotThrow(group.addLine.bind(group, l3));
+ });
+
+ suite('hideInContextControl', () => {
+ let groups;
+ setup(() => {
+ groups = [
+ new GrDiffGroup(GrDiffGroup.Type.BOTH, [
+ new GrDiffLine(GrDiffLine.Type.BOTH, 5, 7),
+ new GrDiffLine(GrDiffLine.Type.BOTH, 6, 8),
+ new GrDiffLine(GrDiffLine.Type.BOTH, 7, 9),
+ ]),
+ new GrDiffGroup(GrDiffGroup.Type.DELTA, [
+ new GrDiffLine(GrDiffLine.Type.REMOVE, 8),
+ new GrDiffLine(GrDiffLine.Type.ADD, 0, 10),
+ new GrDiffLine(GrDiffLine.Type.REMOVE, 9),
+ new GrDiffLine(GrDiffLine.Type.ADD, 0, 11),
+ new GrDiffLine(GrDiffLine.Type.REMOVE, 10),
+ new GrDiffLine(GrDiffLine.Type.ADD, 0, 12),
+ ]),
+ new GrDiffGroup(GrDiffGroup.Type.BOTH, [
+ new GrDiffLine(GrDiffLine.Type.BOTH, 11, 13),
+ new GrDiffLine(GrDiffLine.Type.BOTH, 12, 14),
+ new GrDiffLine(GrDiffLine.Type.BOTH, 13, 15),
+ ]),
+ ];
+ });
+
+ test('hides hidden groups in context control', () => {
+ const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 3, 6);
+ assert.equal(collapsedGroups.length, 3);
+
+ assert.equal(collapsedGroups[0], groups[0]);
+
+ assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.equal(collapsedGroups[1].lines.length, 1);
+ assert.equal(
+ collapsedGroups[1].lines[0].type, GrDiffLine.Type.CONTEXT_CONTROL);
+ assert.equal(
+ collapsedGroups[1].lines[0].contextGroups.length, 1);
+ assert.equal(
+ collapsedGroups[1].lines[0].contextGroups[0], groups[1]);
+
+ assert.equal(collapsedGroups[2], groups[2]);
+ });
+
+ test('splits partially hidden groups', () => {
+ const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 4, 7);
+ assert.equal(collapsedGroups.length, 4);
+ assert.equal(collapsedGroups[0], groups[0]);
+
+ assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.DELTA);
+ assert.deepEqual(collapsedGroups[1].adds, [groups[1].adds[0]]);
+ assert.deepEqual(collapsedGroups[1].removes, [groups[1].removes[0]]);
+
+ assert.equal(collapsedGroups[2].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.equal(collapsedGroups[2].lines.length, 1);
+ assert.equal(
+ collapsedGroups[2].lines[0].type, GrDiffLine.Type.CONTEXT_CONTROL);
+ assert.equal(
+ collapsedGroups[2].lines[0].contextGroups.length, 2);
+
+ assert.equal(
+ collapsedGroups[2].lines[0].contextGroups[0].type,
+ GrDiffGroup.Type.DELTA);
+ assert.deepEqual(
+ collapsedGroups[2].lines[0].contextGroups[0].adds,
+ groups[1].adds.slice(1));
+ assert.deepEqual(
+ collapsedGroups[2].lines[0].contextGroups[0].removes,
+ groups[1].removes.slice(1));
+
+ assert.equal(
+ collapsedGroups[2].lines[0].contextGroups[1].type,
+ GrDiffGroup.Type.BOTH);
+ assert.deepEqual(
+ collapsedGroups[2].lines[0].contextGroups[1].lines,
+ [groups[2].lines[0]]);
+
+ assert.equal(collapsedGroups[3].type, GrDiffGroup.Type.BOTH);
+ assert.deepEqual(collapsedGroups[3].lines, groups[2].lines.slice(1));
+ });
+
+ test('groups unchanged if the hidden range is empty', () => {
+ assert.deepEqual(
+ GrDiffGroup.hideInContextControl(groups, 0, 0), groups);
+ });
+
+ test('groups unchanged if there is only 1 line to hide', () => {
+ assert.deepEqual(
+ GrDiffGroup.hideInContextControl(groups, 3, 4), groups);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 8e96147..a3076f0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,964 +14,987 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const ERR_COMMENT_ON_EDIT = 'You cannot comment on an edit.';
- const ERR_COMMENT_ON_EDIT_BASE = 'You cannot comment on the base patch set ' +
- 'of an edit.';
- const ERR_INVALID_LINE = 'Invalid line number: ';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../gr-diff-builder/gr-diff-builder-element.js';
+import '../gr-diff-highlight/gr-diff-highlight.js';
+import '../gr-diff-selection/gr-diff-selection.js';
+import '../gr-syntax-themes/gr-syntax-theme.js';
+import '../gr-ranged-comment-themes/gr-ranged-comment-theme.js';
+import '../../../scripts/hiddenscroll.js';
+import './gr-diff-line.js';
+import './gr-diff-group.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {htmlTemplate} from './gr-diff_html.js';
- const NO_NEWLINE_BASE = 'No newline at end of base file.';
- const NO_NEWLINE_REVISION = 'No newline at end of revision file.';
+const ERR_COMMENT_ON_EDIT = 'You cannot comment on an edit.';
+const ERR_COMMENT_ON_EDIT_BASE = 'You cannot comment on the base patch set ' +
+ 'of an edit.';
+const ERR_INVALID_LINE = 'Invalid line number: ';
- const DiffViewMode = {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- };
+const NO_NEWLINE_BASE = 'No newline at end of base file.';
+const NO_NEWLINE_REVISION = 'No newline at end of revision file.';
- const DiffSide = {
- LEFT: 'left',
- RIGHT: 'right',
- };
+const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+};
- const LARGE_DIFF_THRESHOLD_LINES = 10000;
- const FULL_CONTEXT = -1;
- const LIMITED_CONTEXT = 10;
+const DiffSide = {
+ LEFT: 'left',
+ RIGHT: 'right',
+};
+
+const LARGE_DIFF_THRESHOLD_LINES = 10000;
+const FULL_CONTEXT = -1;
+const LIMITED_CONTEXT = 10;
+
+/**
+ * Compare two ranges. Either argument may be falsy, but will only return
+ * true if both are falsy or if neither are falsy and have the same position
+ * values.
+ *
+ * @param {Gerrit.Range=} a range 1
+ * @param {Gerrit.Range=} b range 2
+ * @return {boolean}
+ */
+Gerrit.rangesEqual = function(a, b) {
+ if (!a && !b) { return true; }
+ if (!a || !b) { return false; }
+ return a.start_line === b.start_line &&
+ a.start_character === b.start_character &&
+ a.end_line === b.end_line &&
+ a.end_character === b.end_character;
+};
+
+function isThreadEl(node) {
+ return node.nodeType === Node.ELEMENT_NODE &&
+ node.classList.contains('comment-thread');
+}
+
+/**
+ * Turn a slot element into the corresponding content element.
+ * Slots are only fully supported in Polymer 2 - in Polymer 1, they are
+ * replaced with content elements during template parsing. This conversion is
+ * not applied for imperatively created slot elements, so this method
+ * implements the same behavior as the template parsing for imperative slots.
+ */
+Gerrit.slotToContent = function(slot) {
+ if (PolymerElement) {
+ return slot;
+ }
+ const content = document.createElement('content');
+ content.name = slot.name;
+ content.setAttribute('select', `[slot='${slot.name}']`);
+ return content;
+};
+
+const COMMIT_MSG_PATH = '/COMMIT_MSG';
+/**
+ * 72 is the inofficial length standard for git commit messages.
+ * Derived from the fact that git log/show appends 4 ws in the beginning of
+ * each line when displaying commit messages. To center the commit message
+ * in an 80 char terminal a 4 ws border is added to the rightmost side:
+ * 4 + 72 + 4
+ */
+const COMMIT_MSG_LINE_LENGTH = 72;
+
+const RENDER_DIFF_TABLE_DEBOUNCE_NAME = 'renderDiffTable';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @extends Polymer.Element
+ */
+class GrDiff extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.PatchSetBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-diff'; }
+ /**
+ * Fired when the user selects a line.
+ *
+ * @event line-selected
+ */
/**
- * Compare two ranges. Either argument may be falsy, but will only return
- * true if both are falsy or if neither are falsy and have the same position
- * values.
+ * Fired if being logged in is required.
*
- * @param {Gerrit.Range=} a range 1
- * @param {Gerrit.Range=} b range 2
- * @return {boolean}
+ * @event show-auth-required
*/
- Gerrit.rangesEqual = function(a, b) {
- if (!a && !b) { return true; }
- if (!a || !b) { return false; }
- return a.start_line === b.start_line &&
- a.start_character === b.start_character &&
- a.end_line === b.end_line &&
- a.end_character === b.end_character;
- };
- function isThreadEl(node) {
- return node.nodeType === Node.ELEMENT_NODE &&
- node.classList.contains('comment-thread');
+ /**
+ * Fired when a comment is created
+ *
+ * @event create-comment
+ */
+
+ /**
+ * Fired when rendering, including syntax highlighting, is done. Also fired
+ * when no rendering can be done because required preferences are not set.
+ *
+ * @event render
+ */
+
+ /**
+ * Fired for interaction reporting when a diff context is expanded.
+ * Contains an event.detail with numLines about the number of lines that
+ * were expanded.
+ *
+ * @event diff-context-expanded
+ */
+
+ static get properties() {
+ return {
+ changeNum: String,
+ noAutoRender: {
+ type: Boolean,
+ value: false,
+ },
+ /** @type {?} */
+ patchRange: Object,
+ path: {
+ type: String,
+ observer: '_pathObserver',
+ },
+ prefs: {
+ type: Object,
+ observer: '_prefsObserver',
+ },
+ projectName: String,
+ displayLine: {
+ type: Boolean,
+ value: false,
+ },
+ isImageDiff: {
+ type: Boolean,
+ },
+ commitRange: Object,
+ hidden: {
+ type: Boolean,
+ reflectToAttribute: true,
+ },
+ noRenderOnPrefsChange: Boolean,
+ /** @type {!Array<!Gerrit.HoveredRange>} */
+ _commentRanges: {
+ type: Array,
+ value: () => [],
+ },
+ /** @type {!Array<!Gerrit.CoverageRange>} */
+ coverageRanges: {
+ type: Array,
+ value: () => [],
+ },
+ lineWrapping: {
+ type: Boolean,
+ value: false,
+ observer: '_lineWrappingObserver',
+ },
+ viewMode: {
+ type: String,
+ value: DiffViewMode.SIDE_BY_SIDE,
+ observer: '_viewModeObserver',
+ },
+
+ /** @type {?Gerrit.LineOfInterest} */
+ lineOfInterest: Object,
+
+ loading: {
+ type: Boolean,
+ value: false,
+ observer: '_loadingChanged',
+ },
+
+ loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+ diff: {
+ type: Object,
+ observer: '_diffChanged',
+ },
+ _diffHeaderItems: {
+ type: Array,
+ value: [],
+ computed: '_computeDiffHeaderItems(diff.*)',
+ },
+ _diffTableClass: {
+ type: String,
+ value: '',
+ },
+ /** @type {?Object} */
+ baseImage: Object,
+ /** @type {?Object} */
+ revisionImage: Object,
+
+ /**
+ * Whether the safety check for large diffs when whole-file is set has
+ * been bypassed. If the value is null, then the safety has not been
+ * bypassed. If the value is a number, then that number represents the
+ * context preference to use when rendering the bypassed diff.
+ *
+ * @type {number|null}
+ */
+ _safetyBypass: {
+ type: Number,
+ value: null,
+ },
+
+ _showWarning: Boolean,
+
+ /** @type {?string} */
+ errorMessage: {
+ type: String,
+ value: null,
+ },
+
+ /** @type {?Object} */
+ blame: {
+ type: Object,
+ value: null,
+ observer: '_blameChanged',
+ },
+
+ parentIndex: Number,
+
+ showNewlineWarningLeft: {
+ type: Boolean,
+ value: false,
+ },
+ showNewlineWarningRight: {
+ type: Boolean,
+ value: false,
+ },
+
+ _newlineWarning: {
+ type: String,
+ computed: '_computeNewlineWarning(' +
+ 'showNewlineWarningLeft, showNewlineWarningRight)',
+ },
+
+ _diffLength: Number,
+
+ /**
+ * Observes comment nodes added or removed after the initial render.
+ * Can be used to unregister when the entire diff is (re-)rendered or upon
+ * detachment.
+ *
+ * @type {?PolymerDomApi.ObserveHandle}
+ */
+ _incrementalNodeObserver: Object,
+
+ /**
+ * Observes comment nodes added or removed at any point.
+ * Can be used to unregister upon detachment.
+ *
+ * @type {?PolymerDomApi.ObserveHandle}
+ */
+ _nodeObserver: Object,
+
+ /** Set by Polymer. */
+ isAttached: Boolean,
+ layers: Array,
+ };
+ }
+
+ static get observers() {
+ return [
+ '_enableSelectionObserver(loggedIn, isAttached)',
+ ];
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('create-range-comment',
+ e => this._handleCreateRangeComment(e));
+ this.addEventListener('render-content',
+ () => this._handleRenderContent());
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._observeNodes();
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this._unobserveIncrementalNodes();
+ this._unobserveNodes();
+ }
+
+ showNoChangeMessage(loading, prefs, diffLength) {
+ return !loading &&
+ prefs && prefs.ignore_whitespace !== 'IGNORE_NONE' &&
+ diffLength === 0;
+ }
+
+ _enableSelectionObserver(loggedIn, isAttached) {
+ // Polymer 2: check for undefined
+ if ([loggedIn, isAttached].some(arg => arg === undefined)) {
+ return;
+ }
+
+ if (loggedIn && isAttached) {
+ this.listen(document, 'selectionchange', '_handleSelectionChange');
+ this.listen(document, 'mouseup', '_handleMouseUp');
+ } else {
+ this.unlisten(document, 'selectionchange', '_handleSelectionChange');
+ this.unlisten(document, 'mouseup', '_handleMouseUp');
+ }
+ }
+
+ _handleSelectionChange() {
+ // Because of shadow DOM selections, we handle the selectionchange here,
+ // and pass the shadow DOM selection into gr-diff-highlight, where the
+ // corresponding range is determined and normalized.
+ const selection = this._getShadowOrDocumentSelection();
+ this.$.highlights.handleSelectionChange(selection, false);
+ }
+
+ _handleMouseUp(e) {
+ // To handle double-click outside of text creating comments, we check on
+ // mouse-up if there's a selection that just covers a line change. We
+ // can't do that on selection change since the user may still be dragging.
+ const selection = this._getShadowOrDocumentSelection();
+ this.$.highlights.handleSelectionChange(selection, true);
+ }
+
+ /** Gets the current selection, preferring the shadow DOM selection. */
+ _getShadowOrDocumentSelection() {
+ // When using native shadow DOM, the selection returned by
+ // document.getSelection() cannot reference the actual DOM elements making
+ // up the diff, because they are in the shadow DOM of the gr-diff element.
+ // This takes the shadow DOM selection if one exists.
+ return this.root.getSelection ?
+ this.root.getSelection() :
+ document.getSelection();
+ }
+
+ _observeNodes() {
+ this._nodeObserver = dom(this).observeNodes(info => {
+ const addedThreadEls = info.addedNodes.filter(isThreadEl);
+ const removedThreadEls = info.removedNodes.filter(isThreadEl);
+ this._updateRanges(addedThreadEls, removedThreadEls);
+ this._redispatchHoverEvents(addedThreadEls);
+ });
+ }
+
+ _updateRanges(addedThreadEls, removedThreadEls) {
+ function commentRangeFromThreadEl(threadEl) {
+ const side = threadEl.getAttribute('comment-side');
+ const range = JSON.parse(threadEl.getAttribute('range'));
+ return {side, range, hovering: false};
+ }
+
+ const addedCommentRanges = addedThreadEls
+ .map(commentRangeFromThreadEl)
+ .filter(({range}) => range);
+ const removedCommentRanges = removedThreadEls
+ .map(commentRangeFromThreadEl)
+ .filter(({range}) => range);
+ for (const removedCommentRange of removedCommentRanges) {
+ const i = this._commentRanges
+ .findIndex(
+ cr => cr.side === removedCommentRange.side &&
+ Gerrit.rangesEqual(cr.range, removedCommentRange.range)
+ );
+ this.splice('_commentRanges', i, 1);
+ }
+
+ if (addedCommentRanges && addedCommentRanges.length) {
+ this.push('_commentRanges', ...addedCommentRanges);
+ }
}
/**
- * Turn a slot element into the corresponding content element.
- * Slots are only fully supported in Polymer 2 - in Polymer 1, they are
- * replaced with content elements during template parsing. This conversion is
- * not applied for imperatively created slot elements, so this method
- * implements the same behavior as the template parsing for imperative slots.
+ * The key locations based on the comments and line of interests,
+ * where lines should not be collapsed.
+ *
+ * @return {{left: Object<(string|number), boolean>,
+ * right: Object<(string|number), boolean>}}
*/
- Gerrit.slotToContent = function(slot) {
- if (Polymer.Element) {
- return slot;
+ _computeKeyLocations() {
+ const keyLocations = {left: {}, right: {}};
+ if (this.lineOfInterest) {
+ const side = this.lineOfInterest.leftSide ? 'left' : 'right';
+ keyLocations[side][this.lineOfInterest.number] = true;
}
- const content = document.createElement('content');
- content.name = slot.name;
- content.setAttribute('select', `[slot='${slot.name}']`);
- return content;
- };
+ const threadEls = dom(this).getEffectiveChildNodes()
+ .filter(isThreadEl);
- const COMMIT_MSG_PATH = '/COMMIT_MSG';
- /**
- * 72 is the inofficial length standard for git commit messages.
- * Derived from the fact that git log/show appends 4 ws in the beginning of
- * each line when displaying commit messages. To center the commit message
- * in an 80 char terminal a 4 ws border is added to the rightmost side:
- * 4 + 72 + 4
- */
- const COMMIT_MSG_LINE_LENGTH = 72;
+ for (const threadEl of threadEls) {
+ const commentSide = threadEl.getAttribute('comment-side');
+ const lineNum = Number(threadEl.getAttribute('line-num')) ||
+ GrDiffLine.FILE;
+ const commentRange = threadEl.range || {};
+ keyLocations[commentSide][lineNum] = true;
+ // Add start_line as well if exists,
+ // the being and end of the range should not be collapsed.
+ if (commentRange.start_line) {
+ keyLocations[commentSide][commentRange.start_line] = true;
+ }
+ }
+ return keyLocations;
+ }
- const RENDER_DIFF_TABLE_DEBOUNCE_NAME = 'renderDiffTable';
+ // Dispatch events that are handled by the gr-diff-highlight.
+ _redispatchHoverEvents(addedThreadEls) {
+ for (const threadEl of addedThreadEls) {
+ threadEl.addEventListener('mouseenter', () => {
+ threadEl.dispatchEvent(new CustomEvent(
+ 'comment-thread-mouseenter', {bubbles: true, composed: true}));
+ });
+ threadEl.addEventListener('mouseleave', () => {
+ threadEl.dispatchEvent(new CustomEvent(
+ 'comment-thread-mouseleave', {bubbles: true, composed: true}));
+ });
+ }
+ }
- /**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @extends Polymer.Element
- */
- class GrDiff extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.PatchSetBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-diff'; }
- /**
- * Fired when the user selects a line.
- *
- * @event line-selected
- */
+ /** Cancel any remaining diff builder rendering work. */
+ cancel() {
+ this.$.diffBuilder.cancel();
+ this.cancelDebouncer(RENDER_DIFF_TABLE_DEBOUNCE_NAME);
+ }
- /**
- * Fired if being logged in is required.
- *
- * @event show-auth-required
- */
-
- /**
- * Fired when a comment is created
- *
- * @event create-comment
- */
-
- /**
- * Fired when rendering, including syntax highlighting, is done. Also fired
- * when no rendering can be done because required preferences are not set.
- *
- * @event render
- */
-
- /**
- * Fired for interaction reporting when a diff context is expanded.
- * Contains an event.detail with numLines about the number of lines that
- * were expanded.
- *
- * @event diff-context-expanded
- */
-
- static get properties() {
- return {
- changeNum: String,
- noAutoRender: {
- type: Boolean,
- value: false,
- },
- /** @type {?} */
- patchRange: Object,
- path: {
- type: String,
- observer: '_pathObserver',
- },
- prefs: {
- type: Object,
- observer: '_prefsObserver',
- },
- projectName: String,
- displayLine: {
- type: Boolean,
- value: false,
- },
- isImageDiff: {
- type: Boolean,
- },
- commitRange: Object,
- hidden: {
- type: Boolean,
- reflectToAttribute: true,
- },
- noRenderOnPrefsChange: Boolean,
- /** @type {!Array<!Gerrit.HoveredRange>} */
- _commentRanges: {
- type: Array,
- value: () => [],
- },
- /** @type {!Array<!Gerrit.CoverageRange>} */
- coverageRanges: {
- type: Array,
- value: () => [],
- },
- lineWrapping: {
- type: Boolean,
- value: false,
- observer: '_lineWrappingObserver',
- },
- viewMode: {
- type: String,
- value: DiffViewMode.SIDE_BY_SIDE,
- observer: '_viewModeObserver',
- },
-
- /** @type {?Gerrit.LineOfInterest} */
- lineOfInterest: Object,
-
- loading: {
- type: Boolean,
- value: false,
- observer: '_loadingChanged',
- },
-
- loggedIn: {
- type: Boolean,
- value: false,
- },
- diff: {
- type: Object,
- observer: '_diffChanged',
- },
- _diffHeaderItems: {
- type: Array,
- value: [],
- computed: '_computeDiffHeaderItems(diff.*)',
- },
- _diffTableClass: {
- type: String,
- value: '',
- },
- /** @type {?Object} */
- baseImage: Object,
- /** @type {?Object} */
- revisionImage: Object,
-
- /**
- * Whether the safety check for large diffs when whole-file is set has
- * been bypassed. If the value is null, then the safety has not been
- * bypassed. If the value is a number, then that number represents the
- * context preference to use when rendering the bypassed diff.
- *
- * @type {number|null}
- */
- _safetyBypass: {
- type: Number,
- value: null,
- },
-
- _showWarning: Boolean,
-
- /** @type {?string} */
- errorMessage: {
- type: String,
- value: null,
- },
-
- /** @type {?Object} */
- blame: {
- type: Object,
- value: null,
- observer: '_blameChanged',
- },
-
- parentIndex: Number,
-
- showNewlineWarningLeft: {
- type: Boolean,
- value: false,
- },
- showNewlineWarningRight: {
- type: Boolean,
- value: false,
- },
-
- _newlineWarning: {
- type: String,
- computed: '_computeNewlineWarning(' +
- 'showNewlineWarningLeft, showNewlineWarningRight)',
- },
-
- _diffLength: Number,
-
- /**
- * Observes comment nodes added or removed after the initial render.
- * Can be used to unregister when the entire diff is (re-)rendered or upon
- * detachment.
- *
- * @type {?PolymerDomApi.ObserveHandle}
- */
- _incrementalNodeObserver: Object,
-
- /**
- * Observes comment nodes added or removed at any point.
- * Can be used to unregister upon detachment.
- *
- * @type {?PolymerDomApi.ObserveHandle}
- */
- _nodeObserver: Object,
-
- /** Set by Polymer. */
- isAttached: Boolean,
- layers: Array,
- };
+ /** @return {!Array<!HTMLElement>} */
+ getCursorStops() {
+ if (this.hidden && this.noAutoRender) {
+ return [];
}
- static get observers() {
- return [
- '_enableSelectionObserver(loggedIn, isAttached)',
- ];
- }
+ return Array.from(
+ dom(this.root).querySelectorAll('.diff-row'));
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('create-range-comment',
- e => this._handleCreateRangeComment(e));
- this.addEventListener('render-content',
- () => this._handleRenderContent());
- }
+ /** @return {boolean} */
+ isRangeSelected() {
+ return !!this.$.highlights.selectedRange;
+ }
- /** @override */
- attached() {
- super.attached();
- this._observeNodes();
- }
+ toggleLeftDiff() {
+ this.toggleClass('no-left');
+ }
- /** @override */
- detached() {
- super.detached();
- this._unobserveIncrementalNodes();
- this._unobserveNodes();
+ _blameChanged(newValue) {
+ this.$.diffBuilder.setBlame(newValue);
+ if (newValue) {
+ this.classList.add('showBlame');
+ } else {
+ this.classList.remove('showBlame');
}
+ }
- showNoChangeMessage(loading, prefs, diffLength) {
- return !loading &&
- prefs && prefs.ignore_whitespace !== 'IGNORE_NONE' &&
- diffLength === 0;
+ /** @return {string} */
+ _computeContainerClass(loggedIn, viewMode, displayLine) {
+ const classes = ['diffContainer'];
+ switch (viewMode) {
+ case DiffViewMode.UNIFIED:
+ classes.push('unified');
+ break;
+ case DiffViewMode.SIDE_BY_SIDE:
+ classes.push('sideBySide');
+ break;
+ default:
+ throw Error('Invalid view mode: ', viewMode);
}
+ if (Gerrit.hiddenscroll) {
+ classes.push('hiddenscroll');
+ }
+ if (loggedIn) {
+ classes.push('canComment');
+ }
+ if (displayLine) {
+ classes.push('displayLine');
+ }
+ return classes.join(' ');
+ }
- _enableSelectionObserver(loggedIn, isAttached) {
- // Polymer 2: check for undefined
- if ([loggedIn, isAttached].some(arg => arg === undefined)) {
+ _handleTap(e) {
+ const el = dom(e).localTarget;
+
+ if (el.classList.contains('showContext')) {
+ this.fire('diff-context-expanded', {
+ numLines: e.detail.numLines,
+ });
+ this.$.diffBuilder.showContext(e.detail.groups, e.detail.section);
+ } else if (el.classList.contains('lineNum')) {
+ this.addDraftAtLine(el);
+ } else if (el.tagName === 'HL' ||
+ el.classList.contains('content') ||
+ el.classList.contains('contentText')) {
+ const target = this.$.diffBuilder.getLineElByChild(el);
+ if (target) { this._selectLine(target); }
+ }
+ }
+
+ _selectLine(el) {
+ this.fire('line-selected', {
+ side: el.classList.contains('left') ? DiffSide.LEFT : DiffSide.RIGHT,
+ number: el.getAttribute('data-value'),
+ path: this.path,
+ });
+ }
+
+ addDraftAtLine(el) {
+ this._selectLine(el);
+ if (!this._isValidElForComment(el)) { return; }
+
+ const value = el.getAttribute('data-value');
+ let lineNum;
+ if (value !== GrDiffLine.FILE) {
+ lineNum = parseInt(value, 10);
+ if (isNaN(lineNum)) {
+ this.fire('show-alert', {message: ERR_INVALID_LINE + value});
return;
}
-
- if (loggedIn && isAttached) {
- this.listen(document, 'selectionchange', '_handleSelectionChange');
- this.listen(document, 'mouseup', '_handleMouseUp');
- } else {
- this.unlisten(document, 'selectionchange', '_handleSelectionChange');
- this.unlisten(document, 'mouseup', '_handleMouseUp');
- }
}
+ this._createComment(el, lineNum);
+ }
- _handleSelectionChange() {
- // Because of shadow DOM selections, we handle the selectionchange here,
- // and pass the shadow DOM selection into gr-diff-highlight, where the
- // corresponding range is determined and normalized.
- const selection = this._getShadowOrDocumentSelection();
- this.$.highlights.handleSelectionChange(selection, false);
+ createRangeComment() {
+ if (!this.isRangeSelected()) {
+ throw Error('Selection is needed for new range comment');
}
+ const {side, range} = this.$.highlights.selectedRange;
+ this._createCommentForSelection(side, range);
+ }
- _handleMouseUp(e) {
- // To handle double-click outside of text creating comments, we check on
- // mouse-up if there's a selection that just covers a line change. We
- // can't do that on selection change since the user may still be dragging.
- const selection = this._getShadowOrDocumentSelection();
- this.$.highlights.handleSelectionChange(selection, true);
+ _createCommentForSelection(side, range) {
+ const lineNum = range.end_line;
+ const lineEl = this.$.diffBuilder.getLineElByNumber(lineNum, side);
+ if (this._isValidElForComment(lineEl)) {
+ this._createComment(lineEl, lineNum, side, range);
}
+ }
- /** Gets the current selection, preferring the shadow DOM selection. */
- _getShadowOrDocumentSelection() {
- // When using native shadow DOM, the selection returned by
- // document.getSelection() cannot reference the actual DOM elements making
- // up the diff, because they are in the shadow DOM of the gr-diff element.
- // This takes the shadow DOM selection if one exists.
- return this.root.getSelection ?
- this.root.getSelection() :
- document.getSelection();
- }
+ _handleCreateRangeComment(e) {
+ const range = e.detail.range;
+ const side = e.detail.side;
+ this._createCommentForSelection(side, range);
+ }
- _observeNodes() {
- this._nodeObserver = Polymer.dom(this).observeNodes(info => {
- const addedThreadEls = info.addedNodes.filter(isThreadEl);
- const removedThreadEls = info.removedNodes.filter(isThreadEl);
- this._updateRanges(addedThreadEls, removedThreadEls);
- this._redispatchHoverEvents(addedThreadEls);
- });
- }
-
- _updateRanges(addedThreadEls, removedThreadEls) {
- function commentRangeFromThreadEl(threadEl) {
- const side = threadEl.getAttribute('comment-side');
- const range = JSON.parse(threadEl.getAttribute('range'));
- return {side, range, hovering: false};
- }
-
- const addedCommentRanges = addedThreadEls
- .map(commentRangeFromThreadEl)
- .filter(({range}) => range);
- const removedCommentRanges = removedThreadEls
- .map(commentRangeFromThreadEl)
- .filter(({range}) => range);
- for (const removedCommentRange of removedCommentRanges) {
- const i = this._commentRanges
- .findIndex(
- cr => cr.side === removedCommentRange.side &&
- Gerrit.rangesEqual(cr.range, removedCommentRange.range)
- );
- this.splice('_commentRanges', i, 1);
- }
-
- if (addedCommentRanges && addedCommentRanges.length) {
- this.push('_commentRanges', ...addedCommentRanges);
- }
- }
-
- /**
- * The key locations based on the comments and line of interests,
- * where lines should not be collapsed.
- *
- * @return {{left: Object<(string|number), boolean>,
- * right: Object<(string|number), boolean>}}
- */
- _computeKeyLocations() {
- const keyLocations = {left: {}, right: {}};
- if (this.lineOfInterest) {
- const side = this.lineOfInterest.leftSide ? 'left' : 'right';
- keyLocations[side][this.lineOfInterest.number] = true;
- }
- const threadEls = Polymer.dom(this).getEffectiveChildNodes()
- .filter(isThreadEl);
-
- for (const threadEl of threadEls) {
- const commentSide = threadEl.getAttribute('comment-side');
- const lineNum = Number(threadEl.getAttribute('line-num')) ||
- GrDiffLine.FILE;
- const commentRange = threadEl.range || {};
- keyLocations[commentSide][lineNum] = true;
- // Add start_line as well if exists,
- // the being and end of the range should not be collapsed.
- if (commentRange.start_line) {
- keyLocations[commentSide][commentRange.start_line] = true;
- }
- }
- return keyLocations;
- }
-
- // Dispatch events that are handled by the gr-diff-highlight.
- _redispatchHoverEvents(addedThreadEls) {
- for (const threadEl of addedThreadEls) {
- threadEl.addEventListener('mouseenter', () => {
- threadEl.dispatchEvent(new CustomEvent(
- 'comment-thread-mouseenter', {bubbles: true, composed: true}));
- });
- threadEl.addEventListener('mouseleave', () => {
- threadEl.dispatchEvent(new CustomEvent(
- 'comment-thread-mouseleave', {bubbles: true, composed: true}));
- });
- }
- }
-
- /** Cancel any remaining diff builder rendering work. */
- cancel() {
- this.$.diffBuilder.cancel();
- this.cancelDebouncer(RENDER_DIFF_TABLE_DEBOUNCE_NAME);
- }
-
- /** @return {!Array<!HTMLElement>} */
- getCursorStops() {
- if (this.hidden && this.noAutoRender) {
- return [];
- }
-
- return Array.from(
- Polymer.dom(this.root).querySelectorAll('.diff-row'));
- }
-
- /** @return {boolean} */
- isRangeSelected() {
- return !!this.$.highlights.selectedRange;
- }
-
- toggleLeftDiff() {
- this.toggleClass('no-left');
- }
-
- _blameChanged(newValue) {
- this.$.diffBuilder.setBlame(newValue);
- if (newValue) {
- this.classList.add('showBlame');
- } else {
- this.classList.remove('showBlame');
- }
- }
-
- /** @return {string} */
- _computeContainerClass(loggedIn, viewMode, displayLine) {
- const classes = ['diffContainer'];
- switch (viewMode) {
- case DiffViewMode.UNIFIED:
- classes.push('unified');
- break;
- case DiffViewMode.SIDE_BY_SIDE:
- classes.push('sideBySide');
- break;
- default:
- throw Error('Invalid view mode: ', viewMode);
- }
- if (Gerrit.hiddenscroll) {
- classes.push('hiddenscroll');
- }
- if (loggedIn) {
- classes.push('canComment');
- }
- if (displayLine) {
- classes.push('displayLine');
- }
- return classes.join(' ');
- }
-
- _handleTap(e) {
- const el = Polymer.dom(e).localTarget;
-
- if (el.classList.contains('showContext')) {
- this.fire('diff-context-expanded', {
- numLines: e.detail.numLines,
- });
- this.$.diffBuilder.showContext(e.detail.groups, e.detail.section);
- } else if (el.classList.contains('lineNum')) {
- this.addDraftAtLine(el);
- } else if (el.tagName === 'HL' ||
- el.classList.contains('content') ||
- el.classList.contains('contentText')) {
- const target = this.$.diffBuilder.getLineElByChild(el);
- if (target) { this._selectLine(target); }
- }
- }
-
- _selectLine(el) {
- this.fire('line-selected', {
- side: el.classList.contains('left') ? DiffSide.LEFT : DiffSide.RIGHT,
- number: el.getAttribute('data-value'),
- path: this.path,
- });
- }
-
- addDraftAtLine(el) {
- this._selectLine(el);
- if (!this._isValidElForComment(el)) { return; }
-
- const value = el.getAttribute('data-value');
- let lineNum;
- if (value !== GrDiffLine.FILE) {
- lineNum = parseInt(value, 10);
- if (isNaN(lineNum)) {
- this.fire('show-alert', {message: ERR_INVALID_LINE + value});
- return;
- }
- }
- this._createComment(el, lineNum);
- }
-
- createRangeComment() {
- if (!this.isRangeSelected()) {
- throw Error('Selection is needed for new range comment');
- }
- const {side, range} = this.$.highlights.selectedRange;
- this._createCommentForSelection(side, range);
- }
-
- _createCommentForSelection(side, range) {
- const lineNum = range.end_line;
- const lineEl = this.$.diffBuilder.getLineElByNumber(lineNum, side);
- if (this._isValidElForComment(lineEl)) {
- this._createComment(lineEl, lineNum, side, range);
- }
- }
-
- _handleCreateRangeComment(e) {
- const range = e.detail.range;
- const side = e.detail.side;
- this._createCommentForSelection(side, range);
- }
-
- /** @return {boolean} */
- _isValidElForComment(el) {
- if (!this.loggedIn) {
- this.fire('show-auth-required');
- return false;
- }
- const patchNum = el.classList.contains(DiffSide.LEFT) ?
- this.patchRange.basePatchNum :
- this.patchRange.patchNum;
-
- const isEdit = this.patchNumEquals(patchNum, this.EDIT_NAME);
- const isEditBase = this.patchNumEquals(patchNum, this.PARENT_NAME) &&
- this.patchNumEquals(this.patchRange.patchNum, this.EDIT_NAME);
-
- if (isEdit) {
- this.fire('show-alert', {message: ERR_COMMENT_ON_EDIT});
- return false;
- } else if (isEditBase) {
- this.fire('show-alert', {message: ERR_COMMENT_ON_EDIT_BASE});
- return false;
- }
- return true;
- }
-
- /**
- * @param {!Object} lineEl
- * @param {number=} lineNum
- * @param {string=} side
- * @param {!Object=} range
- */
- _createComment(lineEl, lineNum, side, range) {
- const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
- const contentEl = contentText.parentElement;
- side = side ||
- this._getCommentSideByLineAndContent(lineEl, contentEl);
- const patchForNewThreads = this._getPatchNumByLineAndContent(
- lineEl, contentEl);
- const isOnParent =
- this._getIsParentCommentByLineAndContent(lineEl, contentEl);
- this.dispatchEvent(new CustomEvent('create-comment', {
- bubbles: true,
- composed: true,
- detail: {
- lineNum,
- side,
- patchNum: patchForNewThreads,
- isOnParent,
- range,
- },
- }));
- }
-
- _getThreadGroupForLine(contentEl) {
- return contentEl.querySelector('.thread-group');
- }
-
- /**
- * Gets or creates a comment thread group for a specific line and side on a
- * diff.
- *
- * @param {!Object} contentEl
- * @param {!Gerrit.DiffSide} commentSide
- * @return {!Node}
- */
- _getOrCreateThreadGroup(contentEl, commentSide) {
- // Check if thread group exists.
- let threadGroupEl = this._getThreadGroupForLine(contentEl);
- if (!threadGroupEl) {
- threadGroupEl = document.createElement('div');
- threadGroupEl.className = 'thread-group';
- threadGroupEl.setAttribute('data-side', commentSide);
- contentEl.appendChild(threadGroupEl);
- }
- return threadGroupEl;
- }
-
- /**
- * The value to be used for the patch number of new comments created at the
- * given line and content elements.
- *
- * In two cases of creating a comment on the left side, the patch number to
- * be used should actually be right side of the patch range:
- * - When the patch range is against the parent comment of a normal change.
- * Such comments declare themmselves to be on the left using side=PARENT.
- * - If the patch range is against the indexed parent of a merge change.
- * Such comments declare themselves to be on the given parent by
- * specifying the parent index via parent=i.
- *
- * @return {number}
- */
- _getPatchNumByLineAndContent(lineEl, contentEl) {
- let patchNum = this.patchRange.patchNum;
-
- if ((lineEl.classList.contains(DiffSide.LEFT) ||
- contentEl.classList.contains('remove')) &&
- this.patchRange.basePatchNum !== 'PARENT' &&
- !this.isMergeParent(this.patchRange.basePatchNum)) {
- patchNum = this.patchRange.basePatchNum;
- }
- return patchNum;
- }
-
- /** @return {boolean} */
- _getIsParentCommentByLineAndContent(lineEl, contentEl) {
- if ((lineEl.classList.contains(DiffSide.LEFT) ||
- contentEl.classList.contains('remove')) &&
- (this.patchRange.basePatchNum === 'PARENT' ||
- this.isMergeParent(this.patchRange.basePatchNum))) {
- return true;
- }
+ /** @return {boolean} */
+ _isValidElForComment(el) {
+ if (!this.loggedIn) {
+ this.fire('show-auth-required');
return false;
}
+ const patchNum = el.classList.contains(DiffSide.LEFT) ?
+ this.patchRange.basePatchNum :
+ this.patchRange.patchNum;
- /** @return {string} */
- _getCommentSideByLineAndContent(lineEl, contentEl) {
- let side = 'right';
- if (lineEl.classList.contains(DiffSide.LEFT) ||
- contentEl.classList.contains('remove')) {
- side = 'left';
- }
- return side;
+ const isEdit = this.patchNumEquals(patchNum, this.EDIT_NAME);
+ const isEditBase = this.patchNumEquals(patchNum, this.PARENT_NAME) &&
+ this.patchNumEquals(this.patchRange.patchNum, this.EDIT_NAME);
+
+ if (isEdit) {
+ this.fire('show-alert', {message: ERR_COMMENT_ON_EDIT});
+ return false;
+ } else if (isEditBase) {
+ this.fire('show-alert', {message: ERR_COMMENT_ON_EDIT_BASE});
+ return false;
}
+ return true;
+ }
- _prefsObserver(newPrefs, oldPrefs) {
- // Scan the preference objects one level deep to see if they differ.
- let differ = !oldPrefs;
- if (newPrefs && oldPrefs) {
- for (const key in newPrefs) {
- if (newPrefs[key] !== oldPrefs[key]) {
- differ = true;
- }
+ /**
+ * @param {!Object} lineEl
+ * @param {number=} lineNum
+ * @param {string=} side
+ * @param {!Object=} range
+ */
+ _createComment(lineEl, lineNum, side, range) {
+ const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
+ const contentEl = contentText.parentElement;
+ side = side ||
+ this._getCommentSideByLineAndContent(lineEl, contentEl);
+ const patchForNewThreads = this._getPatchNumByLineAndContent(
+ lineEl, contentEl);
+ const isOnParent =
+ this._getIsParentCommentByLineAndContent(lineEl, contentEl);
+ this.dispatchEvent(new CustomEvent('create-comment', {
+ bubbles: true,
+ composed: true,
+ detail: {
+ lineNum,
+ side,
+ patchNum: patchForNewThreads,
+ isOnParent,
+ range,
+ },
+ }));
+ }
+
+ _getThreadGroupForLine(contentEl) {
+ return contentEl.querySelector('.thread-group');
+ }
+
+ /**
+ * Gets or creates a comment thread group for a specific line and side on a
+ * diff.
+ *
+ * @param {!Object} contentEl
+ * @param {!Gerrit.DiffSide} commentSide
+ * @return {!Node}
+ */
+ _getOrCreateThreadGroup(contentEl, commentSide) {
+ // Check if thread group exists.
+ let threadGroupEl = this._getThreadGroupForLine(contentEl);
+ if (!threadGroupEl) {
+ threadGroupEl = document.createElement('div');
+ threadGroupEl.className = 'thread-group';
+ threadGroupEl.setAttribute('data-side', commentSide);
+ contentEl.appendChild(threadGroupEl);
+ }
+ return threadGroupEl;
+ }
+
+ /**
+ * The value to be used for the patch number of new comments created at the
+ * given line and content elements.
+ *
+ * In two cases of creating a comment on the left side, the patch number to
+ * be used should actually be right side of the patch range:
+ * - When the patch range is against the parent comment of a normal change.
+ * Such comments declare themmselves to be on the left using side=PARENT.
+ * - If the patch range is against the indexed parent of a merge change.
+ * Such comments declare themselves to be on the given parent by
+ * specifying the parent index via parent=i.
+ *
+ * @return {number}
+ */
+ _getPatchNumByLineAndContent(lineEl, contentEl) {
+ let patchNum = this.patchRange.patchNum;
+
+ if ((lineEl.classList.contains(DiffSide.LEFT) ||
+ contentEl.classList.contains('remove')) &&
+ this.patchRange.basePatchNum !== 'PARENT' &&
+ !this.isMergeParent(this.patchRange.basePatchNum)) {
+ patchNum = this.patchRange.basePatchNum;
+ }
+ return patchNum;
+ }
+
+ /** @return {boolean} */
+ _getIsParentCommentByLineAndContent(lineEl, contentEl) {
+ if ((lineEl.classList.contains(DiffSide.LEFT) ||
+ contentEl.classList.contains('remove')) &&
+ (this.patchRange.basePatchNum === 'PARENT' ||
+ this.isMergeParent(this.patchRange.basePatchNum))) {
+ return true;
+ }
+ return false;
+ }
+
+ /** @return {string} */
+ _getCommentSideByLineAndContent(lineEl, contentEl) {
+ let side = 'right';
+ if (lineEl.classList.contains(DiffSide.LEFT) ||
+ contentEl.classList.contains('remove')) {
+ side = 'left';
+ }
+ return side;
+ }
+
+ _prefsObserver(newPrefs, oldPrefs) {
+ // Scan the preference objects one level deep to see if they differ.
+ let differ = !oldPrefs;
+ if (newPrefs && oldPrefs) {
+ for (const key in newPrefs) {
+ if (newPrefs[key] !== oldPrefs[key]) {
+ differ = true;
}
}
-
- if (differ) {
- this._prefsChanged(newPrefs);
- }
}
- _pathObserver() {
- // Call _prefsChanged(), because line-limit style value depends on path.
- this._prefsChanged(this.prefs);
- }
-
- _viewModeObserver() {
- this._prefsChanged(this.prefs);
- }
-
- /** @param {boolean} newValue */
- _loadingChanged(newValue) {
- if (newValue) {
- this.cancel();
- this._blame = null;
- this._safetyBypass = null;
- this._showWarning = false;
- this.clearDiffContent();
- }
- }
-
- _lineWrappingObserver() {
- this._prefsChanged(this.prefs);
- }
-
- _prefsChanged(prefs) {
- if (!prefs) { return; }
-
- this._blame = null;
-
- const lineLength = this.path === COMMIT_MSG_PATH ?
- COMMIT_MSG_LINE_LENGTH : prefs.line_length;
- const stylesToUpdate = {};
-
- if (prefs.line_wrapping) {
- this._diffTableClass = 'full-width';
- if (this.viewMode === 'SIDE_BY_SIDE') {
- stylesToUpdate['--content-width'] = 'none';
- stylesToUpdate['--line-limit'] = lineLength + 'ch';
- }
- } else {
- this._diffTableClass = '';
- stylesToUpdate['--content-width'] = lineLength + 'ch';
- }
-
- if (prefs.font_size) {
- stylesToUpdate['--font-size'] = prefs.font_size + 'px';
- }
-
- this.updateStyles(stylesToUpdate);
-
- if (this.diff && !this.noRenderOnPrefsChange) {
- this._debounceRenderDiffTable();
- }
- }
-
- _diffChanged(newValue) {
- if (newValue) {
- this._diffLength = this.getDiffLength(newValue);
- this._debounceRenderDiffTable();
- }
- }
-
- /**
- * When called multiple times from the same microtask, will call
- * _renderDiffTable only once, in the next microtask, unless it is cancelled
- * before that microtask runs.
- *
- * This should be used instead of calling _renderDiffTable directly to
- * render the diff in response to an input change, because there may be
- * multiple inputs changing in the same microtask, but we only want to
- * render once.
- */
- _debounceRenderDiffTable() {
- this.debounce(
- RENDER_DIFF_TABLE_DEBOUNCE_NAME, () => this._renderDiffTable());
- }
-
- _renderDiffTable() {
- this._unobserveIncrementalNodes();
- if (!this.prefs) {
- this.dispatchEvent(
- new CustomEvent('render', {bubbles: true, composed: true}));
- return;
- }
- if (this.prefs.context === -1 &&
- this._diffLength >= LARGE_DIFF_THRESHOLD_LINES &&
- this._safetyBypass === null) {
- this._showWarning = true;
- this.dispatchEvent(
- new CustomEvent('render', {bubbles: true, composed: true}));
- return;
- }
-
- this._showWarning = false;
-
- const keyLocations = this._computeKeyLocations();
- this.$.diffBuilder.render(keyLocations, this._getBypassPrefs())
- .then(() => {
- this.dispatchEvent(
- new CustomEvent('render', {
- bubbles: true,
- composed: true,
- detail: {contentRendered: true},
- }));
- });
- }
-
- _handleRenderContent() {
- this._incrementalNodeObserver = Polymer.dom(this).observeNodes(info => {
- const addedThreadEls = info.addedNodes.filter(isThreadEl);
- // Removed nodes do not need to be handled because all this code does is
- // adding a slot for the added thread elements, and the extra slots do
- // not hurt. It's probably a bigger performance cost to remove them than
- // to keep them around. Medium term we can even consider to add one slot
- // for each line from the start.
- let lastEl;
- for (const threadEl of addedThreadEls) {
- const lineNumString = threadEl.getAttribute('line-num') || 'FILE';
- const commentSide = threadEl.getAttribute('comment-side');
- const lineEl = this.$.diffBuilder.getLineElByNumber(
- lineNumString, commentSide);
- const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
- const contentEl = contentText.parentElement;
- const threadGroupEl = this._getOrCreateThreadGroup(
- contentEl, commentSide);
- // Create a slot for the thread and attach it to the thread group.
- // The Polyfill has some bugs and this only works if the slot is
- // attached to the group after the group is attached to the DOM.
- // The thread group may already have a slot with the right name, but
- // that is okay because the first matching slot is used and the rest
- // are ignored.
- const slot = document.createElement('slot');
- slot.name = threadEl.getAttribute('slot');
- Polymer.dom(threadGroupEl).appendChild(Gerrit.slotToContent(slot));
- lastEl = threadEl;
- }
-
- // Safari is not binding newly created comment-thread
- // with the slot somehow, replace itself will rebind it
- // @see Issue 11182
- if (lastEl && lastEl.replaceWith) {
- lastEl.replaceWith(lastEl);
- }
- });
- }
-
- _unobserveIncrementalNodes() {
- if (this._incrementalNodeObserver) {
- Polymer.dom(this).unobserveNodes(this._incrementalNodeObserver);
- }
- }
-
- _unobserveNodes() {
- if (this._nodeObserver) {
- Polymer.dom(this).unobserveNodes(this._nodeObserver);
- }
- }
-
- /**
- * Get the preferences object including the safety bypass context (if any).
- */
- _getBypassPrefs() {
- if (this._safetyBypass !== null) {
- return Object.assign({}, this.prefs, {context: this._safetyBypass});
- }
- return this.prefs;
- }
-
- clearDiffContent() {
- this._unobserveIncrementalNodes();
- this.$.diffTable.innerHTML = null;
- }
-
- /** @return {!Array} */
- _computeDiffHeaderItems(diffInfoRecord) {
- const diffInfo = diffInfoRecord.base;
- if (!diffInfo || !diffInfo.diff_header) { return []; }
- return diffInfo.diff_header
- .filter(item => !(item.startsWith('diff --git ') ||
- item.startsWith('index ') ||
- item.startsWith('+++ ') ||
- item.startsWith('--- ') ||
- item === 'Binary files differ'));
- }
-
- /** @return {boolean} */
- _computeDiffHeaderHidden(items) {
- return items.length === 0;
- }
-
- _handleFullBypass() {
- this._safetyBypass = FULL_CONTEXT;
- this._debounceRenderDiffTable();
- }
-
- _handleLimitedBypass() {
- this._safetyBypass = LIMITED_CONTEXT;
- this._debounceRenderDiffTable();
- }
-
- /** @return {string} */
- _computeWarningClass(showWarning) {
- return showWarning ? 'warn' : '';
- }
-
- /**
- * @param {string} errorMessage
- * @return {string}
- */
- _computeErrorClass(errorMessage) {
- return errorMessage ? 'showError' : '';
- }
-
- expandAllContext() {
- this._handleFullBypass();
- }
-
- /**
- * @param {!boolean} warnLeft
- * @param {!boolean} warnRight
- * @return {string|null}
- */
- _computeNewlineWarning(warnLeft, warnRight) {
- const messages = [];
- if (warnLeft) {
- messages.push(NO_NEWLINE_BASE);
- }
- if (warnRight) {
- messages.push(NO_NEWLINE_REVISION);
- }
- if (!messages.length) { return null; }
- return messages.join(' — ');
- }
-
- /**
- * @param {string} warning
- * @param {boolean} loading
- * @return {string}
- */
- _computeNewlineWarningClass(warning, loading) {
- if (loading || !warning) { return 'newlineWarning hidden'; }
- return 'newlineWarning';
- }
-
- /**
- * Get the approximate length of the diff as the sum of the maximum
- * length of the chunks.
- *
- * @param {Object} diff object
- * @return {number}
- */
- getDiffLength(diff) {
- if (!diff) return 0;
- return diff.content.reduce((sum, sec) => {
- if (sec.hasOwnProperty('ab')) {
- return sum + sec.ab.length;
- } else {
- return sum + Math.max(
- sec.hasOwnProperty('a') ? sec.a.length : 0,
- sec.hasOwnProperty('b') ? sec.b.length : 0);
- }
- }, 0);
+ if (differ) {
+ this._prefsChanged(newPrefs);
}
}
- customElements.define(GrDiff.is, GrDiff);
-})();
+ _pathObserver() {
+ // Call _prefsChanged(), because line-limit style value depends on path.
+ this._prefsChanged(this.prefs);
+ }
+
+ _viewModeObserver() {
+ this._prefsChanged(this.prefs);
+ }
+
+ _cleanup() {
+ this.cancel();
+ this._blame = null;
+ this._safetyBypass = null;
+ this._showWarning = false;
+ this.clearDiffContent();
+ }
+
+ /** @param {boolean} newValue */
+ _loadingChanged(newValue) {
+ if (newValue) {
+ this._cleanup();
+ }
+ }
+
+ _lineWrappingObserver() {
+ this._prefsChanged(this.prefs);
+ }
+
+ _prefsChanged(prefs) {
+ if (!prefs) { return; }
+
+ this._blame = null;
+
+ const lineLength = this.path === COMMIT_MSG_PATH ?
+ COMMIT_MSG_LINE_LENGTH : prefs.line_length;
+ const stylesToUpdate = {};
+
+ if (prefs.line_wrapping) {
+ this._diffTableClass = 'full-width';
+ if (this.viewMode === 'SIDE_BY_SIDE') {
+ stylesToUpdate['--content-width'] = 'none';
+ stylesToUpdate['--line-limit'] = lineLength + 'ch';
+ }
+ } else {
+ this._diffTableClass = '';
+ stylesToUpdate['--content-width'] = lineLength + 'ch';
+ }
+
+ if (prefs.font_size) {
+ stylesToUpdate['--font-size'] = prefs.font_size + 'px';
+ }
+
+ this.updateStyles(stylesToUpdate);
+
+ if (this.diff && !this.noRenderOnPrefsChange) {
+ this._debounceRenderDiffTable();
+ }
+ }
+
+ _diffChanged(newValue) {
+ if (newValue) {
+ this._cleanup();
+ this._diffLength = this.getDiffLength(newValue);
+ this._debounceRenderDiffTable();
+ }
+ }
+
+ /**
+ * When called multiple times from the same microtask, will call
+ * _renderDiffTable only once, in the next microtask, unless it is cancelled
+ * before that microtask runs.
+ *
+ * This should be used instead of calling _renderDiffTable directly to
+ * render the diff in response to an input change, because there may be
+ * multiple inputs changing in the same microtask, but we only want to
+ * render once.
+ */
+ _debounceRenderDiffTable() {
+ this.debounce(
+ RENDER_DIFF_TABLE_DEBOUNCE_NAME, () => this._renderDiffTable());
+ }
+
+ _renderDiffTable() {
+ if (!this.prefs) {
+ this.dispatchEvent(
+ new CustomEvent('render', {bubbles: true, composed: true}));
+ return;
+ }
+ if (this.prefs.context === -1 &&
+ this._diffLength >= LARGE_DIFF_THRESHOLD_LINES &&
+ this._safetyBypass === null) {
+ this._showWarning = true;
+ this.dispatchEvent(
+ new CustomEvent('render', {bubbles: true, composed: true}));
+ return;
+ }
+
+ this._showWarning = false;
+
+ const keyLocations = this._computeKeyLocations();
+ this.$.diffBuilder.render(keyLocations, this._getBypassPrefs())
+ .then(() => {
+ this.dispatchEvent(
+ new CustomEvent('render', {
+ bubbles: true,
+ composed: true,
+ detail: {contentRendered: true},
+ }));
+ });
+ }
+
+ _handleRenderContent() {
+ this._incrementalNodeObserver = dom(this).observeNodes(info => {
+ const addedThreadEls = info.addedNodes.filter(isThreadEl);
+ // Removed nodes do not need to be handled because all this code does is
+ // adding a slot for the added thread elements, and the extra slots do
+ // not hurt. It's probably a bigger performance cost to remove them than
+ // to keep them around. Medium term we can even consider to add one slot
+ // for each line from the start.
+ let lastEl;
+ for (const threadEl of addedThreadEls) {
+ const lineNumString = threadEl.getAttribute('line-num') || 'FILE';
+ const commentSide = threadEl.getAttribute('comment-side');
+ const lineEl = this.$.diffBuilder.getLineElByNumber(
+ lineNumString, commentSide);
+ const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
+ const contentEl = contentText.parentElement;
+ const threadGroupEl = this._getOrCreateThreadGroup(
+ contentEl, commentSide);
+ // Create a slot for the thread and attach it to the thread group.
+ // The Polyfill has some bugs and this only works if the slot is
+ // attached to the group after the group is attached to the DOM.
+ // The thread group may already have a slot with the right name, but
+ // that is okay because the first matching slot is used and the rest
+ // are ignored.
+ const slot = document.createElement('slot');
+ slot.name = threadEl.getAttribute('slot');
+ dom(threadGroupEl).appendChild(Gerrit.slotToContent(slot));
+ lastEl = threadEl;
+ }
+
+ // Safari is not binding newly created comment-thread
+ // with the slot somehow, replace itself will rebind it
+ // @see Issue 11182
+ if (lastEl && lastEl.replaceWith) {
+ lastEl.replaceWith(lastEl);
+ }
+ });
+ }
+
+ _unobserveIncrementalNodes() {
+ if (this._incrementalNodeObserver) {
+ dom(this).unobserveNodes(this._incrementalNodeObserver);
+ }
+ }
+
+ _unobserveNodes() {
+ if (this._nodeObserver) {
+ dom(this).unobserveNodes(this._nodeObserver);
+ }
+ }
+
+ /**
+ * Get the preferences object including the safety bypass context (if any).
+ */
+ _getBypassPrefs() {
+ if (this._safetyBypass !== null) {
+ return Object.assign({}, this.prefs, {context: this._safetyBypass});
+ }
+ return this.prefs;
+ }
+
+ clearDiffContent() {
+ this._unobserveIncrementalNodes();
+ this.$.diffTable.innerHTML = null;
+ }
+
+ /** @return {!Array} */
+ _computeDiffHeaderItems(diffInfoRecord) {
+ const diffInfo = diffInfoRecord.base;
+ if (!diffInfo || !diffInfo.diff_header) { return []; }
+ return diffInfo.diff_header
+ .filter(item => !(item.startsWith('diff --git ') ||
+ item.startsWith('index ') ||
+ item.startsWith('+++ ') ||
+ item.startsWith('--- ') ||
+ item === 'Binary files differ'));
+ }
+
+ /** @return {boolean} */
+ _computeDiffHeaderHidden(items) {
+ return items.length === 0;
+ }
+
+ _handleFullBypass() {
+ this._safetyBypass = FULL_CONTEXT;
+ this._debounceRenderDiffTable();
+ }
+
+ _handleLimitedBypass() {
+ this._safetyBypass = LIMITED_CONTEXT;
+ this._debounceRenderDiffTable();
+ }
+
+ /** @return {string} */
+ _computeWarningClass(showWarning) {
+ return showWarning ? 'warn' : '';
+ }
+
+ /**
+ * @param {string} errorMessage
+ * @return {string}
+ */
+ _computeErrorClass(errorMessage) {
+ return errorMessage ? 'showError' : '';
+ }
+
+ expandAllContext() {
+ this._handleFullBypass();
+ }
+
+ /**
+ * @param {!boolean} warnLeft
+ * @param {!boolean} warnRight
+ * @return {string|null}
+ */
+ _computeNewlineWarning(warnLeft, warnRight) {
+ const messages = [];
+ if (warnLeft) {
+ messages.push(NO_NEWLINE_BASE);
+ }
+ if (warnRight) {
+ messages.push(NO_NEWLINE_REVISION);
+ }
+ if (!messages.length) { return null; }
+ return messages.join(' — ');
+ }
+
+ /**
+ * @param {string} warning
+ * @param {boolean} loading
+ * @return {string}
+ */
+ _computeNewlineWarningClass(warning, loading) {
+ if (loading || !warning) { return 'newlineWarning hidden'; }
+ return 'newlineWarning';
+ }
+
+ /**
+ * Get the approximate length of the diff as the sum of the maximum
+ * length of the chunks.
+ *
+ * @param {Object} diff object
+ * @return {number}
+ */
+ getDiffLength(diff) {
+ if (!diff) return 0;
+ return diff.content.reduce((sum, sec) => {
+ if (sec.hasOwnProperty('ab')) {
+ return sum + sec.ab.length;
+ } else {
+ return sum + Math.max(
+ sec.hasOwnProperty('a') ? sec.a.length : 0,
+ sec.hasOwnProperty('b') ? sec.b.length : 0);
+ }
+ }, 0);
+ }
+}
+
+customElements.define(GrDiff.is, GrDiff);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
similarity index 80%
rename from polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
index 003be05..c6a7e98 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
@@ -1,35 +1,22 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @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.js';
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../gr-diff-builder/gr-diff-builder-element.html">
-<link rel="import" href="../gr-diff-highlight/gr-diff-highlight.html">
-<link rel="import" href="../gr-diff-selection/gr-diff-selection.html">
-<link rel="import" href="../gr-syntax-themes/gr-syntax-theme.html">
-<link rel="import" href="../gr-ranged-comment-themes/gr-ranged-comment-theme.html">
-
-<script src="../../../scripts/hiddenscroll.js"></script>
-
-<dom-module id="gr-diff">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host(.no-left) .sideBySide .left,
:host(.no-left) .sideBySide .left + td,
@@ -182,7 +169,7 @@
.content .contentText:empty:after {
/* Newline, to ensure empty lines are one line-height tall. */
- content: '\A';
+ content: '\\A';
}
.contextControl {
background-color: var(--diff-context-control-background-color);
@@ -212,7 +199,7 @@
}
.br:after {
/* Line feed */
- content: '\A';
+ content: '\\A';
}
.tab {
display: inline-block;
@@ -220,7 +207,7 @@
.tab-indicator:before {
color: var(--diff-tab-indicator-color);
/* >> character */
- content: '\00BB';
+ content: '\\00BB';
position: absolute;
}
/* Is defined after other background-colors, such that this
@@ -361,38 +348,16 @@
<style include="gr-ranged-comment-theme">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
- <div id="diffHeader" hidden$="[[_computeDiffHeaderHidden(_diffHeaderItems)]]">
- <template
- is="dom-repeat"
- items="[[_diffHeaderItems]]">
+ <div id="diffHeader" hidden\$="[[_computeDiffHeaderHidden(_diffHeaderItems)]]">
+ <template is="dom-repeat" items="[[_diffHeaderItems]]">
<div>[[item]]</div>
</template>
</div>
- <div class$="[[_computeContainerClass(loggedIn, viewMode, displayLine)]]"
- on-tap="_handleTap">
+ <div class\$="[[_computeContainerClass(loggedIn, viewMode, displayLine)]]" on-tap="_handleTap">
<gr-diff-selection diff="[[diff]]">
- <gr-diff-highlight
- id="highlights"
- logged-in="[[loggedIn]]"
- comment-ranges="{{_commentRanges}}">
- <gr-diff-builder
- id="diffBuilder"
- comment-ranges="[[_commentRanges]]"
- coverage-ranges="[[coverageRanges]]"
- project-name="[[projectName]]"
- diff="[[diff]]"
- path="[[path]]"
- change-num="[[changeNum]]"
- patch-num="[[patchRange.patchNum]]"
- view-mode="[[viewMode]]"
- is-image-diff="[[isImageDiff]]"
- base-image="[[baseImage]]"
- layers="[[layers]]"
- revision-image="[[revisionImage]]">
- <table
- id="diffTable"
- class$="[[_diffTableClass]]"
- role="presentation"></table>
+ <gr-diff-highlight id="highlights" logged-in="[[loggedIn]]" comment-ranges="{{_commentRanges}}">
+ <gr-diff-builder id="diffBuilder" comment-ranges="[[_commentRanges]]" coverage-ranges="[[coverageRanges]]" project-name="[[projectName]]" diff="[[diff]]" path="[[path]]" change-num="[[changeNum]]" patch-num="[[patchRange.patchNum]]" view-mode="[[viewMode]]" is-image-diff="[[isImageDiff]]" base-image="[[baseImage]]" layers="[[layers]]" revision-image="[[revisionImage]]">
+ <table id="diffTable" class\$="[[_diffTableClass]]" role="presentation"></table>
<template is="dom-if" if="[[showNoChangeMessage(loading, prefs, _diffLength)]]">
<div class="whitespace-change-only-message">
@@ -404,13 +369,13 @@
</gr-diff-highlight>
</gr-diff-selection>
</div>
- <div class$="[[_computeNewlineWarningClass(_newlineWarning, loading)]]">
+ <div class\$="[[_computeNewlineWarningClass(_newlineWarning, loading)]]">
[[_newlineWarning]]
</div>
- <div id="loadingError" class$="[[_computeErrorClass(errorMessage)]]">
+ <div id="loadingError" class\$="[[_computeErrorClass(errorMessage)]]">
[[errorMessage]]
</div>
- <div id="sizeWarning" class$="[[_computeWarningClass(_showWarning)]]">
+ <div id="sizeWarning" class\$="[[_computeWarningClass(_showWarning)]]">
<p>
Prevented render because "Whole file" is enabled and this diff is very
large (about [[_diffLength]] lines).
@@ -422,8 +387,4 @@
Render anyway (may be slow)
</gr-button>
</div>
- </template>
- <script src="gr-diff-line.js"></script>
- <script src="gr-diff-group.js"></script>
- <script src="gr-diff.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index ba46549..08576a2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -19,20 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<script src="/components/web-component-tester/data/a11ySuite.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
-<link rel="import" href="gr-diff.html">
-
-<script>void(0);</script>
<test-fixture id="basic">
<template>
@@ -40,932 +31,247 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import './gr-diff.js';
+import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-diff tests', () => {
+ let element;
+ let sandbox;
- const MINIMAL_PREFS = {tab_size: 2, line_length: 80};
+ const MINIMAL_PREFS = {tab_size: 2, line_length: 80};
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('selectionchange event handling', () => {
+ const emulateSelection = function() {
+ document.dispatchEvent(new CustomEvent('selectionchange'));
+ };
setup(() => {
- sandbox = sinon.sandbox.create();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- suite('selectionchange event handling', () => {
- const emulateSelection = function() {
- document.dispatchEvent(new CustomEvent('selectionchange'));
- };
-
- setup(() => {
- element = fixture('basic');
- sandbox.stub(element.$.highlights, 'handleSelectionChange');
- });
-
- test('enabled if logged in', () => {
- element.loggedIn = true;
- emulateSelection();
- assert.isTrue(element.$.highlights.handleSelectionChange.called);
- });
-
- test('ignored if logged out', () => {
- element.loggedIn = false;
- emulateSelection();
- assert.isFalse(element.$.highlights.handleSelectionChange.called);
- });
- });
-
- test('cancel', () => {
element = fixture('basic');
- const cancelStub = sandbox.stub(element.$.diffBuilder, 'cancel');
- element.cancel();
- assert.isTrue(cancelStub.calledOnce);
+ sandbox.stub(element.$.highlights, 'handleSelectionChange');
});
- test('line limit with line_wrapping', () => {
+ test('enabled if logged in', () => {
+ element.loggedIn = true;
+ emulateSelection();
+ assert.isTrue(element.$.highlights.handleSelectionChange.called);
+ });
+
+ test('ignored if logged out', () => {
+ element.loggedIn = false;
+ emulateSelection();
+ assert.isFalse(element.$.highlights.handleSelectionChange.called);
+ });
+ });
+
+ test('cancel', () => {
+ element = fixture('basic');
+ const cancelStub = sandbox.stub(element.$.diffBuilder, 'cancel');
+ element.cancel();
+ assert.isTrue(cancelStub.calledOnce);
+ });
+
+ test('line limit with line_wrapping', () => {
+ element = fixture('basic');
+ element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: true});
+ flushAsynchronousOperations();
+ assert.equal(util.getComputedStyleValue('--line-limit', element), '80ch');
+ });
+
+ test('line limit without line_wrapping', () => {
+ element = fixture('basic');
+ element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: false});
+ flushAsynchronousOperations();
+ assert.isNotOk(util.getComputedStyleValue('--line-limit', element));
+ });
+
+ suite('_get{PatchNum|IsParentComment}ByLineAndContent', () => {
+ let lineEl;
+ let contentEl;
+
+ setup(() => {
element = fixture('basic');
- element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: true});
- flushAsynchronousOperations();
- assert.equal(util.getComputedStyleValue('--line-limit', element), '80ch');
+ lineEl = document.createElement('td');
+ contentEl = document.createElement('span');
});
- test('line limit without line_wrapping', () => {
- element = fixture('basic');
- element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: false});
- flushAsynchronousOperations();
- assert.isNotOk(util.getComputedStyleValue('--line-limit', element));
- });
-
- suite('_get{PatchNum|IsParentComment}ByLineAndContent', () => {
- let lineEl;
- let contentEl;
-
- setup(() => {
- element = fixture('basic');
- lineEl = document.createElement('td');
- contentEl = document.createElement('span');
+ suite('_getPatchNumByLineAndContent', () => {
+ test('right side', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
+ lineEl.classList.add('right');
+ assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
+ 4);
});
- suite('_getPatchNumByLineAndContent', () => {
- test('right side', () => {
- element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
- lineEl.classList.add('right');
- assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
- 4);
- });
-
- test('left side parent by linenum', () => {
- element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
- lineEl.classList.add('left');
- assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
- 4);
- });
-
- test('left side parent by content', () => {
- element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
- contentEl.classList.add('remove');
- assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
- 4);
- });
-
- test('left side merge parent', () => {
- element.patchRange = {patchNum: 4, basePatchNum: -2};
- contentEl.classList.add('remove');
- assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
- 4);
- });
-
- test('left side non parent', () => {
- element.patchRange = {patchNum: 4, basePatchNum: 3};
- contentEl.classList.add('remove');
- assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
- 3);
- });
+ test('left side parent by linenum', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
+ lineEl.classList.add('left');
+ assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
+ 4);
});
- suite('_getIsParentCommentByLineAndContent', () => {
- test('right side', () => {
- element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
- lineEl.classList.add('right');
- assert.isFalse(
- element._getIsParentCommentByLineAndContent(lineEl, contentEl));
- });
+ test('left side parent by content', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
+ contentEl.classList.add('remove');
+ assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
+ 4);
+ });
- test('left side parent by linenum', () => {
- element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
- lineEl.classList.add('left');
- assert.isTrue(
- element._getIsParentCommentByLineAndContent(lineEl, contentEl));
- });
+ test('left side merge parent', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: -2};
+ contentEl.classList.add('remove');
+ assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
+ 4);
+ });
- test('left side parent by content', () => {
- element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
- contentEl.classList.add('remove');
- assert.isTrue(
- element._getIsParentCommentByLineAndContent(lineEl, contentEl));
- });
-
- test('left side merge parent', () => {
- element.patchRange = {patchNum: 4, basePatchNum: -2};
- contentEl.classList.add('remove');
- assert.isTrue(
- element._getIsParentCommentByLineAndContent(lineEl, contentEl));
- });
-
- test('left side non parent', () => {
- element.patchRange = {patchNum: 4, basePatchNum: 3};
- contentEl.classList.add('remove');
- assert.isFalse(
- element._getIsParentCommentByLineAndContent(lineEl, contentEl));
- });
+ test('left side non parent', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: 3};
+ contentEl.classList.add('remove');
+ assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
+ 3);
});
});
- suite('not logged in', () => {
- setup(() => {
- const getLoggedInPromise = Promise.resolve(false);
- stub('gr-rest-api-interface', {
- getLoggedIn() { return getLoggedInPromise; },
- });
- element = fixture('basic');
- return getLoggedInPromise;
- });
-
- test('toggleLeftDiff', () => {
- element.toggleLeftDiff();
- assert.isTrue(element.classList.contains('no-left'));
- element.toggleLeftDiff();
- assert.isFalse(element.classList.contains('no-left'));
- });
-
- test('addDraftAtLine', () => {
- sandbox.stub(element, '_selectLine');
- const loggedInErrorSpy = sandbox.spy();
- element.addEventListener('show-auth-required', loggedInErrorSpy);
- element.addDraftAtLine();
- assert.isTrue(loggedInErrorSpy.called);
- });
-
- test('view does not start with displayLine classList', () => {
+ suite('_getIsParentCommentByLineAndContent', () => {
+ test('right side', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
+ lineEl.classList.add('right');
assert.isFalse(
- element.shadowRoot
- .querySelector('.diffContainer')
- .classList
- .contains('displayLine'));
+ element._getIsParentCommentByLineAndContent(lineEl, contentEl));
});
- test('displayLine class added called when displayLine is true', () => {
- const spy = sandbox.spy(element, '_computeContainerClass');
- element.displayLine = true;
- assert.isTrue(spy.called);
+ test('left side parent by linenum', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
+ lineEl.classList.add('left');
assert.isTrue(
- element.shadowRoot
- .querySelector('.diffContainer')
- .classList
- .contains('displayLine'));
+ element._getIsParentCommentByLineAndContent(lineEl, contentEl));
});
- test('thread groups', () => {
- const contentEl = document.createElement('div');
-
- element.changeNum = 123;
- element.patchRange = {basePatchNum: 1, patchNum: 2};
- element.path = 'file.txt';
-
- const mock = document.createElement('mock-diff-response');
- element.$.diffBuilder._builder = element.$.diffBuilder._getDiffBuilder(
- mock.diffResponse, Object.assign({}, MINIMAL_PREFS));
-
- // No thread groups.
- assert.isNotOk(element._getThreadGroupForLine(contentEl));
-
- // A thread group gets created.
- const threadGroupEl = element._getOrCreateThreadGroup(contentEl);
- assert.isOk(threadGroupEl);
-
- // The new thread group can be fetched.
- assert.isOk(element._getThreadGroupForLine(contentEl));
-
- assert.equal(contentEl.querySelectorAll('.thread-group').length, 1);
+ test('left side parent by content', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
+ contentEl.classList.add('remove');
+ assert.isTrue(
+ element._getIsParentCommentByLineAndContent(lineEl, contentEl));
});
- suite('image diffs', () => {
- let mockFile1;
- let mockFile2;
- setup(() => {
- mockFile1 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
- 'wsAAAAAAAAAAAAAAAAA/w==',
- type: 'image/bmp',
- };
- mockFile2 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
- 'wsAAAAAAAAAAAAA/////w==',
- type: 'image/bmp',
- };
-
- element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
- element.isImageDiff = true;
- element.prefs = {
- auto_hide_diff_table_header: true,
- context: 10,
- cursor_blink_rate: 0,
- font_size: 12,
- ignore_whitespace: 'IGNORE_NONE',
- intraline_difference: true,
- line_length: 100,
- line_wrapping: false,
- show_line_endings: true,
- show_tabs: true,
- show_whitespace_errors: true,
- syntax_highlighting: true,
- tab_size: 8,
- theme: 'DEFAULT',
- };
- });
-
- test('renders image diffs with same file name', done => {
- const rendered = () => {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
-
- // Left image rendered with the parent commit's version of the file.
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const leftLabel =
- element.$.diffTable.querySelector('td.left label');
- const leftLabelContent = leftLabel.querySelector('.label');
- const leftLabelName = leftLabel.querySelector('.name');
-
- const rightImage =
- element.$.diffTable.querySelector('td.right img');
- const rightLabel = element.$.diffTable.querySelector(
- 'td.right label');
- const rightLabelContent = rightLabel.querySelector('.label');
- const rightLabelName = rightLabel.querySelector('.name');
-
- assert.isNotOk(rightLabelName);
- assert.isNotOk(leftLabelName);
-
- let leftLoaded = false;
- let rightLoaded = false;
-
- leftImage.addEventListener('load', () => {
- assert.isOk(leftImage);
- assert.equal(leftImage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile1.body);
- assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
- leftLoaded = true;
- if (rightLoaded) {
- element.removeEventListener('render', rendered);
- done();
- }
- });
-
- rightImage.addEventListener('load', () => {
- assert.isOk(rightImage);
- assert.equal(rightImage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile2.body);
- assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
-
- rightLoaded = true;
- if (leftLoaded) {
- element.removeEventListener('render', rendered);
- done();
- }
- });
- };
-
- element.addEventListener('render', rendered);
-
- element.baseImage = mockFile1;
- element.revisionImage = mockFile2;
- element.diff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index 2adc47d..f9c2f2c 100644',
- '--- a/carrot.jpg',
- '+++ b/carrot.jpg',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- });
-
- test('renders image diffs with a different file name', done => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot2.jpg',
- 'index 2adc47d..f9c2f2c 100644',
- '--- a/carrot.jpg',
- '+++ b/carrot2.jpg',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
-
- const rendered = () => {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
-
- // Left image rendered with the parent commit's version of the file.
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const leftLabel =
- element.$.diffTable.querySelector('td.left label');
- const leftLabelContent = leftLabel.querySelector('.label');
- const leftLabelName = leftLabel.querySelector('.name');
-
- const rightImage =
- element.$.diffTable.querySelector('td.right img');
- const rightLabel = element.$.diffTable.querySelector(
- 'td.right label');
- const rightLabelContent = rightLabel.querySelector('.label');
- const rightLabelName = rightLabel.querySelector('.name');
-
- assert.isOk(rightLabelName);
- assert.isOk(leftLabelName);
- assert.equal(leftLabelName.textContent, mockDiff.meta_a.name);
- assert.equal(rightLabelName.textContent, mockDiff.meta_b.name);
-
- let leftLoaded = false;
- let rightLoaded = false;
-
- leftImage.addEventListener('load', () => {
- assert.isOk(leftImage);
- assert.equal(leftImage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile1.body);
- assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
- leftLoaded = true;
- if (rightLoaded) {
- element.removeEventListener('render', rendered);
- done();
- }
- });
-
- rightImage.addEventListener('load', () => {
- assert.isOk(rightImage);
- assert.equal(rightImage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile2.body);
- assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
-
- rightLoaded = true;
- if (leftLoaded) {
- element.removeEventListener('render', rendered);
- done();
- }
- });
- };
-
- element.addEventListener('render', rendered);
-
- element.baseImage = mockFile1;
- element.baseImage._name = mockDiff.meta_a.name;
- element.revisionImage = mockFile2;
- element.revisionImage._name = mockDiff.meta_b.name;
- element.diff = mockDiff;
- });
-
- test('renders added image', done => {
- const mockDiff = {
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'ADDED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index 0000000..f9c2f2c 100644',
- '--- /dev/null',
- '+++ b/carrot.jpg',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
-
- function rendered() {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
-
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const rightImage = element.$.diffTable.querySelector('td.right img');
-
- assert.isNotOk(leftImage);
- assert.isOk(rightImage);
- done();
- element.removeEventListener('render', rendered);
- }
- element.addEventListener('render', rendered);
-
- element.revisionImage = mockFile2;
- element.diff = mockDiff;
- });
-
- test('renders removed image', done => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'DELETED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index f9c2f2c..0000000 100644',
- '--- a/carrot.jpg',
- '+++ /dev/null',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
-
- function rendered() {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
-
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const rightImage = element.$.diffTable.querySelector('td.right img');
-
- assert.isOk(leftImage);
- assert.isNotOk(rightImage);
- done();
- element.removeEventListener('render', rendered);
- }
- element.addEventListener('render', rendered);
-
- element.baseImage = mockFile1;
- element.diff = mockDiff;
- });
-
- test('does not render disallowed image type', done => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'DELETED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index f9c2f2c..0000000 100644',
- '--- a/carrot.jpg',
- '+++ /dev/null',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- mockFile1.type = 'image/jpeg-evil';
-
- function rendered() {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
- const leftImage = element.$.diffTable.querySelector('td.left img');
- assert.isNotOk(leftImage);
- done();
- element.removeEventListener('render', rendered);
- }
- element.addEventListener('render', rendered);
-
- element.baseImage = mockFile1;
- element.diff = mockDiff;
- });
+ test('left side merge parent', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: -2};
+ contentEl.classList.add('remove');
+ assert.isTrue(
+ element._getIsParentCommentByLineAndContent(lineEl, contentEl));
});
- test('_handleTap lineNum', done => {
- const addDraftStub = sandbox.stub(element, 'addDraftAtLine');
- const el = document.createElement('div');
- el.className = 'lineNum';
- el.addEventListener('click', e => {
- element._handleTap(e);
- assert.isTrue(addDraftStub.called);
- assert.equal(addDraftStub.lastCall.args[0], el);
- done();
- });
- el.click();
+ test('left side non parent', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: 3};
+ contentEl.classList.add('remove');
+ assert.isFalse(
+ element._getIsParentCommentByLineAndContent(lineEl, contentEl));
});
+ });
+ });
- test('_handleTap context', done => {
- const showContextStub =
- sandbox.stub(element.$.diffBuilder, 'showContext');
- const el = document.createElement('div');
- el.className = 'showContext';
- el.addEventListener('click', e => {
- element._handleTap(e);
- assert.isTrue(showContextStub.called);
- done();
- });
- el.click();
+ suite('not logged in', () => {
+ setup(() => {
+ const getLoggedInPromise = Promise.resolve(false);
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return getLoggedInPromise; },
});
+ element = fixture('basic');
+ return getLoggedInPromise;
+ });
- test('_handleTap content', done => {
- const content = document.createElement('div');
- const lineEl = document.createElement('div');
+ test('toggleLeftDiff', () => {
+ element.toggleLeftDiff();
+ assert.isTrue(element.classList.contains('no-left'));
+ element.toggleLeftDiff();
+ assert.isFalse(element.classList.contains('no-left'));
+ });
- const selectStub = sandbox.stub(element, '_selectLine');
- sandbox.stub(element.$.diffBuilder, 'getLineElByChild', () => lineEl);
+ test('addDraftAtLine', () => {
+ sandbox.stub(element, '_selectLine');
+ const loggedInErrorSpy = sandbox.spy();
+ element.addEventListener('show-auth-required', loggedInErrorSpy);
+ element.addDraftAtLine();
+ assert.isTrue(loggedInErrorSpy.called);
+ });
- content.className = 'content';
- content.addEventListener('click', e => {
- element._handleTap(e);
- assert.isTrue(selectStub.called);
- assert.equal(selectStub.lastCall.args[0], lineEl);
- done();
- });
- content.click();
- });
+ test('view does not start with displayLine classList', () => {
+ assert.isFalse(
+ element.shadowRoot
+ .querySelector('.diffContainer')
+ .classList
+ .contains('displayLine'));
+ });
- suite('getCursorStops', () => {
- const setupDiff = function() {
- const mock = document.createElement('mock-diff-response');
- element.diff = mock.diffResponse;
- element.prefs = {
- context: 10,
- tab_size: 8,
- font_size: 12,
- line_length: 100,
- cursor_blink_rate: 0,
- line_wrapping: false,
- intraline_difference: true,
- show_line_endings: true,
- show_tabs: true,
- show_whitespace_errors: true,
- syntax_highlighting: true,
- auto_hide_diff_table_header: true,
- theme: 'DEFAULT',
- ignore_whitespace: 'IGNORE_NONE',
- };
+ test('displayLine class added called when displayLine is true', () => {
+ const spy = sandbox.spy(element, '_computeContainerClass');
+ element.displayLine = true;
+ assert.isTrue(spy.called);
+ assert.isTrue(
+ element.shadowRoot
+ .querySelector('.diffContainer')
+ .classList
+ .contains('displayLine'));
+ });
- element._renderDiffTable();
- flushAsynchronousOperations();
+ test('thread groups', () => {
+ const contentEl = document.createElement('div');
+
+ element.changeNum = 123;
+ element.patchRange = {basePatchNum: 1, patchNum: 2};
+ element.path = 'file.txt';
+
+ const mock = document.createElement('mock-diff-response');
+ element.$.diffBuilder._builder = element.$.diffBuilder._getDiffBuilder(
+ mock.diffResponse, Object.assign({}, MINIMAL_PREFS));
+
+ // No thread groups.
+ assert.isNotOk(element._getThreadGroupForLine(contentEl));
+
+ // A thread group gets created.
+ const threadGroupEl = element._getOrCreateThreadGroup(contentEl);
+ assert.isOk(threadGroupEl);
+
+ // The new thread group can be fetched.
+ assert.isOk(element._getThreadGroupForLine(contentEl));
+
+ assert.equal(contentEl.querySelectorAll('.thread-group').length, 1);
+ });
+
+ suite('image diffs', () => {
+ let mockFile1;
+ let mockFile2;
+ setup(() => {
+ mockFile1 = {
+ body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAAAAAA/w==',
+ type: 'image/bmp',
+ };
+ mockFile2 = {
+ body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAA/////w==',
+ type: 'image/bmp',
};
- test('getCursorStops returns [] when hidden and noAutoRender', () => {
- element.noAutoRender = true;
- setupDiff();
- element.hidden = true;
- assert.equal(element.getCursorStops().length, 0);
- });
-
- test('getCursorStops', () => {
- setupDiff();
- assert.equal(element.getCursorStops().length, 50);
- });
- });
-
- test('adds .hiddenscroll', () => {
- Gerrit.hiddenscroll = true;
- element.displayLine = true;
- assert.include(element.shadowRoot
- .querySelector('.diffContainer').className, 'hiddenscroll');
- });
- });
-
- suite('logged in', () => {
- let fakeLineEl;
- setup(() => {
- element = fixture('basic');
- element.loggedIn = true;
- element.patchRange = {};
-
- fakeLineEl = {
- getAttribute: sandbox.stub().returns(42),
- classList: {
- contains: sandbox.stub().returns(true),
- },
- };
- });
-
- test('addDraftAtLine', () => {
- sandbox.stub(element, '_selectLine');
- sandbox.stub(element, '_createComment');
- element.addDraftAtLine(fakeLineEl);
- assert.isTrue(element._createComment
- .calledWithExactly(fakeLineEl, 42));
- });
-
- test('addDraftAtLine on an edit', () => {
- element.patchRange.basePatchNum = element.EDIT_NAME;
- sandbox.stub(element, '_selectLine');
- sandbox.stub(element, '_createComment');
- const alertSpy = sandbox.spy();
- element.addEventListener('show-alert', alertSpy);
- element.addDraftAtLine(fakeLineEl);
- assert.isTrue(alertSpy.called);
- assert.isFalse(element._createComment.called);
- });
-
- test('addDraftAtLine on an edit base', () => {
- element.patchRange.patchNum = element.EDIT_NAME;
- element.patchRange.basePatchNum = element.PARENT_NAME;
- sandbox.stub(element, '_selectLine');
- sandbox.stub(element, '_createComment');
- const alertSpy = sandbox.spy();
- element.addEventListener('show-alert', alertSpy);
- element.addDraftAtLine(fakeLineEl);
- assert.isTrue(alertSpy.called);
- assert.isFalse(element._createComment.called);
- });
-
- suite('change in preferences', () => {
- setup(() => {
- element.diff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- diff_header: [],
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- content: [{skip: 66}],
- };
- element.flushDebouncer('renderDiffTable');
- });
-
- test('change in preferences re-renders diff', () => {
- sandbox.stub(element, '_renderDiffTable');
- element.prefs = Object.assign(
- {}, MINIMAL_PREFS, {time_format: 'HHMM_12'});
- element.flushDebouncer('renderDiffTable');
- assert.isTrue(element._renderDiffTable.called);
- });
-
- test('change in preferences does not re-renders diff with ' +
- 'noRenderOnPrefsChange', () => {
- sandbox.stub(element, '_renderDiffTable');
- element.noRenderOnPrefsChange = true;
- element.prefs = Object.assign(
- {}, MINIMAL_PREFS, {time_format: 'HHMM_12'});
- element.flushDebouncer('renderDiffTable');
- assert.isFalse(element._renderDiffTable.called);
- });
- });
- });
-
- suite('diff header', () => {
- setup(() => {
- element = fixture('basic');
- element.diff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- diff_header: [],
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- content: [{skip: 66}],
- };
- });
-
- test('hidden', () => {
- assert.equal(element._diffHeaderItems.length, 0);
- element.push('diff.diff_header', 'diff --git a/test.jpg b/test.jpg');
- assert.equal(element._diffHeaderItems.length, 0);
- element.push('diff.diff_header', 'index 2adc47d..f9c2f2c 100644');
- assert.equal(element._diffHeaderItems.length, 0);
- element.push('diff.diff_header', '--- a/test.jpg');
- assert.equal(element._diffHeaderItems.length, 0);
- element.push('diff.diff_header', '+++ b/test.jpg');
- assert.equal(element._diffHeaderItems.length, 0);
- element.push('diff.diff_header', 'test');
- assert.equal(element._diffHeaderItems.length, 1);
- flushAsynchronousOperations();
-
- assert.equal(element.$.diffHeader.textContent.trim(), 'test');
- });
-
- test('binary files', () => {
- element.diff.binary = true;
- assert.equal(element._diffHeaderItems.length, 0);
- element.push('diff.diff_header', 'diff --git a/test.jpg b/test.jpg');
- assert.equal(element._diffHeaderItems.length, 0);
- element.push('diff.diff_header', 'test');
- assert.equal(element._diffHeaderItems.length, 1);
- element.push('diff.diff_header', 'Binary files differ');
- assert.equal(element._diffHeaderItems.length, 1);
- });
- });
-
- suite('safety and bypass', () => {
- let renderStub;
-
- setup(() => {
- element = fixture('basic');
- renderStub = sandbox.stub(element.$.diffBuilder, 'render',
- () => {
- element.$.diffBuilder.dispatchEvent(
- new CustomEvent('render', {bubbles: true, composed: true}));
- return Promise.resolve({});
- });
- const mock = document.createElement('mock-diff-response');
- sandbox.stub(element, 'getDiffLength').returns(10000);
- element.diff = mock.diffResponse;
- element.noRenderOnPrefsChange = true;
- });
-
- test('large render w/ context = 10', done => {
- element.prefs = Object.assign({}, MINIMAL_PREFS, {context: 10});
- function rendered() {
- assert.isTrue(renderStub.called);
- assert.isFalse(element._showWarning);
- done();
- element.removeEventListener('render', rendered);
- }
- element.addEventListener('render', rendered);
- element._renderDiffTable();
- });
-
- test('large render w/ whole file and bypass', done => {
- element.prefs = Object.assign({}, MINIMAL_PREFS, {context: -1});
- element._safetyBypass = 10;
- function rendered() {
- assert.isTrue(renderStub.called);
- assert.isFalse(element._showWarning);
- done();
- element.removeEventListener('render', rendered);
- }
- element.addEventListener('render', rendered);
- element._renderDiffTable();
- });
-
- test('large render w/ whole file and no bypass', done => {
- element.prefs = Object.assign({}, MINIMAL_PREFS, {context: -1});
- function rendered() {
- assert.isFalse(renderStub.called);
- assert.isTrue(element._showWarning);
- done();
- element.removeEventListener('render', rendered);
- }
- element.addEventListener('render', rendered);
- element._renderDiffTable();
- });
- });
-
- suite('blame', () => {
- setup(() => {
- element = fixture('basic');
- });
-
- test('unsetting', () => {
- element.blame = [];
- const setBlameSpy = sandbox.spy(element.$.diffBuilder, 'setBlame');
- element.classList.add('showBlame');
- element.blame = null;
- assert.isTrue(setBlameSpy.calledWithExactly(null));
- assert.isFalse(element.classList.contains('showBlame'));
- });
-
- test('setting', () => {
- const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
- element.blame = mockBlame;
- assert.isTrue(element.classList.contains('showBlame'));
- });
- });
-
- suite('trailing newline warnings', () => {
- const NO_NEWLINE_BASE = 'No newline at end of base file.';
- const NO_NEWLINE_REVISION = 'No newline at end of revision file.';
-
- const getWarning = element =>
- element.shadowRoot.querySelector('.newlineWarning').textContent;
-
- setup(() => {
- element = fixture('basic');
- element.showNewlineWarningLeft = false;
- element.showNewlineWarningRight = false;
- });
-
- test('shows combined warning if both sides set to warn', () => {
- element.showNewlineWarningLeft = true;
- element.showNewlineWarningRight = true;
- assert.include(getWarning(element),
- NO_NEWLINE_BASE + ' — ' + NO_NEWLINE_REVISION);
- });
-
- suite('showNewlineWarningLeft', () => {
- test('show warning if true', () => {
- element.showNewlineWarningLeft = true;
- assert.include(getWarning(element), NO_NEWLINE_BASE);
- });
-
- test('hide warning if false', () => {
- element.showNewlineWarningLeft = false;
- assert.notInclude(getWarning(element), NO_NEWLINE_BASE);
- });
-
- test('hide warning if undefined', () => {
- element.showNewlineWarningLeft = undefined;
- assert.notInclude(getWarning(element), NO_NEWLINE_BASE);
- });
- });
-
- suite('showNewlineWarningRight', () => {
- test('show warning if true', () => {
- element.showNewlineWarningRight = true;
- assert.include(getWarning(element), NO_NEWLINE_REVISION);
- });
-
- test('hide warning if false', () => {
- element.showNewlineWarningRight = false;
- assert.notInclude(getWarning(element), NO_NEWLINE_REVISION);
- });
-
- test('hide warning if undefined', () => {
- element.showNewlineWarningRight = undefined;
- assert.notInclude(getWarning(element), NO_NEWLINE_REVISION);
- });
- });
-
- test('_computeNewlineWarningClass', () => {
- const hidden = 'newlineWarning hidden';
- const shown = 'newlineWarning';
- assert.equal(element._computeNewlineWarningClass(null, true), hidden);
- assert.equal(element._computeNewlineWarningClass('foo', true), hidden);
- assert.equal(element._computeNewlineWarningClass(null, false), hidden);
- assert.equal(element._computeNewlineWarningClass('foo', false), shown);
- });
- });
-
- suite('key locations', () => {
- let renderStub;
-
- setup(() => {
- element = fixture('basic');
- element.prefs = {};
- renderStub = sandbox.stub(element.$.diffBuilder, 'render')
- .returns(new Promise(() => {}));
- });
-
- test('lineOfInterest is a key location', () => {
- element.lineOfInterest = {number: 789, leftSide: true};
- element._renderDiffTable();
- assert.isTrue(renderStub.called);
- assert.deepEqual(renderStub.lastCall.args[0], {
- left: {789: true},
- right: {},
- });
- });
-
- test('line comments are key locations', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('comment-side', 'right');
- threadEl.setAttribute('line-num', 3);
- Polymer.dom(element).appendChild(threadEl);
- Polymer.dom.flush();
-
- element._renderDiffTable();
- assert.isTrue(renderStub.called);
- assert.deepEqual(renderStub.lastCall.args[0], {
- left: {},
- right: {3: true},
- });
- });
-
- test('file comments are key locations', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('comment-side', 'left');
- Polymer.dom(element).appendChild(threadEl);
- Polymer.dom.flush();
-
- element._renderDiffTable();
- assert.isTrue(renderStub.called);
- assert.deepEqual(renderStub.lastCall.args[0], {
- left: {FILE: true},
- right: {},
- });
- });
- });
-
- suite('whitespace changes only message', () => {
- const setupDiff = function(ignore_whitespace, diffContent) {
- element = fixture('basic');
+ element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
+ element.isImageDiff = true;
element.prefs = {
- ignore_whitespace,
auto_hide_diff_table_header: true,
context: 10,
cursor_blink_rate: 0,
font_size: 12,
+ ignore_whitespace: 'IGNORE_NONE',
intraline_difference: true,
line_length: 100,
line_wrapping: false,
@@ -976,98 +282,809 @@
tab_size: 8,
theme: 'DEFAULT',
};
+ });
+ test('renders image diffs with same file name', done => {
+ const rendered = () => {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diffBuilder._builder, GrDiffBuilderImage);
+
+ // Left image rendered with the parent commit's version of the file.
+ const leftImage = element.$.diffTable.querySelector('td.left img');
+ const leftLabel =
+ element.$.diffTable.querySelector('td.left label');
+ const leftLabelContent = leftLabel.querySelector('.label');
+ const leftLabelName = leftLabel.querySelector('.name');
+
+ const rightImage =
+ element.$.diffTable.querySelector('td.right img');
+ const rightLabel = element.$.diffTable.querySelector(
+ 'td.right label');
+ const rightLabelContent = rightLabel.querySelector('.label');
+ const rightLabelName = rightLabel.querySelector('.name');
+
+ assert.isNotOk(rightLabelName);
+ assert.isNotOk(leftLabelName);
+
+ let leftLoaded = false;
+ let rightLoaded = false;
+
+ leftImage.addEventListener('load', () => {
+ assert.isOk(leftImage);
+ assert.equal(leftImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile1.body);
+ assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
+ leftLoaded = true;
+ if (rightLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
+ });
+
+ rightImage.addEventListener('load', () => {
+ assert.isOk(rightImage);
+ assert.equal(rightImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile2.body);
+ assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
+
+ rightLoaded = true;
+ if (leftLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
+ });
+ };
+
+ element.addEventListener('render', rendered);
+
+ element.baseImage = mockFile1;
+ element.revisionImage = mockFile2;
element.diff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
intraline_status: 'OK',
change_type: 'MODIFIED',
diff_header: [
- 'diff --git a/carrot.js b/carrot.js',
+ 'diff --git a/carrot.jpg b/carrot.jpg',
'index 2adc47d..f9c2f2c 100644',
- '--- a/carrot.js',
- '+++ b/carrot.jjs',
- 'file differ',
+ '--- a/carrot.jpg',
+ '+++ b/carrot.jpg',
+ 'Binary files differ',
],
- content: diffContent,
+ content: [{skip: 66}],
binary: true,
};
+ });
+
+ test('renders image diffs with a different file name', done => {
+ const mockDiff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot2.jpg',
+ 'index 2adc47d..f9c2f2c 100644',
+ '--- a/carrot.jpg',
+ '+++ b/carrot2.jpg',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+
+ const rendered = () => {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diffBuilder._builder, GrDiffBuilderImage);
+
+ // Left image rendered with the parent commit's version of the file.
+ const leftImage = element.$.diffTable.querySelector('td.left img');
+ const leftLabel =
+ element.$.diffTable.querySelector('td.left label');
+ const leftLabelContent = leftLabel.querySelector('.label');
+ const leftLabelName = leftLabel.querySelector('.name');
+
+ const rightImage =
+ element.$.diffTable.querySelector('td.right img');
+ const rightLabel = element.$.diffTable.querySelector(
+ 'td.right label');
+ const rightLabelContent = rightLabel.querySelector('.label');
+ const rightLabelName = rightLabel.querySelector('.name');
+
+ assert.isOk(rightLabelName);
+ assert.isOk(leftLabelName);
+ assert.equal(leftLabelName.textContent, mockDiff.meta_a.name);
+ assert.equal(rightLabelName.textContent, mockDiff.meta_b.name);
+
+ let leftLoaded = false;
+ let rightLoaded = false;
+
+ leftImage.addEventListener('load', () => {
+ assert.isOk(leftImage);
+ assert.equal(leftImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile1.body);
+ assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
+ leftLoaded = true;
+ if (rightLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
+ });
+
+ rightImage.addEventListener('load', () => {
+ assert.isOk(rightImage);
+ assert.equal(rightImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile2.body);
+ assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
+
+ rightLoaded = true;
+ if (leftLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
+ });
+ };
+
+ element.addEventListener('render', rendered);
+
+ element.baseImage = mockFile1;
+ element.baseImage._name = mockDiff.meta_a.name;
+ element.revisionImage = mockFile2;
+ element.revisionImage._name = mockDiff.meta_b.name;
+ element.diff = mockDiff;
+ });
+
+ test('renders added image', done => {
+ const mockDiff = {
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'ADDED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index 0000000..f9c2f2c 100644',
+ '--- /dev/null',
+ '+++ b/carrot.jpg',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+
+ function rendered() {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diffBuilder._builder, GrDiffBuilderImage);
+
+ const leftImage = element.$.diffTable.querySelector('td.left img');
+ const rightImage = element.$.diffTable.querySelector('td.right img');
+
+ assert.isNotOk(leftImage);
+ assert.isOk(rightImage);
+ done();
+ element.removeEventListener('render', rendered);
+ }
+ element.addEventListener('render', rendered);
+
+ element.revisionImage = mockFile2;
+ element.diff = mockDiff;
+ });
+
+ test('renders removed image', done => {
+ const mockDiff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'DELETED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index f9c2f2c..0000000 100644',
+ '--- a/carrot.jpg',
+ '+++ /dev/null',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+
+ function rendered() {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diffBuilder._builder, GrDiffBuilderImage);
+
+ const leftImage = element.$.diffTable.querySelector('td.left img');
+ const rightImage = element.$.diffTable.querySelector('td.right img');
+
+ assert.isOk(leftImage);
+ assert.isNotOk(rightImage);
+ done();
+ element.removeEventListener('render', rendered);
+ }
+ element.addEventListener('render', rendered);
+
+ element.baseImage = mockFile1;
+ element.diff = mockDiff;
+ });
+
+ test('does not render disallowed image type', done => {
+ const mockDiff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'DELETED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index f9c2f2c..0000000 100644',
+ '--- a/carrot.jpg',
+ '+++ /dev/null',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+ mockFile1.type = 'image/jpeg-evil';
+
+ function rendered() {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diffBuilder._builder, GrDiffBuilderImage);
+ const leftImage = element.$.diffTable.querySelector('td.left img');
+ assert.isNotOk(leftImage);
+ done();
+ element.removeEventListener('render', rendered);
+ }
+ element.addEventListener('render', rendered);
+
+ element.baseImage = mockFile1;
+ element.diff = mockDiff;
+ });
+ });
+
+ test('_handleTap lineNum', done => {
+ const addDraftStub = sandbox.stub(element, 'addDraftAtLine');
+ const el = document.createElement('div');
+ el.className = 'lineNum';
+ el.addEventListener('click', e => {
+ element._handleTap(e);
+ assert.isTrue(addDraftStub.called);
+ assert.equal(addDraftStub.lastCall.args[0], el);
+ done();
+ });
+ el.click();
+ });
+
+ test('_handleTap context', done => {
+ const showContextStub =
+ sandbox.stub(element.$.diffBuilder, 'showContext');
+ const el = document.createElement('div');
+ el.className = 'showContext';
+ el.addEventListener('click', e => {
+ element._handleTap(e);
+ assert.isTrue(showContextStub.called);
+ done();
+ });
+ el.click();
+ });
+
+ test('_handleTap content', done => {
+ const content = document.createElement('div');
+ const lineEl = document.createElement('div');
+
+ const selectStub = sandbox.stub(element, '_selectLine');
+ sandbox.stub(element.$.diffBuilder, 'getLineElByChild', () => lineEl);
+
+ content.className = 'content';
+ content.addEventListener('click', e => {
+ element._handleTap(e);
+ assert.isTrue(selectStub.called);
+ assert.equal(selectStub.lastCall.args[0], lineEl);
+ done();
+ });
+ content.click();
+ });
+
+ suite('getCursorStops', () => {
+ const setupDiff = function() {
+ const mock = document.createElement('mock-diff-response');
+ element.diff = mock.diffResponse;
+ element.prefs = {
+ context: 10,
+ tab_size: 8,
+ font_size: 12,
+ line_length: 100,
+ cursor_blink_rate: 0,
+ line_wrapping: false,
+ intraline_difference: true,
+ show_line_endings: true,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ auto_hide_diff_table_header: true,
+ theme: 'DEFAULT',
+ ignore_whitespace: 'IGNORE_NONE',
+ };
element._renderDiffTable();
flushAsynchronousOperations();
};
- test('show the message if ignore_whitespace is criteria matches', () => {
- setupDiff('IGNORE_ALL', [{skip: 100}]);
- assert.isTrue(element.showNoChangeMessage(
- /* loading= */ false,
- element.prefs,
- element._diffLength
- ));
+ test('getCursorStops returns [] when hidden and noAutoRender', () => {
+ element.noAutoRender = true;
+ setupDiff();
+ element.hidden = true;
+ assert.equal(element.getCursorStops().length, 0);
});
- test('do not show the message if still loading', () => {
- setupDiff('IGNORE_ALL', [{skip: 100}]);
- assert.isFalse(element.showNoChangeMessage(
- /* loading= */ true,
- element.prefs,
- element._diffLength
- ));
- });
-
- test('do not show the message if contains valid changes', () => {
- const content = [{
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- }, {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- }];
- setupDiff('IGNORE_ALL', content);
- assert.equal(element._diffLength, 3);
- assert.isFalse(element.showNoChangeMessage(
- /* loading= */ false,
- element.prefs,
- element._diffLength
- ));
- });
-
- test('do not show message if ignore whitespace is disabled', () => {
- const content = [{
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- }, {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- }];
- setupDiff('IGNORE_NONE', content);
- assert.isFalse(element.showNoChangeMessage(
- /* loading= */ false,
- element.prefs,
- element._diffLength
- ));
+ test('getCursorStops', () => {
+ setupDiff();
+ assert.equal(element.getCursorStops().length, 50);
});
});
- test('getDiffLength', () => {
- const diff = document.createElement('mock-diff-response').diffResponse;
- assert.equal(element.getDiffLength(diff), 52);
+ test('adds .hiddenscroll', () => {
+ Gerrit.hiddenscroll = true;
+ element.displayLine = true;
+ assert.include(element.shadowRoot
+ .querySelector('.diffContainer').className, 'hiddenscroll');
});
+ });
- test('`render` event has contentRendered field in detail', done => {
+ suite('logged in', () => {
+ let fakeLineEl;
+ setup(() => {
element = fixture('basic');
- element.prefs = {};
- sandbox.stub(element.$.diffBuilder, 'render')
- .returns(Promise.resolve());
- element.addEventListener('render', event => {
- assert.isTrue(event.detail.contentRendered);
- done();
+ element.loggedIn = true;
+ element.patchRange = {};
+
+ fakeLineEl = {
+ getAttribute: sandbox.stub().returns(42),
+ classList: {
+ contains: sandbox.stub().returns(true),
+ },
+ };
+ });
+
+ test('addDraftAtLine', () => {
+ sandbox.stub(element, '_selectLine');
+ sandbox.stub(element, '_createComment');
+ element.addDraftAtLine(fakeLineEl);
+ assert.isTrue(element._createComment
+ .calledWithExactly(fakeLineEl, 42));
+ });
+
+ test('addDraftAtLine on an edit', () => {
+ element.patchRange.basePatchNum = element.EDIT_NAME;
+ sandbox.stub(element, '_selectLine');
+ sandbox.stub(element, '_createComment');
+ const alertSpy = sandbox.spy();
+ element.addEventListener('show-alert', alertSpy);
+ element.addDraftAtLine(fakeLineEl);
+ assert.isTrue(alertSpy.called);
+ assert.isFalse(element._createComment.called);
+ });
+
+ test('addDraftAtLine on an edit base', () => {
+ element.patchRange.patchNum = element.EDIT_NAME;
+ element.patchRange.basePatchNum = element.PARENT_NAME;
+ sandbox.stub(element, '_selectLine');
+ sandbox.stub(element, '_createComment');
+ const alertSpy = sandbox.spy();
+ element.addEventListener('show-alert', alertSpy);
+ element.addDraftAtLine(fakeLineEl);
+ assert.isTrue(alertSpy.called);
+ assert.isFalse(element._createComment.called);
+ });
+
+ suite('change in preferences', () => {
+ setup(() => {
+ element.diff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ diff_header: [],
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ content: [{skip: 66}],
+ };
+ element.flushDebouncer('renderDiffTable');
});
+
+ test('change in preferences re-renders diff', () => {
+ sandbox.stub(element, '_renderDiffTable');
+ element.prefs = Object.assign(
+ {}, MINIMAL_PREFS, {time_format: 'HHMM_12'});
+ element.flushDebouncer('renderDiffTable');
+ assert.isTrue(element._renderDiffTable.called);
+ });
+
+ test('change in preferences does not re-renders diff with ' +
+ 'noRenderOnPrefsChange', () => {
+ sandbox.stub(element, '_renderDiffTable');
+ element.noRenderOnPrefsChange = true;
+ element.prefs = Object.assign(
+ {}, MINIMAL_PREFS, {time_format: 'HHMM_12'});
+ element.flushDebouncer('renderDiffTable');
+ assert.isFalse(element._renderDiffTable.called);
+ });
+ });
+ });
+
+ suite('diff header', () => {
+ setup(() => {
+ element = fixture('basic');
+ element.diff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ diff_header: [],
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ content: [{skip: 66}],
+ };
+ });
+
+ test('hidden', () => {
+ assert.equal(element._diffHeaderItems.length, 0);
+ element.push('diff.diff_header', 'diff --git a/test.jpg b/test.jpg');
+ assert.equal(element._diffHeaderItems.length, 0);
+ element.push('diff.diff_header', 'index 2adc47d..f9c2f2c 100644');
+ assert.equal(element._diffHeaderItems.length, 0);
+ element.push('diff.diff_header', '--- a/test.jpg');
+ assert.equal(element._diffHeaderItems.length, 0);
+ element.push('diff.diff_header', '+++ b/test.jpg');
+ assert.equal(element._diffHeaderItems.length, 0);
+ element.push('diff.diff_header', 'test');
+ assert.equal(element._diffHeaderItems.length, 1);
+ flushAsynchronousOperations();
+
+ assert.equal(element.$.diffHeader.textContent.trim(), 'test');
+ });
+
+ test('binary files', () => {
+ element.diff.binary = true;
+ assert.equal(element._diffHeaderItems.length, 0);
+ element.push('diff.diff_header', 'diff --git a/test.jpg b/test.jpg');
+ assert.equal(element._diffHeaderItems.length, 0);
+ element.push('diff.diff_header', 'test');
+ assert.equal(element._diffHeaderItems.length, 1);
+ element.push('diff.diff_header', 'Binary files differ');
+ assert.equal(element._diffHeaderItems.length, 1);
+ });
+ });
+
+ suite('safety and bypass', () => {
+ let renderStub;
+
+ setup(() => {
+ element = fixture('basic');
+ renderStub = sandbox.stub(element.$.diffBuilder, 'render',
+ () => {
+ element.$.diffBuilder.dispatchEvent(
+ new CustomEvent('render', {bubbles: true, composed: true}));
+ return Promise.resolve({});
+ });
+ const mock = document.createElement('mock-diff-response');
+ sandbox.stub(element, 'getDiffLength').returns(10000);
+ element.diff = mock.diffResponse;
+ element.noRenderOnPrefsChange = true;
+ });
+
+ test('large render w/ context = 10', done => {
+ element.prefs = Object.assign({}, MINIMAL_PREFS, {context: 10});
+ function rendered() {
+ assert.isTrue(renderStub.called);
+ assert.isFalse(element._showWarning);
+ done();
+ element.removeEventListener('render', rendered);
+ }
+ element.addEventListener('render', rendered);
+ element._renderDiffTable();
+ });
+
+ test('large render w/ whole file and bypass', done => {
+ element.prefs = Object.assign({}, MINIMAL_PREFS, {context: -1});
+ element._safetyBypass = 10;
+ function rendered() {
+ assert.isTrue(renderStub.called);
+ assert.isFalse(element._showWarning);
+ done();
+ element.removeEventListener('render', rendered);
+ }
+ element.addEventListener('render', rendered);
+ element._renderDiffTable();
+ });
+
+ test('large render w/ whole file and no bypass', done => {
+ element.prefs = Object.assign({}, MINIMAL_PREFS, {context: -1});
+ function rendered() {
+ assert.isFalse(renderStub.called);
+ assert.isTrue(element._showWarning);
+ done();
+ element.removeEventListener('render', rendered);
+ }
+ element.addEventListener('render', rendered);
element._renderDiffTable();
});
});
- a11ySuite('basic');
+ suite('blame', () => {
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ test('unsetting', () => {
+ element.blame = [];
+ const setBlameSpy = sandbox.spy(element.$.diffBuilder, 'setBlame');
+ element.classList.add('showBlame');
+ element.blame = null;
+ assert.isTrue(setBlameSpy.calledWithExactly(null));
+ assert.isFalse(element.classList.contains('showBlame'));
+ });
+
+ test('setting', () => {
+ const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
+ element.blame = mockBlame;
+ assert.isTrue(element.classList.contains('showBlame'));
+ });
+ });
+
+ suite('trailing newline warnings', () => {
+ const NO_NEWLINE_BASE = 'No newline at end of base file.';
+ const NO_NEWLINE_REVISION = 'No newline at end of revision file.';
+
+ const getWarning = element =>
+ element.shadowRoot.querySelector('.newlineWarning').textContent;
+
+ setup(() => {
+ element = fixture('basic');
+ element.showNewlineWarningLeft = false;
+ element.showNewlineWarningRight = false;
+ });
+
+ test('shows combined warning if both sides set to warn', () => {
+ element.showNewlineWarningLeft = true;
+ element.showNewlineWarningRight = true;
+ assert.include(getWarning(element),
+ NO_NEWLINE_BASE + ' — ' + NO_NEWLINE_REVISION);
+ });
+
+ suite('showNewlineWarningLeft', () => {
+ test('show warning if true', () => {
+ element.showNewlineWarningLeft = true;
+ assert.include(getWarning(element), NO_NEWLINE_BASE);
+ });
+
+ test('hide warning if false', () => {
+ element.showNewlineWarningLeft = false;
+ assert.notInclude(getWarning(element), NO_NEWLINE_BASE);
+ });
+
+ test('hide warning if undefined', () => {
+ element.showNewlineWarningLeft = undefined;
+ assert.notInclude(getWarning(element), NO_NEWLINE_BASE);
+ });
+ });
+
+ suite('showNewlineWarningRight', () => {
+ test('show warning if true', () => {
+ element.showNewlineWarningRight = true;
+ assert.include(getWarning(element), NO_NEWLINE_REVISION);
+ });
+
+ test('hide warning if false', () => {
+ element.showNewlineWarningRight = false;
+ assert.notInclude(getWarning(element), NO_NEWLINE_REVISION);
+ });
+
+ test('hide warning if undefined', () => {
+ element.showNewlineWarningRight = undefined;
+ assert.notInclude(getWarning(element), NO_NEWLINE_REVISION);
+ });
+ });
+
+ test('_computeNewlineWarningClass', () => {
+ const hidden = 'newlineWarning hidden';
+ const shown = 'newlineWarning';
+ assert.equal(element._computeNewlineWarningClass(null, true), hidden);
+ assert.equal(element._computeNewlineWarningClass('foo', true), hidden);
+ assert.equal(element._computeNewlineWarningClass(null, false), hidden);
+ assert.equal(element._computeNewlineWarningClass('foo', false), shown);
+ });
+ });
+
+ suite('key locations', () => {
+ let renderStub;
+
+ setup(() => {
+ element = fixture('basic');
+ element.prefs = {};
+ renderStub = sandbox.stub(element.$.diffBuilder, 'render')
+ .returns(new Promise(() => {}));
+ });
+
+ test('lineOfInterest is a key location', () => {
+ element.lineOfInterest = {number: 789, leftSide: true};
+ element._renderDiffTable();
+ assert.isTrue(renderStub.called);
+ assert.deepEqual(renderStub.lastCall.args[0], {
+ left: {789: true},
+ right: {},
+ });
+ });
+
+ test('line comments are key locations', () => {
+ const threadEl = document.createElement('div');
+ threadEl.className = 'comment-thread';
+ threadEl.setAttribute('comment-side', 'right');
+ threadEl.setAttribute('line-num', 3);
+ dom(element).appendChild(threadEl);
+ flush();
+
+ element._renderDiffTable();
+ assert.isTrue(renderStub.called);
+ assert.deepEqual(renderStub.lastCall.args[0], {
+ left: {},
+ right: {3: true},
+ });
+ });
+
+ test('file comments are key locations', () => {
+ const threadEl = document.createElement('div');
+ threadEl.className = 'comment-thread';
+ threadEl.setAttribute('comment-side', 'left');
+ dom(element).appendChild(threadEl);
+ flush();
+
+ element._renderDiffTable();
+ assert.isTrue(renderStub.called);
+ assert.deepEqual(renderStub.lastCall.args[0], {
+ left: {FILE: true},
+ right: {},
+ });
+ });
+ });
+ const setupSampleDiff = function(params) {
+ const {ignore_whitespace, content} = params;
+ element = fixture('basic');
+ element.prefs = {
+ ignore_whitespace: ignore_whitespace || 'IGNORE_ALL',
+ auto_hide_diff_table_header: true,
+ context: 10,
+ cursor_blink_rate: 0,
+ font_size: 12,
+ intraline_difference: true,
+ line_length: 100,
+ line_wrapping: false,
+ show_line_endings: true,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ tab_size: 8,
+ theme: 'DEFAULT',
+ };
+ element.diff = {
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/carrot.js b/carrot.js',
+ 'index 2adc47d..f9c2f2c 100644',
+ '--- a/carrot.js',
+ '+++ b/carrot.jjs',
+ 'file differ',
+ ],
+ content,
+ binary: false,
+ };
+ element._renderDiffTable();
+ flushAsynchronousOperations();
+ };
+
+ test('clear diff table content as soon as diff changes', () => {
+ const content = [{
+ a: ['all work and no play make andybons a dull boy'],
+ }, {
+ b: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ ],
+ }];
+ function assertDiffTableWithContent() {
+ assert.isTrue(element.$.diffTable.innerText.includes(content[0].a));
+ }
+ setupSampleDiff({content});
+ assertDiffTableWithContent();
+ const diffCopy = Object.assign({}, element.diff);
+ element.diff = diffCopy;
+ // immediatelly cleaned up
+ assert.equal(element.$.diffTable.innerHTML, '');
+ element._renderDiffTable();
+ flushAsynchronousOperations();
+ // rendered again
+ assertDiffTableWithContent();
+ });
+
+ suite('whitespace changes only message', () => {
+ test('show the message if ignore_whitespace is criteria matches', () => {
+ setupSampleDiff({content: [{skip: 100}]});
+ assert.isTrue(element.showNoChangeMessage(
+ /* loading= */ false,
+ element.prefs,
+ element._diffLength
+ ));
+ });
+
+ test('do not show the message if still loading', () => {
+ setupSampleDiff({content: [{skip: 100}]});
+ assert.isFalse(element.showNoChangeMessage(
+ /* loading= */ true,
+ element.prefs,
+ element._diffLength
+ ));
+ });
+
+ test('do not show the message if contains valid changes', () => {
+ const content = [{
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ }, {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ }];
+ setupSampleDiff({content});
+ assert.equal(element._diffLength, 3);
+ assert.isFalse(element.showNoChangeMessage(
+ /* loading= */ false,
+ element.prefs,
+ element._diffLength
+ ));
+ });
+
+ test('do not show message if ignore whitespace is disabled', () => {
+ const content = [{
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ }, {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ }];
+ setupSampleDiff({ignore_whitespace: 'IGNORE_NONE', content});
+ assert.isFalse(element.showNoChangeMessage(
+ /* loading= */ false,
+ element.prefs,
+ element._diffLength
+ ));
+ });
+ });
+
+ test('getDiffLength', () => {
+ const diff = document.createElement('mock-diff-response').diffResponse;
+ assert.equal(element.getDiffLength(diff), 52);
+ });
+
+ test('`render` event has contentRendered field in detail', done => {
+ element = fixture('basic');
+ element.prefs = {};
+ sandbox.stub(element.$.diffBuilder, 'render')
+ .returns(Promise.resolve());
+ element.addEventListener('render', event => {
+ assert.isTrue(event.detail.contentRendered);
+ done();
+ });
+ element._renderDiffTable();
+ });
+});
+
+a11ySuite('basic');
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
deleted file mode 100644
index ee1f536..0000000
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
+++ /dev/null
@@ -1,92 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-dropdown-list/gr-dropdown-list.html">
-<link rel="import" href="../../shared/gr-count-string-formatter/gr-count-string-formatter.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-
-<dom-module id="gr-patch-range-select">
- <template>
- <style include="shared-styles">
- :host {
- align-items: center;
- display: flex;
- }
- select {
- max-width: 15em;
- }
- .arrow {
- color: var(--deemphasized-text-color);
- margin: 0 var(--spacing-m);
- }
- gr-dropdown-list {
- --trigger-style: {
- color: var(--deemphasized-text-color);
- text-transform: none;
- font-family: var(--font-family);
- }
- --trigger-hover-color: rgba(0,0,0,.6);
- }
- @media screen and (max-width: 50em) {
- .filesWeblinks {
- display: none;
- }
- gr-dropdown-list {
- --native-select-style: {
- max-width: 5.25em;
- }
- --dropdown-content-stype: {
- max-width: 300px;
- }
- }
- }
- </style>
- <span class="patchRange">
- <gr-dropdown-list
- id="basePatchDropdown"
- value="[[basePatchNum]]"
- on-value-change="_handlePatchChange"
- items="[[_baseDropdownContent]]">
- </gr-dropdown-list>
- </span>
- <span is="dom-if" if="[[filesWeblinks.meta_a]]" class="filesWeblinks">
- <template is="dom-repeat" items="[[filesWeblinks.meta_a]]" as="weblink">
- <a target="_blank" rel="noopener"
- href$="[[weblink.url]]">[[weblink.name]]</a>
- </template>
- </span>
- <span class="arrow">→</span>
- <span class="patchRange">
- <gr-dropdown-list
- id="patchNumDropdown"
- value="[[patchNum]]"
- on-value-change="_handlePatchChange"
- items="[[_patchDropdownContent]]">
- </gr-dropdown-list>
- <span is="dom-if" if="[[filesWeblinks.meta_b]]" class="filesWeblinks">
- <template is="dom-repeat" items="[[filesWeblinks.meta_b]]" as="weblink">
- <a target="_blank"
- href$="[[weblink.url]]">[[weblink.name]]</a>
- </template>
- </span>
- </span>
- </template>
- <script src="gr-patch-range-select.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
index d24e0bc..e9a64c4 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,278 +14,290 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- // Maximum length for patch set descriptions.
- const PATCH_DESC_MAX_LENGTH = 500;
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-dropdown-list/gr-dropdown-list.js';
+import '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
+import '../../shared/gr-select/gr-select.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-patch-range-select_html.js';
- /**
- * @appliesMixin Gerrit.PatchSetMixin
- */
- /**
- * Fired when the patch range changes
- *
- * @event patch-range-change
- *
- * @property {string} patchNum
- * @property {string} basePatchNum
- * @extends Polymer.Element
- */
- class GrPatchRangeSelect extends Polymer.mixinBehaviors( [
- Gerrit.PatchSetBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-patch-range-select'; }
+// Maximum length for patch set descriptions.
+const PATCH_DESC_MAX_LENGTH = 500;
- static get properties() {
- return {
- availablePatches: Array,
- _baseDropdownContent: {
- type: Object,
- computed: '_computeBaseDropdownContent(availablePatches, patchNum,' +
- '_sortedRevisions, changeComments, revisionInfo)',
- },
- _patchDropdownContent: {
- type: Object,
- computed: '_computePatchDropdownContent(availablePatches,' +
- 'basePatchNum, _sortedRevisions, changeComments)',
- },
- changeNum: String,
- changeComments: Object,
- /** @type {{ meta_a: !Array, meta_b: !Array}} */
- filesWeblinks: Object,
- patchNum: String,
- basePatchNum: String,
- revisions: Object,
- revisionInfo: Object,
- _sortedRevisions: Array,
- };
- }
+/**
+ * @appliesMixin Gerrit.PatchSetMixin
+ */
+/**
+ * Fired when the patch range changes
+ *
+ * @event patch-range-change
+ *
+ * @property {string} patchNum
+ * @property {string} basePatchNum
+ * @extends Polymer.Element
+ */
+class GrPatchRangeSelect extends mixinBehaviors( [
+ Gerrit.PatchSetBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- static get observers() {
- return [
- '_updateSortedRevisions(revisions.*)',
- ];
- }
+ static get is() { return 'gr-patch-range-select'; }
- _getShaForPatch(patch) {
- return patch.sha.substring(0, 10);
- }
-
- _computeBaseDropdownContent(availablePatches, patchNum, _sortedRevisions,
- changeComments, revisionInfo) {
- // Polymer 2: check for undefined
- if ([
- availablePatches,
- patchNum,
- _sortedRevisions,
- changeComments,
- revisionInfo,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const parentCounts = revisionInfo.getParentCountMap();
- const currentParentCount = parentCounts.hasOwnProperty(patchNum) ?
- parentCounts[patchNum] : 1;
- const maxParents = revisionInfo.getMaxParents();
- const isMerge = currentParentCount > 1;
-
- const dropdownContent = [];
- for (const basePatch of availablePatches) {
- const basePatchNum = basePatch.num;
- const entry = this._createDropdownEntry(basePatchNum, 'Patchset ',
- _sortedRevisions, changeComments, this._getShaForPatch(basePatch));
- dropdownContent.push(Object.assign({}, entry, {
- disabled: this._computeLeftDisabled(
- basePatch.num, patchNum, _sortedRevisions),
- }));
- }
-
- dropdownContent.push({
- text: isMerge ? 'Auto Merge' : 'Base',
- value: 'PARENT',
- });
-
- for (let idx = 0; isMerge && idx < maxParents; idx++) {
- dropdownContent.push({
- disabled: idx >= currentParentCount,
- triggerText: `Parent ${idx + 1}`,
- text: `Parent ${idx + 1}`,
- mobileText: `Parent ${idx + 1}`,
- value: -(idx + 1),
- });
- }
-
- return dropdownContent;
- }
-
- _computeMobileText(patchNum, changeComments, revisions) {
- return `${patchNum}` +
- `${this._computePatchSetCommentsString(changeComments, patchNum)}` +
- `${this._computePatchSetDescription(revisions, patchNum, true)}`;
- }
-
- _computePatchDropdownContent(availablePatches, basePatchNum,
- _sortedRevisions, changeComments) {
- // Polymer 2: check for undefined
- if ([
- availablePatches,
- basePatchNum,
- _sortedRevisions,
- changeComments,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const dropdownContent = [];
- for (const patch of availablePatches) {
- const patchNum = patch.num;
- const entry = this._createDropdownEntry(
- patchNum, patchNum === 'edit' ? '' : 'Patchset ', _sortedRevisions,
- changeComments, this._getShaForPatch(patch));
- dropdownContent.push(Object.assign({}, entry, {
- disabled: this._computeRightDisabled(basePatchNum, patchNum,
- _sortedRevisions),
- }));
- }
- return dropdownContent;
- }
-
- _computeText(patchNum, prefix, changeComments, sha) {
- return `${prefix}${patchNum}` +
- `${this._computePatchSetCommentsString(changeComments, patchNum)}` +
- (` | ${sha}`);
- }
-
- _createDropdownEntry(patchNum, prefix, sortedRevisions, changeComments,
- sha) {
- const entry = {
- triggerText: `${prefix}${patchNum}`,
- text: this._computeText(patchNum, prefix, changeComments, sha),
- mobileText: this._computeMobileText(patchNum, changeComments,
- sortedRevisions),
- bottomText: `${this._computePatchSetDescription(
- sortedRevisions, patchNum)}`,
- value: patchNum,
- };
- const date = this._computePatchSetDate(sortedRevisions, patchNum);
- if (date) {
- entry['date'] = date;
- }
- return entry;
- }
-
- _updateSortedRevisions(revisionsRecord) {
- const revisions = revisionsRecord.base;
- this._sortedRevisions = this.sortRevisions(Object.values(revisions));
- }
-
- /**
- * The basePatchNum should always be <= patchNum -- because sortedRevisions
- * is sorted in reverse order (higher patchset nums first), invalid base
- * patch nums have an index greater than the index of patchNum.
- *
- * @param {number|string} basePatchNum The possible base patch num.
- * @param {number|string} patchNum The current selected patch num.
- * @param {!Array} sortedRevisions
- */
- _computeLeftDisabled(basePatchNum, patchNum, sortedRevisions) {
- return this.findSortedIndex(basePatchNum, sortedRevisions) <=
- this.findSortedIndex(patchNum, sortedRevisions);
- }
-
- /**
- * The basePatchNum should always be <= patchNum -- because sortedRevisions
- * is sorted in reverse order (higher patchset nums first), invalid patch
- * nums have an index greater than the index of basePatchNum.
- *
- * In addition, if the current basePatchNum is 'PARENT', all patchNums are
- * valid.
- *
- * If the curent basePatchNum is a parent index, then only patches that have
- * at least that many parents are valid.
- *
- * @param {number|string} basePatchNum The current selected base patch num.
- * @param {number|string} patchNum The possible patch num.
- * @param {!Array} sortedRevisions
- * @return {boolean}
- */
- _computeRightDisabled(basePatchNum, patchNum, sortedRevisions) {
- if (this.patchNumEquals(basePatchNum, 'PARENT')) { return false; }
-
- if (this.isMergeParent(basePatchNum)) {
- // Note: parent indices use 1-offset.
- return this.revisionInfo.getParentCount(patchNum) <
- this.getParentIndex(basePatchNum);
- }
-
- return this.findSortedIndex(basePatchNum, sortedRevisions) <=
- this.findSortedIndex(patchNum, sortedRevisions);
- }
-
- _computePatchSetCommentsString(changeComments, patchNum) {
- if (!changeComments) { return; }
-
- const commentCount = changeComments.computeCommentCount(patchNum);
- const commentString = GrCountStringFormatter.computePluralString(
- commentCount, 'comment');
-
- const unresolvedCount = changeComments.computeUnresolvedNum(patchNum);
- const unresolvedString = GrCountStringFormatter.computeString(
- unresolvedCount, 'unresolved');
-
- if (!commentString.length && !unresolvedString.length) {
- return '';
- }
-
- return ` (${commentString}` +
- // Add a comma + space if both comments and unresolved
- (commentString && unresolvedString ? ', ' : '') +
- `${unresolvedString})`;
- }
-
- /**
- * @param {!Array} revisions
- * @param {number|string} patchNum
- * @param {boolean=} opt_addFrontSpace
- */
- _computePatchSetDescription(revisions, patchNum, opt_addFrontSpace) {
- const rev = this.getRevisionByPatchNum(revisions, patchNum);
- return (rev && rev.description) ?
- (opt_addFrontSpace ? ' ' : '') +
- rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
- }
-
- /**
- * @param {!Array} revisions
- * @param {number|string} patchNum
- */
- _computePatchSetDate(revisions, patchNum) {
- const rev = this.getRevisionByPatchNum(revisions, patchNum);
- return rev ? rev.created : undefined;
- }
-
- /**
- * Catches value-change events from the patchset dropdowns and determines
- * whether or not a patch change event should be fired.
- */
- _handlePatchChange(e) {
- const detail = {patchNum: this.patchNum, basePatchNum: this.basePatchNum};
- const target = Polymer.dom(e).localTarget;
-
- if (target === this.$.patchNumDropdown) {
- detail.patchNum = e.detail.value;
- } else {
- detail.basePatchNum = e.detail.value;
- }
-
- this.dispatchEvent(
- new CustomEvent('patch-range-change', {detail, bubbles: false}));
- }
+ static get properties() {
+ return {
+ availablePatches: Array,
+ _baseDropdownContent: {
+ type: Object,
+ computed: '_computeBaseDropdownContent(availablePatches, patchNum,' +
+ '_sortedRevisions, changeComments, revisionInfo)',
+ },
+ _patchDropdownContent: {
+ type: Object,
+ computed: '_computePatchDropdownContent(availablePatches,' +
+ 'basePatchNum, _sortedRevisions, changeComments)',
+ },
+ changeNum: String,
+ changeComments: Object,
+ /** @type {{ meta_a: !Array, meta_b: !Array}} */
+ filesWeblinks: Object,
+ patchNum: String,
+ basePatchNum: String,
+ revisions: Object,
+ revisionInfo: Object,
+ _sortedRevisions: Array,
+ };
}
- customElements.define(GrPatchRangeSelect.is, GrPatchRangeSelect);
-})();
+ static get observers() {
+ return [
+ '_updateSortedRevisions(revisions.*)',
+ ];
+ }
+
+ _getShaForPatch(patch) {
+ return patch.sha.substring(0, 10);
+ }
+
+ _computeBaseDropdownContent(availablePatches, patchNum, _sortedRevisions,
+ changeComments, revisionInfo) {
+ // Polymer 2: check for undefined
+ if ([
+ availablePatches,
+ patchNum,
+ _sortedRevisions,
+ changeComments,
+ revisionInfo,
+ ].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const parentCounts = revisionInfo.getParentCountMap();
+ const currentParentCount = parentCounts.hasOwnProperty(patchNum) ?
+ parentCounts[patchNum] : 1;
+ const maxParents = revisionInfo.getMaxParents();
+ const isMerge = currentParentCount > 1;
+
+ const dropdownContent = [];
+ for (const basePatch of availablePatches) {
+ const basePatchNum = basePatch.num;
+ const entry = this._createDropdownEntry(basePatchNum, 'Patchset ',
+ _sortedRevisions, changeComments, this._getShaForPatch(basePatch));
+ dropdownContent.push(Object.assign({}, entry, {
+ disabled: this._computeLeftDisabled(
+ basePatch.num, patchNum, _sortedRevisions),
+ }));
+ }
+
+ dropdownContent.push({
+ text: isMerge ? 'Auto Merge' : 'Base',
+ value: 'PARENT',
+ });
+
+ for (let idx = 0; isMerge && idx < maxParents; idx++) {
+ dropdownContent.push({
+ disabled: idx >= currentParentCount,
+ triggerText: `Parent ${idx + 1}`,
+ text: `Parent ${idx + 1}`,
+ mobileText: `Parent ${idx + 1}`,
+ value: -(idx + 1),
+ });
+ }
+
+ return dropdownContent;
+ }
+
+ _computeMobileText(patchNum, changeComments, revisions) {
+ return `${patchNum}` +
+ `${this._computePatchSetCommentsString(changeComments, patchNum)}` +
+ `${this._computePatchSetDescription(revisions, patchNum, true)}`;
+ }
+
+ _computePatchDropdownContent(availablePatches, basePatchNum,
+ _sortedRevisions, changeComments) {
+ // Polymer 2: check for undefined
+ if ([
+ availablePatches,
+ basePatchNum,
+ _sortedRevisions,
+ changeComments,
+ ].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const dropdownContent = [];
+ for (const patch of availablePatches) {
+ const patchNum = patch.num;
+ const entry = this._createDropdownEntry(
+ patchNum, patchNum === 'edit' ? '' : 'Patchset ', _sortedRevisions,
+ changeComments, this._getShaForPatch(patch));
+ dropdownContent.push(Object.assign({}, entry, {
+ disabled: this._computeRightDisabled(basePatchNum, patchNum,
+ _sortedRevisions),
+ }));
+ }
+ return dropdownContent;
+ }
+
+ _computeText(patchNum, prefix, changeComments, sha) {
+ return `${prefix}${patchNum}` +
+ `${this._computePatchSetCommentsString(changeComments, patchNum)}` +
+ (` | ${sha}`);
+ }
+
+ _createDropdownEntry(patchNum, prefix, sortedRevisions, changeComments,
+ sha) {
+ const entry = {
+ triggerText: `${prefix}${patchNum}`,
+ text: this._computeText(patchNum, prefix, changeComments, sha),
+ mobileText: this._computeMobileText(patchNum, changeComments,
+ sortedRevisions),
+ bottomText: `${this._computePatchSetDescription(
+ sortedRevisions, patchNum)}`,
+ value: patchNum,
+ };
+ const date = this._computePatchSetDate(sortedRevisions, patchNum);
+ if (date) {
+ entry['date'] = date;
+ }
+ return entry;
+ }
+
+ _updateSortedRevisions(revisionsRecord) {
+ const revisions = revisionsRecord.base;
+ this._sortedRevisions = this.sortRevisions(Object.values(revisions));
+ }
+
+ /**
+ * The basePatchNum should always be <= patchNum -- because sortedRevisions
+ * is sorted in reverse order (higher patchset nums first), invalid base
+ * patch nums have an index greater than the index of patchNum.
+ *
+ * @param {number|string} basePatchNum The possible base patch num.
+ * @param {number|string} patchNum The current selected patch num.
+ * @param {!Array} sortedRevisions
+ */
+ _computeLeftDisabled(basePatchNum, patchNum, sortedRevisions) {
+ return this.findSortedIndex(basePatchNum, sortedRevisions) <=
+ this.findSortedIndex(patchNum, sortedRevisions);
+ }
+
+ /**
+ * The basePatchNum should always be <= patchNum -- because sortedRevisions
+ * is sorted in reverse order (higher patchset nums first), invalid patch
+ * nums have an index greater than the index of basePatchNum.
+ *
+ * In addition, if the current basePatchNum is 'PARENT', all patchNums are
+ * valid.
+ *
+ * If the curent basePatchNum is a parent index, then only patches that have
+ * at least that many parents are valid.
+ *
+ * @param {number|string} basePatchNum The current selected base patch num.
+ * @param {number|string} patchNum The possible patch num.
+ * @param {!Array} sortedRevisions
+ * @return {boolean}
+ */
+ _computeRightDisabled(basePatchNum, patchNum, sortedRevisions) {
+ if (this.patchNumEquals(basePatchNum, 'PARENT')) { return false; }
+
+ if (this.isMergeParent(basePatchNum)) {
+ // Note: parent indices use 1-offset.
+ return this.revisionInfo.getParentCount(patchNum) <
+ this.getParentIndex(basePatchNum);
+ }
+
+ return this.findSortedIndex(basePatchNum, sortedRevisions) <=
+ this.findSortedIndex(patchNum, sortedRevisions);
+ }
+
+ _computePatchSetCommentsString(changeComments, patchNum) {
+ if (!changeComments) { return; }
+
+ const commentCount = changeComments.computeCommentCount({patchNum});
+ const commentString = GrCountStringFormatter.computePluralString(
+ commentCount, 'comment');
+
+ const unresolvedCount = changeComments.computeUnresolvedNum({patchNum});
+ const unresolvedString = GrCountStringFormatter.computeString(
+ unresolvedCount, 'unresolved');
+
+ if (!commentString.length && !unresolvedString.length) {
+ return '';
+ }
+
+ return ` (${commentString}` +
+ // Add a comma + space if both comments and unresolved
+ (commentString && unresolvedString ? ', ' : '') +
+ `${unresolvedString})`;
+ }
+
+ /**
+ * @param {!Array} revisions
+ * @param {number|string} patchNum
+ * @param {boolean=} opt_addFrontSpace
+ */
+ _computePatchSetDescription(revisions, patchNum, opt_addFrontSpace) {
+ const rev = this.getRevisionByPatchNum(revisions, patchNum);
+ return (rev && rev.description) ?
+ (opt_addFrontSpace ? ' ' : '') +
+ rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
+ }
+
+ /**
+ * @param {!Array} revisions
+ * @param {number|string} patchNum
+ */
+ _computePatchSetDate(revisions, patchNum) {
+ const rev = this.getRevisionByPatchNum(revisions, patchNum);
+ return rev ? rev.created : undefined;
+ }
+
+ /**
+ * Catches value-change events from the patchset dropdowns and determines
+ * whether or not a patch change event should be fired.
+ */
+ _handlePatchChange(e) {
+ const detail = {patchNum: this.patchNum, basePatchNum: this.basePatchNum};
+ const target = dom(e).localTarget;
+
+ if (target === this.$.patchNumDropdown) {
+ detail.patchNum = e.detail.value;
+ } else {
+ detail.basePatchNum = e.detail.value;
+ }
+
+ this.dispatchEvent(
+ new CustomEvent('patch-range-change', {detail, bubbles: false}));
+ }
+}
+
+customElements.define(GrPatchRangeSelect.is, GrPatchRangeSelect);
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js
new file mode 100644
index 0000000..5779a90
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js
@@ -0,0 +1,73 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ align-items: center;
+ display: flex;
+ }
+ select {
+ max-width: 15em;
+ }
+ .arrow {
+ color: var(--deemphasized-text-color);
+ margin: 0 var(--spacing-m);
+ }
+ gr-dropdown-list {
+ --trigger-style: {
+ color: var(--deemphasized-text-color);
+ text-transform: none;
+ font-family: var(--font-family);
+ }
+ --trigger-hover-color: rgba(0,0,0,.6);
+ }
+ @media screen and (max-width: 50em) {
+ .filesWeblinks {
+ display: none;
+ }
+ gr-dropdown-list {
+ --native-select-style: {
+ max-width: 5.25em;
+ }
+ --dropdown-content-stype: {
+ max-width: 300px;
+ }
+ }
+ }
+ </style>
+ <span class="patchRange">
+ <gr-dropdown-list id="basePatchDropdown" value="[[basePatchNum]]" on-value-change="_handlePatchChange" items="[[_baseDropdownContent]]">
+ </gr-dropdown-list>
+ </span>
+ <span is="dom-if" if="[[filesWeblinks.meta_a]]" class="filesWeblinks">
+ <template is="dom-repeat" items="[[filesWeblinks.meta_a]]" as="weblink">
+ <a target="_blank" rel="noopener" href\$="[[weblink.url]]">[[weblink.name]]</a>
+ </template>
+ </span>
+ <span class="arrow">→</span>
+ <span class="patchRange">
+ <gr-dropdown-list id="patchNumDropdown" value="[[patchNum]]" on-value-change="_handlePatchChange" items="[[_patchDropdownContent]]">
+ </gr-dropdown-list>
+ <span is="dom-if" if="[[filesWeblinks.meta_b]]" class="filesWeblinks">
+ <template is="dom-repeat" items="[[filesWeblinks.meta_b]]" as="weblink">
+ <a target="_blank" href\$="[[weblink.url]]">[[weblink.name]]</a>
+ </template>
+ </span>
+ </span>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
index 65dedef..797e279 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -19,21 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-patch-range-select</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="/bower_components/page/page.js"></script>
-
-<link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
-<link rel="import" href="../../shared/revision-info/revision-info.html">
-
-<link rel="import" href="gr-patch-range-select.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script src="/node_modules/page/page.js"></script>
<dom-module id="comment-api-mock">
<template>
@@ -41,8 +31,7 @@
change-comments="[[_changeComments]]"></gr-patch-range-select>
<gr-comment-api id="commentAPI"></gr-comment-api>
</template>
- <script src="../../diff/gr-comment-api/gr-comment-api-mock_test.js"></script>
-</dom-module>
+ </dom-module>
<test-fixture id="basic">
<template>
@@ -50,384 +39,390 @@
</template>
</test-fixture>
-<script>
- suite('gr-patch-range-select tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let commentApiWrapper;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../gr-comment-api/gr-comment-api.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import '../../shared/revision-info/revision-info.js';
+import './gr-patch-range-select.js';
+import '../gr-comment-api/gr-comment-api-mock_test.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-patch-range-select tests', () => {
+ let element;
+ let sandbox;
+ let commentApiWrapper;
- function getInfo(revisions) {
- const revisionObj = {};
- for (let i = 0; i < revisions.length; i++) {
- revisionObj[i] = revisions[i];
- }
- return new Gerrit.RevisionInfo({revisions: revisionObj});
+ function getInfo(revisions) {
+ const revisionObj = {};
+ for (let i = 0; i < revisions.length; i++) {
+ revisionObj[i] = revisions[i];
}
+ return new Gerrit.RevisionInfo({revisions: revisionObj});
+ }
- setup(() => {
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getDiffComments() { return Promise.resolve({}); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
+ stub('gr-rest-api-interface', {
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ });
+
+ // Element must be wrapped in an element with direct access to the
+ // comment API.
+ commentApiWrapper = fixture('basic');
+ element = commentApiWrapper.$.patchRange;
+
+ // Stub methods on the changeComments object after changeComments has
+ // been initialized.
+ return commentApiWrapper.loadComments();
+ });
+
+ teardown(() => sandbox.restore());
+
+ test('enabled/disabled options', () => {
+ const patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '3',
+ };
+ const sortedRevisions = [
+ {_number: 3},
+ {_number: element.EDIT_NAME, basePatchNum: 2},
+ {_number: 2},
+ {_number: 1},
+ ];
+ for (const patchNum of ['1', '2', '3']) {
+ assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum,
+ patchNum, sortedRevisions));
+ }
+ for (const basePatchNum of ['1', '2']) {
+ assert.isFalse(element._computeLeftDisabled(basePatchNum,
+ patchRange.patchNum, sortedRevisions));
+ }
+ assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum));
+
+ patchRange.basePatchNum = element.EDIT_NAME;
+ assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum,
+ sortedRevisions));
+ assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '1',
+ sortedRevisions));
+ assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '2',
+ sortedRevisions));
+ assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum, '3',
+ sortedRevisions));
+ assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum,
+ element.EDIT_NAME, sortedRevisions));
+ });
+
+ test('_computeBaseDropdownContent', () => {
+ const availablePatches = [
+ {num: 'edit', sha: '1'},
+ {num: 3, sha: '2'},
+ {num: 2, sha: '3'},
+ {num: 1, sha: '4'},
+ ];
+ const revisions = [
+ {
+ commit: {parents: []},
+ _number: 2,
+ description: 'description',
+ },
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ ];
+ element.revisionInfo = getInfo(revisions);
+ const patchNum = 1;
+ const sortedRevisions = [
+ {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'},
+ {_number: element.EDIT_NAME, basePatchNum: 2},
+ {_number: 2, description: 'description'},
+ {_number: 1},
+ ];
+ const expectedResult = [
+ {
+ disabled: true,
+ triggerText: 'Patchset edit',
+ text: 'Patchset edit | 1',
+ mobileText: 'edit',
+ bottomText: '',
+ value: 'edit',
+ },
+ {
+ disabled: true,
+ triggerText: 'Patchset 3',
+ text: 'Patchset 3 | 2',
+ mobileText: '3',
+ bottomText: '',
+ value: 3,
+ date: 'Mon, 01 Jan 2001 00:00:00 GMT',
+ },
+ {
+ disabled: true,
+ triggerText: 'Patchset 2',
+ text: 'Patchset 2 | 3',
+ mobileText: '2 description',
+ bottomText: 'description',
+ value: 2,
+ },
+ {
+ disabled: true,
+ triggerText: 'Patchset 1',
+ text: 'Patchset 1 | 4',
+ mobileText: '1',
+ bottomText: '',
+ value: 1,
+ },
+ {
+ text: 'Base',
+ value: 'PARENT',
+ },
+ ];
+ assert.deepEqual(element._computeBaseDropdownContent(availablePatches,
+ patchNum, sortedRevisions, element.changeComments,
+ element.revisionInfo),
+ expectedResult);
+ });
+
+ test('_computeBaseDropdownContent called when patchNum updates', () => {
+ element.revisions = [
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ element.availablePatches = [
+ {num: 1, sha: '1'},
+ {num: 2, sha: '2'},
+ {num: 3, sha: '3'},
+ {num: 'edit', sha: '4'},
+ ];
+ element.patchNum = 2;
+ element.basePatchNum = 'PARENT';
+ flushAsynchronousOperations();
+
+ sandbox.stub(element, '_computeBaseDropdownContent');
+
+ // Should be recomputed for each available patch
+ element.set('patchNum', 1);
+ assert.equal(element._computeBaseDropdownContent.callCount, 1);
+ });
+
+ test('_computeBaseDropdownContent called when changeComments update',
+ done => {
+ element.revisions = [
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ element.availablePatches = [
+ {num: 'edit', sha: '1'},
+ {num: 3, sha: '2'},
+ {num: 2, sha: '3'},
+ {num: 1, sha: '4'},
+ ];
+ element.patchNum = 2;
+ element.basePatchNum = 'PARENT';
+ flushAsynchronousOperations();
+
+ // Should be recomputed for each available patch
+ sandbox.stub(element, '_computeBaseDropdownContent');
+ assert.equal(element._computeBaseDropdownContent.callCount, 0);
+ commentApiWrapper.loadComments().then()
+ .then(() => {
+ assert.equal(element._computeBaseDropdownContent.callCount, 1);
+ done();
+ });
});
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = fixture('basic');
- element = commentApiWrapper.$.patchRange;
+ test('_computePatchDropdownContent called when basePatchNum updates', () => {
+ element.revisions = [
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ element.availablePatches = [
+ {num: 1, sha: '1'},
+ {num: 2, sha: '2'},
+ {num: 3, sha: '3'},
+ {num: 'edit', sha: '4'},
+ ];
+ element.patchNum = 2;
+ element.basePatchNum = 'PARENT';
+ flushAsynchronousOperations();
- // Stub methods on the changeComments object after changeComments has
- // been initialized.
- return commentApiWrapper.loadComments();
- });
-
- teardown(() => sandbox.restore());
-
- test('enabled/disabled options', () => {
- const patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '3',
- };
- const sortedRevisions = [
- {_number: 3},
- {_number: element.EDIT_NAME, basePatchNum: 2},
- {_number: 2},
- {_number: 1},
- ];
- for (const patchNum of ['1', '2', '3']) {
- assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum,
- patchNum, sortedRevisions));
- }
- for (const basePatchNum of ['1', '2']) {
- assert.isFalse(element._computeLeftDisabled(basePatchNum,
- patchRange.patchNum, sortedRevisions));
- }
- assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum));
-
- patchRange.basePatchNum = element.EDIT_NAME;
- assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum,
- sortedRevisions));
- assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '1',
- sortedRevisions));
- assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '2',
- sortedRevisions));
- assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum, '3',
- sortedRevisions));
- assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum,
- element.EDIT_NAME, sortedRevisions));
- });
-
- test('_computeBaseDropdownContent', () => {
- const availablePatches = [
- {num: 'edit', sha: '1'},
- {num: 3, sha: '2'},
- {num: 2, sha: '3'},
- {num: 1, sha: '4'},
- ];
- const revisions = [
- {
- commit: {parents: []},
- _number: 2,
- description: 'description',
- },
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(revisions);
- const patchNum = 1;
- const sortedRevisions = [
- {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'},
- {_number: element.EDIT_NAME, basePatchNum: 2},
- {_number: 2, description: 'description'},
- {_number: 1},
- ];
- const expectedResult = [
- {
- disabled: true,
- triggerText: 'Patchset edit',
- text: 'Patchset edit | 1',
- mobileText: 'edit',
- bottomText: '',
- value: 'edit',
- },
- {
- disabled: true,
- triggerText: 'Patchset 3',
- text: 'Patchset 3 | 2',
- mobileText: '3',
- bottomText: '',
- value: 3,
- date: 'Mon, 01 Jan 2001 00:00:00 GMT',
- },
- {
- disabled: true,
- triggerText: 'Patchset 2',
- text: 'Patchset 2 | 3',
- mobileText: '2 description',
- bottomText: 'description',
- value: 2,
- },
- {
- disabled: true,
- triggerText: 'Patchset 1',
- text: 'Patchset 1 | 4',
- mobileText: '1',
- bottomText: '',
- value: 1,
- },
- {
- text: 'Base',
- value: 'PARENT',
- },
- ];
- assert.deepEqual(element._computeBaseDropdownContent(availablePatches,
- patchNum, sortedRevisions, element.changeComments,
- element.revisionInfo),
- expectedResult);
- });
-
- test('_computeBaseDropdownContent called when patchNum updates', () => {
- element.revisions = [
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(element.revisions);
- element.availablePatches = [
- {num: 1, sha: '1'},
- {num: 2, sha: '2'},
- {num: 3, sha: '3'},
- {num: 'edit', sha: '4'},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- flushAsynchronousOperations();
-
- sandbox.stub(element, '_computeBaseDropdownContent');
-
- // Should be recomputed for each available patch
- element.set('patchNum', 1);
- assert.equal(element._computeBaseDropdownContent.callCount, 1);
- });
-
- test('_computeBaseDropdownContent called when changeComments update',
- done => {
- element.revisions = [
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(element.revisions);
- element.availablePatches = [
- {num: 'edit', sha: '1'},
- {num: 3, sha: '2'},
- {num: 2, sha: '3'},
- {num: 1, sha: '4'},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- flushAsynchronousOperations();
-
- // Should be recomputed for each available patch
- sandbox.stub(element, '_computeBaseDropdownContent');
- assert.equal(element._computeBaseDropdownContent.callCount, 0);
- commentApiWrapper.loadComments().then()
- .then(() => {
- assert.equal(element._computeBaseDropdownContent.callCount, 1);
- done();
- });
- });
-
- test('_computePatchDropdownContent called when basePatchNum updates', () => {
- element.revisions = [
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(element.revisions);
- element.availablePatches = [
- {num: 1, sha: '1'},
- {num: 2, sha: '2'},
- {num: 3, sha: '3'},
- {num: 'edit', sha: '4'},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- flushAsynchronousOperations();
-
- // Should be recomputed for each available patch
- sandbox.stub(element, '_computePatchDropdownContent');
- element.set('basePatchNum', 1);
- assert.equal(element._computePatchDropdownContent.callCount, 1);
- });
-
- test('_computePatchDropdownContent called when comments update', done => {
- element.revisions = [
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(element.revisions);
- element.availablePatches = [
- {num: 1, sha: '1'},
- {num: 2, sha: '2'},
- {num: 3, sha: '3'},
- {num: 'edit', sha: '4'},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- flushAsynchronousOperations();
-
- // Should be recomputed for each available patch
- sandbox.stub(element, '_computePatchDropdownContent');
- assert.equal(element._computePatchDropdownContent.callCount, 0);
- commentApiWrapper.loadComments().then()
- .then(() => {
- done();
- });
- });
-
- test('_computePatchDropdownContent', () => {
- const availablePatches = [
- {num: 'edit', sha: '1'},
- {num: 3, sha: '2'},
- {num: 2, sha: '3'},
- {num: 1, sha: '4'},
- ];
- const basePatchNum = 1;
- const sortedRevisions = [
- {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'},
- {_number: element.EDIT_NAME, basePatchNum: 2},
- {_number: 2, description: 'description'},
- {_number: 1},
- ];
-
- const expectedResult = [
- {
- disabled: false,
- triggerText: 'edit',
- text: 'edit | 1',
- mobileText: 'edit',
- bottomText: '',
- value: 'edit',
- },
- {
- disabled: false,
- triggerText: 'Patchset 3',
- text: 'Patchset 3 | 2',
- mobileText: '3',
- bottomText: '',
- value: 3,
- date: 'Mon, 01 Jan 2001 00:00:00 GMT',
- },
- {
- disabled: false,
- triggerText: 'Patchset 2',
- text: 'Patchset 2 | 3',
- mobileText: '2 description',
- bottomText: 'description',
- value: 2,
- },
- {
- disabled: true,
- triggerText: 'Patchset 1',
- text: 'Patchset 1 | 4',
- mobileText: '1',
- bottomText: '',
- value: 1,
- },
- ];
-
- assert.deepEqual(element._computePatchDropdownContent(availablePatches,
- basePatchNum, sortedRevisions, element.changeComments),
- expectedResult);
- });
-
- test('filesWeblinks', () => {
- element.filesWeblinks = {
- meta_a: [
- {
- name: 'foo',
- url: 'f.oo',
- },
- ],
- meta_b: [
- {
- name: 'bar',
- url: 'ba.r',
- },
- ],
- };
- flushAsynchronousOperations();
- const domApi = Polymer.dom(element.root);
- assert.equal(
- domApi.querySelector('a[href="f.oo"]').textContent, 'foo');
- assert.equal(
- domApi.querySelector('a[href="ba.r"]').textContent, 'bar');
- });
-
- test('_computePatchSetCommentsString', () => {
- // Test string with unresolved comments.
- element.changeComments._comments = {
- foo: [{
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- unresolved: true,
- updated: '2017-10-11 20:48:40.000000000',
- }],
- bar: [{
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- updated: '2017-10-12 20:48:40.000000000',
- },
- {
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- updated: '2017-10-13 20:48:40.000000000',
- }],
- abc: [],
- };
-
- assert.equal(element._computePatchSetCommentsString(
- element.changeComments, 1), ' (3 comments, 1 unresolved)');
-
- // Test string with no unresolved comments.
- delete element.changeComments._comments['foo'];
- assert.equal(element._computePatchSetCommentsString(
- element.changeComments, 1), ' (2 comments)');
-
- // Test string with no comments.
- delete element.changeComments._comments['bar'];
- assert.equal(element._computePatchSetCommentsString(
- element.changeComments, 1), '');
- });
-
- test('patch-range-change fires', () => {
- const handler = sandbox.stub();
- element.basePatchNum = 1;
- element.patchNum = 3;
- element.addEventListener('patch-range-change', handler);
-
- element.$.basePatchDropdown._handleValueChange(2, [{value: 2}]);
- assert.isTrue(handler.calledOnce);
- assert.deepEqual(handler.lastCall.args[0].detail,
- {basePatchNum: 2, patchNum: 3});
-
- // BasePatchNum should not have changed, due to one-way data binding.
- element.$.patchNumDropdown._handleValueChange('edit', [{value: 'edit'}]);
- assert.deepEqual(handler.lastCall.args[0].detail,
- {basePatchNum: 1, patchNum: 'edit'});
- });
+ // Should be recomputed for each available patch
+ sandbox.stub(element, '_computePatchDropdownContent');
+ element.set('basePatchNum', 1);
+ assert.equal(element._computePatchDropdownContent.callCount, 1);
});
+
+ test('_computePatchDropdownContent called when comments update', done => {
+ element.revisions = [
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ element.availablePatches = [
+ {num: 1, sha: '1'},
+ {num: 2, sha: '2'},
+ {num: 3, sha: '3'},
+ {num: 'edit', sha: '4'},
+ ];
+ element.patchNum = 2;
+ element.basePatchNum = 'PARENT';
+ flushAsynchronousOperations();
+
+ // Should be recomputed for each available patch
+ sandbox.stub(element, '_computePatchDropdownContent');
+ assert.equal(element._computePatchDropdownContent.callCount, 0);
+ commentApiWrapper.loadComments().then()
+ .then(() => {
+ done();
+ });
+ });
+
+ test('_computePatchDropdownContent', () => {
+ const availablePatches = [
+ {num: 'edit', sha: '1'},
+ {num: 3, sha: '2'},
+ {num: 2, sha: '3'},
+ {num: 1, sha: '4'},
+ ];
+ const basePatchNum = 1;
+ const sortedRevisions = [
+ {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'},
+ {_number: element.EDIT_NAME, basePatchNum: 2},
+ {_number: 2, description: 'description'},
+ {_number: 1},
+ ];
+
+ const expectedResult = [
+ {
+ disabled: false,
+ triggerText: 'edit',
+ text: 'edit | 1',
+ mobileText: 'edit',
+ bottomText: '',
+ value: 'edit',
+ },
+ {
+ disabled: false,
+ triggerText: 'Patchset 3',
+ text: 'Patchset 3 | 2',
+ mobileText: '3',
+ bottomText: '',
+ value: 3,
+ date: 'Mon, 01 Jan 2001 00:00:00 GMT',
+ },
+ {
+ disabled: false,
+ triggerText: 'Patchset 2',
+ text: 'Patchset 2 | 3',
+ mobileText: '2 description',
+ bottomText: 'description',
+ value: 2,
+ },
+ {
+ disabled: true,
+ triggerText: 'Patchset 1',
+ text: 'Patchset 1 | 4',
+ mobileText: '1',
+ bottomText: '',
+ value: 1,
+ },
+ ];
+
+ assert.deepEqual(element._computePatchDropdownContent(availablePatches,
+ basePatchNum, sortedRevisions, element.changeComments),
+ expectedResult);
+ });
+
+ test('filesWeblinks', () => {
+ element.filesWeblinks = {
+ meta_a: [
+ {
+ name: 'foo',
+ url: 'f.oo',
+ },
+ ],
+ meta_b: [
+ {
+ name: 'bar',
+ url: 'ba.r',
+ },
+ ],
+ };
+ flushAsynchronousOperations();
+ const domApi = dom(element.root);
+ assert.equal(
+ domApi.querySelector('a[href="f.oo"]').textContent, 'foo');
+ assert.equal(
+ domApi.querySelector('a[href="ba.r"]').textContent, 'bar');
+ });
+
+ test('_computePatchSetCommentsString', () => {
+ // Test string with unresolved comments.
+ element.changeComments._comments = {
+ foo: [{
+ id: '27dcee4d_f7b77cfa',
+ message: 'test',
+ patch_set: 1,
+ unresolved: true,
+ updated: '2017-10-11 20:48:40.000000000',
+ }],
+ bar: [{
+ id: '27dcee4d_f7b77cfa',
+ message: 'test',
+ patch_set: 1,
+ updated: '2017-10-12 20:48:40.000000000',
+ },
+ {
+ id: '27dcee4d_f7b77cfa',
+ message: 'test',
+ patch_set: 1,
+ updated: '2017-10-13 20:48:40.000000000',
+ }],
+ abc: [],
+ };
+
+ assert.equal(element._computePatchSetCommentsString(
+ element.changeComments, 1), ' (3 comments, 1 unresolved)');
+
+ // Test string with no unresolved comments.
+ delete element.changeComments._comments['foo'];
+ assert.equal(element._computePatchSetCommentsString(
+ element.changeComments, 1), ' (2 comments)');
+
+ // Test string with no comments.
+ delete element.changeComments._comments['bar'];
+ assert.equal(element._computePatchSetCommentsString(
+ element.changeComments, 1), '');
+ });
+
+ test('patch-range-change fires', () => {
+ const handler = sandbox.stub();
+ element.basePatchNum = 1;
+ element.patchNum = 3;
+ element.addEventListener('patch-range-change', handler);
+
+ element.$.basePatchDropdown._handleValueChange(2, [{value: 2}]);
+ assert.isTrue(handler.calledOnce);
+ assert.deepEqual(handler.lastCall.args[0].detail,
+ {basePatchNum: 2, patchNum: 3});
+
+ // BasePatchNum should not have changed, due to one-way data binding.
+ element.$.patchNumDropdown._handleValueChange('edit', [{value: 'edit'}]);
+ assert.deepEqual(handler.lastCall.args[0].detail,
+ {basePatchNum: 1, patchNum: 'edit'});
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html
deleted file mode 100644
index 17a4866..0000000
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.html
+++ /dev/null
@@ -1,24 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<dom-module id="gr-ranged-comment-layer">
- <template>
- </template>
- <script src="../gr-diff-highlight/gr-annotation.js"></script>
- <script src="gr-ranged-comment-layer.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
index fd94b61..8f1b1c3 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
@@ -14,204 +14,210 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- // Polymer 1 adds # before array's key, while Polymer 2 doesn't
- const HOVER_PATH_PATTERN = /^(commentRanges\.\#?\d+)\.hovering$/;
+import '../gr-diff-highlight/gr-annotation.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-ranged-comment-layer_html.js';
- const RANGE_HIGHLIGHT = 'style-scope gr-diff range';
- const HOVER_HIGHLIGHT = 'style-scope gr-diff rangeHighlight';
+// Polymer 1 adds # before array's key, while Polymer 2 doesn't
+const HOVER_PATH_PATTERN = /^(commentRanges\.\#?\d+)\.hovering$/;
- /** @extends Polymer.Element */
- class GrRangedCommentLayer extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-ranged-comment-layer'; }
- /**
- * 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
- */
+const RANGE_HIGHLIGHT = 'style-scope gr-diff range';
+const HOVER_HIGHLIGHT = 'style-scope gr-diff rangeHighlight';
- static get properties() {
- return {
- /** @type {!Array<!Gerrit.HoveredRange>} */
- commentRanges: Array,
- _listeners: {
- type: Array,
- value() { return []; },
- },
- _rangesMap: {
- type: Object,
- value() { return {left: {}, right: {}}; },
- },
- };
+/** @extends Polymer.Element */
+class GrRangedCommentLayer extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-ranged-comment-layer'; }
+ /**
+ * 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
+ */
+
+ static get properties() {
+ return {
+ /** @type {!Array<!Gerrit.HoveredRange>} */
+ commentRanges: Array,
+ _listeners: {
+ type: Array,
+ value() { return []; },
+ },
+ _rangesMap: {
+ type: Object,
+ value() { return {left: {}, right: {}}; },
+ },
+ };
+ }
+
+ static get observers() {
+ return [
+ '_handleCommentRangesChange(commentRanges.*)',
+ ];
+ }
+
+ get styleModuleName() {
+ return 'gr-ranged-comment-styles';
+ }
+
+ /**
+ * Layer method to add annotations to a line.
+ *
+ * @param {!HTMLElement} el The DIV.contentText element to apply the
+ * annotation to.
+ * @param {!HTMLElement} lineNumberEl
+ * @param {!Object} line The line object. (GrDiffLine)
+ */
+ annotate(el, lineNumberEl, line) {
+ let ranges = [];
+ if (line.type === GrDiffLine.Type.REMOVE || (
+ line.type === GrDiffLine.Type.BOTH &&
+ el.getAttribute('data-side') !== 'right')) {
+ ranges = ranges.concat(this._getRangesForLine(line, 'left'));
+ }
+ if (line.type === GrDiffLine.Type.ADD || (
+ line.type === GrDiffLine.Type.BOTH &&
+ el.getAttribute('data-side') !== 'left')) {
+ ranges = ranges.concat(this._getRangesForLine(line, 'right'));
}
- static get observers() {
- return [
- '_handleCommentRangesChange(commentRanges.*)',
- ];
+ for (const range of ranges) {
+ GrAnnotation.annotateElement(el, range.start,
+ range.end - range.start,
+ range.hovering ? HOVER_HIGHLIGHT : RANGE_HIGHLIGHT);
}
+ }
- get styleModuleName() {
- return 'gr-ranged-comment-styles';
+ /**
+ * Register a listener for layer updates.
+ *
+ * @param {function(number, number, string)} fn The update handler function.
+ * Should accept as arguments the line numbers for the start and end of
+ * the update and the side as a string.
+ */
+ addListener(fn) {
+ this._listeners.push(fn);
+ }
+
+ /**
+ * Notify Layer listeners of changes to annotations.
+ *
+ * @param {number} start The line where the update starts.
+ * @param {number} end The line where the update ends.
+ * @param {string} side The side of the update. ('left' or 'right')
+ */
+ _notifyUpdateRange(start, end, side) {
+ for (const listener of this._listeners) {
+ listener(start, end, side);
}
+ }
- /**
- * Layer method to add annotations to a line.
- *
- * @param {!HTMLElement} el The DIV.contentText element to apply the
- * annotation to.
- * @param {!HTMLElement} lineNumberEl
- * @param {!Object} line The line object. (GrDiffLine)
- */
- annotate(el, lineNumberEl, line) {
- let ranges = [];
- if (line.type === GrDiffLine.Type.REMOVE || (
- line.type === GrDiffLine.Type.BOTH &&
- el.getAttribute('data-side') !== 'right')) {
- ranges = ranges.concat(this._getRangesForLine(line, 'left'));
- }
- if (line.type === GrDiffLine.Type.ADD || (
- line.type === GrDiffLine.Type.BOTH &&
- el.getAttribute('data-side') !== 'left')) {
- ranges = ranges.concat(this._getRangesForLine(line, 'right'));
- }
+ /**
+ * Handle change in the ranges by updating the ranges maps and by
+ * emitting appropriate update notifications.
+ *
+ * @param {Object} record The change record.
+ */
+ _handleCommentRangesChange(record) {
+ if (!record) return;
- for (const range of ranges) {
- GrAnnotation.annotateElement(el, range.start,
- range.end - range.start,
- range.hovering ? HOVER_HIGHLIGHT : RANGE_HIGHLIGHT);
+ // If the entire set of comments was changed.
+ if (record.path === 'commentRanges') {
+ this._rangesMap = {left: {}, right: {}};
+ for (const {side, range, hovering} of record.value) {
+ this._updateRangesMap(
+ side, range, hovering, (forLine, start, end, hovering) => {
+ forLine.push({start, end, hovering});
+ });
}
}
- /**
- * Register a listener for layer updates.
- *
- * @param {function(number, number, string)} fn The update handler function.
- * Should accept as arguments the line numbers for the start and end of
- * the update and the side as a string.
- */
- addListener(fn) {
- this._listeners.push(fn);
+ // 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} = this.get(match[1]);
+
+ this._updateRangesMap(
+ side, range, hovering, (forLine, start, end, hovering) => {
+ const index = forLine.findIndex(lineRange =>
+ lineRange.start === start && lineRange.end === end);
+ forLine[index].hovering = hovering;
+ });
}
- /**
- * Notify Layer listeners of changes to annotations.
- *
- * @param {number} start The line where the update starts.
- * @param {number} end The line where the update ends.
- * @param {string} side The side of the update. ('left' or 'right')
- */
- _notifyUpdateRange(start, end, 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.
- *
- * @param {Object} record The change record.
- */
- _handleCommentRangesChange(record) {
- if (!record) return;
-
- // If the entire set of comments was changed.
- if (record.path === 'commentRanges') {
- this._rangesMap = {left: {}, right: {}};
- for (const {side, range, hovering} of record.value) {
+ // If comments were spliced in or out.
+ if (record.path === 'commentRanges.splices') {
+ for (const indexSplice of record.value.indexSplices) {
+ const removed = indexSplice.removed;
+ for (const {side, range, hovering} of removed) {
+ this._updateRangesMap(
+ side, range, hovering, (forLine, start, end) => {
+ const index = forLine.findIndex(lineRange =>
+ lineRange.start === start && lineRange.end === end);
+ forLine.splice(index, 1);
+ });
+ }
+ const added = indexSplice.object.slice(
+ indexSplice.index, indexSplice.index + indexSplice.addedCount);
+ for (const {side, range, hovering} of added) {
this._updateRangesMap(
side, range, hovering, (forLine, start, end, hovering) => {
forLine.push({start, end, hovering});
});
}
}
-
- // 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} = this.get(match[1]);
-
- this._updateRangesMap(
- side, range, hovering, (forLine, start, end, hovering) => {
- const index = forLine.findIndex(lineRange =>
- lineRange.start === start && lineRange.end === end);
- forLine[index].hovering = hovering;
- });
- }
-
- // If comments were spliced in or out.
- if (record.path === 'commentRanges.splices') {
- for (const indexSplice of record.value.indexSplices) {
- const removed = indexSplice.removed;
- for (const {side, range, hovering} of removed) {
- this._updateRangesMap(
- side, range, hovering, (forLine, start, end) => {
- const index = forLine.findIndex(lineRange =>
- lineRange.start === start && lineRange.end === end);
- forLine.splice(index, 1);
- });
- }
- const added = indexSplice.object.slice(
- indexSplice.index, indexSplice.index + indexSplice.addedCount);
- for (const {side, range, hovering} of added) {
- this._updateRangesMap(
- side, range, hovering, (forLine, start, end, hovering) => {
- forLine.push({start, end, hovering});
- });
- }
- }
- }
- }
-
- _updateRangesMap(side, range, hovering, operation) {
- 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);
- }
- this._notifyUpdateRange(range.start_line, range.end_line, side);
- }
-
- _getRangesForLine(line, side) {
- const lineNum = side === 'left' ? line.beforeNumber : line.afterNumber;
- const ranges = this.get(['_rangesMap', side, lineNum]) || [];
- return ranges
- .map(range => {
- // Make a copy, so that the normalization below does not mess with
- // our map.
- range = Object.assign({}, range);
- range.end = range.end === -1 ? line.text.length : range.end;
-
- // Normalize invalid ranges where the start is after the end but the
- // start still makes sense. Set the end to the end of the line.
- // @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;
- })
- // Sort the ranges so that hovering highlights are on top.
- .sort((a, b) => (a.hovering && !b.hovering ? 1 : 0));
}
}
- customElements.define(GrRangedCommentLayer.is, GrRangedCommentLayer);
-})();
+ _updateRangesMap(side, range, hovering, operation) {
+ 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);
+ }
+ this._notifyUpdateRange(range.start_line, range.end_line, side);
+ }
+
+ _getRangesForLine(line, side) {
+ const lineNum = side === 'left' ? line.beforeNumber : line.afterNumber;
+ const ranges = this.get(['_rangesMap', side, lineNum]) || [];
+ return ranges
+ .map(range => {
+ // Make a copy, so that the normalization below does not mess with
+ // our map.
+ range = Object.assign({}, range);
+ range.end = range.end === -1 ? line.text.length : range.end;
+
+ // Normalize invalid ranges where the start is after the end but the
+ // start still makes sense. Set the end to the end of the line.
+ // @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;
+ })
+ // Sort the ranges so that hovering highlights are on top.
+ .sort((a, b) => (a.hovering && !b.hovering ? 1 : 0));
+ }
+}
+
+customElements.define(GrRangedCommentLayer.is, GrRangedCommentLayer);
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.js
new file mode 100644
index 0000000..29757e5
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.js
@@ -0,0 +1,21 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
index 48883c1..5414ba2 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
@@ -19,17 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-ranged-comment-layer</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../gr-diff/gr-diff-line.js"></script>
-
-<link rel="import" href="gr-ranged-comment-layer.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -37,310 +30,312 @@
</template>
</test-fixture>
-<script>
- suite('gr-ranged-comment-layer', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../gr-diff/gr-diff-line.js';
+import './gr-ranged-comment-layer.js';
+suite('gr-ranged-comment-layer', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ const initialCommentRanges = [
+ {
+ side: 'left',
+ range: {
+ end_character: 9,
+ end_line: 39,
+ start_character: 6,
+ start_line: 36,
+ },
+ },
+ {
+ side: 'right',
+ range: {
+ end_character: 22,
+ end_line: 12,
+ start_character: 10,
+ start_line: 10,
+ },
+ },
+ {
+ side: 'right',
+ range: {
+ end_character: 15,
+ end_line: 100,
+ start_character: 5,
+ start_line: 100,
+ },
+ },
+ {
+ side: 'right',
+ range: {
+ end_character: 2,
+ end_line: 55,
+ start_character: 32,
+ start_line: 55,
+ },
+ },
+ ];
+
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.commentRanges = initialCommentRanges;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('annotate', () => {
let sandbox;
+ let el;
+ let line;
+ let annotateElementStub;
+ const lineNumberEl = document.createElement('td');
setup(() => {
- const initialCommentRanges = [
- {
- side: 'left',
- range: {
- end_character: 9,
- end_line: 39,
- start_character: 6,
- start_line: 36,
- },
- },
- {
- side: 'right',
- range: {
- end_character: 22,
- end_line: 12,
- start_character: 10,
- start_line: 10,
- },
- },
- {
- side: 'right',
- range: {
- end_character: 15,
- end_line: 100,
- start_character: 5,
- start_line: 100,
- },
- },
- {
- side: 'right',
- range: {
- end_character: 2,
- end_line: 55,
- start_character: 32,
- start_line: 55,
- },
- },
- ];
-
sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.commentRanges = initialCommentRanges;
+ annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
+ el = document.createElement('div');
+ el.setAttribute('data-side', 'left');
+ line = new GrDiffLine(GrDiffLine.Type.BOTH);
+ line.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit,';
});
teardown(() => {
sandbox.restore();
});
- suite('annotate', () => {
- let sandbox;
- let el;
- let line;
- let annotateElementStub;
- const lineNumberEl = document.createElement('td');
+ test('type=Remove no-comment', () => {
+ line.type = GrDiffLine.Type.REMOVE;
+ line.beforeNumber = 40;
- setup(() => {
- sandbox = sinon.sandbox.create();
- annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
- el = document.createElement('div');
- el.setAttribute('data-side', 'left');
- line = new GrDiffLine(GrDiffLine.Type.BOTH);
- line.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit,';
- });
+ element.annotate(el, lineNumberEl, line);
- teardown(() => {
- sandbox.restore();
- });
-
- test('type=Remove no-comment', () => {
- line.type = GrDiffLine.Type.REMOVE;
- line.beforeNumber = 40;
-
- element.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementStub.called);
- });
-
- test('type=Remove has-comment', () => {
- line.type = GrDiffLine.Type.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');
- });
-
- test('type=Remove has-comment hovering', () => {
- line.type = GrDiffLine.Type.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 rangeHighlight');
- });
-
- test('type=Both has-comment', () => {
- line.type = GrDiffLine.Type.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');
- });
-
- test('type=Both has-comment off side', () => {
- line.type = GrDiffLine.Type.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 = GrDiffLine.Type.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');
- });
+ assert.isFalse(annotateElementStub.called);
});
- test('_handleCommentRangesChange overwrite', () => {
- element.set('commentRanges', []);
+ test('type=Remove has-comment', () => {
+ line.type = GrDiffLine.Type.REMOVE;
+ line.beforeNumber = 36;
+ const expectedStart = 6;
+ const expectedLength = line.text.length - expectedStart;
- assert.equal(Object.keys(element._rangesMap.left).length, 0);
- assert.equal(Object.keys(element._rangesMap.right).length, 0);
+ 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');
});
- test('_handleCommentRangesChange hovering', () => {
- const notifyStub = sinon.stub();
- element.addListener(notifyStub);
- const updateRangesMapSpy = sandbox.spy(element, '_updateRangesMap');
+ test('type=Remove has-comment hovering', () => {
+ line.type = GrDiffLine.Type.REMOVE;
+ line.beforeNumber = 36;
+ element.set(['commentRanges', 0, 'hovering'], true);
- element.set(['commentRanges', 1, 'hovering'], true);
+ const expectedStart = 6;
+ const expectedLength = line.text.length - expectedStart;
- 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');
+ element.annotate(el, lineNumberEl, line);
- assert.isTrue(updateRangesMapSpy.called);
+ 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 rangeHighlight');
});
- test('_handleCommentRangesChange splice out', () => {
- const notifyStub = sinon.stub();
- element.addListener(notifyStub);
+ test('type=Both has-comment', () => {
+ line.type = GrDiffLine.Type.BOTH;
+ line.beforeNumber = 36;
- element.splice('commentRanges', 1, 1);
+ const expectedStart = 6;
+ const expectedLength = line.text.length - expectedStart;
- 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');
+ 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');
});
- test('_handleCommentRangesChange splice in', () => {
- const notifyStub = sinon.stub();
- element.addListener(notifyStub);
+ test('type=Both has-comment off side', () => {
+ line.type = GrDiffLine.Type.BOTH;
+ line.beforeNumber = 36;
+ el.setAttribute('data-side', 'right');
- element.splice('commentRanges', 1, 0, {
- side: 'left',
- range: {
- end_character: 15,
- end_line: 275,
- start_character: 5,
- start_line: 250,
- },
- });
+ element.annotate(el, lineNumberEl, line);
- 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');
+ assert.isFalse(annotateElementStub.called);
});
- test('_handleCommentRangesChange mixed actions', () => {
- const notifyStub = sinon.stub();
- element.addListener(notifyStub);
- const updateRangesMapSpy = sandbox.spy(element, '_updateRangesMap');
+ test('type=Add has-comment', () => {
+ line.type = GrDiffLine.Type.ADD;
+ line.afterNumber = 12;
+ el.setAttribute('data-side', 'right');
- 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);
- });
+ const expectedStart = 0;
+ const expectedLength = 22;
- 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());
+ element.annotate(el, lineNumberEl, line);
- 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 two ranged comments, one spanning ll.10-12 and the other
- // on line 100.
- const rightKeys = [];
- for (let i = 10; i <= 12; 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);
+ 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');
});
});
+
+ 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 = sandbox.spy(element, '_updateRangesMap');
+
+ element.set(['commentRanges', 1, 'hovering'], true);
+
+ 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');
+
+ 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 = sandbox.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 two ranged comments, one spanning ll.10-12 and the other
+ // on line 100.
+ const rightKeys = [];
+ for (let i = 10; i <= 12; 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);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.html
deleted file mode 100644
index cefd241..0000000
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.html
+++ /dev/null
@@ -1,30 +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.
--->
-<dom-module id="gr-ranged-comment-theme">
- <template>
- <style>
- .range {
- background-color: var(--diff-highlight-range-color);
- display: inline;
- }
- .rangeHighlight {
- background-color: var(--diff-highlight-range-hover-color);
- display: inline;
- }
- </style>
- </template>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.js
new file mode 100644
index 0000000..49ed980
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.js
@@ -0,0 +1,41 @@
+/**
+ * @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.
+ */
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-ranged-comment-theme">
+ <template>
+ <style>
+ .range {
+ background-color: var(--diff-highlight-range-color);
+ display: inline;
+ }
+ .rangeHighlight {
+ background-color: var(--diff-highlight-range-hover-color);
+ display: inline;
+ }
+ </style>
+ </template>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
deleted file mode 100644
index aa4d2e1..0000000
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
+++ /dev/null
@@ -1,40 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-tooltip/gr-tooltip.html">
-
-<dom-module id="gr-selection-action-box">
- <template>
- <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>
- </template>
- <script src="gr-selection-action-box.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
index 3d831c9..20d3081 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
@@ -14,93 +14,104 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-tooltip/gr-tooltip.js';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-selection-action-box_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrSelectionActionBox extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-selection-action-box'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the comment creation action was taken (click).
+ *
+ * @event create-comment-requested
*/
- class GrSelectionActionBox extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-selection-action-box'; }
- /**
- * Fired when the comment creation action was taken (click).
- *
- * @event create-comment-requested
- */
- static get properties() {
- return {
- keyEventTarget: {
- type: Object,
- value() { return document.body; },
- },
- positionBelow: Boolean,
- };
- }
-
- /** @override */
- created() {
- super.created();
-
- // See https://crbug.com/gerrit/4767
- this.addEventListener('mousedown',
- e => this._handleMouseDown(e));
- }
-
- placeAbove(el) {
- Polymer.dom.flush();
- const rect = this._getTargetBoundingRect(el);
- const boxRect = this.$.tooltip.getBoundingClientRect();
- const parentRect = this._getParentBoundingClientRect();
- this.style.top =
- rect.top - parentRect.top - boxRect.height - 6 + 'px';
- this.style.left =
- rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px';
- }
-
- placeBelow(el) {
- Polymer.dom.flush();
- const rect = this._getTargetBoundingRect(el);
- const boxRect = this.$.tooltip.getBoundingClientRect();
- const parentRect = this._getParentBoundingClientRect();
- this.style.top =
- rect.top - parentRect.top + boxRect.height - 6 + 'px';
- this.style.left =
- rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px';
- }
-
- _getParentBoundingClientRect() {
- // With native shadow DOM, the parent is the shadow root, not the gr-diff
- // element
- const parent = this.parentElement || this.parentNode.host;
- return parent.getBoundingClientRect();
- }
-
- _getTargetBoundingRect(el) {
- let rect;
- if (el instanceof Text) {
- const range = document.createRange();
- range.selectNode(el);
- rect = range.getBoundingClientRect();
- range.detach();
- } else {
- rect = el.getBoundingClientRect();
- }
- return rect;
- }
-
- _handleMouseDown(e) {
- if (e.button !== 0) { return; } // 0 = main button
- e.preventDefault();
- e.stopPropagation();
- this.fire('create-comment-requested');
- }
+ static get properties() {
+ return {
+ keyEventTarget: {
+ type: Object,
+ value() { return document.body; },
+ },
+ positionBelow: Boolean,
+ };
}
- customElements.define(GrSelectionActionBox.is, GrSelectionActionBox);
-})();
+ /** @override */
+ created() {
+ super.created();
+
+ // See https://crbug.com/gerrit/4767
+ this.addEventListener('mousedown',
+ e => this._handleMouseDown(e));
+ }
+
+ placeAbove(el) {
+ flush();
+ const rect = this._getTargetBoundingRect(el);
+ const boxRect = this.$.tooltip.getBoundingClientRect();
+ const parentRect = this._getParentBoundingClientRect();
+ this.style.top =
+ rect.top - parentRect.top - boxRect.height - 6 + 'px';
+ this.style.left =
+ rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px';
+ }
+
+ placeBelow(el) {
+ flush();
+ const rect = this._getTargetBoundingRect(el);
+ const boxRect = this.$.tooltip.getBoundingClientRect();
+ const parentRect = this._getParentBoundingClientRect();
+ this.style.top =
+ rect.top - parentRect.top + boxRect.height - 6 + 'px';
+ this.style.left =
+ rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px';
+ }
+
+ _getParentBoundingClientRect() {
+ // With native shadow DOM, the parent is the shadow root, not the gr-diff
+ // element
+ const parent = this.parentElement || this.parentNode.host;
+ return parent.getBoundingClientRect();
+ }
+
+ _getTargetBoundingRect(el) {
+ let rect;
+ if (el instanceof Text) {
+ const range = document.createRange();
+ range.selectNode(el);
+ rect = range.getBoundingClientRect();
+ range.detach();
+ } else {
+ rect = el.getBoundingClientRect();
+ }
+ return rect;
+ }
+
+ _handleMouseDown(e) {
+ if (e.button !== 0) { return; } // 0 = main button
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('create-comment-requested');
+ }
+}
+
+customElements.define(GrSelectionActionBox.is, GrSelectionActionBox);
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_html.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_html.js
new file mode 100644
index 0000000..670a755
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_html.js
@@ -0,0 +1,29 @@
+/**
+ * @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.js';
+
+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/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
index bb802f8..bc8484d 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-selection-action-box</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-selection-action-box.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -38,98 +33,99 @@
</template>
</test-fixture>
-<script>
- suite('gr-selection-action-box', async () => {
- await readyToTest();
- let container;
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-selection-action-box.js';
+suite('gr-selection-action-box', () => {
+ let container;
+ let element;
+ let sandbox;
+
+ setup(() => {
+ container = fixture('basic');
+ element = container.querySelector('gr-selection-action-box');
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(element, 'fire');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('ignores regular keys', () => {
+ MockInteractions.pressAndReleaseKeyOn(document.body, 27, null, 'esc');
+ assert.isFalse(element.fire.called);
+ });
+
+ suite('mousedown reacts only to main button', () => {
+ let e;
setup(() => {
- container = fixture('basic');
- element = container.querySelector('gr-selection-action-box');
- sandbox = sinon.sandbox.create();
- sandbox.stub(element, 'fire');
+ e = {
+ button: 0,
+ preventDefault: sandbox.stub(),
+ stopPropagation: sandbox.stub(),
+ };
});
- teardown(() => {
- sandbox.restore();
+ test('event handled if main button', () => {
+ element._handleMouseDown(e);
+ assert.isTrue(e.preventDefault.called);
+ assert(element.fire.calledWithExactly('create-comment-requested'));
});
- test('ignores regular keys', () => {
- MockInteractions.pressAndReleaseKeyOn(document.body, 27, null, 'esc');
+ test('event ignored if not main button', () => {
+ e.button = 1;
+ element._handleMouseDown(e);
+ assert.isFalse(e.preventDefault.called);
assert.isFalse(element.fire.called);
});
+ });
- suite('mousedown reacts only to main button', () => {
- let e;
+ suite('placeAbove', () => {
+ let target;
- setup(() => {
- e = {
- button: 0,
- preventDefault: sandbox.stub(),
- stopPropagation: sandbox.stub(),
- };
- });
-
- test('event handled if main button', () => {
- element._handleMouseDown(e);
- assert.isTrue(e.preventDefault.called);
- assert(element.fire.calledWithExactly('create-comment-requested'));
- });
-
- test('event ignored if not main button', () => {
- e.button = 1;
- element._handleMouseDown(e);
- assert.isFalse(e.preventDefault.called);
- assert.isFalse(element.fire.called);
- });
+ setup(() => {
+ target = container.querySelector('.target');
+ sandbox.stub(container, 'getBoundingClientRect').returns(
+ {top: 1, bottom: 2, left: 3, right: 4, width: 50, height: 6});
+ sandbox.stub(element, '_getTargetBoundingRect').returns(
+ {top: 42, bottom: 20, left: 30, right: 40, width: 100, height: 60});
+ sandbox.stub(element.$.tooltip, 'getBoundingClientRect').returns(
+ {width: 10, height: 10});
});
- suite('placeAbove', () => {
- let target;
+ test('placeAbove for Element argument', () => {
+ element.placeAbove(target);
+ assert.equal(element.style.top, '25px');
+ assert.equal(element.style.left, '72px');
+ });
- setup(() => {
- target = container.querySelector('.target');
- sandbox.stub(container, 'getBoundingClientRect').returns(
- {top: 1, bottom: 2, left: 3, right: 4, width: 50, height: 6});
- sandbox.stub(element, '_getTargetBoundingRect').returns(
- {top: 42, bottom: 20, left: 30, right: 40, width: 100, height: 60});
- sandbox.stub(element.$.tooltip, 'getBoundingClientRect').returns(
- {width: 10, height: 10});
- });
+ test('placeAbove for Text Node argument', () => {
+ element.placeAbove(target.firstChild);
+ assert.equal(element.style.top, '25px');
+ assert.equal(element.style.left, '72px');
+ });
- test('placeAbove for Element argument', () => {
- element.placeAbove(target);
- assert.equal(element.style.top, '25px');
- assert.equal(element.style.left, '72px');
- });
+ test('placeBelow for Element argument', () => {
+ element.placeBelow(target);
+ assert.equal(element.style.top, '45px');
+ assert.equal(element.style.left, '72px');
+ });
- test('placeAbove for Text Node argument', () => {
- element.placeAbove(target.firstChild);
- assert.equal(element.style.top, '25px');
- assert.equal(element.style.left, '72px');
- });
+ test('placeBelow for Text Node argument', () => {
+ element.placeBelow(target.firstChild);
+ assert.equal(element.style.top, '45px');
+ assert.equal(element.style.left, '72px');
+ });
- test('placeBelow for Element argument', () => {
- element.placeBelow(target);
- assert.equal(element.style.top, '45px');
- assert.equal(element.style.left, '72px');
- });
-
- test('placeBelow for Text Node argument', () => {
- element.placeBelow(target.firstChild);
- assert.equal(element.style.top, '45px');
- assert.equal(element.style.left, '72px');
- });
-
- test('uses document.createRange', () => {
- sandbox.spy(document, 'createRange');
- element._getTargetBoundingRect.restore();
- sandbox.spy(element, '_getTargetBoundingRect');
- element.placeAbove(target.firstChild);
- assert.isTrue(document.createRange.called);
- });
+ test('uses document.createRange', () => {
+ sandbox.spy(document, 'createRange');
+ element._getTargetBoundingRect.restore();
+ sandbox.spy(element, '_getTargetBoundingRect');
+ element.placeAbove(target.firstChild);
+ assert.isTrue(document.createRange.called);
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
deleted file mode 100644
index dd6bfec..0000000
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
+++ /dev/null
@@ -1,28 +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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-lib-loader/gr-lib-loader.html">
-
-<dom-module id="gr-syntax-layer">
- <template>
- <gr-lib-loader id="libLoader"></gr-lib-loader>
- </template>
- <script src="../../../scripts/util.js"></script>
- <script src="../gr-diff/gr-diff-line.js"></script>
- <script src="../gr-diff-highlight/gr-annotation.js"></script>
- <script src="gr-syntax-layer.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index b6e2884..33e894b 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -14,534 +14,543 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const LANGUAGE_MAP = {
- 'application/dart': 'dart',
- 'application/json': 'json',
- 'application/x-powershell': 'powershell',
- 'application/typescript': 'typescript',
- 'application/xml': 'xml',
- 'application/xquery': 'xquery',
- 'application/x-erb': 'erb',
- 'text/css': 'css',
- 'text/html': 'html',
- 'text/javascript': 'js',
- 'text/jsx': 'jsx',
- 'text/x-c': 'cpp',
- 'text/x-c++src': 'cpp',
- 'text/x-clojure': 'clojure',
- 'text/x-cmake': 'cmake',
- 'text/x-coffeescript': 'coffeescript',
- 'text/x-common-lisp': 'lisp',
- 'text/x-crystal': 'crystal',
- 'text/x-csharp': 'csharp',
- 'text/x-csrc': 'cpp',
- 'text/x-d': 'd',
- 'text/x-diff': 'diff',
- 'text/x-django': 'django',
- 'text/x-dockerfile': 'dockerfile',
- 'text/x-ebnf': 'ebnf',
- 'text/x-elm': 'elm',
- 'text/x-erlang': 'erlang',
- 'text/x-fortran': 'fortran',
- 'text/x-fsharp': 'fsharp',
- 'text/x-go': 'go',
- 'text/x-groovy': 'groovy',
- 'text/x-haml': 'haml',
- 'text/x-handlebars': 'handlebars',
- 'text/x-haskell': 'haskell',
- 'text/x-haxe': 'haxe',
- 'text/x-ini': 'ini',
- 'text/x-java': 'java',
- 'text/x-julia': 'julia',
- 'text/x-kotlin': 'kotlin',
- 'text/x-latex': 'latex',
- 'text/x-less': 'less',
- 'text/x-lua': 'lua',
- 'text/x-mathematica': 'mathematica',
- 'text/x-nginx-conf': 'nginx',
- 'text/x-nsis': 'nsis',
- 'text/x-objectivec': 'objectivec',
- 'text/x-ocaml': 'ocaml',
- 'text/x-perl': 'perl',
- 'text/x-pgsql': 'pgsql', // postgresql
- 'text/x-php': 'php',
- 'text/x-properties': 'properties',
- 'text/x-protobuf': 'protobuf',
- 'text/x-puppet': 'puppet',
- 'text/x-python': 'python',
- 'text/x-q': 'q',
- 'text/x-ruby': 'ruby',
- 'text/x-rustsrc': 'rust',
- 'text/x-scala': 'scala',
- 'text/x-scss': 'scss',
- 'text/x-scheme': 'scheme',
- 'text/x-shell': 'shell',
- 'text/x-soy': 'soy',
- 'text/x-spreadsheet': 'excel',
- 'text/x-sh': 'bash',
- 'text/x-sql': 'sql',
- 'text/x-swift': 'swift',
- 'text/x-systemverilog': 'sv',
- 'text/x-tcl': 'tcl',
- 'text/x-torque': 'torque',
- 'text/x-twig': 'twig',
- 'text/x-vb': 'vb',
- 'text/x-verilog': 'v',
- 'text/x-vhdl': 'vhdl',
- 'text/x-yaml': 'yaml',
- 'text/vbscript': 'vbscript',
- };
- const ASYNC_DELAY = 10;
+import '../../shared/gr-lib-loader/gr-lib-loader.js';
+import '../../../scripts/util.js';
+import '../gr-diff/gr-diff-line.js';
+import '../gr-diff-highlight/gr-annotation.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-syntax-layer_html.js';
- const CLASS_WHITELIST = {
- 'gr-diff gr-syntax gr-syntax-attr': true,
- 'gr-diff gr-syntax gr-syntax-attribute': true,
- 'gr-diff gr-syntax gr-syntax-built_in': true,
- 'gr-diff gr-syntax gr-syntax-comment': true,
- 'gr-diff gr-syntax gr-syntax-doctag': true,
- 'gr-diff gr-syntax gr-syntax-function': true,
- 'gr-diff gr-syntax gr-syntax-keyword': true,
- 'gr-diff gr-syntax gr-syntax-link': true,
- 'gr-diff gr-syntax gr-syntax-literal': true,
- 'gr-diff gr-syntax gr-syntax-meta': true,
- 'gr-diff gr-syntax gr-syntax-meta-keyword': true,
- 'gr-diff gr-syntax gr-syntax-name': true,
- 'gr-diff gr-syntax gr-syntax-number': true,
- 'gr-diff gr-syntax gr-syntax-params': true,
- 'gr-diff gr-syntax gr-syntax-regexp': true,
- 'gr-diff gr-syntax gr-syntax-selector-attr': true,
- 'gr-diff gr-syntax gr-syntax-selector-class': true,
- 'gr-diff gr-syntax gr-syntax-selector-id': true,
- 'gr-diff gr-syntax gr-syntax-selector-pseudo': true,
- 'gr-diff gr-syntax gr-syntax-selector-tag': true,
- 'gr-diff gr-syntax gr-syntax-string': true,
- 'gr-diff gr-syntax gr-syntax-tag': true,
- 'gr-diff gr-syntax gr-syntax-template-tag': true,
- 'gr-diff gr-syntax gr-syntax-template-variable': true,
- 'gr-diff gr-syntax gr-syntax-title': true,
- 'gr-diff gr-syntax gr-syntax-type': true,
- 'gr-diff gr-syntax gr-syntax-variable': true,
- };
+const LANGUAGE_MAP = {
+ 'application/dart': 'dart',
+ 'application/json': 'json',
+ 'application/x-powershell': 'powershell',
+ 'application/typescript': 'typescript',
+ 'application/xml': 'xml',
+ 'application/xquery': 'xquery',
+ 'application/x-erb': 'erb',
+ 'text/css': 'css',
+ 'text/html': 'html',
+ 'text/javascript': 'js',
+ 'text/jsx': 'jsx',
+ 'text/x-c': 'cpp',
+ 'text/x-c++src': 'cpp',
+ 'text/x-clojure': 'clojure',
+ 'text/x-cmake': 'cmake',
+ 'text/x-coffeescript': 'coffeescript',
+ 'text/x-common-lisp': 'lisp',
+ 'text/x-crystal': 'crystal',
+ 'text/x-csharp': 'csharp',
+ 'text/x-csrc': 'cpp',
+ 'text/x-d': 'd',
+ 'text/x-diff': 'diff',
+ 'text/x-django': 'django',
+ 'text/x-dockerfile': 'dockerfile',
+ 'text/x-ebnf': 'ebnf',
+ 'text/x-elm': 'elm',
+ 'text/x-erlang': 'erlang',
+ 'text/x-fortran': 'fortran',
+ 'text/x-fsharp': 'fsharp',
+ 'text/x-go': 'go',
+ 'text/x-groovy': 'groovy',
+ 'text/x-haml': 'haml',
+ 'text/x-handlebars': 'handlebars',
+ 'text/x-haskell': 'haskell',
+ 'text/x-haxe': 'haxe',
+ 'text/x-ini': 'ini',
+ 'text/x-java': 'java',
+ 'text/x-julia': 'julia',
+ 'text/x-kotlin': 'kotlin',
+ 'text/x-latex': 'latex',
+ 'text/x-less': 'less',
+ 'text/x-lua': 'lua',
+ 'text/x-mathematica': 'mathematica',
+ 'text/x-nginx-conf': 'nginx',
+ 'text/x-nsis': 'nsis',
+ 'text/x-objectivec': 'objectivec',
+ 'text/x-ocaml': 'ocaml',
+ 'text/x-perl': 'perl',
+ 'text/x-pgsql': 'pgsql', // postgresql
+ 'text/x-php': 'php',
+ 'text/x-properties': 'properties',
+ 'text/x-protobuf': 'protobuf',
+ 'text/x-puppet': 'puppet',
+ 'text/x-python': 'python',
+ 'text/x-q': 'q',
+ 'text/x-ruby': 'ruby',
+ 'text/x-rustsrc': 'rust',
+ 'text/x-scala': 'scala',
+ 'text/x-scss': 'scss',
+ 'text/x-scheme': 'scheme',
+ 'text/x-shell': 'shell',
+ 'text/x-soy': 'soy',
+ 'text/x-spreadsheet': 'excel',
+ 'text/x-sh': 'bash',
+ 'text/x-sql': 'sql',
+ 'text/x-swift': 'swift',
+ 'text/x-systemverilog': 'sv',
+ 'text/x-tcl': 'tcl',
+ 'text/x-torque': 'torque',
+ 'text/x-twig': 'twig',
+ 'text/x-vb': 'vb',
+ 'text/x-verilog': 'v',
+ 'text/x-vhdl': 'vhdl',
+ 'text/x-yaml': 'yaml',
+ 'text/vbscript': 'vbscript',
+};
+const ASYNC_DELAY = 10;
- const CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</;
- const CPP_WCHAR_PATTERN = /L\'(\\)?.\'/g;
- const JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g;
- const GO_BACKSLASH_LITERAL = '\'\\\\\'';
- const GLOBAL_LT_PATTERN = /</g;
+const CLASS_WHITELIST = {
+ 'gr-diff gr-syntax gr-syntax-attr': true,
+ 'gr-diff gr-syntax gr-syntax-attribute': true,
+ 'gr-diff gr-syntax gr-syntax-built_in': true,
+ 'gr-diff gr-syntax gr-syntax-comment': true,
+ 'gr-diff gr-syntax gr-syntax-doctag': true,
+ 'gr-diff gr-syntax gr-syntax-function': true,
+ 'gr-diff gr-syntax gr-syntax-keyword': true,
+ 'gr-diff gr-syntax gr-syntax-link': true,
+ 'gr-diff gr-syntax gr-syntax-literal': true,
+ 'gr-diff gr-syntax gr-syntax-meta': true,
+ 'gr-diff gr-syntax gr-syntax-meta-keyword': true,
+ 'gr-diff gr-syntax gr-syntax-name': true,
+ 'gr-diff gr-syntax gr-syntax-number': true,
+ 'gr-diff gr-syntax gr-syntax-params': true,
+ 'gr-diff gr-syntax gr-syntax-regexp': true,
+ 'gr-diff gr-syntax gr-syntax-selector-attr': true,
+ 'gr-diff gr-syntax gr-syntax-selector-class': true,
+ 'gr-diff gr-syntax gr-syntax-selector-id': true,
+ 'gr-diff gr-syntax gr-syntax-selector-pseudo': true,
+ 'gr-diff gr-syntax gr-syntax-selector-tag': true,
+ 'gr-diff gr-syntax gr-syntax-string': true,
+ 'gr-diff gr-syntax gr-syntax-tag': true,
+ 'gr-diff gr-syntax gr-syntax-template-tag': true,
+ 'gr-diff gr-syntax gr-syntax-template-variable': true,
+ 'gr-diff gr-syntax gr-syntax-title': true,
+ 'gr-diff gr-syntax gr-syntax-type': true,
+ 'gr-diff gr-syntax gr-syntax-variable': true,
+};
- /** @extends Polymer.Element */
- class GrSyntaxLayer extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-syntax-layer'; }
+const CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</;
+const CPP_WCHAR_PATTERN = /L\'(\\)?.\'/g;
+const JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g;
+const GO_BACKSLASH_LITERAL = '\'\\\\\'';
+const GLOBAL_LT_PATTERN = /</g;
- static get properties() {
- return {
- diff: {
- type: Object,
- observer: '_diffChanged',
- },
- enabled: {
- type: Boolean,
- value: true,
- },
- _baseRanges: {
- type: Array,
- value() { return []; },
- },
- _revisionRanges: {
- type: Array,
- value() { return []; },
- },
- _baseLanguage: String,
- _revisionLanguage: String,
- _listeners: {
- type: Array,
- value() { return []; },
- },
- /** @type {?number} */
- _processHandle: Number,
- /**
- * The promise last returned from `process()` while the asynchronous
- * processing is running - `null` otherwise. Provides a `cancel()`
- * method that rejects it with `{isCancelled: true}`.
- *
- * @type {?Object}
- */
- _processPromise: {
- type: Object,
- value: null,
- },
- _hljs: Object,
- };
+/** @extends Polymer.Element */
+class GrSyntaxLayer extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-syntax-layer'; }
+
+ static get properties() {
+ return {
+ diff: {
+ type: Object,
+ observer: '_diffChanged',
+ },
+ enabled: {
+ type: Boolean,
+ value: true,
+ },
+ _baseRanges: {
+ type: Array,
+ value() { return []; },
+ },
+ _revisionRanges: {
+ type: Array,
+ value() { return []; },
+ },
+ _baseLanguage: String,
+ _revisionLanguage: String,
+ _listeners: {
+ type: Array,
+ value() { return []; },
+ },
+ /** @type {?number} */
+ _processHandle: Number,
+ /**
+ * The promise last returned from `process()` while the asynchronous
+ * processing is running - `null` otherwise. Provides a `cancel()`
+ * method that rejects it with `{isCancelled: true}`.
+ *
+ * @type {?Object}
+ */
+ _processPromise: {
+ type: Object,
+ value: null,
+ },
+ _hljs: Object,
+ };
+ }
+
+ addListener(fn) {
+ this.push('_listeners', fn);
+ }
+
+ removeListener(fn) {
+ this._listeners = this._listeners.filter(f => f != fn);
+ }
+
+ /**
+ * Annotation layer method to add syntax annotations to the given element
+ * for the given line.
+ *
+ * @param {!HTMLElement} el
+ * @param {!HTMLElement} lineNumberEl
+ * @param {!Object} line (GrDiffLine)
+ */
+ annotate(el, lineNumberEl, line) {
+ if (!this.enabled) { return; }
+
+ // Determine the side.
+ let side;
+ if (line.type === GrDiffLine.Type.REMOVE || (
+ line.type === GrDiffLine.Type.BOTH &&
+ el.getAttribute('data-side') !== 'right')) {
+ side = 'left';
+ } else if (line.type === GrDiffLine.Type.ADD || (
+ el.getAttribute('data-side') !== 'left')) {
+ side = 'right';
}
- addListener(fn) {
- this.push('_listeners', fn);
+ // Find the relevant syntax ranges, if any.
+ let ranges = [];
+ if (side === 'left' && this._baseRanges.length >= line.beforeNumber) {
+ ranges = this._baseRanges[line.beforeNumber - 1] || [];
+ } else if (side === 'right' &&
+ this._revisionRanges.length >= line.afterNumber) {
+ ranges = this._revisionRanges[line.afterNumber - 1] || [];
}
- removeListener(fn) {
- this._listeners = this._listeners.filter(f => f != fn);
+ // Apply the ranges to the element.
+ for (const range of ranges) {
+ GrAnnotation.annotateElement(
+ el, range.start, range.length, range.className);
+ }
+ }
+
+ _getLanguage(diffFileMetaInfo) {
+ // The Gerrit API provides only content-type, but for other users of
+ // gr-diff it may be more convenient to specify the language directly.
+ return diffFileMetaInfo.language ||
+ LANGUAGE_MAP[diffFileMetaInfo.content_type];
+ }
+
+ /**
+ * Start processing syntax for the loaded diff and notify layer listeners
+ * as syntax info comes online.
+ *
+ * @return {Promise}
+ */
+ process() {
+ // Cancel any still running process() calls, because they append to the
+ // same _baseRanges and _revisionRanges fields.
+ this._cancel();
+
+ // Discard existing ranges.
+ this._baseRanges = [];
+ this._revisionRanges = [];
+
+ if (!this.enabled || !this.diff.content.length) {
+ return Promise.resolve();
}
- /**
- * Annotation layer method to add syntax annotations to the given element
- * for the given line.
- *
- * @param {!HTMLElement} el
- * @param {!HTMLElement} lineNumberEl
- * @param {!Object} line (GrDiffLine)
- */
- annotate(el, lineNumberEl, line) {
- if (!this.enabled) { return; }
-
- // Determine the side.
- let side;
- if (line.type === GrDiffLine.Type.REMOVE || (
- line.type === GrDiffLine.Type.BOTH &&
- el.getAttribute('data-side') !== 'right')) {
- side = 'left';
- } else if (line.type === GrDiffLine.Type.ADD || (
- el.getAttribute('data-side') !== 'left')) {
- side = 'right';
- }
-
- // Find the relevant syntax ranges, if any.
- let ranges = [];
- if (side === 'left' && this._baseRanges.length >= line.beforeNumber) {
- ranges = this._baseRanges[line.beforeNumber - 1] || [];
- } else if (side === 'right' &&
- this._revisionRanges.length >= line.afterNumber) {
- ranges = this._revisionRanges[line.afterNumber - 1] || [];
- }
-
- // Apply the ranges to the element.
- for (const range of ranges) {
- GrAnnotation.annotateElement(
- el, range.start, range.length, range.className);
- }
+ if (this.diff.meta_a) {
+ this._baseLanguage = this._getLanguage(this.diff.meta_a);
+ }
+ if (this.diff.meta_b) {
+ this._revisionLanguage = this._getLanguage(this.diff.meta_b);
+ }
+ if (!this._baseLanguage && !this._revisionLanguage) {
+ return Promise.resolve();
}
- _getLanguage(diffFileMetaInfo) {
- // The Gerrit API provides only content-type, but for other users of
- // gr-diff it may be more convenient to specify the language directly.
- return diffFileMetaInfo.language ||
- LANGUAGE_MAP[diffFileMetaInfo.content_type];
+ const state = {
+ sectionIndex: 0,
+ lineIndex: 0,
+ baseContext: undefined,
+ revisionContext: undefined,
+ lineNums: {left: 1, right: 1},
+ lastNotify: {left: 1, right: 1},
+ };
+
+ const rangesCache = new Map();
+
+ this._processPromise = util.makeCancelable(this._loadHLJS()
+ .then(() => new Promise(resolve => {
+ const nextStep = () => {
+ this._processHandle = null;
+ this._processNextLine(state, rangesCache);
+
+ // Move to the next line in the section.
+ state.lineIndex++;
+
+ // If the section has been exhausted, move to the next one.
+ if (this._isSectionDone(state)) {
+ state.lineIndex = 0;
+ state.sectionIndex++;
+ }
+
+ // If all sections have been exhausted, finish.
+ if (state.sectionIndex >= this.diff.content.length) {
+ resolve();
+ this._notify(state);
+ return;
+ }
+
+ if (state.lineIndex % 100 === 0) {
+ this._notify(state);
+ this._processHandle = this.async(nextStep, ASYNC_DELAY);
+ } else {
+ nextStep.call(this);
+ }
+ };
+
+ this._processHandle = this.async(nextStep, 1);
+ })));
+ return this._processPromise
+ .finally(() => { this._processPromise = null; });
+ }
+
+ /**
+ * Cancel any asynchronous syntax processing jobs.
+ */
+ _cancel() {
+ if (this._processHandle != null) {
+ this.cancelAsync(this._processHandle);
+ this._processHandle = null;
}
-
- /**
- * Start processing syntax for the loaded diff and notify layer listeners
- * as syntax info comes online.
- *
- * @return {Promise}
- */
- process() {
- // Cancel any still running process() calls, because they append to the
- // same _baseRanges and _revisionRanges fields.
- this._cancel();
-
- // Discard existing ranges.
- this._baseRanges = [];
- this._revisionRanges = [];
-
- if (!this.enabled || !this.diff.content.length) {
- return Promise.resolve();
- }
-
- if (this.diff.meta_a) {
- this._baseLanguage = this._getLanguage(this.diff.meta_a);
- }
- if (this.diff.meta_b) {
- this._revisionLanguage = this._getLanguage(this.diff.meta_b);
- }
- if (!this._baseLanguage && !this._revisionLanguage) {
- return Promise.resolve();
- }
-
- const state = {
- sectionIndex: 0,
- lineIndex: 0,
- baseContext: undefined,
- revisionContext: undefined,
- lineNums: {left: 1, right: 1},
- lastNotify: {left: 1, right: 1},
- };
-
- const rangesCache = new Map();
-
- this._processPromise = util.makeCancelable(this._loadHLJS()
- .then(() => new Promise(resolve => {
- const nextStep = () => {
- this._processHandle = null;
- this._processNextLine(state, rangesCache);
-
- // Move to the next line in the section.
- state.lineIndex++;
-
- // If the section has been exhausted, move to the next one.
- if (this._isSectionDone(state)) {
- state.lineIndex = 0;
- state.sectionIndex++;
- }
-
- // If all sections have been exhausted, finish.
- if (state.sectionIndex >= this.diff.content.length) {
- resolve();
- this._notify(state);
- return;
- }
-
- if (state.lineIndex % 100 === 0) {
- this._notify(state);
- this._processHandle = this.async(nextStep, ASYNC_DELAY);
- } else {
- nextStep.call(this);
- }
- };
-
- this._processHandle = this.async(nextStep, 1);
- })));
- return this._processPromise
- .finally(() => { this._processPromise = null; });
+ if (this._processPromise) {
+ this._processPromise.cancel();
}
+ }
- /**
- * Cancel any asynchronous syntax processing jobs.
- */
- _cancel() {
- if (this._processHandle != null) {
- this.cancelAsync(this._processHandle);
- this._processHandle = null;
- }
- if (this._processPromise) {
- this._processPromise.cancel();
- }
- }
+ _diffChanged() {
+ this._cancel();
+ this._baseRanges = [];
+ this._revisionRanges = [];
+ }
- _diffChanged() {
- this._cancel();
- this._baseRanges = [];
- this._revisionRanges = [];
- }
+ /**
+ * Take a string of HTML with the (potentially nested) syntax markers
+ * Highlight.js emits and emit a list of text ranges and classes for the
+ * markers.
+ *
+ * @param {string} str The string of HTML.
+ * @param {Map<string, !Array<!Object>>} rangesCache A map for caching
+ * ranges for each string. A cache is read and written by this method.
+ * Since diff is mostly comparing same file on two sides, there is good rate
+ * of duplication at least for parts that are on left and right parts.
+ * @return {!Array<!Object>} The list of ranges.
+ */
+ _rangesFromString(str, rangesCache) {
+ const cached = rangesCache.get(str);
+ if (cached) return cached;
- /**
- * Take a string of HTML with the (potentially nested) syntax markers
- * Highlight.js emits and emit a list of text ranges and classes for the
- * markers.
- *
- * @param {string} str The string of HTML.
- * @param {Map<string, !Array<!Object>>} rangesCache A map for caching
- * ranges for each string. A cache is read and written by this method.
- * Since diff is mostly comparing same file on two sides, there is good rate
- * of duplication at least for parts that are on left and right parts.
- * @return {!Array<!Object>} The list of ranges.
- */
- _rangesFromString(str, rangesCache) {
- const cached = rangesCache.get(str);
- if (cached) return cached;
+ const div = document.createElement('div');
+ div.innerHTML = str;
+ const ranges = this._rangesFromElement(div, 0);
+ rangesCache.set(str, ranges);
+ return ranges;
+ }
- const div = document.createElement('div');
- div.innerHTML = str;
- const ranges = this._rangesFromElement(div, 0);
- rangesCache.set(str, ranges);
- return ranges;
- }
-
- _rangesFromElement(elem, offset) {
- let result = [];
- for (const node of elem.childNodes) {
- const nodeLength = GrAnnotation.getLength(node);
- // Note: HLJS may emit a span with class undefined when it thinks there
- // may be a syntax error.
- if (node.tagName === 'SPAN' && node.className !== 'undefined') {
- if (CLASS_WHITELIST.hasOwnProperty(node.className)) {
- result.push({
- start: offset,
- length: nodeLength,
- className: node.className,
- });
- }
- if (node.children.length) {
- result = result.concat(this._rangesFromElement(node, offset));
- }
+ _rangesFromElement(elem, offset) {
+ let result = [];
+ for (const node of elem.childNodes) {
+ const nodeLength = GrAnnotation.getLength(node);
+ // Note: HLJS may emit a span with class undefined when it thinks there
+ // may be a syntax error.
+ if (node.tagName === 'SPAN' && node.className !== 'undefined') {
+ if (CLASS_WHITELIST.hasOwnProperty(node.className)) {
+ result.push({
+ start: offset,
+ length: nodeLength,
+ className: node.className,
+ });
}
- offset += nodeLength;
+ if (node.children.length) {
+ result = result.concat(this._rangesFromElement(node, offset));
+ }
}
- return result;
+ offset += nodeLength;
}
+ return result;
+ }
- /**
- * For a given state, process the syntax for the next line (or pair of
- * lines).
- *
- * @param {!Object} state The processing state for the layer.
- */
- _processNextLine(state, rangesCache) {
- let baseLine;
- let revisionLine;
+ /**
+ * For a given state, process the syntax for the next line (or pair of
+ * lines).
+ *
+ * @param {!Object} state The processing state for the layer.
+ */
+ _processNextLine(state, rangesCache) {
+ let baseLine;
+ let revisionLine;
- const section = this.diff.content[state.sectionIndex];
- if (section.ab) {
- baseLine = section.ab[state.lineIndex];
- revisionLine = section.ab[state.lineIndex];
+ const section = this.diff.content[state.sectionIndex];
+ if (section.ab) {
+ baseLine = section.ab[state.lineIndex];
+ revisionLine = section.ab[state.lineIndex];
+ state.lineNums.left++;
+ state.lineNums.right++;
+ } else {
+ if (section.a && section.a.length > state.lineIndex) {
+ baseLine = section.a[state.lineIndex];
state.lineNums.left++;
+ }
+ if (section.b && section.b.length > state.lineIndex) {
+ revisionLine = section.b[state.lineIndex];
state.lineNums.right++;
- } else {
- if (section.a && section.a.length > state.lineIndex) {
- baseLine = section.a[state.lineIndex];
- state.lineNums.left++;
- }
- if (section.b && section.b.length > state.lineIndex) {
- revisionLine = section.b[state.lineIndex];
- state.lineNums.right++;
- }
- }
-
- // To store the result of the syntax highlighter.
- let result;
-
- if (this._baseLanguage && baseLine !== undefined &&
- this._hljs.getLanguage(this._baseLanguage)) {
- baseLine = this._workaround(this._baseLanguage, baseLine);
- result = this._hljs.highlight(this._baseLanguage, baseLine, true,
- state.baseContext);
- this.push('_baseRanges',
- this._rangesFromString(result.value, rangesCache));
- state.baseContext = result.top;
- }
-
- if (this._revisionLanguage && revisionLine !== undefined &&
- this._hljs.getLanguage(this._revisionLanguage)) {
- revisionLine = this._workaround(this._revisionLanguage, revisionLine);
- result = this._hljs.highlight(this._revisionLanguage, revisionLine,
- true, state.revisionContext);
- this.push('_revisionRanges',
- this._rangesFromString(result.value, rangesCache));
- state.revisionContext = result.top;
}
}
- /**
- * Ad hoc fixes for HLJS parsing bugs. Rewrite lines of code in constrained
- * cases before sending them into HLJS so that they parse correctly.
- *
- * Important notes:
- * * These tests should be as constrained as possible to avoid interfering
- * with code it shouldn't AND to avoid executing regexes as much as
- * possible.
- * * These tests should document the issue clearly enough that the test can
- * be condidently removed when the issue is solved in HLJS.
- * * These tests should rewrite the line of code to have the same number of
- * characters. This method rewrites the string that gets parsed, but NOT
- * the string that gets displayed and highlighted. Thus, the positions
- * must be consistent.
- *
- * @param {!string} language The name of the HLJS language plugin in use.
- * @param {!string} line The line of code to potentially rewrite.
- * @return {string} A potentially-rewritten line of code.
- */
- _workaround(language, line) {
- if (language === 'cpp') {
- /**
- * Prevent confusing < and << operators for the start of a meta string
- * by converting them to a different operator.
- * {@see Issue 4864}
- * {@see https://github.com/isagalaev/highlight.js/issues/1341}
- */
- if (CPP_DIRECTIVE_WITH_LT_PATTERN.test(line)) {
- line = line.replace(GLOBAL_LT_PATTERN, '|');
- }
+ // To store the result of the syntax highlighter.
+ let result;
- /**
- * Rewrite CPP wchar_t characters literals to wchar_t string literals
- * because HLJS only understands the string form.
- * {@see Issue 5242}
- * {#see https://github.com/isagalaev/highlight.js/issues/1412}
- */
- if (CPP_WCHAR_PATTERN.test(line)) {
- line = line.replace(CPP_WCHAR_PATTERN, 'L"$1."');
- }
+ if (this._baseLanguage && baseLine !== undefined &&
+ this._hljs.getLanguage(this._baseLanguage)) {
+ baseLine = this._workaround(this._baseLanguage, baseLine);
+ result = this._hljs.highlight(this._baseLanguage, baseLine, true,
+ state.baseContext);
+ this.push('_baseRanges',
+ this._rangesFromString(result.value, rangesCache));
+ state.baseContext = result.top;
+ }
- return line;
+ if (this._revisionLanguage && revisionLine !== undefined &&
+ this._hljs.getLanguage(this._revisionLanguage)) {
+ revisionLine = this._workaround(this._revisionLanguage, revisionLine);
+ result = this._hljs.highlight(this._revisionLanguage, revisionLine,
+ true, state.revisionContext);
+ this.push('_revisionRanges',
+ this._rangesFromString(result.value, rangesCache));
+ state.revisionContext = result.top;
+ }
+ }
+
+ /**
+ * Ad hoc fixes for HLJS parsing bugs. Rewrite lines of code in constrained
+ * cases before sending them into HLJS so that they parse correctly.
+ *
+ * Important notes:
+ * * These tests should be as constrained as possible to avoid interfering
+ * with code it shouldn't AND to avoid executing regexes as much as
+ * possible.
+ * * These tests should document the issue clearly enough that the test can
+ * be condidently removed when the issue is solved in HLJS.
+ * * These tests should rewrite the line of code to have the same number of
+ * characters. This method rewrites the string that gets parsed, but NOT
+ * the string that gets displayed and highlighted. Thus, the positions
+ * must be consistent.
+ *
+ * @param {!string} language The name of the HLJS language plugin in use.
+ * @param {!string} line The line of code to potentially rewrite.
+ * @return {string} A potentially-rewritten line of code.
+ */
+ _workaround(language, line) {
+ if (language === 'cpp') {
+ /**
+ * Prevent confusing < and << operators for the start of a meta string
+ * by converting them to a different operator.
+ * {@see Issue 4864}
+ * {@see https://github.com/isagalaev/highlight.js/issues/1341}
+ */
+ if (CPP_DIRECTIVE_WITH_LT_PATTERN.test(line)) {
+ line = line.replace(GLOBAL_LT_PATTERN, '|');
}
/**
- * Prevent confusing the closing paren of a parameterized Java annotation
- * being applied to a formal argument as the closing paren of the argument
- * list. Rewrite the parens as spaces.
- * {@see Issue 4776}
- * {@see https://github.com/isagalaev/highlight.js/issues/1324}
+ * Rewrite CPP wchar_t characters literals to wchar_t string literals
+ * because HLJS only understands the string form.
+ * {@see Issue 5242}
+ * {#see https://github.com/isagalaev/highlight.js/issues/1412}
*/
- if (language === 'java' && JAVA_PARAM_ANNOT_PATTERN.test(line)) {
- return line.replace(JAVA_PARAM_ANNOT_PATTERN, '$1 $2 ');
- }
-
- /**
- * HLJS misunderstands backslash character literals in Go.
- * {@see Issue 5007}
- * {#see https://github.com/isagalaev/highlight.js/issues/1411}
- */
- if (language === 'go' && line.includes(GO_BACKSLASH_LITERAL)) {
- return line.replace(GO_BACKSLASH_LITERAL, '"\\\\"');
+ if (CPP_WCHAR_PATTERN.test(line)) {
+ line = line.replace(CPP_WCHAR_PATTERN, 'L"$1."');
}
return line;
}
/**
- * Tells whether the state has exhausted its current section.
- *
- * @param {!Object} state
- * @return {boolean}
+ * Prevent confusing the closing paren of a parameterized Java annotation
+ * being applied to a formal argument as the closing paren of the argument
+ * list. Rewrite the parens as spaces.
+ * {@see Issue 4776}
+ * {@see https://github.com/isagalaev/highlight.js/issues/1324}
*/
- _isSectionDone(state) {
- const section = this.diff.content[state.sectionIndex];
- if (section.ab) {
- return state.lineIndex >= section.ab.length;
- } else {
- return (!section.a || state.lineIndex >= section.a.length) &&
- (!section.b || state.lineIndex >= section.b.length);
- }
+ if (language === 'java' && JAVA_PARAM_ANNOT_PATTERN.test(line)) {
+ return line.replace(JAVA_PARAM_ANNOT_PATTERN, '$1 $2 ');
}
/**
- * For a given state, notify layer listeners of any processed line ranges
- * that have not yet been notified.
- *
- * @param {!Object} state
+ * HLJS misunderstands backslash character literals in Go.
+ * {@see Issue 5007}
+ * {#see https://github.com/isagalaev/highlight.js/issues/1411}
*/
- _notify(state) {
- if (state.lineNums.left - state.lastNotify.left) {
- this._notifyRange(
- state.lastNotify.left,
- state.lineNums.left,
- 'left');
- state.lastNotify.left = state.lineNums.left;
- }
- if (state.lineNums.right - state.lastNotify.right) {
- this._notifyRange(
- state.lastNotify.right,
- state.lineNums.right,
- 'right');
- state.lastNotify.right = state.lineNums.right;
- }
+ if (language === 'go' && line.includes(GO_BACKSLASH_LITERAL)) {
+ return line.replace(GO_BACKSLASH_LITERAL, '"\\\\"');
}
- _notifyRange(start, end, side) {
- for (const fn of this._listeners) {
- fn(start, end, side);
- }
- }
+ return line;
+ }
- _loadHLJS() {
- return this.$.libLoader.getHLJS().then(hljs => {
- this._hljs = hljs;
- });
+ /**
+ * Tells whether the state has exhausted its current section.
+ *
+ * @param {!Object} state
+ * @return {boolean}
+ */
+ _isSectionDone(state) {
+ const section = this.diff.content[state.sectionIndex];
+ if (section.ab) {
+ return state.lineIndex >= section.ab.length;
+ } else {
+ return (!section.a || state.lineIndex >= section.a.length) &&
+ (!section.b || state.lineIndex >= section.b.length);
}
}
- customElements.define(GrSyntaxLayer.is, GrSyntaxLayer);
-})();
+ /**
+ * For a given state, notify layer listeners of any processed line ranges
+ * that have not yet been notified.
+ *
+ * @param {!Object} state
+ */
+ _notify(state) {
+ if (state.lineNums.left - state.lastNotify.left) {
+ this._notifyRange(
+ state.lastNotify.left,
+ state.lineNums.left,
+ 'left');
+ state.lastNotify.left = state.lineNums.left;
+ }
+ if (state.lineNums.right - state.lastNotify.right) {
+ this._notifyRange(
+ state.lastNotify.right,
+ state.lineNums.right,
+ 'right');
+ state.lastNotify.right = state.lineNums.right;
+ }
+ }
+
+ _notifyRange(start, end, side) {
+ for (const fn of this._listeners) {
+ fn(start, end, side);
+ }
+ }
+
+ _loadHLJS() {
+ return this.$.libLoader.getHLJS().then(hljs => {
+ this._hljs = hljs;
+ });
+ }
+}
+
+customElements.define(GrSyntaxLayer.is, GrSyntaxLayer);
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_html.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_html.js
new file mode 100644
index 0000000..183cbd1c
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_html.js
@@ -0,0 +1,21 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <gr-lib-loader id="libLoader"></gr-lib-loader>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
index 62a3f1e..2385067 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -19,16 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-syntax-layer</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
-<link rel="import" href="gr-syntax-layer.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,469 +30,471 @@
</template>
</test-fixture>
-<script>
- suite('gr-syntax-layer tests', async () => {
- await readyToTest();
- let sandbox;
- let diff;
- let element;
- const lineNumberEl = document.createElement('td');
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import './gr-syntax-layer.js';
+suite('gr-syntax-layer tests', () => {
+ let sandbox;
+ let diff;
+ let element;
+ const lineNumberEl = document.createElement('td');
- function getMockHLJS() {
- const html = '<span class="gr-diff gr-syntax gr-syntax-string">' +
- 'ipsum</span>';
- return {
- configure() {},
- highlight(lang, line, ignore, state) {
- return {
- value: line.replace(/ipsum/, html),
- top: state === undefined ? 1 : state + 1,
- };
- },
- // Return something truthy because this method is used to check if the
- // language is supported.
- getLanguage(s) {
- return {};
- },
- };
- }
+ function getMockHLJS() {
+ const html = '<span class="gr-diff gr-syntax gr-syntax-string">' +
+ 'ipsum</span>';
+ return {
+ configure() {},
+ highlight(lang, line, ignore, state) {
+ return {
+ value: line.replace(/ipsum/, html),
+ top: state === undefined ? 1 : state + 1,
+ };
+ },
+ // Return something truthy because this method is used to check if the
+ // language is supported.
+ getLanguage(s) {
+ return {};
+ },
+ };
+ }
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- const mock = document.createElement('mock-diff-response');
- diff = mock.diffResponse;
- element.diff = diff;
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ const mock = document.createElement('mock-diff-response');
+ diff = mock.diffResponse;
+ element.diff = diff;
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('annotate without range does nothing', () => {
- const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
- const el = document.createElement('div');
- el.textContent = 'Etiam dui, blandit wisi.';
- const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
- line.beforeNumber = 12;
+ test('annotate without range does nothing', () => {
+ const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+ const el = document.createElement('div');
+ el.textContent = 'Etiam dui, blandit wisi.';
+ const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
+ line.beforeNumber = 12;
- element.annotate(el, lineNumberEl, line);
+ element.annotate(el, lineNumberEl, line);
- assert.isFalse(annotationSpy.called);
- });
+ assert.isFalse(annotationSpy.called);
+ });
- test('annotate with range applies it', () => {
- const str = 'Etiam dui, blandit wisi.';
- const start = 6;
- const length = 3;
- const className = 'foobar';
+ test('annotate with range applies it', () => {
+ const str = 'Etiam dui, blandit wisi.';
+ const start = 6;
+ const length = 3;
+ const className = 'foobar';
- const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
- const el = document.createElement('div');
- el.textContent = str;
- const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
- line.beforeNumber = 12;
- element._baseRanges[11] = [{
- start,
- length,
- className,
- }];
+ const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+ const el = document.createElement('div');
+ el.textContent = str;
+ const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
+ line.beforeNumber = 12;
+ element._baseRanges[11] = [{
+ start,
+ length,
+ className,
+ }];
- element.annotate(el, lineNumberEl, line);
+ element.annotate(el, lineNumberEl, line);
- assert.isTrue(annotationSpy.called);
- assert.equal(annotationSpy.lastCall.args[0], el);
- assert.equal(annotationSpy.lastCall.args[1], start);
- assert.equal(annotationSpy.lastCall.args[2], length);
- assert.equal(annotationSpy.lastCall.args[3], className);
- assert.isOk(el.querySelector('hl.' + className));
- });
+ assert.isTrue(annotationSpy.called);
+ assert.equal(annotationSpy.lastCall.args[0], el);
+ assert.equal(annotationSpy.lastCall.args[1], start);
+ assert.equal(annotationSpy.lastCall.args[2], length);
+ assert.equal(annotationSpy.lastCall.args[3], className);
+ assert.isOk(el.querySelector('hl.' + className));
+ });
- test('annotate with range but disabled does nothing', () => {
- const str = 'Etiam dui, blandit wisi.';
- const start = 6;
- const length = 3;
- const className = 'foobar';
+ test('annotate with range but disabled does nothing', () => {
+ const str = 'Etiam dui, blandit wisi.';
+ const start = 6;
+ const length = 3;
+ const className = 'foobar';
- const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
- const el = document.createElement('div');
- el.textContent = str;
- const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
- line.beforeNumber = 12;
- element._baseRanges[11] = [{
- start,
- length,
- className,
- }];
- element.enabled = false;
+ const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+ const el = document.createElement('div');
+ el.textContent = str;
+ const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
+ line.beforeNumber = 12;
+ element._baseRanges[11] = [{
+ start,
+ length,
+ className,
+ }];
+ element.enabled = false;
- element.annotate(el, lineNumberEl, line);
+ element.annotate(el, lineNumberEl, line);
- assert.isFalse(annotationSpy.called);
- });
+ assert.isFalse(annotationSpy.called);
+ });
- test('process on empty diff does nothing', done => {
- element.diff = {
- meta_a: {content_type: 'application/json'},
- meta_b: {content_type: 'application/json'},
- content: [],
- };
- const processNextSpy = sandbox.spy(element, '_processNextLine');
+ test('process on empty diff does nothing', done => {
+ element.diff = {
+ meta_a: {content_type: 'application/json'},
+ meta_b: {content_type: 'application/json'},
+ content: [],
+ };
+ const processNextSpy = sandbox.spy(element, '_processNextLine');
- const processPromise = element.process();
+ const processPromise = element.process();
- processPromise.then(() => {
- assert.isFalse(processNextSpy.called);
- assert.equal(element._baseRanges.length, 0);
- assert.equal(element._revisionRanges.length, 0);
- done();
- });
- });
-
- test('process for unsupported languages does nothing', done => {
- element.diff = {
- meta_a: {content_type: 'text/x+objective-cobol-plus-plus'},
- meta_b: {content_type: 'application/not-a-real-language'},
- content: [],
- };
- const processNextSpy = sandbox.spy(element, '_processNextLine');
-
- const processPromise = element.process();
-
- processPromise.then(() => {
- assert.isFalse(processNextSpy.called);
- assert.equal(element._baseRanges.length, 0);
- assert.equal(element._revisionRanges.length, 0);
- done();
- });
- });
-
- test('process while disabled does nothing', done => {
- const processNextSpy = sandbox.spy(element, '_processNextLine');
- element.enabled = false;
- const loadHLJSSpy = sandbox.spy(element, '_loadHLJS');
-
- const processPromise = element.process();
-
- processPromise.then(() => {
- assert.isFalse(processNextSpy.called);
- assert.equal(element._baseRanges.length, 0);
- assert.equal(element._revisionRanges.length, 0);
- assert.isFalse(loadHLJSSpy.called);
- done();
- });
- });
-
- test('process highlight ipsum', done => {
- element.diff.meta_a.content_type = 'application/json';
- element.diff.meta_b.content_type = 'application/json';
-
- const mockHLJS = getMockHLJS();
- const highlightSpy = sinon.spy(mockHLJS, 'highlight');
- sandbox.stub(element.$.libLoader, 'getHLJS',
- () => Promise.resolve(mockHLJS));
- const processNextSpy = sandbox.spy(element, '_processNextLine');
- const processPromise = element.process();
-
- processPromise.then(() => {
- const linesA = diff.meta_a.lines;
- const linesB = diff.meta_b.lines;
-
- assert.isTrue(processNextSpy.called);
- assert.equal(element._baseRanges.length, linesA);
- assert.equal(element._revisionRanges.length, linesB);
-
- assert.equal(highlightSpy.callCount, linesA + linesB);
-
- // The first line of both sides have a range.
- let ranges = [element._baseRanges[0], element._revisionRanges[0]];
- for (const range of ranges) {
- assert.equal(range.length, 1);
- assert.equal(range[0].className,
- 'gr-diff gr-syntax gr-syntax-string');
- assert.equal(range[0].start, 'lorem '.length);
- assert.equal(range[0].length, 'ipsum'.length);
- }
-
- // There are no ranges from ll.1-12 on the left and ll.1-11 on the
- // right.
- ranges = element._baseRanges.slice(1, 12)
- .concat(element._revisionRanges.slice(1, 11));
-
- for (const range of ranges) {
- assert.equal(range.length, 0);
- }
-
- // There should be another pair of ranges on l.13 for the left and
- // l.12 for the right.
- ranges = [element._baseRanges[13], element._revisionRanges[12]];
-
- for (const range of ranges) {
- assert.equal(range.length, 1);
- assert.equal(range[0].className,
- 'gr-diff gr-syntax gr-syntax-string');
- assert.equal(range[0].start, 32);
- assert.equal(range[0].length, 'ipsum'.length);
- }
-
- // The next group should have a similar instance on either side.
-
- let range = element._baseRanges[15];
- assert.equal(range.length, 1);
- assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string');
- assert.equal(range[0].start, 34);
- assert.equal(range[0].length, 'ipsum'.length);
-
- range = element._revisionRanges[14];
- assert.equal(range.length, 1);
- assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string');
- assert.equal(range[0].start, 35);
- assert.equal(range[0].length, 'ipsum'.length);
-
- done();
- });
- });
-
- test('_diffChanged calls cancel', () => {
- const cancelSpy = sandbox.spy(element, '_diffChanged');
- element.diff = {content: []};
- assert.isTrue(cancelSpy.called);
- });
-
- test('_rangesFromElement no ranges', () => {
- const elem = document.createElement('span');
- elem.textContent = 'Etiam dui, blandit wisi.';
- const offset = 100;
-
- const result = element._rangesFromElement(elem, offset);
-
- assert.equal(result.length, 0);
- });
-
- test('_rangesFromElement single range', () => {
- const str0 = 'Etiam ';
- const str1 = 'dui, blandit';
- const str2 = ' wisi.';
- const className = 'gr-diff gr-syntax gr-syntax-string';
- const offset = 100;
-
- const elem = document.createElement('span');
- elem.appendChild(document.createTextNode(str0));
- const span = document.createElement('span');
- span.textContent = str1;
- span.className = className;
- elem.appendChild(span);
- elem.appendChild(document.createTextNode(str2));
-
- const result = element._rangesFromElement(elem, offset);
-
- assert.equal(result.length, 1);
- assert.equal(result[0].start, str0.length + offset);
- assert.equal(result[0].length, str1.length);
- assert.equal(result[0].className, className);
- });
-
- test('_rangesFromElement non-whitelist', () => {
- const str0 = 'Etiam ';
- const str1 = 'dui, blandit';
- const str2 = ' wisi.';
- const className = 'not-in-the-whitelist';
- const offset = 100;
-
- const elem = document.createElement('span');
- elem.appendChild(document.createTextNode(str0));
- const span = document.createElement('span');
- span.textContent = str1;
- span.className = className;
- elem.appendChild(span);
- elem.appendChild(document.createTextNode(str2));
-
- const result = element._rangesFromElement(elem, offset);
-
- assert.equal(result.length, 0);
- });
-
- test('_rangesFromElement milti range', () => {
- const str0 = 'Etiam ';
- const str1 = 'dui,';
- const str2 = ' blandit';
- const str3 = ' wisi.';
- const className = 'gr-diff gr-syntax gr-syntax-string';
- const offset = 100;
-
- const elem = document.createElement('span');
- elem.appendChild(document.createTextNode(str0));
- let span = document.createElement('span');
- span.textContent = str1;
- span.className = className;
- elem.appendChild(span);
- elem.appendChild(document.createTextNode(str2));
- span = document.createElement('span');
- span.textContent = str3;
- span.className = className;
- elem.appendChild(span);
-
- const result = element._rangesFromElement(elem, offset);
-
- assert.equal(result.length, 2);
-
- assert.equal(result[0].start, str0.length + offset);
- assert.equal(result[0].length, str1.length);
- assert.equal(result[0].className, className);
-
- assert.equal(result[1].start,
- str0.length + str1.length + str2.length + offset);
- assert.equal(result[1].length, str3.length);
- assert.equal(result[1].className, className);
- });
-
- test('_rangesFromElement nested range', () => {
- const str0 = 'Etiam ';
- const str1 = 'dui,';
- const str2 = ' blandit';
- const str3 = ' wisi.';
- const className = 'gr-diff gr-syntax gr-syntax-string';
- const offset = 100;
-
- const elem = document.createElement('span');
- elem.appendChild(document.createTextNode(str0));
- const span1 = document.createElement('span');
- span1.textContent = str1;
- span1.className = className;
- elem.appendChild(span1);
- const span2 = document.createElement('span');
- span2.textContent = str2;
- span2.className = className;
- span1.appendChild(span2);
- elem.appendChild(document.createTextNode(str3));
-
- const result = element._rangesFromElement(elem, offset);
-
- assert.equal(result.length, 2);
-
- assert.equal(result[0].start, str0.length + offset);
- assert.equal(result[0].length, str1.length + str2.length);
- assert.equal(result[0].className, className);
-
- assert.equal(result[1].start, str0.length + str1.length + offset);
- assert.equal(result[1].length, str2.length);
- assert.equal(result[1].className, className);
- });
-
- test('_rangesFromString whitelist allows recursion', () => {
- const str = [
- '<span class="non-whtelisted-class">',
- '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>',
- '</span>'].join('');
- const result = element._rangesFromString(str, new Map());
- assert.notEqual(result.length, 0);
- });
-
- test('_rangesFromString cache same syntax markers', () => {
- sandbox.spy(element, '_rangesFromElement');
- const str =
- '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>';
- const cacheMap = new Map();
- element._rangesFromString(str, cacheMap);
- element._rangesFromString(str, cacheMap);
- assert.isTrue(element._rangesFromElement.calledOnce);
- });
-
- test('_isSectionDone', () => {
- let state = {sectionIndex: 0, lineIndex: 0};
- assert.isFalse(element._isSectionDone(state));
-
- state = {sectionIndex: 0, lineIndex: 2};
- assert.isFalse(element._isSectionDone(state));
-
- state = {sectionIndex: 0, lineIndex: 4};
- assert.isTrue(element._isSectionDone(state));
-
- state = {sectionIndex: 1, lineIndex: 2};
- assert.isFalse(element._isSectionDone(state));
-
- state = {sectionIndex: 1, lineIndex: 3};
- assert.isTrue(element._isSectionDone(state));
-
- state = {sectionIndex: 3, lineIndex: 0};
- assert.isFalse(element._isSectionDone(state));
-
- state = {sectionIndex: 3, lineIndex: 3};
- assert.isFalse(element._isSectionDone(state));
-
- state = {sectionIndex: 3, lineIndex: 4};
- assert.isTrue(element._isSectionDone(state));
- });
-
- test('workaround CPP LT directive', () => {
- // Does nothing to regular line.
- let line = 'int main(int argc, char** argv) { return 0; }';
- assert.equal(element._workaround('cpp', line), line);
-
- // Does nothing to include directive.
- line = '#include <stdio>';
- assert.equal(element._workaround('cpp', line), line);
-
- // Converts left-shift operator in #define.
- line = '#define GiB (1ull << 30)';
- let expected = '#define GiB (1ull || 30)';
- assert.equal(element._workaround('cpp', line), expected);
-
- // Converts less-than operator in #if.
- line = ' #if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 1)';
- expected = ' #if __GNUC__ | 4 || (__GNUC__ == 4 && __GNUC_MINOR__ | 1)';
- assert.equal(element._workaround('cpp', line), expected);
- });
-
- test('workaround Java param-annotation', () => {
- // Does nothing to regular line.
- let line = 'public static void foo(int bar) { }';
- assert.equal(element._workaround('java', line), line);
-
- // Does nothing to regular annotation.
- line = 'public static void foo(@Nullable int bar) { }';
- assert.equal(element._workaround('java', line), line);
-
- // Converts parameterized annotation.
- line = 'public static void foo(@SuppressWarnings("unused") int bar) { }';
- const expected = 'public static void foo(@SuppressWarnings "unused" ' +
- ' int bar) { }';
- assert.equal(element._workaround('java', line), expected);
- });
-
- test('workaround CPP whcar_t character literals', () => {
- // Does nothing to regular line.
- let line = 'int main(int argc, char** argv) { return 0; }';
- assert.equal(element._workaround('cpp', line), line);
-
- // Does nothing to wchar_t string.
- line = 'wchar_t* sz = L"abc 123";';
- assert.equal(element._workaround('cpp', line), line);
-
- // Converts wchar_t character literal to string.
- line = 'wchar_t myChar = L\'#\'';
- let expected = 'wchar_t myChar = L"."';
- assert.equal(element._workaround('cpp', line), expected);
-
- // Converts wchar_t character literal with escape sequence to string.
- line = 'wchar_t myChar = L\'\\"\'';
- expected = 'wchar_t myChar = L"\\."';
- assert.equal(element._workaround('cpp', line), expected);
- });
-
- test('workaround go backslash character literals', () => {
- // Does nothing to regular line.
- let line = 'func foo(in []byte) (lit []byte, n int, err error) {';
- assert.equal(element._workaround('go', line), line);
-
- // Does nothing to string with backslash literal
- line = 'c := "\\\\"';
- assert.equal(element._workaround('go', line), line);
-
- // Converts backslash literal character to a string.
- line = 'c := \'\\\\\'';
- const expected = 'c := "\\\\"';
- assert.equal(element._workaround('go', line), expected);
+ processPromise.then(() => {
+ assert.isFalse(processNextSpy.called);
+ assert.equal(element._baseRanges.length, 0);
+ assert.equal(element._revisionRanges.length, 0);
+ done();
});
});
+
+ test('process for unsupported languages does nothing', done => {
+ element.diff = {
+ meta_a: {content_type: 'text/x+objective-cobol-plus-plus'},
+ meta_b: {content_type: 'application/not-a-real-language'},
+ content: [],
+ };
+ const processNextSpy = sandbox.spy(element, '_processNextLine');
+
+ const processPromise = element.process();
+
+ processPromise.then(() => {
+ assert.isFalse(processNextSpy.called);
+ assert.equal(element._baseRanges.length, 0);
+ assert.equal(element._revisionRanges.length, 0);
+ done();
+ });
+ });
+
+ test('process while disabled does nothing', done => {
+ const processNextSpy = sandbox.spy(element, '_processNextLine');
+ element.enabled = false;
+ const loadHLJSSpy = sandbox.spy(element, '_loadHLJS');
+
+ const processPromise = element.process();
+
+ processPromise.then(() => {
+ assert.isFalse(processNextSpy.called);
+ assert.equal(element._baseRanges.length, 0);
+ assert.equal(element._revisionRanges.length, 0);
+ assert.isFalse(loadHLJSSpy.called);
+ done();
+ });
+ });
+
+ test('process highlight ipsum', done => {
+ element.diff.meta_a.content_type = 'application/json';
+ element.diff.meta_b.content_type = 'application/json';
+
+ const mockHLJS = getMockHLJS();
+ const highlightSpy = sinon.spy(mockHLJS, 'highlight');
+ sandbox.stub(element.$.libLoader, 'getHLJS',
+ () => Promise.resolve(mockHLJS));
+ const processNextSpy = sandbox.spy(element, '_processNextLine');
+ const processPromise = element.process();
+
+ processPromise.then(() => {
+ const linesA = diff.meta_a.lines;
+ const linesB = diff.meta_b.lines;
+
+ assert.isTrue(processNextSpy.called);
+ assert.equal(element._baseRanges.length, linesA);
+ assert.equal(element._revisionRanges.length, linesB);
+
+ assert.equal(highlightSpy.callCount, linesA + linesB);
+
+ // The first line of both sides have a range.
+ let ranges = [element._baseRanges[0], element._revisionRanges[0]];
+ for (const range of ranges) {
+ assert.equal(range.length, 1);
+ assert.equal(range[0].className,
+ 'gr-diff gr-syntax gr-syntax-string');
+ assert.equal(range[0].start, 'lorem '.length);
+ assert.equal(range[0].length, 'ipsum'.length);
+ }
+
+ // There are no ranges from ll.1-12 on the left and ll.1-11 on the
+ // right.
+ ranges = element._baseRanges.slice(1, 12)
+ .concat(element._revisionRanges.slice(1, 11));
+
+ for (const range of ranges) {
+ assert.equal(range.length, 0);
+ }
+
+ // There should be another pair of ranges on l.13 for the left and
+ // l.12 for the right.
+ ranges = [element._baseRanges[13], element._revisionRanges[12]];
+
+ for (const range of ranges) {
+ assert.equal(range.length, 1);
+ assert.equal(range[0].className,
+ 'gr-diff gr-syntax gr-syntax-string');
+ assert.equal(range[0].start, 32);
+ assert.equal(range[0].length, 'ipsum'.length);
+ }
+
+ // The next group should have a similar instance on either side.
+
+ let range = element._baseRanges[15];
+ assert.equal(range.length, 1);
+ assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string');
+ assert.equal(range[0].start, 34);
+ assert.equal(range[0].length, 'ipsum'.length);
+
+ range = element._revisionRanges[14];
+ assert.equal(range.length, 1);
+ assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string');
+ assert.equal(range[0].start, 35);
+ assert.equal(range[0].length, 'ipsum'.length);
+
+ done();
+ });
+ });
+
+ test('_diffChanged calls cancel', () => {
+ const cancelSpy = sandbox.spy(element, '_diffChanged');
+ element.diff = {content: []};
+ assert.isTrue(cancelSpy.called);
+ });
+
+ test('_rangesFromElement no ranges', () => {
+ const elem = document.createElement('span');
+ elem.textContent = 'Etiam dui, blandit wisi.';
+ const offset = 100;
+
+ const result = element._rangesFromElement(elem, offset);
+
+ assert.equal(result.length, 0);
+ });
+
+ test('_rangesFromElement single range', () => {
+ const str0 = 'Etiam ';
+ const str1 = 'dui, blandit';
+ const str2 = ' wisi.';
+ const className = 'gr-diff gr-syntax gr-syntax-string';
+ const offset = 100;
+
+ const elem = document.createElement('span');
+ elem.appendChild(document.createTextNode(str0));
+ const span = document.createElement('span');
+ span.textContent = str1;
+ span.className = className;
+ elem.appendChild(span);
+ elem.appendChild(document.createTextNode(str2));
+
+ const result = element._rangesFromElement(elem, offset);
+
+ assert.equal(result.length, 1);
+ assert.equal(result[0].start, str0.length + offset);
+ assert.equal(result[0].length, str1.length);
+ assert.equal(result[0].className, className);
+ });
+
+ test('_rangesFromElement non-whitelist', () => {
+ const str0 = 'Etiam ';
+ const str1 = 'dui, blandit';
+ const str2 = ' wisi.';
+ const className = 'not-in-the-whitelist';
+ const offset = 100;
+
+ const elem = document.createElement('span');
+ elem.appendChild(document.createTextNode(str0));
+ const span = document.createElement('span');
+ span.textContent = str1;
+ span.className = className;
+ elem.appendChild(span);
+ elem.appendChild(document.createTextNode(str2));
+
+ const result = element._rangesFromElement(elem, offset);
+
+ assert.equal(result.length, 0);
+ });
+
+ test('_rangesFromElement milti range', () => {
+ const str0 = 'Etiam ';
+ const str1 = 'dui,';
+ const str2 = ' blandit';
+ const str3 = ' wisi.';
+ const className = 'gr-diff gr-syntax gr-syntax-string';
+ const offset = 100;
+
+ const elem = document.createElement('span');
+ elem.appendChild(document.createTextNode(str0));
+ let span = document.createElement('span');
+ span.textContent = str1;
+ span.className = className;
+ elem.appendChild(span);
+ elem.appendChild(document.createTextNode(str2));
+ span = document.createElement('span');
+ span.textContent = str3;
+ span.className = className;
+ elem.appendChild(span);
+
+ const result = element._rangesFromElement(elem, offset);
+
+ assert.equal(result.length, 2);
+
+ assert.equal(result[0].start, str0.length + offset);
+ assert.equal(result[0].length, str1.length);
+ assert.equal(result[0].className, className);
+
+ assert.equal(result[1].start,
+ str0.length + str1.length + str2.length + offset);
+ assert.equal(result[1].length, str3.length);
+ assert.equal(result[1].className, className);
+ });
+
+ test('_rangesFromElement nested range', () => {
+ const str0 = 'Etiam ';
+ const str1 = 'dui,';
+ const str2 = ' blandit';
+ const str3 = ' wisi.';
+ const className = 'gr-diff gr-syntax gr-syntax-string';
+ const offset = 100;
+
+ const elem = document.createElement('span');
+ elem.appendChild(document.createTextNode(str0));
+ const span1 = document.createElement('span');
+ span1.textContent = str1;
+ span1.className = className;
+ elem.appendChild(span1);
+ const span2 = document.createElement('span');
+ span2.textContent = str2;
+ span2.className = className;
+ span1.appendChild(span2);
+ elem.appendChild(document.createTextNode(str3));
+
+ const result = element._rangesFromElement(elem, offset);
+
+ assert.equal(result.length, 2);
+
+ assert.equal(result[0].start, str0.length + offset);
+ assert.equal(result[0].length, str1.length + str2.length);
+ assert.equal(result[0].className, className);
+
+ assert.equal(result[1].start, str0.length + str1.length + offset);
+ assert.equal(result[1].length, str2.length);
+ assert.equal(result[1].className, className);
+ });
+
+ test('_rangesFromString whitelist allows recursion', () => {
+ const str = [
+ '<span class="non-whtelisted-class">',
+ '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>',
+ '</span>'].join('');
+ const result = element._rangesFromString(str, new Map());
+ assert.notEqual(result.length, 0);
+ });
+
+ test('_rangesFromString cache same syntax markers', () => {
+ sandbox.spy(element, '_rangesFromElement');
+ const str =
+ '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>';
+ const cacheMap = new Map();
+ element._rangesFromString(str, cacheMap);
+ element._rangesFromString(str, cacheMap);
+ assert.isTrue(element._rangesFromElement.calledOnce);
+ });
+
+ test('_isSectionDone', () => {
+ let state = {sectionIndex: 0, lineIndex: 0};
+ assert.isFalse(element._isSectionDone(state));
+
+ state = {sectionIndex: 0, lineIndex: 2};
+ assert.isFalse(element._isSectionDone(state));
+
+ state = {sectionIndex: 0, lineIndex: 4};
+ assert.isTrue(element._isSectionDone(state));
+
+ state = {sectionIndex: 1, lineIndex: 2};
+ assert.isFalse(element._isSectionDone(state));
+
+ state = {sectionIndex: 1, lineIndex: 3};
+ assert.isTrue(element._isSectionDone(state));
+
+ state = {sectionIndex: 3, lineIndex: 0};
+ assert.isFalse(element._isSectionDone(state));
+
+ state = {sectionIndex: 3, lineIndex: 3};
+ assert.isFalse(element._isSectionDone(state));
+
+ state = {sectionIndex: 3, lineIndex: 4};
+ assert.isTrue(element._isSectionDone(state));
+ });
+
+ test('workaround CPP LT directive', () => {
+ // Does nothing to regular line.
+ let line = 'int main(int argc, char** argv) { return 0; }';
+ assert.equal(element._workaround('cpp', line), line);
+
+ // Does nothing to include directive.
+ line = '#include <stdio>';
+ assert.equal(element._workaround('cpp', line), line);
+
+ // Converts left-shift operator in #define.
+ line = '#define GiB (1ull << 30)';
+ let expected = '#define GiB (1ull || 30)';
+ assert.equal(element._workaround('cpp', line), expected);
+
+ // Converts less-than operator in #if.
+ line = ' #if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 1)';
+ expected = ' #if __GNUC__ | 4 || (__GNUC__ == 4 && __GNUC_MINOR__ | 1)';
+ assert.equal(element._workaround('cpp', line), expected);
+ });
+
+ test('workaround Java param-annotation', () => {
+ // Does nothing to regular line.
+ let line = 'public static void foo(int bar) { }';
+ assert.equal(element._workaround('java', line), line);
+
+ // Does nothing to regular annotation.
+ line = 'public static void foo(@Nullable int bar) { }';
+ assert.equal(element._workaround('java', line), line);
+
+ // Converts parameterized annotation.
+ line = 'public static void foo(@SuppressWarnings("unused") int bar) { }';
+ const expected = 'public static void foo(@SuppressWarnings "unused" ' +
+ ' int bar) { }';
+ assert.equal(element._workaround('java', line), expected);
+ });
+
+ test('workaround CPP whcar_t character literals', () => {
+ // Does nothing to regular line.
+ let line = 'int main(int argc, char** argv) { return 0; }';
+ assert.equal(element._workaround('cpp', line), line);
+
+ // Does nothing to wchar_t string.
+ line = 'wchar_t* sz = L"abc 123";';
+ assert.equal(element._workaround('cpp', line), line);
+
+ // Converts wchar_t character literal to string.
+ line = 'wchar_t myChar = L\'#\'';
+ let expected = 'wchar_t myChar = L"."';
+ assert.equal(element._workaround('cpp', line), expected);
+
+ // Converts wchar_t character literal with escape sequence to string.
+ line = 'wchar_t myChar = L\'\\"\'';
+ expected = 'wchar_t myChar = L"\\."';
+ assert.equal(element._workaround('cpp', line), expected);
+ });
+
+ test('workaround go backslash character literals', () => {
+ // Does nothing to regular line.
+ let line = 'func foo(in []byte) (lit []byte, n int, err error) {';
+ assert.equal(element._workaround('go', line), line);
+
+ // Does nothing to string with backslash literal
+ line = 'c := "\\\\"';
+ assert.equal(element._workaround('go', line), line);
+
+ // Converts backslash literal character to a string.
+ line = 'c := \'\\\\\'';
+ const expected = 'c := "\\\\"';
+ assert.equal(element._workaround('go', line), expected);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.js
similarity index 72%
rename from polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
rename to polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.js
index e5ae06d..76a01de 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.js
@@ -1,20 +1,22 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-<dom-module id="gr-syntax-theme">
+$_documentContainer.innerHTML = `<dom-module id="gr-syntax-theme">
<template>
<style>
/**
@@ -107,4 +109,13 @@
}
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.html b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.html
deleted file mode 100644
index 5ae679e..0000000
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.html
+++ /dev/null
@@ -1,65 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../styles/gr-table-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-documentation-search">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-table-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <gr-list-view
- filter="[[_filter]]"
- items=false
- offset=0
- loading="[[_loading]]"
- path="[[_path]]">
- <table id="list" class="genericList">
- <tr class="headerRow">
- <th class="name topHeader">Name</th>
- <th class="name topHeader"></th>
- <th class="name topHeader"></th>
- </tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
- <td>Loading...</td>
- </tr>
- <tbody class$="[[computeLoadingClass(_loading)]]">
- <template is="dom-repeat" items="[[_documentationSearches]]">
- <tr class="table">
- <td class="name">
- <a href$="[[_computeSearchUrl(item.url)]]">[[item.title]]</a>
- </td>
- <td></td>
- <td></td>
- </tr>
- </template>
- </tbody>
- </table>
- </gr-list-view>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-documentation-search.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
index 022a985..0a57883 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
@@ -14,78 +14,90 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.ListViewMixin
- * @extends Polymer.Element
- */
- class GrDocumentationSearch extends Polymer.mixinBehaviors( [
- Gerrit.ListViewBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-documentation-search'; }
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
+import '../../../styles/gr-table-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-list-view/gr-list-view.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-documentation-search_html.js';
- static get properties() {
- return {
- /**
- * URL params passed from the router.
- */
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
+/**
+ * @appliesMixin Gerrit.ListViewMixin
+ * @extends Polymer.Element
+ */
+class GrDocumentationSearch extends mixinBehaviors( [
+ Gerrit.ListViewBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _path: {
- type: String,
- readOnly: true,
- value: '/Documentation',
- },
- _documentationSearches: Array,
+ static get is() { return 'gr-documentation-search'; }
- _loading: {
- type: Boolean,
- value: true,
- },
- _filter: {
- type: String,
- value: '',
- },
- };
- }
+ static get properties() {
+ return {
+ /**
+ * URL params passed from the router.
+ */
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
- /** @override */
- attached() {
- super.attached();
- this.dispatchEvent(
- new CustomEvent('title-change', {title: 'Documentation Search'}));
- }
+ _path: {
+ type: String,
+ readOnly: true,
+ value: '/Documentation',
+ },
+ _documentationSearches: Array,
- _paramsChanged(params) {
- this._loading = true;
- this._filter = this.getFilterValue(params);
-
- return this._getDocumentationSearches(this._filter);
- }
-
- _getDocumentationSearches(filter) {
- this._documentationSearches = [];
- return this.$.restAPI.getDocumentationSearches(filter)
- .then(searches => {
- // Late response.
- if (filter !== this._filter || !searches) { return; }
- this._documentationSearches = searches;
- this._loading = false;
- });
- }
-
- _computeSearchUrl(url) {
- if (!url) { return ''; }
- return this.getBaseUrl() + '/' + url;
- }
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _filter: {
+ type: String,
+ value: '',
+ },
+ };
}
- customElements.define(GrDocumentationSearch.is, GrDocumentationSearch);
-})();
+ /** @override */
+ attached() {
+ super.attached();
+ this.dispatchEvent(
+ new CustomEvent('title-change', {title: 'Documentation Search'}));
+ }
+
+ _paramsChanged(params) {
+ this._loading = true;
+ this._filter = this.getFilterValue(params);
+
+ return this._getDocumentationSearches(this._filter);
+ }
+
+ _getDocumentationSearches(filter) {
+ this._documentationSearches = [];
+ return this.$.restAPI.getDocumentationSearches(filter)
+ .then(searches => {
+ // Late response.
+ if (filter !== this._filter || !searches) { return; }
+ this._documentationSearches = searches;
+ this._loading = false;
+ });
+ }
+
+ _computeSearchUrl(url) {
+ if (!url) { return ''; }
+ return this.getBaseUrl() + '/' + url;
+ }
+}
+
+customElements.define(GrDocumentationSearch.is, GrDocumentationSearch);
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.js b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.js
new file mode 100644
index 0000000..ced351a
--- /dev/null
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.js
@@ -0,0 +1,50 @@
+/**
+ * @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.js';
+
+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-table-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <gr-list-view filter="[[_filter]]" items="false" offset="0" loading="[[_loading]]" path="[[_path]]">
+ <table id="list" class="genericList">
+ <tbody><tr class="headerRow">
+ <th class="name topHeader">Name</th>
+ <th class="name topHeader"></th>
+ <th class="name topHeader"></th>
+ </tr>
+ <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]">
+ <td>Loading...</td>
+ </tr>
+ </tbody><tbody class\$="[[computeLoadingClass(_loading)]]">
+ <template is="dom-repeat" items="[[_documentationSearches]]">
+ <tr class="table">
+ <td class="name">
+ <a href\$="[[_computeSearchUrl(item.url)]]">[[item.title]]</a>
+ </td>
+ <td></td>
+ <td></td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ </gr-list-view>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html
index e9bf78d..1a238e8 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html
@@ -19,16 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-documentation-search</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-documentation-search.html">
-
-<script>void(0);</script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,90 +31,91 @@
</template>
</test-fixture>
-<script>
- let counter;
- const documentationGenerator = () => {
- return {
- title: `Gerrit Code Review - REST API Developers Notes${++counter}`,
- url: 'Documentation/dev-rest-api.html',
- };
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-documentation-search.js';
+let counter;
+const documentationGenerator = () => {
+ return {
+ title: `Gerrit Code Review - REST API Developers Notes${++counter}`,
+ url: 'Documentation/dev-rest-api.html',
};
+};
- suite('gr-documentation-search tests', async () => {
- await readyToTest();
- let element;
- let documentationSearches;
- let sandbox;
- let value;
+suite('gr-documentation-search tests', () => {
+ let element;
+ let documentationSearches;
+ let sandbox;
+ let value;
- setup(() => {
- sandbox = sinon.sandbox.create();
- sandbox.stub(page, 'show');
- element = fixture('basic');
- counter = 0;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(page, 'show');
+ element = fixture('basic');
+ counter = 0;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('list with searches for documentation', () => {
+ setup(done => {
+ documentationSearches = _.times(26, documentationGenerator);
+ stub('gr-rest-api-interface', {
+ getDocumentationSearches() {
+ return Promise.resolve(documentationSearches);
+ },
+ });
+ element._paramsChanged(value).then(() => { flush(done); });
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('list with searches for documentation', () => {
- setup(done => {
- documentationSearches = _.times(26, documentationGenerator);
- stub('gr-rest-api-interface', {
- getDocumentationSearches() {
- return Promise.resolve(documentationSearches);
- },
- });
- element._paramsChanged(value).then(() => { flush(done); });
- });
-
- test('test for test repo in the list', done => {
- flush(() => {
- assert.equal(element._documentationSearches[0].title,
- 'Gerrit Code Review - REST API Developers Notes1');
- assert.equal(element._documentationSearches[0].url,
- 'Documentation/dev-rest-api.html');
- done();
- });
- });
- });
-
- suite('filter', () => {
- setup(() => {
- documentationSearches = _.times(25, documentationGenerator);
- _.times(1, documentationSearches);
- });
-
- test('_paramsChanged', done => {
- sandbox.stub(
- element.$.restAPI,
- 'getDocumentationSearches',
- () => Promise.resolve(documentationSearches));
- const value = {
- filter: 'test',
- };
- element._paramsChanged(value).then(() => {
- assert.isTrue(element.$.restAPI.getDocumentationSearches.lastCall
- .calledWithExactly('test'));
- done();
- });
- });
- });
-
- suite('loading', () => {
- test('correct contents are displayed', () => {
- assert.isTrue(element._loading);
- assert.equal(element.computeLoadingClass(element._loading), 'loading');
- assert.equal(getComputedStyle(element.$.loading).display, 'block');
-
- element._loading = false;
- element._repos = _.times(25, documentationGenerator);
-
- flushAsynchronousOperations();
- assert.equal(element.computeLoadingClass(element._loading), '');
- assert.equal(getComputedStyle(element.$.loading).display, 'none');
+ test('test for test repo in the list', done => {
+ flush(() => {
+ assert.equal(element._documentationSearches[0].title,
+ 'Gerrit Code Review - REST API Developers Notes1');
+ assert.equal(element._documentationSearches[0].url,
+ 'Documentation/dev-rest-api.html');
+ done();
});
});
});
+
+ suite('filter', () => {
+ setup(() => {
+ documentationSearches = _.times(25, documentationGenerator);
+ _.times(1, documentationSearches);
+ });
+
+ test('_paramsChanged', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'getDocumentationSearches',
+ () => Promise.resolve(documentationSearches));
+ const value = {
+ filter: 'test',
+ };
+ element._paramsChanged(value).then(() => {
+ assert.isTrue(element.$.restAPI.getDocumentationSearches.lastCall
+ .calledWithExactly('test'));
+ done();
+ });
+ });
+ });
+
+ suite('loading', () => {
+ test('correct contents are displayed', () => {
+ assert.isTrue(element._loading);
+ assert.equal(element.computeLoadingClass(element._loading), 'loading');
+ assert.equal(getComputedStyle(element.$.loading).display, 'block');
+
+ element._loading = false;
+ element._repos = _.times(25, documentationGenerator);
+
+ flushAsynchronousOperations();
+ assert.equal(element.computeLoadingClass(element._loading), '');
+ assert.equal(getComputedStyle(element.$.loading).display, 'none');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.html b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.html
deleted file mode 100644
index 19a4e63..0000000
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.html
+++ /dev/null
@@ -1,45 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-default-editor">
- <template>
- <style include="shared-styles">
- textarea {
- border: none;
- box-sizing: border-box;
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-code);
- line-height: var(--line-height-code);
- min-height: 60vh;
- resize: none;
- white-space: pre;
- width: 100%;
- }
- textarea:focus {
- outline: none;
- }
- </style>
- <textarea
- id="textarea"
- value="[[fileContent]]"
- on-input="_handleTextareaInput"></textarea>
- </template>
- <script src="gr-default-editor.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
index 73dbaf8..09f4abf 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
@@ -14,32 +14,38 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrDefaultEditor extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-default-editor'; }
- /**
- * Fired when the content of the editor changes.
- *
- * @event content-change
- */
+import '../../../styles/shared-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-default-editor_html.js';
- static get properties() {
- return {
- fileContent: String,
- };
- }
+/** @extends Polymer.Element */
+class GrDefaultEditor extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _handleTextareaInput(e) {
- this.dispatchEvent(new CustomEvent(
- 'content-change',
- {detail: {value: e.target.value}, bubbles: true, composed: true}));
- }
+ static get is() { return 'gr-default-editor'; }
+ /**
+ * Fired when the content of the editor changes.
+ *
+ * @event content-change
+ */
+
+ static get properties() {
+ return {
+ fileContent: String,
+ };
}
- customElements.define(GrDefaultEditor.is, GrDefaultEditor);
-})();
+ _handleTextareaInput(e) {
+ this.dispatchEvent(new CustomEvent(
+ 'content-change',
+ {detail: {value: e.target.value}, bubbles: true, composed: true}));
+ }
+}
+
+customElements.define(GrDefaultEditor.is, GrDefaultEditor);
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_html.js b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_html.js
new file mode 100644
index 0000000..e7fc6fd
--- /dev/null
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_html.js
@@ -0,0 +1,37 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ textarea {
+ border: none;
+ box-sizing: border-box;
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-code);
+ line-height: var(--line-height-code);
+ min-height: 60vh;
+ resize: none;
+ white-space: pre;
+ width: 100%;
+ }
+ textarea:focus {
+ outline: none;
+ }
+ </style>
+ <textarea id="textarea" value="[[fileContent]]" on-input="_handleTextareaInput"></textarea>
+`;
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
index 228c70e..30cfe39 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
@@ -18,16 +18,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-default-editor</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-
-<link rel="import" href="gr-default-editor.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,26 +29,27 @@
</template>
</test-fixture>
-<script>
- suite('gr-default-editor tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-default-editor.js';
+suite('gr-default-editor tests', () => {
+ let element;
- setup(() => {
- element = fixture('basic');
- element.fileContent = '';
- });
-
- test('fires content-change event', done => {
- const contentChangedHandler = e => {
- assert.equal(e.detail.value, 'test');
- done();
- };
- const textarea = element.$.textarea;
- element.addEventListener('content-change', contentChangedHandler);
- textarea.value = 'test';
- textarea.dispatchEvent(new CustomEvent('input',
- {target: textarea, bubbles: true, composed: true}));
- });
+ setup(() => {
+ element = fixture('basic');
+ element.fileContent = '';
});
+
+ test('fires content-change event', done => {
+ const contentChangedHandler = e => {
+ assert.equal(e.detail.value, 'test');
+ done();
+ };
+ const textarea = element.$.textarea;
+ element.addEventListener('content-change', contentChangedHandler);
+ textarea.value = 'test';
+ textarea.dispatchEvent(new CustomEvent('input',
+ {target: textarea, bubbles: true, composed: true}));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-constants.html b/polygerrit-ui/app/elements/edit/gr-edit-constants.html
deleted file mode 100644
index 5895124..0000000
--- a/polygerrit-ui/app/elements/edit/gr-edit-constants.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<script>
- (function(window) {
- 'use strict';
-
- const GrEditConstants = window.GrEditConstants || {};
-
- // Order corresponds to order in the UI.
- GrEditConstants.Actions = {
- OPEN: {label: 'Add/Open', id: 'open'},
- DELETE: {label: 'Delete', id: 'delete'},
- RENAME: {label: 'Rename', id: 'rename'},
- RESTORE: {label: 'Restore', id: 'restore'},
- };
-
- window.GrEditConstants = GrEditConstants;
- })(window);
-</script>
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-constants.js b/polygerrit-ui/app/elements/edit/gr-edit-constants.js
new file mode 100644
index 0000000..2a929f2
--- /dev/null
+++ b/polygerrit-ui/app/elements/edit/gr-edit-constants.js
@@ -0,0 +1,31 @@
+/**
+ * @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.
+ */
+(function(window) {
+ 'use strict';
+
+ const GrEditConstants = window.GrEditConstants || {};
+
+ // Order corresponds to order in the UI.
+ GrEditConstants.Actions = {
+ OPEN: {label: 'Add/Open', id: 'open'},
+ DELETE: {label: 'Delete', id: 'delete'},
+ RENAME: {label: 'Rename', id: 'rename'},
+ RESTORE: {label: 'Restore', id: 'restore'},
+ };
+
+ window.GrEditConstants = GrEditConstants;
+})(window);
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
deleted file mode 100644
index cb950da..0000000
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
+++ /dev/null
@@ -1,163 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-edit-constants.html">
-
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-edit-controls">
- <template>
- <style include="shared-styles">
- :host {
- align-items: center;
- display: flex;
- justify-content: flex-end;
- }
- .invisible {
- display: none;
- }
- gr-button {
- margin-left: var(--spacing-l);
- text-decoration: none;
- }
- gr-dialog {
- width: 50em;
- }
- gr-dialog .main {
- width: 100%;
- }
- gr-dialog .main > iron-input{
- width: 100%;
- }
- input {
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius);
- margin: var(--spacing-m) 0;
- padding: var(--spacing-s);
- width: 100%;
- box-sizing: content-box;
- }
- @media screen and (max-width: 50em) {
- gr-dialog {
- width: 100vw;
- }
- }
- </style>
- <template is="dom-repeat" items="[[_actions]]" as="action">
- <gr-button
- id$="[[action.id]]"
- class$="[[_computeIsInvisible(action.id, hiddenActions)]]"
- link
- on-click="_handleTap">[[action.label]]</gr-button>
- </template>
- <gr-overlay id="overlay" with-backdrop>
- <gr-dialog
- id="openDialog"
- class="invisible dialog"
- disabled$="[[!_isValidPath(_path)]]"
- confirm-label="Confirm"
- confirm-on-enter
- on-confirm="_handleOpenConfirm"
- on-cancel="_handleDialogCancel">
- <div class="header" slot="header">
- Add a new file or open an existing file
- </div>
- <div class="main" slot="main">
- <gr-autocomplete
- placeholder="Enter an existing or new full file path."
- query="[[_query]]"
- text="{{_path}}"></gr-autocomplete>
- </div>
- </gr-dialog>
- <gr-dialog
- id="deleteDialog"
- class="invisible dialog"
- disabled$="[[!_isValidPath(_path)]]"
- confirm-label="Delete"
- confirm-on-enter
- on-confirm="_handleDeleteConfirm"
- on-cancel="_handleDialogCancel">
- <div class="header" slot="header">Delete a file from the repo</div>
- <div class="main" slot="main">
- <gr-autocomplete
- placeholder="Enter an existing full file path."
- query="[[_query]]"
- text="{{_path}}"></gr-autocomplete>
- </div>
- </gr-dialog>
- <gr-dialog
- id="renameDialog"
- class="invisible dialog"
- disabled$="[[!_computeRenameDisabled(_path, _newPath)]]"
- confirm-label="Rename"
- confirm-on-enter
- on-confirm="_handleRenameConfirm"
- on-cancel="_handleDialogCancel">
- <div class="header" slot="header">Rename a file in the repo</div>
- <div class="main" slot="main">
- <gr-autocomplete
- placeholder="Enter an existing full file path."
- query="[[_query]]"
- text="{{_path}}"></gr-autocomplete>
- <iron-input
- class="newPathIronInput"
- bind-value="{{_newPath}}"
- placeholder="Enter the new path.">
- <input
- class="newPathInput"
- is="iron-input"
- bind-value="{{_newPath}}"
- placeholder="Enter the new path.">
- </iron-input>
- </div>
- </gr-dialog>
- <gr-dialog
- id="restoreDialog"
- class="invisible dialog"
- confirm-label="Restore"
- confirm-on-enter
- on-confirm="_handleRestoreConfirm"
- on-cancel="_handleDialogCancel">
- <div class="header" slot="header">Restore this file?</div>
- <div class="main" slot="main">
- <iron-input
- disabled
- bind-value="{{_path}}">
- <input
- is="iron-input"
- disabled
- bind-value="{{_path}}">
- </iron-input>
- </div>
- </gr-dialog>
- </gr-overlay>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-edit-controls.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
index e655f7b..e17fe03 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
@@ -14,231 +14,250 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.PatchSetMixin
- * @extends Polymer.Element
- */
- class GrEditControls extends Polymer.mixinBehaviors( [
- Gerrit.PatchSetBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-edit-controls'; }
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-dropdown/gr-dropdown.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-edit-constants.js';
+import '../../../styles/shared-styles.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-edit-controls_html.js';
- static get properties() {
- return {
- change: Object,
- patchNum: String,
+/**
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @extends Polymer.Element
+ */
+class GrEditControls extends mixinBehaviors( [
+ Gerrit.PatchSetBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /**
- * TODO(kaspern): by default, the RESTORE action should be hidden in the
- * file-list as it is a per-file action only. Remove this default value
- * when the Actions dictionary is moved to a shared constants file and
- * use the hiddenActions property in the parent component.
- */
- hiddenActions: {
- type: Array,
- value() { return [GrEditConstants.Actions.RESTORE.id]; },
+ static get is() { return 'gr-edit-controls'; }
+
+ static get properties() {
+ return {
+ change: Object,
+ patchNum: String,
+
+ /**
+ * TODO(kaspern): by default, the RESTORE action should be hidden in the
+ * file-list as it is a per-file action only. Remove this default value
+ * when the Actions dictionary is moved to a shared constants file and
+ * use the hiddenActions property in the parent component.
+ */
+ hiddenActions: {
+ type: Array,
+ value() { return [GrEditConstants.Actions.RESTORE.id]; },
+ },
+
+ _actions: {
+ type: Array,
+ value() { return Object.values(GrEditConstants.Actions); },
+ },
+ _path: {
+ type: String,
+ value: '',
+ },
+ _newPath: {
+ type: String,
+ value: '',
+ },
+ _query: {
+ type: Function,
+ value() {
+ return this._queryFiles.bind(this);
},
+ },
+ };
+ }
- _actions: {
- type: Array,
- value() { return Object.values(GrEditConstants.Actions); },
- },
- _path: {
- type: String,
- value: '',
- },
- _newPath: {
- type: String,
- value: '',
- },
- _query: {
- type: Function,
- value() {
- return this._queryFiles.bind(this);
- },
- },
- };
- }
-
- _handleTap(e) {
- e.preventDefault();
- const action = Polymer.dom(e).localTarget.id;
- switch (action) {
- case GrEditConstants.Actions.OPEN.id:
- this.openOpenDialog();
- return;
- case GrEditConstants.Actions.DELETE.id:
- this.openDeleteDialog();
- return;
- case GrEditConstants.Actions.RENAME.id:
- this.openRenameDialog();
- return;
- case GrEditConstants.Actions.RESTORE.id:
- this.openRestoreDialog();
- return;
- }
- }
-
- /**
- * @param {string=} opt_path
- */
- openOpenDialog(opt_path) {
- if (opt_path) { this._path = opt_path; }
- return this._showDialog(this.$.openDialog);
- }
-
- /**
- * @param {string=} opt_path
- */
- openDeleteDialog(opt_path) {
- if (opt_path) { this._path = opt_path; }
- return this._showDialog(this.$.deleteDialog);
- }
-
- /**
- * @param {string=} opt_path
- */
- openRenameDialog(opt_path) {
- if (opt_path) { this._path = opt_path; }
- return this._showDialog(this.$.renameDialog);
- }
-
- /**
- * @param {string=} opt_path
- */
- openRestoreDialog(opt_path) {
- if (opt_path) { this._path = opt_path; }
- return this._showDialog(this.$.restoreDialog);
- }
-
- /**
- * Given a path string, checks that it is a valid file path.
- *
- * @param {string} path
- * @return {boolean}
- */
- _isValidPath(path) {
- // Double negation needed for strict boolean return type.
- return !!path.length && !path.endsWith('/');
- }
-
- _computeRenameDisabled(path, newPath) {
- return this._isValidPath(path) && this._isValidPath(newPath);
- }
-
- /**
- * Given a dom event, gets the dialog that lies along this event path.
- *
- * @param {!Event} e
- * @return {!Element|undefined}
- */
- _getDialogFromEvent(e) {
- return Polymer.dom(e).path.find(element => {
- if (!element.classList) { return false; }
- return element.classList.contains('dialog');
- });
- }
-
- _showDialog(dialog) {
- // Some dialogs may not fire their on-close event when closed in certain
- // ways (e.g. by clicking outside the dialog body). This call prevents
- // multiple dialogs from being shown in the same overlay.
- this._hideAllDialogs();
-
- return this.$.overlay.open().then(() => {
- dialog.classList.toggle('invisible', false);
- const autocomplete = dialog.querySelector('gr-autocomplete');
- if (autocomplete) { autocomplete.focus(); }
- this.async(() => { this.$.overlay.center(); }, 1);
- });
- }
-
- _hideAllDialogs() {
- const dialogs = Polymer.dom(this.root).querySelectorAll('.dialog');
- for (const dialog of dialogs) { this._closeDialog(dialog); }
- }
-
- /**
- * @param {Element|undefined} dialog
- * @param {boolean=} clearInputs
- */
- _closeDialog(dialog, clearInputs) {
- if (!dialog) { return; }
-
- if (clearInputs) {
- // Dialog may have autocompletes and plain inputs -- as these have
- // different properties representing their bound text, it is easier to
- // just make two separate queries.
- dialog.querySelectorAll('gr-autocomplete')
- .forEach(input => { input.text = ''; });
-
- dialog.querySelectorAll('iron-input')
- .forEach(input => { input.bindValue = ''; });
- }
-
- dialog.classList.toggle('invisible', true);
- return this.$.overlay.close();
- }
-
- _handleDialogCancel(e) {
- this._closeDialog(this._getDialogFromEvent(e));
- }
-
- _handleOpenConfirm(e) {
- const url = Gerrit.Nav.getEditUrlForDiff(this.change, this._path,
- this.patchNum);
- Gerrit.Nav.navigateToRelativeUrl(url);
- this._closeDialog(this._getDialogFromEvent(e), true);
- }
-
- _handleDeleteConfirm(e) {
- // Get the dialog before the api call as the event will change during bubbling
- // which will make Polymer.dom(e).path an emtpy array in polymer 2
- const dialog = this._getDialogFromEvent(e);
- this.$.restAPI.deleteFileInChangeEdit(this.change._number, this._path)
- .then(res => {
- if (!res.ok) { return; }
- this._closeDialog(dialog, true);
- Gerrit.Nav.navigateToChange(this.change);
- });
- }
-
- _handleRestoreConfirm(e) {
- const dialog = this._getDialogFromEvent(e);
- this.$.restAPI.restoreFileInChangeEdit(this.change._number, this._path)
- .then(res => {
- if (!res.ok) { return; }
- this._closeDialog(dialog, true);
- Gerrit.Nav.navigateToChange(this.change);
- });
- }
-
- _handleRenameConfirm(e) {
- const dialog = this._getDialogFromEvent(e);
- return this.$.restAPI.renameFileInChangeEdit(this.change._number,
- this._path, this._newPath).then(res => {
- if (!res.ok) { return; }
- this._closeDialog(dialog, true);
- Gerrit.Nav.navigateToChange(this.change);
- });
- }
-
- _queryFiles(input) {
- return this.$.restAPI.queryChangeFiles(this.change._number,
- this.patchNum, input).then(res => res.map(file => {
- return {name: file};
- }));
- }
-
- _computeIsInvisible(id, hiddenActions) {
- return hiddenActions.includes(id) ? 'invisible' : '';
+ _handleTap(e) {
+ e.preventDefault();
+ const action = dom(e).localTarget.id;
+ switch (action) {
+ case GrEditConstants.Actions.OPEN.id:
+ this.openOpenDialog();
+ return;
+ case GrEditConstants.Actions.DELETE.id:
+ this.openDeleteDialog();
+ return;
+ case GrEditConstants.Actions.RENAME.id:
+ this.openRenameDialog();
+ return;
+ case GrEditConstants.Actions.RESTORE.id:
+ this.openRestoreDialog();
+ return;
}
}
- customElements.define(GrEditControls.is, GrEditControls);
-})();
+ /**
+ * @param {string=} opt_path
+ */
+ openOpenDialog(opt_path) {
+ if (opt_path) { this._path = opt_path; }
+ return this._showDialog(this.$.openDialog);
+ }
+
+ /**
+ * @param {string=} opt_path
+ */
+ openDeleteDialog(opt_path) {
+ if (opt_path) { this._path = opt_path; }
+ return this._showDialog(this.$.deleteDialog);
+ }
+
+ /**
+ * @param {string=} opt_path
+ */
+ openRenameDialog(opt_path) {
+ if (opt_path) { this._path = opt_path; }
+ return this._showDialog(this.$.renameDialog);
+ }
+
+ /**
+ * @param {string=} opt_path
+ */
+ openRestoreDialog(opt_path) {
+ if (opt_path) { this._path = opt_path; }
+ return this._showDialog(this.$.restoreDialog);
+ }
+
+ /**
+ * Given a path string, checks that it is a valid file path.
+ *
+ * @param {string} path
+ * @return {boolean}
+ */
+ _isValidPath(path) {
+ // Double negation needed for strict boolean return type.
+ return !!path.length && !path.endsWith('/');
+ }
+
+ _computeRenameDisabled(path, newPath) {
+ return this._isValidPath(path) && this._isValidPath(newPath);
+ }
+
+ /**
+ * Given a dom event, gets the dialog that lies along this event path.
+ *
+ * @param {!Event} e
+ * @return {!Element|undefined}
+ */
+ _getDialogFromEvent(e) {
+ return dom(e).path.find(element => {
+ if (!element.classList) { return false; }
+ return element.classList.contains('dialog');
+ });
+ }
+
+ _showDialog(dialog) {
+ // Some dialogs may not fire their on-close event when closed in certain
+ // ways (e.g. by clicking outside the dialog body). This call prevents
+ // multiple dialogs from being shown in the same overlay.
+ this._hideAllDialogs();
+
+ return this.$.overlay.open().then(() => {
+ dialog.classList.toggle('invisible', false);
+ const autocomplete = dialog.querySelector('gr-autocomplete');
+ if (autocomplete) { autocomplete.focus(); }
+ this.async(() => { this.$.overlay.center(); }, 1);
+ });
+ }
+
+ _hideAllDialogs() {
+ const dialogs = dom(this.root).querySelectorAll('.dialog');
+ for (const dialog of dialogs) { this._closeDialog(dialog); }
+ }
+
+ /**
+ * @param {Element|undefined} dialog
+ * @param {boolean=} clearInputs
+ */
+ _closeDialog(dialog, clearInputs) {
+ if (!dialog) { return; }
+
+ if (clearInputs) {
+ // Dialog may have autocompletes and plain inputs -- as these have
+ // different properties representing their bound text, it is easier to
+ // just make two separate queries.
+ dialog.querySelectorAll('gr-autocomplete')
+ .forEach(input => { input.text = ''; });
+
+ dialog.querySelectorAll('iron-input')
+ .forEach(input => { input.bindValue = ''; });
+ }
+
+ dialog.classList.toggle('invisible', true);
+ return this.$.overlay.close();
+ }
+
+ _handleDialogCancel(e) {
+ this._closeDialog(this._getDialogFromEvent(e));
+ }
+
+ _handleOpenConfirm(e) {
+ const url = Gerrit.Nav.getEditUrlForDiff(this.change, this._path,
+ this.patchNum);
+ Gerrit.Nav.navigateToRelativeUrl(url);
+ this._closeDialog(this._getDialogFromEvent(e), true);
+ }
+
+ _handleDeleteConfirm(e) {
+ // Get the dialog before the api call as the event will change during bubbling
+ // which will make Polymer.dom(e).path an emtpy array in polymer 2
+ const dialog = this._getDialogFromEvent(e);
+ this.$.restAPI.deleteFileInChangeEdit(this.change._number, this._path)
+ .then(res => {
+ if (!res.ok) { return; }
+ this._closeDialog(dialog, true);
+ Gerrit.Nav.navigateToChange(this.change);
+ });
+ }
+
+ _handleRestoreConfirm(e) {
+ const dialog = this._getDialogFromEvent(e);
+ this.$.restAPI.restoreFileInChangeEdit(this.change._number, this._path)
+ .then(res => {
+ if (!res.ok) { return; }
+ this._closeDialog(dialog, true);
+ Gerrit.Nav.navigateToChange(this.change);
+ });
+ }
+
+ _handleRenameConfirm(e) {
+ const dialog = this._getDialogFromEvent(e);
+ return this.$.restAPI.renameFileInChangeEdit(this.change._number,
+ this._path, this._newPath).then(res => {
+ if (!res.ok) { return; }
+ this._closeDialog(dialog, true);
+ Gerrit.Nav.navigateToChange(this.change);
+ });
+ }
+
+ _queryFiles(input) {
+ return this.$.restAPI.queryChangeFiles(this.change._number,
+ this.patchNum, input).then(res => res.map(file => {
+ return {name: file};
+ }));
+ }
+
+ _computeIsInvisible(id, hiddenActions) {
+ return hiddenActions.includes(id) ? 'invisible' : '';
+ }
+}
+
+customElements.define(GrEditControls.is, GrEditControls);
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_html.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_html.js
new file mode 100644
index 0000000..2d09069
--- /dev/null
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_html.js
@@ -0,0 +1,93 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ align-items: center;
+ display: flex;
+ justify-content: flex-end;
+ }
+ .invisible {
+ display: none;
+ }
+ gr-button {
+ margin-left: var(--spacing-l);
+ text-decoration: none;
+ }
+ gr-dialog {
+ width: 50em;
+ }
+ gr-dialog .main {
+ width: 100%;
+ }
+ gr-dialog .main > iron-input{
+ width: 100%;
+ }
+ input {
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ margin: var(--spacing-m) 0;
+ padding: var(--spacing-s);
+ width: 100%;
+ box-sizing: content-box;
+ }
+ @media screen and (max-width: 50em) {
+ gr-dialog {
+ width: 100vw;
+ }
+ }
+ </style>
+ <template is="dom-repeat" items="[[_actions]]" as="action">
+ <gr-button id\$="[[action.id]]" class\$="[[_computeIsInvisible(action.id, hiddenActions)]]" link="" on-click="_handleTap">[[action.label]]</gr-button>
+ </template>
+ <gr-overlay id="overlay" with-backdrop="">
+ <gr-dialog id="openDialog" class="invisible dialog" disabled\$="[[!_isValidPath(_path)]]" confirm-label="Confirm" confirm-on-enter="" on-confirm="_handleOpenConfirm" on-cancel="_handleDialogCancel">
+ <div class="header" slot="header">
+ Add a new file or open an existing file
+ </div>
+ <div class="main" slot="main">
+ <gr-autocomplete placeholder="Enter an existing or new full file path." query="[[_query]]" text="{{_path}}"></gr-autocomplete>
+ </div>
+ </gr-dialog>
+ <gr-dialog id="deleteDialog" class="invisible dialog" disabled\$="[[!_isValidPath(_path)]]" confirm-label="Delete" confirm-on-enter="" on-confirm="_handleDeleteConfirm" on-cancel="_handleDialogCancel">
+ <div class="header" slot="header">Delete a file from the repo</div>
+ <div class="main" slot="main">
+ <gr-autocomplete placeholder="Enter an existing full file path." query="[[_query]]" text="{{_path}}"></gr-autocomplete>
+ </div>
+ </gr-dialog>
+ <gr-dialog id="renameDialog" class="invisible dialog" disabled\$="[[!_computeRenameDisabled(_path, _newPath)]]" confirm-label="Rename" confirm-on-enter="" on-confirm="_handleRenameConfirm" on-cancel="_handleDialogCancel">
+ <div class="header" slot="header">Rename a file in the repo</div>
+ <div class="main" slot="main">
+ <gr-autocomplete placeholder="Enter an existing full file path." query="[[_query]]" text="{{_path}}"></gr-autocomplete>
+ <iron-input class="newPathIronInput" bind-value="{{_newPath}}" placeholder="Enter the new path.">
+ <input class="newPathInput" is="iron-input" bind-value="{{_newPath}}" placeholder="Enter the new path.">
+ </iron-input>
+ </div>
+ </gr-dialog>
+ <gr-dialog id="restoreDialog" class="invisible dialog" confirm-label="Restore" confirm-on-enter="" on-confirm="_handleRestoreConfirm" on-cancel="_handleDialogCancel">
+ <div class="header" slot="header">Restore this file?</div>
+ <div class="main" slot="main">
+ <iron-input disabled="" bind-value="{{_path}}">
+ <input is="iron-input" disabled="" bind-value="{{_path}}">
+ </iron-input>
+ </div>
+ </gr-dialog>
+ </gr-overlay>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
index 80de093..dc019c3 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
@@ -18,16 +18,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-edit-controls</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-
-<link rel="import" href="gr-edit-controls.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,351 +29,354 @@
</template>
</test-fixture>
-<script>
- suite('gr-edit-controls tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let showDialogSpy;
- let closeDialogSpy;
- let queryStub;
-
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-edit-controls.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+suite('gr-edit-controls tests', () => {
+ let element;
+ let sandbox;
+ let showDialogSpy;
+ let closeDialogSpy;
+ let queryStub;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.change = {_number: '42'};
+ showDialogSpy = sandbox.spy(element, '_showDialog');
+ closeDialogSpy = sandbox.spy(element, '_closeDialog');
+ sandbox.stub(element, '_hideAllDialogs');
+ queryStub = sandbox.stub(element.$.restAPI, 'queryChangeFiles')
+ .returns(Promise.resolve([]));
+ flushAsynchronousOperations();
+ });
+
+ teardown(() => { sandbox.restore(); });
+
+ test('all actions exist', () => {
+ assert.equal(dom(element.root).querySelectorAll('gr-button').length,
+ element._actions.length);
+ });
+
+ suite('edit button CUJ', () => {
+ let navStubs;
+ let openAutoCcmplete;
+
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.change = {_number: '42'};
- showDialogSpy = sandbox.spy(element, '_showDialog');
- closeDialogSpy = sandbox.spy(element, '_closeDialog');
- sandbox.stub(element, '_hideAllDialogs');
- queryStub = sandbox.stub(element.$.restAPI, 'queryChangeFiles')
- .returns(Promise.resolve([]));
- flushAsynchronousOperations();
+ navStubs = [
+ sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff'),
+ sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl'),
+ ];
+ openAutoCcmplete = element.$.openDialog.querySelector('gr-autocomplete');
});
-
- teardown(() => { sandbox.restore(); });
-
- test('all actions exist', () => {
- assert.equal(Polymer.dom(element.root).querySelectorAll('gr-button').length,
- element._actions.length);
+
+ test('_isValidPath', () => {
+ assert.isFalse(element._isValidPath(''));
+ assert.isFalse(element._isValidPath('test/'));
+ assert.isFalse(element._isValidPath('/'));
+ assert.isTrue(element._isValidPath('test/path.cpp'));
+ assert.isTrue(element._isValidPath('test.js'));
});
-
- suite('edit button CUJ', () => {
- let navStubs;
- let openAutoCcmplete;
-
- setup(() => {
- navStubs = [
- sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff'),
- sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl'),
- ];
- openAutoCcmplete = element.$.openDialog.querySelector('gr-autocomplete');
- });
-
- test('_isValidPath', () => {
- assert.isFalse(element._isValidPath(''));
- assert.isFalse(element._isValidPath('test/'));
- assert.isFalse(element._isValidPath('/'));
- assert.isTrue(element._isValidPath('test/path.cpp'));
- assert.isTrue(element._isValidPath('test.js'));
- });
-
- test('open', () => {
- MockInteractions.tap(element.shadowRoot.querySelector('#open'));
- element.patchNum = 1;
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element._hideAllDialogs.called);
- assert.isTrue(element.$.openDialog.disabled);
- assert.isFalse(queryStub.called);
- openAutoCcmplete.noDebounce = true;
- openAutoCcmplete.text = 'src/test.cpp';
- assert.isTrue(queryStub.called);
- assert.isFalse(element.$.openDialog.disabled);
- MockInteractions.tap(element.$.openDialog.shadowRoot
- .querySelector('gr-button[primary]'));
- for (const stub of navStubs) { assert.isTrue(stub.called); }
- assert.deepEqual(Gerrit.Nav.getEditUrlForDiff.lastCall.args,
- [element.change, 'src/test.cpp', element.patchNum]);
- assert.isTrue(closeDialogSpy.called);
- });
- });
-
- test('cancel', () => {
- MockInteractions.tap(element.shadowRoot.querySelector('#open'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.openDialog.disabled);
- openAutoCcmplete.noDebounce = true;
- openAutoCcmplete.text = 'src/test.cpp';
- assert.isFalse(element.$.openDialog.disabled);
- MockInteractions.tap(element.$.openDialog.shadowRoot
- .querySelector('gr-button'));
- for (const stub of navStubs) { assert.isFalse(stub.called); }
- assert.isTrue(closeDialogSpy.called);
- assert.equal(element._path, 'src/test.cpp');
- });
+
+ test('open', () => {
+ MockInteractions.tap(element.shadowRoot.querySelector('#open'));
+ element.patchNum = 1;
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(element._hideAllDialogs.called);
+ assert.isTrue(element.$.openDialog.disabled);
+ assert.isFalse(queryStub.called);
+ openAutoCcmplete.noDebounce = true;
+ openAutoCcmplete.text = 'src/test.cpp';
+ assert.isTrue(queryStub.called);
+ assert.isFalse(element.$.openDialog.disabled);
+ MockInteractions.tap(element.$.openDialog.shadowRoot
+ .querySelector('gr-button[primary]'));
+ for (const stub of navStubs) { assert.isTrue(stub.called); }
+ assert.deepEqual(Gerrit.Nav.getEditUrlForDiff.lastCall.args,
+ [element.change, 'src/test.cpp', element.patchNum]);
+ assert.isTrue(closeDialogSpy.called);
});
});
-
- suite('delete button CUJ', () => {
- let navStub;
- let deleteStub;
- let deleteAutocomplete;
-
- setup(() => {
- navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
- deleteStub = sandbox.stub(element.$.restAPI, 'deleteFileInChangeEdit');
- deleteAutocomplete =
- element.$.deleteDialog.querySelector('gr-autocomplete');
+
+ test('cancel', () => {
+ MockInteractions.tap(element.shadowRoot.querySelector('#open'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(element.$.openDialog.disabled);
+ openAutoCcmplete.noDebounce = true;
+ openAutoCcmplete.text = 'src/test.cpp';
+ assert.isFalse(element.$.openDialog.disabled);
+ MockInteractions.tap(element.$.openDialog.shadowRoot
+ .querySelector('gr-button'));
+ for (const stub of navStubs) { assert.isFalse(stub.called); }
+ assert.isTrue(closeDialogSpy.called);
+ assert.equal(element._path, 'src/test.cpp');
});
-
- test('delete', () => {
- deleteStub.returns(Promise.resolve({ok: true}));
- MockInteractions.tap(element.shadowRoot.querySelector('#delete'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.deleteDialog.disabled);
- assert.isFalse(queryStub.called);
- deleteAutocomplete.noDebounce = true;
- deleteAutocomplete.text = 'src/test.cpp';
- assert.isTrue(queryStub.called);
- assert.isFalse(element.$.deleteDialog.disabled);
- MockInteractions.tap(element.$.deleteDialog.shadowRoot
- .querySelector('gr-button[primary]'));
- flushAsynchronousOperations();
-
- assert.isTrue(deleteStub.called);
-
- return deleteStub.lastCall.returnValue.then(() => {
- assert.equal(element._path, '');
- assert.isTrue(navStub.called);
- assert.isTrue(closeDialogSpy.called);
- });
- });
- });
-
- test('delete fails', () => {
- deleteStub.returns(Promise.resolve({ok: false}));
- MockInteractions.tap(element.shadowRoot.querySelector('#delete'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.deleteDialog.disabled);
- assert.isFalse(queryStub.called);
- deleteAutocomplete.noDebounce = true;
- deleteAutocomplete.text = 'src/test.cpp';
- assert.isTrue(queryStub.called);
- assert.isFalse(element.$.deleteDialog.disabled);
- MockInteractions.tap(element.$.deleteDialog.shadowRoot
- .querySelector('gr-button[primary]'));
- flushAsynchronousOperations();
-
- assert.isTrue(deleteStub.called);
-
- return deleteStub.lastCall.returnValue.then(() => {
- assert.isFalse(navStub.called);
- assert.isFalse(closeDialogSpy.called);
- });
- });
- });
-
- test('cancel', () => {
- MockInteractions.tap(element.shadowRoot.querySelector('#delete'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.deleteDialog.disabled);
- element.$.deleteDialog.querySelector('gr-autocomplete').text =
- 'src/test.cpp';
- assert.isFalse(element.$.deleteDialog.disabled);
- MockInteractions.tap(element.$.deleteDialog.shadowRoot
- .querySelector('gr-button'));
- assert.isFalse(navStub.called);
- assert.isTrue(closeDialogSpy.called);
- assert.equal(element._path, 'src/test.cpp');
- });
- });
- });
-
- suite('rename button CUJ', () => {
- let navStub;
- let renameStub;
- let renameAutocomplete;
- const inputSelector = Polymer.Element ?
- '.newPathIronInput' :
- '.newPathInput';
-
- setup(() => {
- navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
- renameStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit');
- renameAutocomplete =
- element.$.renameDialog.querySelector('gr-autocomplete');
- });
-
- test('rename', () => {
- renameStub.returns(Promise.resolve({ok: true}));
- MockInteractions.tap(element.shadowRoot.querySelector('#rename'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.renameDialog.disabled);
- assert.isFalse(queryStub.called);
- renameAutocomplete.noDebounce = true;
- renameAutocomplete.text = 'src/test.cpp';
- assert.isTrue(queryStub.called);
- assert.isTrue(element.$.renameDialog.disabled);
-
- element.$.renameDialog.querySelector(inputSelector).bindValue =
- 'src/test.newPath';
-
- assert.isFalse(element.$.renameDialog.disabled);
- MockInteractions.tap(element.$.renameDialog.shadowRoot
- .querySelector('gr-button[primary]'));
- flushAsynchronousOperations();
-
- assert.isTrue(renameStub.called);
-
- return renameStub.lastCall.returnValue.then(() => {
- assert.equal(element._path, '');
- assert.isTrue(navStub.called);
- assert.isTrue(closeDialogSpy.called);
- });
- });
- });
-
- test('rename fails', () => {
- renameStub.returns(Promise.resolve({ok: false}));
- MockInteractions.tap(element.shadowRoot.querySelector('#rename'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.renameDialog.disabled);
- assert.isFalse(queryStub.called);
- renameAutocomplete.noDebounce = true;
- renameAutocomplete.text = 'src/test.cpp';
- assert.isTrue(queryStub.called);
- assert.isTrue(element.$.renameDialog.disabled);
-
- element.$.renameDialog.querySelector(inputSelector).bindValue =
- 'src/test.newPath';
-
- assert.isFalse(element.$.renameDialog.disabled);
- MockInteractions.tap(element.$.renameDialog.shadowRoot
- .querySelector('gr-button[primary]'));
- flushAsynchronousOperations();
-
- assert.isTrue(renameStub.called);
-
- return renameStub.lastCall.returnValue.then(() => {
- assert.isFalse(navStub.called);
- assert.isFalse(closeDialogSpy.called);
- });
- });
- });
-
- test('cancel', () => {
- MockInteractions.tap(element.shadowRoot.querySelector('#rename'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.renameDialog.disabled);
- element.$.renameDialog.querySelector('gr-autocomplete').text =
- 'src/test.cpp';
- element.$.renameDialog.querySelector(inputSelector).bindValue =
- 'src/test.newPath';
- assert.isFalse(element.$.renameDialog.disabled);
- MockInteractions.tap(element.$.renameDialog.shadowRoot
- .querySelector('gr-button'));
- assert.isFalse(navStub.called);
- assert.isTrue(closeDialogSpy.called);
- assert.equal(element._path, 'src/test.cpp');
- assert.equal(element._newPath, 'src/test.newPath');
- });
- });
- });
-
- suite('restore button CUJ', () => {
- let navStub;
- let restoreStub;
-
- setup(() => {
- navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
- restoreStub = sandbox.stub(element.$.restAPI, 'restoreFileInChangeEdit');
- });
-
- test('restore hidden by default', () => {
- assert.isTrue(element.shadowRoot
- .querySelector('#restore').classList.contains('invisible'));
- });
-
- test('restore', () => {
- restoreStub.returns(Promise.resolve({ok: true}));
- element._path = 'src/test.cpp';
- MockInteractions.tap(element.shadowRoot.querySelector('#restore'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- MockInteractions.tap(element.$.restoreDialog.shadowRoot
- .querySelector('gr-button[primary]'));
- flushAsynchronousOperations();
-
- assert.isTrue(restoreStub.called);
- assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
- return restoreStub.lastCall.returnValue.then(() => {
- assert.equal(element._path, '');
- assert.isTrue(navStub.called);
- assert.isTrue(closeDialogSpy.called);
- });
- });
- });
-
- test('restore fails', () => {
- restoreStub.returns(Promise.resolve({ok: false}));
- element._path = 'src/test.cpp';
- MockInteractions.tap(element.shadowRoot.querySelector('#restore'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- MockInteractions.tap(element.$.restoreDialog.shadowRoot
- .querySelector('gr-button[primary]'));
- flushAsynchronousOperations();
-
- assert.isTrue(restoreStub.called);
- assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
- return restoreStub.lastCall.returnValue.then(() => {
- assert.isFalse(navStub.called);
- assert.isFalse(closeDialogSpy.called);
- });
- });
- });
-
- test('cancel', () => {
- element._path = 'src/test.cpp';
- MockInteractions.tap(element.shadowRoot.querySelector('#restore'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- MockInteractions.tap(element.$.restoreDialog.shadowRoot
- .querySelector('gr-button'));
- assert.isFalse(navStub.called);
- assert.isTrue(closeDialogSpy.called);
- assert.equal(element._path, 'src/test.cpp');
- });
- });
- });
-
- test('openOpenDialog', done => {
- element.openOpenDialog('test/path.cpp')
- .then(() => {
- assert.isFalse(element.$.openDialog.hasAttribute('hidden'));
- assert.equal(
- element.$.openDialog.querySelector('gr-autocomplete').text,
- 'test/path.cpp');
- done();
- });
- });
-
- test('_getDialogFromEvent', () => {
- const spy = sandbox.spy(element, '_getDialogFromEvent');
- element.addEventListener('tap', element._getDialogFromEvent);
-
- MockInteractions.tap(element.$.openDialog);
- flushAsynchronousOperations();
- assert.equal(spy.lastCall.returnValue.id, 'openDialog');
-
- MockInteractions.tap(element.$.deleteDialog);
- flushAsynchronousOperations();
- assert.equal(spy.lastCall.returnValue.id, 'deleteDialog');
-
- MockInteractions.tap(
- element.$.deleteDialog.querySelector('gr-autocomplete'));
- flushAsynchronousOperations();
- assert.equal(spy.lastCall.returnValue.id, 'deleteDialog');
-
- MockInteractions.tap(element);
- flushAsynchronousOperations();
- assert.notOk(spy.lastCall.returnValue);
});
});
+
+ suite('delete button CUJ', () => {
+ let navStub;
+ let deleteStub;
+ let deleteAutocomplete;
+
+ setup(() => {
+ navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ deleteStub = sandbox.stub(element.$.restAPI, 'deleteFileInChangeEdit');
+ deleteAutocomplete =
+ element.$.deleteDialog.querySelector('gr-autocomplete');
+ });
+
+ test('delete', () => {
+ deleteStub.returns(Promise.resolve({ok: true}));
+ MockInteractions.tap(element.shadowRoot.querySelector('#delete'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(element.$.deleteDialog.disabled);
+ assert.isFalse(queryStub.called);
+ deleteAutocomplete.noDebounce = true;
+ deleteAutocomplete.text = 'src/test.cpp';
+ assert.isTrue(queryStub.called);
+ assert.isFalse(element.$.deleteDialog.disabled);
+ MockInteractions.tap(element.$.deleteDialog.shadowRoot
+ .querySelector('gr-button[primary]'));
+ flushAsynchronousOperations();
+
+ assert.isTrue(deleteStub.called);
+
+ return deleteStub.lastCall.returnValue.then(() => {
+ assert.equal(element._path, '');
+ assert.isTrue(navStub.called);
+ assert.isTrue(closeDialogSpy.called);
+ });
+ });
+ });
+
+ test('delete fails', () => {
+ deleteStub.returns(Promise.resolve({ok: false}));
+ MockInteractions.tap(element.shadowRoot.querySelector('#delete'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(element.$.deleteDialog.disabled);
+ assert.isFalse(queryStub.called);
+ deleteAutocomplete.noDebounce = true;
+ deleteAutocomplete.text = 'src/test.cpp';
+ assert.isTrue(queryStub.called);
+ assert.isFalse(element.$.deleteDialog.disabled);
+ MockInteractions.tap(element.$.deleteDialog.shadowRoot
+ .querySelector('gr-button[primary]'));
+ flushAsynchronousOperations();
+
+ assert.isTrue(deleteStub.called);
+
+ return deleteStub.lastCall.returnValue.then(() => {
+ assert.isFalse(navStub.called);
+ assert.isFalse(closeDialogSpy.called);
+ });
+ });
+ });
+
+ test('cancel', () => {
+ MockInteractions.tap(element.shadowRoot.querySelector('#delete'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(element.$.deleteDialog.disabled);
+ element.$.deleteDialog.querySelector('gr-autocomplete').text =
+ 'src/test.cpp';
+ assert.isFalse(element.$.deleteDialog.disabled);
+ MockInteractions.tap(element.$.deleteDialog.shadowRoot
+ .querySelector('gr-button'));
+ assert.isFalse(navStub.called);
+ assert.isTrue(closeDialogSpy.called);
+ assert.equal(element._path, 'src/test.cpp');
+ });
+ });
+ });
+
+ suite('rename button CUJ', () => {
+ let navStub;
+ let renameStub;
+ let renameAutocomplete;
+ const inputSelector = PolymerElement ?
+ '.newPathIronInput' :
+ '.newPathInput';
+
+ setup(() => {
+ navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ renameStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit');
+ renameAutocomplete =
+ element.$.renameDialog.querySelector('gr-autocomplete');
+ });
+
+ test('rename', () => {
+ renameStub.returns(Promise.resolve({ok: true}));
+ MockInteractions.tap(element.shadowRoot.querySelector('#rename'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(element.$.renameDialog.disabled);
+ assert.isFalse(queryStub.called);
+ renameAutocomplete.noDebounce = true;
+ renameAutocomplete.text = 'src/test.cpp';
+ assert.isTrue(queryStub.called);
+ assert.isTrue(element.$.renameDialog.disabled);
+
+ element.$.renameDialog.querySelector(inputSelector).bindValue =
+ 'src/test.newPath';
+
+ assert.isFalse(element.$.renameDialog.disabled);
+ MockInteractions.tap(element.$.renameDialog.shadowRoot
+ .querySelector('gr-button[primary]'));
+ flushAsynchronousOperations();
+
+ assert.isTrue(renameStub.called);
+
+ return renameStub.lastCall.returnValue.then(() => {
+ assert.equal(element._path, '');
+ assert.isTrue(navStub.called);
+ assert.isTrue(closeDialogSpy.called);
+ });
+ });
+ });
+
+ test('rename fails', () => {
+ renameStub.returns(Promise.resolve({ok: false}));
+ MockInteractions.tap(element.shadowRoot.querySelector('#rename'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(element.$.renameDialog.disabled);
+ assert.isFalse(queryStub.called);
+ renameAutocomplete.noDebounce = true;
+ renameAutocomplete.text = 'src/test.cpp';
+ assert.isTrue(queryStub.called);
+ assert.isTrue(element.$.renameDialog.disabled);
+
+ element.$.renameDialog.querySelector(inputSelector).bindValue =
+ 'src/test.newPath';
+
+ assert.isFalse(element.$.renameDialog.disabled);
+ MockInteractions.tap(element.$.renameDialog.shadowRoot
+ .querySelector('gr-button[primary]'));
+ flushAsynchronousOperations();
+
+ assert.isTrue(renameStub.called);
+
+ return renameStub.lastCall.returnValue.then(() => {
+ assert.isFalse(navStub.called);
+ assert.isFalse(closeDialogSpy.called);
+ });
+ });
+ });
+
+ test('cancel', () => {
+ MockInteractions.tap(element.shadowRoot.querySelector('#rename'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(element.$.renameDialog.disabled);
+ element.$.renameDialog.querySelector('gr-autocomplete').text =
+ 'src/test.cpp';
+ element.$.renameDialog.querySelector(inputSelector).bindValue =
+ 'src/test.newPath';
+ assert.isFalse(element.$.renameDialog.disabled);
+ MockInteractions.tap(element.$.renameDialog.shadowRoot
+ .querySelector('gr-button'));
+ assert.isFalse(navStub.called);
+ assert.isTrue(closeDialogSpy.called);
+ assert.equal(element._path, 'src/test.cpp');
+ assert.equal(element._newPath, 'src/test.newPath');
+ });
+ });
+ });
+
+ suite('restore button CUJ', () => {
+ let navStub;
+ let restoreStub;
+
+ setup(() => {
+ navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ restoreStub = sandbox.stub(element.$.restAPI, 'restoreFileInChangeEdit');
+ });
+
+ test('restore hidden by default', () => {
+ assert.isTrue(element.shadowRoot
+ .querySelector('#restore').classList.contains('invisible'));
+ });
+
+ test('restore', () => {
+ restoreStub.returns(Promise.resolve({ok: true}));
+ element._path = 'src/test.cpp';
+ MockInteractions.tap(element.shadowRoot.querySelector('#restore'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ MockInteractions.tap(element.$.restoreDialog.shadowRoot
+ .querySelector('gr-button[primary]'));
+ flushAsynchronousOperations();
+
+ assert.isTrue(restoreStub.called);
+ assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
+ return restoreStub.lastCall.returnValue.then(() => {
+ assert.equal(element._path, '');
+ assert.isTrue(navStub.called);
+ assert.isTrue(closeDialogSpy.called);
+ });
+ });
+ });
+
+ test('restore fails', () => {
+ restoreStub.returns(Promise.resolve({ok: false}));
+ element._path = 'src/test.cpp';
+ MockInteractions.tap(element.shadowRoot.querySelector('#restore'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ MockInteractions.tap(element.$.restoreDialog.shadowRoot
+ .querySelector('gr-button[primary]'));
+ flushAsynchronousOperations();
+
+ assert.isTrue(restoreStub.called);
+ assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
+ return restoreStub.lastCall.returnValue.then(() => {
+ assert.isFalse(navStub.called);
+ assert.isFalse(closeDialogSpy.called);
+ });
+ });
+ });
+
+ test('cancel', () => {
+ element._path = 'src/test.cpp';
+ MockInteractions.tap(element.shadowRoot.querySelector('#restore'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ MockInteractions.tap(element.$.restoreDialog.shadowRoot
+ .querySelector('gr-button'));
+ assert.isFalse(navStub.called);
+ assert.isTrue(closeDialogSpy.called);
+ assert.equal(element._path, 'src/test.cpp');
+ });
+ });
+ });
+
+ test('openOpenDialog', done => {
+ element.openOpenDialog('test/path.cpp')
+ .then(() => {
+ assert.isFalse(element.$.openDialog.hasAttribute('hidden'));
+ assert.equal(
+ element.$.openDialog.querySelector('gr-autocomplete').text,
+ 'test/path.cpp');
+ done();
+ });
+ });
+
+ test('_getDialogFromEvent', () => {
+ const spy = sandbox.spy(element, '_getDialogFromEvent');
+ element.addEventListener('tap', element._getDialogFromEvent);
+
+ MockInteractions.tap(element.$.openDialog);
+ flushAsynchronousOperations();
+ assert.equal(spy.lastCall.returnValue.id, 'openDialog');
+
+ MockInteractions.tap(element.$.deleteDialog);
+ flushAsynchronousOperations();
+ assert.equal(spy.lastCall.returnValue.id, 'deleteDialog');
+
+ MockInteractions.tap(
+ element.$.deleteDialog.querySelector('gr-autocomplete'));
+ flushAsynchronousOperations();
+ assert.equal(spy.lastCall.returnValue.id, 'deleteDialog');
+
+ MockInteractions.tap(element);
+ flushAsynchronousOperations();
+ assert.notOk(spy.lastCall.returnValue);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html
deleted file mode 100644
index f6c7803..0000000
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html
+++ /dev/null
@@ -1,61 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
-<link rel="import" href="../gr-edit-constants.html">
-
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-edit-file-controls">
- <template>
- <style include="shared-styles">
- :host {
- align-items: center;
- display: flex;
- justify-content: flex-end;
- }
- #actions {
- margin-right: var(--spacing-l);
- }
- gr-button,
- gr-dropdown {
- --gr-button: {
- height: 1.8em;
- }
- }
- gr-dropdown {
- --gr-dropdown-item: {
- background-color: transparent;
- border: none;
- color: var(--link-color);
- text-transform: uppercase;
- }
- }
- </style>
- <gr-dropdown
- id="actions"
- items="[[_fileActions]]"
- down-arrow
- vertical-offset="20"
- on-tap-item="_handleActionTap"
- link>Actions</gr-dropdown>
- </template>
- <script src="gr-edit-file-controls.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
index d59fcf7..10bff3c 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
@@ -14,56 +14,65 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrEditFileControls extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-edit-file-controls'; }
- /**
- * Fired when an action in the overflow menu is tapped.
- *
- * @event file-action-tap
- */
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-dropdown/gr-dropdown.js';
+import '../gr-edit-constants.js';
+import '../../../styles/shared-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-edit-file-controls_html.js';
- static get properties() {
- return {
- filePath: String,
- _allFileActions: {
- type: Array,
- value: () => Object.values(GrEditConstants.Actions),
- },
- _fileActions: {
- type: Array,
- computed: '_computeFileActions(_allFileActions)',
- },
- };
- }
+/** @extends Polymer.Element */
+class GrEditFileControls extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _handleActionTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this._dispatchFileAction(e.detail.id, this.filePath);
- }
+ static get is() { return 'gr-edit-file-controls'; }
+ /**
+ * Fired when an action in the overflow menu is tapped.
+ *
+ * @event file-action-tap
+ */
- _dispatchFileAction(action, path) {
- this.dispatchEvent(new CustomEvent(
- 'file-action-tap',
- {detail: {action, path}, bubbles: true, composed: true}));
- }
-
- _computeFileActions(actions) {
- // TODO(kaspern): conditionally disable some actions based on file status.
- return actions.map(action => {
- return {
- name: action.label,
- id: action.id,
- };
- });
- }
+ static get properties() {
+ return {
+ filePath: String,
+ _allFileActions: {
+ type: Array,
+ value: () => Object.values(GrEditConstants.Actions),
+ },
+ _fileActions: {
+ type: Array,
+ computed: '_computeFileActions(_allFileActions)',
+ },
+ };
}
- customElements.define(GrEditFileControls.is, GrEditFileControls);
-})();
+ _handleActionTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this._dispatchFileAction(e.detail.id, this.filePath);
+ }
+
+ _dispatchFileAction(action, path) {
+ this.dispatchEvent(new CustomEvent(
+ 'file-action-tap',
+ {detail: {action, path}, bubbles: true, composed: true}));
+ }
+
+ _computeFileActions(actions) {
+ // TODO(kaspern): conditionally disable some actions based on file status.
+ return actions.map(action => {
+ return {
+ name: action.label,
+ id: action.id,
+ };
+ });
+ }
+}
+
+customElements.define(GrEditFileControls.is, GrEditFileControls);
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.js b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.js
new file mode 100644
index 0000000..7a7ba5d
--- /dev/null
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.js
@@ -0,0 +1,45 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ align-items: center;
+ display: flex;
+ justify-content: flex-end;
+ }
+ #actions {
+ margin-right: var(--spacing-l);
+ }
+ gr-button,
+ gr-dropdown {
+ --gr-button: {
+ height: 1.8em;
+ }
+ }
+ gr-dropdown {
+ --gr-dropdown-item: {
+ background-color: transparent;
+ border: none;
+ color: var(--link-color);
+ text-transform: uppercase;
+ }
+ }
+ </style>
+ <gr-dropdown id="actions" items="[[_fileActions]]" down-arrow="" vertical-offset="20" on-tap-item="_handleActionTap" link="">Actions</gr-dropdown>
+`;
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
index 392a105..46a336f 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
@@ -18,17 +18,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-edit-file-controls</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-
-<link rel="import" href="../gr-edit-constants.html">
-<link rel="import" href="gr-edit-file-controls.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,76 +29,78 @@
</template>
</test-fixture>
-<script>
- suite('gr-edit-file-controls tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let fileActionHandler;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- fileActionHandler = sandbox.stub();
- element.addEventListener('file-action-tap', fileActionHandler);
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('open tap emits event', () => {
- const actions = element.$.actions;
- element.filePath = 'foo';
- actions._open();
- flushAsynchronousOperations();
-
- MockInteractions.tap(actions.shadowRoot
- .querySelector('li [data-id="open"]'));
- assert.isTrue(fileActionHandler.called);
- assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
- {action: GrEditConstants.Actions.OPEN.id, path: 'foo'});
- });
-
- test('delete tap emits event', () => {
- const actions = element.$.actions;
- element.filePath = 'foo';
- actions._open();
- flushAsynchronousOperations();
-
- MockInteractions.tap(actions.shadowRoot
- .querySelector('li [data-id="delete"]'));
- assert.isTrue(fileActionHandler.called);
- assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
- {action: GrEditConstants.Actions.DELETE.id, path: 'foo'});
- });
-
- test('restore tap emits event', () => {
- const actions = element.$.actions;
- element.filePath = 'foo';
- actions._open();
- flushAsynchronousOperations();
-
- MockInteractions.tap(actions.shadowRoot
- .querySelector('li [data-id="restore"]'));
- assert.isTrue(fileActionHandler.called);
- assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
- {action: GrEditConstants.Actions.RESTORE.id, path: 'foo'});
- });
-
- test('rename tap emits event', () => {
- const actions = element.$.actions;
- element.filePath = 'foo';
- actions._open();
- flushAsynchronousOperations();
-
- MockInteractions.tap(actions.shadowRoot
- .querySelector('li [data-id="rename"]'));
- assert.isTrue(fileActionHandler.called);
- assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
- {action: GrEditConstants.Actions.RENAME.id, path: 'foo'});
- });
-
- test('computed properties', () => {
- assert.equal(element._allFileActions.length, 4);
- });
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../gr-edit-constants.js';
+import './gr-edit-file-controls.js';
+suite('gr-edit-file-controls tests', () => {
+ let element;
+ let sandbox;
+ let fileActionHandler;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ fileActionHandler = sandbox.stub();
+ element.addEventListener('file-action-tap', fileActionHandler);
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('open tap emits event', () => {
+ const actions = element.$.actions;
+ element.filePath = 'foo';
+ actions._open();
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(actions.shadowRoot
+ .querySelector('li [data-id="open"]'));
+ assert.isTrue(fileActionHandler.called);
+ assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
+ {action: GrEditConstants.Actions.OPEN.id, path: 'foo'});
+ });
+
+ test('delete tap emits event', () => {
+ const actions = element.$.actions;
+ element.filePath = 'foo';
+ actions._open();
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(actions.shadowRoot
+ .querySelector('li [data-id="delete"]'));
+ assert.isTrue(fileActionHandler.called);
+ assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
+ {action: GrEditConstants.Actions.DELETE.id, path: 'foo'});
+ });
+
+ test('restore tap emits event', () => {
+ const actions = element.$.actions;
+ element.filePath = 'foo';
+ actions._open();
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(actions.shadowRoot
+ .querySelector('li [data-id="restore"]'));
+ assert.isTrue(fileActionHandler.called);
+ assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
+ {action: GrEditConstants.Actions.RESTORE.id, path: 'foo'});
+ });
+
+ test('rename tap emits event', () => {
+ const actions = element.$.actions;
+ element.filePath = 'foo';
+ actions._open();
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(actions.shadowRoot
+ .querySelector('li [data-id="rename"]'));
+ assert.isTrue(fileActionHandler.called);
+ assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
+ {action: GrEditConstants.Actions.RENAME.id, path: 'foo'});
+ });
+
+ test('computed properties', () => {
+ assert.equal(element._allFileActions.length, 4);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
deleted file mode 100644
index 1ae74e1..0000000
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
+++ /dev/null
@@ -1,134 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
-<link rel="import" href="../../shared/gr-fixed-panel/gr-fixed-panel.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-storage/gr-storage.html">
-<link rel="import" href="../gr-default-editor/gr-default-editor.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-editor-view">
- <template>
- <style include="shared-styles">
- :host {
- background-color: var(--view-background-color);
- }
- gr-fixed-panel {
- background-color: var(--edit-mode-background-color);
- border-bottom: 1px var(--border-color) solid;
- z-index: 1;
- }
- header,
- .subHeader {
- align-items: center;
- display: flex;
- justify-content: space-between;
- padding: var(--spacing-m) var(--spacing-l);
- }
- header gr-editable-label {
- font-family: var(--header-font-family);
- font-size: var(--font-size-h3);
- font-weight: var(--font-weight-h3);
- line-height: var(--line-height-h3);
- --label-style: {
- text-overflow: initial;
- white-space: initial;
- word-break: break-all;
- }
- --input-style: {
- margin-top: var(--spacing-l);
- }
- }
- .textareaWrapper {
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius);
- margin: var(--spacing-l);
- }
- .textareaWrapper .editButtons {
- display: none;
- }
- .controlGroup {
- align-items: center;
- display: flex;
- font-family: var(--header-font-family);
- font-size: var(--font-size-h3);
- font-weight: var(--font-weight-h3);
- line-height: var(--line-height-h3);
- }
- .rightControls {
- justify-content: flex-end;
- }
- @media screen and (max-width: 50em) {
- header,
- .subHeader {
- display: block;
- }
- .rightControls {
- float: right;
- }
- }
- </style>
- <gr-fixed-panel keep-on-scroll>
- <header>
- <span class="controlGroup">
- <span>Edit mode</span>
- <span class="separator"></span>
- <gr-editable-label
- label-text="File path"
- value="[[_path]]"
- placeholder="File path..."
- on-changed="_handlePathChanged"></gr-editable-label>
- </span>
- <span class="controlGroup rightControls">
- <gr-button
- id="close"
- link
- on-click="_handleCloseTap">Close</gr-button>
- <gr-button
- id="save"
- disabled$="[[_saveDisabled]]"
- primary
- link
- on-click="_saveEdit">Save</gr-button>
- </span>
- </header>
- </gr-fixed-panel>
- <div class="textareaWrapper">
- <gr-endpoint-decorator id="editorEndpoint" name="editor">
- <gr-endpoint-param name="fileContent" value="[[_newContent]]"></gr-endpoint-param>
- <gr-endpoint-param name="prefs" value="[[_prefs]]"></gr-endpoint-param>
- <gr-endpoint-param name="fileType" value="[[_type]]"></gr-endpoint-param>
- <gr-endpoint-param name="lineNum" value="[[_lineNum]]"></gr-endpoint-param>
- <gr-default-editor id="file" file-content="[[_newContent]]"></gr-default-editor>
- </gr-endpoint-decorator>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- <gr-storage id="storage"></gr-storage>
- </template>
- <script src="gr-editor-view.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
index 64158d0..2303005 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
@@ -14,257 +14,277 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const RESTORED_MESSAGE = 'Content restored from a previous edit.';
- const SAVING_MESSAGE = 'Saving changes...';
- const SAVED_MESSAGE = 'All changes saved';
- const SAVE_FAILED_MSG = 'Failed to save changes';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-editable-label/gr-editable-label.js';
+import '../../shared/gr-fixed-panel/gr-fixed-panel.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-storage/gr-storage.js';
+import '../gr-default-editor/gr-default-editor.js';
+import '../../../styles/shared-styles.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-editor-view_html.js';
- const STORAGE_DEBOUNCE_INTERVAL_MS = 100;
+const RESTORED_MESSAGE = 'Content restored from a previous edit.';
+const SAVING_MESSAGE = 'Saving changes...';
+const SAVED_MESSAGE = 'All changes saved';
+const SAVE_FAILED_MSG = 'Failed to save changes';
+
+const STORAGE_DEBOUNCE_INTERVAL_MS = 100;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.PathListMixin
+ * @extends Polymer.Element
+ */
+class GrEditorView extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.PathListBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-editor-view'; }
+ /**
+ * Fired when the title of the page should change.
+ *
+ * @event title-change
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.PathListMixin
- * @extends Polymer.Element
+ * Fired to notify the user of
+ *
+ * @event show-alert
*/
- class GrEditorView extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.PathListBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-editor-view'; }
+
+ static get properties() {
+ return {
/**
- * Fired when the title of the page should change.
- *
- * @event title-change
+ * URL params passed from the router.
*/
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
- /**
- * Fired to notify the user of
- *
- * @event show-alert
- */
-
- static get properties() {
- return {
- /**
- * URL params passed from the router.
- */
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
-
- _change: Object,
- _changeEditDetail: Object,
- _changeNum: String,
- _patchNum: String,
- _path: String,
- _type: String,
- _content: String,
- _newContent: String,
- _saving: {
- type: Boolean,
- value: false,
- },
- _successfulSave: {
- type: Boolean,
- value: false,
- },
- _saveDisabled: {
- type: Boolean,
- value: true,
- computed: '_computeSaveDisabled(_content, _newContent, _saving)',
- },
- _prefs: Object,
- _lineNum: Number,
- };
- }
-
- get keyBindings() {
- return {
- 'ctrl+s meta+s': '_handleSaveShortcut',
- };
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('content-change',
- e => this._handleContentChange(e));
- }
-
- /** @override */
- attached() {
- super.attached();
- this._getEditPrefs().then(prefs => { this._prefs = prefs; });
- }
-
- get storageKey() {
- return `c${this._changeNum}_ps${this._patchNum}_${this._path}`;
- }
-
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- _getEditPrefs() {
- return this.$.restAPI.getEditPreferences();
- }
-
- _paramsChanged(value) {
- if (value.view !== Gerrit.Nav.View.EDIT) {
- return;
- }
-
- this._changeNum = value.changeNum;
- this._path = value.path;
- this._patchNum = value.patchNum || this.EDIT_NAME;
- this._lineNum = value.lineNum;
-
- // NOTE: This may be called before attachment (e.g. while parentElement is
- // null). Fire title-change in an async so that, if attachment to the DOM
- // has been queued, the event can bubble up to the handler in gr-app.
- this.async(() => {
- const title = `Editing ${this.computeTruncatedPath(this._path)}`;
- this.fire('title-change', {title});
- });
-
- const promises = [];
-
- promises.push(this._getChangeDetail(this._changeNum));
- promises.push(
- this._getFileData(this._changeNum, this._path, this._patchNum));
- return Promise.all(promises);
- }
-
- _getChangeDetail(changeNum) {
- return this.$.restAPI.getDiffChangeDetail(changeNum).then(change => {
- this._change = change;
- });
- }
-
- _handlePathChanged(e) {
- const path = e.detail;
- if (path === this._path) {
- return Promise.resolve();
- }
- return this.$.restAPI.renameFileInChangeEdit(this._changeNum,
- this._path, path).then(res => {
- if (!res.ok) { return; }
-
- this._successfulSave = true;
- this._viewEditInChangeView();
- });
- }
-
- _viewEditInChangeView() {
- const patch = this._successfulSave ? this.EDIT_NAME : this._patchNum;
- Gerrit.Nav.navigateToChange(this._change, patch, null,
- patch !== this.EDIT_NAME);
- }
-
- _getFileData(changeNum, path, patchNum) {
- const storedContent =
- this.$.storage.getEditableContentItem(this.storageKey);
-
- return this.$.restAPI.getFileContent(changeNum, path, patchNum)
- .then(res => {
- if (storedContent && storedContent.message &&
- storedContent.message !== res.content) {
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {message: RESTORED_MESSAGE},
- bubbles: true,
- composed: true,
- }));
-
- this._newContent = storedContent.message;
- } else {
- this._newContent = res.content || '';
- }
- this._content = res.content || '';
-
- // A non-ok response may result if the file does not yet exist.
- // The `type` field of the response is only valid when the file
- // already exists.
- if (res.ok && res.type) {
- this._type = res.type;
- } else {
- this._type = '';
- }
- });
- }
-
- _saveEdit() {
- this._saving = true;
- this._showAlert(SAVING_MESSAGE);
- this.$.storage.eraseEditableContentItem(this.storageKey);
- return this.$.restAPI.saveChangeEdit(this._changeNum, this._path,
- this._newContent).then(res => {
- this._saving = false;
- this._showAlert(res.ok ? SAVED_MESSAGE : SAVE_FAILED_MSG);
- if (!res.ok) { return; }
-
- this._content = this._newContent;
- this._successfulSave = true;
- });
- }
-
- _showAlert(message) {
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {message},
- bubbles: true,
- composed: true,
- }));
- }
-
- _computeSaveDisabled(content, newContent, saving) {
- // Polymer 2: check for undefined
- if ([
- content,
- newContent,
- saving,
- ].some(arg => arg === undefined)) {
- return true;
- }
-
- if (saving) {
- return true;
- }
- return content === newContent;
- }
-
- _handleCloseTap() {
- // TODO(kaspern): Add a confirm dialog if there are unsaved changes.
- this._viewEditInChangeView();
- }
-
- _handleContentChange(e) {
- this.debounce('store', () => {
- const content = e.detail.value;
- if (content) {
- this.set('_newContent', e.detail.value);
- this.$.storage.setEditableContentItem(this.storageKey, content);
- } else {
- this.$.storage.eraseEditableContentItem(this.storageKey);
- }
- }, STORAGE_DEBOUNCE_INTERVAL_MS);
- }
-
- _handleSaveShortcut(e) {
- e.preventDefault();
- if (!this._saveDisabled) {
- this._saveEdit();
- }
- }
+ _change: Object,
+ _changeEditDetail: Object,
+ _changeNum: String,
+ _patchNum: String,
+ _path: String,
+ _type: String,
+ _content: String,
+ _newContent: String,
+ _saving: {
+ type: Boolean,
+ value: false,
+ },
+ _successfulSave: {
+ type: Boolean,
+ value: false,
+ },
+ _saveDisabled: {
+ type: Boolean,
+ value: true,
+ computed: '_computeSaveDisabled(_content, _newContent, _saving)',
+ },
+ _prefs: Object,
+ _lineNum: Number,
+ };
}
- customElements.define(GrEditorView.is, GrEditorView);
-})();
+ get keyBindings() {
+ return {
+ 'ctrl+s meta+s': '_handleSaveShortcut',
+ };
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('content-change',
+ e => this._handleContentChange(e));
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._getEditPrefs().then(prefs => { this._prefs = prefs; });
+ }
+
+ get storageKey() {
+ return `c${this._changeNum}_ps${this._patchNum}_${this._path}`;
+ }
+
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _getEditPrefs() {
+ return this.$.restAPI.getEditPreferences();
+ }
+
+ _paramsChanged(value) {
+ if (value.view !== Gerrit.Nav.View.EDIT) {
+ return;
+ }
+
+ this._changeNum = value.changeNum;
+ this._path = value.path;
+ this._patchNum = value.patchNum || this.EDIT_NAME;
+ this._lineNum = value.lineNum;
+
+ // NOTE: This may be called before attachment (e.g. while parentElement is
+ // null). Fire title-change in an async so that, if attachment to the DOM
+ // has been queued, the event can bubble up to the handler in gr-app.
+ this.async(() => {
+ const title = `Editing ${this.computeTruncatedPath(this._path)}`;
+ this.fire('title-change', {title});
+ });
+
+ const promises = [];
+
+ promises.push(this._getChangeDetail(this._changeNum));
+ promises.push(
+ this._getFileData(this._changeNum, this._path, this._patchNum));
+ return Promise.all(promises);
+ }
+
+ _getChangeDetail(changeNum) {
+ return this.$.restAPI.getDiffChangeDetail(changeNum).then(change => {
+ this._change = change;
+ });
+ }
+
+ _handlePathChanged(e) {
+ const path = e.detail;
+ if (path === this._path) {
+ return Promise.resolve();
+ }
+ return this.$.restAPI.renameFileInChangeEdit(this._changeNum,
+ this._path, path).then(res => {
+ if (!res.ok) { return; }
+
+ this._successfulSave = true;
+ this._viewEditInChangeView();
+ });
+ }
+
+ _viewEditInChangeView() {
+ const patch = this._successfulSave ? this.EDIT_NAME : this._patchNum;
+ Gerrit.Nav.navigateToChange(this._change, patch, null,
+ patch !== this.EDIT_NAME);
+ }
+
+ _getFileData(changeNum, path, patchNum) {
+ const storedContent =
+ this.$.storage.getEditableContentItem(this.storageKey);
+
+ return this.$.restAPI.getFileContent(changeNum, path, patchNum)
+ .then(res => {
+ if (storedContent && storedContent.message &&
+ storedContent.message !== res.content) {
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {message: RESTORED_MESSAGE},
+ bubbles: true,
+ composed: true,
+ }));
+
+ this._newContent = storedContent.message;
+ } else {
+ this._newContent = res.content || '';
+ }
+ this._content = res.content || '';
+
+ // A non-ok response may result if the file does not yet exist.
+ // The `type` field of the response is only valid when the file
+ // already exists.
+ if (res.ok && res.type) {
+ this._type = res.type;
+ } else {
+ this._type = '';
+ }
+ });
+ }
+
+ _saveEdit() {
+ this._saving = true;
+ this._showAlert(SAVING_MESSAGE);
+ this.$.storage.eraseEditableContentItem(this.storageKey);
+ return this.$.restAPI.saveChangeEdit(this._changeNum, this._path,
+ this._newContent).then(res => {
+ this._saving = false;
+ this._showAlert(res.ok ? SAVED_MESSAGE : SAVE_FAILED_MSG);
+ if (!res.ok) { return; }
+
+ this._content = this._newContent;
+ this._successfulSave = true;
+ });
+ }
+
+ _showAlert(message) {
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {message},
+ bubbles: true,
+ composed: true,
+ }));
+ }
+
+ _computeSaveDisabled(content, newContent, saving) {
+ // Polymer 2: check for undefined
+ if ([
+ content,
+ newContent,
+ saving,
+ ].some(arg => arg === undefined)) {
+ return true;
+ }
+
+ if (saving) {
+ return true;
+ }
+ return content === newContent;
+ }
+
+ _handleCloseTap() {
+ // TODO(kaspern): Add a confirm dialog if there are unsaved changes.
+ this._viewEditInChangeView();
+ }
+
+ _handleContentChange(e) {
+ this.debounce('store', () => {
+ const content = e.detail.value;
+ if (content) {
+ this.set('_newContent', e.detail.value);
+ this.$.storage.setEditableContentItem(this.storageKey, content);
+ } else {
+ this.$.storage.eraseEditableContentItem(this.storageKey);
+ }
+ }, STORAGE_DEBOUNCE_INTERVAL_MS);
+ }
+
+ _handleSaveShortcut(e) {
+ e.preventDefault();
+ if (!this._saveDisabled) {
+ this._saveEdit();
+ }
+ }
+}
+
+customElements.define(GrEditorView.is, GrEditorView);
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.js
new file mode 100644
index 0000000..72d29dd
--- /dev/null
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.js
@@ -0,0 +1,103 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ background-color: var(--view-background-color);
+ }
+ gr-fixed-panel {
+ background-color: var(--edit-mode-background-color);
+ border-bottom: 1px var(--border-color) solid;
+ z-index: 1;
+ }
+ header,
+ .subHeader {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+ padding: var(--spacing-m) var(--spacing-l);
+ }
+ header gr-editable-label {
+ font-family: var(--header-font-family);
+ font-size: var(--font-size-h3);
+ font-weight: var(--font-weight-h3);
+ line-height: var(--line-height-h3);
+ --label-style: {
+ text-overflow: initial;
+ white-space: initial;
+ word-break: break-all;
+ }
+ --input-style: {
+ margin-top: var(--spacing-l);
+ }
+ }
+ .textareaWrapper {
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ margin: var(--spacing-l);
+ }
+ .textareaWrapper .editButtons {
+ display: none;
+ }
+ .controlGroup {
+ align-items: center;
+ display: flex;
+ font-family: var(--header-font-family);
+ font-size: var(--font-size-h3);
+ font-weight: var(--font-weight-h3);
+ line-height: var(--line-height-h3);
+ }
+ .rightControls {
+ justify-content: flex-end;
+ }
+ @media screen and (max-width: 50em) {
+ header,
+ .subHeader {
+ display: block;
+ }
+ .rightControls {
+ float: right;
+ }
+ }
+ </style>
+ <gr-fixed-panel keep-on-scroll="">
+ <header>
+ <span class="controlGroup">
+ <span>Edit mode</span>
+ <span class="separator"></span>
+ <gr-editable-label label-text="File path" value="[[_path]]" placeholder="File path..." on-changed="_handlePathChanged"></gr-editable-label>
+ </span>
+ <span class="controlGroup rightControls">
+ <gr-button id="close" link="" on-click="_handleCloseTap">Close</gr-button>
+ <gr-button id="save" disabled\$="[[_saveDisabled]]" primary="" link="" on-click="_saveEdit">Save</gr-button>
+ </span>
+ </header>
+ </gr-fixed-panel>
+ <div class="textareaWrapper">
+ <gr-endpoint-decorator id="editorEndpoint" name="editor">
+ <gr-endpoint-param name="fileContent" value="[[_newContent]]"></gr-endpoint-param>
+ <gr-endpoint-param name="prefs" value="[[_prefs]]"></gr-endpoint-param>
+ <gr-endpoint-param name="fileType" value="[[_type]]"></gr-endpoint-param>
+ <gr-endpoint-param name="lineNum" value="[[_lineNum]]"></gr-endpoint-param>
+ <gr-default-editor id="file" file-content="[[_newContent]]"></gr-default-editor>
+ </gr-endpoint-decorator>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ <gr-storage id="storage"></gr-storage>
+`;
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
index 1d264bc..4659065 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
@@ -18,16 +18,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-editor-view</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-
-<link rel="import" href="gr-editor-view.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,380 +29,381 @@
</template>
</test-fixture>
-<script>
- suite('gr-editor-view tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let savePathStub;
- let saveFileStub;
- let changeDetailStub;
- let navigateStub;
- const mockParams = {
- changeNum: '42',
- path: 'foo/bar.baz',
- patchNum: 'edit',
- };
-
- setup(() => {
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- getEditPreferences() { return Promise.resolve({}); },
- });
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- savePathStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit');
- saveFileStub = sandbox.stub(element.$.restAPI, 'saveChangeEdit');
- changeDetailStub = sandbox.stub(element.$.restAPI, 'getDiffChangeDetail');
- navigateStub = sandbox.stub(element, '_viewEditInChangeView');
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-editor-view.js';
+suite('gr-editor-view tests', () => {
+ let element;
+ let sandbox;
+ let savePathStub;
+ let saveFileStub;
+ let changeDetailStub;
+ let navigateStub;
+ const mockParams = {
+ changeNum: '42',
+ path: 'foo/bar.baz',
+ patchNum: 'edit',
+ };
+
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
+ getEditPreferences() { return Promise.resolve({}); },
});
-
- teardown(() => { sandbox.restore(); });
-
- suite('_paramsChanged', () => {
- test('incorrect view returns immediately', () => {
- element._paramsChanged(
- Object.assign({}, mockParams, {view: Gerrit.Nav.View.DIFF}));
- assert.notOk(element._changeNum);
- });
-
- test('good params proceed', () => {
- changeDetailStub.returns(Promise.resolve({}));
- const fileStub = sandbox.stub(element, '_getFileData', () => {
- element._content = 'text';
- element._newContent = 'text';
- element._type = 'application/octet-stream';
- });
-
- const promises = element._paramsChanged(
- Object.assign({}, mockParams, {view: Gerrit.Nav.View.EDIT}));
-
- flushAsynchronousOperations();
- assert.equal(element._changeNum, mockParams.changeNum);
- assert.equal(element._path, mockParams.path);
- assert.deepEqual(changeDetailStub.lastCall.args[0],
- mockParams.changeNum);
- assert.deepEqual(fileStub.lastCall.args,
- [mockParams.changeNum, mockParams.path, mockParams.patchNum]);
-
- return promises.then(() => {
- assert.equal(element._content, 'text');
- assert.equal(element._newContent, 'text');
- assert.equal(element._type, 'application/octet-stream');
- });
- });
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ savePathStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit');
+ saveFileStub = sandbox.stub(element.$.restAPI, 'saveChangeEdit');
+ changeDetailStub = sandbox.stub(element.$.restAPI, 'getDiffChangeDetail');
+ navigateStub = sandbox.stub(element, '_viewEditInChangeView');
+ });
+
+ teardown(() => { sandbox.restore(); });
+
+ suite('_paramsChanged', () => {
+ test('incorrect view returns immediately', () => {
+ element._paramsChanged(
+ Object.assign({}, mockParams, {view: Gerrit.Nav.View.DIFF}));
+ assert.notOk(element._changeNum);
});
-
- test('edit file path', () => {
- element._changeNum = mockParams.changeNum;
- element._path = mockParams.path;
- savePathStub.onFirstCall().returns(Promise.resolve({}));
- savePathStub.onSecondCall().returns(Promise.resolve({ok: true}));
-
- // Calling with the same path should not navigate.
- return element._handlePathChanged({detail: mockParams.path}).then(() => {
- assert.isFalse(savePathStub.called);
- // !ok response
- element._handlePathChanged({detail: 'newPath'}).then(() => {
- assert.isTrue(savePathStub.called);
- assert.isFalse(navigateStub.called);
- // ok response
- element._handlePathChanged({detail: 'newPath'}).then(() => {
- assert.isTrue(navigateStub.called);
- assert.isTrue(element._successfulSave);
- });
- });
+
+ test('good params proceed', () => {
+ changeDetailStub.returns(Promise.resolve({}));
+ const fileStub = sandbox.stub(element, '_getFileData', () => {
+ element._content = 'text';
+ element._newContent = 'text';
+ element._type = 'application/octet-stream';
});
- });
-
- test('reacts to content-change event', () => {
- const storeStub = sandbox.spy(element.$.storage, 'setEditableContentItem');
- element._newContent = 'test';
- element.$.editorEndpoint.dispatchEvent(new CustomEvent('content-change', {
- bubbles: true, composed: true,
- detail: {value: 'new content value'},
- }));
- element.flushDebouncer('store');
+
+ const promises = element._paramsChanged(
+ Object.assign({}, mockParams, {view: Gerrit.Nav.View.EDIT}));
+
flushAsynchronousOperations();
-
- assert.equal(element._newContent, 'new content value');
- assert.isTrue(storeStub.called);
- assert.equal(storeStub.lastCall.args[1], 'new content value');
- });
-
- suite('edit file content', () => {
- const originalText = 'file text';
- const newText = 'file text changed';
-
- setup(() => {
- element._changeNum = mockParams.changeNum;
- element._path = mockParams.path;
- element._content = originalText;
- element._newContent = originalText;
- flushAsynchronousOperations();
- });
-
- test('initial load', () => {
- assert.equal(element.$.file.fileContent, originalText);
- assert.isTrue(element.$.save.hasAttribute('disabled'));
- });
-
- test('file modification and save, !ok response', () => {
- const saveSpy = sandbox.spy(element, '_saveEdit');
- const eraseStub = sandbox.stub(element.$.storage,
- 'eraseEditableContentItem');
- const alertStub = sandbox.stub(element, '_showAlert');
- saveFileStub.returns(Promise.resolve({ok: false}));
- element._newContent = newText;
- flushAsynchronousOperations();
-
- assert.isFalse(element.$.save.hasAttribute('disabled'));
- assert.isFalse(element._saving);
-
- MockInteractions.tap(element.$.save);
- assert.isTrue(saveSpy.called);
- assert.equal(alertStub.lastCall.args[0], 'Saving changes...');
- assert.isTrue(element._saving);
- assert.isTrue(element.$.save.hasAttribute('disabled'));
-
- return saveSpy.lastCall.returnValue.then(() => {
- assert.isTrue(saveFileStub.called);
- assert.isTrue(eraseStub.called);
- assert.isFalse(element._saving);
- assert.equal(alertStub.lastCall.args[0], 'Failed to save changes');
- assert.deepEqual(saveFileStub.lastCall.args,
- [mockParams.changeNum, mockParams.path, newText]);
- assert.isFalse(navigateStub.called);
- assert.isFalse(element.$.save.hasAttribute('disabled'));
- assert.notEqual(element._content, element._newContent);
- });
- });
-
- test('file modification and save', () => {
- const saveSpy = sandbox.spy(element, '_saveEdit');
- const alertStub = sandbox.stub(element, '_showAlert');
- saveFileStub.returns(Promise.resolve({ok: true}));
- element._newContent = newText;
- flushAsynchronousOperations();
-
- assert.isFalse(element._saving);
- assert.isFalse(element.$.save.hasAttribute('disabled'));
-
- MockInteractions.tap(element.$.save);
- assert.isTrue(saveSpy.called);
- assert.equal(alertStub.lastCall.args[0], 'Saving changes...');
- assert.isTrue(element._saving);
- assert.isTrue(element.$.save.hasAttribute('disabled'));
-
- return saveSpy.lastCall.returnValue.then(() => {
- assert.isTrue(saveFileStub.called);
- assert.isFalse(element._saving);
- assert.equal(alertStub.lastCall.args[0], 'All changes saved');
- assert.isFalse(navigateStub.called);
- assert.isTrue(element.$.save.hasAttribute('disabled'));
- assert.equal(element._content, element._newContent);
- assert.isTrue(element._successfulSave);
- });
- });
-
- test('file modification and close', () => {
- const closeSpy = sandbox.spy(element, '_handleCloseTap');
- element._newContent = newText;
- flushAsynchronousOperations();
-
- assert.isFalse(element.$.save.hasAttribute('disabled'));
-
- MockInteractions.tap(element.$.close);
- assert.isTrue(closeSpy.called);
- assert.isFalse(saveFileStub.called);
- assert.isTrue(navigateStub.called);
- });
- });
-
- suite('_getFileData', () => {
- setup(() => {
- element._newContent = 'initial';
- element._content = 'initial';
- element._type = 'initial';
- sandbox.stub(element.$.storage, 'getEditableContentItem').returns(null);
- });
-
- test('res.ok', () => {
- sandbox.stub(element.$.restAPI, 'getFileContent')
- .returns(Promise.resolve({
- ok: true,
- type: 'text/javascript',
- content: 'new content',
- }));
-
- // Ensure no data is set with a bad response.
- return element._getFileData('1', 'test/path', 'edit').then(() => {
- assert.equal(element._newContent, 'new content');
- assert.equal(element._content, 'new content');
- assert.equal(element._type, 'text/javascript');
- });
- });
-
- test('!res.ok', () => {
- sandbox.stub(element.$.restAPI, 'getFileContent')
- .returns(Promise.resolve({}));
-
- // Ensure no data is set with a bad response.
- return element._getFileData('1', 'test/path', 'edit').then(() => {
- assert.equal(element._newContent, '');
- assert.equal(element._content, '');
- assert.equal(element._type, '');
- });
- });
-
- test('content is undefined', () => {
- sandbox.stub(element.$.restAPI, 'getFileContent')
- .returns(Promise.resolve({
- ok: true,
- type: 'text/javascript',
- }));
-
- return element._getFileData('1', 'test/path', 'edit').then(() => {
- assert.equal(element._newContent, '');
- assert.equal(element._content, '');
- assert.equal(element._type, 'text/javascript');
- });
- });
-
- test('content and type is undefined', () => {
- sandbox.stub(element.$.restAPI, 'getFileContent')
- .returns(Promise.resolve({
- ok: true,
- }));
-
- return element._getFileData('1', 'test/path', 'edit').then(() => {
- assert.equal(element._newContent, '');
- assert.equal(element._content, '');
- assert.equal(element._type, '');
- });
- });
- });
-
- test('_showAlert', done => {
- element.addEventListener('show-alert', e => {
- assert.deepEqual(e.detail, {message: 'test message'});
- assert.isTrue(e.bubbles);
- done();
- });
-
- element._showAlert('test message');
- });
-
- test('_viewEditInChangeView respects _patchNum', () => {
- navigateStub.restore();
- const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
- element._patchNum = element.EDIT_NAME;
- element._viewEditInChangeView();
- assert.equal(navStub.lastCall.args[1], element.EDIT_NAME);
- element._patchNum = '1';
- element._viewEditInChangeView();
- assert.equal(navStub.lastCall.args[1], '1');
- element._successfulSave = true;
- element._viewEditInChangeView();
- assert.equal(navStub.lastCall.args[1], element.EDIT_NAME);
- });
-
- suite('keyboard shortcuts', () => {
- // Used as the spy on the handler for each entry in keyBindings.
- let handleSpy;
-
- suite('_handleSaveShortcut', () => {
- let saveStub;
- setup(() => {
- handleSpy = sandbox.spy(element, '_handleSaveShortcut');
- saveStub = sandbox.stub(element, '_saveEdit');
- });
-
- test('save enabled', () => {
- element._content = '';
- element._newContent = '_test';
- MockInteractions.pressAndReleaseKeyOn(element, 83, 'ctrl', 's');
- flushAsynchronousOperations();
-
- assert.isTrue(handleSpy.calledOnce);
- assert.isTrue(saveStub.calledOnce);
-
- MockInteractions.pressAndReleaseKeyOn(element, 83, 'meta', 's');
- flushAsynchronousOperations();
-
- assert.equal(handleSpy.callCount, 2);
- assert.equal(saveStub.callCount, 2);
- });
-
- test('save disabled', () => {
- MockInteractions.pressAndReleaseKeyOn(element, 83, 'ctrl', 's');
- flushAsynchronousOperations();
-
- assert.isTrue(handleSpy.calledOnce);
- assert.isFalse(saveStub.called);
-
- MockInteractions.pressAndReleaseKeyOn(element, 83, 'meta', 's');
- flushAsynchronousOperations();
-
- assert.equal(handleSpy.callCount, 2);
- assert.isFalse(saveStub.called);
- });
- });
- });
-
- suite('gr-storage caching', () => {
- test('local edit exists', () => {
- sandbox.stub(element.$.storage, 'getEditableContentItem')
- .returns({message: 'pending edit'});
- sandbox.stub(element.$.restAPI, 'getFileContent')
- .returns(Promise.resolve({
- ok: true,
- type: 'text/javascript',
- content: 'old content',
- }));
-
- const alertStub = sandbox.stub();
- element.addEventListener('show-alert', alertStub);
-
- return element._getFileData(1, 'test', 1).then(() => {
- flushAsynchronousOperations();
-
- assert.isTrue(alertStub.called);
- assert.equal(element._newContent, 'pending edit');
- assert.equal(element._content, 'old content');
- assert.equal(element._type, 'text/javascript');
- });
- });
-
- test('local edit exists, is same as remote edit', () => {
- sandbox.stub(element.$.storage, 'getEditableContentItem')
- .returns({message: 'pending edit'});
- sandbox.stub(element.$.restAPI, 'getFileContent')
- .returns(Promise.resolve({
- ok: true,
- type: 'text/javascript',
- content: 'pending edit',
- }));
-
- const alertStub = sandbox.stub();
- element.addEventListener('show-alert', alertStub);
-
- return element._getFileData(1, 'test', 1).then(() => {
- flushAsynchronousOperations();
-
- assert.isFalse(alertStub.called);
- assert.equal(element._newContent, 'pending edit');
- assert.equal(element._content, 'pending edit');
- assert.equal(element._type, 'text/javascript');
- });
- });
-
- test('storage key computation', () => {
- element._changeNum = 1;
- element._patchNum = 1;
- element._path = 'test';
- assert.equal(element.storageKey, 'c1_ps1_test');
+ assert.equal(element._changeNum, mockParams.changeNum);
+ assert.equal(element._path, mockParams.path);
+ assert.deepEqual(changeDetailStub.lastCall.args[0],
+ mockParams.changeNum);
+ assert.deepEqual(fileStub.lastCall.args,
+ [mockParams.changeNum, mockParams.path, mockParams.patchNum]);
+
+ return promises.then(() => {
+ assert.equal(element._content, 'text');
+ assert.equal(element._newContent, 'text');
+ assert.equal(element._type, 'application/octet-stream');
});
});
});
+
+ test('edit file path', () => {
+ element._changeNum = mockParams.changeNum;
+ element._path = mockParams.path;
+ savePathStub.onFirstCall().returns(Promise.resolve({}));
+ savePathStub.onSecondCall().returns(Promise.resolve({ok: true}));
+
+ // Calling with the same path should not navigate.
+ return element._handlePathChanged({detail: mockParams.path}).then(() => {
+ assert.isFalse(savePathStub.called);
+ // !ok response
+ element._handlePathChanged({detail: 'newPath'}).then(() => {
+ assert.isTrue(savePathStub.called);
+ assert.isFalse(navigateStub.called);
+ // ok response
+ element._handlePathChanged({detail: 'newPath'}).then(() => {
+ assert.isTrue(navigateStub.called);
+ assert.isTrue(element._successfulSave);
+ });
+ });
+ });
+ });
+
+ test('reacts to content-change event', () => {
+ const storeStub = sandbox.spy(element.$.storage, 'setEditableContentItem');
+ element._newContent = 'test';
+ element.$.editorEndpoint.dispatchEvent(new CustomEvent('content-change', {
+ bubbles: true, composed: true,
+ detail: {value: 'new content value'},
+ }));
+ element.flushDebouncer('store');
+ flushAsynchronousOperations();
+
+ assert.equal(element._newContent, 'new content value');
+ assert.isTrue(storeStub.called);
+ assert.equal(storeStub.lastCall.args[1], 'new content value');
+ });
+
+ suite('edit file content', () => {
+ const originalText = 'file text';
+ const newText = 'file text changed';
+
+ setup(() => {
+ element._changeNum = mockParams.changeNum;
+ element._path = mockParams.path;
+ element._content = originalText;
+ element._newContent = originalText;
+ flushAsynchronousOperations();
+ });
+
+ test('initial load', () => {
+ assert.equal(element.$.file.fileContent, originalText);
+ assert.isTrue(element.$.save.hasAttribute('disabled'));
+ });
+
+ test('file modification and save, !ok response', () => {
+ const saveSpy = sandbox.spy(element, '_saveEdit');
+ const eraseStub = sandbox.stub(element.$.storage,
+ 'eraseEditableContentItem');
+ const alertStub = sandbox.stub(element, '_showAlert');
+ saveFileStub.returns(Promise.resolve({ok: false}));
+ element._newContent = newText;
+ flushAsynchronousOperations();
+
+ assert.isFalse(element.$.save.hasAttribute('disabled'));
+ assert.isFalse(element._saving);
+
+ MockInteractions.tap(element.$.save);
+ assert.isTrue(saveSpy.called);
+ assert.equal(alertStub.lastCall.args[0], 'Saving changes...');
+ assert.isTrue(element._saving);
+ assert.isTrue(element.$.save.hasAttribute('disabled'));
+
+ return saveSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(saveFileStub.called);
+ assert.isTrue(eraseStub.called);
+ assert.isFalse(element._saving);
+ assert.equal(alertStub.lastCall.args[0], 'Failed to save changes');
+ assert.deepEqual(saveFileStub.lastCall.args,
+ [mockParams.changeNum, mockParams.path, newText]);
+ assert.isFalse(navigateStub.called);
+ assert.isFalse(element.$.save.hasAttribute('disabled'));
+ assert.notEqual(element._content, element._newContent);
+ });
+ });
+
+ test('file modification and save', () => {
+ const saveSpy = sandbox.spy(element, '_saveEdit');
+ const alertStub = sandbox.stub(element, '_showAlert');
+ saveFileStub.returns(Promise.resolve({ok: true}));
+ element._newContent = newText;
+ flushAsynchronousOperations();
+
+ assert.isFalse(element._saving);
+ assert.isFalse(element.$.save.hasAttribute('disabled'));
+
+ MockInteractions.tap(element.$.save);
+ assert.isTrue(saveSpy.called);
+ assert.equal(alertStub.lastCall.args[0], 'Saving changes...');
+ assert.isTrue(element._saving);
+ assert.isTrue(element.$.save.hasAttribute('disabled'));
+
+ return saveSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(saveFileStub.called);
+ assert.isFalse(element._saving);
+ assert.equal(alertStub.lastCall.args[0], 'All changes saved');
+ assert.isFalse(navigateStub.called);
+ assert.isTrue(element.$.save.hasAttribute('disabled'));
+ assert.equal(element._content, element._newContent);
+ assert.isTrue(element._successfulSave);
+ });
+ });
+
+ test('file modification and close', () => {
+ const closeSpy = sandbox.spy(element, '_handleCloseTap');
+ element._newContent = newText;
+ flushAsynchronousOperations();
+
+ assert.isFalse(element.$.save.hasAttribute('disabled'));
+
+ MockInteractions.tap(element.$.close);
+ assert.isTrue(closeSpy.called);
+ assert.isFalse(saveFileStub.called);
+ assert.isTrue(navigateStub.called);
+ });
+ });
+
+ suite('_getFileData', () => {
+ setup(() => {
+ element._newContent = 'initial';
+ element._content = 'initial';
+ element._type = 'initial';
+ sandbox.stub(element.$.storage, 'getEditableContentItem').returns(null);
+ });
+
+ test('res.ok', () => {
+ sandbox.stub(element.$.restAPI, 'getFileContent')
+ .returns(Promise.resolve({
+ ok: true,
+ type: 'text/javascript',
+ content: 'new content',
+ }));
+
+ // Ensure no data is set with a bad response.
+ return element._getFileData('1', 'test/path', 'edit').then(() => {
+ assert.equal(element._newContent, 'new content');
+ assert.equal(element._content, 'new content');
+ assert.equal(element._type, 'text/javascript');
+ });
+ });
+
+ test('!res.ok', () => {
+ sandbox.stub(element.$.restAPI, 'getFileContent')
+ .returns(Promise.resolve({}));
+
+ // Ensure no data is set with a bad response.
+ return element._getFileData('1', 'test/path', 'edit').then(() => {
+ assert.equal(element._newContent, '');
+ assert.equal(element._content, '');
+ assert.equal(element._type, '');
+ });
+ });
+
+ test('content is undefined', () => {
+ sandbox.stub(element.$.restAPI, 'getFileContent')
+ .returns(Promise.resolve({
+ ok: true,
+ type: 'text/javascript',
+ }));
+
+ return element._getFileData('1', 'test/path', 'edit').then(() => {
+ assert.equal(element._newContent, '');
+ assert.equal(element._content, '');
+ assert.equal(element._type, 'text/javascript');
+ });
+ });
+
+ test('content and type is undefined', () => {
+ sandbox.stub(element.$.restAPI, 'getFileContent')
+ .returns(Promise.resolve({
+ ok: true,
+ }));
+
+ return element._getFileData('1', 'test/path', 'edit').then(() => {
+ assert.equal(element._newContent, '');
+ assert.equal(element._content, '');
+ assert.equal(element._type, '');
+ });
+ });
+ });
+
+ test('_showAlert', done => {
+ element.addEventListener('show-alert', e => {
+ assert.deepEqual(e.detail, {message: 'test message'});
+ assert.isTrue(e.bubbles);
+ done();
+ });
+
+ element._showAlert('test message');
+ });
+
+ test('_viewEditInChangeView respects _patchNum', () => {
+ navigateStub.restore();
+ const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ element._patchNum = element.EDIT_NAME;
+ element._viewEditInChangeView();
+ assert.equal(navStub.lastCall.args[1], element.EDIT_NAME);
+ element._patchNum = '1';
+ element._viewEditInChangeView();
+ assert.equal(navStub.lastCall.args[1], '1');
+ element._successfulSave = true;
+ element._viewEditInChangeView();
+ assert.equal(navStub.lastCall.args[1], element.EDIT_NAME);
+ });
+
+ suite('keyboard shortcuts', () => {
+ // Used as the spy on the handler for each entry in keyBindings.
+ let handleSpy;
+
+ suite('_handleSaveShortcut', () => {
+ let saveStub;
+ setup(() => {
+ handleSpy = sandbox.spy(element, '_handleSaveShortcut');
+ saveStub = sandbox.stub(element, '_saveEdit');
+ });
+
+ test('save enabled', () => {
+ element._content = '';
+ element._newContent = '_test';
+ MockInteractions.pressAndReleaseKeyOn(element, 83, 'ctrl', 's');
+ flushAsynchronousOperations();
+
+ assert.isTrue(handleSpy.calledOnce);
+ assert.isTrue(saveStub.calledOnce);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 83, 'meta', 's');
+ flushAsynchronousOperations();
+
+ assert.equal(handleSpy.callCount, 2);
+ assert.equal(saveStub.callCount, 2);
+ });
+
+ test('save disabled', () => {
+ MockInteractions.pressAndReleaseKeyOn(element, 83, 'ctrl', 's');
+ flushAsynchronousOperations();
+
+ assert.isTrue(handleSpy.calledOnce);
+ assert.isFalse(saveStub.called);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 83, 'meta', 's');
+ flushAsynchronousOperations();
+
+ assert.equal(handleSpy.callCount, 2);
+ assert.isFalse(saveStub.called);
+ });
+ });
+ });
+
+ suite('gr-storage caching', () => {
+ test('local edit exists', () => {
+ sandbox.stub(element.$.storage, 'getEditableContentItem')
+ .returns({message: 'pending edit'});
+ sandbox.stub(element.$.restAPI, 'getFileContent')
+ .returns(Promise.resolve({
+ ok: true,
+ type: 'text/javascript',
+ content: 'old content',
+ }));
+
+ const alertStub = sandbox.stub();
+ element.addEventListener('show-alert', alertStub);
+
+ return element._getFileData(1, 'test', 1).then(() => {
+ flushAsynchronousOperations();
+
+ assert.isTrue(alertStub.called);
+ assert.equal(element._newContent, 'pending edit');
+ assert.equal(element._content, 'old content');
+ assert.equal(element._type, 'text/javascript');
+ });
+ });
+
+ test('local edit exists, is same as remote edit', () => {
+ sandbox.stub(element.$.storage, 'getEditableContentItem')
+ .returns({message: 'pending edit'});
+ sandbox.stub(element.$.restAPI, 'getFileContent')
+ .returns(Promise.resolve({
+ ok: true,
+ type: 'text/javascript',
+ content: 'pending edit',
+ }));
+
+ const alertStub = sandbox.stub();
+ element.addEventListener('show-alert', alertStub);
+
+ return element._getFileData(1, 'test', 1).then(() => {
+ flushAsynchronousOperations();
+
+ assert.isFalse(alertStub.called);
+ assert.equal(element._newContent, 'pending edit');
+ assert.equal(element._content, 'pending edit');
+ assert.equal(element._type, 'text/javascript');
+ });
+ });
+
+ test('storage key computation', () => {
+ element._changeNum = 1;
+ element._patchNum = 1;
+ element._path = 'test';
+ assert.equal(element.storageKey, 'c1_ps1_test');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/gr-app-element.html b/polygerrit-ui/app/elements/gr-app-element.html
deleted file mode 100644
index 62f2967..0000000
--- a/polygerrit-ui/app/elements/gr-app-element.html
+++ /dev/null
@@ -1,242 +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.
--->
-<script src="/bower_components/moment/moment.js"></script>
-<script src="../scripts/util.js"></script>
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../styles/shared-styles.html">
-<link rel="import" href="../styles/themes/app-theme.html">
-<link rel="import" href="./admin/gr-admin-view/gr-admin-view.html">
-<link rel="import" href="./documentation/gr-documentation-search/gr-documentation-search.html">
-<link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html">
-<link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html">
-<link rel="import" href="./change/gr-change-view/gr-change-view.html">
-<link rel="import" href="./core/gr-error-manager/gr-error-manager.html">
-<link rel="import" href="./core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html">
-<link rel="import" href="./core/gr-main-header/gr-main-header.html">
-<link rel="import" href="./core/gr-navigation/gr-navigation.html">
-<link rel="import" href="./core/gr-reporting/gr-reporting.html">
-<link rel="import" href="./core/gr-router/gr-router.html">
-<link rel="import" href="./core/gr-smart-search/gr-smart-search.html">
-<link rel="import" href="./diff/gr-diff-view/gr-diff-view.html">
-<link rel="import" href="./edit/gr-editor-view/gr-editor-view.html">
-<link rel="import" href="./plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="./plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="./plugins/gr-external-style/gr-external-style.html">
-<link rel="import" href="./plugins/gr-plugin-host/gr-plugin-host.html">
-<link rel="import" href="./settings/gr-cla-view/gr-cla-view.html">
-<link rel="import" href="./settings/gr-registration-dialog/gr-registration-dialog.html">
-<link rel="import" href="./settings/gr-settings-view/gr-settings-view.html">
-<link rel="import" href="./shared/gr-fixed-panel/gr-fixed-panel.html">
-<link rel="import" href="./shared/gr-lib-loader/gr-lib-loader.html">
-<link rel="import" href="./shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-app-element">
- <template>
- <style include="shared-styles">
- :host {
- background-color: var(--background-color-tertiary);
- display: flex;
- flex-direction: column;
- min-height: 100%;
- }
- gr-fixed-panel {
- /**
- * This one should be greater that the z-index in gr-diff-view
- * because gr-main-header contains overlay.
- */
- z-index: 10;
- }
- gr-main-header,
- footer {
- color: var(--primary-text-color);
- }
- gr-main-header {
- background: var(--header-background, var(--header-background-color, #eee));
- padding: var(--header-padding);
- border-bottom: var(--header-border-bottom);
- border-image: var(--header-border-image);
- border-right: 0;
- border-left: 0;
- border-top: 0;
- box-shadow: var(--header-box-shadow);
- }
- footer {
- background: var(--footer-background, var(--footer-background-color, #eee));
- border-top: var(--footer-border-top);
- display: flex;
- justify-content: space-between;
- padding: var(--spacing-m) var(--spacing-l);
- z-index: 100;
- }
- main {
- flex: 1;
- padding-bottom: var(--spacing-xxl);
- position: relative;
- }
- .errorView {
- align-items: center;
- display: none;
- flex-direction: column;
- justify-content: center;
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- }
- .errorView.show {
- display: flex;
- }
- .errorEmoji {
- font-size: 2.6rem;
- }
- .errorText,
- .errorMoreInfo {
- margin-top: var(--spacing-m);
- }
- .errorText {
- font-family: var(--header-font-family);
- font-size: var(--font-size-h3);
- font-weight: var(--font-weight-h3);
- line-height: var(--line-height-h3);
- }
- .errorMoreInfo {
- color: var(--deemphasized-text-color);
- }
- .feedback {
- color: var(--error-text-color);
- }
- </style>
- <gr-endpoint-decorator name="banner"></gr-endpoint-decorator>
- <gr-fixed-panel id="header">
- <gr-main-header
- id="mainHeader"
- search-query="{{params.query}}"
- on-mobile-search="_mobileSearchToggle"
- login-url="[[_loginUrl]]"
- >
- </gr-main-header>
- </gr-fixed-panel>
- <main>
- <gr-smart-search
- id="search"
- search-query="{{params.query}}"
- hidden="[[!mobileSearch]]">
- </gr-smart-search>
- <template is="dom-if" if="[[_showChangeListView]]" restamp="true">
- <gr-change-list-view
- params="[[params]]"
- account="[[_account]]"
- view-state="{{_viewState.changeListView}}"></gr-change-list-view>
- </template>
- <template is="dom-if" if="[[_showDashboardView]]" restamp="true">
- <gr-dashboard-view
- account="[[_account]]"
- params="[[params]]"
- view-state="{{_viewState.dashboardView}}"></gr-dashboard-view>
- </template>
- <template is="dom-if" if="[[_showChangeView]]" restamp="true">
- <gr-change-view
- params="[[params]]"
- view-state="{{_viewState.changeView}}"
- back-page="[[_lastSearchPage]]"></gr-change-view>
- </template>
- <template is="dom-if" if="[[_showEditorView]]" restamp="true">
- <gr-editor-view
- params="[[params]]"></gr-editor-view>
- </template>
- <template is="dom-if" if="[[_showDiffView]]" restamp="true">
- <gr-diff-view
- params="[[params]]"
- change-view-state="{{_viewState.changeView}}"></gr-diff-view>
- </template>
- <template is="dom-if" if="[[_showSettingsView]]" restamp="true">
- <gr-settings-view
- params="[[params]]"
- on-account-detail-update="_handleAccountDetailUpdate">
- </gr-settings-view>
- </template>
- <template is="dom-if" if="[[_showAdminView]]" restamp="true">
- <gr-admin-view path="[[_path]]"
- params=[[params]]></gr-admin-view>
- </template>
- <template is="dom-if" if="[[_showPluginScreen]]" restamp="true">
- <gr-endpoint-decorator name="[[_pluginScreenName]]">
- <gr-endpoint-param name="token" value="[[params.screen]]"></gr-endpoint-param>
- </gr-endpoint-decorator>
- </template>
- <template is="dom-if" if="[[_showCLAView]]" restamp="true">
- <gr-cla-view></gr-cla-view>
- </template>
- <template is="dom-if" if="[[_showDocumentationSearch]]" restamp="true">
- <gr-documentation-search
- params="[[params]]">
- </gr-documentation-search>
- </template>
- <div id="errorView" class="errorView">
- <div class="errorEmoji">[[_lastError.emoji]]</div>
- <div class="errorText">[[_lastError.text]]</div>
- <div class="errorMoreInfo">[[_lastError.moreInfo]]</div>
- </div>
- </main>
- <footer r="contentinfo">
- <div>
- Powered by <a href="https://www.gerritcodereview.com/" rel="noopener"
- target="_blank">Gerrit Code Review</a>
- ([[_version]])
- <gr-endpoint-decorator name="footer-left"></gr-endpoint-decorator>
- </div>
- <div>
- <template is="dom-if" if="[[_feedbackUrl]]">
- <a class="feedback"
- href$="[[_feedbackUrl]]"
- rel="noopener"
- target="_blank">Report bug</a> |
- </template>
- Press “?” for keyboard shortcuts
- <gr-endpoint-decorator name="footer-right"></gr-endpoint-decorator>
- </div>
- </footer>
- <gr-overlay id="keyboardShortcuts" with-backdrop>
- <gr-keyboard-shortcuts-dialog
- on-close="_handleKeyboardShortcutDialogClose"></gr-keyboard-shortcuts-dialog>
- </gr-overlay>
- <gr-overlay id="registrationOverlay" with-backdrop>
- <gr-registration-dialog
- id="registrationDialog"
- settings-url="[[_settingsUrl]]"
- on-account-detail-update="_handleAccountDetailUpdate"
- on-close="_handleRegistrationDialogClose">
- </gr-registration-dialog>
- </gr-overlay>
- <gr-endpoint-decorator name="plugin-overlay"></gr-endpoint-decorator>
- <gr-error-manager id="errorManager" login-url="[[_loginUrl]]"></gr-error-manager>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- <gr-reporting id="reporting"></gr-reporting>
- <gr-router id="router"></gr-router>
- <gr-plugin-host id="plugins"
- config="[[_serverConfig]]">
- </gr-plugin-host>
- <gr-lib-loader id="libLoader"></gr-lib-loader>
- <gr-external-style id="externalStyleForAll" name="app-theme"></gr-external-style>
- <gr-external-style id="externalStyleForTheme" name="[[getThemeEndpoint()]]"></gr-external-style>
- </template>
- <script src="gr-app-element.js" crossorigin="anonymous"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index ea5a180..70806e2 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -14,531 +14,574 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../scripts/util.js';
+import '../scripts/bundled-polymer.js';
+import '../behaviors/base-url-behavior/base-url-behavior.js';
+import '../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../styles/shared-styles.js';
+import '../styles/themes/app-theme.js';
+import './admin/gr-admin-view/gr-admin-view.js';
+import './documentation/gr-documentation-search/gr-documentation-search.js';
+import './change-list/gr-change-list-view/gr-change-list-view.js';
+import './change-list/gr-dashboard-view/gr-dashboard-view.js';
+import './change/gr-change-view/gr-change-view.js';
+import './core/gr-error-manager/gr-error-manager.js';
+import './core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js';
+import './core/gr-main-header/gr-main-header.js';
+import './core/gr-navigation/gr-navigation.js';
+import './core/gr-reporting/gr-reporting.js';
+import './core/gr-router/gr-router.js';
+import './core/gr-smart-search/gr-smart-search.js';
+import './diff/gr-diff-view/gr-diff-view.js';
+import './edit/gr-editor-view/gr-editor-view.js';
+import './plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import './plugins/gr-endpoint-param/gr-endpoint-param.js';
+import './plugins/gr-external-style/gr-external-style.js';
+import './plugins/gr-plugin-host/gr-plugin-host.js';
+import './settings/gr-cla-view/gr-cla-view.js';
+import './settings/gr-registration-dialog/gr-registration-dialog.js';
+import './settings/gr-settings-view/gr-settings-view.js';
+import './shared/gr-fixed-panel/gr-fixed-panel.js';
+import './shared/gr-lib-loader/gr-lib-loader.js';
+import './shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import moment from 'moment/src/moment.js';
+self.moment = moment;
+import {htmlTemplate} from './gr-app-element_html.js';
+
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrAppElement extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-app-element'; }
/**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * Fired when the URL location changes.
+ *
+ * @event location-change
*/
- class GrAppElement extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-app-element'; }
- /**
- * Fired when the URL location changes.
- *
- * @event location-change
- */
- static get properties() {
- return {
+ static get properties() {
+ return {
+ /**
+ * @type {{ query: string, view: string, screen: string }}
+ */
+ params: Object,
+ keyEventTarget: {
+ type: Object,
+ value() { return document.body; },
+ },
+
+ _account: {
+ type: Object,
+ observer: '_accountChanged',
+ },
+
/**
- * @type {{ query: string, view: string, screen: string }}
+ * The last time the g key was pressed in milliseconds (or a keydown event
+ * was handled if the key is held down).
+ *
+ * @type {number|null}
*/
- params: Object,
- keyEventTarget: {
- type: Object,
- value() { return document.body; },
- },
+ _lastGKeyPressTimestamp: {
+ type: Number,
+ value: null,
+ },
- _account: {
- type: Object,
- observer: '_accountChanged',
- },
+ /**
+ * @type {{ plugin: Object }}
+ */
+ _serverConfig: Object,
+ _version: String,
+ _showChangeListView: Boolean,
+ _showDashboardView: Boolean,
+ _showChangeView: Boolean,
+ _showDiffView: Boolean,
+ _showSettingsView: Boolean,
+ _showAdminView: Boolean,
+ _showCLAView: Boolean,
+ _showEditorView: Boolean,
+ _showPluginScreen: Boolean,
+ _showDocumentationSearch: Boolean,
+ /** @type {?} */
+ _viewState: Object,
+ /** @type {?} */
+ _lastError: Object,
+ _lastSearchPage: String,
+ _path: String,
+ _pluginScreenName: {
+ type: String,
+ computed: '_computePluginScreenName(params)',
+ },
+ _settingsUrl: String,
+ _feedbackUrl: String,
+ // Used to allow searching on mobile
+ mobileSearch: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * The last time the g key was pressed in milliseconds (or a keydown event
- * was handled if the key is held down).
- *
- * @type {number|null}
- */
- _lastGKeyPressTimestamp: {
- type: Number,
- value: null,
- },
+ /**
+ * Other elements in app must open this URL when
+ * user login is required.
+ */
+ _loginUrl: {
+ type: String,
+ value: '/login',
+ },
+ };
+ }
- /**
- * @type {{ plugin: Object }}
- */
- _serverConfig: Object,
- _version: String,
- _showChangeListView: Boolean,
- _showDashboardView: Boolean,
- _showChangeView: Boolean,
- _showDiffView: Boolean,
- _showSettingsView: Boolean,
- _showAdminView: Boolean,
- _showCLAView: Boolean,
- _showEditorView: Boolean,
- _showPluginScreen: Boolean,
- _showDocumentationSearch: Boolean,
- /** @type {?} */
- _viewState: Object,
- /** @type {?} */
- _lastError: Object,
- _lastSearchPage: String,
- _path: String,
- _pluginScreenName: {
- type: String,
- computed: '_computePluginScreenName(params)',
- },
- _settingsUrl: String,
- _feedbackUrl: String,
- // Used to allow searching on mobile
- mobileSearch: {
- type: Boolean,
- value: false,
- },
+ static get observers() {
+ return [
+ '_viewChanged(params.view)',
+ '_paramsChanged(params.*)',
+ ];
+ }
- /**
- * Other elements in app must open this URL when
- * user login is required.
- */
- _loginUrl: {
- type: String,
- value: '/login',
- },
- };
- }
+ keyboardShortcuts() {
+ return {
+ [this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG]: '_showKeyboardShortcuts',
+ [this.Shortcut.GO_TO_USER_DASHBOARD]: '_goToUserDashboard',
+ [this.Shortcut.GO_TO_OPENED_CHANGES]: '_goToOpenedChanges',
+ [this.Shortcut.GO_TO_MERGED_CHANGES]: '_goToMergedChanges',
+ [this.Shortcut.GO_TO_ABANDONED_CHANGES]: '_goToAbandonedChanges',
+ [this.Shortcut.GO_TO_WATCHED_CHANGES]: '_goToWatchedChanges',
+ };
+ }
- static get observers() {
- return [
- '_viewChanged(params.view)',
- '_paramsChanged(params.*)',
- ];
- }
+ /** @override */
+ created() {
+ super.created();
+ this._bindKeyboardShortcuts();
+ this.addEventListener('page-error',
+ e => this._handlePageError(e));
+ this.addEventListener('title-change',
+ e => this._handleTitleChange(e));
+ this.addEventListener('location-change',
+ e => this._handleLocationChange(e));
+ this.addEventListener('rpc-log',
+ e => this._handleRpcLog(e));
+ this.addEventListener('shortcut-triggered',
+ e => this._handleShortcutTriggered(e));
+ }
- keyboardShortcuts() {
- return {
- [this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG]: '_showKeyboardShortcuts',
- [this.Shortcut.GO_TO_USER_DASHBOARD]: '_goToUserDashboard',
- [this.Shortcut.GO_TO_OPENED_CHANGES]: '_goToOpenedChanges',
- [this.Shortcut.GO_TO_MERGED_CHANGES]: '_goToMergedChanges',
- [this.Shortcut.GO_TO_ABANDONED_CHANGES]: '_goToAbandonedChanges',
- [this.Shortcut.GO_TO_WATCHED_CHANGES]: '_goToWatchedChanges',
- };
- }
+ /** @override */
+ ready() {
+ super.ready();
+ this._updateLoginUrl();
+ this.$.reporting.appStarted();
+ this.$.router.start();
- /** @override */
- created() {
- super.created();
- this._bindKeyboardShortcuts();
- this.addEventListener('page-error',
- e => this._handlePageError(e));
- this.addEventListener('title-change',
- e => this._handleTitleChange(e));
- this.addEventListener('location-change',
- e => this._handleLocationChange(e));
- this.addEventListener('rpc-log',
- e => this._handleRpcLog(e));
- this.addEventListener('shortcut-triggered',
- e => this._handleShortcutTriggered(e));
- }
+ this.$.restAPI.getAccount().then(account => {
+ this._account = account;
+ });
+ this.$.restAPI.getConfig().then(config => {
+ this._serverConfig = config;
- /** @override */
- ready() {
- super.ready();
- this._updateLoginUrl();
- this.$.reporting.appStarted();
- this.$.router.start();
-
- this.$.restAPI.getAccount().then(account => {
- this._account = account;
- });
- this.$.restAPI.getConfig().then(config => {
- this._serverConfig = config;
-
- if (config && config.gerrit && config.gerrit.report_bug_url) {
- this._feedbackUrl = config.gerrit.report_bug_url;
- }
- });
- this.$.restAPI.getVersion().then(version => {
- this._version = version;
- this._logWelcome();
- });
-
- if (window.localStorage.getItem('dark-theme')) {
- // No need to add the style module to element again as it's imported
- // by importHref already
- this.$.libLoader.getDarkTheme();
+ if (config && config.gerrit && config.gerrit.report_bug_url) {
+ this._feedbackUrl = config.gerrit.report_bug_url;
}
+ });
+ this.$.restAPI.getVersion().then(version => {
+ this._version = version;
+ this._logWelcome();
+ });
- // Note: this is evaluated here to ensure that it only happens after the
- // router has been initialized. @see Issue 7837
- this._settingsUrl = Gerrit.Nav.getUrlForSettings();
-
- this._viewState = {
- changeView: {
- changeNum: null,
- patchRange: null,
- selectedFileIndex: 0,
- showReplyDialog: false,
- diffMode: null,
- numFilesShown: null,
- scrollTop: 0,
- },
- changeListView: {
- query: null,
- offset: 0,
- selectedChangeIndex: 0,
- },
- dashboardView: {
- selectedChangeIndex: 0,
- },
- };
+ if (window.localStorage.getItem('dark-theme')) {
+ // No need to add the style module to element again as it's imported
+ // by importHref already
+ this.$.libLoader.getDarkTheme();
}
- _bindKeyboardShortcuts() {
- this.bindShortcut(this.Shortcut.SEND_REPLY,
- this.DOC_ONLY, 'ctrl+enter', 'meta+enter');
- this.bindShortcut(this.Shortcut.EMOJI_DROPDOWN,
- this.DOC_ONLY, ':');
+ // Note: this is evaluated here to ensure that it only happens after the
+ // router has been initialized. @see Issue 7837
+ this._settingsUrl = Gerrit.Nav.getUrlForSettings();
- this.bindShortcut(
- this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG, '?');
- this.bindShortcut(
- this.Shortcut.GO_TO_USER_DASHBOARD, this.GO_KEY, 'i');
- this.bindShortcut(
- this.Shortcut.GO_TO_OPENED_CHANGES, this.GO_KEY, 'o');
- this.bindShortcut(
- this.Shortcut.GO_TO_MERGED_CHANGES, this.GO_KEY, 'm');
- this.bindShortcut(
- this.Shortcut.GO_TO_ABANDONED_CHANGES, this.GO_KEY, 'a');
- this.bindShortcut(
- this.Shortcut.GO_TO_WATCHED_CHANGES, this.GO_KEY, 'w');
+ this._viewState = {
+ changeView: {
+ changeNum: null,
+ patchRange: null,
+ selectedFileIndex: 0,
+ showReplyDialog: false,
+ diffMode: null,
+ numFilesShown: null,
+ scrollTop: 0,
+ },
+ changeListView: {
+ query: null,
+ offset: 0,
+ selectedChangeIndex: 0,
+ },
+ dashboardView: {
+ selectedChangeIndex: 0,
+ },
+ };
+ }
- this.bindShortcut(
- this.Shortcut.CURSOR_NEXT_CHANGE, 'j');
- this.bindShortcut(
- this.Shortcut.CURSOR_PREV_CHANGE, 'k');
- this.bindShortcut(
- this.Shortcut.OPEN_CHANGE, 'o');
- this.bindShortcut(
- this.Shortcut.NEXT_PAGE, 'n', ']');
- this.bindShortcut(
- this.Shortcut.PREV_PAGE, 'p', '[');
- this.bindShortcut(
- this.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r:keyup');
- this.bindShortcut(
- this.Shortcut.TOGGLE_CHANGE_STAR, 's:keyup');
- this.bindShortcut(
- this.Shortcut.REFRESH_CHANGE_LIST, 'shift+r:keyup');
- this.bindShortcut(
- this.Shortcut.EDIT_TOPIC, 't');
+ _bindKeyboardShortcuts() {
+ this.bindShortcut(this.Shortcut.SEND_REPLY,
+ this.DOC_ONLY, 'ctrl+enter', 'meta+enter');
+ this.bindShortcut(this.Shortcut.EMOJI_DROPDOWN,
+ this.DOC_ONLY, ':');
- this.bindShortcut(
- this.Shortcut.OPEN_REPLY_DIALOG, 'a');
- this.bindShortcut(
- this.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
- this.bindShortcut(
- this.Shortcut.EXPAND_ALL_MESSAGES, 'x');
- this.bindShortcut(
- this.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
- this.bindShortcut(
- this.Shortcut.REFRESH_CHANGE, 'shift+r:keyup');
- this.bindShortcut(
- this.Shortcut.UP_TO_DASHBOARD, 'u');
- this.bindShortcut(
- this.Shortcut.UP_TO_CHANGE, 'u');
- this.bindShortcut(
- this.Shortcut.TOGGLE_DIFF_MODE, 'm:keyup');
+ this.bindShortcut(
+ this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG, '?');
+ this.bindShortcut(
+ this.Shortcut.GO_TO_USER_DASHBOARD, this.GO_KEY, 'i');
+ this.bindShortcut(
+ this.Shortcut.GO_TO_OPENED_CHANGES, this.GO_KEY, 'o');
+ this.bindShortcut(
+ this.Shortcut.GO_TO_MERGED_CHANGES, this.GO_KEY, 'm');
+ this.bindShortcut(
+ this.Shortcut.GO_TO_ABANDONED_CHANGES, this.GO_KEY, 'a');
+ this.bindShortcut(
+ this.Shortcut.GO_TO_WATCHED_CHANGES, this.GO_KEY, 'w');
- this.bindShortcut(
- this.Shortcut.NEXT_LINE, 'j', 'down');
- this.bindShortcut(
- this.Shortcut.PREV_LINE, 'k', 'up');
- if (this._isCursorManagerSupportMoveToVisibleLine()) {
- this.bindShortcut(
- this.Shortcut.VISIBLE_LINE, '.');
- }
- this.bindShortcut(
- this.Shortcut.NEXT_CHUNK, 'n');
- this.bindShortcut(
- this.Shortcut.PREV_CHUNK, 'p');
- this.bindShortcut(
- this.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
- this.bindShortcut(
- this.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
- this.bindShortcut(
- this.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
- this.bindShortcut(
- this.Shortcut.EXPAND_ALL_COMMENT_THREADS, this.DOC_ONLY, 'e');
- this.bindShortcut(
- this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
- this.DOC_ONLY, 'shift+e');
- this.bindShortcut(
- this.Shortcut.LEFT_PANE, 'shift+left');
- this.bindShortcut(
- this.Shortcut.RIGHT_PANE, 'shift+right');
- this.bindShortcut(
- this.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
- this.bindShortcut(
- this.Shortcut.NEW_COMMENT, 'c');
- this.bindShortcut(
- this.Shortcut.SAVE_COMMENT,
- 'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
- this.bindShortcut(
- this.Shortcut.OPEN_DIFF_PREFS, ',');
- this.bindShortcut(
- this.Shortcut.TOGGLE_DIFF_REVIEWED, 'r:keyup');
+ this.bindShortcut(
+ this.Shortcut.CURSOR_NEXT_CHANGE, 'j');
+ this.bindShortcut(
+ this.Shortcut.CURSOR_PREV_CHANGE, 'k');
+ this.bindShortcut(
+ this.Shortcut.OPEN_CHANGE, 'o');
+ this.bindShortcut(
+ this.Shortcut.NEXT_PAGE, 'n', ']');
+ this.bindShortcut(
+ this.Shortcut.PREV_PAGE, 'p', '[');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r:keyup');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_CHANGE_STAR, 's:keyup');
+ this.bindShortcut(
+ this.Shortcut.REFRESH_CHANGE_LIST, 'shift+r:keyup');
+ this.bindShortcut(
+ this.Shortcut.EDIT_TOPIC, 't');
- this.bindShortcut(
- this.Shortcut.NEXT_FILE, ']');
- this.bindShortcut(
- this.Shortcut.PREV_FILE, '[');
- this.bindShortcut(
- this.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
- this.bindShortcut(
- this.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
- this.bindShortcut(
- this.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
- this.bindShortcut(
- this.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
- this.bindShortcut(
- this.Shortcut.OPEN_FILE, 'o', 'enter');
- this.bindShortcut(
- this.Shortcut.TOGGLE_FILE_REVIEWED, 'r:keyup');
- this.bindShortcut(
- this.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
- this.bindShortcut(
- this.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
- this.bindShortcut(
- this.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
- this.bindShortcut(
- this.Shortcut.TOGGLE_BLAME, 'b');
+ this.bindShortcut(
+ this.Shortcut.OPEN_REPLY_DIALOG, 'a');
+ this.bindShortcut(
+ this.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
+ this.bindShortcut(
+ this.Shortcut.EXPAND_ALL_MESSAGES, 'x');
+ this.bindShortcut(
+ this.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
+ this.bindShortcut(
+ this.Shortcut.REFRESH_CHANGE, 'shift+r:keyup');
+ this.bindShortcut(
+ this.Shortcut.UP_TO_DASHBOARD, 'u');
+ this.bindShortcut(
+ this.Shortcut.UP_TO_CHANGE, 'u');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_DIFF_MODE, 'm:keyup');
+ this.bindShortcut(
+ this.Shortcut.NEXT_LINE, 'j', 'down');
+ this.bindShortcut(
+ this.Shortcut.PREV_LINE, 'k', 'up');
+ if (this._isCursorManagerSupportMoveToVisibleLine()) {
this.bindShortcut(
- this.Shortcut.OPEN_FIRST_FILE, ']');
- this.bindShortcut(
- this.Shortcut.OPEN_LAST_FILE, '[');
-
- this.bindShortcut(
- this.Shortcut.SEARCH, '/');
+ this.Shortcut.VISIBLE_LINE, '.');
}
+ this.bindShortcut(
+ this.Shortcut.NEXT_CHUNK, 'n');
+ this.bindShortcut(
+ this.Shortcut.PREV_CHUNK, 'p');
+ this.bindShortcut(
+ this.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
+ this.bindShortcut(
+ this.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
+ this.bindShortcut(
+ this.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
+ this.bindShortcut(
+ this.Shortcut.EXPAND_ALL_COMMENT_THREADS, this.DOC_ONLY, 'e');
+ this.bindShortcut(
+ this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
+ this.DOC_ONLY, 'shift+e');
+ this.bindShortcut(
+ this.Shortcut.LEFT_PANE, 'shift+left');
+ this.bindShortcut(
+ this.Shortcut.RIGHT_PANE, 'shift+right');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
+ this.bindShortcut(
+ this.Shortcut.NEW_COMMENT, 'c');
+ this.bindShortcut(
+ this.Shortcut.SAVE_COMMENT,
+ 'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
+ this.bindShortcut(
+ this.Shortcut.OPEN_DIFF_PREFS, ',');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_DIFF_REVIEWED, 'r:keyup');
- _isCursorManagerSupportMoveToVisibleLine() {
- // This method is a copy-paste from the
- // method _isIntersectionObserverSupported of gr-cursor-manager.js
- // It is better share this method with gr-cursor-manager,
- // but doing it require a lot if changes instead of 1-line copied code
- return 'IntersectionObserver' in window;
+ this.bindShortcut(
+ this.Shortcut.NEXT_FILE, ']');
+ this.bindShortcut(
+ this.Shortcut.PREV_FILE, '[');
+ this.bindShortcut(
+ this.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
+ this.bindShortcut(
+ this.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
+ this.bindShortcut(
+ this.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
+ this.bindShortcut(
+ this.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
+ this.bindShortcut(
+ this.Shortcut.OPEN_FILE, 'o', 'enter');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_FILE_REVIEWED, 'r:keyup');
+ this.bindShortcut(
+ this.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_BLAME, 'b');
+
+ this.bindShortcut(
+ this.Shortcut.OPEN_FIRST_FILE, ']');
+ this.bindShortcut(
+ this.Shortcut.OPEN_LAST_FILE, '[');
+
+ this.bindShortcut(
+ this.Shortcut.SEARCH, '/');
+ }
+
+ _isCursorManagerSupportMoveToVisibleLine() {
+ // This method is a copy-paste from the
+ // method _isIntersectionObserverSupported of gr-cursor-manager.js
+ // It is better share this method with gr-cursor-manager,
+ // but doing it require a lot if changes instead of 1-line copied code
+ return 'IntersectionObserver' in window;
+ }
+
+ _accountChanged(account) {
+ if (!account) { return; }
+
+ // Preferences are cached when a user is logged in; warm them.
+ this.$.restAPI.getPreferences();
+ this.$.restAPI.getDiffPreferences();
+ this.$.restAPI.getEditPreferences();
+ this.$.errorManager.knownAccountId =
+ this._account && this._account._account_id || null;
+ }
+
+ _viewChanged(view) {
+ this.$.errorView.classList.remove('show');
+ this.set('_showChangeListView', view === Gerrit.Nav.View.SEARCH);
+ this.set('_showDashboardView', view === Gerrit.Nav.View.DASHBOARD);
+ this.set('_showChangeView', view === Gerrit.Nav.View.CHANGE);
+ this.set('_showDiffView', view === Gerrit.Nav.View.DIFF);
+ this.set('_showSettingsView', view === Gerrit.Nav.View.SETTINGS);
+ this.set('_showAdminView', view === Gerrit.Nav.View.ADMIN ||
+ view === Gerrit.Nav.View.GROUP || view === Gerrit.Nav.View.REPO);
+ this.set('_showCLAView', view === Gerrit.Nav.View.AGREEMENTS);
+ this.set('_showEditorView', view === Gerrit.Nav.View.EDIT);
+ const isPluginScreen = view === Gerrit.Nav.View.PLUGIN_SCREEN;
+ this.set('_showPluginScreen', false);
+ // Navigation within plugin screens does not restamp gr-endpoint-decorator
+ // because _showPluginScreen value does not change. To force restamp,
+ // change _showPluginScreen value between true and false.
+ if (isPluginScreen) {
+ this.async(() => this.set('_showPluginScreen', true), 1);
}
-
- _accountChanged(account) {
- if (!account) { return; }
-
- // Preferences are cached when a user is logged in; warm them.
- this.$.restAPI.getPreferences();
- this.$.restAPI.getDiffPreferences();
- this.$.restAPI.getEditPreferences();
- this.$.errorManager.knownAccountId =
- this._account && this._account._account_id || null;
- }
-
- _viewChanged(view) {
- this.$.errorView.classList.remove('show');
- this.set('_showChangeListView', view === Gerrit.Nav.View.SEARCH);
- this.set('_showDashboardView', view === Gerrit.Nav.View.DASHBOARD);
- this.set('_showChangeView', view === Gerrit.Nav.View.CHANGE);
- this.set('_showDiffView', view === Gerrit.Nav.View.DIFF);
- this.set('_showSettingsView', view === Gerrit.Nav.View.SETTINGS);
- this.set('_showAdminView', view === Gerrit.Nav.View.ADMIN ||
- view === Gerrit.Nav.View.GROUP || view === Gerrit.Nav.View.REPO);
- this.set('_showCLAView', view === Gerrit.Nav.View.AGREEMENTS);
- this.set('_showEditorView', view === Gerrit.Nav.View.EDIT);
- const isPluginScreen = view === Gerrit.Nav.View.PLUGIN_SCREEN;
- this.set('_showPluginScreen', false);
- // Navigation within plugin screens does not restamp gr-endpoint-decorator
- // because _showPluginScreen value does not change. To force restamp,
- // change _showPluginScreen value between true and false.
- if (isPluginScreen) {
- this.async(() => this.set('_showPluginScreen', true), 1);
- }
- this.set('_showDocumentationSearch',
- view === Gerrit.Nav.View.DOCUMENTATION_SEARCH);
- if (this.params.justRegistered) {
- this.$.registrationOverlay.open();
- this.$.registrationDialog.loadData().then(() => {
- this.$.registrationOverlay.refit();
- });
- }
- this.$.header.unfloat();
- }
-
- _handleShortcutTriggered(event) {
- const {event: e, goKey} = event.detail;
- // eg: {key: "k:keydown", ..., from: "gr-diff-view"}
- let key = `${e.key}:${e.type}`;
- if (goKey) key = 'g+' + key;
- if (e.shiftKey) key = 'shift+' + key;
- if (e.ctrlKey) key = 'ctrl+' + key;
- if (e.metaKey) key = 'meta+' + key;
- if (e.altKey) key = 'alt+' + key;
- this.$.reporting.reportInteraction('shortcut-triggered', {
- key,
- from: event.path && event.path[0]
- && event.path[0].nodeName || 'unknown',
+ this.set('_showDocumentationSearch',
+ view === Gerrit.Nav.View.DOCUMENTATION_SEARCH);
+ if (this.params.justRegistered) {
+ this.$.registrationOverlay.open();
+ this.$.registrationDialog.loadData().then(() => {
+ this.$.registrationOverlay.refit();
});
}
+ this.$.header.unfloat();
+ }
- _handlePageError(e) {
- const props = [
- '_showChangeListView',
- '_showDashboardView',
- '_showChangeView',
- '_showDiffView',
- '_showSettingsView',
- '_showAdminView',
- ];
- for (const showProp of props) {
- this.set(showProp, false);
- }
+ _handleShortcutTriggered(event) {
+ const {event: e, goKey} = event.detail;
+ // eg: {key: "k:keydown", ..., from: "gr-diff-view"}
+ let key = `${e.key}:${e.type}`;
+ if (goKey) key = 'g+' + key;
+ if (e.shiftKey) key = 'shift+' + key;
+ if (e.ctrlKey) key = 'ctrl+' + key;
+ if (e.metaKey) key = 'meta+' + key;
+ if (e.altKey) key = 'alt+' + key;
+ this.$.reporting.reportInteraction('shortcut-triggered', {
+ key,
+ from: event.path && event.path[0]
+ && event.path[0].nodeName || 'unknown',
+ });
+ }
- this.$.errorView.classList.add('show');
- const response = e.detail.response;
- const err = {text: [response.status, response.statusText].join(' ')};
- if (response.status === 404) {
- err.emoji = '¯\\_(ツ)_/¯';
+ _handlePageError(e) {
+ const props = [
+ '_showChangeListView',
+ '_showDashboardView',
+ '_showChangeView',
+ '_showDiffView',
+ '_showSettingsView',
+ '_showAdminView',
+ ];
+ for (const showProp of props) {
+ this.set(showProp, false);
+ }
+
+ this.$.errorView.classList.add('show');
+ const response = e.detail.response;
+ const err = {text: [response.status, response.statusText].join(' ')};
+ if (response.status === 404) {
+ err.emoji = '¯\\_(ツ)_/¯';
+ this._lastError = err;
+ } else {
+ err.emoji = 'o_O';
+ response.text().then(text => {
+ err.moreInfo = text;
this._lastError = err;
- } else {
- err.emoji = 'o_O';
- response.text().then(text => {
- err.moreInfo = text;
- this._lastError = err;
- });
- }
- }
-
- _handleLocationChange(e) {
- this._updateLoginUrl();
-
- const hash = e.detail.hash.substring(1);
- let pathname = e.detail.pathname;
- if (pathname.startsWith('/c/') && parseInt(hash, 10) > 0) {
- pathname += '@' + hash;
- }
- this.set('_path', pathname);
- }
-
- _updateLoginUrl() {
- const baseUrl = this.getBaseUrl();
- if (baseUrl) {
- // Strip the canonical path from the path since needing canonical in
- // the path is uneeded and breaks the url.
- this._loginUrl = baseUrl + '/login/' + encodeURIComponent(
- '/' + window.location.pathname.substring(baseUrl.length) +
- window.location.search +
- window.location.hash);
- } else {
- this._loginUrl = '/login/' + encodeURIComponent(
- window.location.pathname +
- window.location.search +
- window.location.hash);
- }
- }
-
- _paramsChanged(paramsRecord) {
- const params = paramsRecord.base;
- const viewsToCheck = [Gerrit.Nav.View.SEARCH, Gerrit.Nav.View.DASHBOARD];
- if (viewsToCheck.includes(params.view)) {
- this.set('_lastSearchPage', location.pathname);
- }
- }
-
- _handleTitleChange(e) {
- if (e.detail.title) {
- document.title = e.detail.title + ' · Gerrit Code Review';
- } else {
- document.title = '';
- }
- }
-
- _showKeyboardShortcuts(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
- this.$.keyboardShortcuts.open();
- }
-
- _handleKeyboardShortcutDialogClose() {
- this.$.keyboardShortcuts.close();
- }
-
- _handleAccountDetailUpdate(e) {
- this.$.mainHeader.reload();
- if (this.params.view === Gerrit.Nav.View.SETTINGS) {
- this.shadowRoot.querySelector('gr-settings-view').reloadAccountDetail();
- }
- }
-
- _handleRegistrationDialogClose(e) {
- this.params.justRegistered = false;
- this.$.registrationOverlay.close();
- }
-
- _goToOpenedChanges() {
- Gerrit.Nav.navigateToStatusSearch('open');
- }
-
- _goToUserDashboard() {
- Gerrit.Nav.navigateToUserDashboard();
- }
-
- _goToMergedChanges() {
- Gerrit.Nav.navigateToStatusSearch('merged');
- }
-
- _goToAbandonedChanges() {
- Gerrit.Nav.navigateToStatusSearch('abandoned');
- }
-
- _goToWatchedChanges() {
- // The query is hardcoded, and doesn't respect custom menu entries
- Gerrit.Nav.navigateToSearchQuery('is:watched is:open');
- }
-
- _computePluginScreenName({plugin, screen}) {
- if (!plugin || !screen) return '';
- return `${plugin}-screen-${screen}`;
- }
-
- _logWelcome() {
- console.group('Runtime Info');
- console.log('Gerrit UI (PolyGerrit)');
- console.log(`Gerrit Server Version: ${this._version}`);
- if (window.VERSION_INFO) {
- console.log(`UI Version Info: ${window.VERSION_INFO}`);
- }
- if (this._feedbackUrl) {
- console.log(`Please file bugs and feedback at: ${this._feedbackUrl}`);
- }
- console.groupEnd();
- }
-
- /**
- * Intercept RPC log events emitted by REST API interfaces.
- * Note: the REST API interface cannot use gr-reporting directly because
- * that would create a cyclic dependency.
- */
- _handleRpcLog(e) {
- this.$.reporting.reportRpcTiming(e.detail.anonymizedUrl,
- e.detail.elapsed);
- }
-
- _mobileSearchToggle(e) {
- this.mobileSearch = !this.mobileSearch;
- }
-
- getThemeEndpoint() {
- // For now, we only have dark mode and light mode
- return window.localStorage.getItem('dark-theme') ?
- 'app-theme-dark' :
- 'app-theme-light';
+ });
}
}
- customElements.define(GrAppElement.is, GrAppElement);
-})();
+ _handleLocationChange(e) {
+ this._updateLoginUrl();
+
+ const hash = e.detail.hash.substring(1);
+ let pathname = e.detail.pathname;
+ if (pathname.startsWith('/c/') && parseInt(hash, 10) > 0) {
+ pathname += '@' + hash;
+ }
+ this.set('_path', pathname);
+ }
+
+ _updateLoginUrl() {
+ const baseUrl = this.getBaseUrl();
+ if (baseUrl) {
+ // Strip the canonical path from the path since needing canonical in
+ // the path is uneeded and breaks the url.
+ this._loginUrl = baseUrl + '/login/' + encodeURIComponent(
+ '/' + window.location.pathname.substring(baseUrl.length) +
+ window.location.search +
+ window.location.hash);
+ } else {
+ this._loginUrl = '/login/' + encodeURIComponent(
+ window.location.pathname +
+ window.location.search +
+ window.location.hash);
+ }
+ }
+
+ _paramsChanged(paramsRecord) {
+ const params = paramsRecord.base;
+ const viewsToCheck = [Gerrit.Nav.View.SEARCH, Gerrit.Nav.View.DASHBOARD];
+ if (viewsToCheck.includes(params.view)) {
+ this.set('_lastSearchPage', location.pathname);
+ }
+ }
+
+ _handleTitleChange(e) {
+ if (e.detail.title) {
+ document.title = e.detail.title + ' · Gerrit Code Review';
+ } else {
+ document.title = '';
+ }
+ }
+
+ _showKeyboardShortcuts(e) {
+ // same shortcut should close the dialog if pressed again
+ // when dialog is open
+ if (this.$.keyboardShortcuts.opened) {
+ this.$.keyboardShortcuts.close();
+ return;
+ }
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+ this.$.keyboardShortcuts.open();
+ }
+
+ _handleKeyboardShortcutDialogClose() {
+ this.$.keyboardShortcuts.close();
+ }
+
+ _handleAccountDetailUpdate(e) {
+ this.$.mainHeader.reload();
+ if (this.params.view === Gerrit.Nav.View.SETTINGS) {
+ this.shadowRoot.querySelector('gr-settings-view').reloadAccountDetail();
+ }
+ }
+
+ _handleRegistrationDialogClose(e) {
+ this.params.justRegistered = false;
+ this.$.registrationOverlay.close();
+ }
+
+ _goToOpenedChanges() {
+ Gerrit.Nav.navigateToStatusSearch('open');
+ }
+
+ _goToUserDashboard() {
+ Gerrit.Nav.navigateToUserDashboard();
+ }
+
+ _goToMergedChanges() {
+ Gerrit.Nav.navigateToStatusSearch('merged');
+ }
+
+ _goToAbandonedChanges() {
+ Gerrit.Nav.navigateToStatusSearch('abandoned');
+ }
+
+ _goToWatchedChanges() {
+ // The query is hardcoded, and doesn't respect custom menu entries
+ Gerrit.Nav.navigateToSearchQuery('is:watched is:open');
+ }
+
+ _computePluginScreenName({plugin, screen}) {
+ if (!plugin || !screen) return '';
+ return `${plugin}-screen-${screen}`;
+ }
+
+ _logWelcome() {
+ console.group('Runtime Info');
+ console.log('Gerrit UI (PolyGerrit)');
+ console.log(`Gerrit Server Version: ${this._version}`);
+ if (window.VERSION_INFO) {
+ console.log(`UI Version Info: ${window.VERSION_INFO}`);
+ }
+ if (this._feedbackUrl) {
+ console.log(`Please file bugs and feedback at: ${this._feedbackUrl}`);
+ }
+ console.groupEnd();
+ }
+
+ /**
+ * Intercept RPC log events emitted by REST API interfaces.
+ * Note: the REST API interface cannot use gr-reporting directly because
+ * that would create a cyclic dependency.
+ */
+ _handleRpcLog(e) {
+ this.$.reporting.reportRpcTiming(e.detail.anonymizedUrl,
+ e.detail.elapsed);
+ }
+
+ _mobileSearchToggle(e) {
+ this.mobileSearch = !this.mobileSearch;
+ }
+
+ getThemeEndpoint() {
+ // For now, we only have dark mode and light mode
+ return window.localStorage.getItem('dark-theme') ?
+ 'app-theme-dark' :
+ 'app-theme-light';
+ }
+}
+
+customElements.define(GrAppElement.is, GrAppElement);
diff --git a/polygerrit-ui/app/elements/gr-app-element_html.js b/polygerrit-ui/app/elements/gr-app-element_html.js
new file mode 100644
index 0000000..3951d9d
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app-element_html.js
@@ -0,0 +1,174 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ background-color: var(--background-color-tertiary);
+ display: flex;
+ flex-direction: column;
+ min-height: 100%;
+ }
+ gr-fixed-panel {
+ /**
+ * This one should be greater that the z-index in gr-diff-view
+ * because gr-main-header contains overlay.
+ */
+ z-index: 10;
+ }
+ gr-main-header,
+ footer {
+ color: var(--primary-text-color);
+ }
+ gr-main-header {
+ background: var(--header-background, var(--header-background-color, #eee));
+ padding: var(--header-padding);
+ border-bottom: var(--header-border-bottom);
+ border-image: var(--header-border-image);
+ border-right: 0;
+ border-left: 0;
+ border-top: 0;
+ box-shadow: var(--header-box-shadow);
+ }
+ footer {
+ background: var(--footer-background, var(--footer-background-color, #eee));
+ border-top: var(--footer-border-top);
+ display: flex;
+ justify-content: space-between;
+ padding: var(--spacing-m) var(--spacing-l);
+ z-index: 100;
+ }
+ main {
+ flex: 1;
+ padding-bottom: var(--spacing-xxl);
+ position: relative;
+ }
+ .errorView {
+ align-items: center;
+ display: none;
+ flex-direction: column;
+ justify-content: center;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ }
+ .errorView.show {
+ display: flex;
+ }
+ .errorEmoji {
+ font-size: 2.6rem;
+ }
+ .errorText,
+ .errorMoreInfo {
+ margin-top: var(--spacing-m);
+ }
+ .errorText {
+ font-family: var(--header-font-family);
+ font-size: var(--font-size-h3);
+ font-weight: var(--font-weight-h3);
+ line-height: var(--line-height-h3);
+ }
+ .errorMoreInfo {
+ color: var(--deemphasized-text-color);
+ }
+ .feedback {
+ color: var(--error-text-color);
+ }
+ </style>
+ <gr-endpoint-decorator name="banner"></gr-endpoint-decorator>
+ <gr-fixed-panel id="header">
+ <gr-main-header id="mainHeader" search-query="{{params.query}}" on-mobile-search="_mobileSearchToggle" login-url="[[_loginUrl]]">
+ </gr-main-header>
+ </gr-fixed-panel>
+ <main>
+ <gr-smart-search id="search" search-query="{{params.query}}" hidden="[[!mobileSearch]]">
+ </gr-smart-search>
+ <template is="dom-if" if="[[_showChangeListView]]" restamp="true">
+ <gr-change-list-view params="[[params]]" account="[[_account]]" view-state="{{_viewState.changeListView}}"></gr-change-list-view>
+ </template>
+ <template is="dom-if" if="[[_showDashboardView]]" restamp="true">
+ <gr-dashboard-view account="[[_account]]" params="[[params]]" view-state="{{_viewState.dashboardView}}"></gr-dashboard-view>
+ </template>
+ <template is="dom-if" if="[[_showChangeView]]" restamp="true">
+ <gr-change-view params="[[params]]" view-state="{{_viewState.changeView}}" back-page="[[_lastSearchPage]]"></gr-change-view>
+ </template>
+ <template is="dom-if" if="[[_showEditorView]]" restamp="true">
+ <gr-editor-view params="[[params]]"></gr-editor-view>
+ </template>
+ <template is="dom-if" if="[[_showDiffView]]" restamp="true">
+ <gr-diff-view params="[[params]]" change-view-state="{{_viewState.changeView}}"></gr-diff-view>
+ </template>
+ <template is="dom-if" if="[[_showSettingsView]]" restamp="true">
+ <gr-settings-view params="[[params]]" on-account-detail-update="_handleAccountDetailUpdate">
+ </gr-settings-view>
+ </template>
+ <template is="dom-if" if="[[_showAdminView]]" restamp="true">
+ <gr-admin-view path="[[_path]]" params="[[params]]"></gr-admin-view>
+ </template>
+ <template is="dom-if" if="[[_showPluginScreen]]" restamp="true">
+ <gr-endpoint-decorator name="[[_pluginScreenName]]">
+ <gr-endpoint-param name="token" value="[[params.screen]]"></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </template>
+ <template is="dom-if" if="[[_showCLAView]]" restamp="true">
+ <gr-cla-view></gr-cla-view>
+ </template>
+ <template is="dom-if" if="[[_showDocumentationSearch]]" restamp="true">
+ <gr-documentation-search params="[[params]]">
+ </gr-documentation-search>
+ </template>
+ <div id="errorView" class="errorView">
+ <div class="errorEmoji">[[_lastError.emoji]]</div>
+ <div class="errorText">[[_lastError.text]]</div>
+ <div class="errorMoreInfo">[[_lastError.moreInfo]]</div>
+ </div>
+ </main>
+ <footer r="contentinfo">
+ <div>
+ Powered by <a href="https://www.gerritcodereview.com/" rel="noopener" target="_blank">Gerrit Code Review</a>
+ ([[_version]])
+ <gr-endpoint-decorator name="footer-left"></gr-endpoint-decorator>
+ </div>
+ <div>
+ <template is="dom-if" if="[[_feedbackUrl]]">
+ <a class="feedback" href\$="[[_feedbackUrl]]" rel="noopener" target="_blank">Report bug</a> |
+ </template>
+ Press “?” for keyboard shortcuts
+ <gr-endpoint-decorator name="footer-right"></gr-endpoint-decorator>
+ </div>
+ </footer>
+ <gr-overlay id="keyboardShortcuts" with-backdrop="">
+ <gr-keyboard-shortcuts-dialog on-close="_handleKeyboardShortcutDialogClose"></gr-keyboard-shortcuts-dialog>
+ </gr-overlay>
+ <gr-overlay id="registrationOverlay" with-backdrop="">
+ <gr-registration-dialog id="registrationDialog" settings-url="[[_settingsUrl]]" on-account-detail-update="_handleAccountDetailUpdate" on-close="_handleRegistrationDialogClose">
+ </gr-registration-dialog>
+ </gr-overlay>
+ <gr-endpoint-decorator name="plugin-overlay"></gr-endpoint-decorator>
+ <gr-error-manager id="errorManager" login-url="[[_loginUrl]]"></gr-error-manager>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ <gr-reporting id="reporting"></gr-reporting>
+ <gr-router id="router"></gr-router>
+ <gr-plugin-host id="plugins" config="[[_serverConfig]]">
+ </gr-plugin-host>
+ <gr-lib-loader id="libLoader"></gr-lib-loader>
+ <gr-external-style id="externalStyleForAll" name="app-theme"></gr-external-style>
+ <gr-external-style id="externalStyleForTheme" name="[[getThemeEndpoint()]]"></gr-external-style>
+`;
diff --git a/polygerrit-ui/app/elements/gr-app-init.js b/polygerrit-ui/app/elements/gr-app-init.js
new file mode 100644
index 0000000..d1851e8
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app-init.js
@@ -0,0 +1,24 @@
+/**
+ * @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.
+ */
+
+if (!window.Polymer) {
+ window.Polymer = {
+ passiveTouchGestures: true,
+ lazyRegister: true,
+ };
+}
+window.Gerrit = window.Gerrit || {};
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index ce6a693..1483f7a 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -1,46 +1 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<script>
- if (!window.Polymer) {
- window.Polymer = {
- passiveTouchGestures: true,
- lazyRegister: true,
- };
- }
- window.Gerrit = window.Gerrit || {};
-</script>
-<script src="./font-roboto-local-loader.js" type="module" />
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/polymer-resin/standalone/polymer-resin.html">
-<!-- TODO(taoalpha): Remove once all legacyUndefinedCheck removed. -->
-<link rel="import" href="../behaviors/safe-types-behavior/safe-types-behavior.html">
-<script>
- security.polymer_resin.install({
- allowedIdentifierPrefixes: [''],
- reportHandler: security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER,
- safeTypesBridge: Gerrit.SafeTypes.safeTypesBridge,
- });
-</script>
-
-<link rel="import" href="./gr-app-element.html">
-<dom-module id="gr-app">
- <template>
- <gr-app-element id="app-element"></gr-app-element>
- </template>
- <script src="gr-app.js" crossorigin="anonymous"></script>
-</dom-module>
+<script src='./gr-app.js' type='module'></script>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index da54ac4..ac5a04d 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,15 +14,38 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+/* TODO(taoalpha): Remove once all legacyUndefinedCheck removed. */
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+import './gr-app-init.js';
- /** @extends Polymer.Element */
- class GrApp extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-app'; }
- }
+import './font-roboto-local-loader.js';
+import '../scripts/bundled-polymer.js';
+import 'polymer-resin/standalone/polymer-resin.js';
+import '../behaviors/safe-types-behavior/safe-types-behavior.js';
+import './gr-app-element.js';
+import './change-list/gr-embed-dashboard/gr-embed-dashboard.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-app_html.js';
- customElements.define(GrApp.is, GrApp);
-})();
+security.polymer_resin.install({
+ allowedIdentifierPrefixes: [''],
+ reportHandler: security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER,
+ safeTypesBridge: Gerrit.SafeTypes.safeTypesBridge,
+});
+
+/** @extends Polymer.Element */
+class GrApp extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-app'; }
+}
+
+customElements.define(GrApp.is, GrApp);
diff --git a/polygerrit-ui/app/elements/gr-app_html.js b/polygerrit-ui/app/elements/gr-app_html.js
new file mode 100644
index 0000000..fcf773f
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app_html.js
@@ -0,0 +1,21 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <gr-app-element id="app-element"></gr-app-element>
+`;
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
index cc25e5a..71ebaa7 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -19,21 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-app</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../test/test-pre-setup.js"></script>
-<link rel="import" href="../test/common-test-setup.html"/>
-
-<script>
- const link = document.createElement('link');
- link.setAttribute('rel', 'import');
- link.setAttribute('href', 'gr-app.html');
- document.head.appendChild(link);
-</script>
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -41,74 +30,75 @@
</template>
</test-fixture>
-<script>
- suite('gr-app tests', async () => {
- await readyToTest();
- let sandbox;
- let element;
+<script type="module">
+import '../test/common-test-setup.js';
+import './gr-app.js';
+suite('gr-app tests', () => {
+ let sandbox;
+ let element;
- setup(done => {
- sandbox = sinon.sandbox.create();
- stub('gr-reporting', {
- appStarted: sandbox.stub(),
- });
- stub('gr-account-dropdown', {
- _getTopContent: sinon.stub(),
- });
- stub('gr-router', {
- start: sandbox.stub(),
- });
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve({}); },
- getAccountCapabilities() { return Promise.resolve({}); },
- getConfig() {
- return Promise.resolve({
- plugin: {},
- auth: {
- auth_type: undefined,
- },
- });
- },
- getPreferences() { return Promise.resolve({my: []}); },
- getDiffPreferences() { return Promise.resolve({}); },
- getEditPreferences() { return Promise.resolve({}); },
- getVersion() { return Promise.resolve(42); },
- probePath() { return Promise.resolve(42); },
- });
-
- element = fixture('basic');
- flush(done);
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-reporting', {
+ appStarted: sandbox.stub(),
+ });
+ stub('gr-account-dropdown', {
+ _getTopContent: sinon.stub(),
+ });
+ stub('gr-router', {
+ start: sandbox.stub(),
+ });
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve({}); },
+ getAccountCapabilities() { return Promise.resolve({}); },
+ getConfig() {
+ return Promise.resolve({
+ plugin: {},
+ auth: {
+ auth_type: undefined,
+ },
+ });
+ },
+ getPreferences() { return Promise.resolve({my: []}); },
+ getDiffPreferences() { return Promise.resolve({}); },
+ getEditPreferences() { return Promise.resolve({}); },
+ getVersion() { return Promise.resolve(42); },
+ probePath() { return Promise.resolve(42); },
});
- teardown(() => {
- sandbox.restore();
- });
+ element = fixture('basic');
+ flush(done);
+ });
- appElement = () => element.$['app-element'];
+ teardown(() => {
+ sandbox.restore();
+ });
- test('reporting', () => {
- assert.isTrue(appElement().$.reporting.appStarted.calledOnce);
- });
+ const appElement = () => element.$['app-element'];
- test('reporting called before router start', () => {
- const element = appElement();
- const appStartedStub = element.$.reporting.appStarted;
- const routerStartStub = element.$.router.start;
- sinon.assert.callOrder(appStartedStub, routerStartStub);
- });
+ test('reporting', () => {
+ assert.isTrue(appElement().$.reporting.appStarted.calledOnce);
+ });
- test('passes config to gr-plugin-host', () => {
- const config = appElement().$.restAPI.getConfig;
- return config.lastCall.returnValue.then(config => {
- assert.deepEqual(appElement().$.plugins.config, config);
- });
- });
+ test('reporting called before router start', () => {
+ const element = appElement();
+ const appStartedStub = element.$.reporting.appStarted;
+ const routerStartStub = element.$.router.start;
+ sinon.assert.callOrder(appStartedStub, routerStartStub);
+ });
- test('_paramsChanged sets search page', () => {
- appElement()._paramsChanged({base: {view: Gerrit.Nav.View.CHANGE}});
- assert.notOk(appElement()._lastSearchPage);
- appElement()._paramsChanged({base: {view: Gerrit.Nav.View.SEARCH}});
- assert.ok(appElement()._lastSearchPage);
+ test('passes config to gr-plugin-host', () => {
+ const config = appElement().$.restAPI.getConfig;
+ return config.lastCall.returnValue.then(config => {
+ assert.deepEqual(appElement().$.plugins.config, config);
});
});
+
+ test('_paramsChanged sets search page', () => {
+ appElement()._paramsChanged({base: {view: Gerrit.Nav.View.CHANGE}});
+ assert.notOk(appElement()._lastSearchPage);
+ appElement()._paramsChanged({base: {view: Gerrit.Nav.View.SEARCH}});
+ assert.ok(appElement()._lastSearchPage);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.html b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.html
deleted file mode 100644
index 756c435..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<script src="gr-admin-api.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
index f10f922..de1fc86 100644
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
@@ -19,54 +19,50 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-admin-api</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-<link rel="import" href="gr-admin-api.html">
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import './gr-admin-api.js';
+suite('gr-admin-api tests', () => {
+ let sandbox;
+ let adminApi;
-<script>
- suite('gr-admin-api tests', async () => {
- await readyToTest();
- let sandbox;
- let adminApi;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- Gerrit._loadPlugins([]);
- adminApi = plugin.admin();
- });
-
- teardown(() => {
- adminApi = null;
- sandbox.restore();
- });
-
- test('exists', () => {
- assert.isOk(adminApi);
- });
-
- test('addMenuLink', () => {
- adminApi.addMenuLink('text', 'url');
- const links = adminApi.getMenuLinks();
- assert.equal(links.length, 1);
- assert.deepEqual(links[0], {text: 'text', url: 'url', capability: null});
- });
-
- test('addMenuLinkWithCapability', () => {
- adminApi.addMenuLink('text', 'url', 'capability');
- const links = adminApi.getMenuLinks();
- assert.equal(links.length, 1);
- assert.deepEqual(links[0],
- {text: 'text', url: 'url', capability: 'capability'});
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ Gerrit._loadPlugins([]);
+ adminApi = plugin.admin();
});
+
+ teardown(() => {
+ adminApi = null;
+ sandbox.restore();
+ });
+
+ test('exists', () => {
+ assert.isOk(adminApi);
+ });
+
+ test('addMenuLink', () => {
+ adminApi.addMenuLink('text', 'url');
+ const links = adminApi.getMenuLinks();
+ assert.equal(links.length, 1);
+ assert.deepEqual(links[0], {text: 'text', url: 'url', capability: null});
+ });
+
+ test('addMenuLinkWithCapability', () => {
+ adminApi.addMenuLink('text', 'url', 'capability');
+ const links = adminApi.getMenuLinks();
+ assert.equal(links.length, 1);
+ assert.deepEqual(links[0],
+ {text: 'text', url: 'url', capability: 'capability'});
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html
deleted file mode 100644
index ece8677..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<dom-module id="gr-attribute-helper">
- <script src="gr-attribute-helper.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
index 0cff8e9..09620ef 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
@@ -14,6 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-attribute-helper">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
index 87d3142..fd2fd5b 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
@@ -19,30 +19,26 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-attribute-helper</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-attribute-helper.html"/>
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<dom-element id="some-element">
- <script>
- readyToTest().then(() => {
- Polymer({
- is: 'some-element',
- properties: {
- fooBar: {
- type: Object,
- notify: true,
- },
- },
- });
- });
- </script>
+ <script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-attribute-helper.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+Polymer({
+ is: 'some-element',
+ properties: {
+ fooBar: {
+ type: Object,
+ notify: true,
+ },
+ },
+});
+</script>
</dom-element>
@@ -52,54 +48,55 @@
</template>
</test-fixture>
-<script>
- suite('gr-attribute-helper tests', async () => {
- await readyToTest();
- let element;
- let instance;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-attribute-helper.js';
+suite('gr-attribute-helper tests', () => {
+ let element;
+ let instance;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- instance = new GrAttributeHelper(element);
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('resolved on value change from undefined', () => {
- const promise = instance.get('fooBar').then(value => {
- assert.equal(value, 'foo! bar!');
- });
- element.fooBar = 'foo! bar!';
- return promise;
- });
-
- test('resolves to current attribute value', () => {
- element.fooBar = 'foo-foo-bar';
- const promise = instance.get('fooBar').then(value => {
- assert.equal(value, 'foo-foo-bar');
- });
- element.fooBar = 'no bar';
- return promise;
- });
-
- test('bind', () => {
- const stub = sandbox.stub();
- element.fooBar = 'bar foo';
- const unbind = instance.bind('fooBar', stub);
- element.fooBar = 'partridge in a foo tree';
- element.fooBar = 'five gold bars';
- assert.equal(stub.callCount, 3);
- assert.deepEqual(stub.args[0], ['bar foo']);
- assert.deepEqual(stub.args[1], ['partridge in a foo tree']);
- assert.deepEqual(stub.args[2], ['five gold bars']);
- stub.reset();
- unbind();
- instance.fooBar = 'ladies dancing';
- assert.isFalse(stub.called);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ instance = new GrAttributeHelper(element);
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('resolved on value change from undefined', () => {
+ const promise = instance.get('fooBar').then(value => {
+ assert.equal(value, 'foo! bar!');
+ });
+ element.fooBar = 'foo! bar!';
+ return promise;
+ });
+
+ test('resolves to current attribute value', () => {
+ element.fooBar = 'foo-foo-bar';
+ const promise = instance.get('fooBar').then(value => {
+ assert.equal(value, 'foo-foo-bar');
+ });
+ element.fooBar = 'no bar';
+ return promise;
+ });
+
+ test('bind', () => {
+ const stub = sandbox.stub();
+ element.fooBar = 'bar foo';
+ const unbind = instance.bind('fooBar', stub);
+ element.fooBar = 'partridge in a foo tree';
+ element.fooBar = 'five gold bars';
+ assert.equal(stub.callCount, 3);
+ assert.deepEqual(stub.args[0], ['bar foo']);
+ assert.deepEqual(stub.args[1], ['partridge in a foo tree']);
+ assert.deepEqual(stub.args[2], ['five gold bars']);
+ stub.reset();
+ unbind();
+ instance.fooBar = 'ladies dancing';
+ assert.isFalse(stub.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html
deleted file mode 100644
index dd532e1..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<dom-module id="gr-change-metadata-api">
- <script src="gr-change-metadata-api.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
index 80abf23..daf48f0 100644
--- a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
@@ -14,6 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-change-metadata-api">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.html b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.html
deleted file mode 100644
index 8b9000f..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<dom-module id="gr-dom-hooks">
- <script src="gr-dom-hooks.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
index fb9adb5..9497493 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
@@ -14,6 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-dom-hooks">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
index 524b1b9..70d7a7d 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
@@ -19,16 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-dom-hooks</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-dom-hooks.html"/>
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,131 +30,133 @@
</template>
</test-fixture>
-<script>
- suite('gr-dom-hooks tests', async () => {
- await readyToTest();
- const PUBLIC_METHODS =[
- 'onAttached',
- 'onDetached',
- 'getLastAttached',
- 'getAllAttached',
- 'getModuleName',
- ];
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-dom-hooks.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+suite('gr-dom-hooks tests', () => {
+ const PUBLIC_METHODS =[
+ 'onAttached',
+ 'onDetached',
+ 'getLastAttached',
+ 'getAllAttached',
+ 'getModuleName',
+ ];
- let instance;
- let sandbox;
- let hook;
- let hookInternal;
+ let instance;
+ let sandbox;
+ let hook;
+ let hookInternal;
- setup(() => {
- sandbox = sinon.sandbox.create();
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- instance = new GrDomHooksManager(plugin);
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ instance = new GrDomHooksManager(plugin);
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('placeholder', () => {
+ setup(()=>{
+ sandbox.stub(GrDomHook.prototype, '_createPlaceholder');
+ hookInternal = instance.getDomHook('foo-bar');
+ hook = hookInternal.getPublicAPI();
});
- teardown(() => {
- sandbox.restore();
+ test('public hook API has only public methods', () => {
+ assert.deepEqual(Object.keys(hook), PUBLIC_METHODS);
});
- suite('placeholder', () => {
- setup(()=>{
- sandbox.stub(GrDomHook.prototype, '_createPlaceholder');
- hookInternal = instance.getDomHook('foo-bar');
- hook = hookInternal.getPublicAPI();
- });
-
- test('public hook API has only public methods', () => {
- assert.deepEqual(Object.keys(hook), PUBLIC_METHODS);
- });
-
- test('registers placeholder class', () => {
- assert.isTrue(hookInternal._createPlaceholder.calledWithExactly(
- 'testplugin-autogenerated-foo-bar'));
- });
-
- test('getModuleName()', () => {
- const hookName = Object.keys(instance._hooks).pop();
- assert.equal(hookName, 'testplugin-autogenerated-foo-bar');
- assert.equal(hook.getModuleName(), 'testplugin-autogenerated-foo-bar');
- });
+ test('registers placeholder class', () => {
+ assert.isTrue(hookInternal._createPlaceholder.calledWithExactly(
+ 'testplugin-autogenerated-foo-bar'));
});
- suite('custom element', () => {
- setup(() => {
- hookInternal = instance.getDomHook('foo-bar', 'my-el');
- hook = hookInternal.getPublicAPI();
- });
-
- test('public hook API has only public methods', () => {
- assert.deepEqual(Object.keys(hook), PUBLIC_METHODS);
- });
-
- test('getModuleName()', () => {
- const hookName = Object.keys(instance._hooks).pop();
- assert.equal(hookName, 'foo-bar my-el');
- assert.equal(hook.getModuleName(), 'my-el');
- });
-
- test('onAttached', () => {
- const onAttachedSpy = sandbox.spy();
- hook.onAttached(onAttachedSpy);
- const [el1, el2] = [
- document.createElement(hook.getModuleName()),
- document.createElement(hook.getModuleName()),
- ];
- hookInternal.handleInstanceAttached(el1);
- hookInternal.handleInstanceAttached(el2);
- assert.isTrue(onAttachedSpy.firstCall.calledWithExactly(el1));
- assert.isTrue(onAttachedSpy.secondCall.calledWithExactly(el2));
- });
-
- test('onDetached', () => {
- const onDetachedSpy = sandbox.spy();
- hook.onDetached(onDetachedSpy);
- const [el1, el2] = [
- document.createElement(hook.getModuleName()),
- document.createElement(hook.getModuleName()),
- ];
- hookInternal.handleInstanceDetached(el1);
- assert.isTrue(onDetachedSpy.firstCall.calledWithExactly(el1));
- hookInternal.handleInstanceDetached(el2);
- assert.isTrue(onDetachedSpy.secondCall.calledWithExactly(el2));
- });
-
- test('getAllAttached', () => {
- const [el1, el2] = [
- document.createElement(hook.getModuleName()),
- document.createElement(hook.getModuleName()),
- ];
- el1.textContent = 'one';
- el2.textContent = 'two';
- hookInternal.handleInstanceAttached(el1);
- hookInternal.handleInstanceAttached(el2);
- assert.deepEqual([el1, el2], hook.getAllAttached());
- hookInternal.handleInstanceDetached(el1);
- assert.deepEqual([el2], hook.getAllAttached());
- });
-
- test('getLastAttached', () => {
- const beforeAttachedPromise = hook.getLastAttached().then(
- el => assert.strictEqual(el1, el));
- const [el1, el2] = [
- document.createElement(hook.getModuleName()),
- document.createElement(hook.getModuleName()),
- ];
- el1.textContent = 'one';
- el2.textContent = 'two';
- hookInternal.handleInstanceAttached(el1);
- hookInternal.handleInstanceAttached(el2);
- const afterAttachedPromise = hook.getLastAttached().then(
- el => assert.strictEqual(el2, el));
- return Promise.all([
- beforeAttachedPromise,
- afterAttachedPromise,
- ]);
- });
+ test('getModuleName()', () => {
+ const hookName = Object.keys(instance._hooks).pop();
+ assert.equal(hookName, 'testplugin-autogenerated-foo-bar');
+ assert.equal(hook.getModuleName(), 'testplugin-autogenerated-foo-bar');
});
});
+
+ suite('custom element', () => {
+ setup(() => {
+ hookInternal = instance.getDomHook('foo-bar', 'my-el');
+ hook = hookInternal.getPublicAPI();
+ });
+
+ test('public hook API has only public methods', () => {
+ assert.deepEqual(Object.keys(hook), PUBLIC_METHODS);
+ });
+
+ test('getModuleName()', () => {
+ const hookName = Object.keys(instance._hooks).pop();
+ assert.equal(hookName, 'foo-bar my-el');
+ assert.equal(hook.getModuleName(), 'my-el');
+ });
+
+ test('onAttached', () => {
+ const onAttachedSpy = sandbox.spy();
+ hook.onAttached(onAttachedSpy);
+ const [el1, el2] = [
+ document.createElement(hook.getModuleName()),
+ document.createElement(hook.getModuleName()),
+ ];
+ hookInternal.handleInstanceAttached(el1);
+ hookInternal.handleInstanceAttached(el2);
+ assert.isTrue(onAttachedSpy.firstCall.calledWithExactly(el1));
+ assert.isTrue(onAttachedSpy.secondCall.calledWithExactly(el2));
+ });
+
+ test('onDetached', () => {
+ const onDetachedSpy = sandbox.spy();
+ hook.onDetached(onDetachedSpy);
+ const [el1, el2] = [
+ document.createElement(hook.getModuleName()),
+ document.createElement(hook.getModuleName()),
+ ];
+ hookInternal.handleInstanceDetached(el1);
+ assert.isTrue(onDetachedSpy.firstCall.calledWithExactly(el1));
+ hookInternal.handleInstanceDetached(el2);
+ assert.isTrue(onDetachedSpy.secondCall.calledWithExactly(el2));
+ });
+
+ test('getAllAttached', () => {
+ const [el1, el2] = [
+ document.createElement(hook.getModuleName()),
+ document.createElement(hook.getModuleName()),
+ ];
+ el1.textContent = 'one';
+ el2.textContent = 'two';
+ hookInternal.handleInstanceAttached(el1);
+ hookInternal.handleInstanceAttached(el2);
+ assert.deepEqual([el1, el2], hook.getAllAttached());
+ hookInternal.handleInstanceDetached(el1);
+ assert.deepEqual([el2], hook.getAllAttached());
+ });
+
+ test('getLastAttached', () => {
+ const beforeAttachedPromise = hook.getLastAttached().then(
+ el => assert.strictEqual(el1, el));
+ const [el1, el2] = [
+ document.createElement(hook.getModuleName()),
+ document.createElement(hook.getModuleName()),
+ ];
+ el1.textContent = 'one';
+ el2.textContent = 'two';
+ hookInternal.handleInstanceAttached(el1);
+ hookInternal.handleInstanceAttached(el2);
+ const afterAttachedPromise = hook.getLastAttached().then(
+ el => assert.strictEqual(el2, el));
+ return Promise.all([
+ beforeAttachedPromise,
+ afterAttachedPromise,
+ ]);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html
deleted file mode 100644
index ab892ac..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html
+++ /dev/null
@@ -1,26 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-
-<dom-module id="gr-endpoint-decorator">
- <template strip-whitespace>
- <slot></slot>
- </template>
- <script src="gr-endpoint-decorator.js"></script>
-</dom-module>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
index 1c10642..e1624b9 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
@@ -14,155 +14,163 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const INIT_PROPERTIES_TIMEOUT_MS = 10000;
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import {importHref} from '../../../scripts/import-href.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-endpoint-decorator_html.js';
- /** @extends Polymer.Element */
- class GrEndpointDecorator extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-endpoint-decorator'; }
+const INIT_PROPERTIES_TIMEOUT_MS = 10000;
- static get properties() {
- return {
- name: String,
- /** @type {!Map} */
- _domHooks: {
- type: Map,
- value() { return new Map(); },
- },
- /**
- * This map prevents importing the same endpoint twice.
- * Without caching, if a plugin is loaded after the loaded plugins
- * callback fires, it will be imported twice and appear twice on the page.
- *
- * @type {!Map}
- */
- _initializedPlugins: {
- type: Map,
- value() { return new Map(); },
- },
- };
- }
+/** @extends Polymer.Element */
+class GrEndpointDecorator extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /** @override */
- detached() {
- super.detached();
- for (const [el, domHook] of this._domHooks) {
- domHook.handleInstanceDetached(el);
- }
- }
+ static get is() { return 'gr-endpoint-decorator'; }
- /**
- * @suppress {checkTypes}
- */
- _import(url) {
- return new Promise((resolve, reject) => {
- Polymer.importHref(url, resolve, reject);
- });
- }
+ static get properties() {
+ return {
+ name: String,
+ /** @type {!Map} */
+ _domHooks: {
+ type: Map,
+ value() { return new Map(); },
+ },
+ /**
+ * This map prevents importing the same endpoint twice.
+ * Without caching, if a plugin is loaded after the loaded plugins
+ * callback fires, it will be imported twice and appear twice on the page.
+ *
+ * @type {!Map}
+ */
+ _initializedPlugins: {
+ type: Map,
+ value() { return new Map(); },
+ },
+ };
+ }
- _initDecoration(name, plugin) {
- const el = document.createElement(name);
- return this._initProperties(el, plugin,
- this.getContentChildren().find(
- el => el.nodeName !== 'GR-ENDPOINT-PARAM'))
- .then(el => this._appendChild(el));
- }
-
- _initReplacement(name, plugin) {
- this.getContentChildNodes()
- .filter(node => node.nodeName !== 'GR-ENDPOINT-PARAM')
- .forEach(node => node.remove());
- const el = document.createElement(name);
- return this._initProperties(el, plugin).then(
- el => this._appendChild(el));
- }
-
- _getEndpointParams() {
- return Array.from(
- Polymer.dom(this).querySelectorAll('gr-endpoint-param'));
- }
-
- /**
- * @param {!Element} el
- * @param {!Object} plugin
- * @param {!Element=} opt_content
- * @return {!Promise<Element>}
- */
- _initProperties(el, plugin, opt_content) {
- el.plugin = plugin;
- if (opt_content) {
- el.content = opt_content;
- }
- const expectProperties = this._getEndpointParams().map(paramEl => {
- const helper = plugin.attributeHelper(paramEl);
- const paramName = paramEl.getAttribute('name');
- return helper.get('value').then(
- value => helper.bind('value',
- value => plugin.attributeHelper(el).set(paramName, value))
- );
- });
- let timeoutId;
- const timeout = new Promise(
- resolve => timeoutId = setTimeout(() => {
- console.warn(
- 'Timeout waiting for endpoint properties initialization: ' +
- `plugin ${plugin.getPluginName()}, endpoint ${this.name}`);
- }, INIT_PROPERTIES_TIMEOUT_MS));
- return Promise.race([timeout, Promise.all(expectProperties)])
- .then(() => {
- clearTimeout(timeoutId);
- return el;
- });
- }
-
- _appendChild(el) {
- return Polymer.dom(this.root).appendChild(el);
- }
-
- _initModule({moduleName, plugin, type, domHook}) {
- const name = plugin.getPluginName() + '.' + moduleName;
- if (this._initializedPlugins.get(name)) {
- return;
- }
- let initPromise;
- switch (type) {
- case 'decorate':
- initPromise = this._initDecoration(moduleName, plugin);
- break;
- case 'replace':
- initPromise = this._initReplacement(moduleName, plugin);
- break;
- }
- if (!initPromise) {
- console.warn('Unable to initialize module ' + name);
- }
- this._initializedPlugins.set(name, true);
- initPromise.then(el => {
- domHook.handleInstanceAttached(el);
- this._domHooks.set(el, domHook);
- });
- }
-
- /** @override */
- ready() {
- super.ready();
- Gerrit._endpoints.onNewEndpoint(this.name, this._initModule.bind(this));
- Gerrit.awaitPluginsLoaded()
- .then(() => Promise.all(
- Gerrit._endpoints.getPlugins(this.name).map(
- pluginUrl => this._import(pluginUrl)))
- )
- .then(() =>
- Gerrit._endpoints
- .getDetails(this.name)
- .forEach(this._initModule, this)
- );
+ /** @override */
+ detached() {
+ super.detached();
+ for (const [el, domHook] of this._domHooks) {
+ domHook.handleInstanceDetached(el);
}
}
- customElements.define(GrEndpointDecorator.is, GrEndpointDecorator);
-})();
+ /**
+ * @suppress {checkTypes}
+ */
+ _import(url) {
+ return new Promise((resolve, reject) => {
+ importHref(url, resolve, reject);
+ });
+ }
+
+ _initDecoration(name, plugin) {
+ const el = document.createElement(name);
+ return this._initProperties(el, plugin,
+ this.getContentChildren().find(
+ el => el.nodeName !== 'GR-ENDPOINT-PARAM'))
+ .then(el => this._appendChild(el));
+ }
+
+ _initReplacement(name, plugin) {
+ this.getContentChildNodes()
+ .filter(node => node.nodeName !== 'GR-ENDPOINT-PARAM')
+ .forEach(node => node.remove());
+ const el = document.createElement(name);
+ return this._initProperties(el, plugin).then(
+ el => this._appendChild(el));
+ }
+
+ _getEndpointParams() {
+ return Array.from(
+ dom(this).querySelectorAll('gr-endpoint-param'));
+ }
+
+ /**
+ * @param {!Element} el
+ * @param {!Object} plugin
+ * @param {!Element=} opt_content
+ * @return {!Promise<Element>}
+ */
+ _initProperties(el, plugin, opt_content) {
+ el.plugin = plugin;
+ if (opt_content) {
+ el.content = opt_content;
+ }
+ const expectProperties = this._getEndpointParams().map(paramEl => {
+ const helper = plugin.attributeHelper(paramEl);
+ const paramName = paramEl.getAttribute('name');
+ return helper.get('value').then(
+ value => helper.bind('value',
+ value => plugin.attributeHelper(el).set(paramName, value))
+ );
+ });
+ let timeoutId;
+ const timeout = new Promise(
+ resolve => timeoutId = setTimeout(() => {
+ console.warn(
+ 'Timeout waiting for endpoint properties initialization: ' +
+ `plugin ${plugin.getPluginName()}, endpoint ${this.name}`);
+ }, INIT_PROPERTIES_TIMEOUT_MS));
+ return Promise.race([timeout, Promise.all(expectProperties)])
+ .then(() => {
+ clearTimeout(timeoutId);
+ return el;
+ });
+ }
+
+ _appendChild(el) {
+ return dom(this.root).appendChild(el);
+ }
+
+ _initModule({moduleName, plugin, type, domHook}) {
+ const name = plugin.getPluginName() + '.' + moduleName;
+ if (this._initializedPlugins.get(name)) {
+ return;
+ }
+ let initPromise;
+ switch (type) {
+ case 'decorate':
+ initPromise = this._initDecoration(moduleName, plugin);
+ break;
+ case 'replace':
+ initPromise = this._initReplacement(moduleName, plugin);
+ break;
+ }
+ if (!initPromise) {
+ console.warn('Unable to initialize module ' + name);
+ }
+ this._initializedPlugins.set(name, true);
+ initPromise.then(el => {
+ domHook.handleInstanceAttached(el);
+ this._domHooks.set(el, domHook);
+ });
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ Gerrit._endpoints.onNewEndpoint(this.name, this._initModule.bind(this));
+ Gerrit.awaitPluginsLoaded()
+ .then(() => Promise.all(
+ Gerrit._endpoints.getPlugins(this.name).map(
+ pluginUrl => this._import(pluginUrl)))
+ )
+ .then(() =>
+ Gerrit._endpoints
+ .getDetails(this.name)
+ .forEach(this._initModule, this)
+ );
+ }
+}
+
+customElements.define(GrEndpointDecorator.is, GrEndpointDecorator);
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.js
new file mode 100644
index 0000000..1644c07
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.js
@@ -0,0 +1,21 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <slot></slot>
+`;
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
index e0d91fa..d167446 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
@@ -19,16 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-endpoint-decorator</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-endpoint-decorator.html">
-<link rel="import" href="../gr-endpoint-param/gr-endpoint-param.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -46,149 +40,152 @@
</template>
</test-fixture>
-<script>
- suite('gr-endpoint-decorator', async () => {
- await readyToTest();
- let container;
- let sandbox;
- let plugin;
- let decorationHook;
- let replacementHook;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-endpoint-decorator.js';
+import '../gr-endpoint-param/gr-endpoint-param.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-endpoint-decorator', () => {
+ let container;
+ let sandbox;
+ let plugin;
+ let decorationHook;
+ let replacementHook;
- setup(done => {
- sandbox = sinon.sandbox.create();
- stub('gr-endpoint-decorator', {
- _import: sandbox.stub().returns(Promise.resolve()),
- });
- Gerrit._testOnly_resetPlugins();
- container = fixture('basic');
- Gerrit.install(p => plugin = p, '0.1', 'http://some/plugin/url.html');
- // Decoration
- decorationHook = plugin.registerCustomComponent('first', 'some-module');
- // Replacement
- replacementHook = plugin.registerCustomComponent(
- 'second', 'other-module', {replace: true});
- // Mimic all plugins loaded.
- Gerrit._loadPlugins([]);
- flush(done);
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-endpoint-decorator', {
+ _import: sandbox.stub().returns(Promise.resolve()),
});
+ Gerrit._testOnly_resetPlugins();
+ container = fixture('basic');
+ Gerrit.install(p => plugin = p, '0.1', 'http://some/plugin/url.html');
+ // Decoration
+ decorationHook = plugin.registerCustomComponent('first', 'some-module');
+ // Replacement
+ replacementHook = plugin.registerCustomComponent(
+ 'second', 'other-module', {replace: true});
+ // Mimic all plugins loaded.
+ Gerrit._loadPlugins([]);
+ flush(done);
+ });
- teardown(() => {
- sandbox.restore();
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('imports plugin-provided modules into endpoints', () => {
+ const endpoints =
+ Array.from(container.querySelectorAll('gr-endpoint-decorator'));
+ assert.equal(endpoints.length, 3);
+ endpoints.forEach(element => {
+ assert.isTrue(
+ element._import.calledWith(new URL('http://some/plugin/url.html')));
});
+ });
- test('imports plugin-provided modules into endpoints', () => {
- const endpoints =
- Array.from(container.querySelectorAll('gr-endpoint-decorator'));
- assert.equal(endpoints.length, 3);
- endpoints.forEach(element => {
- assert.isTrue(
- element._import.calledWith(new URL('http://some/plugin/url.html')));
- });
- });
+ test('decoration', () => {
+ const element =
+ container.querySelector('gr-endpoint-decorator[name="first"]');
+ const modules = Array.from(dom(element.root).children).filter(
+ element => element.nodeName === 'SOME-MODULE');
+ assert.equal(modules.length, 1);
+ const [module] = modules;
+ assert.isOk(module);
+ assert.equal(module['someparam'], 'barbar');
+ return decorationHook.getLastAttached().then(element => {
+ assert.strictEqual(element, module);
+ })
+ .then(() => {
+ element.remove();
+ assert.equal(decorationHook.getAllAttached().length, 0);
+ });
+ });
- test('decoration', () => {
+ test('replacement', () => {
+ const element =
+ container.querySelector('gr-endpoint-decorator[name="second"]');
+ const module = Array.from(dom(element.root).children).find(
+ element => element.nodeName === 'OTHER-MODULE');
+ assert.isOk(module);
+ assert.equal(module['someparam'], 'foofoo');
+ return replacementHook.getLastAttached()
+ .then(element => {
+ assert.strictEqual(element, module);
+ })
+ .then(() => {
+ element.remove();
+ assert.equal(replacementHook.getAllAttached().length, 0);
+ });
+ });
+
+ test('late registration', done => {
+ plugin.registerCustomComponent('banana', 'noob-noob');
+ flush(() => {
const element =
- container.querySelector('gr-endpoint-decorator[name="first"]');
- const modules = Array.from(Polymer.dom(element.root).children).filter(
- element => element.nodeName === 'SOME-MODULE');
- assert.equal(modules.length, 1);
- const [module] = modules;
+ container.querySelector('gr-endpoint-decorator[name="banana"]');
+ const module = Array.from(dom(element.root).children).find(
+ element => element.nodeName === 'NOOB-NOOB');
assert.isOk(module);
- assert.equal(module['someparam'], 'barbar');
- return decorationHook.getLastAttached().then(element => {
- assert.strictEqual(element, module);
- })
- .then(() => {
- element.remove();
- assert.equal(decorationHook.getAllAttached().length, 0);
- });
+ done();
});
+ });
- test('replacement', () => {
+ test('two modules', done => {
+ plugin.registerCustomComponent('banana', 'mod-one');
+ plugin.registerCustomComponent('banana', 'mod-two');
+ flush(() => {
const element =
- container.querySelector('gr-endpoint-decorator[name="second"]');
- const module = Array.from(Polymer.dom(element.root).children).find(
- element => element.nodeName === 'OTHER-MODULE');
- assert.isOk(module);
- assert.equal(module['someparam'], 'foofoo');
- return replacementHook.getLastAttached()
- .then(element => {
- assert.strictEqual(element, module);
- })
- .then(() => {
- element.remove();
- assert.equal(replacementHook.getAllAttached().length, 0);
- });
+ container.querySelector('gr-endpoint-decorator[name="banana"]');
+ const module1 = Array.from(dom(element.root).children).find(
+ element => element.nodeName === 'MOD-ONE');
+ assert.isOk(module1);
+ const module2 = Array.from(dom(element.root).children).find(
+ element => element.nodeName === 'MOD-TWO');
+ assert.isOk(module2);
+ done();
});
+ });
- test('late registration', done => {
- plugin.registerCustomComponent('banana', 'noob-noob');
+ test('late param setup', done => {
+ const element =
+ container.querySelector('gr-endpoint-decorator[name="banana"]');
+ const param = dom(element).querySelector('gr-endpoint-param');
+ param['value'] = undefined;
+ plugin.registerCustomComponent('banana', 'noob-noob');
+ flush(() => {
+ let module = Array.from(dom(element.root).children).find(
+ element => element.nodeName === 'NOOB-NOOB');
+ // Module waits for param to be defined.
+ assert.isNotOk(module);
+ const value = {abc: 'def'};
+ param.value = value;
flush(() => {
- const element =
- container.querySelector('gr-endpoint-decorator[name="banana"]');
- const module = Array.from(Polymer.dom(element.root).children).find(
+ module = Array.from(dom(element.root).children).find(
element => element.nodeName === 'NOOB-NOOB');
assert.isOk(module);
- done();
- });
- });
-
- test('two modules', done => {
- plugin.registerCustomComponent('banana', 'mod-one');
- plugin.registerCustomComponent('banana', 'mod-two');
- flush(() => {
- const element =
- container.querySelector('gr-endpoint-decorator[name="banana"]');
- const module1 = Array.from(Polymer.dom(element.root).children).find(
- element => element.nodeName === 'MOD-ONE');
- assert.isOk(module1);
- const module2 = Array.from(Polymer.dom(element.root).children).find(
- element => element.nodeName === 'MOD-TWO');
- assert.isOk(module2);
- done();
- });
- });
-
- test('late param setup', done => {
- const element =
- container.querySelector('gr-endpoint-decorator[name="banana"]');
- const param = Polymer.dom(element).querySelector('gr-endpoint-param');
- param['value'] = undefined;
- plugin.registerCustomComponent('banana', 'noob-noob');
- flush(() => {
- let module = Array.from(Polymer.dom(element.root).children).find(
- element => element.nodeName === 'NOOB-NOOB');
- // Module waits for param to be defined.
- assert.isNotOk(module);
- const value = {abc: 'def'};
- param.value = value;
- flush(() => {
- module = Array.from(Polymer.dom(element.root).children).find(
- element => element.nodeName === 'NOOB-NOOB');
- assert.isOk(module);
- assert.strictEqual(module['someParam'], value);
- done();
- });
- });
- });
-
- test('param is bound', done => {
- const element =
- container.querySelector('gr-endpoint-decorator[name="banana"]');
- const param = Polymer.dom(element).querySelector('gr-endpoint-param');
- const value1 = {abc: 'def'};
- const value2 = {def: 'abc'};
- param.value = value1;
- plugin.registerCustomComponent('banana', 'noob-noob');
- flush(() => {
- const module = Array.from(Polymer.dom(element.root).children).find(
- element => element.nodeName === 'NOOB-NOOB');
- assert.strictEqual(module['someParam'], value1);
- param.value = value2;
- assert.strictEqual(module['someParam'], value2);
+ assert.strictEqual(module['someParam'], value);
done();
});
});
});
+
+ test('param is bound', done => {
+ const element =
+ container.querySelector('gr-endpoint-decorator[name="banana"]');
+ const param = dom(element).querySelector('gr-endpoint-param');
+ const value1 = {abc: 'def'};
+ const value2 = {def: 'abc'};
+ param.value = value1;
+ plugin.registerCustomComponent('banana', 'noob-noob');
+ flush(() => {
+ const module = Array.from(dom(element.root).children).find(
+ element => element.nodeName === 'NOOB-NOOB');
+ assert.strictEqual(module['someParam'], value1);
+ param.value = value2;
+ assert.strictEqual(module['someParam'], value2);
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.html
deleted file mode 100644
index 6a5b558..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<dom-module id="gr-endpoint-param">
- <script src="gr-endpoint-param.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
index bcad7f9..9574391 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
@@ -14,41 +14,43 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrEndpointParam extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-endpoint-param'; }
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
- static get properties() {
- return {
- name: String,
- value: {
- type: Object,
- notify: true,
- observer: '_valueChanged',
- },
- };
- }
+/** @extends Polymer.Element */
+class GrEndpointParam extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get is() { return 'gr-endpoint-param'; }
- _valueChanged(newValue, oldValue) {
- /* In polymer 2 the following change was made:
- "Property change notifications (property-changed events) aren't fired when
- the value changes as a result of a binding from the host"
- (see https://polymer-library.polymer-project.org/2.0/docs/about_20).
- To workaround this problem, we fire the event from the observer.
- In some cases this fire the event twice, but our code is
- ready for it.
- */
- const detail = {
- value: newValue,
- };
- this.dispatchEvent(new CustomEvent('value-changed', {detail}));
- }
+ static get properties() {
+ return {
+ name: String,
+ value: {
+ type: Object,
+ notify: true,
+ observer: '_valueChanged',
+ },
+ };
}
- customElements.define(GrEndpointParam.is, GrEndpointParam);
-})();
+ _valueChanged(newValue, oldValue) {
+ /* In polymer 2 the following change was made:
+ "Property change notifications (property-changed events) aren't fired when
+ the value changes as a result of a binding from the host"
+ (see https://polymer-library.polymer-project.org/2.0/docs/about_20).
+ To workaround this problem, we fire the event from the observer.
+ In some cases this fire the event twice, but our code is
+ ready for it.
+ */
+ const detail = {
+ value: newValue,
+ };
+ this.dispatchEvent(new CustomEvent('value-changed', {detail}));
+ }
+}
+
+customElements.define(GrEndpointParam.is, GrEndpointParam);
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html
deleted file mode 100644
index 15db861..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-
-<dom-module id="gr-event-helper">
- <script src="gr-event-helper.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
index 481a467..66d42d3 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
@@ -14,6 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-event-helper">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
index a6eaebf..bd61a96 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
@@ -19,35 +19,31 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-event-helper</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-event-helper.html"/>
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<dom-element id="some-element">
- <script>
- readyToTest().then(() => {
- Polymer({
- is: 'some-element',
+ <script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-event-helper.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+Polymer({
+ is: 'some-element',
- properties: {
- fooBar: {
- type: Object,
- notify: true,
- },
- },
+ properties: {
+ fooBar: {
+ type: Object,
+ notify: true,
+ },
+ },
- behaviors: [
- Gerrit.FireBehavior,
- ],
- });
- });
- </script>
+ behaviors: [
+ Gerrit.FireBehavior,
+ ],
+});
+</script>
</dom-element>
@@ -57,85 +53,87 @@
</template>
</test-fixture>
-<script>
- suite('gr-event-helper tests', async () => {
- await readyToTest();
- let element;
- let instance;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-event-helper.js';
+import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
+suite('gr-event-helper tests', () => {
+ let element;
+ let instance;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- instance = new GrEventHelper(element);
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('onTap()', done => {
- instance.onTap(() => {
- done();
- });
- MockInteractions.tap(element);
- });
-
- test('onTap() cancel', () => {
- const tapStub = sandbox.stub();
- Polymer.Gestures.addListener(element.parentElement, 'tap', tapStub);
- instance.onTap(() => false);
- MockInteractions.tap(element);
- flushAsynchronousOperations();
- assert.isFalse(tapStub.called);
- });
-
- test('onClick() cancel', () => {
- const tapStub = sandbox.stub();
- element.parentElement.addEventListener('click', tapStub);
- instance.onTap(() => false);
- MockInteractions.tap(element);
- flushAsynchronousOperations();
- assert.isFalse(tapStub.called);
- });
-
- test('captureTap()', done => {
- instance.captureTap(() => {
- done();
- });
- MockInteractions.tap(element);
- });
-
- test('captureClick()', done => {
- instance.captureClick(() => {
- done();
- });
- MockInteractions.tap(element);
- });
-
- test('captureTap() cancels tap()', () => {
- const tapStub = sandbox.stub();
- Polymer.Gestures.addListener(element.parentElement, 'tap', tapStub);
- instance.captureTap(() => false);
- MockInteractions.tap(element);
- flushAsynchronousOperations();
- assert.isFalse(tapStub.called);
- });
-
- test('captureClick() cancels click()', () => {
- const tapStub = sandbox.stub();
- element.addEventListener('click', tapStub);
- instance.captureTap(() => false);
- MockInteractions.tap(element);
- flushAsynchronousOperations();
- assert.isFalse(tapStub.called);
- });
-
- test('on()', done => {
- instance.on('foo', () => {
- done();
- });
- element.fire('foo');
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ instance = new GrEventHelper(element);
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('onTap()', done => {
+ instance.onTap(() => {
+ done();
+ });
+ MockInteractions.tap(element);
+ });
+
+ test('onTap() cancel', () => {
+ const tapStub = sandbox.stub();
+ addListener(element.parentElement, 'tap', tapStub);
+ instance.onTap(() => false);
+ MockInteractions.tap(element);
+ flushAsynchronousOperations();
+ assert.isFalse(tapStub.called);
+ });
+
+ test('onClick() cancel', () => {
+ const tapStub = sandbox.stub();
+ element.parentElement.addEventListener('click', tapStub);
+ instance.onTap(() => false);
+ MockInteractions.tap(element);
+ flushAsynchronousOperations();
+ assert.isFalse(tapStub.called);
+ });
+
+ test('captureTap()', done => {
+ instance.captureTap(() => {
+ done();
+ });
+ MockInteractions.tap(element);
+ });
+
+ test('captureClick()', done => {
+ instance.captureClick(() => {
+ done();
+ });
+ MockInteractions.tap(element);
+ });
+
+ test('captureTap() cancels tap()', () => {
+ const tapStub = sandbox.stub();
+ addListener(element.parentElement, 'tap', tapStub);
+ instance.captureTap(() => false);
+ MockInteractions.tap(element);
+ flushAsynchronousOperations();
+ assert.isFalse(tapStub.called);
+ });
+
+ test('captureClick() cancels click()', () => {
+ const tapStub = sandbox.stub();
+ element.addEventListener('click', tapStub);
+ instance.captureTap(() => false);
+ MockInteractions.tap(element);
+ flushAsynchronousOperations();
+ assert.isFalse(tapStub.called);
+ });
+
+ test('on()', done => {
+ instance.on('foo', () => {
+ done();
+ });
+ element.fire('foo');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.html b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.html
deleted file mode 100644
index 6a55349..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.html
+++ /dev/null
@@ -1,26 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-
-<dom-module id="gr-external-style">
- <template>
- <slot></slot>
- </template>
- <script src="gr-external-style.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
index d12a571..ba4dd58 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
@@ -14,78 +14,92 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrExternalStyle extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-external-style'; }
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import {importHref} from '../../../scripts/import-href.js';
+import {updateStyles} from '@polymer/polymer/lib/mixins/element-mixin.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-external-style_html.js';
- static get properties() {
- return {
- name: String,
- _urlsImported: {
- type: Array,
- value() { return []; },
- },
- _stylesApplied: {
- type: Array,
- value() { return []; },
- },
- };
- }
+/** @extends Polymer.Element */
+class GrExternalStyle extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /**
- * @suppress {checkTypes}
- */
- _import(url) {
- if (this._urlsImported.includes(url)) { return Promise.resolve(); }
- this._urlsImported.push(url);
- return new Promise((resolve, reject) => {
- Polymer.importHref(url, resolve, reject);
- });
- }
+ static get is() { return 'gr-external-style'; }
- _applyStyle(name) {
- if (this._stylesApplied.includes(name)) { return; }
- this._stylesApplied.push(name);
-
- const s = document.createElement('style');
- s.setAttribute('include', name);
- const cs = document.createElement('custom-style');
- cs.appendChild(s);
- // When using Shadow DOM <custom-style> must be added to the <body>.
- // Within <gr-external-style> itself the styles would have no effect.
- const topEl = document.getElementsByTagName('body')[0];
- topEl.insertBefore(cs, topEl.firstChild);
- Polymer.updateStyles();
- }
-
- _importAndApply() {
- Promise.all(Gerrit._endpoints.getPlugins(this.name).map(
- pluginUrl => this._import(pluginUrl))
- ).then(() => {
- const moduleNames = Gerrit._endpoints.getModules(this.name);
- for (const name of moduleNames) {
- this._applyStyle(name);
- }
- });
- }
-
- /** @override */
- attached() {
- super.attached();
- this._importAndApply();
- }
-
- /** @override */
- ready() {
- super.ready();
- Gerrit.awaitPluginsLoaded().then(() => this._importAndApply());
- }
+ static get properties() {
+ return {
+ name: String,
+ _urlsImported: {
+ type: Array,
+ value() { return []; },
+ },
+ _stylesApplied: {
+ type: Array,
+ value() { return []; },
+ },
+ };
}
- customElements.define(GrExternalStyle.is, GrExternalStyle);
-})();
+ _importHref(url, resolve, reject) {
+ // It is impossible to mock es6-module imported function.
+ // The _importHref function is mocked in test.
+ importHref(url, resolve, reject);
+ }
+
+ /**
+ * @suppress {checkTypes}
+ */
+ _import(url) {
+ if (this._urlsImported.includes(url)) { return Promise.resolve(); }
+ this._urlsImported.push(url);
+ return new Promise((resolve, reject) => {
+ this._importHref(url, resolve, reject);
+ });
+ }
+
+ _applyStyle(name) {
+ if (this._stylesApplied.includes(name)) { return; }
+ this._stylesApplied.push(name);
+
+ const s = document.createElement('style');
+ s.setAttribute('include', name);
+ const cs = document.createElement('custom-style');
+ cs.appendChild(s);
+ // When using Shadow DOM <custom-style> must be added to the <body>.
+ // Within <gr-external-style> itself the styles would have no effect.
+ const topEl = document.getElementsByTagName('body')[0];
+ topEl.insertBefore(cs, topEl.firstChild);
+ updateStyles();
+ }
+
+ _importAndApply() {
+ Promise.all(Gerrit._endpoints.getPlugins(this.name).map(
+ pluginUrl => this._import(pluginUrl))
+ ).then(() => {
+ const moduleNames = Gerrit._endpoints.getModules(this.name);
+ for (const name of moduleNames) {
+ this._applyStyle(name);
+ }
+ });
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._importAndApply();
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ Gerrit.awaitPluginsLoaded().then(() => this._importAndApply());
+ }
+}
+
+customElements.define(GrExternalStyle.is, GrExternalStyle);
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_html.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_html.js
new file mode 100644
index 0000000..1644c07
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_html.js
@@ -0,0 +1,21 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <slot></slot>
+`;
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
index c3592f5..a6c8111 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
@@ -19,106 +19,109 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-external-style</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-external-style.html">
-
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
<gr-external-style name="foo"></gr-external-style>
</template>
</test-fixture>
-<script>
- suite('gr-external-style integration tests', async () => {
- await readyToTest();
- const TEST_URL = 'http://some/plugin/url.html';
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-external-style.js';
+suite('gr-external-style integration tests', () => {
+ const TEST_URL = 'http://some/plugin/url.html';
- let sandbox;
- let element;
- let plugin;
+ let sandbox;
+ let element;
+ let plugin;
+ let importHrefStub;
- const installPlugin = () => {
- if (plugin) { return; }
- Gerrit.install(p => {
- plugin = p;
- }, '0.1', TEST_URL);
- };
+ const installPlugin = () => {
+ if (plugin) { return; }
+ Gerrit.install(p => {
+ plugin = p;
+ }, '0.1', TEST_URL);
+ };
- const createElement = () => {
- element = fixture('basic');
- sandbox.spy(element, '_applyStyle');
- };
+ const createElement = () => {
+ element = fixture('basic');
+ sandbox.spy(element, '_applyStyle');
+ };
- /**
- * Installs the plugin, creates the element, registers style module.
- */
- const lateRegister = () => {
- installPlugin();
- createElement();
- plugin.registerStyleModule('foo', 'some-module');
- };
+ /**
+ * Installs the plugin, creates the element, registers style module.
+ */
+ const lateRegister = () => {
+ installPlugin();
+ createElement();
+ plugin.registerStyleModule('foo', 'some-module');
+ };
- /**
- * Installs the plugin, registers style module, creates the element.
- */
- const earlyRegister = () => {
- installPlugin();
- plugin.registerStyleModule('foo', 'some-module');
- createElement();
- };
+ /**
+ * Installs the plugin, registers style module, creates the element.
+ */
+ const earlyRegister = () => {
+ installPlugin();
+ plugin.registerStyleModule('foo', 'some-module');
+ createElement();
+ };
- setup(() => {
- sandbox = sinon.sandbox.create();
- sandbox.stub(Polymer, 'importHref', (url, resolve) => resolve());
- sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ importHrefStub = sandbox.stub().callsArg(1);
+ stub('gr-external-style', {
+ _importHref: (url, resolve, reject) => {
+ importHrefStub(url, resolve, reject);
+ },
});
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('imports plugin-provided module', async () => {
- lateRegister();
- await new Promise(flush);
- assert.isTrue(Polymer.importHref.calledWith(new URL(TEST_URL)));
- });
-
- test('applies plugin-provided styles', async () => {
- lateRegister();
- await new Promise(flush);
- assert.isTrue(element._applyStyle.calledWith('some-module'));
- });
-
- test('does not double import', async () => {
- earlyRegister();
- await new Promise(flush);
- plugin.registerStyleModule('foo', 'some-module');
- await new Promise(flush);
- const urlsImported =
- element._urlsImported.filter(url => url.toString() === TEST_URL);
- assert.strictEqual(urlsImported.length, 1);
- });
-
- test('does not double apply', async () => {
- earlyRegister();
- await new Promise(flush);
- plugin.registerStyleModule('foo', 'some-module');
- await new Promise(flush);
- const stylesApplied =
- element._stylesApplied.filter(name => name === 'some-module');
- assert.strictEqual(stylesApplied.length, 1);
- });
-
- test('loads and applies preloaded modules', async () => {
- earlyRegister();
- await new Promise(flush);
- assert.isTrue(Polymer.importHref.calledWith(new URL(TEST_URL)));
- assert.isTrue(element._applyStyle.calledWith('some-module'));
- });
+ sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('imports plugin-provided module', async () => {
+ lateRegister();
+ await new Promise(flush);
+ assert.isTrue(importHrefStub.calledWith(new URL(TEST_URL)));
+ });
+
+ test('applies plugin-provided styles', async () => {
+ lateRegister();
+ await new Promise(flush);
+ assert.isTrue(element._applyStyle.calledWith('some-module'));
+ });
+
+ test('does not double import', async () => {
+ earlyRegister();
+ await new Promise(flush);
+ plugin.registerStyleModule('foo', 'some-module');
+ await new Promise(flush);
+ const urlsImported =
+ element._urlsImported.filter(url => url.toString() === TEST_URL);
+ assert.strictEqual(urlsImported.length, 1);
+ });
+
+ test('does not double apply', async () => {
+ earlyRegister();
+ await new Promise(flush);
+ plugin.registerStyleModule('foo', 'some-module');
+ await new Promise(flush);
+ const stylesApplied =
+ element._stylesApplied.filter(name => name === 'some-module');
+ assert.strictEqual(stylesApplied.length, 1);
+ });
+
+ test('loads and applies preloaded modules', async () => {
+ earlyRegister();
+ await new Promise(flush);
+ assert.isTrue(importHrefStub.calledWith(new URL(TEST_URL)));
+ assert.isTrue(element._applyStyle.calledWith('some-module'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
deleted file mode 100644
index f277899..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-
-<dom-module id="gr-plugin-host">
- <script src="gr-plugin-host.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
index da050fb..1236f97 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -14,59 +14,63 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrPluginHost extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-plugin-host'; }
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
- static get properties() {
- return {
- config: {
- type: Object,
- observer: '_configChanged',
- },
- };
- }
+/** @extends Polymer.Element */
+class GrPluginHost extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get is() { return 'gr-plugin-host'; }
- _configChanged(config) {
- const plugins = config.plugin;
- const htmlPlugins = (plugins.html_resource_paths || []);
- const jsPlugins =
- this._handleMigrations(plugins.js_resource_paths || [], htmlPlugins);
- const shouldLoadTheme = config.default_theme &&
- !Gerrit._isPluginPreloaded('preloaded:gerrit-theme');
- const themeToLoad =
- shouldLoadTheme ? [config.default_theme] : [];
-
- // Theme should be loaded first if has one to have better UX
- const pluginsPending =
- themeToLoad.concat(jsPlugins, htmlPlugins);
-
- const pluginOpts = {};
-
- if (shouldLoadTheme) {
- // Theme needs to be loaded synchronous.
- pluginOpts[config.default_theme] = {sync: true};
- }
-
- Gerrit._loadPlugins(pluginsPending, pluginOpts);
- }
-
- /**
- * Omit .js plugins that have .html counterparts.
- * For example, if plugin provides foo.js and foo.html, skip foo.js.
- */
- _handleMigrations(jsPlugins, htmlPlugins) {
- return jsPlugins.filter(url => {
- const counterpart = url.replace(/\.js$/, '.html');
- return !htmlPlugins.includes(counterpart);
- });
- }
+ static get properties() {
+ return {
+ config: {
+ type: Object,
+ observer: '_configChanged',
+ },
+ };
}
- customElements.define(GrPluginHost.is, GrPluginHost);
-})();
+ _configChanged(config) {
+ const plugins = config.plugin;
+ const htmlPlugins = (plugins.html_resource_paths || []);
+ const jsPlugins =
+ this._handleMigrations(plugins.js_resource_paths || [], htmlPlugins);
+ const shouldLoadTheme = config.default_theme &&
+ !Gerrit._isPluginPreloaded('preloaded:gerrit-theme');
+ const themeToLoad =
+ shouldLoadTheme ? [config.default_theme] : [];
+
+ // Theme should be loaded first if has one to have better UX
+ const pluginsPending =
+ themeToLoad.concat(jsPlugins, htmlPlugins);
+
+ const pluginOpts = {};
+
+ if (shouldLoadTheme) {
+ // Theme needs to be loaded synchronous.
+ pluginOpts[config.default_theme] = {sync: true};
+ }
+
+ Gerrit._loadPlugins(pluginsPending, pluginOpts);
+ }
+
+ /**
+ * Omit .js plugins that have .html counterparts.
+ * For example, if plugin provides foo.js and foo.html, skip foo.js.
+ */
+ _handleMigrations(jsPlugins, htmlPlugins) {
+ return jsPlugins.filter(url => {
+ const counterpart = url.replace(/\.js$/, '.html');
+ return !htmlPlugins.includes(counterpart);
+ });
+ }
+}
+
+customElements.define(GrPluginHost.is, GrPluginHost);
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
index 9a5eb15..008b274 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-plugin-host</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-plugin-host.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,63 +30,63 @@
</template>
</test-fixture>
-<script>
- suite('gr-plugin-host tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-plugin-host.js';
+suite('gr-plugin-host tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- sandbox.stub(document.body, 'appendChild');
- sandbox.stub(Polymer, 'importHref');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('load plugins should be called', () => {
- sandbox.stub(Gerrit, '_loadPlugins');
- element.config = {
- plugin: {
- html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
- js_resource_paths: ['plugins/42'],
- },
- };
- assert.isTrue(Gerrit._loadPlugins.calledOnce);
- assert.isTrue(Gerrit._loadPlugins.calledWith([
- 'plugins/42', 'plugins/foo/bar', 'plugins/baz',
- ], {}));
- });
-
- test('theme plugins should be loaded if enabled', () => {
- sandbox.stub(Gerrit, '_loadPlugins');
- element.config = {
- default_theme: 'gerrit-theme.html',
- plugin: {
- html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
- js_resource_paths: ['plugins/42'],
- },
- };
- assert.isTrue(Gerrit._loadPlugins.calledOnce);
- assert.isTrue(Gerrit._loadPlugins.calledWith([
- 'gerrit-theme.html', 'plugins/42', 'plugins/foo/bar', 'plugins/baz',
- ], {'gerrit-theme.html': {sync: true}}));
- });
-
- test('skip theme if preloaded', () => {
- sandbox.stub(Gerrit, '_isPluginPreloaded')
- .withArgs('preloaded:gerrit-theme')
- .returns(true);
- sandbox.stub(Gerrit, '_loadPlugins');
- element.config = {
- default_theme: '/oof',
- plugin: {},
- };
- assert.isTrue(Gerrit._loadPlugins.calledOnce);
- assert.isTrue(Gerrit._loadPlugins.calledWith([], {}));
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(document.body, 'appendChild');
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('load plugins should be called', () => {
+ sandbox.stub(Gerrit, '_loadPlugins');
+ element.config = {
+ plugin: {
+ html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
+ js_resource_paths: ['plugins/42'],
+ },
+ };
+ assert.isTrue(Gerrit._loadPlugins.calledOnce);
+ assert.isTrue(Gerrit._loadPlugins.calledWith([
+ 'plugins/42', 'plugins/foo/bar', 'plugins/baz',
+ ], {}));
+ });
+
+ test('theme plugins should be loaded if enabled', () => {
+ sandbox.stub(Gerrit, '_loadPlugins');
+ element.config = {
+ default_theme: 'gerrit-theme.html',
+ plugin: {
+ html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
+ js_resource_paths: ['plugins/42'],
+ },
+ };
+ assert.isTrue(Gerrit._loadPlugins.calledOnce);
+ assert.isTrue(Gerrit._loadPlugins.calledWith([
+ 'gerrit-theme.html', 'plugins/42', 'plugins/foo/bar', 'plugins/baz',
+ ], {'gerrit-theme.html': {sync: true}}));
+ });
+
+ test('skip theme if preloaded', () => {
+ sandbox.stub(Gerrit, '_isPluginPreloaded')
+ .withArgs('preloaded:gerrit-theme')
+ .returns(true);
+ sandbox.stub(Gerrit, '_loadPlugins');
+ element.config = {
+ default_theme: '/oof',
+ plugin: {},
+ };
+ assert.isTrue(Gerrit._loadPlugins.calledOnce);
+ assert.isTrue(Gerrit._loadPlugins.calledWith([], {}));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html
deleted file mode 100644
index d084445..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-
-<dom-module id="gr-plugin-popup">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <gr-overlay id="overlay" with-backdrop>
- <slot></slot>
- </gr-overlay>
- </template>
- <script src="gr-plugin-popup.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
index 30bf6c8..db44cea 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
@@ -14,13 +14,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+import '../../shared/gr-overlay/gr-overlay.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-plugin-popup_html.js';
+
(function(window) {
'use strict';
/** @extends Polymer.Element */
- class GrPluginPopup extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
+ class GrPluginPopup extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
static get is() { return 'gr-plugin-popup'; }
get opened() {
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_html.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_html.js
new file mode 100644
index 0000000..779cbad
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_html.js
@@ -0,0 +1,26 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <gr-overlay id="overlay" with-backdrop="">
+ <slot></slot>
+ </gr-overlay>
+`;
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
index 1617cd5..2e65365 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-plugin-popup</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-plugin-popup.html"/>
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,39 +30,40 @@
</template>
</test-fixture>
-<script>
- suite('gr-plugin-popup tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-plugin-popup.js';
+suite('gr-plugin-popup tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- stub('gr-overlay', {
- open: sandbox.stub().returns(Promise.resolve()),
- close: sandbox.stub(),
- });
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('exists', () => {
- assert.isOk(element);
- });
-
- test('open uses open() from gr-overlay', done => {
- element.open().then(() => {
- assert.isTrue(element.$.overlay.open.called);
- done();
- });
- });
-
- test('close uses close() from gr-overlay', () => {
- element.close();
- assert.isTrue(element.$.overlay.close.called);
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ stub('gr-overlay', {
+ open: sandbox.stub().returns(Promise.resolve()),
+ close: sandbox.stub(),
});
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('exists', () => {
+ assert.isOk(element);
+ });
+
+ test('open uses open() from gr-overlay', done => {
+ element.open().then(() => {
+ assert.isTrue(element.$.overlay.open.called);
+ done();
+ });
+ });
+
+ test('close uses close() from gr-overlay', () => {
+ element.close();
+ assert.isTrue(element.$.overlay.close.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html
deleted file mode 100644
index a8bb06b..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="gr-plugin-popup.html">
-
-<dom-module id="gr-popup-interface">
- <script src="gr-popup-interface.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
index c3588a1..e9d3e36 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
@@ -14,6 +14,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+import './gr-plugin-popup.js';
+import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-popup-interface">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
@@ -35,7 +47,7 @@
}
GrPopupInterface.prototype._getElement = function() {
- return Polymer.dom(this._popup);
+ return dom(this._popup);
};
/**
@@ -52,12 +64,12 @@
.then(hookEl => {
const popup = document.createElement('gr-plugin-popup');
if (this._moduleName) {
- const el = Polymer.dom(popup).appendChild(
+ const el = dom(popup).appendChild(
document.createElement(this._moduleName));
el.plugin = this.plugin;
}
- this._popup = Polymer.dom(hookEl).appendChild(popup);
- Polymer.dom.flush();
+ this._popup = dom(hookEl).appendChild(popup);
+ flush();
return this._popup.open().then(() => this);
});
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
index ffa3f2d..661058e 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
@@ -19,16 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-popup-interface</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-popup-interface.html"/>
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="container">
<template>
@@ -40,87 +34,92 @@
<template>
<div id="barfoo">some test module</div>
</template>
- <script>
- readyToTest().then(() => {
- Polymer({is: 'gr-user-test-popup'});
- });
- </script>
+ <script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-popup-interface.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+Polymer({is: 'gr-user-test-popup'});
+</script>
</dom-module>
-<script>
- suite('gr-popup-interface tests', async () => {
- await readyToTest();
- let container;
- let instance;
- let plugin;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-popup-interface.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-popup-interface tests', () => {
+ let container;
+ let instance;
+ let plugin;
+ let sandbox;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ container = fixture('container');
+ sandbox.stub(plugin, 'hook').returns({
+ getLastAttached() {
+ return Promise.resolve(container);
+ },
+ });
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('manual', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- container = fixture('container');
- sandbox.stub(plugin, 'hook').returns({
- getLastAttached() {
- return Promise.resolve(container);
- },
+ instance = new GrPopupInterface(plugin);
+ });
+
+ test('open', done => {
+ instance.open().then(api => {
+ assert.strictEqual(api, instance);
+ const manual = document.createElement('div');
+ manual.id = 'foobar';
+ manual.innerHTML = 'manual content';
+ api._getElement().appendChild(manual);
+ flushAsynchronousOperations();
+ assert.equal(
+ container.querySelector('#foobar').textContent, 'manual content');
+ done();
});
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('manual', () => {
- setup(() => {
- instance = new GrPopupInterface(plugin);
- });
-
- test('open', done => {
- instance.open().then(api => {
- assert.strictEqual(api, instance);
- const manual = document.createElement('div');
- manual.id = 'foobar';
- manual.innerHTML = 'manual content';
- api._getElement().appendChild(manual);
- flushAsynchronousOperations();
- assert.equal(
- container.querySelector('#foobar').textContent, 'manual content');
- done();
- });
- });
-
- test('close', done => {
- instance.open().then(api => {
- assert.isTrue(api._getElement().node.opened);
- api.close();
- assert.isFalse(api._getElement().node.opened);
- done();
- });
- });
- });
-
- suite('components', () => {
- setup(() => {
- instance = new GrPopupInterface(plugin, 'gr-user-test-popup');
- });
-
- test('open', done => {
- instance.open().then(api => {
- assert.isNotNull(
- Polymer.dom(container).querySelector('gr-user-test-popup'));
- done();
- });
- });
-
- test('close', done => {
- instance.open().then(api => {
- assert.isTrue(api._getElement().node.opened);
- api.close();
- assert.isFalse(api._getElement().node.opened);
- done();
- });
+ test('close', done => {
+ instance.open().then(api => {
+ assert.isTrue(api._getElement().node.opened);
+ api.close();
+ assert.isFalse(api._getElement().node.opened);
+ done();
});
});
});
+
+ suite('components', () => {
+ setup(() => {
+ instance = new GrPopupInterface(plugin, 'gr-user-test-popup');
+ });
+
+ test('open', done => {
+ instance.open().then(api => {
+ assert.isNotNull(
+ dom(container).querySelector('gr-user-test-popup'));
+ done();
+ });
+ });
+
+ test('close', done => {
+ instance.open().then(api => {
+ assert.isTrue(api._getElement().node.opened);
+ api.close();
+ assert.isFalse(api._getElement().node.opened);
+ done();
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.html
deleted file mode 100644
index 593c1e0..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../admin/gr-repo-command/gr-repo-command.html">
-
-<dom-module id="gr-plugin-repo-command">
- <template>
- <gr-repo-command title="[[title]]">
- </gr-repo-command>
- </template>
- <script>
- Polymer({
- is: 'gr-plugin-repo-command',
- properties: {
- title: String,
- repoName: String,
- config: Object,
- },
- });
- </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
new file mode 100644
index 0000000..f9a2bdf
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
@@ -0,0 +1,35 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../../scripts/bundled-polymer.js';
+
+import '../../admin/gr-repo-command/gr-repo-command.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+Polymer({
+ _template: html`
+ <gr-repo-command title="[[title]]">
+ </gr-repo-command>
+`,
+
+ is: 'gr-plugin-repo-command',
+
+ properties: {
+ title: String,
+ repoName: String,
+ config: Object,
+ },
+});
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html
deleted file mode 100644
index b3f6aec..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="gr-plugin-repo-command.html">
-
-<dom-module id="gr-repo-api">
- <script src="gr-repo-api.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
index b59cce6..6c1a3c8 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
@@ -14,6 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+import './gr-plugin-repo-command.js';
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-repo-api">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
index adc1de5..0eed6d9 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
@@ -19,16 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-api</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="../gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="gr-repo-api.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -37,52 +31,54 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo-api tests', async () => {
- await readyToTest();
- let sandbox;
- let repoApi;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../gr-endpoint-decorator/gr-endpoint-decorator.js';
+import './gr-repo-api.js';
+suite('gr-repo-api tests', () => {
+ let sandbox;
+ let repoApi;
- setup(() => {
- sandbox = sinon.sandbox.create();
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- Gerrit._loadPlugins([]);
- repoApi = plugin.project();
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ Gerrit._loadPlugins([]);
+ repoApi = plugin.project();
+ });
- teardown(() => {
- repoApi = null;
- sandbox.restore();
- });
+ teardown(() => {
+ repoApi = null;
+ sandbox.restore();
+ });
- test('exists', () => {
- assert.isOk(repoApi);
- });
+ test('exists', () => {
+ assert.isOk(repoApi);
+ });
- test('works', done => {
- const attachedStub = sandbox.stub();
- const tapStub = sandbox.stub();
- repoApi
- .createCommand('foo', attachedStub)
- .onTap(tapStub);
- const element = fixture('basic');
- flush(() => {
- assert.isTrue(attachedStub.called);
- const pluginCommand = element.shadowRoot
- .querySelector('gr-plugin-repo-command');
- assert.isOk(pluginCommand);
- const command = pluginCommand.shadowRoot
- .querySelector('gr-repo-command');
- assert.isOk(command);
- assert.equal(command.title, 'foo');
- assert.isFalse(tapStub.called);
- MockInteractions.tap(command.shadowRoot
- .querySelector('gr-button'));
- assert.isTrue(tapStub.called);
- done();
- });
+ test('works', done => {
+ const attachedStub = sandbox.stub();
+ const tapStub = sandbox.stub();
+ repoApi
+ .createCommand('foo', attachedStub)
+ .onTap(tapStub);
+ const element = fixture('basic');
+ flush(() => {
+ assert.isTrue(attachedStub.called);
+ const pluginCommand = element.shadowRoot
+ .querySelector('gr-plugin-repo-command');
+ assert.isOk(pluginCommand);
+ const command = pluginCommand.shadowRoot
+ .querySelector('gr-repo-command');
+ assert.isOk(command);
+ assert.equal(command.title, 'foo');
+ assert.isFalse(tapStub.called);
+ MockInteractions.tap(command.shadowRoot
+ .querySelector('gr-button'));
+ assert.isTrue(tapStub.called);
+ done();
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html
deleted file mode 100644
index 999ecfa..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html
+++ /dev/null
@@ -1,25 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Settings
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../settings/gr-settings-view/gr-settings-item.html">
-<link rel="import" href="../../settings/gr-settings-view/gr-settings-menu-item.html">
-
-<dom-module id="gr-settings-api">
- <script src="gr-settings-api.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
index 5ed4c1a..a8bfccdd 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
@@ -14,6 +14,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../settings/gr-settings-view/gr-settings-item.js';
+import '../../settings/gr-settings-view/gr-settings-menu-item.js';
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-settings-api">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
index 2efd182..e4c643a 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
@@ -19,16 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-settings-api</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="../gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="gr-settings-api.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -39,51 +33,53 @@
</template>
</test-fixture>
-<script>
- suite('gr-settings-api tests', async () => {
- await readyToTest();
- let sandbox;
- let settingsApi;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../gr-endpoint-decorator/gr-endpoint-decorator.js';
+import './gr-settings-api.js';
+suite('gr-settings-api tests', () => {
+ let sandbox;
+ let settingsApi;
- setup(() => {
- sandbox = sinon.sandbox.create();
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- Gerrit._loadPlugins([]);
- settingsApi = plugin.settings();
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ Gerrit._loadPlugins([]);
+ settingsApi = plugin.settings();
+ });
- teardown(() => {
- settingsApi = null;
- sandbox.restore();
- });
+ teardown(() => {
+ settingsApi = null;
+ sandbox.restore();
+ });
- test('exists', () => {
- assert.isOk(settingsApi);
- });
+ test('exists', () => {
+ assert.isOk(settingsApi);
+ });
- test('works', done => {
- settingsApi
- .title('foo')
- .token('bar')
- .module('some-settings-screen')
- .build();
- const element = fixture('basic');
- flush(() => {
- const [menuItemEl, itemEl] = element;
- const menuItem = menuItemEl.shadowRoot
- .querySelector('gr-settings-menu-item');
- assert.isOk(menuItem);
- assert.equal(menuItem.title, 'foo');
- assert.equal(menuItem.href, '#x/testplugin/bar');
- const item = itemEl.shadowRoot
- .querySelector('gr-settings-item');
- assert.isOk(item);
- assert.equal(item.title, 'foo');
- assert.equal(item.anchor, 'x/testplugin/bar');
- done();
- });
+ test('works', done => {
+ settingsApi
+ .title('foo')
+ .token('bar')
+ .module('some-settings-screen')
+ .build();
+ const element = fixture('basic');
+ flush(() => {
+ const [menuItemEl, itemEl] = element;
+ const menuItem = menuItemEl.shadowRoot
+ .querySelector('gr-settings-menu-item');
+ assert.isOk(menuItem);
+ assert.equal(menuItem.title, 'foo');
+ assert.equal(menuItem.href, '#x/testplugin/bar');
+ const item = itemEl.shadowRoot
+ .querySelector('gr-settings-item');
+ assert.isOk(item);
+ assert.equal(item.title, 'foo');
+ assert.equal(item.anchor, 'x/testplugin/bar');
+ done();
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.html b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.html
deleted file mode 100644
index 74b87c8..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.html
+++ /dev/null
@@ -1,18 +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.
--->
-
-<script src="gr-styles-api.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html
index e4a92f6..4901a37 100644
--- a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html
@@ -19,33 +19,61 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-admin-api</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-<link rel="import" href="gr-styles-api.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<dom-module id="gr-style-test-element">
<template>
<div id="wrapper"></div>
</template>
- <script>
- readyToTest().then(() => {
- Polymer({is: 'gr-style-test-element'});
- });
- </script>
+ <script type="module">
+import '../../../test/common-test-setup.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import './gr-styles-api.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+Polymer({is: 'gr-style-test-element'});
+</script>
</dom-module>
-<script>
- suite('gr-styles-api tests', async () => {
- await readyToTest();
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import './gr-styles-api.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-styles-api tests', () => {
+ let sandbox;
+ let stylesApi;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ Gerrit._loadPlugins([]);
+ stylesApi = plugin.styles();
+ });
+
+ teardown(() => {
+ stylesApi = null;
+ sandbox.restore();
+ });
+
+ test('exists', () => {
+ assert.isOk(stylesApi);
+ });
+
+ test('css', () => {
+ const styleObject = stylesApi.css('background: red');
+ assert.isDefined(styleObject);
+ });
+
+ suite('GrStyleObject tests', () => {
let sandbox;
let stylesApi;
+ let displayInlineStyle;
+ let displayNoneStyle;
setup(() => {
sandbox = sinon.sandbox.create();
@@ -54,133 +82,104 @@
'http://test.com/plugins/testplugin/static/test.js');
Gerrit._loadPlugins([]);
stylesApi = plugin.styles();
+ displayInlineStyle = stylesApi.css('display: inline');
+ displayNoneStyle = stylesApi.css('display: none');
});
teardown(() => {
+ displayInlineStyle = null;
+ displayNoneStyle = null;
stylesApi = null;
sandbox.restore();
});
- test('exists', () => {
- assert.isOk(stylesApi);
+ function createNestedElements(parentElement) {
+ /* parentElement
+ * |--- element1
+ * |--- element2
+ * |--- element3
+ **/
+ const element1 = document.createElement('div');
+ const element2 = document.createElement('div');
+ const element3 = document.createElement('div');
+ dom(parentElement).appendChild(element1);
+ dom(parentElement).appendChild(element2);
+ dom(element2).appendChild(element3);
+
+ return [element1, element2, element3];
+ }
+
+ test('getClassName - body level elements', () => {
+ const bodyLevelElements = createNestedElements(document.body);
+
+ testGetClassName(bodyLevelElements);
});
- test('css', () => {
- const styleObject = stylesApi.css('background: red');
- assert.isDefined(styleObject);
+ test('getClassName - elements inside polymer element', () => {
+ const polymerElement = document.createElement('gr-style-test-element');
+ dom(document.body).appendChild(polymerElement);
+ const contentElements = createNestedElements(polymerElement.$.wrapper);
+
+ testGetClassName(contentElements);
});
- suite('GrStyleObject tests', () => {
- let sandbox;
- let stylesApi;
- let displayInlineStyle;
- let displayNoneStyle;
+ function testGetClassName(elements) {
+ assertAllElementsHaveDefaultStyle(elements);
- setup(() => {
- sandbox = sinon.sandbox.create();
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- Gerrit._loadPlugins([]);
- stylesApi = plugin.styles();
- displayInlineStyle = stylesApi.css('display: inline');
- displayNoneStyle = stylesApi.css('display: none');
- });
+ const className1 = displayInlineStyle.getClassName(elements[0]);
+ const className2 = displayNoneStyle.getClassName(elements[1]);
+ const className3 = displayInlineStyle.getClassName(elements[2]);
- teardown(() => {
- displayInlineStyle = null;
- displayNoneStyle = null;
- stylesApi = null;
- sandbox.restore();
- });
+ assert.notEqual(className2, className1);
+ assert.equal(className3, className1);
- function createNestedElements(parentElement) {
- /* parentElement
- * |--- element1
- * |--- element2
- * |--- element3
- **/
- const element1 = document.createElement('div');
- const element2 = document.createElement('div');
- const element3 = document.createElement('div');
- Polymer.dom(parentElement).appendChild(element1);
- Polymer.dom(parentElement).appendChild(element2);
- Polymer.dom(element2).appendChild(element3);
+ assertAllElementsHaveDefaultStyle(elements);
- return [element1, element2, element3];
+ elements[0].classList.add(className1);
+ elements[1].classList.add(className2);
+ elements[2].classList.add(className1);
+
+ assertDisplayPropertyValues(elements, ['inline', 'none', 'inline']);
+ }
+
+ test('apply - body level elements', () => {
+ const bodyLevelElements = createNestedElements(document.body);
+
+ testApply(bodyLevelElements);
+ });
+
+ test('apply - elements inside polymer element', () => {
+ const polymerElement = document.createElement('gr-style-test-element');
+ dom(document.body).appendChild(polymerElement);
+ const contentElements = createNestedElements(polymerElement.$.wrapper);
+
+ testApply(contentElements);
+ });
+
+ function testApply(elements) {
+ assertAllElementsHaveDefaultStyle(elements);
+ displayInlineStyle.apply(elements[0]);
+ displayNoneStyle.apply(elements[1]);
+ displayInlineStyle.apply(elements[2]);
+ assertDisplayPropertyValues(elements, ['inline', 'none', 'inline']);
+ }
+
+ function assertAllElementsHaveDefaultStyle(elements) {
+ for (const element of elements) {
+ assert.equal(getComputedStyle(element).getPropertyValue('display'),
+ 'block');
}
+ }
- test('getClassName - body level elements', () => {
- const bodyLevelElements = createNestedElements(document.body);
-
- testGetClassName(bodyLevelElements);
- });
-
- test('getClassName - elements inside polymer element', () => {
- const polymerElement = document.createElement('gr-style-test-element');
- Polymer.dom(document.body).appendChild(polymerElement);
- const contentElements = createNestedElements(polymerElement.$.wrapper);
-
- testGetClassName(contentElements);
- });
-
- function testGetClassName(elements) {
- assertAllElementsHaveDefaultStyle(elements);
-
- const className1 = displayInlineStyle.getClassName(elements[0]);
- const className2 = displayNoneStyle.getClassName(elements[1]);
- const className3 = displayInlineStyle.getClassName(elements[2]);
-
- assert.notEqual(className2, className1);
- assert.equal(className3, className1);
-
- assertAllElementsHaveDefaultStyle(elements);
-
- elements[0].classList.add(className1);
- elements[1].classList.add(className2);
- elements[2].classList.add(className1);
-
- assertDisplayPropertyValues(elements, ['inline', 'none', 'inline']);
- }
-
- test('apply - body level elements', () => {
- const bodyLevelElements = createNestedElements(document.body);
-
- testApply(bodyLevelElements);
- });
-
- test('apply - elements inside polymer element', () => {
- const polymerElement = document.createElement('gr-style-test-element');
- Polymer.dom(document.body).appendChild(polymerElement);
- const contentElements = createNestedElements(polymerElement.$.wrapper);
-
- testApply(contentElements);
- });
-
- function testApply(elements) {
- assertAllElementsHaveDefaultStyle(elements);
- displayInlineStyle.apply(elements[0]);
- displayNoneStyle.apply(elements[1]);
- displayInlineStyle.apply(elements[2]);
- assertDisplayPropertyValues(elements, ['inline', 'none', 'inline']);
- }
-
- function assertAllElementsHaveDefaultStyle(elements) {
- for (const element of elements) {
- assert.equal(getComputedStyle(element).getPropertyValue('display'),
- 'block');
+ function assertDisplayPropertyValues(elements, expectedDisplayValues) {
+ for (const key in elements) {
+ if (elements.hasOwnProperty(key)) {
+ assert.equal(
+ getComputedStyle(elements[key]).getPropertyValue('display'),
+ expectedDisplayValues[key]);
}
}
-
- function assertDisplayPropertyValues(elements, expectedDisplayValues) {
- for (const key in elements) {
- if (elements.hasOwnProperty(key)) {
- assert.equal(
- getComputedStyle(elements[key]).getPropertyValue('display'),
- expectedDisplayValues[key]);
- }
- }
- }
- });
+ }
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.html
deleted file mode 100644
index f0eacd2..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.html
+++ /dev/null
@@ -1,46 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<dom-module id="gr-custom-plugin-header">
- <template>
- <style>
- img {
- width: 1em;
- height: 1em;
- vertical-align: middle;
- }
- .title {
- margin-left: var(--spacing-xs);
- }
- </style>
- <span>
- <img src="[[logoUrl]]" hidden$="[[!logoUrl]]">
- <span class="title">[[title]]</span>
- </span>
- </template>
- <script>
- Polymer({
- is: 'gr-custom-plugin-header',
- properties: {
- logoUrl: String,
- title: String,
- },
- });
- </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
new file mode 100644
index 0000000..411a7c8
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../../scripts/bundled-polymer.js';
+
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+Polymer({
+ _template: html`
+ <style>
+ img {
+ width: 1em;
+ height: 1em;
+ vertical-align: middle;
+ }
+ .title {
+ margin-left: var(--spacing-xs);
+ }
+ </style>
+ <span>
+ <img src="[[logoUrl]]" hidden\$="[[!logoUrl]]">
+ <span class="title">[[title]]</span>
+ </span>
+`,
+
+ is: 'gr-custom-plugin-header',
+
+ properties: {
+ logoUrl: String,
+ title: String,
+ },
+});
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html
deleted file mode 100644
index ef1c9d4..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="gr-custom-plugin-header.html">
-
-<dom-module id="gr-theme-api">
- <script src="gr-theme-api.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
index d145f52f..8da680b 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
@@ -14,6 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+import './gr-custom-plugin-header.js';
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-theme-api">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
index 80853f5..83ea1bd 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
@@ -19,16 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-theme-api</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="../gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="gr-theme-api.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="header-title">
<template>
@@ -38,50 +32,52 @@
</template>
</test-fixture>
-<script>
- suite('gr-theme-api tests', async () => {
- await readyToTest();
- let sandbox;
- let theme;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../gr-endpoint-decorator/gr-endpoint-decorator.js';
+import './gr-theme-api.js';
+suite('gr-theme-api tests', () => {
+ let sandbox;
+ let theme;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ theme = plugin.theme();
+ });
+
+ teardown(() => {
+ theme = null;
+ sandbox.restore();
+ });
+
+ test('exists', () => {
+ assert.isOk(theme);
+ });
+
+ suite('header-title', () => {
+ let customHeader;
setup(() => {
- sandbox = sinon.sandbox.create();
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- theme = plugin.theme();
- });
-
- teardown(() => {
- theme = null;
- sandbox.restore();
- });
-
- test('exists', () => {
- assert.isOk(theme);
- });
-
- suite('header-title', () => {
- let customHeader;
-
- setup(() => {
- fixture('header-title');
- stub('gr-custom-plugin-header', {
- /** @override */
- ready() { customHeader = this; },
- });
- Gerrit._loadPlugins([]);
+ fixture('header-title');
+ stub('gr-custom-plugin-header', {
+ /** @override */
+ ready() { customHeader = this; },
});
+ Gerrit._loadPlugins([]);
+ });
- test('sets logo and title', done => {
- theme.setHeaderLogoAndTitle('foo.jpg', 'bar');
- flush(() => {
- assert.isNotNull(customHeader);
- assert.equal(customHeader.logoUrl, 'foo.jpg');
- assert.equal(customHeader.title, 'bar');
- done();
- });
+ test('sets logo and title', done => {
+ theme.setHeaderLogoAndTitle('foo.jpg', 'bar');
+ flush(() => {
+ assert.isNotNull(customHeader);
+ assert.equal(customHeader.logoUrl, 'foo.jpg');
+ assert.equal(customHeader.title, 'bar');
+ done();
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
deleted file mode 100644
index e685030..0000000
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
+++ /dev/null
@@ -1,137 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-avatar/gr-avatar.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-
-<dom-module id="gr-account-info">
- <template>
- <style include="shared-styles">
- gr-avatar {
- height: 120px;
- width: 120px;
- margin-right: var(--spacing-xs);
- vertical-align: -.25em;
- }
- div section.hide {
- display: none;
- }
- </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">
- <section>
- <span class="title"></span>
- <span class="value">
- <gr-avatar account="[[_account]]"
- image-size="120"></gr-avatar>
- </span>
- </section>
- <section class$="[[_hideAvatarChangeUrl(_avatarChangeUrl)]]">
- <span class="title"></span>
- <span class="value">
- <a href$="[[_avatarChangeUrl]]">
- Change avatar
- </a>
- </span>
- </section>
- <section>
- <span class="title">ID</span>
- <span class="value">[[_account._account_id]]</span>
- </section>
- <section>
- <span class="title">Email</span>
- <span class="value">[[_account.email]]</span>
- </section>
- <section>
- <span class="title">Registered</span>
- <span class="value">
- <gr-date-formatter
- has-tooltip
- date-str="[[_account.registered_on]]"></gr-date-formatter>
- </span>
- </section>
- <section id="usernameSection">
- <span class="title">Username</span>
- <span
- hidden$="[[usernameMutable]]"
- class="value">[[_username]]</span>
- <span
- hidden$="[[!usernameMutable]]"
- class="value">
- <iron-input
- on-keydown="_handleKeydown"
- bind-value="{{_username}}">
- <input
- is="iron-input"
- id="usernameInput"
- disabled="[[_saving]]"
- on-keydown="_handleKeydown"
- bind-value="{{_username}}">
- </iron-input>
- </span>
- </section>
- <section id="nameSection">
- <span class="title">Full name</span>
- <span
- hidden$="[[nameMutable]]"
- class="value">[[_account.name]]</span>
- <span
- hidden$="[[!nameMutable]]"
- class="value">
- <iron-input
- on-keydown="_handleKeydown"
- bind-value="{{_account.name}}">
- <input
- is="iron-input"
- id="nameInput"
- disabled="[[_saving]]"
- on-keydown="_handleKeydown"
- bind-value="{{_account.name}}">
- </iron-input>
- </span>
- </section>
- <section>
- <span class="title">Status (e.g. "Vacation")</span>
- <span class="value">
- <iron-input
- on-keydown="_handleKeydown"
- bind-value="{{_account.status}}">
- <input
- is="iron-input"
- id="statusInput"
- disabled="[[_saving]]"
- on-keydown="_handleKeydown"
- bind-value="{{_account.status}}">
- </iron-input>
- </span>
- </section>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-account-info.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
index 7bf641d..0c4b706 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -14,195 +14,226 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../shared/gr-avatar/gr-avatar.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-account-info_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrAccountInfo extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-account-info'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when account details are changed.
+ *
+ * @event account-detail-update
*/
- class GrAccountInfo extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-account-info'; }
- /**
- * Fired when account details are changed.
- *
- * @event account-detail-update
- */
- static get properties() {
- return {
- usernameMutable: {
- type: Boolean,
- notify: true,
- computed: '_computeUsernameMutable(_serverConfig, _account.username)',
- },
- nameMutable: {
- type: Boolean,
- notify: true,
- computed: '_computeNameMutable(_serverConfig)',
- },
- hasUnsavedChanges: {
- type: Boolean,
- notify: true,
- computed: '_computeHasUnsavedChanges(_hasNameChange, ' +
- '_hasUsernameChange, _hasStatusChange)',
- },
+ static get properties() {
+ return {
+ usernameMutable: {
+ type: Boolean,
+ notify: true,
+ computed: '_computeUsernameMutable(_serverConfig, _account.username)',
+ },
+ nameMutable: {
+ type: Boolean,
+ notify: true,
+ computed: '_computeNameMutable(_serverConfig)',
+ },
+ hasUnsavedChanges: {
+ type: Boolean,
+ notify: true,
+ computed: '_computeHasUnsavedChanges(_hasNameChange, ' +
+ '_hasUsernameChange, _hasStatusChange, _hasDisplayNameChange)',
+ },
- _hasNameChange: Boolean,
- _hasUsernameChange: Boolean,
- _hasStatusChange: Boolean,
- _loading: {
- type: Boolean,
- value: false,
- },
- _saving: {
- type: Boolean,
- value: false,
- },
- /** @type {?} */
- _account: Object,
- _serverConfig: Object,
- _username: {
- type: String,
- observer: '_usernameChanged',
- },
- _avatarChangeUrl: {
- type: String,
- value: '',
- },
- };
+ _hasNameChange: Boolean,
+ _hasUsernameChange: Boolean,
+ _hasDisplayNameChange: Boolean,
+ _hasStatusChange: Boolean,
+ _loading: {
+ type: Boolean,
+ value: false,
+ },
+ _saving: {
+ type: Boolean,
+ value: false,
+ },
+ /** @type {?} */
+ _account: Object,
+ _serverConfig: Object,
+ _username: {
+ type: String,
+ observer: '_usernameChanged',
+ },
+ _avatarChangeUrl: {
+ type: String,
+ value: '',
+ },
+ };
+ }
+
+ static get observers() {
+ return [
+ '_nameChanged(_account.name)',
+ '_statusChanged(_account.status)',
+ '_displayNameChanged(_account.display_name)',
+ ];
+ }
+
+ loadData() {
+ const promises = [];
+
+ this._loading = true;
+
+ promises.push(this.$.restAPI.getConfig().then(config => {
+ this._serverConfig = config;
+ }));
+
+ promises.push(this.$.restAPI.getAccount().then(account => {
+ this._hasNameChange = false;
+ this._hasUsernameChange = false;
+ this._hasDisplayNameChange = false;
+ this._hasStatusChange = false;
+ // Provide predefined value for username to trigger computation of
+ // username mutability.
+ account.username = account.username || '';
+ this._account = account;
+ this._username = account.username;
+ }));
+
+ promises.push(this.$.restAPI.getAvatarChangeUrl().then(url => {
+ this._avatarChangeUrl = url;
+ }));
+
+ return Promise.all(promises).then(() => {
+ this._loading = false;
+ });
+ }
+
+ save() {
+ if (!this.hasUnsavedChanges) {
+ return Promise.resolve();
}
- static get observers() {
- return [
- '_nameChanged(_account.name)',
- '_statusChanged(_account.status)',
- ];
+ this._saving = true;
+ // Set only the fields that have changed.
+ // Must be done in sequence to avoid race conditions (@see Issue 5721)
+ return this._maybeSetName()
+ .then(() => this._maybeSetUsername())
+ .then(() => this._maybeSetDisplayName())
+ .then(() => this._maybeSetStatus())
+ .then(() => {
+ this._hasNameChange = false;
+ this._hasDisplayNameChange = false;
+ this._hasStatusChange = false;
+ this._saving = false;
+ this.fire('account-detail-update');
+ });
+ }
+
+ _maybeSetName() {
+ return this._hasNameChange && this.nameMutable ?
+ this.$.restAPI.setAccountName(this._account.name) :
+ Promise.resolve();
+ }
+
+ _maybeSetUsername() {
+ return this._hasUsernameChange && this.usernameMutable ?
+ this.$.restAPI.setAccountUsername(this._username) :
+ Promise.resolve();
+ }
+
+ _maybeSetDisplayName() {
+ return this._hasDisplayNameChange ?
+ this.$.restAPI.setAccountDisplayName(this._account.display_name) :
+ Promise.resolve();
+ }
+
+ _maybeSetStatus() {
+ return this._hasStatusChange ?
+ this.$.restAPI.setAccountStatus(this._account.status) :
+ Promise.resolve();
+ }
+
+ _computeHasUnsavedChanges(nameChanged, usernameChanged, statusChanged,
+ displayNameChanged) {
+ return nameChanged || usernameChanged || statusChanged
+ || displayNameChanged;
+ }
+
+ _computeUsernameMutable(config, username) {
+ // Polymer 2: check for undefined
+ if ([
+ config,
+ username,
+ ].some(arg => arg === undefined)) {
+ return undefined;
}
- loadData() {
- const promises = [];
+ // Username may not be changed once it is set.
+ return config.auth.editable_account_fields.includes('USER_NAME') &&
+ !username;
+ }
- this._loading = true;
+ _computeNameMutable(config) {
+ return config.auth.editable_account_fields.includes('FULL_NAME');
+ }
- promises.push(this.$.restAPI.getConfig().then(config => {
- this._serverConfig = config;
- }));
+ _statusChanged() {
+ if (this._loading) { return; }
+ this._hasStatusChange = true;
+ }
- promises.push(this.$.restAPI.getAccount().then(account => {
- this._hasNameChange = false;
- this._hasUsernameChange = false;
- this._hasStatusChange = false;
- // Provide predefined value for username to trigger computation of
- // username mutability.
- account.username = account.username || '';
- this._account = account;
- this._username = account.username;
- }));
+ _displayNameChanged() {
+ if (this._loading) { return; }
+ this._hasDisplayNameChange = true;
+ }
- promises.push(this.$.restAPI.getAvatarChangeUrl().then(url => {
- this._avatarChangeUrl = url;
- }));
+ _usernameChanged() {
+ if (this._loading || !this._account) { return; }
+ this._hasUsernameChange =
+ (this._account.username || '') !== (this._username || '');
+ }
- return Promise.all(promises).then(() => {
- this._loading = false;
- });
- }
+ _nameChanged() {
+ if (this._loading) { return; }
+ this._hasNameChange = true;
+ }
- save() {
- if (!this.hasUnsavedChanges) {
- return Promise.resolve();
- }
-
- this._saving = true;
- // Set only the fields that have changed.
- // Must be done in sequence to avoid race conditions (@see Issue 5721)
- return this._maybeSetName()
- .then(this._maybeSetUsername.bind(this))
- .then(this._maybeSetStatus.bind(this))
- .then(() => {
- this._hasNameChange = false;
- this._hasStatusChange = false;
- this._saving = false;
- this.fire('account-detail-update');
- });
- }
-
- _maybeSetName() {
- return this._hasNameChange && this.nameMutable ?
- this.$.restAPI.setAccountName(this._account.name) :
- Promise.resolve();
- }
-
- _maybeSetUsername() {
- return this._hasUsernameChange && this.usernameMutable ?
- this.$.restAPI.setAccountUsername(this._username) :
- Promise.resolve();
- }
-
- _maybeSetStatus() {
- return this._hasStatusChange ?
- this.$.restAPI.setAccountStatus(this._account.status) :
- Promise.resolve();
- }
-
- _computeHasUnsavedChanges(nameChanged, usernameChanged, statusChanged) {
- return nameChanged || usernameChanged || statusChanged;
- }
-
- _computeUsernameMutable(config, username) {
- // Polymer 2: check for undefined
- if ([
- config,
- username,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- // Username may not be changed once it is set.
- return config.auth.editable_account_fields.includes('USER_NAME') &&
- !username;
- }
-
- _computeNameMutable(config) {
- return config.auth.editable_account_fields.includes('FULL_NAME');
- }
-
- _statusChanged() {
- if (this._loading) { return; }
- this._hasStatusChange = true;
- }
-
- _usernameChanged() {
- if (this._loading || !this._account) { return; }
- this._hasUsernameChange =
- (this._account.username || '') !== (this._username || '');
- }
-
- _nameChanged() {
- if (this._loading) { return; }
- this._hasNameChange = true;
- }
-
- _handleKeydown(e) {
- if (e.keyCode === 13) { // Enter
- e.stopPropagation();
- this.save();
- }
- }
-
- _hideAvatarChangeUrl(avatarChangeUrl) {
- if (!avatarChangeUrl) {
- return 'hide';
- }
-
- return '';
+ _handleKeydown(e) {
+ if (e.keyCode === 13) { // Enter
+ e.stopPropagation();
+ this.save();
}
}
- customElements.define(GrAccountInfo.is, GrAccountInfo);
-})();
+ _hideAvatarChangeUrl(avatarChangeUrl) {
+ if (!avatarChangeUrl) {
+ return 'hide';
+ }
+
+ return '';
+ }
+}
+
+customElements.define(GrAccountInfo.is, GrAccountInfo);
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.js
new file mode 100644
index 0000000..0f5ddc8
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.js
@@ -0,0 +1,99 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ gr-avatar {
+ height: 120px;
+ width: 120px;
+ margin-right: var(--spacing-xs);
+ vertical-align: -.25em;
+ }
+ div section.hide {
+ display: none;
+ }
+ </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">
+ <section>
+ <span class="title"></span>
+ <span class="value">
+ <gr-avatar account="[[_account]]" image-size="120"></gr-avatar>
+ </span>
+ </section>
+ <section class\$="[[_hideAvatarChangeUrl(_avatarChangeUrl)]]">
+ <span class="title"></span>
+ <span class="value">
+ <a href\$="[[_avatarChangeUrl]]">
+ Change avatar
+ </a>
+ </span>
+ </section>
+ <section>
+ <span class="title">ID</span>
+ <span class="value">[[_account._account_id]]</span>
+ </section>
+ <section>
+ <span class="title">Email</span>
+ <span class="value">[[_account.email]]</span>
+ </section>
+ <section>
+ <span class="title">Registered</span>
+ <span class="value">
+ <gr-date-formatter has-tooltip="" date-str="[[_account.registered_on]]"></gr-date-formatter>
+ </span>
+ </section>
+ <section id="usernameSection">
+ <span class="title">Username</span>
+ <span hidden\$="[[usernameMutable]]" class="value">[[_username]]</span>
+ <span hidden\$="[[!usernameMutable]]" class="value">
+ <iron-input on-keydown="_handleKeydown" bind-value="{{_username}}">
+ <input is="iron-input" id="usernameInput" disabled="[[_saving]]" on-keydown="_handleKeydown" bind-value="{{_username}}">
+ </iron-input>
+ </span>
+ </section>
+ <section id="nameSection">
+ <span class="title">Full name</span>
+ <span hidden\$="[[nameMutable]]" class="value">[[_account.name]]</span>
+ <span hidden\$="[[!nameMutable]]" class="value">
+ <iron-input on-keydown="_handleKeydown" bind-value="{{_account.name}}">
+ <input is="iron-input" id="nameInput" disabled="[[_saving]]" on-keydown="_handleKeydown" bind-value="{{_account.name}}">
+ </iron-input>
+ </span>
+ </section>
+ <section>
+ <span class="title">Display name (defaults to "Full name")</span>
+ <span class="value">
+ <iron-input on-keydown="_handleKeydown" bind-value="{{_account.display_name}}">
+ <input is="iron-input" id="displayNameInput" disabled="[[_saving]]" on-keydown="_handleKeydown" bind-value="{{_account.display_name}}">
+ </iron-input>
+ </span>
+ </section>
+ <section>
+ <span class="title">Status (e.g. "Vacation")</span>
+ <span class="value">
+ <iron-input on-keydown="_handleKeydown" bind-value="{{_account.status}}">
+ <input is="iron-input" id="statusInput" disabled="[[_saving]]" on-keydown="_handleKeydown" bind-value="{{_account.status}}">
+ </iron-input>
+ </span>
+ </section>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
index 80c7399..14bc5de 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-account-info</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-account-info.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,310 +30,312 @@
</template>
</test-fixture>
-<script>
- suite('gr-account-info tests', async () => {
- await readyToTest();
- let element;
- let account;
- let config;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-account-info.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-account-info tests', () => {
+ let element;
+ let account;
+ let config;
+ let sandbox;
- function valueOf(title) {
- const sections = Polymer.dom(element.root).querySelectorAll('section');
- let titleEl;
- for (let i = 0; i < sections.length; i++) {
- titleEl = sections[i].querySelector('.title');
- if (titleEl.textContent === title) {
- return sections[i].querySelector('.value');
- }
+ function valueOf(title) {
+ const sections = dom(element.root).querySelectorAll('section');
+ let titleEl;
+ for (let i = 0; i < sections.length; i++) {
+ titleEl = sections[i].querySelector('.title');
+ if (titleEl.textContent === title) {
+ return sections[i].querySelector('.value');
}
}
+ }
- setup(done => {
- sandbox = sinon.sandbox.create();
- account = {
- _account_id: 123,
- name: 'user name',
- email: 'user@email',
- username: 'user username',
- registered: '2000-01-01 00:00:00.000000000',
- };
- config = {auth: {editable_account_fields: []}};
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ account = {
+ _account_id: 123,
+ name: 'user name',
+ email: 'user@email',
+ username: 'user username',
+ registered: '2000-01-01 00:00:00.000000000',
+ };
+ config = {auth: {editable_account_fields: []}};
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve(account); },
- getConfig() { return Promise.resolve(config); },
- getPreferences() {
- return Promise.resolve({time_format: 'HHMM_12'});
- },
- });
- element = fixture('basic');
- // Allow the element to render.
- element.loadData().then(() => { flush(done); });
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve(account); },
+ getConfig() { return Promise.resolve(config); },
+ getPreferences() {
+ return Promise.resolve({time_format: 'HHMM_12'});
+ },
});
+ element = fixture('basic');
+ // Allow the element to render.
+ element.loadData().then(() => { flush(done); });
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('basic account info render', () => {
- assert.isFalse(element._loading);
+ test('basic account info render', () => {
+ assert.isFalse(element._loading);
- assert.equal(valueOf('ID').textContent, account._account_id);
- assert.equal(valueOf('Email').textContent, account.email);
- assert.equal(valueOf('Username').textContent, account.username);
- });
+ assert.equal(valueOf('ID').textContent, account._account_id);
+ assert.equal(valueOf('Email').textContent, account.email);
+ assert.equal(valueOf('Username').textContent, account.username);
+ });
- test('full name render (immutable)', () => {
- const section = element.$.nameSection;
- const displaySpan = section.querySelectorAll('.value')[0];
- const inputSpan = section.querySelectorAll('.value')[1];
+ test('full name render (immutable)', () => {
+ const section = element.$.nameSection;
+ const displaySpan = section.querySelectorAll('.value')[0];
+ const inputSpan = section.querySelectorAll('.value')[1];
- assert.isFalse(element.nameMutable);
- assert.isFalse(displaySpan.hasAttribute('hidden'));
- assert.equal(displaySpan.textContent, account.name);
- assert.isTrue(inputSpan.hasAttribute('hidden'));
- });
+ assert.isFalse(element.nameMutable);
+ assert.isFalse(displaySpan.hasAttribute('hidden'));
+ assert.equal(displaySpan.textContent, account.name);
+ assert.isTrue(inputSpan.hasAttribute('hidden'));
+ });
- test('full name render (mutable)', () => {
+ test('full name render (mutable)', () => {
+ element.set('_serverConfig',
+ {auth: {editable_account_fields: ['FULL_NAME']}});
+
+ const section = element.$.nameSection;
+ const displaySpan = section.querySelectorAll('.value')[0];
+ const inputSpan = section.querySelectorAll('.value')[1];
+
+ assert.isTrue(element.nameMutable);
+ assert.isTrue(displaySpan.hasAttribute('hidden'));
+ assert.equal(element.$.nameInput.bindValue, account.name);
+ assert.isFalse(inputSpan.hasAttribute('hidden'));
+ });
+
+ test('username render (immutable)', () => {
+ const section = element.$.usernameSection;
+ const displaySpan = section.querySelectorAll('.value')[0];
+ const inputSpan = section.querySelectorAll('.value')[1];
+
+ assert.isFalse(element.usernameMutable);
+ assert.isFalse(displaySpan.hasAttribute('hidden'));
+ assert.equal(displaySpan.textContent, account.username);
+ assert.isTrue(inputSpan.hasAttribute('hidden'));
+ });
+
+ test('username render (mutable)', () => {
+ element.set('_serverConfig',
+ {auth: {editable_account_fields: ['USER_NAME']}});
+ element.set('_account.username', '');
+ element.set('_username', '');
+
+ const section = element.$.usernameSection;
+ const displaySpan = section.querySelectorAll('.value')[0];
+ const inputSpan = section.querySelectorAll('.value')[1];
+
+ assert.isTrue(element.usernameMutable);
+ assert.isTrue(displaySpan.hasAttribute('hidden'));
+ assert.equal(element.$.usernameInput.bindValue, account.username);
+ assert.isFalse(inputSpan.hasAttribute('hidden'));
+ });
+
+ suite('account info edit', () => {
+ let nameChangedSpy;
+ let usernameChangedSpy;
+ let statusChangedSpy;
+ let nameStub;
+ let usernameStub;
+ let statusStub;
+
+ setup(() => {
+ nameChangedSpy = sandbox.spy(element, '_nameChanged');
+ usernameChangedSpy = sandbox.spy(element, '_usernameChanged');
+ statusChangedSpy = sandbox.spy(element, '_statusChanged');
element.set('_serverConfig',
- {auth: {editable_account_fields: ['FULL_NAME']}});
+ {auth: {editable_account_fields: ['FULL_NAME', 'USER_NAME']}});
- const section = element.$.nameSection;
- const displaySpan = section.querySelectorAll('.value')[0];
- const inputSpan = section.querySelectorAll('.value')[1];
+ nameStub = sandbox.stub(element.$.restAPI, 'setAccountName',
+ name => Promise.resolve());
+ usernameStub = sandbox.stub(element.$.restAPI, 'setAccountUsername',
+ username => Promise.resolve());
+ statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
+ status => Promise.resolve());
+ });
+ test('name', done => {
assert.isTrue(element.nameMutable);
- assert.isTrue(displaySpan.hasAttribute('hidden'));
- assert.equal(element.$.nameInput.bindValue, account.name);
- assert.isFalse(inputSpan.hasAttribute('hidden'));
- });
+ assert.isFalse(element.hasUnsavedChanges);
- test('username render (immutable)', () => {
- const section = element.$.usernameSection;
- const displaySpan = section.querySelectorAll('.value')[0];
- const inputSpan = section.querySelectorAll('.value')[1];
+ element.set('_account.name', 'new name');
- assert.isFalse(element.usernameMutable);
- assert.isFalse(displaySpan.hasAttribute('hidden'));
- assert.equal(displaySpan.textContent, account.username);
- assert.isTrue(inputSpan.hasAttribute('hidden'));
- });
+ assert.isTrue(nameChangedSpy.called);
+ assert.isFalse(statusChangedSpy.called);
+ assert.isTrue(element.hasUnsavedChanges);
- test('username render (mutable)', () => {
- element.set('_serverConfig',
- {auth: {editable_account_fields: ['USER_NAME']}});
- element.set('_account.username', '');
- element.set('_username', '');
-
- const section = element.$.usernameSection;
- const displaySpan = section.querySelectorAll('.value')[0];
- const inputSpan = section.querySelectorAll('.value')[1];
-
- assert.isTrue(element.usernameMutable);
- assert.isTrue(displaySpan.hasAttribute('hidden'));
- assert.equal(element.$.usernameInput.bindValue, account.username);
- assert.isFalse(inputSpan.hasAttribute('hidden'));
- });
-
- suite('account info edit', () => {
- let nameChangedSpy;
- let usernameChangedSpy;
- let statusChangedSpy;
- let nameStub;
- let usernameStub;
- let statusStub;
-
- setup(() => {
- nameChangedSpy = sandbox.spy(element, '_nameChanged');
- usernameChangedSpy = sandbox.spy(element, '_usernameChanged');
- statusChangedSpy = sandbox.spy(element, '_statusChanged');
- element.set('_serverConfig',
- {auth: {editable_account_fields: ['FULL_NAME', 'USER_NAME']}});
-
- nameStub = sandbox.stub(element.$.restAPI, 'setAccountName',
- name => Promise.resolve());
- usernameStub = sandbox.stub(element.$.restAPI, 'setAccountUsername',
- username => Promise.resolve());
- statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
- status => Promise.resolve());
- });
-
- test('name', done => {
- assert.isTrue(element.nameMutable);
- assert.isFalse(element.hasUnsavedChanges);
-
- element.set('_account.name', 'new name');
-
- assert.isTrue(nameChangedSpy.called);
- assert.isFalse(statusChangedSpy.called);
- assert.isTrue(element.hasUnsavedChanges);
-
- element.save().then(() => {
- assert.isFalse(usernameStub.called);
- assert.isTrue(nameStub.called);
- assert.isFalse(statusStub.called);
- nameStub.lastCall.returnValue.then(() => {
- assert.equal(nameStub.lastCall.args[0], 'new name');
- done();
- });
- });
- });
-
- test('username', done => {
- element.set('_account.username', '');
- element._hasUsernameChange = false;
- assert.isTrue(element.usernameMutable);
-
- element.set('_username', 'new username');
-
- assert.isTrue(usernameChangedSpy.called);
- assert.isFalse(statusChangedSpy.called);
- assert.isTrue(element.hasUnsavedChanges);
-
- element.save().then(() => {
- assert.isTrue(usernameStub.called);
- assert.isFalse(nameStub.called);
- assert.isFalse(statusStub.called);
- usernameStub.lastCall.returnValue.then(() => {
- assert.equal(usernameStub.lastCall.args[0], 'new username');
- done();
- });
- });
- });
-
- test('status', done => {
- assert.isFalse(element.hasUnsavedChanges);
-
- element.set('_account.status', 'new status');
-
- assert.isFalse(nameChangedSpy.called);
- assert.isTrue(statusChangedSpy.called);
- assert.isTrue(element.hasUnsavedChanges);
-
- element.save().then(() => {
- assert.isFalse(usernameStub.called);
- assert.isTrue(statusStub.called);
- assert.isFalse(nameStub.called);
- statusStub.lastCall.returnValue.then(() => {
- assert.equal(statusStub.lastCall.args[0], 'new status');
- done();
- });
- });
- });
- });
-
- suite('edit name and status', () => {
- let nameChangedSpy;
- let statusChangedSpy;
- let nameStub;
- let statusStub;
-
- setup(() => {
- nameChangedSpy = sandbox.spy(element, '_nameChanged');
- statusChangedSpy = sandbox.spy(element, '_statusChanged');
- element.set('_serverConfig',
- {auth: {editable_account_fields: ['FULL_NAME']}});
-
- nameStub = sandbox.stub(element.$.restAPI, 'setAccountName',
- name => Promise.resolve());
- statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
- status => Promise.resolve());
- sandbox.stub(element.$.restAPI, 'setAccountUsername',
- username => Promise.resolve());
- });
-
- test('set name and status', done => {
- assert.isTrue(element.nameMutable);
- assert.isFalse(element.hasUnsavedChanges);
-
- element.set('_account.name', 'new name');
-
- assert.isTrue(nameChangedSpy.called);
-
- element.set('_account.status', 'new status');
-
- assert.isTrue(statusChangedSpy.called);
-
- assert.isTrue(element.hasUnsavedChanges);
-
- element.save().then(() => {
- assert.isTrue(statusStub.called);
- assert.isTrue(nameStub.called);
-
+ element.save().then(() => {
+ assert.isFalse(usernameStub.called);
+ assert.isTrue(nameStub.called);
+ assert.isFalse(statusStub.called);
+ nameStub.lastCall.returnValue.then(() => {
assert.equal(nameStub.lastCall.args[0], 'new name');
-
- assert.equal(statusStub.lastCall.args[0], 'new status');
-
done();
});
});
});
- suite('set status but read name', () => {
- let statusChangedSpy;
- let statusStub;
+ test('username', done => {
+ element.set('_account.username', '');
+ element._hasUsernameChange = false;
+ assert.isTrue(element.usernameMutable);
- setup(() => {
- statusChangedSpy = sandbox.spy(element, '_statusChanged');
- element.set('_serverConfig',
- {auth: {editable_account_fields: []}});
+ element.set('_username', 'new username');
- statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
- status => Promise.resolve());
- });
+ assert.isTrue(usernameChangedSpy.called);
+ assert.isFalse(statusChangedSpy.called);
+ assert.isTrue(element.hasUnsavedChanges);
- test('read full name but set status', done => {
- const section = element.$.nameSection;
- const displaySpan = section.querySelectorAll('.value')[0];
- const inputSpan = section.querySelectorAll('.value')[1];
-
- assert.isFalse(element.nameMutable);
-
- assert.isFalse(element.hasUnsavedChanges);
-
- assert.isFalse(displaySpan.hasAttribute('hidden'));
- assert.equal(displaySpan.textContent, account.name);
- assert.isTrue(inputSpan.hasAttribute('hidden'));
-
- element.set('_account.status', 'new status');
-
- assert.isTrue(statusChangedSpy.called);
-
- assert.isTrue(element.hasUnsavedChanges);
-
- element.save().then(() => {
- assert.isTrue(statusStub.called);
- statusStub.lastCall.returnValue.then(() => {
- assert.equal(statusStub.lastCall.args[0], 'new status');
- done();
- });
+ element.save().then(() => {
+ assert.isTrue(usernameStub.called);
+ assert.isFalse(nameStub.called);
+ assert.isFalse(statusStub.called);
+ usernameStub.lastCall.returnValue.then(() => {
+ assert.equal(usernameStub.lastCall.args[0], 'new username');
+ done();
});
});
});
- test('_usernameChanged compares usernames with loose equality', () => {
- element._account = {};
- element._username = '';
- element._hasUsernameChange = false;
- element._loading = false;
- // _usernameChanged is an observer, but call it here after setting
- // _hasUsernameChange in the test to force recomputation.
- element._usernameChanged();
- flushAsynchronousOperations();
+ test('status', done => {
+ assert.isFalse(element.hasUnsavedChanges);
- assert.isFalse(element._hasUsernameChange);
+ element.set('_account.status', 'new status');
- element.set('_username', 'test');
- flushAsynchronousOperations();
+ assert.isFalse(nameChangedSpy.called);
+ assert.isTrue(statusChangedSpy.called);
+ assert.isTrue(element.hasUnsavedChanges);
- assert.isTrue(element._hasUsernameChange);
- });
-
- test('_hideAvatarChangeUrl', () => {
- assert.equal(element._hideAvatarChangeUrl(''), 'hide');
-
- assert.equal(element._hideAvatarChangeUrl('https://example.com'), '');
+ element.save().then(() => {
+ assert.isFalse(usernameStub.called);
+ assert.isTrue(statusStub.called);
+ assert.isFalse(nameStub.called);
+ statusStub.lastCall.returnValue.then(() => {
+ assert.equal(statusStub.lastCall.args[0], 'new status');
+ done();
+ });
+ });
});
});
+
+ suite('edit name and status', () => {
+ let nameChangedSpy;
+ let statusChangedSpy;
+ let nameStub;
+ let statusStub;
+
+ setup(() => {
+ nameChangedSpy = sandbox.spy(element, '_nameChanged');
+ statusChangedSpy = sandbox.spy(element, '_statusChanged');
+ element.set('_serverConfig',
+ {auth: {editable_account_fields: ['FULL_NAME']}});
+
+ nameStub = sandbox.stub(element.$.restAPI, 'setAccountName',
+ name => Promise.resolve());
+ statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
+ status => Promise.resolve());
+ sandbox.stub(element.$.restAPI, 'setAccountUsername',
+ username => Promise.resolve());
+ });
+
+ test('set name and status', done => {
+ assert.isTrue(element.nameMutable);
+ assert.isFalse(element.hasUnsavedChanges);
+
+ element.set('_account.name', 'new name');
+
+ assert.isTrue(nameChangedSpy.called);
+
+ element.set('_account.status', 'new status');
+
+ assert.isTrue(statusChangedSpy.called);
+
+ assert.isTrue(element.hasUnsavedChanges);
+
+ element.save().then(() => {
+ assert.isTrue(statusStub.called);
+ assert.isTrue(nameStub.called);
+
+ assert.equal(nameStub.lastCall.args[0], 'new name');
+
+ assert.equal(statusStub.lastCall.args[0], 'new status');
+
+ done();
+ });
+ });
+ });
+
+ suite('set status but read name', () => {
+ let statusChangedSpy;
+ let statusStub;
+
+ setup(() => {
+ statusChangedSpy = sandbox.spy(element, '_statusChanged');
+ element.set('_serverConfig',
+ {auth: {editable_account_fields: []}});
+
+ statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
+ status => Promise.resolve());
+ });
+
+ test('read full name but set status', done => {
+ const section = element.$.nameSection;
+ const displaySpan = section.querySelectorAll('.value')[0];
+ const inputSpan = section.querySelectorAll('.value')[1];
+
+ assert.isFalse(element.nameMutable);
+
+ assert.isFalse(element.hasUnsavedChanges);
+
+ assert.isFalse(displaySpan.hasAttribute('hidden'));
+ assert.equal(displaySpan.textContent, account.name);
+ assert.isTrue(inputSpan.hasAttribute('hidden'));
+
+ element.set('_account.status', 'new status');
+
+ assert.isTrue(statusChangedSpy.called);
+
+ assert.isTrue(element.hasUnsavedChanges);
+
+ element.save().then(() => {
+ assert.isTrue(statusStub.called);
+ statusStub.lastCall.returnValue.then(() => {
+ assert.equal(statusStub.lastCall.args[0], 'new status');
+ done();
+ });
+ });
+ });
+ });
+
+ test('_usernameChanged compares usernames with loose equality', () => {
+ element._account = {};
+ element._username = '';
+ element._hasUsernameChange = false;
+ element._loading = false;
+ // _usernameChanged is an observer, but call it here after setting
+ // _hasUsernameChange in the test to force recomputation.
+ element._usernameChanged();
+ flushAsynchronousOperations();
+
+ assert.isFalse(element._hasUsernameChange);
+
+ element.set('_username', 'test');
+ flushAsynchronousOperations();
+
+ assert.isTrue(element._hasUsernameChange);
+ });
+
+ test('_hideAvatarChangeUrl', () => {
+ assert.equal(element._hideAvatarChangeUrl(''), 'hide');
+
+ assert.equal(element._hideAvatarChangeUrl('https://example.com'), '');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
deleted file mode 100644
index 74d92d3..0000000
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
+++ /dev/null
@@ -1,64 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-agreements-list">
- <template>
- <style include="shared-styles">
- #agreements .nameColumn {
- min-width: 15em;
- width: auto;
- }
- #agreements .descriptionColumn {
- width: auto;
- }
- </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 id="agreements">
- <thead>
- <tr>
- <th class="nameColumn">Name</th>
- <th class="descriptionColumn">Description</th>
- </tr>
- </thead>
- <tbody>
- <template is="dom-repeat" items="[[_agreements]]">
- <tr>
- <td class="nameColumn">
- <a href$="[[getUrlBase(item.url)]]" rel="external">
- [[item.name]]
- </a>
- </td>
- <td class="descriptionColumn">[[item.description]]</td>
- </tr>
- </template>
- </tbody>
- </table>
- <a href$="[[getUrl()]]">New Contributor Agreement</a>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-agreements-list.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
index 67dc0c4..18a0419 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
@@ -14,46 +14,56 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @extends Polymer.Element
- */
- class GrAgreementsList extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-agreements-list'; }
+import '../../../scripts/bundled-polymer.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-agreements-list_html.js';
- static get properties() {
- return {
- _agreements: Array,
- };
- }
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @extends Polymer.Element
+ */
+class GrAgreementsList extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** @override */
- attached() {
- super.attached();
- this.loadData();
- }
+ static get is() { return 'gr-agreements-list'; }
- loadData() {
- return this.$.restAPI.getAccountAgreements().then(agreements => {
- this._agreements = agreements;
- });
- }
-
- getUrl() {
- return this.getBaseUrl() + '/settings/new-agreement';
- }
-
- getUrlBase(item) {
- return this.getBaseUrl() + '/' + item;
- }
+ static get properties() {
+ return {
+ _agreements: Array,
+ };
}
- customElements.define(GrAgreementsList.is, GrAgreementsList);
-})();
+ /** @override */
+ attached() {
+ super.attached();
+ this.loadData();
+ }
+
+ loadData() {
+ return this.$.restAPI.getAccountAgreements().then(agreements => {
+ this._agreements = agreements;
+ });
+ }
+
+ getUrl() {
+ return this.getBaseUrl() + '/settings/new-agreement';
+ }
+
+ getUrlBase(item) {
+ return this.getBaseUrl() + '/' + item;
+ }
+}
+
+customElements.define(GrAgreementsList.is, GrAgreementsList);
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_html.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_html.js
new file mode 100644
index 0000000..4bd9365
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_html.js
@@ -0,0 +1,56 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ #agreements .nameColumn {
+ min-width: 15em;
+ width: auto;
+ }
+ #agreements .descriptionColumn {
+ width: auto;
+ }
+ </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 id="agreements">
+ <thead>
+ <tr>
+ <th class="nameColumn">Name</th>
+ <th class="descriptionColumn">Description</th>
+ </tr>
+ </thead>
+ <tbody>
+ <template is="dom-repeat" items="[[_agreements]]">
+ <tr>
+ <td class="nameColumn">
+ <a href\$="[[getUrlBase(item.url)]]" rel="external">
+ [[item.name]]
+ </a>
+ </td>
+ <td class="descriptionColumn">[[item.description]]</td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ <a href\$="[[getUrl()]]">New Contributor Agreement</a>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html
index 39b8663..0a008a4 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-settings-view</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-agreements-list.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,38 +30,40 @@
</template>
</test-fixture>
-<script>
- suite('gr-agreements-list tests', async () => {
- await readyToTest();
- let element;
- let agreements;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-agreements-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-agreements-list tests', () => {
+ let element;
+ let agreements;
- setup(done => {
- agreements = [{
- url: 'some url',
- description: 'Agreements 1 description',
- name: 'Agreements 1',
- }];
+ setup(done => {
+ agreements = [{
+ url: 'some url',
+ description: 'Agreements 1 description',
+ name: 'Agreements 1',
+ }];
- stub('gr-rest-api-interface', {
- getAccountAgreements() { return Promise.resolve(agreements); },
- });
-
- element = fixture('basic');
-
- element.loadData().then(() => { flush(done); });
+ stub('gr-rest-api-interface', {
+ getAccountAgreements() { return Promise.resolve(agreements); },
});
- test('renders', () => {
- const rows = Polymer.dom(element.root).querySelectorAll('tbody tr');
+ element = fixture('basic');
- assert.equal(rows.length, 1);
-
- const nameCells = Array.from(rows).map(row =>
- row.querySelectorAll('td')[0].textContent.trim()
- );
-
- assert.equal(nameCells[0], 'Agreements 1');
- });
+ element.loadData().then(() => { flush(done); });
});
+
+ test('renders', () => {
+ const rows = dom(element.root).querySelectorAll('tbody tr');
+
+ assert.equal(rows.length, 1);
+
+ const nameCells = Array.from(rows).map(row =>
+ row.querySelectorAll('td')[0].textContent.trim()
+ );
+
+ assert.equal(nameCells[0], 'Agreements 1');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
deleted file mode 100644
index 09a9dbc..0000000
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
+++ /dev/null
@@ -1,86 +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.
--->
-<link rel="import" href="../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-
-<dom-module id="gr-change-table-editor">
- <template>
- <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>Number</td>
- <td class="checkboxContainer"
- on-click="_handleCheckboxContainerClick">
- <input
- type="checkbox"
- name="number"
- on-click="_handleNumberCheckboxClick"
- checked$="[[showNumber]]">
- </td>
- </tr>
- <template is="dom-repeat" items="[[columnNames]]">
- <tr>
- <td>[[item]]</td>
- <td class="checkboxContainer"
- on-click="_handleCheckboxContainerClick">
- <input
- type="checkbox"
- name="[[item]]"
- on-click="_handleTargetClick"
- checked$="[[!isColumnHidden(item, displayedColumns)]]">
- </td>
- </tr>
- </template>
- </tbody>
- </table>
- </div>
- </template>
- <script src="gr-change-table-editor.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
index 8521126..85c34a4 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
@@ -14,72 +14,85 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
- /**
- * @appliesMixin Gerrit.ChangeTableMixin
- * @extends Polymer.Element
- */
- class GrChangeTableEditor extends Polymer.mixinBehaviors( [
- Gerrit.ChangeTableBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-change-table-editor'; }
+import '../../../scripts/bundled-polymer.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import '../../../styles/gr-form-styles.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-change-table-editor_html.js';
- static get properties() {
- return {
- displayedColumns: {
- type: Array,
- notify: true,
- },
- showNumber: {
- type: Boolean,
- notify: true,
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.ChangeTableMixin
+ * @extends Polymer.Element
+ */
+class GrChangeTableEditor extends mixinBehaviors( [
+ Gerrit.ChangeTableBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /**
- * Get the list of enabled column names from whichever checkboxes are
- * checked (excluding the number checkbox).
- *
- * @return {!Array<string>}
- */
- _getDisplayedColumns() {
- return Array.from(Polymer.dom(this.root)
- .querySelectorAll('.checkboxContainer input:not([name=number])'))
- .filter(checkbox => checkbox.checked)
- .map(checkbox => checkbox.name);
- }
+ static get is() { return 'gr-change-table-editor'; }
- /**
- * Handle a click on a checkbox container and relay the click to the checkbox it
- * contains.
- */
- _handleCheckboxContainerClick(e) {
- const checkbox = Polymer.dom(e.target).querySelector('input');
- if (!checkbox) { return; }
- checkbox.click();
- }
-
- /**
- * Handle a click on the number checkbox and update the showNumber property
- * accordingly.
- */
- _handleNumberCheckboxClick(e) {
- this.showNumber = Polymer.dom(e).rootTarget.checked;
- }
-
- /**
- * Handle a click on a displayed column checkboxes (excluding number) and
- * update the displayedColumns property accordingly.
- */
- _handleTargetClick(e) {
- this.set('displayedColumns', this._getDisplayedColumns());
- }
+ static get properties() {
+ return {
+ displayedColumns: {
+ type: Array,
+ notify: true,
+ },
+ showNumber: {
+ type: Boolean,
+ notify: true,
+ },
+ };
}
- customElements.define(GrChangeTableEditor.is, GrChangeTableEditor);
-})();
+ /**
+ * Get the list of enabled column names from whichever checkboxes are
+ * checked (excluding the number checkbox).
+ *
+ * @return {!Array<string>}
+ */
+ _getDisplayedColumns() {
+ return Array.from(dom(this.root)
+ .querySelectorAll('.checkboxContainer input:not([name=number])'))
+ .filter(checkbox => checkbox.checked)
+ .map(checkbox => checkbox.name);
+ }
+
+ /**
+ * Handle a click on a checkbox container and relay the click to the checkbox it
+ * contains.
+ */
+ _handleCheckboxContainerClick(e) {
+ const checkbox = dom(e.target).querySelector('input');
+ if (!checkbox) { return; }
+ checkbox.click();
+ }
+
+ /**
+ * Handle a click on the number checkbox and update the showNumber property
+ * accordingly.
+ */
+ _handleNumberCheckboxClick(e) {
+ this.showNumber = dom(e).rootTarget.checked;
+ }
+
+ /**
+ * Handle a click on a displayed column checkboxes (excluding number) and
+ * update the displayedColumns property accordingly.
+ */
+ _handleTargetClick(e) {
+ this.set('displayedColumns', this._getDisplayedColumns());
+ }
+}
+
+customElements.define(GrChangeTableEditor.is, GrChangeTableEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.js
new file mode 100644
index 0000000..7aa785c
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.js
@@ -0,0 +1,67 @@
+/**
+ * @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.js';
+
+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>Number</td>
+ <td class="checkboxContainer" on-click="_handleCheckboxContainerClick">
+ <input type="checkbox" name="number" on-click="_handleNumberCheckboxClick" checked\$="[[showNumber]]">
+ </td>
+ </tr>
+ <template is="dom-repeat" items="[[columnNames]]">
+ <tr>
+ <td>[[item]]</td>
+ <td class="checkboxContainer" on-click="_handleCheckboxContainerClick">
+ <input type="checkbox" name="[[item]]" on-click="_handleTargetClick" checked\$="[[!isColumnHidden(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.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
index 460d6bc..287ac3b 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-settings-view</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-change-table-editor.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,138 +30,139 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-table-editor tests', async () => {
- await readyToTest();
- let element;
- let columns;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-change-table-editor.js';
+suite('gr-change-table-editor tests', () => {
+ let element;
+ let columns;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
- columns = [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Repo',
- 'Branch',
- 'Updated',
- ];
+ columns = [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Repo',
+ 'Branch',
+ 'Updated',
+ ];
- element.set('displayedColumns', columns);
- element.showNumber = false;
- flushAsynchronousOperations();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('renders', () => {
- const rows = element.shadowRoot
- .querySelector('tbody').querySelectorAll('tr');
- let tds;
-
- // The `+ 1` is for the number column, which isn't included in the change
- // table behavior's list.
- assert.equal(rows.length, element.columnNames.length + 1);
- for (let i = 0; i < columns.length; i++) {
- tds = rows[i + 1].querySelectorAll('td');
- assert.equal(tds[0].textContent, columns[i]);
- }
- });
-
- test('hide item', () => {
- const checkbox = element.shadowRoot
- .querySelector('table tr:nth-child(2) input');
- const isChecked = checkbox.checked;
- const displayedLength = element.displayedColumns.length;
- assert.isTrue(isChecked);
-
- MockInteractions.tap(checkbox);
- flushAsynchronousOperations();
-
- assert.equal(element.displayedColumns.length, displayedLength - 1);
- });
-
- test('show item', () => {
- element.set('displayedColumns', [
- 'Status',
- 'Owner',
- 'Assignee',
- 'Repo',
- 'Branch',
- 'Updated',
- ]);
- flushAsynchronousOperations();
- const checkbox = element.shadowRoot
- .querySelector('table tr:nth-child(2) input');
- const isChecked = checkbox.checked;
- const displayedLength = element.displayedColumns.length;
- assert.isFalse(isChecked);
- assert.equal(element.shadowRoot
- .querySelector('table').style.display, '');
-
- MockInteractions.tap(checkbox);
- flushAsynchronousOperations();
-
- assert.equal(element.displayedColumns.length,
- displayedLength + 1);
- });
-
- test('_getDisplayedColumns', () => {
- assert.deepEqual(element._getDisplayedColumns(), columns);
- MockInteractions.tap(
- element.shadowRoot
- .querySelector('.checkboxContainer input[name=Assignee]'));
- assert.deepEqual(element._getDisplayedColumns(),
- columns.filter(c => c !== 'Assignee'));
- });
-
- test('_handleCheckboxContainerClick relayes taps to checkboxes', () => {
- sandbox.stub(element, '_handleNumberCheckboxClick');
- sandbox.stub(element, '_handleTargetClick');
-
- MockInteractions.tap(
- element.shadowRoot
- .querySelector('table tr:first-of-type .checkboxContainer'));
- assert.isTrue(element._handleNumberCheckboxClick.calledOnce);
- assert.isFalse(element._handleTargetClick.called);
-
- MockInteractions.tap(
- element.shadowRoot
- .querySelector('table tr:last-of-type .checkboxContainer'));
- assert.isTrue(element._handleNumberCheckboxClick.calledOnce);
- assert.isTrue(element._handleTargetClick.calledOnce);
- });
-
- test('_handleNumberCheckboxClick', () => {
- sandbox.spy(element, '_handleNumberCheckboxClick');
-
- MockInteractions
- .tap(element.shadowRoot
- .querySelector('.checkboxContainer input[name=number]'));
- assert.isTrue(element._handleNumberCheckboxClick.calledOnce);
- assert.isTrue(element.showNumber);
-
- MockInteractions
- .tap(element.shadowRoot
- .querySelector('.checkboxContainer input[name=number]'));
- assert.isTrue(element._handleNumberCheckboxClick.calledTwice);
- assert.isFalse(element.showNumber);
- });
-
- test('_handleTargetClick', () => {
- sandbox.spy(element, '_handleTargetClick');
- assert.include(element.displayedColumns, 'Assignee');
- MockInteractions
- .tap(element.shadowRoot
- .querySelector('.checkboxContainer input[name=Assignee]'));
- assert.isTrue(element._handleTargetClick.calledOnce);
- assert.notInclude(element.displayedColumns, 'Assignee');
- });
+ element.set('displayedColumns', columns);
+ element.showNumber = false;
+ flushAsynchronousOperations();
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('renders', () => {
+ const rows = element.shadowRoot
+ .querySelector('tbody').querySelectorAll('tr');
+ let tds;
+
+ // The `+ 1` is for the number column, which isn't included in the change
+ // table behavior's list.
+ assert.equal(rows.length, element.columnNames.length + 1);
+ for (let i = 0; i < columns.length; i++) {
+ tds = rows[i + 1].querySelectorAll('td');
+ assert.equal(tds[0].textContent, columns[i]);
+ }
+ });
+
+ test('hide item', () => {
+ const checkbox = element.shadowRoot
+ .querySelector('table tr:nth-child(2) input');
+ const isChecked = checkbox.checked;
+ const displayedLength = element.displayedColumns.length;
+ assert.isTrue(isChecked);
+
+ MockInteractions.tap(checkbox);
+ flushAsynchronousOperations();
+
+ assert.equal(element.displayedColumns.length, displayedLength - 1);
+ });
+
+ test('show item', () => {
+ element.set('displayedColumns', [
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Repo',
+ 'Branch',
+ 'Updated',
+ ]);
+ flushAsynchronousOperations();
+ const checkbox = element.shadowRoot
+ .querySelector('table tr:nth-child(2) input');
+ const isChecked = checkbox.checked;
+ const displayedLength = element.displayedColumns.length;
+ assert.isFalse(isChecked);
+ assert.equal(element.shadowRoot
+ .querySelector('table').style.display, '');
+
+ MockInteractions.tap(checkbox);
+ flushAsynchronousOperations();
+
+ assert.equal(element.displayedColumns.length,
+ displayedLength + 1);
+ });
+
+ test('_getDisplayedColumns', () => {
+ assert.deepEqual(element._getDisplayedColumns(), columns);
+ MockInteractions.tap(
+ element.shadowRoot
+ .querySelector('.checkboxContainer input[name=Assignee]'));
+ assert.deepEqual(element._getDisplayedColumns(),
+ columns.filter(c => c !== 'Assignee'));
+ });
+
+ test('_handleCheckboxContainerClick relayes taps to checkboxes', () => {
+ sandbox.stub(element, '_handleNumberCheckboxClick');
+ sandbox.stub(element, '_handleTargetClick');
+
+ MockInteractions.tap(
+ element.shadowRoot
+ .querySelector('table tr:first-of-type .checkboxContainer'));
+ assert.isTrue(element._handleNumberCheckboxClick.calledOnce);
+ assert.isFalse(element._handleTargetClick.called);
+
+ MockInteractions.tap(
+ element.shadowRoot
+ .querySelector('table tr:last-of-type .checkboxContainer'));
+ assert.isTrue(element._handleNumberCheckboxClick.calledOnce);
+ assert.isTrue(element._handleTargetClick.calledOnce);
+ });
+
+ test('_handleNumberCheckboxClick', () => {
+ sandbox.spy(element, '_handleNumberCheckboxClick');
+
+ MockInteractions
+ .tap(element.shadowRoot
+ .querySelector('.checkboxContainer input[name=number]'));
+ assert.isTrue(element._handleNumberCheckboxClick.calledOnce);
+ assert.isTrue(element.showNumber);
+
+ MockInteractions
+ .tap(element.shadowRoot
+ .querySelector('.checkboxContainer input[name=number]'));
+ assert.isTrue(element._handleNumberCheckboxClick.calledTwice);
+ assert.isFalse(element.showNumber);
+ });
+
+ test('_handleTargetClick', () => {
+ sandbox.spy(element, '_handleTargetClick');
+ assert.include(element.displayedColumns, 'Assignee');
+ MockInteractions
+ .tap(element.shadowRoot
+ .querySelector('.checkboxContainer input[name=Assignee]'));
+ assert.isTrue(element._handleTargetClick.calledOnce);
+ assert.notInclude(element.displayedColumns, 'Assignee');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html
deleted file mode 100644
index fb5d64f..0000000
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html
+++ /dev/null
@@ -1,117 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-cla-view">
- <template>
- <style include="shared-styles">
- h1 {
- margin-bottom: var(--spacing-m);
- }
- h3 {
- margin-bottom: var(--spacing-m);
- }
- .agreementsUrl {
- border: 1px solid #b0bdcc;
- margin-bottom: var(--spacing-xl);
- margin-left: var(--spacing-xl);
- margin-right: var(--spacing-xl);
- padding: var(--spacing-s);
- }
- #claNewAgreementsLabel {
- font-weight: var(--font-weight-bold);
- }
- #claNewAgreement {
- display: none;
- }
- #claNewAgreement.show {
- display: block;
- }
- .contributorAgreementButton {
- font-weight: var(--font-weight-bold);
- }
- .alreadySubmittedText {
- color: var(--error-text-color);
- margin: 0 var(--spacing-xxl);
- padding: var(--spacing-m);
- }
- .alreadySubmittedText.hide,
- .hideAgreementsTextBox {
- display: none;
- }
- main {
- margin: var(--spacing-xxl) auto;
- max-width: 50em;
- }
- </style>
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <main>
- <h1>New Contributor Agreement</h1>
- <h3>Select an agreement type:</h3>
- <template is="dom-repeat" items="[[_serverConfig.auth.contributor_agreements]]">
- <span class="contributorAgreementButton">
- <input id$="claNewAgreementsInput[[item.name]]"
- name="claNewAgreementsRadio"
- type="radio"
- data-name$="[[item.name]]"
- data-url$="[[item.url]]"
- on-click="_handleShowAgreement"
- disabled$="[[_disableAgreements(item, _groups, _signedAgreements)]]">
- <label id="claNewAgreementsLabel">[[item.name]]</label>
- </span>
- <div class$="alreadySubmittedText [[_hideAgreements(item, _groups, _signedAgreements)]]">
- Agreement already submitted.
- </div>
- <div class="agreementsUrl">
- [[item.description]]
- </div>
- </template>
- <div id="claNewAgreement" class$="[[_computeShowAgreementsClass(_showAgreements)]]">
- <h3 class="smallHeading">Review the agreement:</h3>
- <div id="agreementsUrl" class="agreementsUrl">
- <a href$="[[_agreementsUrl]]" target="blank" rel="noopener">
- Please review the agreement.</a>
- </div>
- <div class$="agreementsTextBox [[_computeHideAgreementClass(_agreementName, _serverConfig.auth.contributor_agreements)]]">
- <h3 class="smallHeading">Complete the agreement:</h3>
- <iron-input bind-value="{{_agreementsText}}"
- placeholder="Enter 'I agree' here">
- <input id="input-agreements"
- is="iron-input"
- bind-value="{{_agreementsText}}"
- placeholder="Enter 'I agree' here">
- </iron-input>
- <gr-button on-click="_handleSaveAgreements" disabled="[[_disableAgreementsText(_agreementsText)]]">
- Submit
- </gr-button>
- </div>
- </div>
- </main>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-cla-view.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
index cff1d54..373ac63 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
@@ -14,151 +14,164 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
- */
- class GrClaView extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-cla-view'; }
+import '@polymer/iron-input/iron-input.js';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-cla-view_html.js';
- static get properties() {
- return {
- _groups: Object,
- /** @type {?} */
- _serverConfig: Object,
- _agreementsText: String,
- _agreementName: String,
- _signedAgreements: Array,
- _showAgreements: {
- type: Boolean,
- value: false,
- },
- _agreementsUrl: String,
- };
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrClaView extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-cla-view'; }
+
+ static get properties() {
+ return {
+ _groups: Object,
+ /** @type {?} */
+ _serverConfig: Object,
+ _agreementsText: String,
+ _agreementName: String,
+ _signedAgreements: Array,
+ _showAgreements: {
+ type: Boolean,
+ value: false,
+ },
+ _agreementsUrl: String,
+ };
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this.loadData();
+
+ this.fire('title-change', {title: 'New Contributor Agreement'});
+ }
+
+ loadData() {
+ const promises = [];
+ promises.push(this.$.restAPI.getConfig(true).then(config => {
+ this._serverConfig = config;
+ }));
+
+ promises.push(this.$.restAPI.getAccountGroups().then(groups => {
+ this._groups = groups.sort((a, b) => a.name.localeCompare(b.name));
+ }));
+
+ promises.push(this.$.restAPI.getAccountAgreements().then(agreements => {
+ this._signedAgreements = agreements || [];
+ }));
+
+ return Promise.all(promises);
+ }
+
+ _getAgreementsUrl(configUrl) {
+ let url;
+ if (!configUrl) {
+ return '';
+ }
+ if (configUrl.startsWith('http:') || configUrl.startsWith('https:')) {
+ url = configUrl;
+ } else {
+ url = this.getBaseUrl() + '/' + configUrl;
}
- /** @override */
- attached() {
- super.attached();
+ return url;
+ }
+
+ _handleShowAgreement(e) {
+ this._agreementName = e.target.getAttribute('data-name');
+ this._agreementsUrl =
+ this._getAgreementsUrl(e.target.getAttribute('data-url'));
+ this._showAgreements = true;
+ }
+
+ _handleSaveAgreements(e) {
+ this._createToast('Agreement saving...');
+
+ const name = this._agreementName;
+ return this.$.restAPI.saveAccountAgreement({name}).then(res => {
+ let message = 'Agreement failed to be submitted, please try again';
+ if (res.status === 200) {
+ message = 'Agreement has been successfully submited.';
+ }
+ this._createToast(message);
this.loadData();
+ this._agreementsText = '';
+ this._showAgreements = false;
+ });
+ }
- this.fire('title-change', {title: 'New Contributor Agreement'});
- }
+ _createToast(message) {
+ this.dispatchEvent(new CustomEvent(
+ 'show-alert', {detail: {message}, bubbles: true, composed: true}));
+ }
- loadData() {
- const promises = [];
- promises.push(this.$.restAPI.getConfig(true).then(config => {
- this._serverConfig = config;
- }));
+ _computeShowAgreementsClass(agreements) {
+ return agreements ? 'show' : '';
+ }
- promises.push(this.$.restAPI.getAccountGroups().then(groups => {
- this._groups = groups.sort((a, b) => a.name.localeCompare(b.name));
- }));
-
- promises.push(this.$.restAPI.getAccountAgreements().then(agreements => {
- this._signedAgreements = agreements || [];
- }));
-
- return Promise.all(promises);
- }
-
- _getAgreementsUrl(configUrl) {
- let url;
- if (!configUrl) {
- return '';
+ _disableAgreements(item, groups, signedAgreements) {
+ if (!groups) return false;
+ for (const group of groups) {
+ if ((item && item.auto_verify_group &&
+ item.auto_verify_group.id === group.id) ||
+ signedAgreements.find(i => i.name === item.name)) {
+ return true;
}
- if (configUrl.startsWith('http:') || configUrl.startsWith('https:')) {
- url = configUrl;
- } else {
- url = this.getBaseUrl() + '/' + configUrl;
+ }
+ return false;
+ }
+
+ _hideAgreements(item, groups, signedAgreements) {
+ return this._disableAgreements(item, groups, signedAgreements) ?
+ '' : 'hide';
+ }
+
+ _disableAgreementsText(text) {
+ return text.toLowerCase() === 'i agree' ? false : true;
+ }
+
+ // This checks for auto_verify_group,
+ // if specified it returns 'hideAgreementsTextBox' which
+ // then hides the text box and submit button.
+ _computeHideAgreementClass(name, config) {
+ if (!config) return '';
+ for (const key in config) {
+ if (!config.hasOwnProperty(key)) {
+ continue;
}
-
- return url;
- }
-
- _handleShowAgreement(e) {
- this._agreementName = e.target.getAttribute('data-name');
- this._agreementsUrl =
- this._getAgreementsUrl(e.target.getAttribute('data-url'));
- this._showAgreements = true;
- }
-
- _handleSaveAgreements(e) {
- this._createToast('Agreement saving...');
-
- const name = this._agreementName;
- return this.$.restAPI.saveAccountAgreement({name}).then(res => {
- let message = 'Agreement failed to be submitted, please try again';
- if (res.status === 200) {
- message = 'Agreement has been successfully submited.';
- }
- this._createToast(message);
- this.loadData();
- this._agreementsText = '';
- this._showAgreements = false;
- });
- }
-
- _createToast(message) {
- this.dispatchEvent(new CustomEvent(
- 'show-alert', {detail: {message}, bubbles: true, composed: true}));
- }
-
- _computeShowAgreementsClass(agreements) {
- return agreements ? 'show' : '';
- }
-
- _disableAgreements(item, groups, signedAgreements) {
- if (!groups) return false;
- for (const group of groups) {
- if ((item && item.auto_verify_group &&
- item.auto_verify_group.id === group.id) ||
- signedAgreements.find(i => i.name === item.name)) {
- return true;
- }
- }
- return false;
- }
-
- _hideAgreements(item, groups, signedAgreements) {
- return this._disableAgreements(item, groups, signedAgreements) ?
- '' : 'hide';
- }
-
- _disableAgreementsText(text) {
- return text.toLowerCase() === 'i agree' ? false : true;
- }
-
- // This checks for auto_verify_group,
- // if specified it returns 'hideAgreementsTextBox' which
- // then hides the text box and submit button.
- _computeHideAgreementClass(name, config) {
- if (!config) return '';
- for (const key in config) {
- if (!config.hasOwnProperty(key)) {
+ for (const prop in config[key]) {
+ if (!config[key].hasOwnProperty(prop)) {
continue;
}
- for (const prop in config[key]) {
- if (!config[key].hasOwnProperty(prop)) {
- continue;
- }
- if (name === config[key].name &&
- !config[key].auto_verify_group) {
- return 'hideAgreementsTextBox';
- }
+ if (name === config[key].name &&
+ !config[key].auto_verify_group) {
+ return 'hideAgreementsTextBox';
}
}
}
}
+}
- customElements.define(GrClaView.is, GrClaView);
-})();
+customElements.define(GrClaView.is, GrClaView);
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js
new file mode 100644
index 0000000..2c2fca0
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js
@@ -0,0 +1,96 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ h1 {
+ margin-bottom: var(--spacing-m);
+ }
+ h3 {
+ margin-bottom: var(--spacing-m);
+ }
+ .agreementsUrl {
+ border: 1px solid #b0bdcc;
+ margin-bottom: var(--spacing-xl);
+ margin-left: var(--spacing-xl);
+ margin-right: var(--spacing-xl);
+ padding: var(--spacing-s);
+ }
+ #claNewAgreementsLabel {
+ font-weight: var(--font-weight-bold);
+ }
+ #claNewAgreement {
+ display: none;
+ }
+ #claNewAgreement.show {
+ display: block;
+ }
+ .contributorAgreementButton {
+ font-weight: var(--font-weight-bold);
+ }
+ .alreadySubmittedText {
+ color: var(--error-text-color);
+ margin: 0 var(--spacing-xxl);
+ padding: var(--spacing-m);
+ }
+ .alreadySubmittedText.hide,
+ .hideAgreementsTextBox {
+ display: none;
+ }
+ main {
+ margin: var(--spacing-xxl) auto;
+ max-width: 50em;
+ }
+ </style>
+ <style include="gr-form-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <main>
+ <h1>New Contributor Agreement</h1>
+ <h3>Select an agreement type:</h3>
+ <template is="dom-repeat" items="[[_serverConfig.auth.contributor_agreements]]">
+ <span class="contributorAgreementButton">
+ <input id\$="claNewAgreementsInput[[item.name]]" name="claNewAgreementsRadio" type="radio" data-name\$="[[item.name]]" data-url\$="[[item.url]]" on-click="_handleShowAgreement" disabled\$="[[_disableAgreements(item, _groups, _signedAgreements)]]">
+ <label id="claNewAgreementsLabel">[[item.name]]</label>
+ </span>
+ <div class\$="alreadySubmittedText [[_hideAgreements(item, _groups, _signedAgreements)]]">
+ Agreement already submitted.
+ </div>
+ <div class="agreementsUrl">
+ [[item.description]]
+ </div>
+ </template>
+ <div id="claNewAgreement" class\$="[[_computeShowAgreementsClass(_showAgreements)]]">
+ <h3 class="smallHeading">Review the agreement:</h3>
+ <div id="agreementsUrl" class="agreementsUrl">
+ <a href\$="[[_agreementsUrl]]" target="blank" rel="noopener">
+ Please review the agreement.</a>
+ </div>
+ <div class\$="agreementsTextBox [[_computeHideAgreementClass(_agreementName, _serverConfig.auth.contributor_agreements)]]">
+ <h3 class="smallHeading">Complete the agreement:</h3>
+ <iron-input bind-value="{{_agreementsText}}" placeholder="Enter 'I agree' here">
+ <input id="input-agreements" is="iron-input" bind-value="{{_agreementsText}}" placeholder="Enter 'I agree' here">
+ </iron-input>
+ <gr-button on-click="_handleSaveAgreements" disabled="[[_disableAgreementsText(_agreementsText)]]">
+ Submit
+ </gr-button>
+ </div>
+ </div>
+ </main>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html
index d40d36d..a1874db 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-cla-view</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-cla-view.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,162 +30,164 @@
</template>
</test-fixture>
-<script>
- suite('gr-cla-view tests', async () => {
- await readyToTest();
- let element;
- const signedAgreements = [{
- name: 'CLA',
- description: 'Contributor License Agreement',
- url: 'static/cla.html',
- }];
- const auth = {
- name: 'Individual',
- description: 'test-description',
- url: 'static/cla_individual.html',
- auto_verify_group: {
- url: '#/admin/groups/uuid-e9aaddc47f305be7661ad4db9b66f9b707bd19a0',
- options: {
- visible_to_all: true,
- },
- group_id: 20,
- owner: 'CLA Accepted - Individual',
- owner_id: 'e9aaddc47f305be7661ad4db9b66f9b707bd19a0',
- created_on: '2017-07-31 15:11:04.000000000',
- id: 'e9aaddc47f305be7661ad4db9b66f9b707bd19a0',
- name: 'CLA Accepted - Individual',
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-cla-view.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-cla-view tests', () => {
+ let element;
+ const signedAgreements = [{
+ name: 'CLA',
+ description: 'Contributor License Agreement',
+ url: 'static/cla.html',
+ }];
+ const auth = {
+ name: 'Individual',
+ description: 'test-description',
+ url: 'static/cla_individual.html',
+ auto_verify_group: {
+ url: '#/admin/groups/uuid-e9aaddc47f305be7661ad4db9b66f9b707bd19a0',
+ options: {
+ visible_to_all: true,
},
- };
-
- const auth2 = {
- name: 'Individual2',
- description: 'test-description2',
- url: 'static/cla_individual2.html',
- auto_verify_group: {
- url: '#/admin/groups/uuid-bc53f2738ef8ad0b3a4f53846ff59b05822caecb',
- options: {},
- group_id: 21,
- owner: 'CLA Accepted - Individual2',
- owner_id: 'bc53f2738ef8ad0b3a4f53846ff59b05822caecb',
- created_on: '2017-07-31 15:25:42.000000000',
- id: 'bc53f2738ef8ad0b3a4f53846ff59b05822caecb',
- name: 'CLA Accepted - Individual2',
- },
- };
-
- const auth3 = {
- name: 'CLA',
- description: 'Contributor License Agreement',
- url: 'static/cla_individual.html',
- };
-
- const config = {
- auth: {
- use_contributor_agreements: true,
- contributor_agreements: [
- {
- name: 'Individual',
- description: 'test-description',
- url: 'static/cla_individual.html',
- },
- {
- name: 'CLA',
- description: 'Contributor License Agreement',
- url: 'static/cla.html',
- }],
- },
- };
- const config2 = {
- auth: {
- use_contributor_agreements: true,
- contributor_agreements: [
- {
- name: 'Individual2',
- description: 'test-description2',
- url: 'static/cla_individual2.html',
- },
- ],
- },
- };
- const groups = [{
- options: {visible_to_all: true},
+ group_id: 20,
+ owner: 'CLA Accepted - Individual',
+ owner_id: 'e9aaddc47f305be7661ad4db9b66f9b707bd19a0',
+ created_on: '2017-07-31 15:11:04.000000000',
id: 'e9aaddc47f305be7661ad4db9b66f9b707bd19a0',
- group_id: 3,
name: 'CLA Accepted - Individual',
},
- ];
+ };
- setup(done => {
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve(config); },
- getAccountGroups() { return Promise.resolve(groups); },
- getAccountAgreements() { return Promise.resolve(signedAgreements); },
- });
- element = fixture('basic');
- element.loadData().then(() => { flush(done); });
- });
+ const auth2 = {
+ name: 'Individual2',
+ description: 'test-description2',
+ url: 'static/cla_individual2.html',
+ auto_verify_group: {
+ url: '#/admin/groups/uuid-bc53f2738ef8ad0b3a4f53846ff59b05822caecb',
+ options: {},
+ group_id: 21,
+ owner: 'CLA Accepted - Individual2',
+ owner_id: 'bc53f2738ef8ad0b3a4f53846ff59b05822caecb',
+ created_on: '2017-07-31 15:25:42.000000000',
+ id: 'bc53f2738ef8ad0b3a4f53846ff59b05822caecb',
+ name: 'CLA Accepted - Individual2',
+ },
+ };
- test('renders as expected with signed agreement', () => {
- const agreementSections = Polymer.dom(element.root)
- .querySelectorAll('.contributorAgreementButton');
- const agreementSubmittedTexts = Polymer.dom(element.root)
- .querySelectorAll('.alreadySubmittedText');
- assert.equal(agreementSections.length, 2);
- assert.isFalse(agreementSections[0].querySelector('input').disabled);
- assert.equal(getComputedStyle(agreementSubmittedTexts[0]).display,
- 'none');
- assert.isTrue(agreementSections[1].querySelector('input').disabled);
- assert.notEqual(getComputedStyle(agreementSubmittedTexts[1]).display,
- 'none');
- });
+ const auth3 = {
+ name: 'CLA',
+ description: 'Contributor License Agreement',
+ url: 'static/cla_individual.html',
+ };
- test('_disableAgreements', () => {
- // In the auto verify group and have not yet signed agreement
- assert.isTrue(
- element._disableAgreements(auth, groups, signedAgreements));
- // Not in the auto verify group and have not yet signed agreement
- assert.isFalse(
- element._disableAgreements(auth2, groups, signedAgreements));
- // Not in the auto verify group, have signed agreement
- assert.isTrue(
- element._disableAgreements(auth3, groups, signedAgreements));
- // Make sure the undefined check works
- assert.isFalse(
- element._disableAgreements(auth, undefined, signedAgreements));
- });
+ const config = {
+ auth: {
+ use_contributor_agreements: true,
+ contributor_agreements: [
+ {
+ name: 'Individual',
+ description: 'test-description',
+ url: 'static/cla_individual.html',
+ },
+ {
+ name: 'CLA',
+ description: 'Contributor License Agreement',
+ url: 'static/cla.html',
+ }],
+ },
+ };
+ const config2 = {
+ auth: {
+ use_contributor_agreements: true,
+ contributor_agreements: [
+ {
+ name: 'Individual2',
+ description: 'test-description2',
+ url: 'static/cla_individual2.html',
+ },
+ ],
+ },
+ };
+ const groups = [{
+ options: {visible_to_all: true},
+ id: 'e9aaddc47f305be7661ad4db9b66f9b707bd19a0',
+ group_id: 3,
+ name: 'CLA Accepted - Individual',
+ },
+ ];
- test('_hideAgreements', () => {
- // Not in the auto verify group and have not yet signed agreement
- assert.equal(
- element._hideAgreements(auth, groups, signedAgreements), '');
- // In the auto verify group
- assert.equal(
- element._hideAgreements(auth2, groups, signedAgreements), 'hide');
- // Not in the auto verify group, have signed agreement
- assert.equal(
- element._hideAgreements(auth3, groups, signedAgreements), '');
+ setup(done => {
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve(config); },
+ getAccountGroups() { return Promise.resolve(groups); },
+ getAccountAgreements() { return Promise.resolve(signedAgreements); },
});
-
- test('_disableAgreementsText', () => {
- assert.isFalse(element._disableAgreementsText('I AGREE'));
- assert.isTrue(element._disableAgreementsText('I DO NOT AGREE'));
- });
-
- test('_computeHideAgreementClass', () => {
- assert.equal(
- element._computeHideAgreementClass(
- auth.name, config.auth.contributor_agreements),
- 'hideAgreementsTextBox');
- assert.isUndefined(
- element._computeHideAgreementClass(
- auth.name, config2.auth.contributor_agreements));
- });
-
- test('_getAgreementsUrl', () => {
- assert.equal(element._getAgreementsUrl(
- 'http://test.org/test.html'), 'http://test.org/test.html');
- assert.equal(element._getAgreementsUrl(
- 'test_cla.html'), '/test_cla.html');
- });
+ element = fixture('basic');
+ element.loadData().then(() => { flush(done); });
});
+
+ test('renders as expected with signed agreement', () => {
+ const agreementSections = dom(element.root)
+ .querySelectorAll('.contributorAgreementButton');
+ const agreementSubmittedTexts = dom(element.root)
+ .querySelectorAll('.alreadySubmittedText');
+ assert.equal(agreementSections.length, 2);
+ assert.isFalse(agreementSections[0].querySelector('input').disabled);
+ assert.equal(getComputedStyle(agreementSubmittedTexts[0]).display,
+ 'none');
+ assert.isTrue(agreementSections[1].querySelector('input').disabled);
+ assert.notEqual(getComputedStyle(agreementSubmittedTexts[1]).display,
+ 'none');
+ });
+
+ test('_disableAgreements', () => {
+ // In the auto verify group and have not yet signed agreement
+ assert.isTrue(
+ element._disableAgreements(auth, groups, signedAgreements));
+ // Not in the auto verify group and have not yet signed agreement
+ assert.isFalse(
+ element._disableAgreements(auth2, groups, signedAgreements));
+ // Not in the auto verify group, have signed agreement
+ assert.isTrue(
+ element._disableAgreements(auth3, groups, signedAgreements));
+ // Make sure the undefined check works
+ assert.isFalse(
+ element._disableAgreements(auth, undefined, signedAgreements));
+ });
+
+ test('_hideAgreements', () => {
+ // Not in the auto verify group and have not yet signed agreement
+ assert.equal(
+ element._hideAgreements(auth, groups, signedAgreements), '');
+ // In the auto verify group
+ assert.equal(
+ element._hideAgreements(auth2, groups, signedAgreements), 'hide');
+ // Not in the auto verify group, have signed agreement
+ assert.equal(
+ element._hideAgreements(auth3, groups, signedAgreements), '');
+ });
+
+ test('_disableAgreementsText', () => {
+ assert.isFalse(element._disableAgreementsText('I AGREE'));
+ assert.isTrue(element._disableAgreementsText('I DO NOT AGREE'));
+ });
+
+ test('_computeHideAgreementClass', () => {
+ assert.equal(
+ element._computeHideAgreementClass(
+ auth.name, config.auth.contributor_agreements),
+ 'hideAgreementsTextBox');
+ assert.isUndefined(
+ element._computeHideAgreementClass(
+ auth.name, config2.auth.contributor_agreements));
+ });
+
+ test('_getAgreementsUrl', () => {
+ assert.equal(element._getAgreementsUrl(
+ 'http://test.org/test.html'), 'http://test.org/test.html');
+ assert.equal(element._getAgreementsUrl(
+ 'test_cla.html'), '/test_cla.html');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
deleted file mode 100644
index 80440c7..0000000
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
+++ /dev/null
@@ -1,161 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-
-<dom-module id="gr-edit-preferences">
- <template>
- <style include="shared-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>
- <div id="editPreferences" class="gr-form-styles">
- <section>
- <span class="title">Tab width</span>
- <span class="value">
- <iron-input
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{editPrefs.tab_size}}"
- on-keypress="_handleEditPrefsChanged"
- on-change="_handleEditPrefsChanged">
- <input
- is="iron-input"
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{editPrefs.tab_size}}"
- on-keypress="_handleEditPrefsChanged"
- on-change="_handleEditPrefsChanged">
- </iron-input>
- </span>
- </section>
- <section>
- <span class="title">Columns</span>
- <span class="value">
- <iron-input
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{editPrefs.line_length}}"
- on-keypress="_handleEditPrefsChanged"
- on-change="_handleEditPrefsChanged">
- <input
- is="iron-input"
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{editPrefs.line_length}}"
- on-keypress="_handleEditPrefsChanged"
- on-change="_handleEditPrefsChanged">
- </iron-input>
- </span>
- </section>
- <section>
- <span class="title">Indent unit</span>
- <span class="value">
- <iron-input
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{editPrefs.indent_unit}}"
- on-keypress="_handleEditPrefsChanged"
- on-change="_handleEditPrefsChanged">
- <input
- is="iron-input"
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{editPrefs.indent_unit}}"
- on-keypress="_handleEditPrefsChanged"
- on-change="_handleEditPrefsChanged">
- </iron-input>
- </span>
- </section>
- <section>
- <span class="title">Syntax highlighting</span>
- <span class="value">
- <input
- id="editSyntaxHighlighting"
- type="checkbox"
- checked$="[[editPrefs.syntax_highlighting]]"
- on-change="_handleEditSyntaxHighlightingChanged">
- </span>
- </section>
- <section>
- <span class="title">Show tabs</span>
- <span class="value">
- <input
- id="editShowTabs"
- type="checkbox"
- checked$="[[editPrefs.show_tabs]]"
- on-change="_handleEditShowTabsChanged">
- </span>
- </section>
- <section>
- <span class="title">Match brackets</span>
- <span class="value">
- <input
- id="showMatchBrackets"
- type="checkbox"
- checked$="[[editPrefs.match_brackets]]"
- on-change="_handleMatchBracketsChanged">
- </span>
- </section>
- <section>
- <span class="title">Line wrapping</span>
- <span class="value">
- <input
- id="editShowLineWrapping"
- type="checkbox"
- checked$="[[editPrefs.line_wrapping]]"
- on-change="_handleEditLineWrappingChanged">
- </span>
- </section>
- <section>
- <span class="title">Indent with tabs</span>
- <span class="value">
- <input
- id="showIndentWithTabs"
- type="checkbox"
- checked$="[[editPrefs.indent_with_tabs]]"
- on-change="_handleIndentWithTabsChanged">
- </span>
- </section>
- <section>
- <span class="title">Auto close brackets</span>
- <span class="value">
- <input
- id="showAutoCloseBrackets"
- type="checkbox"
- checked$="[[editPrefs.auto_close_brackets]]"
- on-change="_handleAutoCloseBracketsChanged">
- </span>
- </section>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-edit-preferences.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
index 9523136..2a7ac06 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
@@ -14,76 +14,86 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrEditPreferences extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-edit-preferences'; }
+import '@polymer/iron-input/iron-input.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-edit-preferences_html.js';
- static get properties() {
- return {
- hasUnsavedChanges: {
- type: Boolean,
- notify: true,
- value: false,
- },
+/** @extends Polymer.Element */
+class GrEditPreferences extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /** @type {?} */
- editPrefs: Object,
- };
- }
+ static get is() { return 'gr-edit-preferences'; }
- loadData() {
- return this.$.restAPI.getEditPreferences().then(prefs => {
- this.editPrefs = prefs;
- });
- }
+ static get properties() {
+ return {
+ hasUnsavedChanges: {
+ type: Boolean,
+ notify: true,
+ value: false,
+ },
- _handleEditPrefsChanged() {
- this.hasUnsavedChanges = true;
- }
-
- _handleEditSyntaxHighlightingChanged() {
- this.set('editPrefs.syntax_highlighting',
- this.$.editSyntaxHighlighting.checked);
- this._handleEditPrefsChanged();
- }
-
- _handleEditShowTabsChanged() {
- this.set('editPrefs.show_tabs', this.$.editShowTabs.checked);
- this._handleEditPrefsChanged();
- }
-
- _handleMatchBracketsChanged() {
- this.set('editPrefs.match_brackets', this.$.showMatchBrackets.checked);
- this._handleEditPrefsChanged();
- }
-
- _handleEditLineWrappingChanged() {
- this.set('editPrefs.line_wrapping', this.$.editShowLineWrapping.checked);
- this._handleEditPrefsChanged();
- }
-
- _handleIndentWithTabsChanged() {
- this.set('editPrefs.indent_with_tabs', this.$.showIndentWithTabs.checked);
- this._handleEditPrefsChanged();
- }
-
- _handleAutoCloseBracketsChanged() {
- this.set('editPrefs.auto_close_brackets',
- this.$.showAutoCloseBrackets.checked);
- this._handleEditPrefsChanged();
- }
-
- save() {
- return this.$.restAPI.saveEditPreferences(this.editPrefs).then(res => {
- this.hasUnsavedChanges = false;
- });
- }
+ /** @type {?} */
+ editPrefs: Object,
+ };
}
- customElements.define(GrEditPreferences.is, GrEditPreferences);
-})();
+ loadData() {
+ return this.$.restAPI.getEditPreferences().then(prefs => {
+ this.editPrefs = prefs;
+ });
+ }
+
+ _handleEditPrefsChanged() {
+ this.hasUnsavedChanges = true;
+ }
+
+ _handleEditSyntaxHighlightingChanged() {
+ this.set('editPrefs.syntax_highlighting',
+ this.$.editSyntaxHighlighting.checked);
+ this._handleEditPrefsChanged();
+ }
+
+ _handleEditShowTabsChanged() {
+ this.set('editPrefs.show_tabs', this.$.editShowTabs.checked);
+ this._handleEditPrefsChanged();
+ }
+
+ _handleMatchBracketsChanged() {
+ this.set('editPrefs.match_brackets', this.$.showMatchBrackets.checked);
+ this._handleEditPrefsChanged();
+ }
+
+ _handleEditLineWrappingChanged() {
+ this.set('editPrefs.line_wrapping', this.$.editShowLineWrapping.checked);
+ this._handleEditPrefsChanged();
+ }
+
+ _handleIndentWithTabsChanged() {
+ this.set('editPrefs.indent_with_tabs', this.$.showIndentWithTabs.checked);
+ this._handleEditPrefsChanged();
+ }
+
+ _handleAutoCloseBracketsChanged() {
+ this.set('editPrefs.auto_close_brackets',
+ this.$.showAutoCloseBrackets.checked);
+ this._handleEditPrefsChanged();
+ }
+
+ save() {
+ return this.$.restAPI.saveEditPreferences(this.editPrefs).then(res => {
+ this.hasUnsavedChanges = false;
+ });
+ }
+}
+
+customElements.define(GrEditPreferences.is, GrEditPreferences);
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.js
new file mode 100644
index 0000000..de22dbb
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.js
@@ -0,0 +1,89 @@
+/**
+ * @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.js';
+
+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">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <div id="editPreferences" class="gr-form-styles">
+ <section>
+ <span class="title">Tab width</span>
+ <span class="value">
+ <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.tab_size}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged">
+ <input is="iron-input" type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.tab_size}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged">
+ </iron-input>
+ </span>
+ </section>
+ <section>
+ <span class="title">Columns</span>
+ <span class="value">
+ <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.line_length}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged">
+ <input is="iron-input" type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.line_length}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged">
+ </iron-input>
+ </span>
+ </section>
+ <section>
+ <span class="title">Indent unit</span>
+ <span class="value">
+ <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.indent_unit}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged">
+ <input is="iron-input" type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.indent_unit}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged">
+ </iron-input>
+ </span>
+ </section>
+ <section>
+ <span class="title">Syntax highlighting</span>
+ <span class="value">
+ <input id="editSyntaxHighlighting" type="checkbox" checked\$="[[editPrefs.syntax_highlighting]]" on-change="_handleEditSyntaxHighlightingChanged">
+ </span>
+ </section>
+ <section>
+ <span class="title">Show tabs</span>
+ <span class="value">
+ <input id="editShowTabs" type="checkbox" checked\$="[[editPrefs.show_tabs]]" on-change="_handleEditShowTabsChanged">
+ </span>
+ </section>
+ <section>
+ <span class="title">Match brackets</span>
+ <span class="value">
+ <input id="showMatchBrackets" type="checkbox" checked\$="[[editPrefs.match_brackets]]" on-change="_handleMatchBracketsChanged">
+ </span>
+ </section>
+ <section>
+ <span class="title">Line wrapping</span>
+ <span class="value">
+ <input id="editShowLineWrapping" type="checkbox" checked\$="[[editPrefs.line_wrapping]]" on-change="_handleEditLineWrappingChanged">
+ </span>
+ </section>
+ <section>
+ <span class="title">Indent with tabs</span>
+ <span class="value">
+ <input id="showIndentWithTabs" type="checkbox" checked\$="[[editPrefs.indent_with_tabs]]" on-change="_handleIndentWithTabsChanged">
+ </span>
+ </section>
+ <section>
+ <span class="title">Auto close brackets</span>
+ <span class="value">
+ <input id="showAutoCloseBrackets" type="checkbox" checked\$="[[editPrefs.auto_close_brackets]]" on-change="_handleAutoCloseBracketsChanged">
+ </span>
+ </section>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
index 3c99977..47e295c 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-edit-preferences</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-edit-preferences.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,95 +30,96 @@
</template>
</test-fixture>
-<script>
- suite('gr-edit-preferences tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let editPreferences;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-edit-preferences.js';
+suite('gr-edit-preferences tests', () => {
+ let element;
+ let sandbox;
+ let editPreferences;
- function valueOf(title, fieldsetid) {
- const sections = element.$[fieldsetid].querySelectorAll('section');
- let titleEl;
- for (let i = 0; i < sections.length; i++) {
- titleEl = sections[i].querySelector('.title');
- if (titleEl.textContent.trim() === title) {
- return sections[i].querySelector('.value');
- }
+ function valueOf(title, fieldsetid) {
+ const sections = element.$[fieldsetid].querySelectorAll('section');
+ let titleEl;
+ for (let i = 0; i < sections.length; i++) {
+ titleEl = sections[i].querySelector('.title');
+ if (titleEl.textContent.trim() === title) {
+ return sections[i].querySelector('.value');
}
}
+ }
- setup(() => {
- editPreferences = {
- auto_close_brackets: false,
- cursor_blink_rate: 0,
- hide_line_numbers: false,
- hide_top_menu: false,
- indent_unit: 2,
- indent_with_tabs: false,
- key_map_type: 'DEFAULT',
- line_length: 100,
- line_wrapping: false,
- match_brackets: true,
- show_base: false,
- show_tabs: true,
- show_whitespace_errors: true,
- syntax_highlighting: true,
- tab_size: 8,
- theme: 'DEFAULT',
- };
+ setup(() => {
+ editPreferences = {
+ auto_close_brackets: false,
+ cursor_blink_rate: 0,
+ hide_line_numbers: false,
+ hide_top_menu: false,
+ indent_unit: 2,
+ indent_with_tabs: false,
+ key_map_type: 'DEFAULT',
+ line_length: 100,
+ line_wrapping: false,
+ match_brackets: true,
+ show_base: false,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ tab_size: 8,
+ theme: 'DEFAULT',
+ };
- stub('gr-rest-api-interface', {
- getEditPreferences() {
- return Promise.resolve(editPreferences);
- },
- });
-
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- return element.loadData();
+ stub('gr-rest-api-interface', {
+ getEditPreferences() {
+ return Promise.resolve(editPreferences);
+ },
});
- teardown(() => { sandbox.restore(); });
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ return element.loadData();
+ });
- test('renders', () => {
- // Rendered with the expected preferences selected.
- assert.equal(valueOf('Tab width', 'editPreferences')
- .firstElementChild.bindValue, editPreferences.tab_size);
- assert.equal(valueOf('Columns', 'editPreferences')
- .firstElementChild.bindValue, editPreferences.line_length);
- assert.equal(valueOf('Indent unit', 'editPreferences')
- .firstElementChild.bindValue, editPreferences.indent_unit);
- assert.equal(valueOf('Syntax highlighting', 'editPreferences')
- .firstElementChild.checked, editPreferences.syntax_highlighting);
- assert.equal(valueOf('Show tabs', 'editPreferences')
- .firstElementChild.checked, editPreferences.show_tabs);
- assert.equal(valueOf('Match brackets', 'editPreferences')
- .firstElementChild.checked, editPreferences.match_brackets);
- assert.equal(valueOf('Line wrapping', 'editPreferences')
- .firstElementChild.checked, editPreferences.line_wrapping);
- assert.equal(valueOf('Indent with tabs', 'editPreferences')
- .firstElementChild.checked, editPreferences.indent_with_tabs);
- assert.equal(valueOf('Auto close brackets', 'editPreferences')
- .firstElementChild.checked, editPreferences.auto_close_brackets);
+ teardown(() => { sandbox.restore(); });
+ test('renders', () => {
+ // Rendered with the expected preferences selected.
+ assert.equal(valueOf('Tab width', 'editPreferences')
+ .firstElementChild.bindValue, editPreferences.tab_size);
+ assert.equal(valueOf('Columns', 'editPreferences')
+ .firstElementChild.bindValue, editPreferences.line_length);
+ assert.equal(valueOf('Indent unit', 'editPreferences')
+ .firstElementChild.bindValue, editPreferences.indent_unit);
+ assert.equal(valueOf('Syntax highlighting', 'editPreferences')
+ .firstElementChild.checked, editPreferences.syntax_highlighting);
+ assert.equal(valueOf('Show tabs', 'editPreferences')
+ .firstElementChild.checked, editPreferences.show_tabs);
+ assert.equal(valueOf('Match brackets', 'editPreferences')
+ .firstElementChild.checked, editPreferences.match_brackets);
+ assert.equal(valueOf('Line wrapping', 'editPreferences')
+ .firstElementChild.checked, editPreferences.line_wrapping);
+ assert.equal(valueOf('Indent with tabs', 'editPreferences')
+ .firstElementChild.checked, editPreferences.indent_with_tabs);
+ assert.equal(valueOf('Auto close brackets', 'editPreferences')
+ .firstElementChild.checked, editPreferences.auto_close_brackets);
+
+ assert.isFalse(element.hasUnsavedChanges);
+ });
+
+ test('save changes', () => {
+ sandbox.stub(element.$.restAPI, 'saveEditPreferences')
+ .returns(Promise.resolve());
+ const showTabsCheckbox = valueOf('Show tabs', 'editPreferences')
+ .firstElementChild;
+ showTabsCheckbox.checked = false;
+ element._handleEditShowTabsChanged();
+
+ assert.isTrue(element.hasUnsavedChanges);
+
+ // Save the change.
+ return element.save().then(() => {
assert.isFalse(element.hasUnsavedChanges);
});
-
- test('save changes', () => {
- sandbox.stub(element.$.restAPI, 'saveEditPreferences')
- .returns(Promise.resolve());
- const showTabsCheckbox = valueOf('Show tabs', 'editPreferences')
- .firstElementChild;
- showTabsCheckbox.checked = false;
- element._handleEditShowTabsChanged();
-
- assert.isTrue(element.hasUnsavedChanges);
-
- // Save the change.
- return element.save().then(() => {
- assert.isFalse(element.hasUnsavedChanges);
- });
- });
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
deleted file mode 100644
index 041b2a7..0000000
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
+++ /dev/null
@@ -1,100 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-email-editor">
- <template>
- <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
- is="iron-input"
- class="preferredRadio"
- type="radio"
- on-change="_handlePreferredChange"
- name="preferred"
- value="[[item.email]]"
- checked$="[[item.preferred]]">
- </iron-input>
- </td>
- <td>
- <gr-button
- data-index$="[[index]]"
- on-click="_handleDeleteButton"
- disabled="[[item.preferred]]"
- class="remove-button">Delete</gr-button>
- </td>
- </tr>
- </template>
- </tbody>
- </table>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-email-editor.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
index c60568c..fc97079 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
@@ -14,89 +14,99 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrEmailEditor extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-email-editor'; }
+import '@polymer/iron-input/iron-input.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-email-editor_html.js';
- static get properties() {
- return {
- hasUnsavedChanges: {
- type: Boolean,
- notify: true,
- value: false,
- },
+/** @extends Polymer.Element */
+class GrEmailEditor extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _emails: Array,
- _emailsToRemove: {
- type: Array,
- value() { return []; },
- },
- /** @type {?string} */
- _newPreferred: {
- type: String,
- value: null,
- },
- };
+ static get is() { return 'gr-email-editor'; }
+
+ static get properties() {
+ return {
+ hasUnsavedChanges: {
+ type: Boolean,
+ notify: true,
+ value: false,
+ },
+
+ _emails: Array,
+ _emailsToRemove: {
+ type: Array,
+ value() { return []; },
+ },
+ /** @type {?string} */
+ _newPreferred: {
+ type: String,
+ value: null,
+ },
+ };
+ }
+
+ loadData() {
+ return this.$.restAPI.getAccountEmails().then(emails => {
+ this._emails = emails;
+ });
+ }
+
+ save() {
+ const promises = [];
+
+ for (const emailObj of this._emailsToRemove) {
+ promises.push(this.$.restAPI.deleteAccountEmail(emailObj.email));
}
- loadData() {
- return this.$.restAPI.getAccountEmails().then(emails => {
- this._emails = emails;
- });
+ if (this._newPreferred) {
+ promises.push(this.$.restAPI.setPreferredAccountEmail(
+ this._newPreferred));
}
- save() {
- const promises = [];
+ return Promise.all(promises).then(() => {
+ this._emailsToRemove = [];
+ this._newPreferred = null;
+ this.hasUnsavedChanges = false;
+ });
+ }
- for (const emailObj of this._emailsToRemove) {
- promises.push(this.$.restAPI.deleteAccountEmail(emailObj.email));
- }
+ _handleDeleteButton(e) {
+ const index = parseInt(dom(e).localTarget
+ .getAttribute('data-index'), 10);
+ const email = this._emails[index];
+ this.push('_emailsToRemove', email);
+ this.splice('_emails', index, 1);
+ this.hasUnsavedChanges = true;
+ }
- if (this._newPreferred) {
- promises.push(this.$.restAPI.setPreferredAccountEmail(
- this._newPreferred));
- }
-
- return Promise.all(promises).then(() => {
- this._emailsToRemove = [];
- this._newPreferred = null;
- this.hasUnsavedChanges = false;
- });
- }
-
- _handleDeleteButton(e) {
- const index = parseInt(Polymer.dom(e).localTarget
- .getAttribute('data-index'), 10);
- const email = this._emails[index];
- this.push('_emailsToRemove', email);
- this.splice('_emails', index, 1);
- this.hasUnsavedChanges = true;
- }
-
- _handlePreferredControlClick(e) {
- if (e.target.classList.contains('preferredControl')) {
- e.target.firstElementChild.click();
- }
- }
-
- _handlePreferredChange(e) {
- 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);
- }
- }
+ _handlePreferredControlClick(e) {
+ if (e.target.classList.contains('preferredControl')) {
+ e.target.firstElementChild.click();
}
}
- customElements.define(GrEmailEditor.is, GrEmailEditor);
-})();
+ _handlePreferredChange(e) {
+ 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);
+ }
+ }
+ }
+}
+
+customElements.define(GrEmailEditor.is, GrEmailEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.js b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.js
new file mode 100644
index 0000000..b02df3c
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.js
@@ -0,0 +1,75 @@
+/**
+ * @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.js';
+
+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 is="iron-input" class="preferredRadio" type="radio" on-change="_handlePreferredChange" name="preferred" value="[[item.email]]" checked\$="[[item.preferred]]">
+ </iron-input>
+ </td>
+ <td>
+ <gr-button data-index\$="[[index]]" on-click="_handleDeleteButton" disabled="[[item.preferred]]" class="remove-button">Delete</gr-button>
+ </td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
index ecb108d..dbdd2d6 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-email-editor</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-email-editor.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,121 +30,122 @@
</template>
</test-fixture>
-<script>
- suite('gr-email-editor tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-email-editor.js';
+suite('gr-email-editor tests', () => {
+ let element;
- setup(done => {
- const emails = [
- {email: 'email@one.com'},
- {email: 'email@two.com', preferred: true},
- {email: 'email@three.com'},
- ];
+ setup(done => {
+ const emails = [
+ {email: 'email@one.com'},
+ {email: 'email@two.com', preferred: true},
+ {email: 'email@three.com'},
+ ];
- stub('gr-rest-api-interface', {
- getAccountEmails() { return Promise.resolve(emails); },
- });
-
- element = fixture('basic');
-
- element.loadData().then(flush(done));
+ stub('gr-rest-api-interface', {
+ getAccountEmails() { return Promise.resolve(emails); },
});
- test('renders', () => {
- const rows = element.shadowRoot
- .querySelector('table').querySelectorAll('tbody tr');
+ element = fixture('basic');
- assert.equal(rows.length, 3);
+ element.loadData().then(flush(done));
+ });
- assert.isFalse(rows[0].querySelector('input[type=radio]').checked);
- assert.isNotOk(rows[0].querySelector('gr-button').disabled);
+ test('renders', () => {
+ const rows = element.shadowRoot
+ .querySelector('table').querySelectorAll('tbody tr');
- assert.isTrue(rows[1].querySelector('input[type=radio]').checked);
- assert.isOk(rows[1].querySelector('gr-button').disabled);
+ assert.equal(rows.length, 3);
- assert.isFalse(rows[2].querySelector('input[type=radio]').checked);
- assert.isNotOk(rows[2].querySelector('gr-button').disabled);
+ assert.isFalse(rows[0].querySelector('input[type=radio]').checked);
+ assert.isNotOk(rows[0].querySelector('gr-button').disabled);
- assert.isFalse(element.hasUnsavedChanges);
- });
+ assert.isTrue(rows[1].querySelector('input[type=radio]').checked);
+ assert.isOk(rows[1].querySelector('gr-button').disabled);
- test('edit preferred', () => {
- const preferredChangedSpy = sinon.spy(element, '_handlePreferredChange');
- const radios = element.shadowRoot
- .querySelector('table').querySelectorAll('input[type=radio]');
+ assert.isFalse(rows[2].querySelector('input[type=radio]').checked);
+ assert.isNotOk(rows[2].querySelector('gr-button').disabled);
- assert.isFalse(element.hasUnsavedChanges);
- 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.isFalse(element.hasUnsavedChanges);
+ });
- radios[0].click();
+ test('edit preferred', () => {
+ const preferredChangedSpy = sinon.spy(element, '_handlePreferredChange');
+ const radios = element.shadowRoot
+ .querySelector('table').querySelectorAll('input[type=radio]');
- assert.isTrue(element.hasUnsavedChanges);
- 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.isFalse(element.hasUnsavedChanges);
+ 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);
- test('delete email', () => {
- const buttons = element.shadowRoot
- .querySelector('table').querySelectorAll('gr-button');
+ radios[0].click();
- assert.isFalse(element.hasUnsavedChanges);
- assert.isNotOk(element._newPreferred);
- assert.equal(element._emailsToRemove.length, 0);
- assert.equal(element._emails.length, 3);
+ assert.isTrue(element.hasUnsavedChanges);
+ 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);
+ });
- buttons[2].click();
+ test('delete email', () => {
+ const buttons = element.shadowRoot
+ .querySelector('table').querySelectorAll('gr-button');
- assert.isTrue(element.hasUnsavedChanges);
- assert.isNotOk(element._newPreferred);
- assert.equal(element._emailsToRemove.length, 1);
- assert.equal(element._emails.length, 2);
+ assert.isFalse(element.hasUnsavedChanges);
+ assert.isNotOk(element._newPreferred);
+ assert.equal(element._emailsToRemove.length, 0);
+ assert.equal(element._emails.length, 3);
- assert.equal(element._emailsToRemove[0].email, 'email@three.com');
- });
+ buttons[2].click();
- test('save changes', done => {
- const deleteEmailStub =
- sinon.stub(element.$.restAPI, 'deleteAccountEmail');
- const setPreferredStub = sinon.stub(element.$.restAPI,
- 'setPreferredAccountEmail');
- const rows = element.shadowRoot
- .querySelector('table').querySelectorAll('tbody tr');
+ assert.isTrue(element.hasUnsavedChanges);
+ assert.isNotOk(element._newPreferred);
+ assert.equal(element._emailsToRemove.length, 1);
+ assert.equal(element._emails.length, 2);
- assert.isFalse(element.hasUnsavedChanges);
- assert.isNotOk(element._newPreferred);
- assert.equal(element._emailsToRemove.length, 0);
- assert.equal(element._emails.length, 3);
+ assert.equal(element._emailsToRemove[0].email, 'email@three.com');
+ });
- // Delete the first email and set the last as preferred.
- rows[0].querySelector('gr-button').click();
- rows[2].querySelector('input[type=radio]').click();
+ test('save changes', done => {
+ const deleteEmailStub =
+ sinon.stub(element.$.restAPI, 'deleteAccountEmail');
+ const setPreferredStub = sinon.stub(element.$.restAPI,
+ 'setPreferredAccountEmail');
+ const rows = element.shadowRoot
+ .querySelector('table').querySelectorAll('tbody tr');
- 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.isFalse(element.hasUnsavedChanges);
+ assert.isNotOk(element._newPreferred);
+ assert.equal(element._emailsToRemove.length, 0);
+ assert.equal(element._emails.length, 3);
- // Save the changes.
- element.save().then(() => {
- assert.equal(deleteEmailStub.callCount, 1);
- assert.equal(deleteEmailStub.getCall(0).args[0], 'email@one.com');
+ // Delete the first email and set the last as preferred.
+ rows[0].querySelector('gr-button').click();
+ rows[2].querySelector('input[type=radio]').click();
- assert.isTrue(setPreferredStub.called);
- assert.equal(setPreferredStub.getCall(0).args[0], 'email@three.com');
+ 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);
- done();
- });
+ // Save the changes.
+ element.save().then(() => {
+ assert.equal(deleteEmailStub.callCount, 1);
+ assert.equal(deleteEmailStub.getCall(0).args[0], 'email@one.com');
+
+ assert.isTrue(setPreferredStub.called);
+ assert.equal(setPreferredStub.getCall(0).args[0], 'email@three.com');
+
+ done();
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
deleted file mode 100644
index 7b8a191..0000000
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
+++ /dev/null
@@ -1,145 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-gpg-editor">
- <template>
- <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;
- }
- .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);
- }
- </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
- has-tooltip
- button-title="Copy GPG public key to clipboard"
- hide-input
- 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>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-gpg-editor.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
index 9f04915..90631c7 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
@@ -14,100 +14,113 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrGpgEditor extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-gpg-editor'; }
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../styles/gr-form-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-gpg-editor_html.js';
- static get properties() {
- return {
- hasUnsavedChanges: {
- type: Boolean,
- value: false,
- notify: true,
- },
- _keys: Array,
- /** @type {?} */
- _keyToView: Object,
- _newKey: {
- type: String,
- value: '',
- },
- _keysToRemove: {
- type: Array,
- value() { return []; },
- },
- };
- }
+/** @extends Polymer.Element */
+class GrGpgEditor extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- loadData() {
- this._keys = [];
- return this.$.restAPI.getAccountGPGKeys().then(keys => {
- if (!keys) {
- return;
- }
- this._keys = Object.keys(keys)
- .map(key => {
- const gpgKey = keys[key];
- gpgKey.id = key;
- return gpgKey;
- });
- });
- }
+ static get is() { return 'gr-gpg-editor'; }
- save() {
- const promises = this._keysToRemove.map(key => {
- this.$.restAPI.deleteAccountGPGKey(key.id);
- });
-
- return Promise.all(promises).then(() => {
- this._keysToRemove = [];
- this.hasUnsavedChanges = false;
- });
- }
-
- _showKey(e) {
- const el = Polymer.dom(e).localTarget;
- const index = parseInt(el.getAttribute('data-index'), 10);
- this._keyToView = this._keys[index];
- this.$.viewKeyOverlay.open();
- }
-
- _closeOverlay() {
- this.$.viewKeyOverlay.close();
- }
-
- _handleDeleteKey(e) {
- const el = Polymer.dom(e).localTarget;
- const index = parseInt(el.getAttribute('data-index'), 10);
- this.push('_keysToRemove', this._keys[index]);
- this.splice('_keys', index, 1);
- this.hasUnsavedChanges = true;
- }
-
- _handleAddKey() {
- this.$.addButton.disabled = true;
- this.$.newKey.disabled = true;
- return this.$.restAPI.addAccountGPGKey({add: [this._newKey.trim()]})
- .then(key => {
- this.$.newKey.disabled = false;
- this._newKey = '';
- this.loadData();
- })
- .catch(() => {
- this.$.addButton.disabled = false;
- this.$.newKey.disabled = false;
- });
- }
-
- _computeAddButtonDisabled(newKey) {
- return !newKey.length;
- }
+ static get properties() {
+ return {
+ hasUnsavedChanges: {
+ type: Boolean,
+ value: false,
+ notify: true,
+ },
+ _keys: Array,
+ /** @type {?} */
+ _keyToView: Object,
+ _newKey: {
+ type: String,
+ value: '',
+ },
+ _keysToRemove: {
+ type: Array,
+ value() { return []; },
+ },
+ };
}
- customElements.define(GrGpgEditor.is, GrGpgEditor);
-})();
+ loadData() {
+ this._keys = [];
+ return this.$.restAPI.getAccountGPGKeys().then(keys => {
+ if (!keys) {
+ return;
+ }
+ this._keys = Object.keys(keys)
+ .map(key => {
+ const gpgKey = keys[key];
+ gpgKey.id = key;
+ return gpgKey;
+ });
+ });
+ }
+
+ save() {
+ const promises = this._keysToRemove.map(key => {
+ this.$.restAPI.deleteAccountGPGKey(key.id);
+ });
+
+ return Promise.all(promises).then(() => {
+ this._keysToRemove = [];
+ this.hasUnsavedChanges = false;
+ });
+ }
+
+ _showKey(e) {
+ const el = dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ this._keyToView = this._keys[index];
+ this.$.viewKeyOverlay.open();
+ }
+
+ _closeOverlay() {
+ this.$.viewKeyOverlay.close();
+ }
+
+ _handleDeleteKey(e) {
+ const el = dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ this.push('_keysToRemove', this._keys[index]);
+ this.splice('_keys', index, 1);
+ this.hasUnsavedChanges = true;
+ }
+
+ _handleAddKey() {
+ this.$.addButton.disabled = true;
+ this.$.newKey.disabled = true;
+ return this.$.restAPI.addAccountGPGKey({add: [this._newKey.trim()]})
+ .then(key => {
+ this.$.newKey.disabled = false;
+ this._newKey = '';
+ this.loadData();
+ })
+ .catch(() => {
+ this.$.addButton.disabled = false;
+ this.$.newKey.disabled = false;
+ });
+ }
+
+ _computeAddButtonDisabled(newKey) {
+ return !newKey.length;
+ }
+}
+
+customElements.define(GrGpgEditor.is, GrGpgEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.js b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.js
new file mode 100644
index 0000000..3ec4642
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.js
@@ -0,0 +1,114 @@
+/**
+ * @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.js';
+
+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;
+ }
+ .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);
+ }
+ </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 has-tooltip="" button-title="Copy GPG public key to clipboard" hide-input="" 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>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
index 08c36fe..c9daa89 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-gpg-editor</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-gpg-editor.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,163 +30,165 @@
</template>
</test-fixture>
-<script>
- suite('gr-gpg-editor tests', async () => {
- await readyToTest();
- let element;
- let keys;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-gpg-editor.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-gpg-editor tests', () => {
+ let element;
+ let keys;
- setup(done => {
- const fingerprint1 = '0192 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B';
- const fingerprint2 = '0196 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B';
- keys = {
- AFC8A49B: {
- fingerprint: fingerprint1,
- user_ids: [
- 'John Doe john.doe@example.com',
- ],
- key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
- '\nVersion: BCPG v1.52\n\t<key 1>',
- status: 'TRUSTED',
- problems: [],
- },
- AED9B59C: {
- fingerprint: fingerprint2,
- user_ids: [
- 'Gerrit gerrit@example.com',
- ],
- key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
- '\nVersion: BCPG v1.52\n\t<key 2>',
- status: 'TRUSTED',
- problems: [],
- },
- };
+ setup(done => {
+ const fingerprint1 = '0192 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B';
+ const fingerprint2 = '0196 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B';
+ keys = {
+ AFC8A49B: {
+ fingerprint: fingerprint1,
+ user_ids: [
+ 'John Doe john.doe@example.com',
+ ],
+ key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
+ '\nVersion: BCPG v1.52\n\t<key 1>',
+ status: 'TRUSTED',
+ problems: [],
+ },
+ AED9B59C: {
+ fingerprint: fingerprint2,
+ user_ids: [
+ 'Gerrit gerrit@example.com',
+ ],
+ key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
+ '\nVersion: BCPG v1.52\n\t<key 2>',
+ status: 'TRUSTED',
+ problems: [],
+ },
+ };
- stub('gr-rest-api-interface', {
- getAccountGPGKeys() { return Promise.resolve(keys); },
- });
-
- element = fixture('basic');
-
- element.loadData().then(() => { flush(done); });
+ stub('gr-rest-api-interface', {
+ getAccountGPGKeys() { return Promise.resolve(keys); },
});
- test('renders', () => {
- const rows = Polymer.dom(element.root).querySelectorAll('tbody tr');
+ element = fixture('basic');
- assert.equal(rows.length, 2);
+ element.loadData().then(() => { flush(done); });
+ });
- let cells = rows[0].querySelectorAll('td');
- assert.equal(cells[0].textContent, 'AFC8A49B');
+ test('renders', () => {
+ const rows = dom(element.root).querySelectorAll('tbody tr');
- cells = rows[1].querySelectorAll('td');
- assert.equal(cells[0].textContent, 'AED9B59C');
- });
+ assert.equal(rows.length, 2);
- test('remove key', done => {
- const lastKey = keys[Object.keys(keys)[1]];
+ let cells = rows[0].querySelectorAll('td');
+ assert.equal(cells[0].textContent, 'AFC8A49B');
- const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountGPGKey',
- () => Promise.resolve());
+ cells = rows[1].querySelectorAll('td');
+ assert.equal(cells[0].textContent, 'AED9B59C');
+ });
+ test('remove key', done => {
+ const lastKey = keys[Object.keys(keys)[1]];
+
+ const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountGPGKey',
+ () => Promise.resolve());
+
+ assert.equal(element._keysToRemove.length, 0);
+ assert.isFalse(element.hasUnsavedChanges);
+
+ // Get the delete button for the last row.
+ const button = dom(element.root).querySelector(
+ 'tbody tr:last-of-type td:nth-child(6) gr-button');
+
+ MockInteractions.tap(button);
+
+ 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);
+
+ element.save().then(() => {
+ assert.isTrue(saveStub.called);
+ assert.equal(saveStub.lastCall.args[0], Object.keys(keys)[1]);
assert.equal(element._keysToRemove.length, 0);
assert.isFalse(element.hasUnsavedChanges);
-
- // Get the delete button for the last row.
- const button = Polymer.dom(element.root).querySelector(
- 'tbody tr:last-of-type td:nth-child(6) gr-button');
-
- MockInteractions.tap(button);
-
- 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);
-
- element.save().then(() => {
- assert.isTrue(saveStub.called);
- assert.equal(saveStub.lastCall.args[0], Object.keys(keys)[1]);
- assert.equal(element._keysToRemove.length, 0);
- assert.isFalse(element.hasUnsavedChanges);
- done();
- });
- });
-
- test('show key', () => {
- const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open');
-
- // Get the show button for the last row.
- const button = Polymer.dom(element.root).querySelector(
- 'tbody tr:last-of-type td:nth-child(4) gr-button');
-
- MockInteractions.tap(button);
-
- assert.equal(element._keyToView, keys[Object.keys(keys)[1]]);
- assert.isTrue(openSpy.called);
- });
-
- test('add key', done => {
- const newKeyString =
- '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
- '\nVersion: BCPG v1.52\n\t<key 3>';
- const newKeyObject = {
- ADE8A59B: {
- fingerprint: '0194 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B',
- user_ids: [
- 'John john@example.com',
- ],
- key: newKeyString,
- status: 'TRUSTED',
- problems: [],
- },
- };
-
- const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
- () => Promise.resolve(newKeyObject));
-
- element._newKey = newKeyString;
-
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
-
- element._handleAddKey().then(() => {
- assert.isTrue(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
- assert.equal(element._keys.length, 2);
- done();
- });
-
- assert.isTrue(element.$.addButton.disabled);
- assert.isTrue(element.$.newKey.disabled);
-
- assert.isTrue(addStub.called);
- assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
- });
-
- test('add invalid key', done => {
- const newKeyString = 'not even close to valid';
-
- const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
- () => Promise.reject(new Error('error')));
-
- element._newKey = newKeyString;
-
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
-
- element._handleAddKey().then(() => {
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
- assert.equal(element._keys.length, 2);
- done();
- });
-
- assert.isTrue(element.$.addButton.disabled);
- assert.isTrue(element.$.newKey.disabled);
-
- assert.isTrue(addStub.called);
- assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
+ done();
});
});
+
+ test('show key', () => {
+ const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open');
+
+ // Get the show button for the last row.
+ const button = dom(element.root).querySelector(
+ 'tbody tr:last-of-type td:nth-child(4) gr-button');
+
+ MockInteractions.tap(button);
+
+ assert.equal(element._keyToView, keys[Object.keys(keys)[1]]);
+ assert.isTrue(openSpy.called);
+ });
+
+ test('add key', done => {
+ const newKeyString =
+ '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
+ '\nVersion: BCPG v1.52\n\t<key 3>';
+ const newKeyObject = {
+ ADE8A59B: {
+ fingerprint: '0194 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B',
+ user_ids: [
+ 'John john@example.com',
+ ],
+ key: newKeyString,
+ status: 'TRUSTED',
+ problems: [],
+ },
+ };
+
+ const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
+ () => Promise.resolve(newKeyObject));
+
+ element._newKey = newKeyString;
+
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+
+ element._handleAddKey().then(() => {
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+ assert.equal(element._keys.length, 2);
+ done();
+ });
+
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isTrue(element.$.newKey.disabled);
+
+ assert.isTrue(addStub.called);
+ assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
+ });
+
+ test('add invalid key', done => {
+ const newKeyString = 'not even close to valid';
+
+ const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
+ () => Promise.reject(new Error('error')));
+
+ element._newKey = newKeyString;
+
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+
+ element._handleAddKey().then(() => {
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+ assert.equal(element._keys.length, 2);
+ done();
+ });
+
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isTrue(element.$.newKey.disabled);
+
+ assert.isTrue(addStub.called);
+ assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
deleted file mode 100644
index e51294d..0000000
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
+++ /dev/null
@@ -1,69 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-group-list">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- #groups .nameColumn {
- min-width: 11em;
- width: auto;
- }
- .descriptionHeader {
- min-width: 21.5em;
- }
- .visibleCell {
- text-align: center;
- width: 6em;
- }
- </style>
- <div class="gr-form-styles">
- <table id="groups">
- <thead>
- <tr>
- <th class="nameHeader">Name</th>
- <th class="descriptionHeader">Description</th>
- <th class="visibleCell">Visible to all</th>
- </tr>
- </thead>
- <tbody>
- <template is="dom-repeat" items="[[_groups]]">
- <tr>
- <td class="nameColumn">
- <a href$="[[_computeGroupPath(item)]]">
- [[item.name]]
- </a>
- </td>
- <td>[[item.description]]</td>
- <td class="visibleCell">[[_computeVisibleToAll(item)]]</td>
- </tr>
- </template>
- </tbody>
- </table>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-group-list.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
index c7b5faa..01739cd 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
@@ -14,39 +14,48 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrGroupList extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-group-list'; }
+import '../../../styles/shared-styles.js';
+import '../../../styles/gr-form-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-group-list_html.js';
- static get properties() {
- return {
- _groups: Array,
- };
- }
+/** @extends Polymer.Element */
+class GrGroupList extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- loadData() {
- return this.$.restAPI.getAccountGroups().then(groups => {
- this._groups = groups.sort((a, b) => a.name.localeCompare(b.name));
- });
- }
+ static get is() { return 'gr-group-list'; }
- _computeVisibleToAll(group) {
- return group.options.visible_to_all ? 'Yes' : 'No';
- }
-
- _computeGroupPath(group) {
- if (!group || !group.id) { return; }
-
- // Group ID is already encoded from the API
- // Decode it here to match with our router encoding behavior
- return Gerrit.Nav.getUrlForGroup(decodeURIComponent(group.id));
- }
+ static get properties() {
+ return {
+ _groups: Array,
+ };
}
- customElements.define(GrGroupList.is, GrGroupList);
-})();
+ loadData() {
+ return this.$.restAPI.getAccountGroups().then(groups => {
+ this._groups = groups.sort((a, b) => a.name.localeCompare(b.name));
+ });
+ }
+
+ _computeVisibleToAll(group) {
+ return group.options.visible_to_all ? 'Yes' : 'No';
+ }
+
+ _computeGroupPath(group) {
+ if (!group || !group.id) { return; }
+
+ // Group ID is already encoded from the API
+ // Decode it here to match with our router encoding behavior
+ return Gerrit.Nav.getUrlForGroup(decodeURIComponent(group.id));
+ }
+}
+
+customElements.define(GrGroupList.is, GrGroupList);
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_html.js b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_html.js
new file mode 100644
index 0000000..ddacd31
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_html.js
@@ -0,0 +1,61 @@
+/**
+ * @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.js';
+
+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">
+ #groups .nameColumn {
+ min-width: 11em;
+ width: auto;
+ }
+ .descriptionHeader {
+ min-width: 21.5em;
+ }
+ .visibleCell {
+ text-align: center;
+ width: 6em;
+ }
+ </style>
+ <div class="gr-form-styles">
+ <table id="groups">
+ <thead>
+ <tr>
+ <th class="nameHeader">Name</th>
+ <th class="descriptionHeader">Description</th>
+ <th class="visibleCell">Visible to all</th>
+ </tr>
+ </thead>
+ <tbody>
+ <template is="dom-repeat" items="[[_groups]]">
+ <tr>
+ <td class="nameColumn">
+ <a href\$="[[_computeGroupPath(item)]]">
+ [[item.name]]
+ </a>
+ </td>
+ <td>[[item.description]]</td>
+ <td class="visibleCell">[[_computeVisibleToAll(item)]]</td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
index 10b67ec..52a3edc 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-settings-view</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-group-list.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,81 +30,83 @@
</template>
</test-fixture>
-<script>
- suite('gr-group-list tests', async () => {
- await readyToTest();
- let sandbox;
- let element;
- let groups;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-group-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-group-list tests', () => {
+ let sandbox;
+ let element;
+ let groups;
- setup(done => {
- sandbox = sinon.sandbox.create();
- groups = [{
- url: 'some url',
- options: {},
- description: 'Group 1 description',
- group_id: 1,
- owner: 'Administrators',
- owner_id: '123',
- id: 'abc',
- name: 'Group 1',
- }, {
- options: {visible_to_all: true},
- id: '456',
- name: 'Group 2',
- }, {
- options: {},
- id: '789',
- name: 'Group 3',
- }];
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ groups = [{
+ url: 'some url',
+ options: {},
+ description: 'Group 1 description',
+ group_id: 1,
+ owner: 'Administrators',
+ owner_id: '123',
+ id: 'abc',
+ name: 'Group 1',
+ }, {
+ options: {visible_to_all: true},
+ id: '456',
+ name: 'Group 2',
+ }, {
+ options: {},
+ id: '789',
+ name: 'Group 3',
+ }];
- stub('gr-rest-api-interface', {
- getAccountGroups() { return Promise.resolve(groups); },
- });
-
- element = fixture('basic');
-
- element.loadData().then(() => { flush(done); });
+ stub('gr-rest-api-interface', {
+ getAccountGroups() { return Promise.resolve(groups); },
});
- teardown(() => { sandbox.restore(); });
+ element = fixture('basic');
- test('renders', () => {
- const rows = Array.from(
- Polymer.dom(element.root).querySelectorAll('tbody tr'));
-
- assert.equal(rows.length, 3);
-
- const nameCells = rows.map(row =>
- row.querySelectorAll('td a')[0].textContent.trim()
- );
-
- assert.equal(nameCells[0], 'Group 1');
- assert.equal(nameCells[1], 'Group 2');
- assert.equal(nameCells[2], 'Group 3');
- });
-
- test('_computeVisibleToAll', () => {
- assert.equal(element._computeVisibleToAll(groups[0]), 'No');
- assert.equal(element._computeVisibleToAll(groups[1]), 'Yes');
- });
-
- test('_computeGroupPath', () => {
- sandbox.stub(Gerrit.Nav, 'getUrlForGroup',
- () => '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
-
- let group = {
- id: 'e2cd66f88a2db4d391ac068a92d987effbe872f5',
- };
-
- assert.equal(element._computeGroupPath(group),
- '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
-
- group = {
- name: 'admin',
- };
-
- assert.isUndefined(element._computeGroupPath(group));
- });
+ element.loadData().then(() => { flush(done); });
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('renders', () => {
+ const rows = Array.from(
+ dom(element.root).querySelectorAll('tbody tr'));
+
+ assert.equal(rows.length, 3);
+
+ const nameCells = rows.map(row =>
+ row.querySelectorAll('td a')[0].textContent.trim()
+ );
+
+ assert.equal(nameCells[0], 'Group 1');
+ assert.equal(nameCells[1], 'Group 2');
+ assert.equal(nameCells[2], 'Group 3');
+ });
+
+ test('_computeVisibleToAll', () => {
+ assert.equal(element._computeVisibleToAll(groups[0]), 'No');
+ assert.equal(element._computeVisibleToAll(groups[1]), 'Yes');
+ });
+
+ test('_computeGroupPath', () => {
+ sandbox.stub(Gerrit.Nav, 'getUrlForGroup',
+ () => '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
+
+ let group = {
+ id: 'e2cd66f88a2db4d391ac068a92d987effbe872f5',
+ };
+
+ assert.equal(element._computeGroupPath(group),
+ '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
+
+ group = {
+ name: 'admin',
+ };
+
+ assert.isUndefined(element._computeGroupPath(group));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html
deleted file mode 100644
index 22ba457..0000000
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html
+++ /dev/null
@@ -1,106 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-http-password">
- <template>
- <style include="shared-styles">
- .password {
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-mono);
- line-height: var(--line-height-mono);
- }
- #generatedPasswordOverlay {
- padding: var(--spacing-xxl);
- width: 50em;
- }
- #generatedPasswordDisplay {
- margin: var(--spacing-l) 0;
- }
- #generatedPasswordDisplay .title {
- width: unset;
- }
- #generatedPasswordDisplay .value {
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-mono);
- line-height: var(--line-height-mono);
- }
- #passwordWarning {
- font-style: italic;
- text-align: center;
- }
- .closeButton {
- bottom: 2em;
- position: absolute;
- right: 2em;
- }
- </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">
- <div hidden$="[[_passwordUrl]]">
- <section>
- <span class="title">Username</span>
- <span class="value">[[_username]]</span>
- </section>
- <gr-button
- id="generateButton"
- on-click="_handleGenerateTap">Generate new password</gr-button>
- </div>
- <span hidden$="[[!_passwordUrl]]">
- <a href$="[[_passwordUrl]]" target="_blank" rel="noopener">
- Obtain password</a>
- (opens in a new tab)
- </span>
- </div>
- <gr-overlay
- id="generatedPasswordOverlay"
- on-iron-overlay-closed="_generatedPasswordOverlayClosed"
- with-backdrop>
- <div class="gr-form-styles">
- <section id="generatedPasswordDisplay">
- <span class="title">New Password:</span>
- <span class="value">[[_generatedPassword]]</span>
- <gr-copy-clipboard
- has-tooltip
- button-title="Copy password to clipboard"
- hide-input
- text="[[_generatedPassword]]">
- </gr-copy-clipboard>
- </section>
- <section id="passwordWarning">
- This password will not be displayed again.<br>
- If you lose it, you will need to generate a new one.
- </section>
- <gr-button
- link
- class="closeButton"
- on-click="_closeOverlay">Close</gr-button>
- </div>
- </gr-overlay>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-http-password.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
index efd0c39..02657f8 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
@@ -14,59 +14,70 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrHttpPassword extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-http-password'; }
+import '../../../styles/gr-form-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-http-password_html.js';
- static get properties() {
- return {
- _username: String,
- _generatedPassword: String,
- _passwordUrl: String,
- };
- }
+/** @extends Polymer.Element */
+class GrHttpPassword extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /** @override */
- attached() {
- super.attached();
- this.loadData();
- }
+ static get is() { return 'gr-http-password'; }
- loadData() {
- const promises = [];
-
- promises.push(this.$.restAPI.getAccount().then(account => {
- this._username = account.username;
- }));
-
- promises.push(this.$.restAPI.getConfig().then(info => {
- this._passwordUrl = info.auth.http_password_url || null;
- }));
-
- return Promise.all(promises);
- }
-
- _handleGenerateTap() {
- this._generatedPassword = 'Generating...';
- this.$.generatedPasswordOverlay.open();
- this.$.restAPI.generateAccountHttpPassword().then(newPassword => {
- this._generatedPassword = newPassword;
- });
- }
-
- _closeOverlay() {
- this.$.generatedPasswordOverlay.close();
- }
-
- _generatedPasswordOverlayClosed() {
- this._generatedPassword = '';
- }
+ static get properties() {
+ return {
+ _username: String,
+ _generatedPassword: String,
+ _passwordUrl: String,
+ };
}
- customElements.define(GrHttpPassword.is, GrHttpPassword);
-})();
+ /** @override */
+ attached() {
+ super.attached();
+ this.loadData();
+ }
+
+ loadData() {
+ const promises = [];
+
+ promises.push(this.$.restAPI.getAccount().then(account => {
+ this._username = account.username;
+ }));
+
+ promises.push(this.$.restAPI.getConfig().then(info => {
+ this._passwordUrl = info.auth.http_password_url || null;
+ }));
+
+ return Promise.all(promises);
+ }
+
+ _handleGenerateTap() {
+ this._generatedPassword = 'Generating...';
+ this.$.generatedPasswordOverlay.open();
+ this.$.restAPI.generateAccountHttpPassword().then(newPassword => {
+ this._generatedPassword = newPassword;
+ });
+ }
+
+ _closeOverlay() {
+ this.$.generatedPasswordOverlay.close();
+ }
+
+ _generatedPasswordOverlayClosed() {
+ this._generatedPassword = '';
+ }
+}
+
+customElements.define(GrHttpPassword.is, GrHttpPassword);
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_html.js b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_html.js
new file mode 100644
index 0000000..b75f56e
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_html.js
@@ -0,0 +1,84 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ .password {
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-mono);
+ line-height: var(--line-height-mono);
+ }
+ #generatedPasswordOverlay {
+ padding: var(--spacing-xxl);
+ width: 50em;
+ }
+ #generatedPasswordDisplay {
+ margin: var(--spacing-l) 0;
+ }
+ #generatedPasswordDisplay .title {
+ width: unset;
+ }
+ #generatedPasswordDisplay .value {
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-mono);
+ line-height: var(--line-height-mono);
+ }
+ #passwordWarning {
+ font-style: italic;
+ text-align: center;
+ }
+ .closeButton {
+ bottom: 2em;
+ position: absolute;
+ right: 2em;
+ }
+ </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">
+ <div hidden\$="[[_passwordUrl]]">
+ <section>
+ <span class="title">Username</span>
+ <span class="value">[[_username]]</span>
+ </section>
+ <gr-button id="generateButton" on-click="_handleGenerateTap">Generate new password</gr-button>
+ </div>
+ <span hidden\$="[[!_passwordUrl]]">
+ <a href\$="[[_passwordUrl]]" target="_blank" rel="noopener">
+ Obtain password</a>
+ (opens in a new tab)
+ </span>
+ </div>
+ <gr-overlay id="generatedPasswordOverlay" on-iron-overlay-closed="_generatedPasswordOverlayClosed" with-backdrop="">
+ <div class="gr-form-styles">
+ <section id="generatedPasswordDisplay">
+ <span class="title">New Password:</span>
+ <span class="value">[[_generatedPassword]]</span>
+ <gr-copy-clipboard has-tooltip="" button-title="Copy password to clipboard" hide-input="" text="[[_generatedPassword]]">
+ </gr-copy-clipboard>
+ </section>
+ <section id="passwordWarning">
+ This password will not be displayed again.<br>
+ If you lose it, you will need to generate a new one.
+ </section>
+ <gr-button link="" class="closeButton" on-click="_closeOverlay">Close</gr-button>
+ </div>
+ </gr-overlay>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
index 974a0f2..b31fa50 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-settings-view</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-http-password.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,61 +30,61 @@
</template>
</test-fixture>
-<script>
- suite('gr-http-password tests', async () => {
- await readyToTest();
- let element;
- let account;
- let config;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-http-password.js';
+suite('gr-http-password tests', () => {
+ let element;
+ let account;
+ let config;
- setup(done => {
- account = {username: 'user name'};
- config = {auth: {}};
+ setup(done => {
+ account = {username: 'user name'};
+ config = {auth: {}};
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve(account); },
- getConfig() { return Promise.resolve(config); },
- });
-
- element = fixture('basic');
- element.loadData().then(() => { flush(done); });
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve(account); },
+ getConfig() { return Promise.resolve(config); },
});
- test('generate password', () => {
- const button = element.$.generateButton;
- const nextPassword = 'the new password';
- let generateResolve;
- const generateStub = sinon.stub(element.$.restAPI,
- 'generateAccountHttpPassword', () => new Promise(resolve => {
- generateResolve = resolve;
- }));
+ element = fixture('basic');
+ element.loadData().then(() => { flush(done); });
+ });
- assert.isNotOk(element._generatedPassword);
+ test('generate password', () => {
+ const button = element.$.generateButton;
+ const nextPassword = 'the new password';
+ let generateResolve;
+ const generateStub = sinon.stub(element.$.restAPI,
+ 'generateAccountHttpPassword', () => new Promise(resolve => {
+ generateResolve = resolve;
+ }));
- MockInteractions.tap(button);
+ assert.isNotOk(element._generatedPassword);
- assert.isTrue(generateStub.called);
- assert.equal(element._generatedPassword, 'Generating...');
+ MockInteractions.tap(button);
- generateResolve(nextPassword);
+ assert.isTrue(generateStub.called);
+ assert.equal(element._generatedPassword, 'Generating...');
- generateStub.lastCall.returnValue.then(() => {
- assert.equal(element._generatedPassword, nextPassword);
- });
- });
+ generateResolve(nextPassword);
- test('without http_password_url', () => {
- assert.isNull(element._passwordUrl);
- });
-
- test('with http_password_url', done => {
- config.auth.http_password_url = 'http://example.com/';
- element.loadData().then(() => {
- assert.isNotNull(element._passwordUrl);
- assert.equal(element._passwordUrl, config.auth.http_password_url);
- done();
- });
+ generateStub.lastCall.returnValue.then(() => {
+ assert.equal(element._generatedPassword, nextPassword);
});
});
+ test('without http_password_url', () => {
+ assert.isNull(element._passwordUrl);
+ });
+
+ test('with http_password_url', done => {
+ config.auth.http_password_url = 'http://example.com/';
+ element.loadData().then(() => {
+ assert.isNotNull(element._passwordUrl);
+ assert.equal(element._passwordUrl, config.auth.http_password_url);
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.html b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.html
deleted file mode 100644
index 53d74f2..0000000
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.html
+++ /dev/null
@@ -1,108 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-identities">
- <template>
- <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]]"
- item-type="id"></gr-confirm-delete-item-dialog>
- </gr-overlay>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-identities.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
index ac4f9e4..57f0e1d 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
@@ -14,95 +14,108 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const AUTH = [
- 'OPENID',
- 'OAUTH',
- ];
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../../styles/gr-form-styles.js';
+import '../../admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-identities_html.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @extends Polymer.Element
- */
- class GrIdentities extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-identities'; }
+const AUTH = [
+ 'OPENID',
+ 'OAUTH',
+];
- static get properties() {
- return {
- _identities: Object,
- _idName: String,
- serverConfig: Object,
- _showLinkAnotherIdentity: {
- type: Boolean,
- computed: '_computeShowLinkAnotherIdentity(serverConfig)',
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @extends Polymer.Element
+ */
+class GrIdentities extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- loadData() {
- return this.$.restAPI.getExternalIds().then(id => {
- this._identities = id;
- });
- }
+ static get is() { return 'gr-identities'; }
- _computeIdentity(id) {
- return id && id.startsWith('mailto:') ? '' : id;
- }
-
- _computeHideDeleteClass(canDelete) {
- return canDelete ? 'show' : '';
- }
-
- _handleDeleteItemConfirm() {
- this.$.overlay.close();
- return this.$.restAPI.deleteAccountIdentity([this._idName])
- .then(() => { this.loadData(); });
- }
-
- _handleConfirmDialogCancel() {
- this.$.overlay.close();
- }
-
- _handleDeleteItem(e) {
- const name = e.model.get('item.identity');
- if (!name) { return; }
- this._idName = name;
- this.$.overlay.open();
- }
-
- _computeIsTrusted(item) {
- return item ? '' : 'Untrusted';
- }
-
- filterIdentities(item) {
- return !item.identity.startsWith('username:');
- }
-
- _computeShowLinkAnotherIdentity(config) {
- if (config && config.auth &&
- config.auth.git_basic_auth_policy) {
- return AUTH.includes(
- config.auth.git_basic_auth_policy.toUpperCase());
- }
-
- return false;
- }
-
- _computeLinkAnotherIdentity() {
- const baseUrl = this.getBaseUrl() || '';
- let pathname = window.location.pathname;
- if (baseUrl) {
- pathname = '/' + pathname.substring(baseUrl.length);
- }
- return baseUrl + '/login/' + encodeURIComponent(pathname) + '?link';
- }
+ static get properties() {
+ return {
+ _identities: Object,
+ _idName: String,
+ serverConfig: Object,
+ _showLinkAnotherIdentity: {
+ type: Boolean,
+ computed: '_computeShowLinkAnotherIdentity(serverConfig)',
+ },
+ };
}
- customElements.define(GrIdentities.is, GrIdentities);
-})();
+ loadData() {
+ return this.$.restAPI.getExternalIds().then(id => {
+ this._identities = id;
+ });
+ }
+
+ _computeIdentity(id) {
+ return id && id.startsWith('mailto:') ? '' : id;
+ }
+
+ _computeHideDeleteClass(canDelete) {
+ return canDelete ? 'show' : '';
+ }
+
+ _handleDeleteItemConfirm() {
+ this.$.overlay.close();
+ return this.$.restAPI.deleteAccountIdentity([this._idName])
+ .then(() => { this.loadData(); });
+ }
+
+ _handleConfirmDialogCancel() {
+ this.$.overlay.close();
+ }
+
+ _handleDeleteItem(e) {
+ const name = e.model.get('item.identity');
+ if (!name) { return; }
+ this._idName = name;
+ this.$.overlay.open();
+ }
+
+ _computeIsTrusted(item) {
+ return item ? '' : 'Untrusted';
+ }
+
+ filterIdentities(item) {
+ return !item.identity.startsWith('username:');
+ }
+
+ _computeShowLinkAnotherIdentity(config) {
+ if (config && config.auth &&
+ config.auth.git_basic_auth_policy) {
+ return AUTH.includes(
+ config.auth.git_basic_auth_policy.toUpperCase());
+ }
+
+ return false;
+ }
+
+ _computeLinkAnotherIdentity() {
+ const baseUrl = this.getBaseUrl() || '';
+ let pathname = window.location.pathname;
+ if (baseUrl) {
+ pathname = '/' + pathname.substring(baseUrl.length);
+ }
+ return baseUrl + '/login/' + encodeURIComponent(pathname) + '?link';
+ }
+}
+
+customElements.define(GrIdentities.is, GrIdentities);
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.js b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.js
new file mode 100644
index 0000000..f1424cc
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.js
@@ -0,0 +1,90 @@
+/**
+ * @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.js';
+
+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]]" item-type="id"></gr-confirm-delete-item-dialog>
+ </gr-overlay>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
index be73a0c..5c40b47 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-identities</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-identities.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,158 +30,160 @@
</template>
</test-fixture>
-<script>
- suite('gr-identities tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- const ids = [
- {
- identity: 'username:john',
- email_address: 'john.doe@example.com',
- trusted: true,
- }, {
- identity: 'gerrit:gerrit',
- email_address: 'gerrit@example.com',
- }, {
- identity: 'mailto:gerrit2@example.com',
- email_address: 'gerrit2@example.com',
- trusted: true,
- can_delete: true,
- },
- ];
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-identities.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-identities tests', () => {
+ let element;
+ let sandbox;
+ const ids = [
+ {
+ identity: 'username:john',
+ email_address: 'john.doe@example.com',
+ trusted: true,
+ }, {
+ identity: 'gerrit:gerrit',
+ email_address: 'gerrit@example.com',
+ }, {
+ identity: 'mailto:gerrit2@example.com',
+ email_address: 'gerrit2@example.com',
+ trusted: true,
+ can_delete: true,
+ },
+ ];
- setup(done => {
- sandbox = sinon.sandbox.create();
+ setup(done => {
+ sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getExternalIds() { return Promise.resolve(ids); },
- });
-
- element = fixture('basic');
-
- element.loadData().then(() => { flush(done); });
+ stub('gr-rest-api-interface', {
+ getExternalIds() { return Promise.resolve(ids); },
});
- teardown(() => {
- sandbox.restore();
- });
+ element = fixture('basic');
- test('renders', () => {
- const rows = Array.from(
- Polymer.dom(element.root).querySelectorAll('tbody tr'));
+ element.loadData().then(() => { flush(done); });
+ });
- assert.equal(rows.length, 2);
+ teardown(() => {
+ sandbox.restore();
+ });
- const nameCells = rows.map(row =>
- row.querySelectorAll('td')[2].textContent
- );
+ test('renders', () => {
+ const rows = Array.from(
+ dom(element.root).querySelectorAll('tbody tr'));
- assert.equal(nameCells[0], 'gerrit:gerrit');
- assert.equal(nameCells[1], '');
- });
+ assert.equal(rows.length, 2);
- test('renders email', () => {
- const rows = Array.from(
- Polymer.dom(element.root).querySelectorAll('tbody tr'));
+ const nameCells = rows.map(row =>
+ row.querySelectorAll('td')[2].textContent
+ );
- assert.equal(rows.length, 2);
+ assert.equal(nameCells[0], 'gerrit:gerrit');
+ assert.equal(nameCells[1], '');
+ });
- const nameCells = rows.map(row =>
- row.querySelectorAll('td')[1].textContent
- );
+ test('renders email', () => {
+ const rows = Array.from(
+ dom(element.root).querySelectorAll('tbody tr'));
- assert.equal(nameCells[0], 'gerrit@example.com');
- assert.equal(nameCells[1], 'gerrit2@example.com');
- });
+ assert.equal(rows.length, 2);
- test('_computeIdentity', () => {
- assert.equal(
- element._computeIdentity(ids[0].identity), 'username:john');
- assert.equal(element._computeIdentity(ids[2].identity), '');
- });
+ const nameCells = rows.map(row =>
+ row.querySelectorAll('td')[1].textContent
+ );
- test('filterIdentities', () => {
- assert.isFalse(element.filterIdentities(ids[0]));
+ assert.equal(nameCells[0], 'gerrit@example.com');
+ assert.equal(nameCells[1], 'gerrit2@example.com');
+ });
- assert.isTrue(element.filterIdentities(ids[1]));
- });
+ test('_computeIdentity', () => {
+ assert.equal(
+ element._computeIdentity(ids[0].identity), 'username:john');
+ assert.equal(element._computeIdentity(ids[2].identity), '');
+ });
- test('delete id', done => {
- element._idName = 'mailto:gerrit2@example.com';
- const loadDataStub = sandbox.stub(element, 'loadData');
- element._handleDeleteItemConfirm().then(() => {
- assert.isTrue(loadDataStub.called);
- done();
- });
- });
+ test('filterIdentities', () => {
+ assert.isFalse(element.filterIdentities(ids[0]));
- test('_handleDeleteItem opens modal', () => {
- const deleteBtn =
- Polymer.dom(element.root).querySelector('.deleteButton');
- const deleteItem = sandbox.stub(element, '_handleDeleteItem');
- MockInteractions.tap(deleteBtn);
- assert.isTrue(deleteItem.called);
- });
+ assert.isTrue(element.filterIdentities(ids[1]));
+ });
- test('_computeShowLinkAnotherIdentity', () => {
- let serverConfig;
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'OAUTH',
- },
- };
- assert.isTrue(element._computeShowLinkAnotherIdentity(serverConfig));
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'OpenID',
- },
- };
- assert.isTrue(element._computeShowLinkAnotherIdentity(serverConfig));
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'HTTP_LDAP',
- },
- };
- assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig));
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'LDAP',
- },
- };
- assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig));
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'HTTP',
- },
- };
- assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig));
-
- serverConfig = {};
- assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig));
- });
-
- test('_showLinkAnotherIdentity', () => {
- element.serverConfig = {
- auth: {
- git_basic_auth_policy: 'OAUTH',
- },
- };
-
- assert.isTrue(element._showLinkAnotherIdentity);
-
- element.serverConfig = {
- auth: {
- git_basic_auth_policy: 'LDAP',
- },
- };
-
- assert.isFalse(element._showLinkAnotherIdentity);
+ test('delete id', done => {
+ element._idName = 'mailto:gerrit2@example.com';
+ const loadDataStub = sandbox.stub(element, 'loadData');
+ element._handleDeleteItemConfirm().then(() => {
+ assert.isTrue(loadDataStub.called);
+ done();
});
});
+
+ test('_handleDeleteItem opens modal', () => {
+ const deleteBtn =
+ dom(element.root).querySelector('.deleteButton');
+ const deleteItem = sandbox.stub(element, '_handleDeleteItem');
+ MockInteractions.tap(deleteBtn);
+ assert.isTrue(deleteItem.called);
+ });
+
+ test('_computeShowLinkAnotherIdentity', () => {
+ let serverConfig;
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'OAUTH',
+ },
+ };
+ assert.isTrue(element._computeShowLinkAnotherIdentity(serverConfig));
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'OpenID',
+ },
+ };
+ assert.isTrue(element._computeShowLinkAnotherIdentity(serverConfig));
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'HTTP_LDAP',
+ },
+ };
+ assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig));
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'LDAP',
+ },
+ };
+ assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig));
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'HTTP',
+ },
+ };
+ assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig));
+
+ serverConfig = {};
+ assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig));
+ });
+
+ test('_showLinkAnotherIdentity', () => {
+ element.serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'OAUTH',
+ },
+ };
+
+ assert.isTrue(element._showLinkAnotherIdentity);
+
+ element.serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'LDAP',
+ },
+ };
+
+ assert.isFalse(element._showLinkAnotherIdentity);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
deleted file mode 100644
index 46fc165..0000000
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
+++ /dev/null
@@ -1,129 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-
-<dom-module id="gr-menu-editor">
- <template>
- <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>
- </template>
- <script src="gr-menu-editor.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
index 0ee232b..42982fd 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
@@ -14,68 +14,80 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrMenuEditor extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-menu-editor'; }
+import '@polymer/iron-input/iron-input.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import '../../../styles/gr-form-styles.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-menu-editor_html.js';
- static get properties() {
- return {
- menuItems: Array,
- _newName: String,
- _newUrl: String,
- };
- }
+/** @extends Polymer.Element */
+class GrMenuEditor extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _handleMoveUpButton(e) {
- const index = Number(Polymer.dom(e).localTarget.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);
- }
+ static get is() { return 'gr-menu-editor'; }
- _handleMoveDownButton(e) {
- const index = Number(Polymer.dom(e).localTarget.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);
- }
-
- _handleDeleteButton(e) {
- const index = Number(Polymer.dom(e).localTarget.dataset.index);
- this.splice('menuItems', index, 1);
- }
-
- _handleAddButton() {
- if (this._computeAddDisabled(this._newName, this._newUrl)) { return; }
-
- this.splice('menuItems', this.menuItems.length, 0, {
- name: this._newName,
- url: this._newUrl,
- target: '_blank',
- });
-
- this._newName = '';
- this._newUrl = '';
- }
-
- _computeAddDisabled(newName, newUrl) {
- return !newName.length || !newUrl.length;
- }
-
- _handleInputKeydown(e) {
- if (e.keyCode === 13) {
- e.stopPropagation();
- this._handleAddButton();
- }
- }
+ static get properties() {
+ return {
+ menuItems: Array,
+ _newName: String,
+ _newUrl: String,
+ };
}
- customElements.define(GrMenuEditor.is, GrMenuEditor);
-})();
+ _handleMoveUpButton(e) {
+ const index = Number(dom(e).localTarget.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);
+ }
+
+ _handleMoveDownButton(e) {
+ const index = Number(dom(e).localTarget.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);
+ }
+
+ _handleDeleteButton(e) {
+ const index = Number(dom(e).localTarget.dataset.index);
+ this.splice('menuItems', index, 1);
+ }
+
+ _handleAddButton() {
+ if (this._computeAddDisabled(this._newName, this._newUrl)) { return; }
+
+ this.splice('menuItems', this.menuItems.length, 0, {
+ name: this._newName,
+ url: this._newUrl,
+ target: '_blank',
+ });
+
+ this._newName = '';
+ this._newUrl = '';
+ }
+
+ _computeAddDisabled(newName, newUrl) {
+ return !newName.length || !newUrl.length;
+ }
+
+ _handleInputKeydown(e) {
+ if (e.keyCode === 13) {
+ e.stopPropagation();
+ this._handleAddButton();
+ }
+ }
+}
+
+customElements.define(GrMenuEditor.is, GrMenuEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.js b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.js
new file mode 100644
index 0000000..58b654f
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.js
@@ -0,0 +1,88 @@
+/**
+ * @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.js';
+
+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.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
index a5f2074..b3e9a15 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-settings-view</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-menu-editor.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,146 +30,148 @@
</template>
</test-fixture>
-<script>
- suite('gr-menu-editor tests', async () => {
- await readyToTest();
- let element;
- let menu;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-menu-editor.js';
+import {flush as flush$0} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-menu-editor tests', () => {
+ let element;
+ let menu;
- function assertMenuNamesEqual(element, expected) {
- const names = element.menuItems.map(i => i.name);
- assert.equal(names.length, expected.length);
- for (let i = 0; i < names.length; i++) {
- assert.equal(names[i], expected[i]);
- }
+ function assertMenuNamesEqual(element, expected) {
+ const names = element.menuItems.map(i => i.name);
+ assert.equal(names.length, expected.length);
+ for (let i = 0; i < names.length; i++) {
+ assert.equal(names[i], expected[i]);
+ }
+ }
+
+ // Click the up/down button (according to direction) for the index'th row.
+ // The index of the first row is 0, corresponding to the array.
+ function move(element, index, direction) {
+ const selector = 'tr:nth-child(' + (index + 1) + ') .move' +
+ direction + 'Button';
+ const button =
+ element.shadowRoot
+ .querySelector('tbody').querySelector(selector)
+ .shadowRoot
+ .querySelector('paper-button');
+ MockInteractions.tap(button);
+ }
+
+ setup(done => {
+ element = fixture('basic');
+ 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);
+ flush$0();
+ flush(done);
+ });
+
+ test('renders', () => {
+ const rows = element.shadowRoot
+ .querySelector('tbody').querySelectorAll('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);
}
- // Click the up/down button (according to direction) for the index'th row.
- // The index of the first row is 0, corresponding to the array.
- function move(element, index, direction) {
- const selector = 'tr:nth-child(' + (index + 1) + ') .move' +
- direction + 'Button';
- const button =
- element.shadowRoot
- .querySelector('tbody').querySelector(selector)
- .shadowRoot
- .querySelector('paper-button');
- MockInteractions.tap(button);
- }
+ assert.isTrue(element._computeAddDisabled(element._newName,
+ element._newUrl));
+ });
- setup(done => {
- element = fixture('basic');
- 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);
- Polymer.dom.flush();
- flush(done);
- });
+ test('_computeAddDisabled', () => {
+ assert.isTrue(element._computeAddDisabled('', ''));
+ assert.isTrue(element._computeAddDisabled('name', ''));
+ assert.isTrue(element._computeAddDisabled('', 'url'));
+ assert.isFalse(element._computeAddDisabled('name', 'url'));
+ });
- test('renders', () => {
- const rows = element.shadowRoot
- .querySelector('tbody').querySelectorAll('tr');
- let tds;
+ test('add a new menu item', () => {
+ const newName = 'new name';
+ const newUrl = 'new url';
- 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);
- }
+ element._newName = newName;
+ element._newUrl = newUrl;
+ assert.isFalse(element._computeAddDisabled(element._newName,
+ element._newUrl));
- assert.isTrue(element._computeAddDisabled(element._newName,
- element._newUrl));
- });
+ const originalMenuLength = element.menuItems.length;
- test('_computeAddDisabled', () => {
- assert.isTrue(element._computeAddDisabled('', ''));
- assert.isTrue(element._computeAddDisabled('name', ''));
- assert.isTrue(element._computeAddDisabled('', 'url'));
- assert.isFalse(element._computeAddDisabled('name', 'url'));
- });
+ element._handleAddButton();
- test('add a new menu item', () => {
- const newName = 'new name';
- const newUrl = 'new url';
+ assert.equal(element.menuItems.length, originalMenuLength + 1);
+ assert.equal(element.menuItems[element.menuItems.length - 1].name,
+ newName);
+ assert.equal(element.menuItems[element.menuItems.length - 1].url, newUrl);
+ });
- element._newName = newName;
- element._newUrl = newUrl;
- assert.isFalse(element._computeAddDisabled(element._newName,
- element._newUrl));
+ test('move items down', () => {
+ assertMenuNamesEqual(element,
+ ['first name', 'second name', 'third name']);
- const originalMenuLength = element.menuItems.length;
+ // Move the middle item down
+ move(element, 1, 'Down');
+ assertMenuNamesEqual(element,
+ ['first name', 'third name', 'second name']);
- element._handleAddButton();
+ // Moving the bottom item down is a no-op.
+ move(element, 2, 'Down');
+ assertMenuNamesEqual(element,
+ ['first name', 'third name', 'second name']);
+ });
- assert.equal(element.menuItems.length, originalMenuLength + 1);
- assert.equal(element.menuItems[element.menuItems.length - 1].name,
- newName);
- assert.equal(element.menuItems[element.menuItems.length - 1].url, newUrl);
- });
+ test('move items up', () => {
+ assertMenuNamesEqual(element,
+ ['first name', 'second name', 'third name']);
- test('move items down', () => {
- assertMenuNamesEqual(element,
- ['first name', 'second name', 'third name']);
+ // Move the last item up twice to be the first.
+ move(element, 2, 'Up');
+ move(element, 1, 'Up');
+ assertMenuNamesEqual(element,
+ ['third name', 'first name', 'second name']);
- // Move the middle item down
- move(element, 1, 'Down');
- assertMenuNamesEqual(element,
- ['first name', 'third name', 'second name']);
+ // Moving the top item up is a no-op.
+ move(element, 0, 'Up');
+ assertMenuNamesEqual(element,
+ ['third name', 'first name', 'second name']);
+ });
- // Moving the bottom item down is a no-op.
- move(element, 2, 'Down');
- assertMenuNamesEqual(element,
- ['first name', 'third name', 'second name']);
- });
+ test('remove item', () => {
+ assertMenuNamesEqual(element,
+ ['first name', 'second name', 'third name']);
- test('move items up', () => {
- assertMenuNamesEqual(element,
- ['first name', 'second name', 'third name']);
+ // Tap the delete button for the middle item.
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('tbody')
+ .querySelector('tr:nth-child(2) .remove-button')
+ .shadowRoot
+ .querySelector('paper-button'));
- // Move the last item up twice to be the first.
- move(element, 2, 'Up');
- move(element, 1, 'Up');
- assertMenuNamesEqual(element,
- ['third name', 'first name', 'second name']);
+ assertMenuNamesEqual(element, ['first name', 'third name']);
- // Moving the top item up is a no-op.
- move(element, 0, 'Up');
- assertMenuNamesEqual(element,
- ['third name', 'first name', 'second name']);
- });
-
- test('remove item', () => {
- assertMenuNamesEqual(element,
- ['first name', 'second name', 'third name']);
-
- // Tap the delete button for the middle item.
+ // Delete remaining items.
+ for (let i = 0; i < 2; i++) {
MockInteractions.tap(element.shadowRoot
.querySelector('tbody')
- .querySelector('tr:nth-child(2) .remove-button')
+ .querySelector('tr:first-child .remove-button')
.shadowRoot
.querySelector('paper-button'));
+ }
+ assertMenuNamesEqual(element, []);
- assertMenuNamesEqual(element, ['first name', 'third name']);
-
- // Delete remaining items.
- for (let i = 0; i < 2; i++) {
- MockInteractions.tap(element.shadowRoot
- .querySelector('tbody')
- .querySelector('tr:first-child .remove-button')
- .shadowRoot
- .querySelector('paper-button'));
- }
- assertMenuNamesEqual(element, []);
-
- // Add item to empty menu.
- element._newName = 'new name';
- element._newUrl = 'new url';
- element._handleAddButton();
- assertMenuNamesEqual(element, ['new name']);
- });
+ // Add item to empty menu.
+ element._newName = 'new name';
+ element._newUrl = 'new url';
+ element._handleAddButton();
+ assertMenuNamesEqual(element, ['new name']);
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
deleted file mode 100644
index c289a49..0000000
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
+++ /dev/null
@@ -1,142 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-registration-dialog">
- <template>
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- :host {
- display: block;
- }
- main {
- max-width: 46em;
- }
- :host(.loading) main {
- display: none;
- }
- .loadingMessage {
- display: none;
- font-style: italic;
- }
- :host(.loading) .loadingMessage {
- display: block;
- }
- hr {
- margin-top: var(--spacing-l);
- margin-bottom: var(--spacing-l);
- }
- header {
- border-bottom: 1px solid var(--border-color);
- font-weight: var(--font-weight-bold);
- margin-bottom: var(--spacing-l);
- }
- .container {
- padding: var(--spacing-m) var(--spacing-xl);
- }
- footer {
- display: flex;
- justify-content: flex-end;
- }
- footer gr-button {
- margin-left: var(--spacing-l);
- }
- input {
- width: 20em;
- }
- section.hide {
- display: none;
- }
- </style>
- <div class="container gr-form-styles">
- <header>Please confirm your contact information</header>
- <div class="loadingMessage">Loading...</div>
- <main>
- <p>
- The following contact information was automatically obtained when you
- signed in to the site. This information is used to display who you are
- to others, and to send updates to code reviews you have either started
- or subscribed to.
- </p>
- <hr>
- <section>
- <div class="title">Full Name</div>
- <iron-input
- bind-value="{{_account.name}}">
- <input
- is="iron-input"
- id="name"
- bind-value="{{_account.name}}"
- disabled="[[_saving]]">
- </iron-input>
- </section>
- <section class$="[[_computeUsernameClass(_usernameMutable)]]">
- <div class="title">Username</div>
- <iron-input
- bind-value="{{_account.username}}">
- <input
- is="iron-input"
- id="username"
- bind-value="{{_account.username}}"
- disabled="[[_saving]]">
- </iron-input>
- </section>
- <section>
- <div class="title">Preferred Email</div>
- <select
- id="email"
- disabled="[[_saving]]">
- <option value="[[_account.email]]">[[_account.email]]</option>
- <template is="dom-repeat" items="[[_account.secondary_emails]]">
- <option value="[[item]]">[[item]]</option>
- </template>
- </select>
- </section>
- <hr>
- <p>
- More configuration options for Gerrit may be found in the
- <a on-click="close" href$="[[settingsUrl]]">settings</a>.
- </p>
- </main>
- <footer>
- <gr-button
- id="closeButton"
- link
- disabled="[[_saving]]"
- on-click="_handleClose">Close</gr-button>
- <gr-button
- id="saveButton"
- primary
- link
- disabled="[[_computeSaveDisabled(_account.name, _account.email, _saving)]]"
- on-click="_handleSave">Save</gr-button>
- </footer>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-registration-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
index 4bb98d0..c20800f 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
@@ -14,142 +14,155 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '@polymer/iron-input/iron-input.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/gr-form-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-registration-dialog_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrRegistrationDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-registration-dialog'; }
+ /**
+ * Fired when account details are changed.
+ *
+ * @event account-detail-update
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the close button is pressed.
+ *
+ * @event close
*/
- class GrRegistrationDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-registration-dialog'; }
- /**
- * Fired when account details are changed.
- *
- * @event account-detail-update
- */
- /**
- * Fired when the close button is pressed.
- *
- * @event close
- */
-
- static get properties() {
- return {
- settingsUrl: String,
- /** @type {?} */
- _account: {
- type: Object,
- value: () => {
- // Prepopulate possibly undefined fields with values to trigger
- // computed bindings.
- return {email: null, name: null, username: null};
- },
+ static get properties() {
+ return {
+ settingsUrl: String,
+ /** @type {?} */
+ _account: {
+ type: Object,
+ value: () => {
+ // Prepopulate possibly undefined fields with values to trigger
+ // computed bindings.
+ return {email: null, name: null, username: null};
},
- _usernameMutable: {
- type: Boolean,
- computed: '_computeUsernameMutable(_serverConfig, _account.username)',
- },
- _loading: {
- type: Boolean,
- value: true,
- observer: '_loadingChanged',
- },
- _saving: {
- type: Boolean,
- value: false,
- },
- _serverConfig: Object,
- };
- }
-
- /** @override */
- ready() {
- super.ready();
- this._ensureAttribute('role', 'dialog');
- }
-
- loadData() {
- this._loading = true;
-
- const loadAccount = this.$.restAPI.getAccount().then(account => {
- // Using Object.assign here allows preservation of the default values
- // supplied in the value generating function of this._account, unless
- // they are overridden by properties in the account from the response.
- this._account = Object.assign({}, this._account, account);
- });
-
- const loadConfig = this.$.restAPI.getConfig().then(config => {
- this._serverConfig = config;
- });
-
- return Promise.all([loadAccount, loadConfig]).then(() => {
- this._loading = false;
- });
- }
-
- _save() {
- this._saving = true;
- const promises = [
- this.$.restAPI.setAccountName(this.$.name.value),
- this.$.restAPI.setPreferredAccountEmail(this.$.email.value || ''),
- ];
-
- if (this._usernameMutable) {
- promises.push(this.$.restAPI.setAccountUsername(this.$.username.value));
- }
-
- return Promise.all(promises).then(() => {
- this._saving = false;
- this.fire('account-detail-update');
- });
- }
-
- _handleSave(e) {
- e.preventDefault();
- this._save().then(this.close.bind(this));
- }
-
- _handleClose(e) {
- e.preventDefault();
- this.close();
- }
-
- close() {
- this._saving = true; // disable buttons indefinitely
- this.fire('close');
- }
-
- _computeSaveDisabled(name, email, saving) {
- return !name || !email || saving;
- }
-
- _computeUsernameMutable(config, username) {
- // Polymer 2: check for undefined
- if ([
- config,
- username,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- return config.auth.editable_account_fields.includes('USER_NAME') &&
- !username;
- }
-
- _computeUsernameClass(usernameMutable) {
- return usernameMutable ? '' : 'hide';
- }
-
- _loadingChanged() {
- this.classList.toggle('loading', this._loading);
- }
+ },
+ _usernameMutable: {
+ type: Boolean,
+ computed: '_computeUsernameMutable(_serverConfig, _account.username)',
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ observer: '_loadingChanged',
+ },
+ _saving: {
+ type: Boolean,
+ value: false,
+ },
+ _serverConfig: Object,
+ };
}
- customElements.define(GrRegistrationDialog.is, GrRegistrationDialog);
-})();
+ /** @override */
+ ready() {
+ super.ready();
+ this._ensureAttribute('role', 'dialog');
+ }
+
+ loadData() {
+ this._loading = true;
+
+ const loadAccount = this.$.restAPI.getAccount().then(account => {
+ // Using Object.assign here allows preservation of the default values
+ // supplied in the value generating function of this._account, unless
+ // they are overridden by properties in the account from the response.
+ this._account = Object.assign({}, this._account, account);
+ });
+
+ const loadConfig = this.$.restAPI.getConfig().then(config => {
+ this._serverConfig = config;
+ });
+
+ return Promise.all([loadAccount, loadConfig]).then(() => {
+ this._loading = false;
+ });
+ }
+
+ _save() {
+ this._saving = true;
+ const promises = [
+ this.$.restAPI.setAccountName(this.$.name.value),
+ this.$.restAPI.setPreferredAccountEmail(this.$.email.value || ''),
+ ];
+
+ if (this._usernameMutable) {
+ promises.push(this.$.restAPI.setAccountUsername(this.$.username.value));
+ }
+
+ return Promise.all(promises).then(() => {
+ this._saving = false;
+ this.fire('account-detail-update');
+ });
+ }
+
+ _handleSave(e) {
+ e.preventDefault();
+ this._save().then(this.close.bind(this));
+ }
+
+ _handleClose(e) {
+ e.preventDefault();
+ this.close();
+ }
+
+ close() {
+ this._saving = true; // disable buttons indefinitely
+ this.fire('close');
+ }
+
+ _computeSaveDisabled(name, email, saving) {
+ return !name || !email || saving;
+ }
+
+ _computeUsernameMutable(config, username) {
+ // Polymer 2: check for undefined
+ if ([
+ config,
+ username,
+ ].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ return config.auth.editable_account_fields.includes('USER_NAME') &&
+ !username;
+ }
+
+ _computeUsernameClass(usernameMutable) {
+ return usernameMutable ? '' : 'hide';
+ }
+
+ _loadingChanged() {
+ this.classList.toggle('loading', this._loading);
+ }
+}
+
+customElements.define(GrRegistrationDialog.is, GrRegistrationDialog);
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.js
new file mode 100644
index 0000000..737e6d5
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.js
@@ -0,0 +1,110 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="gr-form-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="shared-styles">
+ :host {
+ display: block;
+ }
+ main {
+ max-width: 46em;
+ }
+ :host(.loading) main {
+ display: none;
+ }
+ .loadingMessage {
+ display: none;
+ font-style: italic;
+ }
+ :host(.loading) .loadingMessage {
+ display: block;
+ }
+ hr {
+ margin-top: var(--spacing-l);
+ margin-bottom: var(--spacing-l);
+ }
+ header {
+ border-bottom: 1px solid var(--border-color);
+ font-weight: var(--font-weight-bold);
+ margin-bottom: var(--spacing-l);
+ }
+ .container {
+ padding: var(--spacing-m) var(--spacing-xl);
+ }
+ footer {
+ display: flex;
+ justify-content: flex-end;
+ }
+ footer gr-button {
+ margin-left: var(--spacing-l);
+ }
+ input {
+ width: 20em;
+ }
+ section.hide {
+ display: none;
+ }
+ </style>
+ <div class="container gr-form-styles">
+ <header>Please confirm your contact information</header>
+ <div class="loadingMessage">Loading...</div>
+ <main>
+ <p>
+ The following contact information was automatically obtained when you
+ signed in to the site. This information is used to display who you are
+ to others, and to send updates to code reviews you have either started
+ or subscribed to.
+ </p>
+ <hr>
+ <section>
+ <div class="title">Full Name</div>
+ <iron-input bind-value="{{_account.name}}">
+ <input is="iron-input" id="name" bind-value="{{_account.name}}" disabled="[[_saving]]">
+ </iron-input>
+ </section>
+ <section class\$="[[_computeUsernameClass(_usernameMutable)]]">
+ <div class="title">Username</div>
+ <iron-input bind-value="{{_account.username}}">
+ <input is="iron-input" id="username" bind-value="{{_account.username}}" disabled="[[_saving]]">
+ </iron-input>
+ </section>
+ <section>
+ <div class="title">Preferred Email</div>
+ <select id="email" disabled="[[_saving]]">
+ <option value="[[_account.email]]">[[_account.email]]</option>
+ <template is="dom-repeat" items="[[_account.secondary_emails]]">
+ <option value="[[item]]">[[item]]</option>
+ </template>
+ </select>
+ </section>
+ <hr>
+ <p>
+ More configuration options for Gerrit may be found in the
+ <a on-click="close" href\$="[[settingsUrl]]">settings</a>.
+ </p>
+ </main>
+ <footer>
+ <gr-button id="closeButton" link="" disabled="[[_saving]]" on-click="_handleClose">Close</gr-button>
+ <gr-button id="saveButton" primary="" link="" disabled="[[_computeSaveDisabled(_account.name, _account.email, _saving)]]" on-click="_handleSave">Save</gr-button>
+ </footer>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
index a3be75c..501dae5 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-registration-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-registration-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -41,149 +36,150 @@
</template>
</test-fixture>
-<script>
- suite('gr-registration-dialog tests', async () => {
- await readyToTest();
- let element;
- let account;
- let sandbox;
- let _listeners;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-registration-dialog.js';
+suite('gr-registration-dialog tests', () => {
+ let element;
+ let account;
+ let sandbox;
+ let _listeners;
- setup(() => {
- sandbox = sinon.sandbox.create();
- _listeners = {};
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ _listeners = {};
- account = {
- name: 'name',
- username: null,
- email: 'email',
- secondary_emails: [
- 'email2',
- 'email3',
- ],
- };
+ account = {
+ name: 'name',
+ username: null,
+ email: 'email',
+ secondary_emails: [
+ 'email2',
+ 'email3',
+ ],
+ };
- stub('gr-rest-api-interface', {
- getAccount() {
- return Promise.resolve(account);
- },
- setAccountName(name) {
- account.name = name;
- return Promise.resolve();
- },
- setAccountUsername(username) {
- account.username = username;
- return Promise.resolve();
- },
- setPreferredAccountEmail(email) {
- account.email = email;
- return Promise.resolve();
- },
- getConfig() {
- return Promise.resolve(
- {auth: {editable_account_fields: ['USER_NAME']}});
- },
- });
-
- element = fixture('basic');
-
- return element.loadData();
+ stub('gr-rest-api-interface', {
+ getAccount() {
+ return Promise.resolve(account);
+ },
+ setAccountName(name) {
+ account.name = name;
+ return Promise.resolve();
+ },
+ setAccountUsername(username) {
+ account.username = username;
+ return Promise.resolve();
+ },
+ setPreferredAccountEmail(email) {
+ account.email = email;
+ return Promise.resolve();
+ },
+ getConfig() {
+ return Promise.resolve(
+ {auth: {editable_account_fields: ['USER_NAME']}});
+ },
});
- teardown(() => {
- sandbox.restore();
- for (const eventType in _listeners) {
- if (_listeners.hasOwnProperty(eventType)) {
- element.removeEventListener(eventType, _listeners[eventType]);
- }
+ element = fixture('basic');
+
+ return element.loadData();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ for (const eventType in _listeners) {
+ if (_listeners.hasOwnProperty(eventType)) {
+ element.removeEventListener(eventType, _listeners[eventType]);
}
- });
-
- function listen(eventType) {
- return new Promise(resolve => {
- _listeners[eventType] = function() { resolve(); };
- element.addEventListener(eventType, _listeners[eventType]);
- });
}
+ });
- function save(opt_action) {
- const promise = listen('account-detail-update');
- if (opt_action) {
- opt_action();
- } else {
- MockInteractions.tap(element.$.saveButton);
- }
- return promise;
+ function listen(eventType) {
+ return new Promise(resolve => {
+ _listeners[eventType] = function() { resolve(); };
+ element.addEventListener(eventType, _listeners[eventType]);
+ });
+ }
+
+ function save(opt_action) {
+ const promise = listen('account-detail-update');
+ if (opt_action) {
+ opt_action();
+ } else {
+ MockInteractions.tap(element.$.saveButton);
}
+ return promise;
+ }
- function close(opt_action) {
- const promise = listen('close');
- if (opt_action) {
- opt_action();
- } else {
- MockInteractions.tap(element.$.closeButton);
- }
- return promise;
+ function close(opt_action) {
+ const promise = listen('close');
+ if (opt_action) {
+ opt_action();
+ } else {
+ MockInteractions.tap(element.$.closeButton);
}
+ return promise;
+ }
- test('fires the close event on close', done => {
- close().then(done);
- });
+ test('fires the close event on close', done => {
+ close().then(done);
+ });
- test('fires the close event on save', done => {
- close(() => {
- MockInteractions.tap(element.$.saveButton);
- }).then(done);
- });
+ test('fires the close event on save', done => {
+ close(() => {
+ MockInteractions.tap(element.$.saveButton);
+ }).then(done);
+ });
- test('saves account details', done => {
- flush(() => {
- element.$.name.value = 'new name';
- element.$.username.value = 'new username';
- element.$.email.value = 'email3';
+ test('saves account details', done => {
+ flush(() => {
+ element.$.name.value = 'new name';
+ element.$.username.value = 'new username';
+ element.$.email.value = 'email3';
- // Nothing should be committed yet.
- assert.equal(account.name, 'name');
- assert.isNotOk(account.username);
- assert.equal(account.email, 'email');
+ // Nothing should be committed yet.
+ assert.equal(account.name, 'name');
+ assert.isNotOk(account.username);
+ assert.equal(account.email, 'email');
- // Save and verify new values are committed.
- save()
- .then(() => {
- assert.equal(account.name, 'new name');
- assert.equal(account.username, 'new username');
- assert.equal(account.email, 'email3');
- })
- .then(done);
- });
- });
-
- test('email select properly populated', done => {
- element._account = {email: 'foo', secondary_emails: ['bar', 'baz']};
- flush(() => {
- assert.equal(element.$.email.value, 'foo');
- done();
- });
- });
-
- test('save btn disabled', () => {
- const compute = element._computeSaveDisabled;
- assert.isTrue(compute('', '', false));
- assert.isTrue(compute('', 'test', false));
- assert.isTrue(compute('test', '', false));
- assert.isTrue(compute('test', 'test', true));
- assert.isFalse(compute('test', 'test', false));
- });
-
- test('_computeUsernameMutable', () => {
- assert.isTrue(element._computeUsernameMutable(
- {auth: {editable_account_fields: ['USER_NAME']}}, null));
- assert.isFalse(element._computeUsernameMutable(
- {auth: {editable_account_fields: ['USER_NAME']}}, 'abc'));
- assert.isFalse(element._computeUsernameMutable(
- {auth: {editable_account_fields: []}}, null));
- assert.isFalse(element._computeUsernameMutable(
- {auth: {editable_account_fields: []}}, 'abc'));
+ // Save and verify new values are committed.
+ save()
+ .then(() => {
+ assert.equal(account.name, 'new name');
+ assert.equal(account.username, 'new username');
+ assert.equal(account.email, 'email3');
+ })
+ .then(done);
});
});
+
+ test('email select properly populated', done => {
+ element._account = {email: 'foo', secondary_emails: ['bar', 'baz']};
+ flush(() => {
+ assert.equal(element.$.email.value, 'foo');
+ done();
+ });
+ });
+
+ test('save btn disabled', () => {
+ const compute = element._computeSaveDisabled;
+ assert.isTrue(compute('', '', false));
+ assert.isTrue(compute('', 'test', false));
+ assert.isTrue(compute('test', '', false));
+ assert.isTrue(compute('test', 'test', true));
+ assert.isFalse(compute('test', 'test', false));
+ });
+
+ test('_computeUsernameMutable', () => {
+ assert.isTrue(element._computeUsernameMutable(
+ {auth: {editable_account_fields: ['USER_NAME']}}, null));
+ assert.isFalse(element._computeUsernameMutable(
+ {auth: {editable_account_fields: ['USER_NAME']}}, 'abc'));
+ assert.isFalse(element._computeUsernameMutable(
+ {auth: {editable_account_fields: []}}, null));
+ assert.isFalse(element._computeUsernameMutable(
+ {auth: {editable_account_fields: []}}, 'abc'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.html
deleted file mode 100644
index 937ee79..0000000
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<dom-module id="gr-settings-item">
- <template>
- <style>
- :host {
- display: block;
- margin-bottom: var(--spacing-xxl);
- }
- </style>
- <h2 id="[[anchor]]">[[title]]</h2>
- <slot></slot>
- </template>
- <script src="gr-settings-item.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
index bae1f38..3884a15 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
@@ -14,22 +14,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-settings-item_html.js';
- /** @extends Polymer.Element */
- class GrSettingsItem extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-settings-item'; }
+/** @extends Polymer.Element */
+class GrSettingsItem extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- static get properties() {
- return {
- anchor: String,
- title: String,
- };
- }
+ static get is() { return 'gr-settings-item'; }
+
+ static get properties() {
+ return {
+ anchor: String,
+ title: String,
+ };
}
+}
- customElements.define(GrSettingsItem.is, GrSettingsItem);
-})();
+customElements.define(GrSettingsItem.is, GrSettingsItem);
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js
new file mode 100644
index 0000000..accb8c8
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js
@@ -0,0 +1,28 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style>
+ :host {
+ display: block;
+ margin-bottom: var(--spacing-xxl);
+ }
+ </style>
+ <h2 id="[[anchor]]">[[title]]</h2>
+ <slot></slot>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html
deleted file mode 100644
index c356e80..0000000
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/gr-page-nav-styles.html">
-
-<dom-module id="gr-settings-menu-item">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-page-nav-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <div class="navStyles">
- <li><a href$="[[href]]">[[title]]</a></li>
- </div>
- </template>
- <script src="gr-settings-menu-item.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
index d5a7eb7..5b11516 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
@@ -14,22 +14,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrSettingsMenuItem extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-settings-menu-item'; }
+import '../../../styles/gr-page-nav-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-settings-menu-item_html.js';
- static get properties() {
- return {
- href: String,
- title: String,
- };
- }
+/** @extends Polymer.Element */
+class GrSettingsMenuItem extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-settings-menu-item'; }
+
+ static get properties() {
+ return {
+ href: String,
+ title: String,
+ };
}
+}
- customElements.define(GrSettingsMenuItem.is, GrSettingsMenuItem);
-})();
+customElements.define(GrSettingsMenuItem.is, GrSettingsMenuItem);
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item_html.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item_html.js
new file mode 100644
index 0000000..5cb129f
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item_html.js
@@ -0,0 +1,29 @@
+/**
+ * @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.js';
+
+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-page-nav-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <div class="navStyles">
+ <li><a href\$="[[href]]">[[title]]</a></li>
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
deleted file mode 100644
index e71aef5..0000000
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ /dev/null
@@ -1,522 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-
-<link rel="import" href="../../../behaviors/docs-url-behavior/docs-url-behavior.html">
-<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
-
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/gr-menu-page-styles.html">
-<link rel="import" href="../../../styles/gr-page-nav-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../settings/gr-change-table-editor/gr-change-table-editor.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-diff-preferences/gr-diff-preferences.html">
-<link rel="import" href="../../shared/gr-page-nav/gr-page-nav.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-<link rel="import" href="../gr-account-info/gr-account-info.html">
-<link rel="import" href="../gr-agreements-list/gr-agreements-list.html">
-<link rel="import" href="../gr-edit-preferences/gr-edit-preferences.html">
-<link rel="import" href="../gr-email-editor/gr-email-editor.html">
-<link rel="import" href="../gr-gpg-editor/gr-gpg-editor.html">
-<link rel="import" href="../gr-group-list/gr-group-list.html">
-<link rel="import" href="../gr-http-password/gr-http-password.html">
-<link rel="import" href="../gr-identities/gr-identities.html">
-<link rel="import" href="../gr-menu-editor/gr-menu-editor.html">
-<link rel="import" href="../gr-ssh-editor/gr-ssh-editor.html">
-<link rel="import" href="../gr-watched-projects-editor/gr-watched-projects-editor.html">
-
-<dom-module id="gr-settings-view">
- <template>
- <style include="shared-styles">
- :host {
- color: var(--primary-text-color);
- }
- .newEmailInput {
- width: 20em;
- }
- #email {
- margin-bottom: var(--spacing-l);
- }
- main section.darkToggle {
- display: block;
- }
- .filters p,
- .darkToggle p {
- margin-bottom: var(--spacing-l);
- }
- .queryExample em {
- color: violet;
- }
- .toggle {
- align-items: center;
- display: flex;
- margin-bottom: var(--spacing-l);
- margin-right: var(--spacing-l);
- }
- </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>
- <style include="gr-page-nav-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <div class="loading" hidden$="[[!_loading]]">Loading...</div>
- <div hidden$="[[_loading]]" hidden>
- <gr-page-nav class="navStyles">
- <ul>
- <li><a href="#Profile">Profile</a></li>
- <li><a href="#Preferences">Preferences</a></li>
- <li><a href="#DiffPreferences">Diff Preferences</a></li>
- <li><a href="#EditPreferences">Edit Preferences</a></li>
- <li><a href="#Menu">Menu</a></li>
- <li><a href="#ChangeTableColumns">Change Table Columns</a></li>
- <li><a href="#Notifications">Notifications</a></li>
- <li><a href="#EmailAddresses">Email Addresses</a></li>
- <template is="dom-if" if="[[_showHttpAuth(_serverConfig)]]">
- <li><a href="#HTTPCredentials">HTTP Credentials</a></li>
- </template>
- <li hidden$="[[!_serverConfig.sshd]]"><a href="#SSHKeys">
- SSH Keys
- </a></li>
- <li hidden$="[[!_serverConfig.receive.enable_signed_push]]"><a href="#GPGKeys">
- GPG Keys
- </a></li>
- <li><a href="#Groups">Groups</a></li>
- <li><a href="#Identities">Identities</a></li>
- <template is="dom-if" if="[[_serverConfig.auth.use_contributor_agreements]]">
- <li>
- <a href="#Agreements">Agreements</a>
- </li>
- </template>
- <li><a href="#MailFilters">Mail Filters</a></li>
- <gr-endpoint-decorator name="settings-menu-item">
- </gr-endpoint-decorator>
- </ul>
- </gr-page-nav>
- <main class="gr-form-styles">
- <h1>User Settings</h1>
- <section class="darkToggle">
- <div class="toggle">
- <paper-toggle-button
- checked="[[_isDark]]"
- on-change="_handleToggleDark"></paper-toggle-button>
- <div>Dark theme (alpha)</div>
- </div>
- <p>
- Gerrit's dark theme is in early alpha, and almost definitely will
- not play nicely with themes set by specific Gerrit hosts. Filing
- feedback via the link in the app footer is strongly encouraged!
- </p>
- </section>
- <h2
- id="Profile"
- class$="[[_computeHeaderClass(_accountInfoChanged)]]">Profile</h2>
- <fieldset id="profile">
- <gr-account-info
- id="accountInfo"
- has-unsaved-changes="{{_accountInfoChanged}}"></gr-account-info>
- <gr-button
- on-click="_handleSaveAccountInfo"
- disabled="[[!_accountInfoChanged]]">Save changes</gr-button>
- </fieldset>
- <h2
- id="Preferences"
- class$="[[_computeHeaderClass(_prefsChanged)]]">Preferences</h2>
- <fieldset id="preferences">
- <section>
- <span class="title">Changes per page</span>
- <span class="value">
- <gr-select
- bind-value="{{_localPrefs.changes_per_page}}">
- <select>
- <option value="10">10 rows per page</option>
- <option value="25">25 rows per page</option>
- <option value="50">50 rows per page</option>
- <option value="100">100 rows per page</option>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Date/time format</span>
- <span class="value">
- <gr-select
- bind-value="{{_localPrefs.date_format}}">
- <select>
- <option value="STD">Jun 3 ; Jun 3, 2016</option>
- <option value="US">06/03 ; 06/03/16</option>
- <option value="ISO">06-03 ; 2016-06-03</option>
- <option value="EURO">3. Jun ; 03.06.2016</option>
- <option value="UK">03/06 ; 03/06/2016</option>
- </select>
- </gr-select>
- <gr-select
- bind-value="{{_localPrefs.time_format}}">
- <select>
- <option value="HHMM_12">4:10 PM</option>
- <option value="HHMM_24">16:10</option>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Email notifications</span>
- <span class="value">
- <gr-select
- bind-value="{{_localPrefs.email_strategy}}">
- <select>
- <option value="CC_ON_OWN_COMMENTS">Every comment</option>
- <option value="ENABLED">Only comments left by others</option>
- <option value="DISABLED">None</option>
- </select>
- </gr-select>
- </span>
- </section>
- <section hidden$="[[!_localPrefs.email_format]]">
- <span class="title">Email format</span>
- <span class="value">
- <gr-select
- bind-value="{{_localPrefs.email_format}}">
- <select>
- <option value="HTML_PLAINTEXT">HTML and plaintext</option>
- <option value="PLAINTEXT">Plaintext only</option>
- </select>
- </gr-select>
- </span>
- </section>
- <section hidden$="[[!_localPrefs.default_base_for_merges]]">
- <span class="title">Default Base For Merges</span>
- <span class="value">
- <gr-select
- bind-value="{{_localPrefs.default_base_for_merges}}">
- <select>
- <option value="AUTO_MERGE">Auto Merge</option>
- <option value="FIRST_PARENT">First Parent</option>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Show Relative Dates In Changes Table</span>
- <span class="value">
- <input
- id="relativeDateInChangeTable"
- type="checkbox"
- checked$="[[_localPrefs.relative_date_in_change_table]]"
- on-change="_handleRelativeDateInChangeTable">
- </span>
- </section>
- <section>
- <span class="title">Diff view</span>
- <span class="value">
- <gr-select
- bind-value="{{_localPrefs.diff_view}}">
- <select>
- <option value="SIDE_BY_SIDE">Side by side</option>
- <option value="UNIFIED_DIFF">Unified diff</option>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Show size bars in file list</span>
- <span class="value">
- <input
- id="showSizeBarsInFileList"
- type="checkbox"
- checked$="[[_localPrefs.size_bar_in_change_table]]"
- on-change="_handleShowSizeBarsInFileListChanged">
- </span>
- </section>
- <section>
- <span class="title">Publish comments on push</span>
- <span class="value">
- <input
- id="publishCommentsOnPush"
- type="checkbox"
- checked$="[[_localPrefs.publish_comments_on_push]]"
- on-change="_handlePublishCommentsOnPushChanged">
- </span>
- </section>
- <section>
- <span class="title">Set new changes to "work in progress" by default</span>
- <span class="value">
- <input
- id="workInProgressByDefault"
- type="checkbox"
- checked$="[[_localPrefs.work_in_progress_by_default]]"
- on-change="_handleWorkInProgressByDefault">
- </span>
- </section>
- <section>
- <span class="title">
- Insert Signed-off-by Footer For Inline Edit Changes
- </span>
- <span class="value">
- <input
- id="insertSignedOff"
- type="checkbox"
- checked$="[[_localPrefs.signed_off_by]]"
- on-change="_handleInsertSignedOff">
- </span>
- </section>
- <gr-button
- id="savePrefs"
- on-click="_handleSavePreferences"
- disabled="[[!_prefsChanged]]">Save changes</gr-button>
- </fieldset>
- <h2
- id="DiffPreferences"
- class$="[[_computeHeaderClass(_diffPrefsChanged)]]">
- Diff Preferences
- </h2>
- <fieldset id="diffPreferences">
- <gr-diff-preferences
- id="diffPrefs"
- has-unsaved-changes="{{_diffPrefsChanged}}"></gr-diff-preferences>
- <gr-button
- id="saveDiffPrefs"
- on-click="_handleSaveDiffPreferences"
- disabled$="[[!_diffPrefsChanged]]">Save changes</gr-button>
- </fieldset>
- <h2
- id="EditPreferences"
- class$="[[_computeHeaderClass(_editPrefsChanged)]]">
- Edit Preferences
- </h2>
- <fieldset id="editPreferences">
- <gr-edit-preferences
- id="editPrefs"
- has-unsaved-changes="{{_editPrefsChanged}}"></gr-edit-preferences>
- <gr-button
- id="saveEditPrefs"
- on-click="_handleSaveEditPreferences"
- disabled$="[[!_editPrefsChanged]]">Save changes</gr-button>
- </fieldset>
- <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="resetMenu"
- link
- on-click="_handleResetMenuButton">Reset</gr-button>
- </fieldset>
- <h2 id="ChangeTableColumns"
- class$="[[_computeHeaderClass(_changeTableChanged)]]">
- Change Table Columns
- </h2>
- <fieldset id="changeTableColumns">
- <gr-change-table-editor
- show-number="{{_showNumber}}"
- displayed-columns="{{_localChangeTableColumns}}">
- </gr-change-table-editor>
- <gr-button
- id="saveChangeTable"
- on-click="_handleSaveChangeTable"
- disabled="[[!_changeTableChanged]]">Save changes</gr-button>
- </fieldset>
- <h2
- id="Notifications"
- class$="[[_computeHeaderClass(_watchedProjectsChanged)]]">
- Notifications
- </h2>
- <fieldset id="watchedProjects">
- <gr-watched-projects-editor
- has-unsaved-changes="{{_watchedProjectsChanged}}"
- id="watchedProjectsEditor"></gr-watched-projects-editor>
- <gr-button
- on-click="_handleSaveWatchedProjects"
- disabled$="[[!_watchedProjectsChanged]]"
- id="_handleSaveWatchedProjects">Save changes</gr-button>
- </fieldset>
- <h2
- id="EmailAddresses"
- class$="[[_computeHeaderClass(_emailsChanged)]]">
- Email Addresses
- </h2>
- <fieldset id="email">
- <gr-email-editor
- id="emailEditor"
- has-unsaved-changes="{{_emailsChanged}}"></gr-email-editor>
- <gr-button
- on-click="_handleSaveEmails"
- disabled$="[[!_emailsChanged]]">Save changes</gr-button>
- </fieldset>
- <fieldset id="newEmail">
- <section>
- <span class="title">New email address</span>
- <span class="value">
- <iron-input
- class="newEmailInput"
- bind-value="{{_newEmail}}"
- type="text"
- on-keydown="_handleNewEmailKeydown"
- placeholder="email@example.com">
- <input
- class="newEmailInput"
- bind-value="{{_newEmail}}"
- is="iron-input"
- type="text"
- disabled="[[_addingEmail]]"
- on-keydown="_handleNewEmailKeydown"
- placeholder="email@example.com">
- </iron-input>
- </span>
- </section>
- <section
- id="verificationSentMessage"
- hidden$="[[!_lastSentVerificationEmail]]">
- <p>
- A verification email was sent to
- <em>[[_lastSentVerificationEmail]]</em>. Please check your inbox.
- </p>
- </section>
- <gr-button
- disabled="[[!_computeAddEmailButtonEnabled(_newEmail, _addingEmail)]]"
- on-click="_handleAddEmailButton">Send verification</gr-button>
- </fieldset>
- <template is="dom-if" if="[[_showHttpAuth(_serverConfig)]]">
- <div>
- <h2 id="HTTPCredentials">HTTP Credentials</h2>
- <fieldset>
- <gr-http-password id="httpPass"></gr-http-password>
- </fieldset>
- </div>
- </template>
- <div hidden$="[[!_serverConfig.sshd]]">
- <h2
- id="SSHKeys"
- class$="[[_computeHeaderClass(_keysChanged)]]">SSH keys</h2>
- <gr-ssh-editor
- id="sshEditor"
- has-unsaved-changes="{{_keysChanged}}"></gr-ssh-editor>
- </div>
- <div hidden$="[[!_serverConfig.receive.enable_signed_push]]">
- <h2
- id="GPGKeys"
- class$="[[_computeHeaderClass(_gpgKeysChanged)]]">GPG keys</h2>
- <gr-gpg-editor
- id="gpgEditor"
- has-unsaved-changes="{{_gpgKeysChanged}}"></gr-gpg-editor>
- </div>
- <h2 id="Groups">Groups</h2>
- <fieldset>
- <gr-group-list id="groupList"></gr-group-list>
- </fieldset>
- <h2 id="Identities">Identities</h2>
- <fieldset>
- <gr-identities id="identities" server-config="[[_serverConfig]]"></gr-identities>
- </fieldset>
- <template is="dom-if" if="[[_serverConfig.auth.use_contributor_agreements]]">
- <h2 id="Agreements">Agreements</h2>
- <fieldset>
- <gr-agreements-list id="agreementsList"></gr-agreements-list>
- </fieldset>
- </template>
- <h2 id="MailFilters">Mail Filters</h2>
- <fieldset class="filters">
- <p>
- Gerrit emails include metadata about the change to support
- writing mail filters.
- </p>
- <p>
- Here are some example Gmail queries that can be used for filters or
- for searching through archived messages. View the
- <a href$="[[_getFilterDocsLink(_docsBaseUrl)]]"
- target="_blank"
- rel="nofollow">Gerrit documentation</a>
- for the complete set of footers.
- </p>
- <table>
- <tbody>
- <tr><th>Name</th><th>Query</th></tr>
- <tr>
- <td>Changes requesting my review</td>
- <td>
- <code class="queryExample">
- "Gerrit-Reviewer: <em>Your Name</em>
- <<em>your.email@example.com</em>>"
- </code>
- </td>
- </tr>
- <tr>
- <td>Changes from a specific owner</td>
- <td>
- <code class="queryExample">
- "Gerrit-Owner: <em>Owner name</em>
- <<em>owner.email@example.com</em>>"
- </code>
- </td>
- </tr>
- <tr>
- <td>Changes targeting a specific branch</td>
- <td>
- <code class="queryExample">
- "Gerrit-Branch: <em>branch-name</em>"
- </code>
- </td>
- </tr>
- <tr>
- <td>Changes in a specific project</td>
- <td>
- <code class="queryExample">
- "Gerrit-Project: <em>project-name</em>"
- </code>
- </td>
- </tr>
- <tr>
- <td>Messages related to a specific Change ID</td>
- <td>
- <code class="queryExample">
- "Gerrit-Change-Id: <em>Change ID</em>"
- </code>
- </td>
- </tr>
- <tr>
- <td>Messages related to a specific change number</td>
- <td>
- <code class="queryExample">
- "Gerrit-Change-Number: <em>change number</em>"
- </code>
- </td>
- </tr>
- </tbody>
- </table>
- </fieldset>
- <gr-endpoint-decorator name="settings-screen">
- </gr-endpoint-decorator>
- </main>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="../../../scripts/util.js"></script>
- <script src="gr-settings-view.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 78bad8c..733fa56 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -14,456 +14,489 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const PREFS_SECTION_FIELDS = [
- 'changes_per_page',
- 'date_format',
- 'time_format',
- 'email_strategy',
- 'diff_view',
- 'publish_comments_on_push',
- 'work_in_progress_by_default',
- 'default_base_for_merges',
- 'signed_off_by',
- 'email_format',
- 'size_bar_in_change_table',
- 'relative_date_in_change_table',
- ];
+import '@polymer/iron-input/iron-input.js';
+import '../../../behaviors/docs-url-behavior/docs-url-behavior.js';
+import '@polymer/paper-toggle-button/paper-toggle-button.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/gr-menu-page-styles.js';
+import '../../../styles/gr-page-nav-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../gr-change-table-editor/gr-change-table-editor.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-diff-preferences/gr-diff-preferences.js';
+import '../../shared/gr-page-nav/gr-page-nav.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.js';
+import '../gr-account-info/gr-account-info.js';
+import '../gr-agreements-list/gr-agreements-list.js';
+import '../gr-edit-preferences/gr-edit-preferences.js';
+import '../gr-email-editor/gr-email-editor.js';
+import '../gr-gpg-editor/gr-gpg-editor.js';
+import '../gr-group-list/gr-group-list.js';
+import '../gr-http-password/gr-http-password.js';
+import '../gr-identities/gr-identities.js';
+import '../gr-menu-editor/gr-menu-editor.js';
+import '../gr-ssh-editor/gr-ssh-editor.js';
+import '../gr-watched-projects-editor/gr-watched-projects-editor.js';
+import '../../../scripts/util.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-settings-view_html.js';
- const GERRIT_DOCS_BASE_URL = 'https://gerrit-review.googlesource.com/' +
- 'Documentation';
- const GERRIT_DOCS_FILTER_PATH = '/user-notify.html';
- const ABSOLUTE_URL_PATTERN = /^https?:/;
- const TRAILING_SLASH_PATTERN = /\/$/;
+const PREFS_SECTION_FIELDS = [
+ 'changes_per_page',
+ 'date_format',
+ 'time_format',
+ 'email_strategy',
+ 'diff_view',
+ 'publish_comments_on_push',
+ 'work_in_progress_by_default',
+ 'default_base_for_merges',
+ 'signed_off_by',
+ 'email_format',
+ 'size_bar_in_change_table',
+ 'relative_date_in_change_table',
+];
- const RELOAD_MESSAGE = 'Reloading...';
+const GERRIT_DOCS_BASE_URL = 'https://gerrit-review.googlesource.com/' +
+ 'Documentation';
+const GERRIT_DOCS_FILTER_PATH = '/user-notify.html';
+const ABSOLUTE_URL_PATTERN = /^https?:/;
+const TRAILING_SLASH_PATTERN = /\/$/;
- const HTTP_AUTH = [
- 'HTTP',
- 'HTTP_LDAP',
- ];
+const RELOAD_MESSAGE = 'Reloading...';
+
+const HTTP_AUTH = [
+ 'HTTP',
+ 'HTTP_LDAP',
+];
+
+/**
+ * @appliesMixin Gerrit.DocsUrlMixin
+ * @appliesMixin Gerrit.ChangeTableMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrSettingsView extends mixinBehaviors( [
+ Gerrit.DocsUrlBehavior,
+ Gerrit.ChangeTableBehavior,
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-settings-view'; }
+ /**
+ * Fired when the title of the page should change.
+ *
+ * @event title-change
+ */
/**
- * @appliesMixin Gerrit.DocsUrlMixin
- * @appliesMixin Gerrit.ChangeTableMixin
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired with email confirmation text, or when the page reloads.
+ *
+ * @event show-alert
*/
- class GrSettingsView extends Polymer.mixinBehaviors( [
- Gerrit.DocsUrlBehavior,
- Gerrit.ChangeTableBehavior,
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-settings-view'; }
- /**
- * Fired when the title of the page should change.
- *
- * @event title-change
- */
- /**
- * Fired with email confirmation text, or when the page reloads.
- *
- * @event show-alert
- */
+ static get properties() {
+ return {
+ prefs: {
+ type: Object,
+ value() { return {}; },
+ },
+ params: {
+ type: Object,
+ value() { return {}; },
+ },
+ _accountInfoChanged: Boolean,
+ _changeTableColumnsNotDisplayed: Array,
+ /** @type {?} */
+ _localPrefs: {
+ type: Object,
+ value() { return {}; },
+ },
+ _localChangeTableColumns: {
+ type: Array,
+ value() { return []; },
+ },
+ _localMenu: {
+ type: Array,
+ value() { return []; },
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _changeTableChanged: {
+ type: Boolean,
+ value: false,
+ },
+ _prefsChanged: {
+ type: Boolean,
+ value: false,
+ },
+ /** @type {?} */
+ _diffPrefsChanged: Boolean,
+ /** @type {?} */
+ _editPrefsChanged: Boolean,
+ _menuChanged: {
+ type: Boolean,
+ value: false,
+ },
+ _watchedProjectsChanged: {
+ type: Boolean,
+ value: false,
+ },
+ _keysChanged: {
+ type: Boolean,
+ value: false,
+ },
+ _gpgKeysChanged: {
+ type: Boolean,
+ value: false,
+ },
+ _newEmail: String,
+ _addingEmail: {
+ type: Boolean,
+ value: false,
+ },
+ _lastSentVerificationEmail: {
+ type: String,
+ value: null,
+ },
+ /** @type {?} */
+ _serverConfig: Object,
+ /** @type {?string} */
+ _docsBaseUrl: String,
+ _emailsChanged: Boolean,
- static get properties() {
- return {
- prefs: {
- type: Object,
- value() { return {}; },
- },
- params: {
- type: Object,
- value() { return {}; },
- },
- _accountInfoChanged: Boolean,
- _changeTableColumnsNotDisplayed: Array,
- /** @type {?} */
- _localPrefs: {
- type: Object,
- value() { return {}; },
- },
- _localChangeTableColumns: {
- type: Array,
- value() { return []; },
- },
- _localMenu: {
- type: Array,
- value() { return []; },
- },
- _loading: {
- type: Boolean,
- value: true,
- },
- _changeTableChanged: {
- type: Boolean,
- value: false,
- },
- _prefsChanged: {
- type: Boolean,
- value: false,
- },
- /** @type {?} */
- _diffPrefsChanged: Boolean,
- /** @type {?} */
- _editPrefsChanged: Boolean,
- _menuChanged: {
- type: Boolean,
- value: false,
- },
- _watchedProjectsChanged: {
- type: Boolean,
- value: false,
- },
- _keysChanged: {
- type: Boolean,
- value: false,
- },
- _gpgKeysChanged: {
- type: Boolean,
- value: false,
- },
- _newEmail: String,
- _addingEmail: {
- type: Boolean,
- value: false,
- },
- _lastSentVerificationEmail: {
- type: String,
- value: null,
- },
- /** @type {?} */
- _serverConfig: Object,
- /** @type {?string} */
- _docsBaseUrl: String,
- _emailsChanged: Boolean,
+ /**
+ * For testing purposes.
+ */
+ _loadingPromise: Object,
- /**
- * For testing purposes.
- */
- _loadingPromise: Object,
+ _showNumber: Boolean,
- _showNumber: Boolean,
+ _isDark: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
- _isDark: {
- type: Boolean,
- value: false,
- },
- };
- }
+ static get observers() {
+ return [
+ '_handlePrefsChanged(_localPrefs.*)',
+ '_handleMenuChanged(_localMenu.splices)',
+ '_handleChangeTableChanged(_localChangeTableColumns, _showNumber)',
+ ];
+ }
- static get observers() {
- return [
- '_handlePrefsChanged(_localPrefs.*)',
- '_handleMenuChanged(_localMenu.splices)',
- '_handleChangeTableChanged(_localChangeTableColumns, _showNumber)',
- ];
- }
+ /** @override */
+ attached() {
+ super.attached();
+ // Polymer 2: anchor tag won't work on shadow DOM
+ // we need to manually calling scrollIntoView when hash changed
+ this.listen(window, 'location-change', '_handleLocationChange');
+ this.fire('title-change', {title: 'Settings'});
- /** @override */
- attached() {
- super.attached();
- // Polymer 2: anchor tag won't work on shadow DOM
- // we need to manually calling scrollIntoView when hash changed
- this.listen(window, 'location-change', '_handleLocationChange');
- this.fire('title-change', {title: 'Settings'});
+ this._isDark = !!window.localStorage.getItem('dark-theme');
- this._isDark = !!window.localStorage.getItem('dark-theme');
+ const promises = [
+ this.$.accountInfo.loadData(),
+ this.$.watchedProjectsEditor.loadData(),
+ this.$.groupList.loadData(),
+ this.$.identities.loadData(),
+ this.$.editPrefs.loadData(),
+ this.$.diffPrefs.loadData(),
+ ];
- const promises = [
- this.$.accountInfo.loadData(),
- this.$.watchedProjectsEditor.loadData(),
- this.$.groupList.loadData(),
- this.$.identities.loadData(),
- this.$.editPrefs.loadData(),
- this.$.diffPrefs.loadData(),
- ];
-
- promises.push(this.$.restAPI.getPreferences().then(prefs => {
- this.prefs = prefs;
- this._showNumber = !!prefs.legacycid_in_change_table;
- this._copyPrefs('_localPrefs', 'prefs');
- this._cloneMenu(prefs.my);
- this._cloneChangeTableColumns();
- }));
-
- promises.push(this.$.restAPI.getConfig().then(config => {
- this._serverConfig = config;
- const configPromises = [];
-
- if (this._serverConfig && this._serverConfig.sshd) {
- configPromises.push(this.$.sshEditor.loadData());
- }
-
- if (this._serverConfig &&
- this._serverConfig.receive &&
- this._serverConfig.receive.enable_signed_push) {
- configPromises.push(this.$.gpgEditor.loadData());
- }
-
- configPromises.push(
- this.getDocsBaseUrl(config, this.$.restAPI)
- .then(baseUrl => { this._docsBaseUrl = baseUrl; }));
-
- return Promise.all(configPromises);
- }));
-
- if (this.params.emailToken) {
- promises.push(this.$.restAPI.confirmEmail(this.params.emailToken).then(
- message => {
- if (message) {
- this.fire('show-alert', {message});
- }
- this.$.emailEditor.loadData();
- }));
- } else {
- promises.push(this.$.emailEditor.loadData());
- }
-
- this._loadingPromise = Promise.all(promises).then(() => {
- this._loading = false;
-
- // Handle anchor tag for initial load
- this._handleLocationChange();
- });
- }
-
- /** @override */
- detached() {
- super.detached();
- this.unlisten(window, 'location-change', '_handleLocationChange');
- }
-
- _handleLocationChange() {
- // Handle anchor tag after dom attached
- const urlHash = window.location.hash;
- if (urlHash) {
- // Use shadowRoot for Polymer 2
- const elem = (this.shadowRoot || document).querySelector(urlHash);
- if (elem) {
- elem.scrollIntoView();
- }
- }
- }
-
- reloadAccountDetail() {
- Promise.all([
- this.$.accountInfo.loadData(),
- this.$.emailEditor.loadData(),
- ]);
- }
-
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _copyPrefs(to, from) {
- for (let i = 0; i < PREFS_SECTION_FIELDS.length; i++) {
- this.set([to, PREFS_SECTION_FIELDS[i]],
- this[from][PREFS_SECTION_FIELDS[i]]);
- }
- }
-
- _cloneMenu(prefs) {
- const menu = [];
- for (const item of prefs) {
- menu.push({
- name: item.name,
- url: item.url,
- target: item.target,
- });
- }
- this._localMenu = menu;
- }
-
- _cloneChangeTableColumns() {
- let columns = this.getVisibleColumns(this.prefs.change_table);
-
- if (columns.length === 0) {
- columns = this.columnNames;
- this._changeTableColumnsNotDisplayed = [];
- } else {
- this._changeTableColumnsNotDisplayed = this.getComplementColumns(
- this.prefs.change_table);
- }
- this._localChangeTableColumns = columns;
- }
-
- _formatChangeTableColumns(changeTableArray) {
- return changeTableArray.map(item => {
- return {column: item};
- });
- }
-
- _handleChangeTableChanged() {
- if (this._isLoading()) { return; }
- this._changeTableChanged = true;
- }
-
- _handlePrefsChanged(prefs) {
- if (this._isLoading()) { return; }
- this._prefsChanged = true;
- }
-
- _handleRelativeDateInChangeTable() {
- this.set('_localPrefs.relative_date_in_change_table',
- this.$.relativeDateInChangeTable.checked);
- }
-
- _handleShowSizeBarsInFileListChanged() {
- this.set('_localPrefs.size_bar_in_change_table',
- this.$.showSizeBarsInFileList.checked);
- }
-
- _handlePublishCommentsOnPushChanged() {
- this.set('_localPrefs.publish_comments_on_push',
- this.$.publishCommentsOnPush.checked);
- }
-
- _handleWorkInProgressByDefault() {
- this.set('_localPrefs.work_in_progress_by_default',
- this.$.workInProgressByDefault.checked);
- }
-
- _handleInsertSignedOff() {
- this.set('_localPrefs.signed_off_by', this.$.insertSignedOff.checked);
- }
-
- _handleMenuChanged() {
- if (this._isLoading()) { return; }
- this._menuChanged = true;
- }
-
- _handleSaveAccountInfo() {
- this.$.accountInfo.save();
- }
-
- _handleSavePreferences() {
- this._copyPrefs('prefs', '_localPrefs');
-
- return this.$.restAPI.savePreferences(this.prefs).then(() => {
- this._prefsChanged = false;
- });
- }
-
- _handleSaveChangeTable() {
- this.set('prefs.change_table', this._localChangeTableColumns);
- this.set('prefs.legacycid_in_change_table', this._showNumber);
+ promises.push(this.$.restAPI.getPreferences().then(prefs => {
+ this.prefs = prefs;
+ this._showNumber = !!prefs.legacycid_in_change_table;
+ this._copyPrefs('_localPrefs', 'prefs');
+ this._cloneMenu(prefs.my);
this._cloneChangeTableColumns();
- return this.$.restAPI.savePreferences(this.prefs).then(() => {
- this._changeTableChanged = false;
- });
- }
+ }));
- _handleSaveDiffPreferences() {
- this.$.diffPrefs.save();
- }
+ promises.push(this.$.restAPI.getConfig().then(config => {
+ this._serverConfig = config;
+ const configPromises = [];
- _handleSaveEditPreferences() {
- this.$.editPrefs.save();
- }
-
- _handleSaveMenu() {
- this.set('prefs.my', this._localMenu);
- this._cloneMenu(this.prefs.my);
- return this.$.restAPI.savePreferences(this.prefs).then(() => {
- this._menuChanged = false;
- });
- }
-
- _handleResetMenuButton() {
- return this.$.restAPI.getDefaultPreferences().then(data => {
- if (data && data.my) {
- this._cloneMenu(data.my);
- }
- });
- }
-
- _handleSaveWatchedProjects() {
- this.$.watchedProjectsEditor.save();
- }
-
- _computeHeaderClass(changed) {
- return changed ? 'edited' : '';
- }
-
- _handleSaveEmails() {
- this.$.emailEditor.save();
- }
-
- _handleNewEmailKeydown(e) {
- if (e.keyCode === 13) { // Enter
- e.stopPropagation();
- this._handleAddEmailButton();
- }
- }
-
- _isNewEmailValid(newEmail) {
- return newEmail && newEmail.includes('@');
- }
-
- _computeAddEmailButtonEnabled(newEmail, addingEmail) {
- return this._isNewEmailValid(newEmail) && !addingEmail;
- }
-
- _handleAddEmailButton() {
- if (!this._isNewEmailValid(this._newEmail)) { return; }
-
- this._addingEmail = true;
- this.$.restAPI.addAccountEmail(this._newEmail).then(response => {
- this._addingEmail = false;
-
- // If it was unsuccessful.
- if (response.status < 200 || response.status >= 300) { return; }
-
- this._lastSentVerificationEmail = this._newEmail;
- this._newEmail = '';
- });
- }
-
- _getFilterDocsLink(docsBaseUrl) {
- let base = docsBaseUrl;
- if (!base || !ABSOLUTE_URL_PATTERN.test(base)) {
- base = GERRIT_DOCS_BASE_URL;
+ if (this._serverConfig && this._serverConfig.sshd) {
+ configPromises.push(this.$.sshEditor.loadData());
}
- // Remove any trailing slash, since it is in the GERRIT_DOCS_FILTER_PATH.
- base = base.replace(TRAILING_SLASH_PATTERN, '');
-
- return base + GERRIT_DOCS_FILTER_PATH;
- }
-
- _handleToggleDark() {
- if (this._isDark) {
- window.localStorage.removeItem('dark-theme');
- } else {
- window.localStorage.setItem('dark-theme', 'true');
- }
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {message: RELOAD_MESSAGE},
- bubbles: true,
- composed: true,
- }));
- this.async(() => {
- window.location.reload();
- }, 1);
- }
-
- _showHttpAuth(config) {
- if (config && config.auth &&
- config.auth.git_basic_auth_policy) {
- return HTTP_AUTH.includes(
- config.auth.git_basic_auth_policy.toUpperCase());
+ if (this._serverConfig &&
+ this._serverConfig.receive &&
+ this._serverConfig.receive.enable_signed_push) {
+ configPromises.push(this.$.gpgEditor.loadData());
}
- return false;
+ configPromises.push(
+ this.getDocsBaseUrl(config, this.$.restAPI)
+ .then(baseUrl => { this._docsBaseUrl = baseUrl; }));
+
+ return Promise.all(configPromises);
+ }));
+
+ if (this.params.emailToken) {
+ promises.push(this.$.restAPI.confirmEmail(this.params.emailToken).then(
+ message => {
+ if (message) {
+ this.fire('show-alert', {message});
+ }
+ this.$.emailEditor.loadData();
+ }));
+ } else {
+ promises.push(this.$.emailEditor.loadData());
+ }
+
+ this._loadingPromise = Promise.all(promises).then(() => {
+ this._loading = false;
+
+ // Handle anchor tag for initial load
+ this._handleLocationChange();
+ });
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this.unlisten(window, 'location-change', '_handleLocationChange');
+ }
+
+ _handleLocationChange() {
+ // Handle anchor tag after dom attached
+ const urlHash = window.location.hash;
+ if (urlHash) {
+ // Use shadowRoot for Polymer 2
+ const elem = (this.shadowRoot || document).querySelector(urlHash);
+ if (elem) {
+ elem.scrollIntoView();
+ }
}
}
- customElements.define(GrSettingsView.is, GrSettingsView);
-})();
+ reloadAccountDetail() {
+ Promise.all([
+ this.$.accountInfo.loadData(),
+ this.$.emailEditor.loadData(),
+ ]);
+ }
+
+ _isLoading() {
+ return this._loading || this._loading === undefined;
+ }
+
+ _copyPrefs(to, from) {
+ for (let i = 0; i < PREFS_SECTION_FIELDS.length; i++) {
+ this.set([to, PREFS_SECTION_FIELDS[i]],
+ this[from][PREFS_SECTION_FIELDS[i]]);
+ }
+ }
+
+ _cloneMenu(prefs) {
+ const menu = [];
+ for (const item of prefs) {
+ menu.push({
+ name: item.name,
+ url: item.url,
+ target: item.target,
+ });
+ }
+ this._localMenu = menu;
+ }
+
+ _cloneChangeTableColumns() {
+ let columns = this.getVisibleColumns(this.prefs.change_table);
+
+ if (columns.length === 0) {
+ columns = this.columnNames;
+ this._changeTableColumnsNotDisplayed = [];
+ } else {
+ this._changeTableColumnsNotDisplayed = this.getComplementColumns(
+ this.prefs.change_table);
+ }
+ this._localChangeTableColumns = columns;
+ }
+
+ _formatChangeTableColumns(changeTableArray) {
+ return changeTableArray.map(item => {
+ return {column: item};
+ });
+ }
+
+ _handleChangeTableChanged() {
+ if (this._isLoading()) { return; }
+ this._changeTableChanged = true;
+ }
+
+ _handlePrefsChanged(prefs) {
+ if (this._isLoading()) { return; }
+ this._prefsChanged = true;
+ }
+
+ _handleRelativeDateInChangeTable() {
+ this.set('_localPrefs.relative_date_in_change_table',
+ this.$.relativeDateInChangeTable.checked);
+ }
+
+ _handleShowSizeBarsInFileListChanged() {
+ this.set('_localPrefs.size_bar_in_change_table',
+ this.$.showSizeBarsInFileList.checked);
+ }
+
+ _handlePublishCommentsOnPushChanged() {
+ this.set('_localPrefs.publish_comments_on_push',
+ this.$.publishCommentsOnPush.checked);
+ }
+
+ _handleWorkInProgressByDefault() {
+ this.set('_localPrefs.work_in_progress_by_default',
+ this.$.workInProgressByDefault.checked);
+ }
+
+ _handleInsertSignedOff() {
+ this.set('_localPrefs.signed_off_by', this.$.insertSignedOff.checked);
+ }
+
+ _handleMenuChanged() {
+ if (this._isLoading()) { return; }
+ this._menuChanged = true;
+ }
+
+ _handleSaveAccountInfo() {
+ this.$.accountInfo.save();
+ }
+
+ _handleSavePreferences() {
+ this._copyPrefs('prefs', '_localPrefs');
+
+ return this.$.restAPI.savePreferences(this.prefs).then(() => {
+ this._prefsChanged = false;
+ });
+ }
+
+ _handleSaveChangeTable() {
+ this.set('prefs.change_table', this._localChangeTableColumns);
+ this.set('prefs.legacycid_in_change_table', this._showNumber);
+ this._cloneChangeTableColumns();
+ return this.$.restAPI.savePreferences(this.prefs).then(() => {
+ this._changeTableChanged = false;
+ });
+ }
+
+ _handleSaveDiffPreferences() {
+ this.$.diffPrefs.save();
+ }
+
+ _handleSaveEditPreferences() {
+ this.$.editPrefs.save();
+ }
+
+ _handleSaveMenu() {
+ this.set('prefs.my', this._localMenu);
+ this._cloneMenu(this.prefs.my);
+ return this.$.restAPI.savePreferences(this.prefs).then(() => {
+ this._menuChanged = false;
+ });
+ }
+
+ _handleResetMenuButton() {
+ return this.$.restAPI.getDefaultPreferences().then(data => {
+ if (data && data.my) {
+ this._cloneMenu(data.my);
+ }
+ });
+ }
+
+ _handleSaveWatchedProjects() {
+ this.$.watchedProjectsEditor.save();
+ }
+
+ _computeHeaderClass(changed) {
+ return changed ? 'edited' : '';
+ }
+
+ _handleSaveEmails() {
+ this.$.emailEditor.save();
+ }
+
+ _handleNewEmailKeydown(e) {
+ if (e.keyCode === 13) { // Enter
+ e.stopPropagation();
+ this._handleAddEmailButton();
+ }
+ }
+
+ _isNewEmailValid(newEmail) {
+ return newEmail && newEmail.includes('@');
+ }
+
+ _computeAddEmailButtonEnabled(newEmail, addingEmail) {
+ return this._isNewEmailValid(newEmail) && !addingEmail;
+ }
+
+ _handleAddEmailButton() {
+ if (!this._isNewEmailValid(this._newEmail)) { return; }
+
+ this._addingEmail = true;
+ this.$.restAPI.addAccountEmail(this._newEmail).then(response => {
+ this._addingEmail = false;
+
+ // If it was unsuccessful.
+ if (response.status < 200 || response.status >= 300) { return; }
+
+ this._lastSentVerificationEmail = this._newEmail;
+ this._newEmail = '';
+ });
+ }
+
+ _getFilterDocsLink(docsBaseUrl) {
+ let base = docsBaseUrl;
+ if (!base || !ABSOLUTE_URL_PATTERN.test(base)) {
+ base = GERRIT_DOCS_BASE_URL;
+ }
+
+ // Remove any trailing slash, since it is in the GERRIT_DOCS_FILTER_PATH.
+ base = base.replace(TRAILING_SLASH_PATTERN, '');
+
+ return base + GERRIT_DOCS_FILTER_PATH;
+ }
+
+ _handleToggleDark() {
+ if (this._isDark) {
+ window.localStorage.removeItem('dark-theme');
+ } else {
+ window.localStorage.setItem('dark-theme', 'true');
+ }
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {message: RELOAD_MESSAGE},
+ bubbles: true,
+ composed: true,
+ }));
+ this.async(() => {
+ window.location.reload();
+ }, 1);
+ }
+
+ _showHttpAuth(config) {
+ if (config && config.auth &&
+ config.auth.git_basic_auth_policy) {
+ return HTTP_AUTH.includes(
+ config.auth.git_basic_auth_policy.toUpperCase());
+ }
+
+ return false;
+ }
+}
+
+customElements.define(GrSettingsView.is, GrSettingsView);
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js
new file mode 100644
index 0000000..0f03ec1
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js
@@ -0,0 +1,383 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ color: var(--primary-text-color);
+ }
+ .newEmailInput {
+ width: 20em;
+ }
+ #email {
+ margin-bottom: var(--spacing-l);
+ }
+ main section.darkToggle {
+ display: block;
+ }
+ .filters p,
+ .darkToggle p {
+ margin-bottom: var(--spacing-l);
+ }
+ .queryExample em {
+ color: violet;
+ }
+ .toggle {
+ align-items: center;
+ display: flex;
+ margin-bottom: var(--spacing-l);
+ margin-right: var(--spacing-l);
+ }
+ </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>
+ <style include="gr-page-nav-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <div class="loading" hidden\$="[[!_loading]]">Loading...</div>
+ <div hidden\$="[[_loading]]" hidden="">
+ <gr-page-nav class="navStyles">
+ <ul>
+ <li><a href="#Profile">Profile</a></li>
+ <li><a href="#Preferences">Preferences</a></li>
+ <li><a href="#DiffPreferences">Diff Preferences</a></li>
+ <li><a href="#EditPreferences">Edit Preferences</a></li>
+ <li><a href="#Menu">Menu</a></li>
+ <li><a href="#ChangeTableColumns">Change Table Columns</a></li>
+ <li><a href="#Notifications">Notifications</a></li>
+ <li><a href="#EmailAddresses">Email Addresses</a></li>
+ <template is="dom-if" if="[[_showHttpAuth(_serverConfig)]]">
+ <li><a href="#HTTPCredentials">HTTP Credentials</a></li>
+ </template>
+ <li hidden\$="[[!_serverConfig.sshd]]"><a href="#SSHKeys">
+ SSH Keys
+ </a></li>
+ <li hidden\$="[[!_serverConfig.receive.enable_signed_push]]"><a href="#GPGKeys">
+ GPG Keys
+ </a></li>
+ <li><a href="#Groups">Groups</a></li>
+ <li><a href="#Identities">Identities</a></li>
+ <template is="dom-if" if="[[_serverConfig.auth.use_contributor_agreements]]">
+ <li>
+ <a href="#Agreements">Agreements</a>
+ </li>
+ </template>
+ <li><a href="#MailFilters">Mail Filters</a></li>
+ <gr-endpoint-decorator name="settings-menu-item">
+ </gr-endpoint-decorator>
+ </ul>
+ </gr-page-nav>
+ <main class="gr-form-styles">
+ <h1>User Settings</h1>
+ <section class="darkToggle">
+ <div class="toggle">
+ <paper-toggle-button checked="[[_isDark]]" on-change="_handleToggleDark"></paper-toggle-button>
+ <div>Dark theme (alpha)</div>
+ </div>
+ <p>
+ Gerrit's dark theme is in early alpha, and almost definitely will
+ not play nicely with themes set by specific Gerrit hosts. Filing
+ feedback via the link in the app footer is strongly encouraged!
+ </p>
+ </section>
+ <h2 id="Profile" class\$="[[_computeHeaderClass(_accountInfoChanged)]]">Profile</h2>
+ <fieldset id="profile">
+ <gr-account-info id="accountInfo" has-unsaved-changes="{{_accountInfoChanged}}"></gr-account-info>
+ <gr-button on-click="_handleSaveAccountInfo" disabled="[[!_accountInfoChanged]]">Save changes</gr-button>
+ </fieldset>
+ <h2 id="Preferences" class\$="[[_computeHeaderClass(_prefsChanged)]]">Preferences</h2>
+ <fieldset id="preferences">
+ <section>
+ <span class="title">Changes per page</span>
+ <span class="value">
+ <gr-select bind-value="{{_localPrefs.changes_per_page}}">
+ <select>
+ <option value="10">10 rows per page</option>
+ <option value="25">25 rows per page</option>
+ <option value="50">50 rows per page</option>
+ <option value="100">100 rows per page</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title">Date/time format</span>
+ <span class="value">
+ <gr-select bind-value="{{_localPrefs.date_format}}">
+ <select>
+ <option value="STD">Jun 3 ; Jun 3, 2016</option>
+ <option value="US">06/03 ; 06/03/16</option>
+ <option value="ISO">06-03 ; 2016-06-03</option>
+ <option value="EURO">3. Jun ; 03.06.2016</option>
+ <option value="UK">03/06 ; 03/06/2016</option>
+ </select>
+ </gr-select>
+ <gr-select bind-value="{{_localPrefs.time_format}}">
+ <select>
+ <option value="HHMM_12">4:10 PM</option>
+ <option value="HHMM_24">16:10</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title">Email notifications</span>
+ <span class="value">
+ <gr-select bind-value="{{_localPrefs.email_strategy}}">
+ <select>
+ <option value="CC_ON_OWN_COMMENTS">Every comment</option>
+ <option value="ENABLED">Only comments left by others</option>
+ <option value="DISABLED">None</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section hidden\$="[[!_localPrefs.email_format]]">
+ <span class="title">Email format</span>
+ <span class="value">
+ <gr-select bind-value="{{_localPrefs.email_format}}">
+ <select>
+ <option value="HTML_PLAINTEXT">HTML and plaintext</option>
+ <option value="PLAINTEXT">Plaintext only</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section hidden\$="[[!_localPrefs.default_base_for_merges]]">
+ <span class="title">Default Base For Merges</span>
+ <span class="value">
+ <gr-select bind-value="{{_localPrefs.default_base_for_merges}}">
+ <select>
+ <option value="AUTO_MERGE">Auto Merge</option>
+ <option value="FIRST_PARENT">First Parent</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title">Show Relative Dates In Changes Table</span>
+ <span class="value">
+ <input id="relativeDateInChangeTable" type="checkbox" checked\$="[[_localPrefs.relative_date_in_change_table]]" on-change="_handleRelativeDateInChangeTable">
+ </span>
+ </section>
+ <section>
+ <span class="title">Diff view</span>
+ <span class="value">
+ <gr-select bind-value="{{_localPrefs.diff_view}}">
+ <select>
+ <option value="SIDE_BY_SIDE">Side by side</option>
+ <option value="UNIFIED_DIFF">Unified diff</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title">Show size bars in file list</span>
+ <span class="value">
+ <input id="showSizeBarsInFileList" type="checkbox" checked\$="[[_localPrefs.size_bar_in_change_table]]" on-change="_handleShowSizeBarsInFileListChanged">
+ </span>
+ </section>
+ <section>
+ <span class="title">Publish comments on push</span>
+ <span class="value">
+ <input id="publishCommentsOnPush" type="checkbox" checked\$="[[_localPrefs.publish_comments_on_push]]" on-change="_handlePublishCommentsOnPushChanged">
+ </span>
+ </section>
+ <section>
+ <span class="title">Set new changes to "work in progress" by default</span>
+ <span class="value">
+ <input id="workInProgressByDefault" type="checkbox" checked\$="[[_localPrefs.work_in_progress_by_default]]" on-change="_handleWorkInProgressByDefault">
+ </span>
+ </section>
+ <section>
+ <span class="title">
+ Insert Signed-off-by Footer For Inline Edit Changes
+ </span>
+ <span class="value">
+ <input id="insertSignedOff" type="checkbox" checked\$="[[_localPrefs.signed_off_by]]" on-change="_handleInsertSignedOff">
+ </span>
+ </section>
+ <gr-button id="savePrefs" on-click="_handleSavePreferences" disabled="[[!_prefsChanged]]">Save changes</gr-button>
+ </fieldset>
+ <h2 id="DiffPreferences" class\$="[[_computeHeaderClass(_diffPrefsChanged)]]">
+ Diff Preferences
+ </h2>
+ <fieldset id="diffPreferences">
+ <gr-diff-preferences id="diffPrefs" has-unsaved-changes="{{_diffPrefsChanged}}"></gr-diff-preferences>
+ <gr-button id="saveDiffPrefs" on-click="_handleSaveDiffPreferences" disabled\$="[[!_diffPrefsChanged]]">Save changes</gr-button>
+ </fieldset>
+ <h2 id="EditPreferences" class\$="[[_computeHeaderClass(_editPrefsChanged)]]">
+ Edit Preferences
+ </h2>
+ <fieldset id="editPreferences">
+ <gr-edit-preferences id="editPrefs" has-unsaved-changes="{{_editPrefsChanged}}"></gr-edit-preferences>
+ <gr-button id="saveEditPrefs" on-click="_handleSaveEditPreferences" disabled\$="[[!_editPrefsChanged]]">Save changes</gr-button>
+ </fieldset>
+ <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="resetMenu" link="" on-click="_handleResetMenuButton">Reset</gr-button>
+ </fieldset>
+ <h2 id="ChangeTableColumns" class\$="[[_computeHeaderClass(_changeTableChanged)]]">
+ Change Table Columns
+ </h2>
+ <fieldset id="changeTableColumns">
+ <gr-change-table-editor show-number="{{_showNumber}}" displayed-columns="{{_localChangeTableColumns}}">
+ </gr-change-table-editor>
+ <gr-button id="saveChangeTable" on-click="_handleSaveChangeTable" disabled="[[!_changeTableChanged]]">Save changes</gr-button>
+ </fieldset>
+ <h2 id="Notifications" class\$="[[_computeHeaderClass(_watchedProjectsChanged)]]">
+ Notifications
+ </h2>
+ <fieldset id="watchedProjects">
+ <gr-watched-projects-editor has-unsaved-changes="{{_watchedProjectsChanged}}" id="watchedProjectsEditor"></gr-watched-projects-editor>
+ <gr-button on-click="_handleSaveWatchedProjects" disabled\$="[[!_watchedProjectsChanged]]" id="_handleSaveWatchedProjects">Save changes</gr-button>
+ </fieldset>
+ <h2 id="EmailAddresses" class\$="[[_computeHeaderClass(_emailsChanged)]]">
+ Email Addresses
+ </h2>
+ <fieldset id="email">
+ <gr-email-editor id="emailEditor" has-unsaved-changes="{{_emailsChanged}}"></gr-email-editor>
+ <gr-button on-click="_handleSaveEmails" disabled\$="[[!_emailsChanged]]">Save changes</gr-button>
+ </fieldset>
+ <fieldset id="newEmail">
+ <section>
+ <span class="title">New email address</span>
+ <span class="value">
+ <iron-input class="newEmailInput" bind-value="{{_newEmail}}" type="text" on-keydown="_handleNewEmailKeydown" placeholder="email@example.com">
+ <input class="newEmailInput" bind-value="{{_newEmail}}" is="iron-input" type="text" disabled="[[_addingEmail]]" on-keydown="_handleNewEmailKeydown" placeholder="email@example.com">
+ </iron-input>
+ </span>
+ </section>
+ <section id="verificationSentMessage" hidden\$="[[!_lastSentVerificationEmail]]">
+ <p>
+ A verification email was sent to
+ <em>[[_lastSentVerificationEmail]]</em>. Please check your inbox.
+ </p>
+ </section>
+ <gr-button disabled="[[!_computeAddEmailButtonEnabled(_newEmail, _addingEmail)]]" on-click="_handleAddEmailButton">Send verification</gr-button>
+ </fieldset>
+ <template is="dom-if" if="[[_showHttpAuth(_serverConfig)]]">
+ <div>
+ <h2 id="HTTPCredentials">HTTP Credentials</h2>
+ <fieldset>
+ <gr-http-password id="httpPass"></gr-http-password>
+ </fieldset>
+ </div>
+ </template>
+ <div hidden\$="[[!_serverConfig.sshd]]">
+ <h2 id="SSHKeys" class\$="[[_computeHeaderClass(_keysChanged)]]">SSH keys</h2>
+ <gr-ssh-editor id="sshEditor" has-unsaved-changes="{{_keysChanged}}"></gr-ssh-editor>
+ </div>
+ <div hidden\$="[[!_serverConfig.receive.enable_signed_push]]">
+ <h2 id="GPGKeys" class\$="[[_computeHeaderClass(_gpgKeysChanged)]]">GPG keys</h2>
+ <gr-gpg-editor id="gpgEditor" has-unsaved-changes="{{_gpgKeysChanged}}"></gr-gpg-editor>
+ </div>
+ <h2 id="Groups">Groups</h2>
+ <fieldset>
+ <gr-group-list id="groupList"></gr-group-list>
+ </fieldset>
+ <h2 id="Identities">Identities</h2>
+ <fieldset>
+ <gr-identities id="identities" server-config="[[_serverConfig]]"></gr-identities>
+ </fieldset>
+ <template is="dom-if" if="[[_serverConfig.auth.use_contributor_agreements]]">
+ <h2 id="Agreements">Agreements</h2>
+ <fieldset>
+ <gr-agreements-list id="agreementsList"></gr-agreements-list>
+ </fieldset>
+ </template>
+ <h2 id="MailFilters">Mail Filters</h2>
+ <fieldset class="filters">
+ <p>
+ Gerrit emails include metadata about the change to support
+ writing mail filters.
+ </p>
+ <p>
+ Here are some example Gmail queries that can be used for filters or
+ for searching through archived messages. View the
+ <a href\$="[[_getFilterDocsLink(_docsBaseUrl)]]" target="_blank" rel="nofollow">Gerrit documentation</a>
+ for the complete set of footers.
+ </p>
+ <table>
+ <tbody>
+ <tr><th>Name</th><th>Query</th></tr>
+ <tr>
+ <td>Changes requesting my review</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Reviewer: <em>Your Name</em>
+ <<em>your.email@example.com</em>>"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Changes from a specific owner</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Owner: <em>Owner name</em>
+ <<em>owner.email@example.com</em>>"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Changes targeting a specific branch</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Branch: <em>branch-name</em>"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Changes in a specific project</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Project: <em>project-name</em>"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Messages related to a specific Change ID</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Change-Id: <em>Change ID</em>"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Messages related to a specific change number</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Change-Number: <em>change number</em>"
+ </code>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </fieldset>
+ <gr-endpoint-decorator name="settings-screen">
+ </gr-endpoint-decorator>
+ </main>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index cd268c6..b6e59ae 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-settings-view</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-settings-view.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -41,488 +36,490 @@
</template>
</test-fixture>
-<script>
- suite('gr-settings-view tests', async () => {
- await readyToTest();
- let element;
- let account;
- let preferences;
- let config;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-settings-view.js';
+import {flush, dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-settings-view tests', () => {
+ let element;
+ let account;
+ let preferences;
+ let config;
+ let sandbox;
- function valueOf(title, fieldsetid) {
- const sections = element.$[fieldsetid].querySelectorAll('section');
- let titleEl;
- for (let i = 0; i < sections.length; i++) {
- titleEl = sections[i].querySelector('.title');
- if (titleEl.textContent.trim() === title) {
- return sections[i].querySelector('.value');
- }
+ function valueOf(title, fieldsetid) {
+ const sections = element.$[fieldsetid].querySelectorAll('section');
+ let titleEl;
+ for (let i = 0; i < sections.length; i++) {
+ titleEl = sections[i].querySelector('.title');
+ if (titleEl.textContent.trim() === title) {
+ return sections[i].querySelector('.value');
}
}
+ }
- // Because deepEqual isn't behaving in Safari.
- function assertMenusEqual(actual, expected) {
- assert.equal(actual.length, expected.length);
- for (let i = 0; i < actual.length; i++) {
- assert.equal(actual[i].name, expected[i].name);
- assert.equal(actual[i].url, expected[i].url);
- }
+ // Because deepEqual isn't behaving in Safari.
+ function assertMenusEqual(actual, expected) {
+ assert.equal(actual.length, expected.length);
+ for (let i = 0; i < actual.length; i++) {
+ assert.equal(actual[i].name, expected[i].name);
+ assert.equal(actual[i].url, expected[i].url);
}
+ }
- function stubAddAccountEmail(statusCode) {
- return sandbox.stub(element.$.restAPI, 'addAccountEmail',
- () => Promise.resolve({status: statusCode}));
- }
+ function stubAddAccountEmail(statusCode) {
+ return sandbox.stub(element.$.restAPI, 'addAccountEmail',
+ () => Promise.resolve({status: statusCode}));
+ }
- setup(done => {
- sandbox = sinon.sandbox.create();
- account = {
- _account_id: 123,
- name: 'user name',
- email: 'user@email',
- username: 'user username',
- registered: '2000-01-01 00:00:00.000000000',
- };
- preferences = {
- changes_per_page: 25,
- date_format: 'UK',
- time_format: 'HHMM_12',
- diff_view: 'UNIFIED_DIFF',
- email_strategy: 'ENABLED',
- email_format: 'HTML_PLAINTEXT',
- default_base_for_merges: 'FIRST_PARENT',
- relative_date_in_change_table: false,
- size_bar_in_change_table: true,
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ account = {
+ _account_id: 123,
+ name: 'user name',
+ email: 'user@email',
+ username: 'user username',
+ registered: '2000-01-01 00:00:00.000000000',
+ };
+ preferences = {
+ changes_per_page: 25,
+ date_format: 'UK',
+ time_format: 'HHMM_12',
+ diff_view: 'UNIFIED_DIFF',
+ email_strategy: 'ENABLED',
+ email_format: 'HTML_PLAINTEXT',
+ default_base_for_merges: 'FIRST_PARENT',
+ relative_date_in_change_table: false,
+ size_bar_in_change_table: true,
- my: [
- {url: '/first/url', name: 'first name', target: '_blank'},
- {url: '/second/url', name: 'second name', target: '_blank'},
- ],
- change_table: [],
- };
- config = {auth: {editable_account_fields: []}};
+ my: [
+ {url: '/first/url', name: 'first name', target: '_blank'},
+ {url: '/second/url', name: 'second name', target: '_blank'},
+ ],
+ change_table: [],
+ };
+ config = {auth: {editable_account_fields: []}};
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- getAccount() { return Promise.resolve(account); },
- getPreferences() { return Promise.resolve(preferences); },
- getWatchedProjects() {
- return Promise.resolve([]);
- },
- getAccountEmails() { return Promise.resolve(); },
- getConfig() { return Promise.resolve(config); },
- getAccountGroups() { return Promise.resolve([]); },
- });
- element = fixture('basic');
-
- // Allow the element to render.
- element._loadingPromise.then(done);
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
+ getAccount() { return Promise.resolve(account); },
+ getPreferences() { return Promise.resolve(preferences); },
+ getWatchedProjects() {
+ return Promise.resolve([]);
+ },
+ getAccountEmails() { return Promise.resolve(); },
+ getConfig() { return Promise.resolve(config); },
+ getAccountGroups() { return Promise.resolve([]); },
});
+ element = fixture('basic');
- teardown(() => {
- sandbox.restore();
- });
+ // Allow the element to render.
+ element._loadingPromise.then(done);
+ });
- test('calls the title-change event', () => {
- const titleChangedStub = sandbox.stub();
+ teardown(() => {
+ sandbox.restore();
+ });
- // Create a new view.
- const newElement = document.createElement('gr-settings-view');
- newElement.addEventListener('title-change', titleChangedStub);
+ test('calls the title-change event', () => {
+ const titleChangedStub = sandbox.stub();
- // Attach it to the fixture.
- const blank = fixture('blank');
- blank.appendChild(newElement);
+ // Create a new view.
+ const newElement = document.createElement('gr-settings-view');
+ newElement.addEventListener('title-change', titleChangedStub);
- Polymer.dom.flush();
+ // Attach it to the fixture.
+ const blank = fixture('blank');
+ blank.appendChild(newElement);
- assert.isTrue(titleChangedStub.called);
- assert.equal(titleChangedStub.getCall(0).args[0].detail.title,
- 'Settings');
- });
+ flush();
- test('user preferences', done => {
- // Rendered with the expected preferences selected.
- assert.equal(valueOf('Changes per page', 'preferences')
- .firstElementChild.bindValue, preferences.changes_per_page);
- assert.equal(valueOf('Date/time format', 'preferences')
- .firstElementChild.bindValue, preferences.date_format);
- assert.equal(valueOf('Date/time format', 'preferences')
- .lastElementChild.bindValue, preferences.time_format);
- assert.equal(valueOf('Email notifications', 'preferences')
- .firstElementChild.bindValue, preferences.email_strategy);
- assert.equal(valueOf('Email format', 'preferences')
- .firstElementChild.bindValue, preferences.email_format);
- assert.equal(valueOf('Default Base For Merges', 'preferences')
- .firstElementChild.bindValue, preferences.default_base_for_merges);
- assert.equal(
- valueOf('Show Relative Dates In Changes Table', 'preferences')
- .firstElementChild.checked, false);
- assert.equal(valueOf('Diff view', 'preferences')
- .firstElementChild.bindValue, preferences.diff_view);
- assert.equal(valueOf('Show size bars in file list', 'preferences')
- .firstElementChild.checked, true);
- assert.equal(valueOf('Publish comments on push', 'preferences')
- .firstElementChild.checked, false);
- assert.equal(valueOf(
- 'Set new changes to "work in progress" by default', 'preferences')
- .firstElementChild.checked, false);
- assert.equal(valueOf(
- 'Insert Signed-off-by Footer For Inline Edit Changes', 'preferences')
- .firstElementChild.checked, false);
+ assert.isTrue(titleChangedStub.called);
+ assert.equal(titleChangedStub.getCall(0).args[0].detail.title,
+ 'Settings');
+ });
- assert.isFalse(element._prefsChanged);
- assert.isFalse(element._menuChanged);
+ test('user preferences', done => {
+ // Rendered with the expected preferences selected.
+ assert.equal(valueOf('Changes per page', 'preferences')
+ .firstElementChild.bindValue, preferences.changes_per_page);
+ assert.equal(valueOf('Date/time format', 'preferences')
+ .firstElementChild.bindValue, preferences.date_format);
+ assert.equal(valueOf('Date/time format', 'preferences')
+ .lastElementChild.bindValue, preferences.time_format);
+ assert.equal(valueOf('Email notifications', 'preferences')
+ .firstElementChild.bindValue, preferences.email_strategy);
+ assert.equal(valueOf('Email format', 'preferences')
+ .firstElementChild.bindValue, preferences.email_format);
+ assert.equal(valueOf('Default Base For Merges', 'preferences')
+ .firstElementChild.bindValue, preferences.default_base_for_merges);
+ assert.equal(
+ valueOf('Show Relative Dates In Changes Table', 'preferences')
+ .firstElementChild.checked, false);
+ assert.equal(valueOf('Diff view', 'preferences')
+ .firstElementChild.bindValue, preferences.diff_view);
+ assert.equal(valueOf('Show size bars in file list', 'preferences')
+ .firstElementChild.checked, true);
+ assert.equal(valueOf('Publish comments on push', 'preferences')
+ .firstElementChild.checked, false);
+ assert.equal(valueOf(
+ 'Set new changes to "work in progress" by default', 'preferences')
+ .firstElementChild.checked, false);
+ assert.equal(valueOf(
+ 'Insert Signed-off-by Footer For Inline Edit Changes', 'preferences')
+ .firstElementChild.checked, false);
- // Change the diff view element.
- const diffSelect = valueOf('Diff view', 'preferences').firstElementChild;
- diffSelect.bindValue = 'SIDE_BY_SIDE';
+ assert.isFalse(element._prefsChanged);
+ assert.isFalse(element._menuChanged);
- const publishOnPush =
- valueOf('Publish comments on push', 'preferences').firstElementChild;
- diffSelect.fire('change');
+ // Change the diff view element.
+ const diffSelect = valueOf('Diff view', 'preferences').firstElementChild;
+ diffSelect.bindValue = 'SIDE_BY_SIDE';
- MockInteractions.tap(publishOnPush);
-
- assert.isTrue(element._prefsChanged);
- assert.isFalse(element._menuChanged);
-
- stub('gr-rest-api-interface', {
- savePreferences(prefs) {
- assert.equal(prefs.diff_view, 'SIDE_BY_SIDE');
- assertMenusEqual(prefs.my, preferences.my);
- assert.equal(prefs.publish_comments_on_push, true);
- return Promise.resolve();
- },
- });
-
- // Save the change.
- element._handleSavePreferences().then(() => {
- assert.isFalse(element._prefsChanged);
- assert.isFalse(element._menuChanged);
- done();
- });
- });
-
- test('publish comments on push', done => {
- const publishCommentsOnPush =
+ const publishOnPush =
valueOf('Publish comments on push', 'preferences').firstElementChild;
- MockInteractions.tap(publishCommentsOnPush);
+ diffSelect.fire('change');
- assert.isFalse(element._menuChanged);
- assert.isTrue(element._prefsChanged);
+ MockInteractions.tap(publishOnPush);
- stub('gr-rest-api-interface', {
- savePreferences(prefs) {
- assert.equal(prefs.publish_comments_on_push, true);
- return Promise.resolve();
- },
- });
+ assert.isTrue(element._prefsChanged);
+ assert.isFalse(element._menuChanged);
- // Save the change.
- element._handleSavePreferences().then(() => {
- assert.isFalse(element._prefsChanged);
- assert.isFalse(element._menuChanged);
- done();
- });
+ stub('gr-rest-api-interface', {
+ savePreferences(prefs) {
+ assert.equal(prefs.diff_view, 'SIDE_BY_SIDE');
+ assertMenusEqual(prefs.my, preferences.my);
+ assert.equal(prefs.publish_comments_on_push, true);
+ return Promise.resolve();
+ },
});
- test('set new changes work-in-progress', done => {
- const newChangesWorkInProgress =
- valueOf('Set new changes to "work in progress" by default',
- 'preferences').firstElementChild;
- MockInteractions.tap(newChangesWorkInProgress);
-
+ // Save the change.
+ element._handleSavePreferences().then(() => {
+ assert.isFalse(element._prefsChanged);
assert.isFalse(element._menuChanged);
- assert.isTrue(element._prefsChanged);
+ done();
+ });
+ });
- stub('gr-rest-api-interface', {
- savePreferences(prefs) {
- assert.equal(prefs.work_in_progress_by_default, true);
- return Promise.resolve();
- },
- });
+ test('publish comments on push', done => {
+ const publishCommentsOnPush =
+ valueOf('Publish comments on push', 'preferences').firstElementChild;
+ MockInteractions.tap(publishCommentsOnPush);
- // Save the change.
- element._handleSavePreferences().then(() => {
- assert.isFalse(element._prefsChanged);
- assert.isFalse(element._menuChanged);
- done();
- });
+ assert.isFalse(element._menuChanged);
+ assert.isTrue(element._prefsChanged);
+
+ stub('gr-rest-api-interface', {
+ savePreferences(prefs) {
+ assert.equal(prefs.publish_comments_on_push, true);
+ return Promise.resolve();
+ },
});
- test('menu', done => {
+ // Save the change.
+ element._handleSavePreferences().then(() => {
+ assert.isFalse(element._prefsChanged);
+ assert.isFalse(element._menuChanged);
+ done();
+ });
+ });
+
+ test('set new changes work-in-progress', done => {
+ const newChangesWorkInProgress =
+ valueOf('Set new changes to "work in progress" by default',
+ 'preferences').firstElementChild;
+ MockInteractions.tap(newChangesWorkInProgress);
+
+ assert.isFalse(element._menuChanged);
+ assert.isTrue(element._prefsChanged);
+
+ stub('gr-rest-api-interface', {
+ savePreferences(prefs) {
+ assert.equal(prefs.work_in_progress_by_default, true);
+ return Promise.resolve();
+ },
+ });
+
+ // Save the change.
+ element._handleSavePreferences().then(() => {
+ assert.isFalse(element._prefsChanged);
+ assert.isFalse(element._menuChanged);
+ done();
+ });
+ });
+
+ test('menu', done => {
+ assert.isFalse(element._menuChanged);
+ assert.isFalse(element._prefsChanged);
+
+ assertMenusEqual(element._localMenu, preferences.my);
+
+ const menu = element.$.menu.firstElementChild;
+ let tableRows = dom(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 = dom(menu.root).querySelectorAll('tbody tr');
+ assert.equal(tableRows.length, preferences.my.length + 1);
+
+ assert.isTrue(element._menuChanged);
+ assert.isFalse(element._prefsChanged);
+
+ stub('gr-rest-api-interface', {
+ savePreferences(prefs) {
+ assertMenusEqual(prefs.my, element._localMenu);
+ return Promise.resolve();
+ },
+ });
+
+ element._handleSaveMenu().then(() => {
assert.isFalse(element._menuChanged);
assert.isFalse(element._prefsChanged);
-
- assertMenusEqual(element._localMenu, preferences.my);
-
- const menu = element.$.menu.firstElementChild;
- let tableRows = Polymer.dom(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: ''});
- Polymer.dom.flush();
-
- tableRows = Polymer.dom(menu.root).querySelectorAll('tbody tr');
- assert.equal(tableRows.length, preferences.my.length + 1);
-
- assert.isTrue(element._menuChanged);
- assert.isFalse(element._prefsChanged);
-
- stub('gr-rest-api-interface', {
- savePreferences(prefs) {
- assertMenusEqual(prefs.my, element._localMenu);
- return Promise.resolve();
- },
- });
-
- element._handleSaveMenu().then(() => {
- assert.isFalse(element._menuChanged);
- assert.isFalse(element._prefsChanged);
- assertMenusEqual(element.prefs.my, element._localMenu);
- done();
- });
+ assertMenusEqual(element.prefs.my, element._localMenu);
+ done();
});
+ });
- test('add email validation', () => {
- assert.isFalse(element._isNewEmailValid('invalid email'));
- assert.isTrue(element._isNewEmailValid('vaguely@valid.email'));
+ test('add email validation', () => {
+ assert.isFalse(element._isNewEmailValid('invalid email'));
+ assert.isTrue(element._isNewEmailValid('vaguely@valid.email'));
- assert.isFalse(
- element._computeAddEmailButtonEnabled('invalid email'), true);
- assert.isFalse(
- element._computeAddEmailButtonEnabled('vaguely@valid.email', true));
- assert.isTrue(
- element._computeAddEmailButtonEnabled('vaguely@valid.email', false));
+ assert.isFalse(
+ element._computeAddEmailButtonEnabled('invalid email'), true);
+ assert.isFalse(
+ element._computeAddEmailButtonEnabled('vaguely@valid.email', true));
+ assert.isTrue(
+ element._computeAddEmailButtonEnabled('vaguely@valid.email', false));
+ });
+
+ test('add email does not save invalid', () => {
+ const addEmailStub = stubAddAccountEmail(201);
+
+ assert.isFalse(element._addingEmail);
+ assert.isNotOk(element._lastSentVerificationEmail);
+ element._newEmail = 'invalid email';
+
+ element._handleAddEmailButton();
+
+ assert.isFalse(element._addingEmail);
+ assert.isFalse(addEmailStub.called);
+ assert.isNotOk(element._lastSentVerificationEmail);
+
+ assert.isFalse(addEmailStub.called);
+ });
+
+ test('add email does save valid', done => {
+ const addEmailStub = stubAddAccountEmail(201);
+
+ assert.isFalse(element._addingEmail);
+ assert.isNotOk(element._lastSentVerificationEmail);
+ element._newEmail = 'valid@email.com';
+
+ element._handleAddEmailButton();
+
+ assert.isTrue(element._addingEmail);
+ assert.isTrue(addEmailStub.called);
+
+ assert.isTrue(addEmailStub.called);
+ addEmailStub.lastCall.returnValue.then(() => {
+ assert.isOk(element._lastSentVerificationEmail);
+ done();
});
+ });
- test('add email does not save invalid', () => {
- const addEmailStub = stubAddAccountEmail(201);
+ test('add email does not set last-email if error', done => {
+ const addEmailStub = stubAddAccountEmail(500);
- assert.isFalse(element._addingEmail);
+ assert.isNotOk(element._lastSentVerificationEmail);
+ element._newEmail = 'valid@email.com';
+
+ element._handleAddEmailButton();
+
+ assert.isTrue(addEmailStub.called);
+ addEmailStub.lastCall.returnValue.then(() => {
assert.isNotOk(element._lastSentVerificationEmail);
- element._newEmail = 'invalid email';
-
- element._handleAddEmailButton();
-
- assert.isFalse(element._addingEmail);
- assert.isFalse(addEmailStub.called);
- assert.isNotOk(element._lastSentVerificationEmail);
-
- assert.isFalse(addEmailStub.called);
+ done();
});
+ });
- test('add email does save valid', done => {
- const addEmailStub = stubAddAccountEmail(201);
+ test('emails are loaded without emailToken', () => {
+ sandbox.stub(element.$.emailEditor, 'loadData');
+ element.params = {};
+ element.attached();
+ assert.isTrue(element.$.emailEditor.loadData.calledOnce);
+ });
- assert.isFalse(element._addingEmail);
- assert.isNotOk(element._lastSentVerificationEmail);
- element._newEmail = 'valid@email.com';
+ test('_handleSaveChangeTable', () => {
+ let newColumns = ['Owner', 'Project', 'Branch'];
+ element._localChangeTableColumns = newColumns.slice(0);
+ element._showNumber = false;
+ const cloneStub = sandbox.stub(element, '_cloneChangeTableColumns');
+ element._handleSaveChangeTable();
+ assert.isTrue(cloneStub.calledOnce);
+ assert.deepEqual(element.prefs.change_table, newColumns);
+ assert.isNotOk(element.prefs.legacycid_in_change_table);
- element._handleAddEmailButton();
+ newColumns = ['Size'];
+ element._localChangeTableColumns = newColumns;
+ element._showNumber = true;
+ element._handleSaveChangeTable();
+ assert.isTrue(cloneStub.calledTwice);
+ assert.deepEqual(element.prefs.change_table, newColumns);
+ assert.isTrue(element.prefs.legacycid_in_change_table);
+ });
- assert.isTrue(element._addingEmail);
- assert.isTrue(addEmailStub.called);
-
- assert.isTrue(addEmailStub.called);
- addEmailStub.lastCall.returnValue.then(() => {
- assert.isOk(element._lastSentVerificationEmail);
- done();
- });
- });
-
- test('add email does not set last-email if error', done => {
- const addEmailStub = stubAddAccountEmail(500);
-
- assert.isNotOk(element._lastSentVerificationEmail);
- element._newEmail = 'valid@email.com';
-
- element._handleAddEmailButton();
-
- assert.isTrue(addEmailStub.called);
- addEmailStub.lastCall.returnValue.then(() => {
- assert.isNotOk(element._lastSentVerificationEmail);
- done();
- });
- });
-
- test('emails are loaded without emailToken', () => {
- sandbox.stub(element.$.emailEditor, 'loadData');
- element.params = {};
- element.attached();
- assert.isTrue(element.$.emailEditor.loadData.calledOnce);
- });
-
- test('_handleSaveChangeTable', () => {
- let newColumns = ['Owner', 'Project', 'Branch'];
- element._localChangeTableColumns = newColumns.slice(0);
- element._showNumber = false;
- const cloneStub = sandbox.stub(element, '_cloneChangeTableColumns');
- element._handleSaveChangeTable();
- assert.isTrue(cloneStub.calledOnce);
- assert.deepEqual(element.prefs.change_table, newColumns);
- assert.isNotOk(element.prefs.legacycid_in_change_table);
-
- newColumns = ['Size'];
- element._localChangeTableColumns = newColumns;
- element._showNumber = true;
- element._handleSaveChangeTable();
- assert.isTrue(cloneStub.calledTwice);
- assert.deepEqual(element.prefs.change_table, newColumns);
- assert.isTrue(element.prefs.legacycid_in_change_table);
- });
-
- test('reset menu item back to default', done => {
- const originalMenu = {
- 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'},
- ],
- };
-
- stub('gr-rest-api-interface', {
- getDefaultPreferences() { return Promise.resolve(originalMenu); },
- });
-
- const updatedMenu = [
+ test('reset menu item back to default', done => {
+ const originalMenu = {
+ 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'},
- {url: '/fourth/url', name: 'fourth name', target: '_blank'},
- ];
+ ],
+ };
- element.set('_localMenu', updatedMenu);
-
- element._handleResetMenuButton().then(() => {
- assertMenusEqual(element._localMenu, originalMenu.my);
- done();
- });
+ stub('gr-rest-api-interface', {
+ getDefaultPreferences() { return Promise.resolve(originalMenu); },
});
- test('test that reset button is called', () => {
- const overlayOpen = sandbox.stub(element, '_handleResetMenuButton');
+ 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'},
+ ];
- MockInteractions.tap(element.$.resetMenu);
+ element.set('_localMenu', updatedMenu);
- assert.isTrue(overlayOpen.called);
- });
-
- test('_showHttpAuth', () => {
- let serverConfig;
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'HTTP',
- },
- };
-
- assert.isTrue(element._showHttpAuth(serverConfig));
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'HTTP_LDAP',
- },
- };
-
- assert.isTrue(element._showHttpAuth(serverConfig));
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'LDAP',
- },
- };
-
- assert.isFalse(element._showHttpAuth(serverConfig));
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'OAUTH',
- },
- };
-
- assert.isFalse(element._showHttpAuth(serverConfig));
-
- serverConfig = {};
-
- assert.isFalse(element._showHttpAuth(serverConfig));
- });
-
- suite('_getFilterDocsLink', () => {
- test('with http: docs base URL', () => {
- const base = 'http://example.com/';
- const result = element._getFilterDocsLink(base);
- assert.equal(result, 'http://example.com/user-notify.html');
- });
-
- test('with http: docs base URL without slash', () => {
- const base = 'http://example.com';
- const result = element._getFilterDocsLink(base);
- assert.equal(result, 'http://example.com/user-notify.html');
- });
-
- test('with https: docs base URL', () => {
- const base = 'https://example.com/';
- const result = element._getFilterDocsLink(base);
- assert.equal(result, 'https://example.com/user-notify.html');
- });
-
- test('without docs base URL', () => {
- const result = element._getFilterDocsLink(null);
- assert.equal(result, 'https://gerrit-review.googlesource.com/' +
- 'Documentation/user-notify.html');
- });
-
- test('ignores non HTTP links', () => {
- const base = 'javascript://alert("evil");';
- const result = element._getFilterDocsLink(base);
- assert.equal(result, 'https://gerrit-review.googlesource.com/' +
- 'Documentation/user-notify.html');
- });
- });
-
- suite('when email verification token is provided', () => {
- let resolveConfirm;
-
- setup(() => {
- sandbox.stub(element.$.emailEditor, 'loadData');
- sandbox.stub(
- element.$.restAPI,
- 'confirmEmail',
- () => new Promise(resolve => { resolveConfirm = resolve; }));
- element.params = {emailToken: 'foo'};
- element.attached();
- });
-
- test('it is used to confirm email via rest API', () => {
- assert.isTrue(element.$.restAPI.confirmEmail.calledOnce);
- assert.isTrue(element.$.restAPI.confirmEmail.calledWith('foo'));
- });
-
- test('emails are not loaded initially', () => {
- assert.isFalse(element.$.emailEditor.loadData.called);
- });
-
- test('user emails are loaded after email confirmed', done => {
- element._loadingPromise.then(() => {
- assert.isTrue(element.$.emailEditor.loadData.calledOnce);
- done();
- });
- resolveConfirm();
- });
-
- test('show-alert is fired when email is confirmed', done => {
- sandbox.spy(element, 'fire');
- element._loadingPromise.then(() => {
- assert.isTrue(
- element.fire.calledWith('show-alert', {message: 'bar'}));
- done();
- });
- resolveConfirm('bar');
- });
+ element._handleResetMenuButton().then(() => {
+ assertMenusEqual(element._localMenu, originalMenu.my);
+ done();
});
});
+
+ test('test that reset button is called', () => {
+ const overlayOpen = sandbox.stub(element, '_handleResetMenuButton');
+
+ MockInteractions.tap(element.$.resetMenu);
+
+ assert.isTrue(overlayOpen.called);
+ });
+
+ test('_showHttpAuth', () => {
+ let serverConfig;
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'HTTP',
+ },
+ };
+
+ assert.isTrue(element._showHttpAuth(serverConfig));
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'HTTP_LDAP',
+ },
+ };
+
+ assert.isTrue(element._showHttpAuth(serverConfig));
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'LDAP',
+ },
+ };
+
+ assert.isFalse(element._showHttpAuth(serverConfig));
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'OAUTH',
+ },
+ };
+
+ assert.isFalse(element._showHttpAuth(serverConfig));
+
+ serverConfig = {};
+
+ assert.isFalse(element._showHttpAuth(serverConfig));
+ });
+
+ suite('_getFilterDocsLink', () => {
+ test('with http: docs base URL', () => {
+ const base = 'http://example.com/';
+ const result = element._getFilterDocsLink(base);
+ assert.equal(result, 'http://example.com/user-notify.html');
+ });
+
+ test('with http: docs base URL without slash', () => {
+ const base = 'http://example.com';
+ const result = element._getFilterDocsLink(base);
+ assert.equal(result, 'http://example.com/user-notify.html');
+ });
+
+ test('with https: docs base URL', () => {
+ const base = 'https://example.com/';
+ const result = element._getFilterDocsLink(base);
+ assert.equal(result, 'https://example.com/user-notify.html');
+ });
+
+ test('without docs base URL', () => {
+ const result = element._getFilterDocsLink(null);
+ assert.equal(result, 'https://gerrit-review.googlesource.com/' +
+ 'Documentation/user-notify.html');
+ });
+
+ test('ignores non HTTP links', () => {
+ const base = 'javascript://alert("evil");';
+ const result = element._getFilterDocsLink(base);
+ assert.equal(result, 'https://gerrit-review.googlesource.com/' +
+ 'Documentation/user-notify.html');
+ });
+ });
+
+ suite('when email verification token is provided', () => {
+ let resolveConfirm;
+
+ setup(() => {
+ sandbox.stub(element.$.emailEditor, 'loadData');
+ sandbox.stub(
+ element.$.restAPI,
+ 'confirmEmail',
+ () => new Promise(resolve => { resolveConfirm = resolve; }));
+ element.params = {emailToken: 'foo'};
+ element.attached();
+ });
+
+ test('it is used to confirm email via rest API', () => {
+ assert.isTrue(element.$.restAPI.confirmEmail.calledOnce);
+ assert.isTrue(element.$.restAPI.confirmEmail.calledWith('foo'));
+ });
+
+ test('emails are not loaded initially', () => {
+ assert.isFalse(element.$.emailEditor.loadData.called);
+ });
+
+ test('user emails are loaded after email confirmed', done => {
+ element._loadingPromise.then(() => {
+ assert.isTrue(element.$.emailEditor.loadData.calledOnce);
+ done();
+ });
+ resolveConfirm();
+ });
+
+ test('show-alert is fired when email is confirmed', done => {
+ sandbox.spy(element, 'fire');
+ element._loadingPromise.then(() => {
+ assert.isTrue(
+ element.fire.calledWith('show-alert', {message: 'bar'}));
+ done();
+ });
+ resolveConfirm('bar');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
deleted file mode 100644
index dd02ccd..0000000
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
+++ /dev/null
@@ -1,150 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-ssh-editor">
- <template>
- <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]]"
- link>Click to View</gr-button>
- </td>
- <td>
- <gr-copy-clipboard
- has-tooltip
- button-title="Copy SSH public key to clipboard"
- hide-input
- 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>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-ssh-editor.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
index 44fb48c..814eb7a 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
@@ -14,95 +14,108 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrSshEditor extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-ssh-editor'; }
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../styles/gr-form-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-ssh-editor_html.js';
- static get properties() {
- return {
- hasUnsavedChanges: {
- type: Boolean,
- value: false,
- notify: true,
- },
- _keys: Array,
- /** @type {?} */
- _keyToView: Object,
- _newKey: {
- type: String,
- value: '',
- },
- _keysToRemove: {
- type: Array,
- value() { return []; },
- },
- };
- }
+/** @extends Polymer.Element */
+class GrSshEditor extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- loadData() {
- return this.$.restAPI.getAccountSSHKeys().then(keys => {
- this._keys = keys;
- });
- }
+ static get is() { return 'gr-ssh-editor'; }
- save() {
- const promises = this._keysToRemove.map(key => {
- this.$.restAPI.deleteAccountSSHKey(key.seq);
- });
-
- return Promise.all(promises).then(() => {
- this._keysToRemove = [];
- this.hasUnsavedChanges = false;
- });
- }
-
- _getStatusLabel(isValid) {
- return isValid ? 'Valid' : 'Invalid';
- }
-
- _showKey(e) {
- const el = Polymer.dom(e).localTarget;
- const index = parseInt(el.getAttribute('data-index'), 10);
- this._keyToView = this._keys[index];
- this.$.viewKeyOverlay.open();
- }
-
- _closeOverlay() {
- this.$.viewKeyOverlay.close();
- }
-
- _handleDeleteKey(e) {
- const el = Polymer.dom(e).localTarget;
- const index = parseInt(el.getAttribute('data-index'), 10);
- this.push('_keysToRemove', this._keys[index]);
- this.splice('_keys', index, 1);
- this.hasUnsavedChanges = true;
- }
-
- _handleAddKey() {
- this.$.addButton.disabled = true;
- this.$.newKey.disabled = true;
- return this.$.restAPI.addAccountSSHKey(this._newKey.trim())
- .then(key => {
- this.$.newKey.disabled = false;
- this._newKey = '';
- this.push('_keys', key);
- })
- .catch(() => {
- this.$.addButton.disabled = false;
- this.$.newKey.disabled = false;
- });
- }
-
- _computeAddButtonDisabled(newKey) {
- return !newKey.length;
- }
+ static get properties() {
+ return {
+ hasUnsavedChanges: {
+ type: Boolean,
+ value: false,
+ notify: true,
+ },
+ _keys: Array,
+ /** @type {?} */
+ _keyToView: Object,
+ _newKey: {
+ type: String,
+ value: '',
+ },
+ _keysToRemove: {
+ type: Array,
+ value() { return []; },
+ },
+ };
}
- customElements.define(GrSshEditor.is, GrSshEditor);
-})();
+ loadData() {
+ return this.$.restAPI.getAccountSSHKeys().then(keys => {
+ this._keys = keys;
+ });
+ }
+
+ save() {
+ const promises = this._keysToRemove.map(key => {
+ this.$.restAPI.deleteAccountSSHKey(key.seq);
+ });
+
+ return Promise.all(promises).then(() => {
+ this._keysToRemove = [];
+ this.hasUnsavedChanges = false;
+ });
+ }
+
+ _getStatusLabel(isValid) {
+ return isValid ? 'Valid' : 'Invalid';
+ }
+
+ _showKey(e) {
+ const el = dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ this._keyToView = this._keys[index];
+ this.$.viewKeyOverlay.open();
+ }
+
+ _closeOverlay() {
+ this.$.viewKeyOverlay.close();
+ }
+
+ _handleDeleteKey(e) {
+ const el = dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ this.push('_keysToRemove', this._keys[index]);
+ this.splice('_keys', index, 1);
+ this.hasUnsavedChanges = true;
+ }
+
+ _handleAddKey() {
+ this.$.addButton.disabled = true;
+ this.$.newKey.disabled = true;
+ return this.$.restAPI.addAccountSSHKey(this._newKey.trim())
+ .then(key => {
+ this.$.newKey.disabled = false;
+ this._newKey = '';
+ this.push('_keys', key);
+ })
+ .catch(() => {
+ this.$.addButton.disabled = false;
+ this.$.newKey.disabled = false;
+ });
+ }
+
+ _computeAddButtonDisabled(newKey) {
+ return !newKey.length;
+ }
+}
+
+customElements.define(GrSshEditor.is, GrSshEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.js b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.js
new file mode 100644
index 0000000..3cefb90e
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.js
@@ -0,0 +1,116 @@
+/**
+ * @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.js';
+
+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 has-tooltip="" button-title="Copy SSH public key to clipboard" hide-input="" 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>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
index 82d427d..ed1a7ca 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-ssh-editor</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-ssh-editor.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,149 +30,151 @@
</template>
</test-fixture>
-<script>
- suite('gr-ssh-editor tests', async () => {
- await readyToTest();
- let element;
- let keys;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-ssh-editor.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-ssh-editor tests', () => {
+ let element;
+ let keys;
- setup(done => {
- keys = [{
- seq: 1,
- ssh_public_key: 'ssh-rsa <key 1> comment-one@machine-one',
- encoded_key: '<key 1>',
- algorithm: 'ssh-rsa',
- comment: 'comment-one@machine-one',
- valid: true,
- }, {
- seq: 2,
- ssh_public_key: 'ssh-rsa <key 2> comment-two@machine-two',
- encoded_key: '<key 2>',
- algorithm: 'ssh-rsa',
- comment: 'comment-two@machine-two',
- valid: true,
- }];
+ setup(done => {
+ keys = [{
+ seq: 1,
+ ssh_public_key: 'ssh-rsa <key 1> comment-one@machine-one',
+ encoded_key: '<key 1>',
+ algorithm: 'ssh-rsa',
+ comment: 'comment-one@machine-one',
+ valid: true,
+ }, {
+ seq: 2,
+ ssh_public_key: 'ssh-rsa <key 2> comment-two@machine-two',
+ encoded_key: '<key 2>',
+ algorithm: 'ssh-rsa',
+ comment: 'comment-two@machine-two',
+ valid: true,
+ }];
- stub('gr-rest-api-interface', {
- getAccountSSHKeys() { return Promise.resolve(keys); },
- });
-
- element = fixture('basic');
-
- element.loadData().then(() => { flush(done); });
+ stub('gr-rest-api-interface', {
+ getAccountSSHKeys() { return Promise.resolve(keys); },
});
- test('renders', () => {
- const rows = Polymer.dom(element.root).querySelectorAll('tbody tr');
+ element = fixture('basic');
- assert.equal(rows.length, 2);
+ element.loadData().then(() => { flush(done); });
+ });
- let cells = rows[0].querySelectorAll('td');
- assert.equal(cells[0].textContent, keys[0].comment);
+ test('renders', () => {
+ const rows = dom(element.root).querySelectorAll('tbody tr');
- cells = rows[1].querySelectorAll('td');
- assert.equal(cells[0].textContent, keys[1].comment);
- });
+ assert.equal(rows.length, 2);
- test('remove key', done => {
- const lastKey = keys[1];
+ let cells = rows[0].querySelectorAll('td');
+ assert.equal(cells[0].textContent, keys[0].comment);
- const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountSSHKey',
- () => Promise.resolve());
+ cells = rows[1].querySelectorAll('td');
+ assert.equal(cells[0].textContent, keys[1].comment);
+ });
+ test('remove key', done => {
+ const lastKey = keys[1];
+
+ const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountSSHKey',
+ () => Promise.resolve());
+
+ assert.equal(element._keysToRemove.length, 0);
+ assert.isFalse(element.hasUnsavedChanges);
+
+ // Get the delete button for the last row.
+ const button = dom(element.root).querySelector(
+ 'tbody tr:last-of-type td:nth-child(5) gr-button');
+
+ MockInteractions.tap(button);
+
+ 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);
+
+ element.save().then(() => {
+ assert.isTrue(saveStub.called);
+ assert.equal(saveStub.lastCall.args[0], lastKey.seq);
assert.equal(element._keysToRemove.length, 0);
assert.isFalse(element.hasUnsavedChanges);
-
- // Get the delete button for the last row.
- const button = Polymer.dom(element.root).querySelector(
- 'tbody tr:last-of-type td:nth-child(5) gr-button');
-
- MockInteractions.tap(button);
-
- 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);
-
- element.save().then(() => {
- assert.isTrue(saveStub.called);
- assert.equal(saveStub.lastCall.args[0], lastKey.seq);
- assert.equal(element._keysToRemove.length, 0);
- assert.isFalse(element.hasUnsavedChanges);
- done();
- });
- });
-
- test('show key', () => {
- const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open');
-
- // Get the show button for the last row.
- const button = Polymer.dom(element.root).querySelector(
- 'tbody tr:last-of-type td:nth-child(3) gr-button');
-
- MockInteractions.tap(button);
-
- assert.equal(element._keyToView, keys[1]);
- assert.isTrue(openSpy.called);
- });
-
- test('add key', done => {
- const newKeyString = 'ssh-rsa <key 3> comment-three@machine-three';
- const newKeyObject = {
- seq: 3,
- ssh_public_key: newKeyString,
- encoded_key: '<key 3>',
- algorithm: 'ssh-rsa',
- comment: 'comment-three@machine-three',
- valid: true,
- };
-
- const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey',
- () => Promise.resolve(newKeyObject));
-
- element._newKey = newKeyString;
-
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
-
- element._handleAddKey().then(() => {
- assert.isTrue(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
- assert.equal(element._keys.length, 3);
- done();
- });
-
- assert.isTrue(element.$.addButton.disabled);
- assert.isTrue(element.$.newKey.disabled);
-
- assert.isTrue(addStub.called);
- assert.equal(addStub.lastCall.args[0], newKeyString);
- });
-
- test('add invalid key', done => {
- const newKeyString = 'not even close to valid';
-
- const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey',
- () => Promise.reject(new Error('error')));
-
- element._newKey = newKeyString;
-
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
-
- element._handleAddKey().then(() => {
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
- assert.equal(element._keys.length, 2);
- done();
- });
-
- assert.isTrue(element.$.addButton.disabled);
- assert.isTrue(element.$.newKey.disabled);
-
- assert.isTrue(addStub.called);
- assert.equal(addStub.lastCall.args[0], newKeyString);
+ done();
});
});
+
+ test('show key', () => {
+ const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open');
+
+ // Get the show button for the last row.
+ const button = dom(element.root).querySelector(
+ 'tbody tr:last-of-type td:nth-child(3) gr-button');
+
+ MockInteractions.tap(button);
+
+ assert.equal(element._keyToView, keys[1]);
+ assert.isTrue(openSpy.called);
+ });
+
+ test('add key', done => {
+ const newKeyString = 'ssh-rsa <key 3> comment-three@machine-three';
+ const newKeyObject = {
+ seq: 3,
+ ssh_public_key: newKeyString,
+ encoded_key: '<key 3>',
+ algorithm: 'ssh-rsa',
+ comment: 'comment-three@machine-three',
+ valid: true,
+ };
+
+ const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey',
+ () => Promise.resolve(newKeyObject));
+
+ element._newKey = newKeyString;
+
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+
+ element._handleAddKey().then(() => {
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+ assert.equal(element._keys.length, 3);
+ done();
+ });
+
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isTrue(element.$.newKey.disabled);
+
+ assert.isTrue(addStub.called);
+ assert.equal(addStub.lastCall.args[0], newKeyString);
+ });
+
+ test('add invalid key', done => {
+ const newKeyString = 'not even close to valid';
+
+ const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey',
+ () => Promise.reject(new Error('error')));
+
+ element._newKey = newKeyString;
+
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+
+ element._handleAddKey().then(() => {
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+ assert.equal(element._keys.length, 2);
+ done();
+ });
+
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isTrue(element.$.newKey.disabled);
+
+ assert.isTrue(addStub.called);
+ assert.equal(addStub.lastCall.args[0], newKeyString);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
deleted file mode 100644
index b1ecb2e..0000000
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
+++ /dev/null
@@ -1,129 +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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-watched-projects-editor">
- <template>
- <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
- class="newFilterInput"
- placeholder="branch:name, or other search expression">
- <input
- id="newFilter"
- class="newFilterInput"
- is="iron-input"
- 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>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-watched-projects-editor.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
index df115ca..b8960e8 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
@@ -14,169 +14,181 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const NOTIFICATION_TYPES = [
- {name: 'Changes', key: 'notify_new_changes'},
- {name: 'Patches', key: 'notify_new_patch_sets'},
- {name: 'Comments', key: 'notify_all_comments'},
- {name: 'Submits', key: 'notify_submitted_changes'},
- {name: 'Abandons', key: 'notify_abandoned_changes'},
- ];
+import '@polymer/iron-input/iron-input.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-watched-projects-editor_html.js';
- /** @extends Polymer.Element */
- class GrWatchedProjectsEditor extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-watched-projects-editor'; }
+const NOTIFICATION_TYPES = [
+ {name: 'Changes', key: 'notify_new_changes'},
+ {name: 'Patches', key: 'notify_new_patch_sets'},
+ {name: 'Comments', key: 'notify_all_comments'},
+ {name: 'Submits', key: 'notify_submitted_changes'},
+ {name: 'Abandons', key: 'notify_abandoned_changes'},
+];
- static get properties() {
- return {
- hasUnsavedChanges: {
- type: Boolean,
- value: false,
- notify: true,
+/** @extends Polymer.Element */
+class GrWatchedProjectsEditor extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-watched-projects-editor'; }
+
+ static get properties() {
+ return {
+ hasUnsavedChanges: {
+ type: Boolean,
+ value: false,
+ notify: true,
+ },
+
+ _projects: Array,
+ _projectsToRemove: {
+ type: Array,
+ value() { return []; },
+ },
+ _query: {
+ type: Function,
+ value() {
+ return this._getProjectSuggestions.bind(this);
},
-
- _projects: Array,
- _projectsToRemove: {
- type: Array,
- value() { return []; },
- },
- _query: {
- type: Function,
- value() {
- return this._getProjectSuggestions.bind(this);
- },
- },
- };
- }
-
- loadData() {
- return this.$.restAPI.getWatchedProjects().then(projs => {
- this._projects = projs;
- });
- }
-
- save() {
- let deletePromise;
- if (this._projectsToRemove.length) {
- deletePromise = this.$.restAPI.deleteWatchedProjects(
- this._projectsToRemove);
- } else {
- deletePromise = Promise.resolve();
- }
-
- return deletePromise
- .then(() => this.$.restAPI.saveWatchedProjects(this._projects))
- .then(projects => {
- this._projects = projects;
- this._projectsToRemove = [];
- this.hasUnsavedChanges = false;
- });
- }
-
- _getTypes() {
- return NOTIFICATION_TYPES;
- }
-
- _getTypeCount() {
- return this._getTypes().length;
- }
-
- _computeCheckboxChecked(project, key) {
- return project.hasOwnProperty(key);
- }
-
- _getProjectSuggestions(input) {
- return this.$.restAPI.getSuggestedProjects(input)
- .then(response => {
- const projects = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- projects.push({
- name: key,
- value: response[key],
- });
- }
- return projects;
- });
- }
-
- _handleRemoveProject(e) {
- const el = Polymer.dom(e).localTarget;
- const index = parseInt(el.getAttribute('data-index'), 10);
- const project = this._projects[index];
- this.splice('_projects', index, 1);
- this.push('_projectsToRemove', project);
- this.hasUnsavedChanges = true;
- }
-
- _canAddProject(project, text, filter) {
- if ((!project || !project.id) && !text) { return false; }
-
- // This will only be used if not using the auto complete
- if (!project && text) { return true; }
-
- // Check if the project with filter is already in the list. Compare
- // filters using == to coalesce null and undefined.
- for (let i = 0; i < this._projects.length; i++) {
- if (this._projects[i].project === project.id &&
- this._projects[i].filter == filter) {
- return false;
- }
- }
-
- return true;
- }
-
- _getNewProjectIndex(name, filter) {
- let i;
- for (i = 0; i < this._projects.length; i++) {
- if (this._projects[i].project > name ||
- (this._projects[i].project === name &&
- this._projects[i].filter > filter)) {
- break;
- }
- }
- return i;
- }
-
- _handleAddProject() {
- const newProject = this.$.newProject.value;
- const newProjectName = this.$.newProject.text;
- const filter = this.$.newFilter.value || null;
-
- if (!this._canAddProject(newProject, newProjectName, filter)) { return; }
-
- const insertIndex = this._getNewProjectIndex(newProjectName, filter);
-
- this.splice('_projects', insertIndex, 0, {
- project: newProjectName,
- filter,
- _is_local: true,
- });
-
- this.$.newProject.clear();
- this.$.newFilter.bindValue = '';
- this.hasUnsavedChanges = true;
- }
-
- _handleCheckboxChange(e) {
- const el = Polymer.dom(e).localTarget;
- const index = parseInt(el.getAttribute('data-index'), 10);
- const key = el.getAttribute('data-key');
- const checked = el.checked;
- this.set(['_projects', index, key], !!checked);
- this.hasUnsavedChanges = true;
- }
-
- _handleNotifCellClick(e) {
- const checkbox = Polymer.dom(e.target).querySelector('input');
- if (checkbox) { checkbox.click(); }
- }
+ },
+ };
}
- customElements.define(GrWatchedProjectsEditor.is, GrWatchedProjectsEditor);
-})();
+ loadData() {
+ return this.$.restAPI.getWatchedProjects().then(projs => {
+ this._projects = projs;
+ });
+ }
+
+ save() {
+ let deletePromise;
+ if (this._projectsToRemove.length) {
+ deletePromise = this.$.restAPI.deleteWatchedProjects(
+ this._projectsToRemove);
+ } else {
+ deletePromise = Promise.resolve();
+ }
+
+ return deletePromise
+ .then(() => this.$.restAPI.saveWatchedProjects(this._projects))
+ .then(projects => {
+ this._projects = projects;
+ this._projectsToRemove = [];
+ this.hasUnsavedChanges = false;
+ });
+ }
+
+ _getTypes() {
+ return NOTIFICATION_TYPES;
+ }
+
+ _getTypeCount() {
+ return this._getTypes().length;
+ }
+
+ _computeCheckboxChecked(project, key) {
+ return project.hasOwnProperty(key);
+ }
+
+ _getProjectSuggestions(input) {
+ return this.$.restAPI.getSuggestedProjects(input)
+ .then(response => {
+ const projects = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ projects.push({
+ name: key,
+ value: response[key],
+ });
+ }
+ return projects;
+ });
+ }
+
+ _handleRemoveProject(e) {
+ const el = dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ const project = this._projects[index];
+ this.splice('_projects', index, 1);
+ this.push('_projectsToRemove', project);
+ this.hasUnsavedChanges = true;
+ }
+
+ _canAddProject(project, text, filter) {
+ if ((!project || !project.id) && !text) { return false; }
+
+ // This will only be used if not using the auto complete
+ if (!project && text) { return true; }
+
+ // Check if the project with filter is already in the list. Compare
+ // filters using == to coalesce null and undefined.
+ for (let i = 0; i < this._projects.length; i++) {
+ if (this._projects[i].project === project.id &&
+ this._projects[i].filter == filter) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ _getNewProjectIndex(name, filter) {
+ let i;
+ for (i = 0; i < this._projects.length; i++) {
+ if (this._projects[i].project > name ||
+ (this._projects[i].project === name &&
+ this._projects[i].filter > filter)) {
+ break;
+ }
+ }
+ return i;
+ }
+
+ _handleAddProject() {
+ const newProject = this.$.newProject.value;
+ const newProjectName = this.$.newProject.text;
+ const filter = this.$.newFilter.value || null;
+
+ if (!this._canAddProject(newProject, newProjectName, filter)) { return; }
+
+ const insertIndex = this._getNewProjectIndex(newProjectName, filter);
+
+ this.splice('_projects', insertIndex, 0, {
+ project: newProjectName,
+ filter,
+ _is_local: true,
+ });
+
+ this.$.newProject.clear();
+ this.$.newFilter.bindValue = '';
+ this.hasUnsavedChanges = true;
+ }
+
+ _handleCheckboxChange(e) {
+ const el = dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ const key = el.getAttribute('data-key');
+ const checked = el.checked;
+ this.set(['_projects', index, key], !!checked);
+ this.hasUnsavedChanges = true;
+ }
+
+ _handleNotifCellClick(e) {
+ const checkbox = dom(e.target).querySelector('input');
+ if (checkbox) { checkbox.click(); }
+ }
+}
+
+customElements.define(GrWatchedProjectsEditor.is, GrWatchedProjectsEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.js b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.js
new file mode 100644
index 0000000..bc381e0
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.js
@@ -0,0 +1,93 @@
+/**
+ * @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.js';
+
+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 class="newFilterInput" placeholder="branch:name, or other search expression">
+ <input id="newFilter" class="newFilterInput" is="iron-input" 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>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
index c96d6a0..2bf0ca7 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-settings-view</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-watched-projects-editor.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,184 +30,185 @@
</template>
</test-fixture>
-<script>
- suite('gr-watched-projects-editor tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-watched-projects-editor.js';
+suite('gr-watched-projects-editor tests', () => {
+ let element;
- setup(done => {
- const projects = [
- {
- project: 'project a',
- notify_submitted_changes: true,
- notify_abandoned_changes: true,
- }, {
- project: 'project b',
- filter: 'filter 1',
- notify_new_changes: true,
- }, {
- project: 'project b',
- filter: 'filter 2',
- }, {
- project: 'project c',
- notify_new_changes: true,
- notify_new_patch_sets: true,
- notify_all_comments: true,
- },
- ];
+ setup(done => {
+ const projects = [
+ {
+ project: 'project a',
+ notify_submitted_changes: true,
+ notify_abandoned_changes: true,
+ }, {
+ project: 'project b',
+ filter: 'filter 1',
+ notify_new_changes: true,
+ }, {
+ project: 'project b',
+ filter: 'filter 2',
+ }, {
+ project: 'project c',
+ notify_new_changes: true,
+ notify_new_patch_sets: true,
+ notify_all_comments: true,
+ },
+ ];
- stub('gr-rest-api-interface', {
- getSuggestedProjects(input) {
- if (input.startsWith('th')) {
- return Promise.resolve({'the project': {
- id: 'the project',
- state: 'ACTIVE',
- web_links: [],
- }});
- } else {
- return Promise.resolve({});
- }
- },
- getWatchedProjects() {
- return Promise.resolve(projects);
- },
- });
-
- element = fixture('basic');
-
- element.loadData().then(() => { flush(done); });
+ stub('gr-rest-api-interface', {
+ getSuggestedProjects(input) {
+ if (input.startsWith('th')) {
+ return Promise.resolve({'the project': {
+ id: 'the project',
+ state: 'ACTIVE',
+ web_links: [],
+ }});
+ } else {
+ return Promise.resolve({});
+ }
+ },
+ getWatchedProjects() {
+ return Promise.resolve(projects);
+ },
});
- test('renders', () => {
- const rows = element.shadowRoot
- .querySelector('table').querySelectorAll('tbody tr');
- assert.equal(rows.length, 4);
+ element = fixture('basic');
- function getKeysOfRow(row) {
- const boxes = rows[row].querySelectorAll('input[checked]');
- return Array.prototype.map.call(boxes,
- e => e.getAttribute('data-key'));
- }
+ element.loadData().then(() => { flush(done); });
+ });
- let checkedKeys = getKeysOfRow(0);
- assert.equal(checkedKeys.length, 2);
- assert.equal(checkedKeys[0], 'notify_submitted_changes');
- assert.equal(checkedKeys[1], 'notify_abandoned_changes');
+ test('renders', () => {
+ const rows = element.shadowRoot
+ .querySelector('table').querySelectorAll('tbody tr');
+ assert.equal(rows.length, 4);
- checkedKeys = getKeysOfRow(1);
- assert.equal(checkedKeys.length, 1);
- assert.equal(checkedKeys[0], 'notify_new_changes');
+ function getKeysOfRow(row) {
+ const boxes = rows[row].querySelectorAll('input[checked]');
+ return Array.prototype.map.call(boxes,
+ e => e.getAttribute('data-key'));
+ }
- checkedKeys = getKeysOfRow(2);
- assert.equal(checkedKeys.length, 0);
+ let checkedKeys = getKeysOfRow(0);
+ assert.equal(checkedKeys.length, 2);
+ assert.equal(checkedKeys[0], 'notify_submitted_changes');
+ assert.equal(checkedKeys[1], 'notify_abandoned_changes');
- checkedKeys = getKeysOfRow(3);
- assert.equal(checkedKeys.length, 3);
- assert.equal(checkedKeys[0], 'notify_new_changes');
- assert.equal(checkedKeys[1], 'notify_new_patch_sets');
- assert.equal(checkedKeys[2], 'notify_all_comments');
- });
+ checkedKeys = getKeysOfRow(1);
+ assert.equal(checkedKeys.length, 1);
+ assert.equal(checkedKeys[0], 'notify_new_changes');
- test('_getProjectSuggestions empty', done => {
- element._getProjectSuggestions('nonexistent').then(projects => {
- assert.equal(projects.length, 0);
- done();
- });
- });
+ checkedKeys = getKeysOfRow(2);
+ assert.equal(checkedKeys.length, 0);
- test('_getProjectSuggestions non-empty', done => {
- element._getProjectSuggestions('the project').then(projects => {
- assert.equal(projects.length, 1);
- assert.equal(projects[0].name, 'the project');
- done();
- });
- });
+ checkedKeys = getKeysOfRow(3);
+ assert.equal(checkedKeys.length, 3);
+ assert.equal(checkedKeys[0], 'notify_new_changes');
+ assert.equal(checkedKeys[1], 'notify_new_patch_sets');
+ assert.equal(checkedKeys[2], 'notify_all_comments');
+ });
- test('_getProjectSuggestions non-empty with two letter project', done => {
- element._getProjectSuggestions('th').then(projects => {
- assert.equal(projects.length, 1);
- assert.equal(projects[0].name, 'the project');
- done();
- });
- });
-
- test('_canAddProject', () => {
- assert.isFalse(element._canAddProject(null, null, null));
- assert.isFalse(element._canAddProject({}, null, null));
-
- // Can add a project that is not in the list.
- assert.isTrue(element._canAddProject({id: 'project d'}, null, null));
- assert.isTrue(element._canAddProject({id: 'project d'}, null, 'filter 3'));
-
- // Cannot add a project that is in the list with no filter.
- assert.isFalse(element._canAddProject({id: 'project a'}, null, null));
-
- // Can add a project that is in the list if the filter differs.
- assert.isTrue(element._canAddProject({id: 'project a'}, null, 'filter 4'));
-
- // Cannot add a project that is in the list with the same filter.
- assert.isFalse(element._canAddProject({id: 'project b'}, null, 'filter 1'));
- assert.isFalse(element._canAddProject({id: 'project b'}, null, 'filter 2'));
-
- // Can add a project that is in the list using a new filter.
- assert.isTrue(element._canAddProject({id: 'project b'}, null, 'filter 3'));
-
- // Can add a project that is not added by the auto complete
- assert.isTrue(element._canAddProject(null, 'test', null));
- });
-
- test('_getNewProjectIndex', () => {
- // Projects are sorted in ASCII order.
- 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);
-
- // Projects with filters follow those without
- assert.equal(element._getNewProjectIndex('project c', 'filter'), 4);
- });
-
- test('_handleAddProject', () => {
- element.$.newProject.value = {id: 'project d'};
- element.$.newProject.setText('project d');
- element.$.newFilter.bindValue = '';
-
- element._handleAddProject();
-
- assert.equal(element._projects.length, 5);
- assert.equal(element._projects[4].project, 'project d');
- assert.isNotOk(element._projects[4].filter);
- assert.isTrue(element._projects[4]._is_local);
- });
-
- test('_handleAddProject with invalid inputs', () => {
- element.$.newProject.value = {id: 'project b'};
- element.$.newProject.setText('project b');
- element.$.newFilter.bindValue = 'filter 1';
- element.$.newFilter.value = 'filter 1';
-
- element._handleAddProject();
-
- assert.equal(element._projects.length, 4);
- });
-
- test('_handleRemoveProject', () => {
- assert.equal(element._projectsToRemove, 0);
- const button = element.shadowRoot
- .querySelector('table tbody tr:nth-child(2) gr-button');
- MockInteractions.tap(button);
-
- flushAsynchronousOperations();
-
- const rows = element.shadowRoot
- .querySelector('table tbody').querySelectorAll('tr');
- assert.equal(rows.length, 3);
-
- assert.equal(element._projectsToRemove.length, 1);
- assert.equal(element._projectsToRemove[0].project, 'project b');
+ test('_getProjectSuggestions empty', done => {
+ element._getProjectSuggestions('nonexistent').then(projects => {
+ assert.equal(projects.length, 0);
+ done();
});
});
+
+ test('_getProjectSuggestions non-empty', done => {
+ element._getProjectSuggestions('the project').then(projects => {
+ assert.equal(projects.length, 1);
+ assert.equal(projects[0].name, 'the project');
+ done();
+ });
+ });
+
+ test('_getProjectSuggestions non-empty with two letter project', done => {
+ element._getProjectSuggestions('th').then(projects => {
+ assert.equal(projects.length, 1);
+ assert.equal(projects[0].name, 'the project');
+ done();
+ });
+ });
+
+ test('_canAddProject', () => {
+ assert.isFalse(element._canAddProject(null, null, null));
+ assert.isFalse(element._canAddProject({}, null, null));
+
+ // Can add a project that is not in the list.
+ assert.isTrue(element._canAddProject({id: 'project d'}, null, null));
+ assert.isTrue(element._canAddProject({id: 'project d'}, null, 'filter 3'));
+
+ // Cannot add a project that is in the list with no filter.
+ assert.isFalse(element._canAddProject({id: 'project a'}, null, null));
+
+ // Can add a project that is in the list if the filter differs.
+ assert.isTrue(element._canAddProject({id: 'project a'}, null, 'filter 4'));
+
+ // Cannot add a project that is in the list with the same filter.
+ assert.isFalse(element._canAddProject({id: 'project b'}, null, 'filter 1'));
+ assert.isFalse(element._canAddProject({id: 'project b'}, null, 'filter 2'));
+
+ // Can add a project that is in the list using a new filter.
+ assert.isTrue(element._canAddProject({id: 'project b'}, null, 'filter 3'));
+
+ // Can add a project that is not added by the auto complete
+ assert.isTrue(element._canAddProject(null, 'test', null));
+ });
+
+ test('_getNewProjectIndex', () => {
+ // Projects are sorted in ASCII order.
+ 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);
+
+ // Projects with filters follow those without
+ assert.equal(element._getNewProjectIndex('project c', 'filter'), 4);
+ });
+
+ test('_handleAddProject', () => {
+ element.$.newProject.value = {id: 'project d'};
+ element.$.newProject.setText('project d');
+ element.$.newFilter.bindValue = '';
+
+ element._handleAddProject();
+
+ assert.equal(element._projects.length, 5);
+ assert.equal(element._projects[4].project, 'project d');
+ assert.isNotOk(element._projects[4].filter);
+ assert.isTrue(element._projects[4]._is_local);
+ });
+
+ test('_handleAddProject with invalid inputs', () => {
+ element.$.newProject.value = {id: 'project b'};
+ element.$.newProject.setText('project b');
+ element.$.newFilter.bindValue = 'filter 1';
+ element.$.newFilter.value = 'filter 1';
+
+ element._handleAddProject();
+
+ assert.equal(element._projects.length, 4);
+ });
+
+ test('_handleRemoveProject', () => {
+ assert.equal(element._projectsToRemove, 0);
+ const button = element.shadowRoot
+ .querySelector('table tbody tr:nth-child(2) gr-button');
+ MockInteractions.tap(button);
+
+ flushAsynchronousOperations();
+
+ const rows = element.shadowRoot
+ .querySelector('table tbody').querySelectorAll('tr');
+ assert.equal(rows.length, 3);
+
+ assert.equal(element._projectsToRemove.length, 1);
+ assert.equal(element._projectsToRemove[0].project, 'project b');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
deleted file mode 100644
index 7e2d872..0000000
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
+++ /dev/null
@@ -1,110 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../gr-account-link/gr-account-link.html">
-<link rel="import" href="../gr-button/gr-button.html">
-<link rel="import" href="../gr-icons/gr-icons.html">
-<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-account-chip">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- overflow: hidden;
- }
- .container {
- align-items: center;
- background: var(--chip-background-color);
- border-radius: .75em;
- display: inline-flex;
- padding: 0 var(--spacing-m);
- }
- :host([show-avatar]) .container {
- padding-left: 0;
- }
- gr-button.remove {
- --gr-remove-button-style: {
- border: 0;
- color: var(--deemphasized-text-color);
- font-weight: var(--font-weight-normal);
- height: .6em;
- line-height: 10px;
- margin-left: var(--spacing-xs);
- padding: 0;
- text-decoration: none;
- }
- }
-
- gr-button.remove:hover,
- gr-button.remove:focus {
- --gr-button: {
- @apply --gr-remove-button-style;
- color: #333;
- }
- }
- gr-button.remove {
- --gr-button: {
- @apply --gr-remove-button-style;
- }
- }
- :host:focus {
- border-color: transparent;
- box-shadow: none;
- outline: none;
- }
- :host:focus .container,
- :host:focus gr-button {
- background: #ccc;
- }
- .transparentBackground,
- gr-button.transparentBackground {
- background-color: transparent;
- padding: 0;
- }
- :host([disabled]) {
- opacity: .6;
- pointer-events: none;
- }
- iron-icon {
- height: 1.2rem;
- width: 1.2rem;
- }
- </style>
- <div class$="container [[_getBackgroundClass(transparentBackground)]]">
- <gr-account-link account="[[account]]"
- additional-text="[[additionalText]]">
- </gr-account-link>
- <gr-button
- id="remove"
- link
- hidden$="[[!removable]]"
- hidden
- tabindex="-1"
- aria-label="Remove"
- class$="remove [[_getBackgroundClass(transparentBackground)]]"
- on-click="_handleRemoveTap">
- <iron-icon icon="gr-icons:close"></iron-icon>
- </gr-button>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-account-chip.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
index 8cd2021..22fd1c20 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
@@ -14,80 +14,92 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../gr-account-link/gr-account-link.js';
+import '../gr-button/gr-button.js';
+import '../gr-icons/gr-icons.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-account-chip_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrAccountChip extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-account-chip'; }
+ /**
+ * Fired to indicate a key was pressed while this chip was focused.
+ *
+ * @event account-chip-keydown
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired to indicate this chip should be removed, i.e. when the x button is
+ * clicked or when the remove function is called.
+ *
+ * @event remove
*/
- class GrAccountChip extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-account-chip'; }
- /**
- * Fired to indicate a key was pressed while this chip was focused.
- *
- * @event account-chip-keydown
- */
- /**
- * Fired to indicate this chip should be removed, i.e. when the x button is
- * clicked or when the remove function is called.
- *
- * @event remove
- */
-
- static get properties() {
- return {
- account: Object,
- additionalText: String,
- disabled: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- removable: {
- type: Boolean,
- value: false,
- },
- showAvatar: {
- type: Boolean,
- reflectToAttribute: true,
- },
- transparentBackground: {
- type: Boolean,
- value: false,
- },
- };
- }
-
- /** @override */
- ready() {
- super.ready();
- this._getHasAvatars().then(hasAvatars => {
- this.showAvatar = hasAvatars;
- });
- }
-
- _getBackgroundClass(transparent) {
- return transparent ? 'transparentBackground' : '';
- }
-
- _handleRemoveTap(e) {
- e.preventDefault();
- this.fire('remove', {account: this.account});
- }
-
- _getHasAvatars() {
- return this.$.restAPI.getConfig()
- .then(cfg => Promise.resolve(!!(
- cfg && cfg.plugin && cfg.plugin.has_avatars
- )));
- }
+ static get properties() {
+ return {
+ account: Object,
+ voteableText: String,
+ disabled: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ removable: {
+ type: Boolean,
+ value: false,
+ },
+ showAvatar: {
+ type: Boolean,
+ reflectToAttribute: true,
+ },
+ transparentBackground: {
+ type: Boolean,
+ value: false,
+ },
+ };
}
- customElements.define(GrAccountChip.is, GrAccountChip);
-})();
+ /** @override */
+ ready() {
+ super.ready();
+ this._getHasAvatars().then(hasAvatars => {
+ this.showAvatar = hasAvatars;
+ });
+ }
+
+ _getBackgroundClass(transparent) {
+ return transparent ? 'transparentBackground' : '';
+ }
+
+ _handleRemoveTap(e) {
+ e.preventDefault();
+ this.fire('remove', {account: this.account});
+ }
+
+ _getHasAvatars() {
+ return this.$.restAPI.getConfig()
+ .then(cfg => Promise.resolve(!!(
+ cfg && cfg.plugin && cfg.plugin.has_avatars
+ )));
+ }
+}
+
+customElements.define(GrAccountChip.is, GrAccountChip);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js
new file mode 100644
index 0000000..14bbd57
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js
@@ -0,0 +1,91 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ overflow: hidden;
+ }
+ .container {
+ align-items: center;
+ background: var(--chip-background-color);
+ border-radius: .75em;
+ display: inline-flex;
+ padding: 0 var(--spacing-m);
+ }
+ :host([show-avatar]) .container {
+ padding-left: 0;
+ }
+ gr-button.remove {
+ --gr-remove-button-style: {
+ border: 0;
+ color: var(--deemphasized-text-color);
+ font-weight: var(--font-weight-normal);
+ height: .6em;
+ line-height: 10px;
+ margin-left: var(--spacing-xs);
+ padding: 0;
+ text-decoration: none;
+ }
+ }
+
+ gr-button.remove:hover,
+ gr-button.remove:focus {
+ --gr-button: {
+ @apply --gr-remove-button-style;
+ color: #333;
+ }
+ }
+ gr-button.remove {
+ --gr-button: {
+ @apply --gr-remove-button-style;
+ }
+ }
+ :host:focus {
+ border-color: transparent;
+ box-shadow: none;
+ outline: none;
+ }
+ :host:focus .container,
+ :host:focus gr-button {
+ background: #ccc;
+ }
+ .transparentBackground,
+ gr-button.transparentBackground {
+ background-color: transparent;
+ padding: 0;
+ }
+ :host([disabled]) {
+ opacity: .6;
+ pointer-events: none;
+ }
+ iron-icon {
+ height: 1.2rem;
+ width: 1.2rem;
+ }
+ </style>
+ <div class\$="container [[_getBackgroundClass(transparentBackground)]]">
+ <gr-account-link account="[[account]]" voteable-text="[[voteableText]]">
+ </gr-account-link>
+ <gr-button id="remove" link="" hidden\$="[[!removable]]" hidden="" tabindex="-1" aria-label="Remove" class\$="remove [[_getBackgroundClass(transparentBackground)]]" on-click="_handleRemoveTap">
+ <iron-icon icon="gr-icons:close"></iron-icon>
+ </gr-button>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html
deleted file mode 100644
index 992ea8407..0000000
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html
+++ /dev/null
@@ -1,48 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-account-entry">
- <template>
- <style include="shared-styles">
- gr-autocomplete {
- display: inline-block;
- flex: 1;
- overflow: hidden;
- }
- </style>
- <gr-autocomplete
- id="input"
- borderless="[[borderless]]"
- placeholder="[[placeholder]]"
- threshold="[[suggestFrom]]"
- query="[[querySuggestions]]"
- allow-non-suggested-values="[[allowAnyInput]]"
- on-commit="_handleInputCommit"
- clear-on-commit
- warn-uncommitted
- text="{{_inputText}}"
- vertical-offset="24">
- </gr-autocomplete>
- </template>
- <script src="gr-account-entry.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
index d2a111a..49e984c 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
@@ -14,96 +14,105 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../gr-autocomplete/gr-autocomplete.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-account-entry_html.js';
+
+/**
+ * gr-account-entry is an element for entering account
+ * and/or group with autocomplete support.
+ *
+ * @extends Polymer.Element
+ */
+class GrAccountEntry extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-account-entry'; }
+ /**
+ * Fired when an account is entered.
+ *
+ * @event add
+ */
/**
- * gr-account-entry is an element for entering account
- * and/or group with autocomplete support.
+ * When allowAnyInput is true, account-text-changed is fired when input text
+ * changed. This is needed so that the reply dialog's save button can be
+ * enabled for arbitrary cc's, which don't need a 'commit'.
*
- * @extends Polymer.Element
+ * @event account-text-changed
*/
- class GrAccountEntry extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-account-entry'; }
- /**
- * Fired when an account is entered.
- *
- * @event add
- */
- /**
- * When allowAnyInput is true, account-text-changed is fired when input text
- * changed. This is needed so that the reply dialog's save button can be
- * enabled for arbitrary cc's, which don't need a 'commit'.
- *
- * @event account-text-changed
- */
+ static get properties() {
+ return {
+ allowAnyInput: Boolean,
+ borderless: Boolean,
+ placeholder: String,
- static get properties() {
- return {
- allowAnyInput: Boolean,
- borderless: Boolean,
- placeholder: String,
+ // suggestFrom = 0 to enable default suggestions.
+ suggestFrom: {
+ type: Number,
+ value: 0,
+ },
- // suggestFrom = 0 to enable default suggestions.
- suggestFrom: {
- type: Number,
- value: 0,
+ /** @type {!function(string): !Promise<Array<{name, value}>>} */
+ querySuggestions: {
+ type: Function,
+ notify: true,
+ value() {
+ return input => Promise.resolve([]);
},
+ },
- /** @type {!function(string): !Promise<Array<{name, value}>>} */
- querySuggestions: {
- type: Function,
- notify: true,
- value() {
- return input => Promise.resolve([]);
- },
- },
+ _config: Object,
+ /** The value of the autocomplete entry. */
+ _inputText: {
+ type: String,
+ observer: '_inputTextChanged',
+ },
- _config: Object,
- /** The value of the autocomplete entry. */
- _inputText: {
- type: String,
- observer: '_inputTextChanged',
- },
-
- };
- }
-
- get focusStart() {
- return this.$.input.focusStart;
- }
-
- focus() {
- this.$.input.focus();
- }
-
- clear() {
- this.$.input.clear();
- }
-
- setText(text) {
- this.$.input.setText(text);
- }
-
- getText() {
- return this.$.input.text;
- }
-
- _handleInputCommit(e) {
- this.fire('add', {value: e.detail.value});
- this.$.input.focus();
- }
-
- _inputTextChanged(text) {
- if (text.length && this.allowAnyInput) {
- this.dispatchEvent(new CustomEvent(
- 'account-text-changed', {bubbles: true, composed: true}));
- }
- }
+ };
}
- customElements.define(GrAccountEntry.is, GrAccountEntry);
-})();
+ get focusStart() {
+ return this.$.input.focusStart;
+ }
+
+ focus() {
+ this.$.input.focus();
+ }
+
+ clear() {
+ this.$.input.clear();
+ }
+
+ setText(text) {
+ this.$.input.setText(text);
+ }
+
+ getText() {
+ return this.$.input.text;
+ }
+
+ _handleInputCommit(e) {
+ this.fire('add', {value: e.detail.value});
+ this.$.input.focus();
+ }
+
+ _inputTextChanged(text) {
+ if (text.length && this.allowAnyInput) {
+ this.dispatchEvent(new CustomEvent(
+ 'account-text-changed', {bubbles: true, composed: true}));
+ }
+ }
+}
+
+customElements.define(GrAccountEntry.is, GrAccountEntry);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.js b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.js
new file mode 100644
index 0000000..281526d
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.js
@@ -0,0 +1,29 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ gr-autocomplete {
+ display: inline-block;
+ flex: 1;
+ overflow: hidden;
+ }
+ </style>
+ <gr-autocomplete id="input" borderless="[[borderless]]" placeholder="[[placeholder]]" threshold="[[suggestFrom]]" query="[[querySuggestions]]" allow-non-suggested-values="[[allowAnyInput]]" on-commit="_handleInputCommit" clear-on-commit="" warn-uncommitted="" text="{{_inputText}}" vertical-offset="24">
+ </gr-autocomplete>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html
index 51310eb..6e8b493 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html
@@ -19,17 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-account-entry</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="gr-account-entry.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -37,78 +30,80 @@
</template>
</test-fixture>
-<script>
- suite('gr-account-entry tests', async () => {
- await readyToTest();
- let sandbox;
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-account-entry.js';
+suite('gr-account-entry tests', () => {
+ let sandbox;
+ let element;
- const suggestion1 = {
- email: 'email1@example.com',
- _account_id: 1,
- some_property: 'value',
- };
- const suggestion2 = {
- email: 'email2@example.com',
- _account_id: 2,
- };
- const suggestion3 = {
- email: 'email25@example.com',
- _account_id: 25,
- some_other_property: 'other value',
- };
+ const suggestion1 = {
+ email: 'email1@example.com',
+ _account_id: 1,
+ some_property: 'value',
+ };
+ const suggestion2 = {
+ email: 'email2@example.com',
+ _account_id: 2,
+ };
+ const suggestion3 = {
+ email: 'email25@example.com',
+ _account_id: 25,
+ some_other_property: 'other value',
+ };
- setup(done => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- return flush(done);
- });
+ setup(done => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ return flush(done);
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- suite('stubbed values for querySuggestions', () => {
- setup(() => {
- element.querySuggestions = input => Promise.resolve([
- suggestion1,
- suggestion2,
- suggestion3,
- ]);
- });
- });
-
- test('account-text-changed fired when input text changed and allowAnyInput',
- () => {
- // Spy on query, as that is called when _updateSuggestions proceeds.
- const changeStub = sandbox.stub();
- element.allowAnyInput = true;
- element.querySuggestions = input => Promise.resolve([]);
- element.addEventListener('account-text-changed', changeStub);
- element.$.input.text = 'a';
- assert.isTrue(changeStub.calledOnce);
- element.$.input.text = 'ab';
- assert.isTrue(changeStub.calledTwice);
- });
-
- test('account-text-changed not fired when input text changed without ' +
- 'allowAnyInput', () => {
- // Spy on query, as that is called when _updateSuggestions proceeds.
- const changeStub = sandbox.stub();
- element.querySuggestions = input => Promise.resolve([]);
- element.addEventListener('account-text-changed', changeStub);
- element.$.input.text = 'a';
- assert.isFalse(changeStub.called);
- });
-
- test('setText', () => {
- // Spy on query, as that is called when _updateSuggestions proceeds.
- const suggestSpy = sandbox.spy(element.$.input, 'query');
- element.setText('test text');
- flushAsynchronousOperations();
-
- assert.equal(element.$.input.$.input.value, 'test text');
- assert.isFalse(suggestSpy.called);
+ suite('stubbed values for querySuggestions', () => {
+ setup(() => {
+ element.querySuggestions = input => Promise.resolve([
+ suggestion1,
+ suggestion2,
+ suggestion3,
+ ]);
});
});
+
+ test('account-text-changed fired when input text changed and allowAnyInput',
+ () => {
+ // Spy on query, as that is called when _updateSuggestions proceeds.
+ const changeStub = sandbox.stub();
+ element.allowAnyInput = true;
+ element.querySuggestions = input => Promise.resolve([]);
+ element.addEventListener('account-text-changed', changeStub);
+ element.$.input.text = 'a';
+ assert.isTrue(changeStub.calledOnce);
+ element.$.input.text = 'ab';
+ assert.isTrue(changeStub.calledTwice);
+ });
+
+ test('account-text-changed not fired when input text changed without ' +
+ 'allowAnyInput', () => {
+ // Spy on query, as that is called when _updateSuggestions proceeds.
+ const changeStub = sandbox.stub();
+ element.querySuggestions = input => Promise.resolve([]);
+ element.addEventListener('account-text-changed', changeStub);
+ element.$.input.text = 'a';
+ assert.isFalse(changeStub.called);
+ });
+
+ test('setText', () => {
+ // Spy on query, as that is called when _updateSuggestions proceeds.
+ const suggestSpy = sandbox.spy(element.$.input, 'query');
+ element.setText('test text');
+ flushAsynchronousOperations();
+
+ assert.equal(element.$.input.$.input.value, 'test text');
+ assert.isFalse(suggestSpy.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
deleted file mode 100644
index 4bf1f1b..0000000
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
+++ /dev/null
@@ -1,78 +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.
--->
-
-<link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html">
-<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../gr-avatar/gr-avatar.html">
-<link rel="import" href="../gr-limited-text/gr-limited-text.html">
-<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-account-label">
- <template>
- <style include="shared-styles">
- :host {
- display: inline;
- }
- :host::after {
- content: var(--account-label-suffix);
- }
- gr-avatar {
- height: var(--line-height-normal);
- width: var(--line-height-normal);
- vertical-align: top;
- }
- .text {
- @apply --gr-account-label-text-style;
- }
- .text:hover {
- @apply --gr-account-label-text-hover-style;
- }
- .email,
- .showEmail .name {
- display: none;
- }
- .showEmail .email {
- display: inline-block;
- }
- </style>
- <span>
- <template is="dom-if" if="[[!hideAvatar]]">
- <gr-avatar account="[[account]]"
- image-size="[[avatarImageSize]]"></gr-avatar>
- </template>
- <span class$="text [[_computeShowEmailClass(account)]]">
- <span class="name">
- [[_computeName(account, _serverConfig)]]</span>
- <span class="email">
- [[_computeEmailStr(account)]]
- </span>
- <template is="dom-if" if="[[account.status]]">
- (<gr-limited-text
- disable-tooltip="true"
- limit="[[_computeStatusTextLength(account, _serverConfig)]]"
- text="[[account.status]]">
- </gr-limited-text>)
- </template>
- </span>
- </span>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="../../../scripts/util.js"></script>
- <script src="gr-account-label.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
index 34c4cb6..d279563 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -14,121 +14,63 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.DisplayNameMixin
- * @appliesMixin Gerrit.TooltipMixin
- * @extends Polymer.Element
- */
- class GrAccountLabel extends Polymer.mixinBehaviors( [
- Gerrit.DisplayNameBehavior,
- Gerrit.TooltipBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-account-label'; }
+import '@polymer/iron-icon/iron-icon.js';
+import '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js';
+import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../gr-avatar/gr-avatar.js';
+import '../gr-hovercard-account/gr-hovercard-account.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../scripts/util.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-account-label_html.js';
- static get properties() {
- return {
+/**
+ * @appliesMixin Gerrit.DisplayNameMixin
+ * @extends Polymer.Element
+ */
+class GrAccountLabel extends mixinBehaviors( [
+ Gerrit.DisplayNameBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-account-label'; }
+
+ static get properties() {
+ return {
/**
* @type {{ name: string, status: string }}
*/
- account: Object,
- avatarImageSize: {
- type: Number,
- value: 32,
- },
- title: {
- type: String,
- reflectToAttribute: true,
- computed: '_computeAccountTitle(account, additionalText)',
- },
- additionalText: String,
- hasTooltip: {
- type: Boolean,
- reflectToAttribute: true,
- computed: '_computeHasTooltip(account)',
- },
- hideAvatar: {
- type: Boolean,
- value: false,
- },
- _serverConfig: {
- type: Object,
- value: null,
- },
- };
- }
-
- /** @override */
- ready() {
- super.ready();
- if (!this.additionalText) { this.additionalText = ''; }
- this.$.restAPI.getConfig()
- .then(config => { this._serverConfig = config; });
- }
-
- _computeName(account, config) {
- return this.getUserName(config, account, false);
- }
-
- _computeStatusTextLength(account, config) {
- // 35 as the max length of the name + status
- return Math.max(10, 35 - this._computeName(account, config).length);
- }
-
- _computeAccountTitle(account, tooltip) {
- // Polymer 2: check for undefined
- if ([
- account,
- tooltip,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- if (!account) { return; }
- let result = '';
- if (this._computeName(account, this._serverConfig)) {
- result += this._computeName(account, this._serverConfig);
- }
- if (account.email) {
- result += ` <${account.email}>`;
- }
- if (this.additionalText) {
- result += ` ${this.additionalText}`;
- }
-
- // Show status in the label tooltip instead of
- // in a separate tooltip on status
- if (account.status) {
- result += ` (${account.status})`;
- }
-
- return result;
- }
-
- _computeShowEmailClass(account) {
- if (!account || account.name || !account.email) { return ''; }
- return 'showEmail';
- }
-
- _computeEmailStr(account) {
- if (!account || !account.email) {
- return '';
- }
- if (account.name) {
- return '(' + account.email + ')';
- }
- return account.email;
- }
-
- _computeHasTooltip(account) {
- // If an account has loaded to fire this method, then set to true.
- return !!account;
- }
+ account: Object,
+ voteableText: String,
+ hideAvatar: {
+ type: Boolean,
+ value: false,
+ },
+ _serverConfig: {
+ type: Object,
+ value: null,
+ },
+ };
}
- customElements.define(GrAccountLabel.is, GrAccountLabel);
-})();
+ /** @override */
+ ready() {
+ super.ready();
+ this.$.restAPI.getConfig()
+ .then(config => { this._serverConfig = config; });
+ }
+
+ _computeName(account, config) {
+ return this.getUserName(config, account);
+ }
+}
+
+customElements.define(GrAccountLabel.is, GrAccountLabel);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js
new file mode 100644
index 0000000..a7d01ae
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js
@@ -0,0 +1,60 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: inline;
+ }
+ :host::after {
+ content: var(--account-label-suffix);
+ }
+ gr-avatar {
+ height: var(--line-height-normal);
+ width: var(--line-height-normal);
+ vertical-align: top;
+ }
+ .text {
+ @apply --gr-account-label-text-style;
+ }
+ .text:hover {
+ @apply --gr-account-label-text-hover-style;
+ }
+ iron-icon {
+ width: 14px;
+ height: 14px;
+ vertical-align: top;
+ position: relative;
+ top: 2px;
+ }
+ </style>
+ <span>
+ <gr-hovercard-account account="[[account]]" voteable-text="[[voteableText]]"></gr-hovercard-account>
+ <template is="dom-if" if="[[!hideAvatar]]">
+ <gr-avatar account="[[account]]" image-size="32"></gr-avatar>
+ </template>
+ <span class="text">
+ <span class="name">
+ [[_computeName(account, _serverConfig)]]</span>
+ <template is="dom-if" if="[[account.status]]">
+ <iron-icon icon="gr-icons:calendar"></iron-icon>
+ </template>
+ </span>
+ </span>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
index f5a9b8d..ffe9647 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
@@ -19,17 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-account-label</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="gr-account-label.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -37,151 +30,65 @@
</template>
</test-fixture>
-<script>
- suite('gr-account-label tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-account-label.js';
+suite('gr-account-label tests', () => {
+ let element;
- setup(() => {
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getLoggedIn() { return Promise.resolve(false); },
- });
- element = fixture('basic');
- element._config = {
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getLoggedIn() { return Promise.resolve(false); },
+ });
+ element = fixture('basic');
+ element._config = {
+ user: {
+ anonymous_coward_name: 'Anonymous Coward',
+ },
+ };
+ });
+
+ test('null guard', () => {
+ assert.doesNotThrow(() => {
+ element.account = null;
+ });
+ });
+
+ suite('_computeName', () => {
+ test('not showing anonymous', () => {
+ const account = {name: 'Wyatt'};
+ assert.deepEqual(element._computeName(account, null), 'Wyatt');
+ });
+
+ test('showing anonymous but no config', () => {
+ const account = {};
+ assert.deepEqual(element._computeName(account, null),
+ 'Anonymous');
+ });
+
+ test('test for Anonymous Coward user and replace with Anonymous', () => {
+ const config = {
user: {
anonymous_coward_name: 'Anonymous Coward',
},
};
+ const account = {};
+ assert.deepEqual(element._computeName(account, config),
+ 'Anonymous');
});
- test('null guard', () => {
- assert.doesNotThrow(() => {
- element.account = null;
- });
- });
-
- test('missing email', () => {
- assert.equal('', element._computeEmailStr({name: 'foo'}));
- });
-
- test('computed fields', () => {
- assert.equal(
- element._computeAccountTitle({
- name: 'Andrew Bonventre',
- email: 'andybons+gerrit@gmail.com',
- }, /* additionalText= */ ''),
- 'Andrew Bonventre <andybons+gerrit@gmail.com>');
-
- assert.equal(
- element._computeAccountTitle({
- name: 'Andrew Bonventre',
- }, /* additionalText= */ ''),
- 'Andrew Bonventre');
-
- assert.equal(
- element._computeAccountTitle({
- email: 'andybons+gerrit@gmail.com',
- }, /* additionalText= */ ''),
- 'Anonymous <andybons+gerrit@gmail.com>');
-
- assert.equal(element._computeShowEmailClass(
- {
- name: 'Andrew Bonventre',
- email: 'andybons+gerrit@gmail.com',
- }, /* additionalText= */ ''), '');
-
- assert.equal(element._computeShowEmailClass(
- {
- email: 'andybons+gerrit@gmail.com',
- }, /* additionalText= */ ''), 'showEmail');
-
- assert.equal(element._computeShowEmailClass(
- {name: 'Andrew Bonventre'},
- /* additionalText= */ ''
- ),
- '');
-
- assert.equal(element._computeShowEmailClass(undefined), '');
-
- assert.equal(
- element._computeEmailStr({name: 'test', email: 'test'}), '(test)');
- assert.equal(element._computeEmailStr({email: 'test'}, ''), 'test');
- });
-
- suite('_computeName', () => {
- test('not showing anonymous', () => {
- const account = {name: 'Wyatt'};
- assert.deepEqual(element._computeName(account, null), 'Wyatt');
- });
-
- test('showing anonymous but no config', () => {
- const account = {};
- assert.deepEqual(element._computeName(account, null),
- 'Anonymous');
- });
-
- test('test for Anonymous Coward user and replace with Anonymous', () => {
- const config = {
- user: {
- anonymous_coward_name: 'Anonymous Coward',
- },
- };
- const account = {};
- assert.deepEqual(element._computeName(account, config),
- 'Anonymous');
- });
-
- test('test for anonymous_coward_name', () => {
- const config = {
- user: {
- anonymous_coward_name: 'TestAnon',
- },
- };
- const account = {};
- assert.deepEqual(element._computeName(account, config),
- 'TestAnon');
- });
- });
-
- suite('status in tooltip', () => {
- setup(() => {
- element = fixture('basic');
- element.account = {
- name: 'test',
- email: 'test@google.com',
- status: 'OOO until Aug 10th',
- };
- element._config = {
- user: {
- anonymous_coward_name: 'Anonymous Coward',
- },
- };
- });
-
- test('tooltip should contain status text', () => {
- assert.deepEqual(element.title,
- 'test <test@google.com> (OOO until Aug 10th)');
- });
-
- test('status text should not have tooltip', () => {
- flushAsynchronousOperations();
- assert.deepEqual(element.shadowRoot
- .querySelector('gr-limited-text').title, '');
- });
-
- test('status text should honor the name length and total length', () => {
- assert.deepEqual(
- element._computeStatusTextLength(element.account, element._config),
- 31
- );
- assert.deepEqual(
- element._computeStatusTextLength({
- name: 'a very long long long long name',
- }, element._config),
- 10
- );
- });
+ test('test for anonymous_coward_name', () => {
+ const config = {
+ user: {
+ anonymous_coward_name: 'TestAnon',
+ },
+ };
+ const account = {};
+ assert.deepEqual(element._computeName(account, config),
+ 'TestAnon');
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
deleted file mode 100644
index d3575b2..0000000
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
+++ /dev/null
@@ -1,49 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../gr-account-label/gr-account-label.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-account-link">
- <template>
- <style include="shared-styles">
- :host {
- display: inline-block;
- }
- a {
- color: var(--primary-text-color);
- text-decoration: none;
- }
- gr-account-label {
- --gr-account-label-text-hover-style: {
- text-decoration: underline;
- };
- }
- </style>
- <span>
- <a href$="[[_computeOwnerLink(account)]]" tabindex="-1">
- <gr-account-label account="[[account]]"
- additional-text="[[additionalText]]"
- avatar-image-size="[[avatarImageSize]]"></gr-account-label>
- </a>
- </span>
- </template>
- <script src="gr-account-link.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
index b0ce04c..e0d5583 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,38 +14,44 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @extends Polymer.Element
- */
- class GrAccountLink extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-account-link'; }
+import '../../../scripts/bundled-polymer.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../gr-account-label/gr-account-label.js';
+import '../../../styles/shared-styles.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-account-link_html.js';
- static get properties() {
- return {
- additionalText: String,
- account: Object,
- avatarImageSize: {
- type: Number,
- value: 32,
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @extends Polymer.Element
+ */
+class GrAccountLink extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _computeOwnerLink(account) {
- if (!account) { return; }
- return Gerrit.Nav.getUrlForOwner(
- account.email || account.username || account.name ||
- account._account_id);
- }
+ static get is() { return 'gr-account-link'; }
+
+ static get properties() {
+ return {
+ voteableText: String,
+ account: Object,
+ };
}
- customElements.define(GrAccountLink.is, GrAccountLink);
-})();
+ _computeOwnerLink(account) {
+ if (!account) { return; }
+ return Gerrit.Nav.getUrlForOwner(
+ account.email || account.username || account.name ||
+ account._account_id);
+ }
+}
+
+customElements.define(GrAccountLink.is, GrAccountLink);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js
new file mode 100644
index 0000000..4f1ea44
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js
@@ -0,0 +1,39 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: inline-block;
+ }
+ a {
+ color: var(--primary-text-color);
+ text-decoration: none;
+ }
+ gr-account-label {
+ --gr-account-label-text-hover-style: {
+ text-decoration: underline;
+ };
+ }
+ </style>
+ <span>
+ <a href\$="[[_computeOwnerLink(account)]]" tabindex="-1">
+ <gr-account-label account="[[account]]" voteable-text="[[voteableText]]"></gr-account-label>
+ </a>
+ </span>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
index c648661..dc9cbda 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-account-link</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-account-link.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,48 +30,49 @@
</template>
</test-fixture>
-<script>
- suite('gr-account-link tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-account-link.js';
+suite('gr-account-link tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
});
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('computed fields', () => {
- const url = 'test/url';
- const urlStub = sandbox.stub(Gerrit.Nav, 'getUrlForOwner').returns(url);
- const account = {
- email: 'email',
- username: 'username',
- name: 'name',
- _account_id: '_account_id',
- };
- assert.isNotOk(element._computeOwnerLink());
- assert.equal(element._computeOwnerLink(account), url);
- assert.isTrue(urlStub.lastCall.calledWithExactly('email'));
-
- delete account.email;
- assert.equal(element._computeOwnerLink(account), url);
- assert.isTrue(urlStub.lastCall.calledWithExactly('username'));
-
- delete account.username;
- assert.equal(element._computeOwnerLink(account), url);
- assert.isTrue(urlStub.lastCall.calledWithExactly('name'));
-
- delete account.name;
- assert.equal(element._computeOwnerLink(account), url);
- assert.isTrue(urlStub.lastCall.calledWithExactly('_account_id'));
- });
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('computed fields', () => {
+ const url = 'test/url';
+ const urlStub = sandbox.stub(Gerrit.Nav, 'getUrlForOwner').returns(url);
+ const account = {
+ email: 'email',
+ username: 'username',
+ name: 'name',
+ _account_id: '_account_id',
+ };
+ assert.isNotOk(element._computeOwnerLink());
+ assert.equal(element._computeOwnerLink(account), url);
+ assert.isTrue(urlStub.lastCall.calledWithExactly('email'));
+
+ delete account.email;
+ assert.equal(element._computeOwnerLink(account), url);
+ assert.isTrue(urlStub.lastCall.calledWithExactly('username'));
+
+ delete account.username;
+ assert.equal(element._computeOwnerLink(account), url);
+ assert.isTrue(urlStub.lastCall.calledWithExactly('name'));
+
+ delete account.name;
+ assert.equal(element._computeOwnerLink(account), url);
+ assert.isTrue(urlStub.lastCall.calledWithExactly('_account_id'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.html
deleted file mode 100644
index 37591d8..0000000
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.html
+++ /dev/null
@@ -1,79 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../gr-account-chip/gr-account-chip.html">
-<link rel="import" href="../gr-account-entry/gr-account-entry.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-account-list">
- <template>
- <style include="shared-styles">
- gr-account-chip {
- display: inline-block;
- margin: var(--spacing-xs) var(--spacing-xs) var(--spacing-xs) 0;
- }
- gr-account-entry {
- display: flex;
- flex: 1;
- min-width: 10em;
- margin: var(--spacing-xs) var(--spacing-xs) var(--spacing-xs) 0;
- }
- .group {
- --account-label-suffix: ' (group)';
- }
- .pending-add {
- font-style: italic;
- }
- .list {
- align-items: center;
- display: flex;
- flex-wrap: wrap;
- @apply --account-list-style;
- }
- </style>
- <!--
- NOTE(Issue 6419): Nest the inner dom-repeat template in a div rather than
- as a direct child of the dom-module's template.
- -->
- <div class="list">
- <template id="chips" is="dom-repeat" items="[[accounts]]" as="account">
- <gr-account-chip
- account="[[account]]"
- class$="[[_computeChipClass(account)]]"
- data-account-id$="[[account._account_id]]"
- removable="[[_computeRemovable(account, readonly)]]"
- on-keydown="_handleChipKeydown"
- tabindex="-1">
- </gr-account-chip>
- </template>
- </div>
- <gr-account-entry
- borderless
- hidden$="[[_computeEntryHidden(maxCount, accounts.*, readonly)]]"
- id="entry"
- placeholder="[[placeholder]]"
- on-add="_handleAdd"
- on-input-keydown="_handleInputKeydown"
- allow-any-input="[[allowAnyInput]]"
- query-suggestions="[[_querySuggestions]]">
- </gr-account-entry>
- <slot></slot>
- </template>
- <script src="gr-account-list.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
index 7955d50..2e8f768 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
@@ -14,342 +14,353 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const VALID_EMAIL_ALERT = 'Please input a valid email.';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../gr-account-chip/gr-account-chip.js';
+import '../gr-account-entry/gr-account-entry.js';
+import '../../../styles/shared-styles.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-account-list_html.js';
+const VALID_EMAIL_ALERT = 'Please input a valid email.';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrAccountList extends mixinBehaviors( [
+ // Used in the tests for gr-account-list and other elements tests.
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-account-list'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when user inputs an invalid email address.
+ *
+ * @event show-alert
*/
- class GrAccountList extends Polymer.mixinBehaviors( [
- // Used in the tests for gr-account-list and other elements tests.
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-account-list'; }
- /**
- * Fired when user inputs an invalid email address.
- *
- * @event show-alert
- */
- static get properties() {
- return {
- accounts: {
- type: Array,
- value() { return []; },
- notify: true,
- },
- change: Object,
- filter: Function,
- placeholder: String,
- disabled: {
- type: Function,
- value: false,
- },
+ static get properties() {
+ return {
+ accounts: {
+ type: Array,
+ value() { return []; },
+ notify: true,
+ },
+ change: Object,
+ filter: Function,
+ placeholder: String,
+ disabled: {
+ type: Function,
+ value: false,
+ },
- /**
- * Returns suggestions and convert them to list item
- *
- * @type {Gerrit.GrSuggestionsProvider}
- */
- suggestionsProvider: {
- type: Object,
- },
+ /**
+ * Returns suggestions and convert them to list item
+ *
+ * @type {Gerrit.GrSuggestionsProvider}
+ */
+ suggestionsProvider: {
+ type: Object,
+ },
- /**
- * Needed for template checking since value is initially set to null.
- *
- * @type {?Object}
- */
- pendingConfirmation: {
- type: Object,
- value: null,
- notify: true,
- },
- readonly: {
- type: Boolean,
- value: false,
- },
- /**
- * When true, allows for non-suggested inputs to be added.
- */
- allowAnyInput: {
- type: Boolean,
- value: false,
- },
+ /**
+ * Needed for template checking since value is initially set to null.
+ *
+ * @type {?Object}
+ */
+ pendingConfirmation: {
+ type: Object,
+ value: null,
+ notify: true,
+ },
+ readonly: {
+ type: Boolean,
+ value: false,
+ },
+ /**
+ * When true, allows for non-suggested inputs to be added.
+ */
+ allowAnyInput: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * Array of values (groups/accounts) that are removable. When this prop is
- * undefined, all values are removable.
- */
- removableValues: Array,
- maxCount: {
- type: Number,
- value: 0,
- },
+ /**
+ * Array of values (groups/accounts) that are removable. When this prop is
+ * undefined, all values are removable.
+ */
+ removableValues: Array,
+ maxCount: {
+ type: Number,
+ value: 0,
+ },
- /**
- * Returns suggestion items
- *
- * @type {!function(string): Promise<Array<Gerrit.GrSuggestionItem>>}
- */
- _querySuggestions: {
- type: Function,
- value() {
- return this._getSuggestions.bind(this);
- },
+ /**
+ * Returns suggestion items
+ *
+ * @type {!function(string): Promise<Array<Gerrit.GrSuggestionItem>>}
+ */
+ _querySuggestions: {
+ type: Function,
+ value() {
+ return this._getSuggestions.bind(this);
},
+ },
- /**
- * Set to true to disable suggestions on empty input.
- */
- skipSuggestOnEmpty: {
- type: Boolean,
- value: false,
- },
- };
+ /**
+ * Set to true to disable suggestions on empty input.
+ */
+ skipSuggestOnEmpty: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('remove',
+ e => this._handleRemove(e));
+ }
+
+ get accountChips() {
+ return Array.from(
+ dom(this.root).querySelectorAll('gr-account-chip'));
+ }
+
+ get focusStart() {
+ return this.$.entry.focusStart;
+ }
+
+ _getSuggestions(input) {
+ if (this.skipSuggestOnEmpty && !input) {
+ return Promise.resolve([]);
}
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('remove',
- e => this._handleRemove(e));
+ const provider = this.suggestionsProvider;
+ if (!provider) {
+ return Promise.resolve([]);
}
-
- get accountChips() {
- return Array.from(
- Polymer.dom(this.root).querySelectorAll('gr-account-chip'));
- }
-
- get focusStart() {
- return this.$.entry.focusStart;
- }
-
- _getSuggestions(input) {
- if (this.skipSuggestOnEmpty && !input) {
- return Promise.resolve([]);
+ return provider.getSuggestions(input).then(suggestions => {
+ if (!suggestions) { return []; }
+ if (this.filter) {
+ suggestions = suggestions.filter(this.filter);
}
- const provider = this.suggestionsProvider;
- if (!provider) {
- return Promise.resolve([]);
+ return suggestions.map(suggestion =>
+ provider.makeSuggestionItem(suggestion));
+ });
+ }
+
+ _handleAdd(e) {
+ this._addAccountItem(e.detail.value);
+ }
+
+ _addAccountItem(item) {
+ // 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.
+ if (item.account) {
+ const account =
+ Object.assign({}, item.account, {_pendingAdd: true});
+ this.push('accounts', account);
+ } else if (item.group) {
+ if (item.confirm) {
+ this.pendingConfirmation = item;
+ return;
}
- return provider.getSuggestions(input).then(suggestions => {
- if (!suggestions) { return []; }
- if (this.filter) {
- suggestions = suggestions.filter(this.filter);
- }
- return suggestions.map(suggestion =>
- provider.makeSuggestionItem(suggestion));
- });
- }
-
- _handleAdd(e) {
- this._addAccountItem(e.detail.value);
- }
-
- _addAccountItem(item) {
- // 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.
- if (item.account) {
- const account =
- Object.assign({}, item.account, {_pendingAdd: true});
- this.push('accounts', account);
- } else if (item.group) {
- if (item.confirm) {
- this.pendingConfirmation = item;
- return;
- }
- const group = Object.assign({}, item.group,
- {_pendingAdd: true, _group: true});
- this.push('accounts', group);
- } else if (this.allowAnyInput) {
- if (!item.includes('@')) {
- // Repopulate the input with what the user tried to enter and have
- // a toast tell them why they can't enter it.
- this.$.entry.setText(item);
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {message: VALID_EMAIL_ALERT},
- bubbles: true,
- composed: true,
- }));
- return false;
- } else {
- const account = {email: item, _pendingAdd: true};
- this.push('accounts', account);
- }
- }
- this.pendingConfirmation = null;
- return true;
- }
-
- confirmGroup(group) {
- group = Object.assign(
- {}, group, {confirmed: true, _pendingAdd: true, _group: true});
+ const group = Object.assign({}, item.group,
+ {_pendingAdd: true, _group: true});
this.push('accounts', group);
- this.pendingConfirmation = null;
- }
-
- _computeChipClass(account) {
- const classes = [];
- if (account._group) {
- classes.push('group');
+ } else if (this.allowAnyInput) {
+ if (!item.includes('@')) {
+ // Repopulate the input with what the user tried to enter and have
+ // a toast tell them why they can't enter it.
+ this.$.entry.setText(item);
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {message: VALID_EMAIL_ALERT},
+ bubbles: true,
+ composed: true,
+ }));
+ return false;
+ } else {
+ const account = {email: item, _pendingAdd: true};
+ this.push('accounts', account);
}
- if (account._pendingAdd) {
- classes.push('pendingAdd');
- }
- return classes.join(' ');
}
+ this.pendingConfirmation = null;
+ return true;
+ }
- _accountMatches(a, b) {
- if (a && b) {
- if (a._account_id) {
- return a._account_id === b._account_id;
- }
- if (a.email) {
- return a.email === b.email;
+ confirmGroup(group) {
+ group = Object.assign(
+ {}, group, {confirmed: true, _pendingAdd: true, _group: true});
+ this.push('accounts', group);
+ this.pendingConfirmation = null;
+ }
+
+ _computeChipClass(account) {
+ const classes = [];
+ if (account._group) {
+ classes.push('group');
+ }
+ if (account._pendingAdd) {
+ classes.push('pendingAdd');
+ }
+ return classes.join(' ');
+ }
+
+ _accountMatches(a, b) {
+ if (a && b) {
+ if (a._account_id) {
+ return a._account_id === b._account_id;
+ }
+ if (a.email) {
+ return a.email === b.email;
+ }
+ }
+ return a === b;
+ }
+
+ _computeRemovable(account, readonly) {
+ if (readonly) { return false; }
+ if (this.removableValues) {
+ for (let i = 0; i < this.removableValues.length; i++) {
+ if (this._accountMatches(this.removableValues[i], account)) {
+ return true;
}
}
- return a === b;
+ return !!account._pendingAdd;
}
+ return true;
+ }
- _computeRemovable(account, readonly) {
- if (readonly) { return false; }
- if (this.removableValues) {
- for (let i = 0; i < this.removableValues.length; i++) {
- if (this._accountMatches(this.removableValues[i], account)) {
- return true;
- }
- }
- return !!account._pendingAdd;
+ _handleRemove(e) {
+ const toRemove = e.detail.account;
+ this._removeAccount(toRemove);
+ this.$.entry.focus();
+ }
+
+ _removeAccount(toRemove) {
+ if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) {
+ return;
+ }
+ for (let i = 0; i < this.accounts.length; i++) {
+ let matches;
+ const account = this.accounts[i];
+ if (toRemove._group) {
+ matches = toRemove.id === account.id;
+ } else {
+ matches = this._accountMatches(toRemove, account);
}
- return true;
- }
-
- _handleRemove(e) {
- const toRemove = e.detail.account;
- this._removeAccount(toRemove);
- this.$.entry.focus();
- }
-
- _removeAccount(toRemove) {
- if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) {
+ if (matches) {
+ this.splice('accounts', i, 1);
return;
}
- for (let i = 0; i < this.accounts.length; i++) {
- let matches;
- const account = this.accounts[i];
- if (toRemove._group) {
- matches = toRemove.id === account.id;
- } else {
- matches = this._accountMatches(toRemove, account);
+ }
+ console.warn('received remove event for missing account', toRemove);
+ }
+
+ _getNativeInput(paperInput) {
+ // In Polymer 2 inputElement isn't nativeInput anymore
+ return paperInput.$.nativeInput || paperInput.inputElement;
+ }
+
+ _handleInputKeydown(e) {
+ const input = this._getNativeInput(e.detail.input);
+ if (input.selectionStart !== input.selectionEnd ||
+ input.selectionStart !== 0) {
+ return;
+ }
+ switch (e.detail.keyCode) {
+ case 8: // Backspace
+ this._removeAccount(this.accounts[this.accounts.length - 1]);
+ break;
+ case 37: // Left arrow
+ if (this.accountChips[this.accountChips.length - 1]) {
+ this.accountChips[this.accountChips.length - 1].focus();
}
- if (matches) {
- this.splice('accounts', i, 1);
- return;
- }
- }
- console.warn('received remove event for missing account', toRemove);
- }
-
- _getNativeInput(paperInput) {
- // In Polymer 2 inputElement isn't nativeInput anymore
- return paperInput.$.nativeInput || paperInput.inputElement;
- }
-
- _handleInputKeydown(e) {
- const input = this._getNativeInput(e.detail.input);
- if (input.selectionStart !== input.selectionEnd ||
- input.selectionStart !== 0) {
- return;
- }
- switch (e.detail.keyCode) {
- case 8: // Backspace
- this._removeAccount(this.accounts[this.accounts.length - 1]);
- break;
- case 37: // Left arrow
- if (this.accountChips[this.accountChips.length - 1]) {
- this.accountChips[this.accountChips.length - 1].focus();
- }
- break;
- }
- }
-
- _handleChipKeydown(e) {
- const chip = e.target;
- const chips = this.accountChips;
- const index = chips.indexOf(chip);
- switch (e.keyCode) {
- case 8: // Backspace
- case 13: // Enter
- case 32: // Spacebar
- case 46: // Delete
- this._removeAccount(chip.account);
- // Splice from this array to avoid inconsistent ordering of
- // event handling.
- chips.splice(index, 1);
- if (index < chips.length) {
- chips[index].focus();
- } else if (index > 0) {
- chips[index - 1].focus();
- } else {
- this.$.entry.focus();
- }
- break;
- case 37: // Left arrow
- if (index > 0) {
- chip.blur();
- chips[index - 1].focus();
- }
- break;
- case 39: // Right arrow
- chip.blur();
- if (index < chips.length - 1) {
- chips[index + 1].focus();
- } else {
- this.$.entry.focus();
- }
- break;
- }
- }
-
- /**
- * Submit the text of the entry as a reviewer value, if it exists. If it is
- * a successful submit of the text, clear the entry value.
- *
- * @return {boolean} If there is text in the entry, return true if the
- * submission was successful and false if not. If there is no text,
- * return true.
- */
- submitEntryText() {
- const text = this.$.entry.getText();
- if (!text.length) { return true; }
- const wasSubmitted = this._addAccountItem(text);
- if (wasSubmitted) { this.$.entry.clear(); }
- return wasSubmitted;
- }
-
- additions() {
- return this.accounts
- .filter(account => account._pendingAdd)
- .map(account => {
- if (account._group) {
- return {group: account};
- } else {
- return {account};
- }
- });
- }
-
- _computeEntryHidden(maxCount, accountsRecord, readonly) {
- return (maxCount && maxCount <= accountsRecord.base.length) || readonly;
+ break;
}
}
- customElements.define(GrAccountList.is, GrAccountList);
-})();
+ _handleChipKeydown(e) {
+ const chip = e.target;
+ const chips = this.accountChips;
+ const index = chips.indexOf(chip);
+ switch (e.keyCode) {
+ case 8: // Backspace
+ case 13: // Enter
+ case 32: // Spacebar
+ case 46: // Delete
+ this._removeAccount(chip.account);
+ // Splice from this array to avoid inconsistent ordering of
+ // event handling.
+ chips.splice(index, 1);
+ if (index < chips.length) {
+ chips[index].focus();
+ } else if (index > 0) {
+ chips[index - 1].focus();
+ } else {
+ this.$.entry.focus();
+ }
+ break;
+ case 37: // Left arrow
+ if (index > 0) {
+ chip.blur();
+ chips[index - 1].focus();
+ }
+ break;
+ case 39: // Right arrow
+ chip.blur();
+ if (index < chips.length - 1) {
+ chips[index + 1].focus();
+ } else {
+ this.$.entry.focus();
+ }
+ break;
+ }
+ }
+
+ /**
+ * Submit the text of the entry as a reviewer value, if it exists. If it is
+ * a successful submit of the text, clear the entry value.
+ *
+ * @return {boolean} If there is text in the entry, return true if the
+ * submission was successful and false if not. If there is no text,
+ * return true.
+ */
+ submitEntryText() {
+ const text = this.$.entry.getText();
+ if (!text.length) { return true; }
+ const wasSubmitted = this._addAccountItem(text);
+ if (wasSubmitted) { this.$.entry.clear(); }
+ return wasSubmitted;
+ }
+
+ additions() {
+ return this.accounts
+ .filter(account => account._pendingAdd)
+ .map(account => {
+ if (account._group) {
+ return {group: account};
+ } else {
+ return {account};
+ }
+ });
+ }
+
+ _computeEntryHidden(maxCount, accountsRecord, readonly) {
+ return (maxCount && maxCount <= accountsRecord.base.length) || readonly;
+ }
+}
+
+customElements.define(GrAccountList.is, GrAccountList);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.js
new file mode 100644
index 0000000..2438bc1
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.js
@@ -0,0 +1,57 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ gr-account-chip {
+ display: inline-block;
+ margin: var(--spacing-xs) var(--spacing-xs) var(--spacing-xs) 0;
+ }
+ gr-account-entry {
+ display: flex;
+ flex: 1;
+ min-width: 10em;
+ margin: var(--spacing-xs) var(--spacing-xs) var(--spacing-xs) 0;
+ }
+ .group {
+ --account-label-suffix: ' (group)';
+ }
+ .pending-add {
+ font-style: italic;
+ }
+ .list {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ @apply --account-list-style;
+ }
+ </style>
+ <!--
+ NOTE(Issue 6419): Nest the inner dom-repeat template in a div rather than
+ as a direct child of the dom-module's template.
+ -->
+ <div class="list">
+ <template id="chips" is="dom-repeat" items="[[accounts]]" as="account">
+ <gr-account-chip account="[[account]]" class\$="[[_computeChipClass(account)]]" data-account-id\$="[[account._account_id]]" removable="[[_computeRemovable(account, readonly)]]" on-keydown="_handleChipKeydown" tabindex="-1">
+ </gr-account-chip>
+ </template>
+ </div>
+ <gr-account-entry borderless="" hidden\$="[[_computeEntryHidden(maxCount, accounts.*, readonly)]]" id="entry" placeholder="[[placeholder]]" on-add="_handleAdd" on-input-keydown="_handleInputKeydown" allow-any-input="[[allowAnyInput]]" query-suggestions="[[_querySuggestions]]">
+ </gr-account-entry>
+ <slot></slot>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
index 39d0a88..799670e 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-account-list</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-account-list.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,505 +30,508 @@
</template>
</test-fixture>
-<script>
- class MockSuggestionsProvider {
- getSuggestions(input) {
- return Promise.resolve([]);
- }
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-account-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
- makeSuggestionItem(item) {
- return item;
- }
+class MockSuggestionsProvider {
+ getSuggestions(input) {
+ return Promise.resolve([]);
}
- suite('gr-account-list tests', async () => {
- await readyToTest();
- let _nextAccountId = 0;
- const makeAccount = function() {
- const accountId = ++_nextAccountId;
- return {
- _account_id: accountId,
- };
+ makeSuggestionItem(item) {
+ return item;
+ }
+}
+
+suite('gr-account-list tests', () => {
+ let _nextAccountId = 0;
+ const makeAccount = function() {
+ const accountId = ++_nextAccountId;
+ return {
+ _account_id: accountId,
};
- const makeGroup = function() {
- const groupId = 'group' + (++_nextAccountId);
- return {
- id: groupId,
- _group: true,
- };
+ };
+ const makeGroup = function() {
+ const groupId = 'group' + (++_nextAccountId);
+ return {
+ id: groupId,
+ _group: true,
};
+ };
- let existingAccount1;
- let existingAccount2;
- let sandbox;
- let element;
- let suggestionsProvider;
+ let existingAccount1;
+ let existingAccount2;
+ let sandbox;
+ let element;
+ let suggestionsProvider;
- function getChips() {
- return Polymer.dom(element.root).querySelectorAll('gr-account-chip');
- }
+ function getChips() {
+ return dom(element.root).querySelectorAll('gr-account-chip');
+ }
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ existingAccount1 = makeAccount();
+ existingAccount2 = makeAccount();
+
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ });
+ element = fixture('basic');
+ element.accounts = [existingAccount1, existingAccount2];
+ suggestionsProvider = new MockSuggestionsProvider();
+ element.suggestionsProvider = suggestionsProvider;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('account entry only appears when editable', () => {
+ element.readonly = false;
+ assert.isFalse(element.$.entry.hasAttribute('hidden'));
+ element.readonly = true;
+ assert.isTrue(element.$.entry.hasAttribute('hidden'));
+ });
+
+ test('addition and removal of account/group chips', () => {
+ flushAsynchronousOperations();
+ sandbox.stub(element, '_computeRemovable').returns(true);
+ // Existing accounts are listed.
+ let chips = getChips();
+ assert.equal(chips.length, 2);
+ assert.isFalse(chips[0].classList.contains('pendingAdd'));
+ assert.isFalse(chips[1].classList.contains('pendingAdd'));
+
+ // New accounts are added to end with pendingAdd class.
+ const newAccount = makeAccount();
+ element._handleAdd({
+ detail: {
+ value: {
+ account: newAccount,
+ },
+ },
+ });
+ flushAsynchronousOperations();
+ chips = getChips();
+ assert.equal(chips.length, 3);
+ assert.isFalse(chips[0].classList.contains('pendingAdd'));
+ assert.isFalse(chips[1].classList.contains('pendingAdd'));
+ assert.isTrue(chips[2].classList.contains('pendingAdd'));
+
+ // Removed accounts are taken out of the list.
+ element.fire('remove', {account: existingAccount1});
+ flushAsynchronousOperations();
+ chips = getChips();
+ assert.equal(chips.length, 2);
+ assert.isFalse(chips[0].classList.contains('pendingAdd'));
+ assert.isTrue(chips[1].classList.contains('pendingAdd'));
+
+ // Invalid remove is ignored.
+ element.fire('remove', {account: existingAccount1});
+ element.fire('remove', {account: newAccount});
+ flushAsynchronousOperations();
+ chips = getChips();
+ assert.equal(chips.length, 1);
+ assert.isFalse(chips[0].classList.contains('pendingAdd'));
+
+ // New groups are added to end with pendingAdd and group classes.
+ const newGroup = makeGroup();
+ element._handleAdd({
+ detail: {
+ value: {
+ group: newGroup,
+ },
+ },
+ });
+ flushAsynchronousOperations();
+ chips = getChips();
+ assert.equal(chips.length, 2);
+ assert.isTrue(chips[1].classList.contains('group'));
+ assert.isTrue(chips[1].classList.contains('pendingAdd'));
+
+ // Removed groups are taken out of the list.
+ element.fire('remove', {account: newGroup});
+ flushAsynchronousOperations();
+ chips = getChips();
+ assert.equal(chips.length, 1);
+ assert.isFalse(chips[0].classList.contains('pendingAdd'));
+ });
+
+ test('_getSuggestions uses filter correctly', done => {
+ const originalSuggestions = [
+ {
+ email: 'abc@example.com',
+ text: 'abcd',
+ _account_id: 3,
+ },
+ {
+ email: 'qwe@example.com',
+ text: 'qwer',
+ _account_id: 1,
+ },
+ {
+ email: 'xyz@example.com',
+ text: 'aaaaa',
+ _account_id: 25,
+ },
+ ];
+ sandbox.stub(suggestionsProvider, 'getSuggestions')
+ .returns(Promise.resolve(originalSuggestions));
+ sandbox.stub(suggestionsProvider, 'makeSuggestionItem', suggestion => {
+ return {
+ name: suggestion.email,
+ value: suggestion._account_id,
+ };
+ });
+
+ element._getSuggestions().then(suggestions => {
+ // Default is no filtering.
+ assert.equal(suggestions.length, 3);
+
+ // Set up filter that only accepts suggestion1.
+ const accountId = originalSuggestions[0]._account_id;
+ element.filter = function(suggestion) {
+ return suggestion._account_id === accountId;
+ };
+
+ element._getSuggestions()
+ .then(suggestions => {
+ assert.deepEqual(suggestions,
+ [{name: originalSuggestions[0].email,
+ value: originalSuggestions[0]._account_id}]);
+ })
+ .then(done);
+ });
+ });
+
+ test('_computeChipClass', () => {
+ const account = makeAccount();
+ assert.equal(element._computeChipClass(account), '');
+ account._pendingAdd = true;
+ assert.equal(element._computeChipClass(account), 'pendingAdd');
+ account._group = true;
+ assert.equal(element._computeChipClass(account), 'group pendingAdd');
+ account._pendingAdd = false;
+ assert.equal(element._computeChipClass(account), 'group');
+ });
+
+ test('_computeRemovable', () => {
+ const newAccount = makeAccount();
+ newAccount._pendingAdd = true;
+ element.readonly = false;
+ element.removableValues = [];
+ assert.isFalse(element._computeRemovable(existingAccount1, false));
+ assert.isTrue(element._computeRemovable(newAccount, false));
+
+ element.removableValues = [existingAccount1];
+ assert.isTrue(element._computeRemovable(existingAccount1, false));
+ assert.isTrue(element._computeRemovable(newAccount, false));
+ assert.isFalse(element._computeRemovable(existingAccount2, false));
+
+ element.readonly = true;
+ assert.isFalse(element._computeRemovable(existingAccount1, true));
+ assert.isFalse(element._computeRemovable(newAccount, true));
+ });
+
+ test('submitEntryText', () => {
+ element.allowAnyInput = true;
+ flushAsynchronousOperations();
+
+ const getTextStub = sandbox.stub(element.$.entry, 'getText');
+ getTextStub.onFirstCall().returns('');
+ getTextStub.onSecondCall().returns('test');
+ getTextStub.onThirdCall().returns('test@test');
+
+ // When entry is empty, return true.
+ const clearStub = sandbox.stub(element.$.entry, 'clear');
+ assert.isTrue(element.submitEntryText());
+ assert.isFalse(clearStub.called);
+
+ // When entry is invalid, return false.
+ assert.isFalse(element.submitEntryText());
+ assert.isFalse(clearStub.called);
+
+ // When entry is valid, return true and clear text.
+ assert.isTrue(element.submitEntryText());
+ assert.isTrue(clearStub.called);
+ assert.equal(element.additions()[0].account.email, 'test@test');
+ });
+
+ test('additions returns sanitized new accounts and groups', () => {
+ assert.equal(element.additions().length, 0);
+
+ const newAccount = makeAccount();
+ element._handleAdd({
+ detail: {
+ value: {
+ account: newAccount,
+ },
+ },
+ });
+ const newGroup = makeGroup();
+ element._handleAdd({
+ detail: {
+ value: {
+ group: newGroup,
+ },
+ },
+ });
+
+ assert.deepEqual(element.additions(), [
+ {
+ account: {
+ _account_id: newAccount._account_id,
+ _pendingAdd: true,
+ },
+ },
+ {
+ group: {
+ id: newGroup.id,
+ _group: true,
+ _pendingAdd: true,
+ },
+ },
+ ]);
+ });
+
+ test('large group confirmations', () => {
+ assert.isNull(element.pendingConfirmation);
+ assert.deepEqual(element.additions(), []);
+
+ const group = makeGroup();
+ const reviewer = {
+ group,
+ count: 10,
+ confirm: true,
+ };
+ element._handleAdd({
+ detail: {
+ value: reviewer,
+ },
+ });
+
+ assert.deepEqual(element.pendingConfirmation, reviewer);
+ assert.deepEqual(element.additions(), []);
+
+ element.confirmGroup(group);
+ assert.isNull(element.pendingConfirmation);
+ assert.deepEqual(element.additions(), [
+ {
+ group: {
+ id: group.id,
+ _group: true,
+ _pendingAdd: true,
+ confirmed: true,
+ },
+ },
+ ]);
+ });
+
+ test('removeAccount fails if account is not removable', () => {
+ element.readonly = true;
+ const acct = makeAccount();
+ element.accounts = [acct];
+ element._removeAccount(acct);
+ assert.equal(element.accounts.length, 1);
+ });
+
+ test('max-count', () => {
+ element.maxCount = 1;
+ const acct = makeAccount();
+ element._handleAdd({
+ detail: {
+ value: {
+ account: acct,
+ },
+ },
+ });
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.entry.hasAttribute('hidden'));
+ });
+
+ test('enter text calls suggestions provider', done => {
+ const suggestions = [
+ {
+ email: 'abc@example.com',
+ text: 'abcd',
+ },
+ {
+ email: 'qwe@example.com',
+ text: 'qwer',
+ },
+ ];
+ const getSuggestionsStub =
+ sandbox.stub(suggestionsProvider, 'getSuggestions')
+ .returns(Promise.resolve(suggestions));
+
+ const makeSuggestionItemStub =
+ sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item);
+
+ const input = element.$.entry.$.input;
+
+ input.text = 'newTest';
+ MockInteractions.focus(input.$.input);
+ input.noDebounce = true;
+ flushAsynchronousOperations();
+ flush(() => {
+ assert.isTrue(getSuggestionsStub.calledOnce);
+ assert.equal(getSuggestionsStub.lastCall.args[0], 'newTest');
+ assert.equal(makeSuggestionItemStub.getCalls().length, 2);
+ done();
+ });
+ });
+
+ test('suggestion on empty', done => {
+ element.skipSuggestOnEmpty = false;
+ const suggestions = [
+ {
+ email: 'abc@example.com',
+ text: 'abcd',
+ },
+ {
+ email: 'qwe@example.com',
+ text: 'qwer',
+ },
+ ];
+ const getSuggestionsStub =
+ sandbox.stub(suggestionsProvider, 'getSuggestions')
+ .returns(Promise.resolve(suggestions));
+
+ const makeSuggestionItemStub =
+ sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item);
+
+ const input = element.$.entry.$.input;
+
+ input.text = '';
+ MockInteractions.focus(input.$.input);
+ input.noDebounce = true;
+ flushAsynchronousOperations();
+ flush(() => {
+ assert.isTrue(getSuggestionsStub.calledOnce);
+ assert.equal(getSuggestionsStub.lastCall.args[0], '');
+ assert.equal(makeSuggestionItemStub.getCalls().length, 2);
+ done();
+ });
+ });
+
+ test('skip suggestion on empty', done => {
+ element.skipSuggestOnEmpty = true;
+ const getSuggestionsStub =
+ sandbox.stub(suggestionsProvider, 'getSuggestions')
+ .returns(Promise.resolve([]));
+
+ const input = element.$.entry.$.input;
+
+ input.text = '';
+ MockInteractions.focus(input.$.input);
+ input.noDebounce = true;
+ flushAsynchronousOperations();
+ flush(() => {
+ assert.isTrue(getSuggestionsStub.notCalled);
+ done();
+ });
+ });
+
+ suite('allowAnyInput', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
- existingAccount1 = makeAccount();
- existingAccount2 = makeAccount();
-
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- element.accounts = [existingAccount1, existingAccount2];
- suggestionsProvider = new MockSuggestionsProvider();
- element.suggestionsProvider = suggestionsProvider;
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('account entry only appears when editable', () => {
- element.readonly = false;
- assert.isFalse(element.$.entry.hasAttribute('hidden'));
- element.readonly = true;
- assert.isTrue(element.$.entry.hasAttribute('hidden'));
- });
-
- test('addition and removal of account/group chips', () => {
- flushAsynchronousOperations();
- sandbox.stub(element, '_computeRemovable').returns(true);
- // Existing accounts are listed.
- let chips = getChips();
- assert.equal(chips.length, 2);
- assert.isFalse(chips[0].classList.contains('pendingAdd'));
- assert.isFalse(chips[1].classList.contains('pendingAdd'));
-
- // New accounts are added to end with pendingAdd class.
- const newAccount = makeAccount();
- element._handleAdd({
- detail: {
- value: {
- account: newAccount,
- },
- },
- });
- flushAsynchronousOperations();
- chips = getChips();
- assert.equal(chips.length, 3);
- assert.isFalse(chips[0].classList.contains('pendingAdd'));
- assert.isFalse(chips[1].classList.contains('pendingAdd'));
- assert.isTrue(chips[2].classList.contains('pendingAdd'));
-
- // Removed accounts are taken out of the list.
- element.fire('remove', {account: existingAccount1});
- flushAsynchronousOperations();
- chips = getChips();
- assert.equal(chips.length, 2);
- assert.isFalse(chips[0].classList.contains('pendingAdd'));
- assert.isTrue(chips[1].classList.contains('pendingAdd'));
-
- // Invalid remove is ignored.
- element.fire('remove', {account: existingAccount1});
- element.fire('remove', {account: newAccount});
- flushAsynchronousOperations();
- chips = getChips();
- assert.equal(chips.length, 1);
- assert.isFalse(chips[0].classList.contains('pendingAdd'));
-
- // New groups are added to end with pendingAdd and group classes.
- const newGroup = makeGroup();
- element._handleAdd({
- detail: {
- value: {
- group: newGroup,
- },
- },
- });
- flushAsynchronousOperations();
- chips = getChips();
- assert.equal(chips.length, 2);
- assert.isTrue(chips[1].classList.contains('group'));
- assert.isTrue(chips[1].classList.contains('pendingAdd'));
-
- // Removed groups are taken out of the list.
- element.fire('remove', {account: newGroup});
- flushAsynchronousOperations();
- chips = getChips();
- assert.equal(chips.length, 1);
- assert.isFalse(chips[0].classList.contains('pendingAdd'));
- });
-
- test('_getSuggestions uses filter correctly', done => {
- const originalSuggestions = [
- {
- email: 'abc@example.com',
- text: 'abcd',
- _account_id: 3,
- },
- {
- email: 'qwe@example.com',
- text: 'qwer',
- _account_id: 1,
- },
- {
- email: 'xyz@example.com',
- text: 'aaaaa',
- _account_id: 25,
- },
- ];
- sandbox.stub(suggestionsProvider, 'getSuggestions')
- .returns(Promise.resolve(originalSuggestions));
- sandbox.stub(suggestionsProvider, 'makeSuggestionItem', suggestion => {
- return {
- name: suggestion.email,
- value: suggestion._account_id,
- };
- });
-
- element._getSuggestions().then(suggestions => {
- // Default is no filtering.
- assert.equal(suggestions.length, 3);
-
- // Set up filter that only accepts suggestion1.
- const accountId = originalSuggestions[0]._account_id;
- element.filter = function(suggestion) {
- return suggestion._account_id === accountId;
- };
-
- element._getSuggestions()
- .then(suggestions => {
- assert.deepEqual(suggestions,
- [{name: originalSuggestions[0].email,
- value: originalSuggestions[0]._account_id}]);
- })
- .then(done);
- });
- });
-
- test('_computeChipClass', () => {
- const account = makeAccount();
- assert.equal(element._computeChipClass(account), '');
- account._pendingAdd = true;
- assert.equal(element._computeChipClass(account), 'pendingAdd');
- account._group = true;
- assert.equal(element._computeChipClass(account), 'group pendingAdd');
- account._pendingAdd = false;
- assert.equal(element._computeChipClass(account), 'group');
- });
-
- test('_computeRemovable', () => {
- const newAccount = makeAccount();
- newAccount._pendingAdd = true;
- element.readonly = false;
- element.removableValues = [];
- assert.isFalse(element._computeRemovable(existingAccount1, false));
- assert.isTrue(element._computeRemovable(newAccount, false));
-
- element.removableValues = [existingAccount1];
- assert.isTrue(element._computeRemovable(existingAccount1, false));
- assert.isTrue(element._computeRemovable(newAccount, false));
- assert.isFalse(element._computeRemovable(existingAccount2, false));
-
- element.readonly = true;
- assert.isFalse(element._computeRemovable(existingAccount1, true));
- assert.isFalse(element._computeRemovable(newAccount, true));
- });
-
- test('submitEntryText', () => {
element.allowAnyInput = true;
- flushAsynchronousOperations();
-
- const getTextStub = sandbox.stub(element.$.entry, 'getText');
- getTextStub.onFirstCall().returns('');
- getTextStub.onSecondCall().returns('test');
- getTextStub.onThirdCall().returns('test@test');
-
- // When entry is empty, return true.
- const clearStub = sandbox.stub(element.$.entry, 'clear');
- assert.isTrue(element.submitEntryText());
- assert.isFalse(clearStub.called);
-
- // When entry is invalid, return false.
- assert.isFalse(element.submitEntryText());
- assert.isFalse(clearStub.called);
-
- // When entry is valid, return true and clear text.
- assert.isTrue(element.submitEntryText());
- assert.isTrue(clearStub.called);
- assert.equal(element.additions()[0].account.email, 'test@test');
});
- test('additions returns sanitized new accounts and groups', () => {
- assert.equal(element.additions().length, 0);
-
- const newAccount = makeAccount();
- element._handleAdd({
- detail: {
- value: {
- account: newAccount,
- },
- },
- });
- const newGroup = makeGroup();
- element._handleAdd({
- detail: {
- value: {
- group: newGroup,
- },
- },
- });
-
- assert.deepEqual(element.additions(), [
- {
- account: {
- _account_id: newAccount._account_id,
- _pendingAdd: true,
- },
- },
- {
- group: {
- id: newGroup.id,
- _group: true,
- _pendingAdd: true,
- },
- },
- ]);
+ test('adds emails', () => {
+ const accountLen = element.accounts.length;
+ element._handleAdd({detail: {value: 'test@test'}});
+ assert.equal(element.accounts.length, accountLen + 1);
+ assert.equal(element.accounts[accountLen].email, 'test@test');
});
- test('large group confirmations', () => {
- assert.isNull(element.pendingConfirmation);
- assert.deepEqual(element.additions(), []);
-
- const group = makeGroup();
- const reviewer = {
- group,
- count: 10,
- confirm: true,
- };
- element._handleAdd({
- detail: {
- value: reviewer,
- },
- });
-
- assert.deepEqual(element.pendingConfirmation, reviewer);
- assert.deepEqual(element.additions(), []);
-
- element.confirmGroup(group);
- assert.isNull(element.pendingConfirmation);
- assert.deepEqual(element.additions(), [
- {
- group: {
- id: group.id,
- _group: true,
- _pendingAdd: true,
- confirmed: true,
- },
- },
- ]);
+ test('toasts on invalid email', () => {
+ const toastHandler = sandbox.stub();
+ element.addEventListener('show-alert', toastHandler);
+ element._handleAdd({detail: {value: 'test'}});
+ assert.isTrue(toastHandler.called);
});
+ });
- test('removeAccount fails if account is not removable', () => {
- element.readonly = true;
- const acct = makeAccount();
- element.accounts = [acct];
- element._removeAccount(acct);
- assert.equal(element.accounts.length, 1);
- });
+ test('_accountMatches', () => {
+ const acct = makeAccount();
- test('max-count', () => {
- element.maxCount = 1;
- const acct = makeAccount();
- element._handleAdd({
- detail: {
- value: {
- account: acct,
- },
- },
- });
- flushAsynchronousOperations();
- assert.isTrue(element.$.entry.hasAttribute('hidden'));
- });
+ assert.isTrue(element._accountMatches(acct, acct));
+ acct.email = 'test';
+ assert.isTrue(element._accountMatches(acct, acct));
+ assert.isTrue(element._accountMatches({email: 'test'}, acct));
- test('enter text calls suggestions provider', done => {
- const suggestions = [
- {
- email: 'abc@example.com',
- text: 'abcd',
- },
- {
- email: 'qwe@example.com',
- text: 'qwer',
- },
- ];
- const getSuggestionsStub =
- sandbox.stub(suggestionsProvider, 'getSuggestions')
- .returns(Promise.resolve(suggestions));
+ assert.isFalse(element._accountMatches({}, acct));
+ assert.isFalse(element._accountMatches({email: 'test2'}, acct));
+ assert.isFalse(element._accountMatches({_account_id: -1}, acct));
+ });
- const makeSuggestionItemStub =
- sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item);
-
+ suite('keyboard interactions', () => {
+ test('backspace at text input start removes last account', done => {
const input = element.$.entry.$.input;
-
- input.text = 'newTest';
- MockInteractions.focus(input.$.input);
- input.noDebounce = true;
- flushAsynchronousOperations();
+ sandbox.stub(input, '_updateSuggestions');
+ sandbox.stub(element, '_computeRemovable').returns(true);
flush(() => {
- assert.isTrue(getSuggestionsStub.calledOnce);
- assert.equal(getSuggestionsStub.lastCall.args[0], 'newTest');
- assert.equal(makeSuggestionItemStub.getCalls().length, 2);
- done();
- });
- });
-
- test('suggestion on empty', done => {
- element.skipSuggestOnEmpty = false;
- const suggestions = [
- {
- email: 'abc@example.com',
- text: 'abcd',
- },
- {
- email: 'qwe@example.com',
- text: 'qwer',
- },
- ];
- const getSuggestionsStub =
- sandbox.stub(suggestionsProvider, 'getSuggestions')
- .returns(Promise.resolve(suggestions));
-
- const makeSuggestionItemStub =
- sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item);
-
- const input = element.$.entry.$.input;
-
- input.text = '';
- MockInteractions.focus(input.$.input);
- input.noDebounce = true;
- flushAsynchronousOperations();
- flush(() => {
- assert.isTrue(getSuggestionsStub.calledOnce);
- assert.equal(getSuggestionsStub.lastCall.args[0], '');
- assert.equal(makeSuggestionItemStub.getCalls().length, 2);
- done();
- });
- });
-
- test('skip suggestion on empty', done => {
- element.skipSuggestOnEmpty = true;
- const getSuggestionsStub =
- sandbox.stub(suggestionsProvider, 'getSuggestions')
- .returns(Promise.resolve([]));
-
- const input = element.$.entry.$.input;
-
- input.text = '';
- MockInteractions.focus(input.$.input);
- input.noDebounce = true;
- flushAsynchronousOperations();
- flush(() => {
- assert.isTrue(getSuggestionsStub.notCalled);
- done();
- });
- });
-
- suite('allowAnyInput', () => {
- setup(() => {
- element.allowAnyInput = true;
- });
-
- test('adds emails', () => {
- const accountLen = element.accounts.length;
- element._handleAdd({detail: {value: 'test@test'}});
- assert.equal(element.accounts.length, accountLen + 1);
- assert.equal(element.accounts[accountLen].email, 'test@test');
- });
-
- test('toasts on invalid email', () => {
- const toastHandler = sandbox.stub();
- element.addEventListener('show-alert', toastHandler);
- element._handleAdd({detail: {value: 'test'}});
- assert.isTrue(toastHandler.called);
- });
- });
-
- test('_accountMatches', () => {
- const acct = makeAccount();
-
- assert.isTrue(element._accountMatches(acct, acct));
- acct.email = 'test';
- assert.isTrue(element._accountMatches(acct, acct));
- assert.isTrue(element._accountMatches({email: 'test'}, acct));
-
- assert.isFalse(element._accountMatches({}, acct));
- assert.isFalse(element._accountMatches({email: 'test2'}, acct));
- assert.isFalse(element._accountMatches({_account_id: -1}, acct));
- });
-
- suite('keyboard interactions', () => {
- test('backspace at text input start removes last account', done => {
- const input = element.$.entry.$.input;
- sandbox.stub(input, '_updateSuggestions');
- sandbox.stub(element, '_computeRemovable').returns(true);
- flush(() => {
- // Next line is a workaround for Firefix not moving cursor
- // on input field update
- assert.equal(
- element._getNativeInput(input.$.input).selectionStart, 0);
- input.text = 'test';
- MockInteractions.focus(input.$.input);
- flushAsynchronousOperations();
- assert.equal(element.accounts.length, 2);
- MockInteractions.pressAndReleaseKeyOn(
- element._getNativeInput(input.$.input), 8); // Backspace
- assert.equal(element.accounts.length, 2);
- input.text = '';
- MockInteractions.pressAndReleaseKeyOn(
- element._getNativeInput(input.$.input), 8); // Backspace
- flushAsynchronousOperations();
- assert.equal(element.accounts.length, 1);
- done();
- });
- });
-
- test('arrow key navigation', done => {
- const input = element.$.entry.$.input;
+ // Next line is a workaround for Firefix not moving cursor
+ // on input field update
+ assert.equal(
+ element._getNativeInput(input.$.input).selectionStart, 0);
+ input.text = 'test';
+ MockInteractions.focus(input.$.input);
+ flushAsynchronousOperations();
+ assert.equal(element.accounts.length, 2);
+ MockInteractions.pressAndReleaseKeyOn(
+ element._getNativeInput(input.$.input), 8); // Backspace
+ assert.equal(element.accounts.length, 2);
input.text = '';
- element.accounts = [makeAccount(), makeAccount()];
- flush(() => {
- MockInteractions.focus(input.$.input);
- flushAsynchronousOperations();
- const chips = element.accountChips;
- const chipsOneSpy = sandbox.spy(chips[1], 'focus');
- MockInteractions.pressAndReleaseKeyOn(input.$.input, 37); // Left
- assert.isTrue(chipsOneSpy.called);
- const chipsZeroSpy = sandbox.spy(chips[0], 'focus');
- MockInteractions.pressAndReleaseKeyOn(chips[1], 37); // Left
- assert.isTrue(chipsZeroSpy.called);
- MockInteractions.pressAndReleaseKeyOn(chips[0], 37); // Left
- assert.isTrue(chipsZeroSpy.calledOnce);
- MockInteractions.pressAndReleaseKeyOn(chips[0], 39); // Right
- assert.isTrue(chipsOneSpy.calledTwice);
- done();
- });
+ MockInteractions.pressAndReleaseKeyOn(
+ element._getNativeInput(input.$.input), 8); // Backspace
+ flushAsynchronousOperations();
+ assert.equal(element.accounts.length, 1);
+ done();
});
+ });
- test('delete', done => {
- element.accounts = [makeAccount(), makeAccount()];
- flush(() => {
- const focusSpy = sandbox.spy(element.accountChips[1], 'focus');
- const removeSpy = sandbox.spy(element, '_removeAccount');
- MockInteractions.pressAndReleaseKeyOn(
- element.accountChips[0], 8); // Backspace
- assert.isTrue(focusSpy.called);
- assert.isTrue(removeSpy.calledOnce);
+ test('arrow key navigation', done => {
+ const input = element.$.entry.$.input;
+ input.text = '';
+ element.accounts = [makeAccount(), makeAccount()];
+ flush(() => {
+ MockInteractions.focus(input.$.input);
+ flushAsynchronousOperations();
+ const chips = element.accountChips;
+ const chipsOneSpy = sandbox.spy(chips[1], 'focus');
+ MockInteractions.pressAndReleaseKeyOn(input.$.input, 37); // Left
+ assert.isTrue(chipsOneSpy.called);
+ const chipsZeroSpy = sandbox.spy(chips[0], 'focus');
+ MockInteractions.pressAndReleaseKeyOn(chips[1], 37); // Left
+ assert.isTrue(chipsZeroSpy.called);
+ MockInteractions.pressAndReleaseKeyOn(chips[0], 37); // Left
+ assert.isTrue(chipsZeroSpy.calledOnce);
+ MockInteractions.pressAndReleaseKeyOn(chips[0], 39); // Right
+ assert.isTrue(chipsOneSpy.calledTwice);
+ done();
+ });
+ });
- MockInteractions.pressAndReleaseKeyOn(
- element.accountChips[1], 46); // Delete
- assert.isTrue(removeSpy.calledTwice);
- done();
- });
+ test('delete', done => {
+ element.accounts = [makeAccount(), makeAccount()];
+ flush(() => {
+ const focusSpy = sandbox.spy(element.accountChips[1], 'focus');
+ const removeSpy = sandbox.spy(element, '_removeAccount');
+ MockInteractions.pressAndReleaseKeyOn(
+ element.accountChips[0], 8); // Backspace
+ assert.isTrue(focusSpy.called);
+ assert.isTrue(removeSpy.calledOnce);
+
+ MockInteractions.pressAndReleaseKeyOn(
+ element.accountChips[1], 46); // Delete
+ assert.isTrue(removeSpy.calledTwice);
+ done();
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
deleted file mode 100644
index 0d44164..0000000
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
+++ /dev/null
@@ -1,86 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../gr-button/gr-button.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<script src="../../../scripts/rootElement.js"></script>
-
-<dom-module id="gr-alert">
- <template>
- <style include="shared-styles">
- /**
- * ALERT: DO NOT ADD TRANSITION PROPERTIES WITHOUT PROPERLY UNDERSTANDING
- * HOW THEY ARE USED IN THE CODE.
- */
- :host([toast]) {
- background-color: var(--tooltip-background-color);
- bottom: 1.25rem;
- border-radius: var(--border-radius);
- box-shadow: var(--elevation-level-2);
- color: var(--view-background-color);
- left: 1.25rem;
- position: fixed;
- transform: translateY(5rem);
- transition: transform var(--gr-alert-transition-duration, 80ms) ease-out;
- z-index: 1000;
- }
- :host([shown]) {
- transform: translateY(0);
- }
- /**
- * NOTE: To avoid style being overwritten by outside of the shadow DOM
- * (as outside styles always win), .content-wrapper is introduced as a
- * wrapper around main content to have better encapsulation, styles that
- * may be affected by outside should be defined on it.
- * In this case, `padding:0px` is defined in main.css for all elements
- * with the universal selector: *.
- */
- .content-wrapper {
- padding: var(--spacing-l) var(--spacing-xl);
- }
- .text {
- color: var(--tooltip-text-color);
- display: inline-block;
- max-height: 10rem;
- max-width: 80vw;
- vertical-align: bottom;
- word-break: break-all;
- }
- .action {
- color: var(--link-color);
- font-weight: var(--font-weight-bold);
- margin-left: var(--spacing-l);
- text-decoration: none;
- --gr-button: {
- padding: 0;
- }
- }
- </style>
- <div class="content-wrapper">
- <span class="text">[[text]]</span>
- <gr-button
- link
- class="action"
- hidden$="[[_hideActionButton]]"
- on-click="_handleActionTap">[[actionText]]</gr-button>
- </div>
- </template>
- <script src="gr-alert.js"></script>
-</dom-module>
-
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
index 6a0769d..dc8eea3 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -14,94 +14,102 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrAlert extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-alert'; }
- /**
- * Fired when the action button is pressed.
- *
- * @event action
- */
+import '../gr-button/gr-button.js';
+import '../../../styles/shared-styles.js';
+import '../../../scripts/rootElement.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-alert_html.js';
- static get properties() {
- return {
- text: String,
- actionText: String,
- /** @type {?string} */
- type: String,
- shown: {
- type: Boolean,
- value: true,
- readOnly: true,
- reflectToAttribute: true,
- },
- toast: {
- type: Boolean,
- value: true,
- reflectToAttribute: true,
- },
+/** @extends Polymer.Element */
+class GrAlert extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _hideActionButton: Boolean,
- _boundTransitionEndHandler: {
- type: Function,
- value() { return this._handleTransitionEnd.bind(this); },
- },
- _actionCallback: Function,
- };
- }
+ static get is() { return 'gr-alert'; }
+ /**
+ * Fired when the action button is pressed.
+ *
+ * @event action
+ */
- /** @override */
- attached() {
- super.attached();
- this.addEventListener('transitionend', this._boundTransitionEndHandler);
- }
+ static get properties() {
+ return {
+ text: String,
+ actionText: String,
+ /** @type {?string} */
+ type: String,
+ shown: {
+ type: Boolean,
+ value: true,
+ readOnly: true,
+ reflectToAttribute: true,
+ },
+ toast: {
+ type: Boolean,
+ value: true,
+ reflectToAttribute: true,
+ },
- /** @override */
- detached() {
- super.detached();
- this.removeEventListener('transitionend',
- this._boundTransitionEndHandler);
- }
+ _hideActionButton: Boolean,
+ _boundTransitionEndHandler: {
+ type: Function,
+ value() { return this._handleTransitionEnd.bind(this); },
+ },
+ _actionCallback: Function,
+ };
+ }
- show(text, opt_actionText, opt_actionCallback) {
- this.text = text;
- this.actionText = opt_actionText;
- this._hideActionButton = !opt_actionText;
- this._actionCallback = opt_actionCallback;
- Gerrit.getRootElement().appendChild(this);
- this._setShown(true);
- }
+ /** @override */
+ attached() {
+ super.attached();
+ this.addEventListener('transitionend', this._boundTransitionEndHandler);
+ }
- hide() {
- this._setShown(false);
- if (this._hasZeroTransitionDuration()) {
- Gerrit.getRootElement().removeChild(this);
- }
- }
+ /** @override */
+ detached() {
+ super.detached();
+ this.removeEventListener('transitionend',
+ this._boundTransitionEndHandler);
+ }
- _hasZeroTransitionDuration() {
- const style = window.getComputedStyle(this);
- // transitionDuration is always given in seconds.
- const duration = Math.round(parseFloat(style.transitionDuration) * 100);
- return duration === 0;
- }
+ show(text, opt_actionText, opt_actionCallback) {
+ this.text = text;
+ this.actionText = opt_actionText;
+ this._hideActionButton = !opt_actionText;
+ this._actionCallback = opt_actionCallback;
+ Gerrit.getRootElement().appendChild(this);
+ this._setShown(true);
+ }
- _handleTransitionEnd(e) {
- if (this.shown) { return; }
-
+ hide() {
+ this._setShown(false);
+ if (this._hasZeroTransitionDuration()) {
Gerrit.getRootElement().removeChild(this);
}
-
- _handleActionTap(e) {
- e.preventDefault();
- if (this._actionCallback) { this._actionCallback(); }
- }
}
- customElements.define(GrAlert.is, GrAlert);
-})();
+ _hasZeroTransitionDuration() {
+ const style = window.getComputedStyle(this);
+ // transitionDuration is always given in seconds.
+ const duration = Math.round(parseFloat(style.transitionDuration) * 100);
+ return duration === 0;
+ }
+
+ _handleTransitionEnd(e) {
+ if (this.shown) { return; }
+
+ Gerrit.getRootElement().removeChild(this);
+ }
+
+ _handleActionTap(e) {
+ e.preventDefault();
+ if (this._actionCallback) { this._actionCallback(); }
+ }
+}
+
+customElements.define(GrAlert.is, GrAlert);
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.js
new file mode 100644
index 0000000..1190516
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.js
@@ -0,0 +1,73 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ /**
+ * ALERT: DO NOT ADD TRANSITION PROPERTIES WITHOUT PROPERLY UNDERSTANDING
+ * HOW THEY ARE USED IN THE CODE.
+ */
+ :host([toast]) {
+ background-color: var(--tooltip-background-color);
+ bottom: 1.25rem;
+ border-radius: var(--border-radius);
+ box-shadow: var(--elevation-level-2);
+ color: var(--view-background-color);
+ left: 1.25rem;
+ position: fixed;
+ transform: translateY(5rem);
+ transition: transform var(--gr-alert-transition-duration, 80ms) ease-out;
+ z-index: 1000;
+ }
+ :host([shown]) {
+ transform: translateY(0);
+ }
+ /**
+ * NOTE: To avoid style being overwritten by outside of the shadow DOM
+ * (as outside styles always win), .content-wrapper is introduced as a
+ * wrapper around main content to have better encapsulation, styles that
+ * may be affected by outside should be defined on it.
+ * In this case, \`padding:0px\` is defined in main.css for all elements
+ * with the universal selector: *.
+ */
+ .content-wrapper {
+ padding: var(--spacing-l) var(--spacing-xl);
+ }
+ .text {
+ color: var(--tooltip-text-color);
+ display: inline-block;
+ max-height: 10rem;
+ max-width: 80vw;
+ vertical-align: bottom;
+ word-break: break-all;
+ }
+ .action {
+ color: var(--link-color);
+ font-weight: var(--font-weight-bold);
+ margin-left: var(--spacing-l);
+ text-decoration: none;
+ --gr-button: {
+ padding: 0;
+ }
+ }
+ </style>
+ <div class="content-wrapper">
+ <span class="text">[[text]]</span>
+ <gr-button link="" class="action" hidden\$="[[_hideActionButton]]" on-click="_handleActionTap">[[actionText]]</gr-button>
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
index 68d782b..582dcf9 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
@@ -19,43 +19,40 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-alert</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-alert.html">
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-alert.js';
+suite('gr-alert tests', () => {
+ let element;
-<script>
- suite('gr-alert tests', async () => {
- await readyToTest();
- let element;
-
- setup(() => {
- element = document.createElement('gr-alert');
- });
-
- teardown(() => {
- if (element.parentNode) {
- element.parentNode.removeChild(element);
- }
- });
-
- test('show/hide', () => {
- assert.isNull(element.parentNode);
- element.show();
- assert.equal(element.parentNode, document.body);
- element.updateStyles({'--gr-alert-transition-duration': '0ms'});
- element.hide();
- assert.isNull(element.parentNode);
- });
-
- test('action event', done => {
- element.show();
- element._actionCallback = done;
- MockInteractions.tap(element.shadowRoot
- .querySelector('.action'));
- });
+ setup(() => {
+ element = document.createElement('gr-alert');
});
+
+ teardown(() => {
+ if (element.parentNode) {
+ element.parentNode.removeChild(element);
+ }
+ });
+
+ test('show/hide', () => {
+ assert.isNull(element.parentNode);
+ element.show();
+ assert.equal(element.parentNode, document.body);
+ element.updateStyles({'--gr-alert-transition-duration': '0ms'});
+ element.hide();
+ assert.isNull(element.parentNode);
+ });
+
+ test('action event', done => {
+ element.show();
+ element._actionCallback = done;
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.action'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
deleted file mode 100644
index 649cd22..0000000
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
+++ /dev/null
@@ -1,108 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
-<link rel="import" href="/bower_components/iron-fit-behavior/iron-fit-behavior.html">
-<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
-<script src="../../../scripts/rootElement.js"></script>
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-autocomplete-dropdown">
- <template>
- <style include="shared-styles">
- :host {
- z-index: 100;
- }
- :host([is-hidden]) {
- display: none;
- }
- ul {
- list-style: none;
- }
- li {
- border-bottom: 1px solid var(--border-color);
- cursor: pointer;
- display: flex;
- justify-content: space-between;
- padding: var(--spacing-m) var(--spacing-l);
- }
- li:last-of-type {
- border: none;
- }
- li:focus {
- outline: none;
- }
- li:hover {
- background-color: var(--hover-background-color);
- }
- li.selected {
- background-color: var(--selection-background-color);
- }
- .dropdown-content {
- background: var(--dropdown-background-color);
- box-shadow: var(--elevation-level-2);
- border-radius: var(--border-radius);
- max-height: 50vh;
- overflow: auto;
- }
- @media only screen and (max-height: 35em) {
- .dropdown-content {
- max-height: 80vh;
- }
- }
- .label {
- color: var(--deemphasized-text-color);
- padding-left: var(--spacing-l);
- }
- .hide {
- display: none;
- }
- </style>
- <div
- class="dropdown-content"
- slot="dropdown-content"
- id="suggestions"
- role="listbox">
- <ul>
- <template is="dom-repeat" items="[[suggestions]]">
- <li data-index$="[[index]]"
- data-value$="[[item.dataValue]]"
- tabindex="-1"
- aria-label$="[[item.name]]"
- class="autocompleteOption"
- role="option"
- on-click="_handleClickItem">
- <span>[[item.text]]</span>
- <span class$="label [[_computeLabelClass(item)]]">[[item.label]]</span>
- </li>
- </template>
- </ul>
- </div>
- <gr-cursor-manager
- id="cursor"
- index="{{index}}"
- cursor-target-class="selected"
- scroll-behavior="never"
- focus-on-move
- stops="[[_suggestionEls]]"></gr-cursor-manager>
- </template>
- <script src="gr-autocomplete-dropdown.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
index 5ca95e1..813d45d 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
@@ -14,179 +14,193 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '@polymer/iron-dropdown/iron-dropdown.js';
+import {IronFitBehavior} from '@polymer/iron-fit-behavior/iron-fit-behavior.js';
+import '../gr-cursor-manager/gr-cursor-manager.js';
+import '../../../scripts/rootElement.js';
+import '../../../styles/shared-styles.js';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-autocomplete-dropdown_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Polymer.IronFitMixin
+ * @extends Polymer.Element
+ */
+class GrAutocompleteDropdown extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+ IronFitBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-autocomplete-dropdown'; }
+ /**
+ * Fired when the dropdown is closed.
+ *
+ * @event dropdown-closed
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Polymer.IronFitMixin
- * @extends Polymer.Element
+ * Fired when item is selected.
+ *
+ * @event item-selected
*/
- class GrAutocompleteDropdown extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- Polymer.IronFitBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-autocomplete-dropdown'; }
- /**
- * Fired when the dropdown is closed.
- *
- * @event dropdown-closed
- */
- /**
- * Fired when item is selected.
- *
- * @event item-selected
- */
+ static get properties() {
+ return {
+ index: Number,
+ isHidden: {
+ type: Boolean,
+ value: true,
+ reflectToAttribute: true,
+ },
+ verticalOffset: {
+ type: Number,
+ value: null,
+ },
+ horizontalOffset: {
+ type: Number,
+ value: null,
+ },
+ suggestions: {
+ type: Array,
+ value: () => [],
+ observer: '_resetCursorStops',
+ },
+ _suggestionEls: Array,
+ };
+ }
- static get properties() {
- return {
- index: Number,
- isHidden: {
- type: Boolean,
- value: true,
- reflectToAttribute: true,
- },
- verticalOffset: {
- type: Number,
- value: null,
- },
- horizontalOffset: {
- type: Number,
- value: null,
- },
- suggestions: {
- type: Array,
- value: () => [],
- observer: '_resetCursorStops',
- },
- _suggestionEls: Array,
- };
- }
+ get keyBindings() {
+ return {
+ up: '_handleUp',
+ down: '_handleDown',
+ enter: '_handleEnter',
+ esc: '_handleEscape',
+ tab: '_handleTab',
+ };
+ }
- get keyBindings() {
- return {
- up: '_handleUp',
- down: '_handleDown',
- enter: '_handleEnter',
- esc: '_handleEscape',
- tab: '_handleTab',
- };
- }
+ close() {
+ this.isHidden = true;
+ }
- close() {
- this.isHidden = true;
- }
+ open() {
+ this.isHidden = false;
+ this._resetCursorStops();
+ // Refit should run after we call Polymer.flush inside _resetCursorStops
+ this.refit();
+ }
- open() {
- this.isHidden = false;
- this._resetCursorStops();
- // Refit should run after we call Polymer.flush inside _resetCursorStops
- this.refit();
- }
+ getCurrentText() {
+ return this.getCursorTarget().dataset.value;
+ }
- getCurrentText() {
- return this.getCursorTarget().dataset.value;
- }
-
- _handleUp(e) {
- if (!this.isHidden) {
- e.preventDefault();
- e.stopPropagation();
- this.cursorUp();
- }
- }
-
- _handleDown(e) {
- if (!this.isHidden) {
- e.preventDefault();
- e.stopPropagation();
- this.cursorDown();
- }
- }
-
- cursorDown() {
- if (!this.isHidden) {
- this.$.cursor.next();
- }
- }
-
- cursorUp() {
- if (!this.isHidden) {
- this.$.cursor.previous();
- }
- }
-
- _handleTab(e) {
+ _handleUp(e) {
+ if (!this.isHidden) {
e.preventDefault();
e.stopPropagation();
- this.fire('item-selected', {
- trigger: 'tab',
- selected: this.$.cursor.target,
- });
- }
-
- _handleEnter(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('item-selected', {
- trigger: 'enter',
- selected: this.$.cursor.target,
- });
- }
-
- _handleEscape() {
- this._fireClose();
- this.close();
- }
-
- _handleClickItem(e) {
- e.preventDefault();
- e.stopPropagation();
- let selected = e.target;
- while (!selected.classList.contains('autocompleteOption')) {
- if (!selected || selected === this) { return; }
- selected = selected.parentElement;
- }
- this.fire('item-selected', {
- trigger: 'click',
- selected,
- });
- }
-
- _fireClose() {
- this.fire('dropdown-closed');
- }
-
- getCursorTarget() {
- return this.$.cursor.target;
- }
-
- _resetCursorStops() {
- if (this.suggestions.length > 0) {
- if (!this.isHidden) {
- Polymer.dom.flush();
- this._suggestionEls = Array.from(
- this.$.suggestions.querySelectorAll('li'));
- this._resetCursorIndex();
- }
- } else {
- this._suggestionEls = [];
- }
- }
-
- _resetCursorIndex() {
- this.$.cursor.setCursorAtIndex(0);
- }
-
- _computeLabelClass(item) {
- return item.label ? '' : 'hide';
+ this.cursorUp();
}
}
- customElements.define(GrAutocompleteDropdown.is, GrAutocompleteDropdown);
-})();
+ _handleDown(e) {
+ if (!this.isHidden) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.cursorDown();
+ }
+ }
+
+ cursorDown() {
+ if (!this.isHidden) {
+ this.$.cursor.next();
+ }
+ }
+
+ cursorUp() {
+ if (!this.isHidden) {
+ this.$.cursor.previous();
+ }
+ }
+
+ _handleTab(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('item-selected', {
+ trigger: 'tab',
+ selected: this.$.cursor.target,
+ });
+ }
+
+ _handleEnter(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('item-selected', {
+ trigger: 'enter',
+ selected: this.$.cursor.target,
+ });
+ }
+
+ _handleEscape() {
+ this._fireClose();
+ this.close();
+ }
+
+ _handleClickItem(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ let selected = e.target;
+ while (!selected.classList.contains('autocompleteOption')) {
+ if (!selected || selected === this) { return; }
+ selected = selected.parentElement;
+ }
+ this.fire('item-selected', {
+ trigger: 'click',
+ selected,
+ });
+ }
+
+ _fireClose() {
+ this.fire('dropdown-closed');
+ }
+
+ getCursorTarget() {
+ return this.$.cursor.target;
+ }
+
+ _resetCursorStops() {
+ if (this.suggestions.length > 0) {
+ if (!this.isHidden) {
+ flush();
+ this._suggestionEls = Array.from(
+ this.$.suggestions.querySelectorAll('li'));
+ this._resetCursorIndex();
+ }
+ } else {
+ this._suggestionEls = [];
+ }
+ }
+
+ _resetCursorIndex() {
+ this.$.cursor.setCursorAtIndex(0);
+ }
+
+ _computeLabelClass(item) {
+ return item.label ? '' : 'hide';
+ }
+}
+
+customElements.define(GrAutocompleteDropdown.is, GrAutocompleteDropdown);
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js
new file mode 100644
index 0000000..711315d
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js
@@ -0,0 +1,80 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ z-index: 100;
+ }
+ :host([is-hidden]) {
+ display: none;
+ }
+ ul {
+ list-style: none;
+ }
+ li {
+ border-bottom: 1px solid var(--border-color);
+ cursor: pointer;
+ display: flex;
+ justify-content: space-between;
+ padding: var(--spacing-m) var(--spacing-l);
+ }
+ li:last-of-type {
+ border: none;
+ }
+ li:focus {
+ outline: none;
+ }
+ li:hover {
+ background-color: var(--hover-background-color);
+ }
+ li.selected {
+ background-color: var(--selection-background-color);
+ }
+ .dropdown-content {
+ background: var(--dropdown-background-color);
+ box-shadow: var(--elevation-level-2);
+ border-radius: var(--border-radius);
+ max-height: 50vh;
+ overflow: auto;
+ }
+ @media only screen and (max-height: 35em) {
+ .dropdown-content {
+ max-height: 80vh;
+ }
+ }
+ .label {
+ color: var(--deemphasized-text-color);
+ padding-left: var(--spacing-l);
+ }
+ .hide {
+ display: none;
+ }
+ </style>
+ <div class="dropdown-content" slot="dropdown-content" id="suggestions" role="listbox">
+ <ul>
+ <template is="dom-repeat" items="[[suggestions]]">
+ <li data-index\$="[[index]]" data-value\$="[[item.dataValue]]" tabindex="-1" aria-label\$="[[item.name]]" class="autocompleteOption" role="option" on-click="_handleClickItem">
+ <span>[[item.text]]</span>
+ <span class\$="label [[_computeLabelClass(item)]]">[[item.label]]</span>
+ </li>
+ </template>
+ </ul>
+ </div>
+ <gr-cursor-manager id="cursor" index="{{index}}" cursor-target-class="selected" scroll-behavior="never" focus-on-move="" stops="[[_suggestionEls]]"></gr-cursor-manager>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
index 9ea8259..4f3d806 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-autocomplete-dropdown</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-autocomplete-dropdown.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,124 +30,124 @@
</template>
</test-fixture>
-<script>
- suite('gr-autocomplete-dropdown', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-autocomplete-dropdown.js';
+suite('gr-autocomplete-dropdown', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.open();
- element.suggestions = [
- {dataValue: 'test value 1', name: 'test name 1', text: 1, label: 'hi'},
- {dataValue: 'test value 2', name: 'test name 2', text: 2}];
- flushAsynchronousOperations();
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.open();
+ element.suggestions = [
+ {dataValue: 'test value 1', name: 'test name 1', text: 1, label: 'hi'},
+ {dataValue: 'test value 2', name: 'test name 2', text: 2}];
+ flushAsynchronousOperations();
+ });
- teardown(() => {
- sandbox.restore();
- if (element.isOpen) element.close();
- });
+ teardown(() => {
+ sandbox.restore();
+ if (element.isOpen) element.close();
+ });
- test('shows labels', () => {
- const els = element.$.suggestions.querySelectorAll('li');
- assert.equal(els[0].innerText.trim(), '1\nhi');
- assert.equal(els[1].innerText.trim(), '2');
- });
+ test('shows labels', () => {
+ const els = element.$.suggestions.querySelectorAll('li');
+ assert.equal(els[0].innerText.trim(), '1\nhi');
+ assert.equal(els[1].innerText.trim(), '2');
+ });
- test('escape key', done => {
- const closeSpy = sandbox.spy(element, 'close');
- MockInteractions.pressAndReleaseKeyOn(element, 27);
- flushAsynchronousOperations();
- assert.isTrue(closeSpy.called);
- done();
- });
+ test('escape key', done => {
+ const closeSpy = sandbox.spy(element, 'close');
+ MockInteractions.pressAndReleaseKeyOn(element, 27);
+ flushAsynchronousOperations();
+ assert.isTrue(closeSpy.called);
+ done();
+ });
- test('tab key', () => {
- const handleTabSpy = sandbox.spy(element, '_handleTab');
- const itemSelectedStub = sandbox.stub();
- element.addEventListener('item-selected', itemSelectedStub);
- MockInteractions.pressAndReleaseKeyOn(element, 9);
- assert.isTrue(handleTabSpy.called);
- assert.equal(element.$.cursor.index, 0);
- assert.isTrue(itemSelectedStub.called);
- assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
- trigger: 'tab',
- selected: element.getCursorTarget(),
- });
- });
-
- test('enter key', () => {
- const handleEnterSpy = sandbox.spy(element, '_handleEnter');
- const itemSelectedStub = sandbox.stub();
- element.addEventListener('item-selected', itemSelectedStub);
- MockInteractions.pressAndReleaseKeyOn(element, 13);
- assert.isTrue(handleEnterSpy.called);
- assert.equal(element.$.cursor.index, 0);
- assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
- trigger: 'enter',
- selected: element.getCursorTarget(),
- });
- });
-
- test('down key', () => {
- element.isHidden = true;
- const nextSpy = sandbox.spy(element.$.cursor, 'next');
- MockInteractions.pressAndReleaseKeyOn(element, 40);
- assert.isFalse(nextSpy.called);
- assert.equal(element.$.cursor.index, 0);
- element.isHidden = false;
- MockInteractions.pressAndReleaseKeyOn(element, 40);
- assert.isTrue(nextSpy.called);
- assert.equal(element.$.cursor.index, 1);
- });
-
- test('up key', () => {
- element.isHidden = true;
- const prevSpy = sandbox.spy(element.$.cursor, 'previous');
- MockInteractions.pressAndReleaseKeyOn(element, 38);
- assert.isFalse(prevSpy.called);
- assert.equal(element.$.cursor.index, 0);
- element.isHidden = false;
- element.$.cursor.setCursorAtIndex(1);
- assert.equal(element.$.cursor.index, 1);
- MockInteractions.pressAndReleaseKeyOn(element, 38);
- assert.isTrue(prevSpy.called);
- assert.equal(element.$.cursor.index, 0);
- });
-
- test('tapping selects item', () => {
- const itemSelectedStub = sandbox.stub();
- element.addEventListener('item-selected', itemSelectedStub);
-
- MockInteractions.tap(element.$.suggestions.querySelectorAll('li')[1]);
- flushAsynchronousOperations();
- assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
- trigger: 'click',
- selected: element.$.suggestions.querySelectorAll('li')[1],
- });
- });
-
- test('tapping child still selects item', () => {
- const itemSelectedStub = sandbox.stub();
- element.addEventListener('item-selected', itemSelectedStub);
-
- MockInteractions.tap(element.$.suggestions.querySelectorAll('li')[0]
- .lastElementChild);
- flushAsynchronousOperations();
- assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
- trigger: 'click',
- selected: element.$.suggestions.querySelectorAll('li')[0],
- });
- });
-
- test('updated suggestions resets cursor stops', () => {
- const resetStopsSpy = sandbox.spy(element, '_resetCursorStops');
- element.suggestions = [];
- assert.isTrue(resetStopsSpy.called);
+ test('tab key', () => {
+ const handleTabSpy = sandbox.spy(element, '_handleTab');
+ const itemSelectedStub = sandbox.stub();
+ element.addEventListener('item-selected', itemSelectedStub);
+ MockInteractions.pressAndReleaseKeyOn(element, 9);
+ assert.isTrue(handleTabSpy.called);
+ assert.equal(element.$.cursor.index, 0);
+ assert.isTrue(itemSelectedStub.called);
+ assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
+ trigger: 'tab',
+ selected: element.getCursorTarget(),
});
});
+ test('enter key', () => {
+ const handleEnterSpy = sandbox.spy(element, '_handleEnter');
+ const itemSelectedStub = sandbox.stub();
+ element.addEventListener('item-selected', itemSelectedStub);
+ MockInteractions.pressAndReleaseKeyOn(element, 13);
+ assert.isTrue(handleEnterSpy.called);
+ assert.equal(element.$.cursor.index, 0);
+ assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
+ trigger: 'enter',
+ selected: element.getCursorTarget(),
+ });
+ });
+
+ test('down key', () => {
+ element.isHidden = true;
+ const nextSpy = sandbox.spy(element.$.cursor, 'next');
+ MockInteractions.pressAndReleaseKeyOn(element, 40);
+ assert.isFalse(nextSpy.called);
+ assert.equal(element.$.cursor.index, 0);
+ element.isHidden = false;
+ MockInteractions.pressAndReleaseKeyOn(element, 40);
+ assert.isTrue(nextSpy.called);
+ assert.equal(element.$.cursor.index, 1);
+ });
+
+ test('up key', () => {
+ element.isHidden = true;
+ const prevSpy = sandbox.spy(element.$.cursor, 'previous');
+ MockInteractions.pressAndReleaseKeyOn(element, 38);
+ assert.isFalse(prevSpy.called);
+ assert.equal(element.$.cursor.index, 0);
+ element.isHidden = false;
+ element.$.cursor.setCursorAtIndex(1);
+ assert.equal(element.$.cursor.index, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 38);
+ assert.isTrue(prevSpy.called);
+ assert.equal(element.$.cursor.index, 0);
+ });
+
+ test('tapping selects item', () => {
+ const itemSelectedStub = sandbox.stub();
+ element.addEventListener('item-selected', itemSelectedStub);
+
+ MockInteractions.tap(element.$.suggestions.querySelectorAll('li')[1]);
+ flushAsynchronousOperations();
+ assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
+ trigger: 'click',
+ selected: element.$.suggestions.querySelectorAll('li')[1],
+ });
+ });
+
+ test('tapping child still selects item', () => {
+ const itemSelectedStub = sandbox.stub();
+ element.addEventListener('item-selected', itemSelectedStub);
+
+ MockInteractions.tap(element.$.suggestions.querySelectorAll('li')[0]
+ .lastElementChild);
+ flushAsynchronousOperations();
+ assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
+ trigger: 'click',
+ selected: element.$.suggestions.querySelectorAll('li')[0],
+ });
+ });
+
+ test('updated suggestions resets cursor stops', () => {
+ const resetStopsSpy = sandbox.spy(element, '_resetCursorStops');
+ element.suggestions = [];
+ assert.isTrue(resetStopsSpy.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
deleted file mode 100644
index 381ef0b..0000000
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ /dev/null
@@ -1,119 +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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/paper-input/paper-input.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html">
-<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-autocomplete">
- <template>
- <style include="shared-styles">
- .searchIcon {
- display: none;
- }
- .searchIcon.showSearchIcon {
- display: inline-block;
- }
- iron-icon {
- margin: 0 var(--spacing-xs);
- vertical-align: top;
- }
- paper-input.borderless {
- border: none;
- padding: 0;
- }
- paper-input {
- background-color: var(--view-background-color);
- color: var(--primary-text-color);
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius);
- padding: var(--spacing-s);
- --paper-input-container: {
- padding: 0;
- };
- --paper-input-container-input: {
- font-size: var(--font-size-normal);
- line-height: var(--line-height-normal);
- };
- /* This is a hack for not being able to set height:0 on the underline
- of a paper-input 2.2.3 element. All the underline fixes below only
- actually work in 3.x.x, so the height must be adjusted directly as
- a workaround until we are on Polymer 3. */
- height: var(--line-height-normal);
- --paper-input-container-underline-height: 0;
- --paper-input-container-underline-wrapper-height: 0;
- --paper-input-container-underline-focus-height: 0;
- --paper-input-container-underline-legacy-height: 0;
- --paper-input-container-underline: {
- height: 0;
- display: none;
- };
- --paper-input-container-underline-focus: {
- height: 0;
- display: none;
- };
- --paper-input-container-underline-disabled: {
- height: 0;
- display: none;
- };
- }
- paper-input.warnUncommitted {
- --paper-input-container-input: {
- color: var(--error-text-color);
- font-size: inherit;
- }
- }
- </style>
- <paper-input
- no-label-float
- id="input"
- class$="[[_computeClass(borderless)]]"
- disabled$="[[disabled]]"
- value="{{text}}"
- placeholder="[[placeholder]]"
- on-keydown="_handleKeydown"
- on-focus="_onInputFocus"
- on-blur="_onInputBlur"
- autocomplete="off">
-
- <!-- prefix as attribute is required to for polymer 1 -->
- <div slot="prefix" prefix>
- <iron-icon
- icon="gr-icons:search"
- class$="searchIcon [[_computeShowSearchIconClass(showSearchIcon)]]">
- </iron-icon>
- </div>
- </paper-input>
- <gr-autocomplete-dropdown
- vertical-align="top"
- vertical-offset="[[verticalOffset]]"
- horizontal-align="left"
- id="suggestions"
- on-item-selected="_handleItemSelect"
- on-keydown="_handleKeydown"
- suggestions="[[_suggestions]]"
- role="listbox"
- index="[[_index]]"
- position-target="[[_inputElement]]">
- </gr-autocomplete-dropdown>
- </template>
- <script src="gr-autocomplete.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 60985c1..22b62db 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -14,449 +14,463 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
- const DEBOUNCE_WAIT_MS = 200;
+import '@polymer/paper-input/paper-input.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../gr-autocomplete-dropdown/gr-autocomplete-dropdown.js';
+import '../gr-cursor-manager/gr-cursor-manager.js';
+import '../gr-icons/gr-icons.js';
+import '../../../styles/shared-styles.js';
+import {flush, dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-autocomplete_html.js';
+
+const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
+const DEBOUNCE_WAIT_MS = 200;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrAutocomplete extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-autocomplete'; }
+ /**
+ * Fired when a value is chosen.
+ *
+ * @event commit
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * Fired when the user cancels.
+ *
+ * @event cancel
*/
- class GrAutocomplete extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-autocomplete'; }
- /**
- * Fired when a value is chosen.
- *
- * @event commit
- */
- /**
- * Fired when the user cancels.
- *
- * @event cancel
- */
+ /**
+ * Fired on keydown to allow for custom hooks into autocomplete textbox
+ * behavior.
+ *
+ * @event input-keydown
+ */
- /**
- * Fired on keydown to allow for custom hooks into autocomplete textbox
- * behavior.
- *
- * @event input-keydown
- */
+ static get properties() {
+ return {
- static get properties() {
- return {
-
- /**
- * Query for requesting autocomplete suggestions. The function should
- * accept the input as a string parameter and return a promise. The
- * promise yields an array of suggestion objects with "name", "label",
- * "value" properties. The "name" property will be displayed in the
- * suggestion entry. The "label" property will, when specified, appear
- * next to the "name" as label text. The "value" property will be emitted
- * if that suggestion is selected.
- *
- * @type {function(string): Promise<?>}
- */
- query: {
- type: Function,
- value() {
- return function() {
- return Promise.resolve([]);
- };
- },
+ /**
+ * Query for requesting autocomplete suggestions. The function should
+ * accept the input as a string parameter and return a promise. The
+ * promise yields an array of suggestion objects with "name", "label",
+ * "value" properties. The "name" property will be displayed in the
+ * suggestion entry. The "label" property will, when specified, appear
+ * next to the "name" as label text. The "value" property will be emitted
+ * if that suggestion is selected.
+ *
+ * @type {function(string): Promise<?>}
+ */
+ query: {
+ type: Function,
+ value() {
+ return function() {
+ return Promise.resolve([]);
+ };
},
+ },
- /**
- * The number of characters that must be typed before suggestions are
- * made. If threshold is zero, default suggestions are enabled.
- */
- threshold: {
- type: Number,
- value: 1,
- },
+ /**
+ * The number of characters that must be typed before suggestions are
+ * made. If threshold is zero, default suggestions are enabled.
+ */
+ threshold: {
+ type: Number,
+ value: 1,
+ },
- allowNonSuggestedValues: Boolean,
- borderless: Boolean,
- disabled: Boolean,
- showSearchIcon: {
- type: Boolean,
- value: false,
- },
- /**
- * Vertical offset needed for an element with 20px line-height, 4px
- * padding and 1px border (30px height total). Plus 1px spacing between
- * input and dropdown. Inputs with different line-height or padding will
- * need to tweak vertical offset.
- */
- verticalOffset: {
- type: Number,
- value: 31,
- },
+ allowNonSuggestedValues: Boolean,
+ borderless: Boolean,
+ disabled: Boolean,
+ showSearchIcon: {
+ type: Boolean,
+ value: false,
+ },
+ /**
+ * Vertical offset needed for an element with 20px line-height, 4px
+ * padding and 1px border (30px height total). Plus 1px spacing between
+ * input and dropdown. Inputs with different line-height or padding will
+ * need to tweak vertical offset.
+ */
+ verticalOffset: {
+ type: Number,
+ value: 31,
+ },
- text: {
- type: String,
- value: '',
- notify: true,
- },
+ text: {
+ type: String,
+ value: '',
+ notify: true,
+ },
- placeholder: String,
+ placeholder: String,
- clearOnCommit: {
- type: Boolean,
- value: false,
- },
+ clearOnCommit: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * When true, tab key autocompletes but does not fire the commit event.
- * When false, tab key not caught, and focus is removed from the element.
- * See Issue 4556, Issue 6645.
- */
- tabComplete: {
- type: Boolean,
- value: false,
- },
+ /**
+ * When true, tab key autocompletes but does not fire the commit event.
+ * When false, tab key not caught, and focus is removed from the element.
+ * See Issue 4556, Issue 6645.
+ */
+ tabComplete: {
+ type: Boolean,
+ value: false,
+ },
- value: {
- type: String,
- notify: true,
- },
+ value: {
+ type: String,
+ notify: true,
+ },
- /**
- * Multi mode appends autocompleted entries to the value.
- * If false, autocompleted entries replace value.
- */
- multi: {
- type: Boolean,
- value: false,
- },
+ /**
+ * Multi mode appends autocompleted entries to the value.
+ * If false, autocompleted entries replace value.
+ */
+ multi: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * When true and uncommitted text is left in the autocomplete input after
- * blurring, the text will appear red.
- */
- warnUncommitted: {
- type: Boolean,
- value: false,
- },
+ /**
+ * When true and uncommitted text is left in the autocomplete input after
+ * blurring, the text will appear red.
+ */
+ warnUncommitted: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * When true, querying for suggestions is not debounced w/r/t keypresses
- */
- noDebounce: {
- type: Boolean,
- value: false,
- },
+ /**
+ * When true, querying for suggestions is not debounced w/r/t keypresses
+ */
+ noDebounce: {
+ type: Boolean,
+ value: false,
+ },
- /** @type {?} */
- _suggestions: {
- type: Array,
- value() { return []; },
- },
+ /** @type {?} */
+ _suggestions: {
+ type: Array,
+ value() { return []; },
+ },
- _suggestionEls: {
- type: Array,
- value() { return []; },
- },
+ _suggestionEls: {
+ type: Array,
+ value() { return []; },
+ },
- _index: Number,
- _disableSuggestions: {
- type: Boolean,
- value: false,
- },
- _focused: {
- type: Boolean,
- value: false,
- },
+ _index: Number,
+ _disableSuggestions: {
+ type: Boolean,
+ value: false,
+ },
+ _focused: {
+ type: Boolean,
+ value: false,
+ },
- /** The DOM element of the selected suggestion. */
- _selected: Object,
- };
+ /** The DOM element of the selected suggestion. */
+ _selected: Object,
+ };
+ }
+
+ static get observers() {
+ return [
+ '_maybeOpenDropdown(_suggestions, _focused)',
+ '_updateSuggestions(text, threshold, noDebounce)',
+ ];
+ }
+
+ get _nativeInput() {
+ // In Polymer 2 inputElement isn't nativeInput anymore
+ return this.$.input.$.nativeInput || this.$.input.inputElement;
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this.listen(document.body, 'click', '_handleBodyClick');
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this.unlisten(document.body, 'click', '_handleBodyClick');
+ this.cancelDebouncer('update-suggestions');
+ }
+
+ get focusStart() {
+ return this.$.input;
+ }
+
+ focus() {
+ this._nativeInput.focus();
+ }
+
+ selectAll() {
+ const nativeInputElement = this._nativeInput;
+ if (!this.$.input.value) { return; }
+ nativeInputElement.setSelectionRange(0, this.$.input.value.length);
+ }
+
+ clear() {
+ this.text = '';
+ }
+
+ _handleItemSelect(e) {
+ // Let _handleKeydown deal with keyboard interaction.
+ if (e.detail.trigger !== 'click') { return; }
+ this._selected = e.detail.selected;
+ this._commit();
+ }
+
+ get _inputElement() {
+ // Polymer2: this.$ can be undefined when this is first evaluated.
+ return this.$ && this.$.input;
+ }
+
+ /**
+ * Set the text of the input without triggering the suggestion dropdown.
+ *
+ * @param {string} text The new text for the input.
+ */
+ setText(text) {
+ this._disableSuggestions = true;
+ this.text = text;
+ this._disableSuggestions = false;
+ }
+
+ _onInputFocus() {
+ this._focused = true;
+ this._updateSuggestions(this.text, this.threshold, this.noDebounce);
+ this.$.input.classList.remove('warnUncommitted');
+ // Needed so that --paper-input-container-input updated style is applied.
+ this.updateStyles();
+ }
+
+ _onInputBlur() {
+ this.$.input.classList.toggle('warnUncommitted',
+ this.warnUncommitted && this.text.length && !this._focused);
+ // Needed so that --paper-input-container-input updated style is applied.
+ this.updateStyles();
+ }
+
+ _updateSuggestions(text, threshold, noDebounce) {
+ // Polymer 2: check for undefined
+ if ([text, threshold, noDebounce].some(arg => arg === undefined)) {
+ return;
}
- static get observers() {
- return [
- '_maybeOpenDropdown(_suggestions, _focused)',
- '_updateSuggestions(text, threshold, noDebounce)',
- ];
+ // Reset _suggestions for every update
+ // This will also prevent from carrying over suggestions:
+ // @see Issue 12039
+ this._suggestions = [];
+
+ // TODO(taoalpha): Also skip if text has not changed
+
+ if (this._disableSuggestions) { return; }
+ if (text.length < threshold) {
+ this.value = '';
+ return;
}
- get _nativeInput() {
- // In Polymer 2 inputElement isn't nativeInput anymore
- return this.$.input.$.nativeInput || this.$.input.inputElement;
+ if (!this._focused) {
+ return;
}
- /** @override */
- attached() {
- super.attached();
- this.listen(document.body, 'click', '_handleBodyClick');
- }
-
- /** @override */
- detached() {
- super.detached();
- this.unlisten(document.body, 'click', '_handleBodyClick');
- this.cancelDebouncer('update-suggestions');
- }
-
- get focusStart() {
- return this.$.input;
- }
-
- focus() {
- this._nativeInput.focus();
- }
-
- selectAll() {
- const nativeInputElement = this._nativeInput;
- if (!this.$.input.value) { return; }
- nativeInputElement.setSelectionRange(0, this.$.input.value.length);
- }
-
- clear() {
- this.text = '';
- }
-
- _handleItemSelect(e) {
- // Let _handleKeydown deal with keyboard interaction.
- if (e.detail.trigger !== 'click') { return; }
- this._selected = e.detail.selected;
- this._commit();
- }
-
- get _inputElement() {
- // Polymer2: this.$ can be undefined when this is first evaluated.
- return this.$ && this.$.input;
- }
-
- /**
- * Set the text of the input without triggering the suggestion dropdown.
- *
- * @param {string} text The new text for the input.
- */
- setText(text) {
- this._disableSuggestions = true;
- this.text = text;
- this._disableSuggestions = false;
- }
-
- _onInputFocus() {
- this._focused = true;
- this._updateSuggestions(this.text, this.threshold, this.noDebounce);
- this.$.input.classList.remove('warnUncommitted');
- // Needed so that --paper-input-container-input updated style is applied.
- this.updateStyles();
- }
-
- _onInputBlur() {
- this.$.input.classList.toggle('warnUncommitted',
- this.warnUncommitted && this.text.length && !this._focused);
- // Needed so that --paper-input-container-input updated style is applied.
- this.updateStyles();
- }
-
- _updateSuggestions(text, threshold, noDebounce) {
- // Polymer 2: check for undefined
- if ([text, threshold, noDebounce].some(arg => arg === undefined)) {
- return;
- }
-
- // Reset _suggestions for every update
- // This will also prevent from carrying over suggestions:
- // @see Issue 12039
- this._suggestions = [];
-
- // TODO(taoalpha): Also skip if text has not changed
-
- if (this._disableSuggestions) { return; }
- if (text.length < threshold) {
- this.value = '';
- return;
- }
-
- if (!this._focused) {
- return;
- }
-
- const update = () => {
- this.query(text).then(suggestions => {
- if (text !== this.text) {
- // Late response.
- return;
- }
- for (const suggestion of suggestions) {
- suggestion.text = suggestion.name;
- }
- this._suggestions = suggestions;
- Polymer.dom.flush();
- if (this._index === -1) {
- this.value = '';
- }
- });
- };
-
- if (noDebounce) {
- update();
- } else {
- this.debounce('update-suggestions', update, DEBOUNCE_WAIT_MS);
- }
- }
-
- _maybeOpenDropdown(suggestions, focused) {
- if (suggestions.length > 0 && focused) {
- return this.$.suggestions.open();
- }
- return this.$.suggestions.close();
- }
-
- _computeClass(borderless) {
- return borderless ? 'borderless' : '';
- }
-
- /**
- * _handleKeydown used for key handling in the this.$.input AND all child
- * autocomplete options.
- */
- _handleKeydown(e) {
- this._focused = true;
- switch (e.keyCode) {
- case 38: // Up
- e.preventDefault();
- this.$.suggestions.cursorUp();
- break;
- case 40: // Down
- e.preventDefault();
- this.$.suggestions.cursorDown();
- break;
- case 27: // Escape
- e.preventDefault();
- this._cancel();
- break;
- case 9: // Tab
- if (this._suggestions.length > 0 && this.tabComplete) {
- e.preventDefault();
- this._handleInputCommit(true);
- this.focus();
- } else {
- this._focused = false;
- }
- break;
- case 13: // Enter
- if (this.modifierPressed(e)) { break; }
- e.preventDefault();
- this._handleInputCommit();
- break;
- default:
- // For any normal keypress, return focus to the input to allow for
- // unbroken user input.
- this.focus();
-
- // Since this has been a normal keypress, the suggestions will have
- // been based on a previous input. Clear them. This prevents an
- // outdated suggestion from being used if the input keystroke is
- // immediately followed by a commit keystroke. @see Issue 8655
- this._suggestions = [];
- }
- this.fire('input-keydown', {keyCode: e.keyCode, input: this.$.input});
- }
-
- _cancel() {
- if (this._suggestions.length) {
- this.set('_suggestions', []);
- } else {
- this.fire('cancel');
- }
- }
-
- /**
- * @param {boolean=} opt_tabComplete
- */
- _handleInputCommit(opt_tabComplete) {
- // Nothing to do if the dropdown is not open.
- if (!this.allowNonSuggestedValues &&
- this.$.suggestions.isHidden) { return; }
-
- this._selected = this.$.suggestions.getCursorTarget();
- this._commit(opt_tabComplete);
- }
-
- _updateValue(suggestion, suggestions) {
- if (!suggestion) { return; }
- const completed = suggestions[suggestion.dataset.index].value;
- if (this.multi) {
- // Append the completed text to the end of the string.
- // Allow spaces within quoted terms.
- const tokens = this.text.match(TOKENIZE_REGEX);
- tokens[tokens.length - 1] = completed;
- this.value = tokens.join(' ');
- } else {
- this.value = completed;
- }
- }
-
- _handleBodyClick(e) {
- const eventPath = Polymer.dom(e).path;
- for (let i = 0; i < eventPath.length; i++) {
- if (eventPath[i] === this) {
+ const update = () => {
+ this.query(text).then(suggestions => {
+ if (text !== this.text) {
+ // Late response.
return;
}
- }
- this._focused = false;
- }
-
- _handleSuggestionTap(e) {
- e.stopPropagation();
- this.$.cursor.setCursor(e.target);
- this._commit();
- }
-
- /**
- * Commits the suggestion, optionally firing the commit event.
- *
- * @param {boolean=} opt_silent Allows for silent committing of an
- * autocomplete suggestion in order to handle cases like tab-to-complete
- * without firing the commit event.
- */
- _commit(opt_silent) {
- // Allow values that are not in suggestion list iff suggestions are empty.
- if (this._suggestions.length > 0) {
- this._updateValue(this._selected, this._suggestions);
- } else {
- this.value = this.text || '';
- }
-
- const value = this.value;
-
- // Value and text are mirrors of each other in multi mode.
- if (this.multi) {
- this.setText(this.value);
- } else {
- if (!this.clearOnCommit && this._selected) {
- this.setText(this._suggestions[this._selected.dataset.index].name);
- } else {
- this.clear();
+ for (const suggestion of suggestions) {
+ suggestion.text = suggestion.name;
}
- }
+ this._suggestions = suggestions;
+ flush();
+ if (this._index === -1) {
+ this.value = '';
+ }
+ });
+ };
- this._suggestions = [];
- if (!opt_silent) {
- this.fire('commit', {value});
- }
-
- this._textChangedSinceCommit = false;
- }
-
- _computeShowSearchIconClass(showSearchIcon) {
- return showSearchIcon ? 'showSearchIcon' : '';
+ if (noDebounce) {
+ update();
+ } else {
+ this.debounce('update-suggestions', update, DEBOUNCE_WAIT_MS);
}
}
- customElements.define(GrAutocomplete.is, GrAutocomplete);
-})();
+ _maybeOpenDropdown(suggestions, focused) {
+ if (suggestions.length > 0 && focused) {
+ return this.$.suggestions.open();
+ }
+ return this.$.suggestions.close();
+ }
+
+ _computeClass(borderless) {
+ return borderless ? 'borderless' : '';
+ }
+
+ /**
+ * _handleKeydown used for key handling in the this.$.input AND all child
+ * autocomplete options.
+ */
+ _handleKeydown(e) {
+ this._focused = true;
+ switch (e.keyCode) {
+ case 38: // Up
+ e.preventDefault();
+ this.$.suggestions.cursorUp();
+ break;
+ case 40: // Down
+ e.preventDefault();
+ this.$.suggestions.cursorDown();
+ break;
+ case 27: // Escape
+ e.preventDefault();
+ this._cancel();
+ break;
+ case 9: // Tab
+ if (this._suggestions.length > 0 && this.tabComplete) {
+ e.preventDefault();
+ this._handleInputCommit(true);
+ this.focus();
+ } else {
+ this._focused = false;
+ }
+ break;
+ case 13: // Enter
+ if (this.modifierPressed(e)) { break; }
+ e.preventDefault();
+ this._handleInputCommit();
+ break;
+ default:
+ // For any normal keypress, return focus to the input to allow for
+ // unbroken user input.
+ this.focus();
+
+ // Since this has been a normal keypress, the suggestions will have
+ // been based on a previous input. Clear them. This prevents an
+ // outdated suggestion from being used if the input keystroke is
+ // immediately followed by a commit keystroke. @see Issue 8655
+ this._suggestions = [];
+ }
+ this.fire('input-keydown', {keyCode: e.keyCode, input: this.$.input});
+ }
+
+ _cancel() {
+ if (this._suggestions.length) {
+ this.set('_suggestions', []);
+ } else {
+ this.fire('cancel');
+ }
+ }
+
+ /**
+ * @param {boolean=} opt_tabComplete
+ */
+ _handleInputCommit(opt_tabComplete) {
+ // Nothing to do if the dropdown is not open.
+ if (!this.allowNonSuggestedValues &&
+ this.$.suggestions.isHidden) { return; }
+
+ this._selected = this.$.suggestions.getCursorTarget();
+ this._commit(opt_tabComplete);
+ }
+
+ _updateValue(suggestion, suggestions) {
+ if (!suggestion) { return; }
+ const completed = suggestions[suggestion.dataset.index].value;
+ if (this.multi) {
+ // Append the completed text to the end of the string.
+ // Allow spaces within quoted terms.
+ const tokens = this.text.match(TOKENIZE_REGEX);
+ tokens[tokens.length - 1] = completed;
+ this.value = tokens.join(' ');
+ } else {
+ this.value = completed;
+ }
+ }
+
+ _handleBodyClick(e) {
+ const eventPath = dom(e).path;
+ for (let i = 0; i < eventPath.length; i++) {
+ if (eventPath[i] === this) {
+ return;
+ }
+ }
+ this._focused = false;
+ }
+
+ _handleSuggestionTap(e) {
+ e.stopPropagation();
+ this.$.cursor.setCursor(e.target);
+ this._commit();
+ }
+
+ /**
+ * Commits the suggestion, optionally firing the commit event.
+ *
+ * @param {boolean=} opt_silent Allows for silent committing of an
+ * autocomplete suggestion in order to handle cases like tab-to-complete
+ * without firing the commit event.
+ */
+ _commit(opt_silent) {
+ // Allow values that are not in suggestion list iff suggestions are empty.
+ if (this._suggestions.length > 0) {
+ this._updateValue(this._selected, this._suggestions);
+ } else {
+ this.value = this.text || '';
+ }
+
+ const value = this.value;
+
+ // Value and text are mirrors of each other in multi mode.
+ if (this.multi) {
+ this.setText(this.value);
+ } else {
+ if (!this.clearOnCommit && this._selected) {
+ this.setText(this._suggestions[this._selected.dataset.index].name);
+ } else {
+ this.clear();
+ }
+ }
+
+ this._suggestions = [];
+ if (!opt_silent) {
+ this.fire('commit', {value});
+ }
+
+ this._textChangedSinceCommit = false;
+ }
+
+ _computeShowSearchIconClass(showSearchIcon) {
+ return showSearchIcon ? 'showSearchIcon' : '';
+ }
+}
+
+customElements.define(GrAutocomplete.is, GrAutocomplete);
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js
new file mode 100644
index 0000000..11726d9
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js
@@ -0,0 +1,87 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ .searchIcon {
+ display: none;
+ }
+ .searchIcon.showSearchIcon {
+ display: inline-block;
+ }
+ iron-icon {
+ margin: 0 var(--spacing-xs);
+ vertical-align: top;
+ }
+ paper-input.borderless {
+ border: none;
+ padding: 0;
+ }
+ paper-input {
+ background-color: var(--view-background-color);
+ color: var(--primary-text-color);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ padding: var(--spacing-s);
+ --paper-input-container: {
+ padding: 0;
+ };
+ --paper-input-container-input: {
+ font-size: var(--font-size-normal);
+ line-height: var(--line-height-normal);
+ };
+ /* This is a hack for not being able to set height:0 on the underline
+ of a paper-input 2.2.3 element. All the underline fixes below only
+ actually work in 3.x.x, so the height must be adjusted directly as
+ a workaround until we are on Polymer 3. */
+ height: var(--line-height-normal);
+ --paper-input-container-underline-height: 0;
+ --paper-input-container-underline-wrapper-height: 0;
+ --paper-input-container-underline-focus-height: 0;
+ --paper-input-container-underline-legacy-height: 0;
+ --paper-input-container-underline: {
+ height: 0;
+ display: none;
+ };
+ --paper-input-container-underline-focus: {
+ height: 0;
+ display: none;
+ };
+ --paper-input-container-underline-disabled: {
+ height: 0;
+ display: none;
+ };
+ }
+ paper-input.warnUncommitted {
+ --paper-input-container-input: {
+ color: var(--error-text-color);
+ font-size: inherit;
+ }
+ }
+ </style>
+ <paper-input no-label-float="" id="input" class\$="[[_computeClass(borderless)]]" disabled\$="[[disabled]]" value="{{text}}" placeholder="[[placeholder]]" on-keydown="_handleKeydown" on-focus="_onInputFocus" on-blur="_onInputBlur" autocomplete="off">
+
+ <!-- prefix as attribute is required to for polymer 1 -->
+ <div slot="prefix" prefix="">
+ <iron-icon icon="gr-icons:search" class\$="searchIcon [[_computeShowSearchIconClass(showSearchIcon)]]">
+ </iron-icon>
+ </div>
+ </paper-input>
+ <gr-autocomplete-dropdown vertical-align="top" vertical-offset="[[verticalOffset]]" horizontal-align="left" id="suggestions" on-item-selected="_handleItemSelect" on-keydown="_handleKeydown" suggestions="[[_suggestions]]" role="listbox" index="[[_index]]" position-target="[[_inputElement]]">
+ </gr-autocomplete-dropdown>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index 295572f..68de4fc 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reviewer-list</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-autocomplete.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,580 +30,582 @@
</template>
</test-fixture>
-<script>
- suite('gr-autocomplete tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- const focusOnInput = element => {
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-autocomplete.js';
+import {dom, flush as flush$0} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-autocomplete tests', () => {
+ let element;
+ let sandbox;
+ const focusOnInput = element => {
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
+ 'enter');
+ };
+
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('renders', () => {
+ let promise;
+ const queryStub = sandbox.spy(input => promise = Promise.resolve([
+ {name: input + ' 0', value: 0},
+ {name: input + ' 1', value: 1},
+ {name: input + ' 2', value: 2},
+ {name: input + ' 3', value: 3},
+ {name: input + ' 4', value: 4},
+ ]));
+ element.query = queryStub;
+ assert.isTrue(element.$.suggestions.isHidden);
+ assert.equal(element.$.suggestions.$.cursor.index, -1);
+
+ focusOnInput(element);
+ element.text = 'blah';
+
+ assert.isTrue(queryStub.called);
+ element._focused = true;
+
+ return promise.then(() => {
+ assert.isFalse(element.$.suggestions.isHidden);
+ const suggestions =
+ dom(element.$.suggestions.root).querySelectorAll('li');
+ assert.equal(suggestions.length, 5);
+
+ for (let i = 0; i < 5; i++) {
+ assert.equal(suggestions[i].innerText.trim(), 'blah ' + i);
+ }
+
+ assert.notEqual(element.$.suggestions.$.cursor.index, -1);
+ });
+ });
+
+ test('selectAll', done => {
+ flush(() => {
+ const nativeInput = element._nativeInput;
+ const selectionStub = sandbox.stub(nativeInput, 'setSelectionRange');
+
+ element.selectAll();
+ assert.isFalse(selectionStub.called);
+
+ element.$.input.value = 'test';
+ element.selectAll();
+ assert.isTrue(selectionStub.called);
+ done();
+ });
+ });
+
+ test('esc key behavior', done => {
+ let promise;
+ const queryStub = sandbox.spy(() => promise = Promise.resolve([
+ {name: 'blah', value: 123},
+ ]));
+ element.query = queryStub;
+
+ assert.isTrue(element.$.suggestions.isHidden);
+
+ element._focused = true;
+ element.text = 'blah';
+
+ promise.then(() => {
+ assert.isFalse(element.$.suggestions.isHidden);
+
+ const cancelHandler = sandbox.spy();
+ element.addEventListener('cancel', cancelHandler);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
+ assert.isFalse(cancelHandler.called);
+ assert.isTrue(element.$.suggestions.isHidden);
+ assert.equal(element._suggestions.length, 0);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
+ assert.isTrue(cancelHandler.called);
+ done();
+ });
+ });
+
+ test('emits commit and handles cursor movement', done => {
+ let promise;
+ const queryStub = sandbox.spy(input => promise = Promise.resolve([
+ {name: input + ' 0', value: 0},
+ {name: input + ' 1', value: 1},
+ {name: input + ' 2', value: 2},
+ {name: input + ' 3', value: 3},
+ {name: input + ' 4', value: 4},
+ ]));
+ element.query = queryStub;
+
+ assert.isTrue(element.$.suggestions.isHidden);
+ assert.equal(element.$.suggestions.$.cursor.index, -1);
+ element._focused = true;
+ element.text = 'blah';
+
+ promise.then(() => {
+ assert.isFalse(element.$.suggestions.isHidden);
+
+ const commitHandler = sandbox.spy();
+ element.addEventListener('commit', commitHandler);
+
+ assert.equal(element.$.suggestions.$.cursor.index, 0);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null,
+ 'down');
+
+ assert.equal(element.$.suggestions.$.cursor.index, 1);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null,
+ 'down');
+
+ assert.equal(element.$.suggestions.$.cursor.index, 2);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 38, null, 'up');
+
+ assert.equal(element.$.suggestions.$.cursor.index, 1);
+
MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
'enter');
- };
+
+ assert.equal(element.value, 1);
+ assert.isTrue(commitHandler.called);
+ assert.equal(commitHandler.getCall(0).args[0].detail.value, 1);
+ assert.isTrue(element.$.suggestions.isHidden);
+ assert.isTrue(element._focused);
+ done();
+ });
+ });
+
+ test('clear-on-commit behavior (off)', done => {
+ let promise;
+ const queryStub = sandbox.spy(() => {
+ promise = Promise.resolve([{name: 'suggestion', value: 0}]);
+ return promise;
+ });
+ element.query = queryStub;
+ focusOnInput(element);
+ element.text = 'blah';
+
+ promise.then(() => {
+ const commitHandler = sandbox.spy();
+ element.addEventListener('commit', commitHandler);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
+ 'enter');
+
+ assert.isTrue(commitHandler.called);
+ assert.equal(element.text, 'suggestion');
+ done();
+ });
+ });
+
+ test('clear-on-commit behavior (on)', done => {
+ let promise;
+ const queryStub = sandbox.spy(() => {
+ promise = Promise.resolve([{name: 'suggestion', value: 0}]);
+ return promise;
+ });
+ element.query = queryStub;
+ focusOnInput(element);
+ element.text = 'blah';
+ element.clearOnCommit = true;
+
+ promise.then(() => {
+ const commitHandler = sandbox.spy();
+ element.addEventListener('commit', commitHandler);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
+ 'enter');
+
+ assert.isTrue(commitHandler.called);
+ assert.equal(element.text, '');
+ done();
+ });
+ });
+
+ test('threshold guards the query', () => {
+ const queryStub = sandbox.spy(() => Promise.resolve([]));
+ element.query = queryStub;
+ element.threshold = 2;
+ focusOnInput(element);
+ element.text = 'a';
+ assert.isFalse(queryStub.called);
+ element.text = 'ab';
+ assert.isTrue(queryStub.called);
+ });
+
+ test('noDebounce=false debounces the query', () => {
+ const queryStub = sandbox.spy(() => Promise.resolve([]));
+ let callback;
+ const debounceStub = sandbox.stub(element, 'debounce',
+ (name, cb) => { callback = cb; });
+ element.query = queryStub;
+ element.noDebounce = false;
+ focusOnInput(element);
+ element.text = 'a';
+ assert.isFalse(queryStub.called);
+ assert.isTrue(debounceStub.called);
+ assert.equal(debounceStub.lastCall.args[2], 200);
+ assert.isFunction(callback);
+ callback();
+ assert.isTrue(queryStub.called);
+ });
+
+ test('_computeClass respects border property', () => {
+ assert.equal(element._computeClass(), '');
+ assert.equal(element._computeClass(false), '');
+ assert.equal(element._computeClass(true), 'borderless');
+ });
+
+ test('undefined or empty text results in no suggestions', () => {
+ element._updateSuggestions(undefined, 0, null);
+ assert.equal(element._suggestions.length, 0);
+ });
+
+ test('when focused', done => {
+ let promise;
+ const queryStub = sandbox.stub()
+ .returns(promise = Promise.resolve([
+ {name: 'suggestion', value: 0},
+ ]));
+ element.query = queryStub;
+ element.suggestOnlyWhenFocus = true;
+ focusOnInput(element);
+ element.text = 'bla';
+ assert.equal(element._focused, true);
+ flushAsynchronousOperations();
+ promise.then(() => {
+ assert.equal(element._suggestions.length, 1);
+ assert.equal(queryStub.notCalled, false);
+ done();
+ });
+ });
+
+ test('when not focused', done => {
+ let promise;
+ const queryStub = sandbox.stub()
+ .returns(promise = Promise.resolve([
+ {name: 'suggestion', value: 0},
+ ]));
+ element.query = queryStub;
+ element.suggestOnlyWhenFocus = true;
+ element.text = 'bla';
+ assert.equal(element._focused, false);
+ flushAsynchronousOperations();
+ promise.then(() => {
+ assert.equal(element._suggestions.length, 0);
+ done();
+ });
+ });
+
+ test('suggestions should not carry over', done => {
+ let promise;
+ const queryStub = sandbox.stub()
+ .returns(promise = Promise.resolve([
+ {name: 'suggestion', value: 0},
+ ]));
+ element.query = queryStub;
+ focusOnInput(element);
+ element.text = 'bla';
+ flushAsynchronousOperations();
+ promise.then(() => {
+ assert.equal(element._suggestions.length, 1);
+ element._updateSuggestions('', 0, false);
+ assert.equal(element._suggestions.length, 0);
+ done();
+ });
+ });
+
+ test('multi completes only the last part of the query', done => {
+ let promise;
+ const queryStub = sandbox.stub()
+ .returns(promise = Promise.resolve([
+ {name: 'suggestion', value: 0},
+ ]));
+ element.query = queryStub;
+ focusOnInput(element);
+ element.text = 'blah blah';
+ element.multi = true;
+
+ promise.then(() => {
+ const commitHandler = sandbox.spy();
+ element.addEventListener('commit', commitHandler);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
+ 'enter');
+
+ assert.isTrue(commitHandler.called);
+ assert.equal(element.text, 'blah 0');
+ done();
+ });
+ });
+
+ test('tabComplete flag functions', () => {
+ // commitHandler checks for the commit event, whereas commitSpy checks for
+ // the _commit function of the element.
+ const commitHandler = sandbox.spy();
+ element.addEventListener('commit', commitHandler);
+ const commitSpy = sandbox.spy(element, '_commit');
+ element._focused = true;
+
+ element._suggestions = ['tunnel snakes rule!'];
+ element.tabComplete = false;
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
+ assert.isFalse(commitHandler.called);
+ assert.isFalse(commitSpy.called);
+ assert.isFalse(element._focused);
+
+ element.tabComplete = true;
+ element._focused = true;
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
+ assert.isFalse(commitHandler.called);
+ assert.isTrue(commitSpy.called);
+ assert.isTrue(element._focused);
+ });
+
+ test('_focused flag properly triggered', done => {
+ flush(() => {
+ assert.isFalse(element._focused);
+ const input = element.shadowRoot
+ .querySelector('paper-input').inputElement;
+ MockInteractions.focus(input);
+ assert.isTrue(element._focused);
+ done();
+ });
+ });
+
+ test('search icon shows with showSearchIcon property', done => {
+ flush(() => {
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('iron-icon')).display,
+ 'none');
+ element.showSearchIcon = true;
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('iron-icon')).display,
+ 'none');
+ done();
+ });
+ });
+
+ test('vertical offset overridden by param if it exists', () => {
+ assert.equal(element.$.suggestions.verticalOffset, 31);
+ element.verticalOffset = 30;
+ assert.equal(element.$.suggestions.verticalOffset, 30);
+ });
+
+ test('_focused flag shows/hides the suggestions', () => {
+ const openStub = sandbox.stub(element.$.suggestions, 'open');
+ const closedStub = sandbox.stub(element.$.suggestions, 'close');
+ element._suggestions = ['hello', 'its me'];
+ assert.isFalse(openStub.called);
+ assert.isTrue(closedStub.calledOnce);
+ element._focused = true;
+ assert.isTrue(openStub.calledOnce);
+ element._suggestions = [];
+ assert.isTrue(closedStub.calledTwice);
+ assert.isTrue(openStub.calledOnce);
+ });
+
+ test('_handleInputCommit with autocomplete hidden does nothing without' +
+ 'without allowNonSuggestedValues', () => {
+ const commitStub = sandbox.stub(element, '_commit');
+ element.$.suggestions.isHidden = true;
+ element._handleInputCommit();
+ assert.isFalse(commitStub.called);
+ });
+
+ test('_handleInputCommit with autocomplete hidden with' +
+ 'allowNonSuggestedValues', () => {
+ const commitStub = sandbox.stub(element, '_commit');
+ element.allowNonSuggestedValues = true;
+ element.$.suggestions.isHidden = true;
+ element._handleInputCommit();
+ assert.isTrue(commitStub.called);
+ });
+
+ test('_handleInputCommit with autocomplete open calls commit', () => {
+ const commitStub = sandbox.stub(element, '_commit');
+ element.$.suggestions.isHidden = false;
+ element._handleInputCommit();
+ assert.isTrue(commitStub.calledOnce);
+ });
+
+ test('_handleInputCommit with autocomplete open calls commit' +
+ 'with allowNonSuggestedValues', () => {
+ const commitStub = sandbox.stub(element, '_commit');
+ element.allowNonSuggestedValues = true;
+ element.$.suggestions.isHidden = false;
+ element._handleInputCommit();
+ assert.isTrue(commitStub.calledOnce);
+ });
+
+ test('issue 8655', () => {
+ function makeSuggestion(s) { return {name: s, text: s, value: s}; }
+ const keydownSpy = sandbox.spy(element, '_handleKeydown');
+ element.setText('file:');
+ element._suggestions =
+ [makeSuggestion('file:'), makeSuggestion('-file:')];
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 88, null, 'x');
+ // Must set the value, because the MockInteraction does not.
+ element.$.input.value = 'file:x';
+ assert.isTrue(keydownSpy.calledOnce);
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.input,
+ 13,
+ null,
+ 'enter'
+ );
+ assert.isTrue(keydownSpy.calledTwice);
+ assert.equal(element.text, 'file:x');
+ });
+
+ suite('focus', () => {
+ let commitSpy;
+ let focusSpy;
setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
+ commitSpy = sandbox.spy(element, '_commit');
});
- teardown(() => {
- sandbox.restore();
- });
+ test('enter does not call focus', () => {
+ element._suggestions = ['sugar bombs'];
+ focusSpy = sandbox.spy(element, 'focus');
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
+ 'enter');
+ flushAsynchronousOperations();
- test('renders', () => {
- let promise;
- const queryStub = sandbox.spy(input => promise = Promise.resolve([
- {name: input + ' 0', value: 0},
- {name: input + ' 1', value: 1},
- {name: input + ' 2', value: 2},
- {name: input + ' 3', value: 3},
- {name: input + ' 4', value: 4},
- ]));
- element.query = queryStub;
- assert.isTrue(element.$.suggestions.isHidden);
- assert.equal(element.$.suggestions.$.cursor.index, -1);
-
- focusOnInput(element);
- element.text = 'blah';
-
- assert.isTrue(queryStub.called);
- element._focused = true;
-
- return promise.then(() => {
- assert.isFalse(element.$.suggestions.isHidden);
- const suggestions =
- Polymer.dom(element.$.suggestions.root).querySelectorAll('li');
- assert.equal(suggestions.length, 5);
-
- for (let i = 0; i < 5; i++) {
- assert.equal(suggestions[i].innerText.trim(), 'blah ' + i);
- }
-
- assert.notEqual(element.$.suggestions.$.cursor.index, -1);
- });
- });
-
- test('selectAll', done => {
- flush(() => {
- const nativeInput = element._nativeInput;
- const selectionStub = sandbox.stub(nativeInput, 'setSelectionRange');
-
- element.selectAll();
- assert.isFalse(selectionStub.called);
-
- element.$.input.value = 'test';
- element.selectAll();
- assert.isTrue(selectionStub.called);
- done();
- });
- });
-
- test('esc key behavior', done => {
- let promise;
- const queryStub = sandbox.spy(() => promise = Promise.resolve([
- {name: 'blah', value: 123},
- ]));
- element.query = queryStub;
-
- assert.isTrue(element.$.suggestions.isHidden);
-
- element._focused = true;
- element.text = 'blah';
-
- promise.then(() => {
- assert.isFalse(element.$.suggestions.isHidden);
-
- const cancelHandler = sandbox.spy();
- element.addEventListener('cancel', cancelHandler);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
- assert.isFalse(cancelHandler.called);
- assert.isTrue(element.$.suggestions.isHidden);
- assert.equal(element._suggestions.length, 0);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
- assert.isTrue(cancelHandler.called);
- done();
- });
- });
-
- test('emits commit and handles cursor movement', done => {
- let promise;
- const queryStub = sandbox.spy(input => promise = Promise.resolve([
- {name: input + ' 0', value: 0},
- {name: input + ' 1', value: 1},
- {name: input + ' 2', value: 2},
- {name: input + ' 3', value: 3},
- {name: input + ' 4', value: 4},
- ]));
- element.query = queryStub;
-
- assert.isTrue(element.$.suggestions.isHidden);
- assert.equal(element.$.suggestions.$.cursor.index, -1);
- element._focused = true;
- element.text = 'blah';
-
- promise.then(() => {
- assert.isFalse(element.$.suggestions.isHidden);
-
- const commitHandler = sandbox.spy();
- element.addEventListener('commit', commitHandler);
-
- assert.equal(element.$.suggestions.$.cursor.index, 0);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null,
- 'down');
-
- assert.equal(element.$.suggestions.$.cursor.index, 1);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null,
- 'down');
-
- assert.equal(element.$.suggestions.$.cursor.index, 2);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 38, null, 'up');
-
- assert.equal(element.$.suggestions.$.cursor.index, 1);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
- 'enter');
-
- assert.equal(element.value, 1);
- assert.isTrue(commitHandler.called);
- assert.equal(commitHandler.getCall(0).args[0].detail.value, 1);
- assert.isTrue(element.$.suggestions.isHidden);
- assert.isTrue(element._focused);
- done();
- });
- });
-
- test('clear-on-commit behavior (off)', done => {
- let promise;
- const queryStub = sandbox.spy(() => {
- promise = Promise.resolve([{name: 'suggestion', value: 0}]);
- return promise;
- });
- element.query = queryStub;
- focusOnInput(element);
- element.text = 'blah';
-
- promise.then(() => {
- const commitHandler = sandbox.spy();
- element.addEventListener('commit', commitHandler);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
- 'enter');
-
- assert.isTrue(commitHandler.called);
- assert.equal(element.text, 'suggestion');
- done();
- });
- });
-
- test('clear-on-commit behavior (on)', done => {
- let promise;
- const queryStub = sandbox.spy(() => {
- promise = Promise.resolve([{name: 'suggestion', value: 0}]);
- return promise;
- });
- element.query = queryStub;
- focusOnInput(element);
- element.text = 'blah';
- element.clearOnCommit = true;
-
- promise.then(() => {
- const commitHandler = sandbox.spy();
- element.addEventListener('commit', commitHandler);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
- 'enter');
-
- assert.isTrue(commitHandler.called);
- assert.equal(element.text, '');
- done();
- });
- });
-
- test('threshold guards the query', () => {
- const queryStub = sandbox.spy(() => Promise.resolve([]));
- element.query = queryStub;
- element.threshold = 2;
- focusOnInput(element);
- element.text = 'a';
- assert.isFalse(queryStub.called);
- element.text = 'ab';
- assert.isTrue(queryStub.called);
- });
-
- test('noDebounce=false debounces the query', () => {
- const queryStub = sandbox.spy(() => Promise.resolve([]));
- let callback;
- const debounceStub = sandbox.stub(element, 'debounce',
- (name, cb) => { callback = cb; });
- element.query = queryStub;
- element.noDebounce = false;
- focusOnInput(element);
- element.text = 'a';
- assert.isFalse(queryStub.called);
- assert.isTrue(debounceStub.called);
- assert.equal(debounceStub.lastCall.args[2], 200);
- assert.isFunction(callback);
- callback();
- assert.isTrue(queryStub.called);
- });
-
- test('_computeClass respects border property', () => {
- assert.equal(element._computeClass(), '');
- assert.equal(element._computeClass(false), '');
- assert.equal(element._computeClass(true), 'borderless');
- });
-
- test('undefined or empty text results in no suggestions', () => {
- element._updateSuggestions(undefined, 0, null);
+ assert.isTrue(commitSpy.called);
+ assert.isFalse(focusSpy.called);
assert.equal(element._suggestions.length, 0);
});
- test('when focused', done => {
- let promise;
- const queryStub = sandbox.stub()
- .returns(promise = Promise.resolve([
- {name: 'suggestion', value: 0},
- ]));
- element.query = queryStub;
- element.suggestOnlyWhenFocus = true;
- focusOnInput(element);
- element.text = 'bla';
- assert.equal(element._focused, true);
- flushAsynchronousOperations();
- promise.then(() => {
- assert.equal(element._suggestions.length, 1);
- assert.equal(queryStub.notCalled, false);
- done();
- });
- });
-
- test('when not focused', done => {
- let promise;
- const queryStub = sandbox.stub()
- .returns(promise = Promise.resolve([
- {name: 'suggestion', value: 0},
- ]));
- element.query = queryStub;
- element.suggestOnlyWhenFocus = true;
- element.text = 'bla';
- assert.equal(element._focused, false);
- flushAsynchronousOperations();
- promise.then(() => {
- assert.equal(element._suggestions.length, 0);
- done();
- });
- });
-
- test('suggestions should not carry over', done => {
- let promise;
- const queryStub = sandbox.stub()
- .returns(promise = Promise.resolve([
- {name: 'suggestion', value: 0},
- ]));
- element.query = queryStub;
- focusOnInput(element);
- element.text = 'bla';
- flushAsynchronousOperations();
- promise.then(() => {
- assert.equal(element._suggestions.length, 1);
- element._updateSuggestions('', 0, false);
- assert.equal(element._suggestions.length, 0);
- done();
- });
- });
-
- test('multi completes only the last part of the query', done => {
- let promise;
- const queryStub = sandbox.stub()
- .returns(promise = Promise.resolve([
- {name: 'suggestion', value: 0},
- ]));
- element.query = queryStub;
- focusOnInput(element);
- element.text = 'blah blah';
- element.multi = true;
-
- promise.then(() => {
- const commitHandler = sandbox.spy();
- element.addEventListener('commit', commitHandler);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
- 'enter');
-
- assert.isTrue(commitHandler.called);
- assert.equal(element.text, 'blah 0');
- done();
- });
- });
-
- test('tabComplete flag functions', () => {
- // commitHandler checks for the commit event, whereas commitSpy checks for
- // the _commit function of the element.
- const commitHandler = sandbox.spy();
+ test('tab in input, tabComplete = true', () => {
+ focusSpy = sandbox.spy(element, 'focus');
+ const commitHandler = sandbox.stub();
element.addEventListener('commit', commitHandler);
- const commitSpy = sandbox.spy(element, '_commit');
- element._focused = true;
-
- element._suggestions = ['tunnel snakes rule!'];
- element.tabComplete = false;
+ element.tabComplete = true;
+ element._suggestions = ['tunnel snakes drool'];
MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
+ flushAsynchronousOperations();
+
+ assert.isTrue(commitSpy.called);
+ assert.isTrue(focusSpy.called);
assert.isFalse(commitHandler.called);
+ assert.equal(element._suggestions.length, 0);
+ });
+
+ test('tab in input, tabComplete = false', () => {
+ element._suggestions = ['sugar bombs'];
+ focusSpy = sandbox.spy(element, 'focus');
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
+ flushAsynchronousOperations();
+
+ assert.isFalse(commitSpy.called);
+ assert.isFalse(focusSpy.called);
+ assert.equal(element._suggestions.length, 1);
+ });
+
+ test('tab on suggestion, tabComplete = false', () => {
+ element._suggestions = [{name: 'sugar bombs'}];
+ element._focused = true;
+ // When tabComplete is false, do not focus.
+ element.tabComplete = false;
+ focusSpy = sandbox.spy(element, 'focus');
+ flush$0();
+ assert.isFalse(element.$.suggestions.isHidden);
+
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.suggestions.shadowRoot
+ .querySelector('li:first-child'), 9, null, 'tab');
+ flushAsynchronousOperations();
assert.isFalse(commitSpy.called);
assert.isFalse(element._focused);
+ });
- element.tabComplete = true;
+ test('tab on suggestion, tabComplete = true', () => {
+ element._suggestions = [{name: 'sugar bombs'}];
element._focused = true;
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
- assert.isFalse(commitHandler.called);
+ // When tabComplete is true, focus.
+ element.tabComplete = true;
+ focusSpy = sandbox.spy(element, 'focus');
+ flush$0();
+ assert.isFalse(element.$.suggestions.isHidden);
+
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.suggestions.shadowRoot
+ .querySelector('li:first-child'), 9, null, 'tab');
+ flushAsynchronousOperations();
+
assert.isTrue(commitSpy.called);
assert.isTrue(element._focused);
});
- test('_focused flag properly triggered', done => {
- flush(() => {
- assert.isFalse(element._focused);
- const input = element.shadowRoot
- .querySelector('paper-input').inputElement;
- MockInteractions.focus(input);
- assert.isTrue(element._focused);
- done();
- });
- });
-
- test('search icon shows with showSearchIcon property', done => {
- flush(() => {
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('iron-icon')).display,
- 'none');
- element.showSearchIcon = true;
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('iron-icon')).display,
- 'none');
- done();
- });
- });
-
- test('vertical offset overridden by param if it exists', () => {
- assert.equal(element.$.suggestions.verticalOffset, 31);
- element.verticalOffset = 30;
- assert.equal(element.$.suggestions.verticalOffset, 30);
- });
-
- test('_focused flag shows/hides the suggestions', () => {
- const openStub = sandbox.stub(element.$.suggestions, 'open');
- const closedStub = sandbox.stub(element.$.suggestions, 'close');
- element._suggestions = ['hello', 'its me'];
- assert.isFalse(openStub.called);
- assert.isTrue(closedStub.calledOnce);
+ test('tap on suggestion commits, does not call focus', () => {
+ focusSpy = sandbox.spy(element, 'focus');
element._focused = true;
- assert.isTrue(openStub.calledOnce);
- element._suggestions = [];
- assert.isTrue(closedStub.calledTwice);
- assert.isTrue(openStub.calledOnce);
- });
-
- test('_handleInputCommit with autocomplete hidden does nothing without' +
- 'without allowNonSuggestedValues', () => {
- const commitStub = sandbox.stub(element, '_commit');
- element.$.suggestions.isHidden = true;
- element._handleInputCommit();
- assert.isFalse(commitStub.called);
- });
-
- test('_handleInputCommit with autocomplete hidden with' +
- 'allowNonSuggestedValues', () => {
- const commitStub = sandbox.stub(element, '_commit');
- element.allowNonSuggestedValues = true;
- element.$.suggestions.isHidden = true;
- element._handleInputCommit();
- assert.isTrue(commitStub.called);
- });
-
- test('_handleInputCommit with autocomplete open calls commit', () => {
- const commitStub = sandbox.stub(element, '_commit');
- element.$.suggestions.isHidden = false;
- element._handleInputCommit();
- assert.isTrue(commitStub.calledOnce);
- });
-
- test('_handleInputCommit with autocomplete open calls commit' +
- 'with allowNonSuggestedValues', () => {
- const commitStub = sandbox.stub(element, '_commit');
- element.allowNonSuggestedValues = true;
- element.$.suggestions.isHidden = false;
- element._handleInputCommit();
- assert.isTrue(commitStub.calledOnce);
- });
-
- test('issue 8655', () => {
- function makeSuggestion(s) { return {name: s, text: s, value: s}; }
- const keydownSpy = sandbox.spy(element, '_handleKeydown');
- element.setText('file:');
- element._suggestions =
- [makeSuggestion('file:'), makeSuggestion('-file:')];
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 88, null, 'x');
- // Must set the value, because the MockInteraction does not.
- element.$.input.value = 'file:x';
- assert.isTrue(keydownSpy.calledOnce);
- MockInteractions.pressAndReleaseKeyOn(
- element.$.input,
- 13,
- null,
- 'enter'
- );
- assert.isTrue(keydownSpy.calledTwice);
- assert.equal(element.text, 'file:x');
- });
-
- suite('focus', () => {
- let commitSpy;
- let focusSpy;
-
- setup(() => {
- commitSpy = sandbox.spy(element, '_commit');
- });
-
- test('enter does not call focus', () => {
- element._suggestions = ['sugar bombs'];
- focusSpy = sandbox.spy(element, 'focus');
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
- 'enter');
- flushAsynchronousOperations();
-
- assert.isTrue(commitSpy.called);
- assert.isFalse(focusSpy.called);
- assert.equal(element._suggestions.length, 0);
- });
-
- test('tab in input, tabComplete = true', () => {
- focusSpy = sandbox.spy(element, 'focus');
- const commitHandler = sandbox.stub();
- element.addEventListener('commit', commitHandler);
- element.tabComplete = true;
- element._suggestions = ['tunnel snakes drool'];
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
- flushAsynchronousOperations();
-
- assert.isTrue(commitSpy.called);
- assert.isTrue(focusSpy.called);
- assert.isFalse(commitHandler.called);
- assert.equal(element._suggestions.length, 0);
- });
-
- test('tab in input, tabComplete = false', () => {
- element._suggestions = ['sugar bombs'];
- focusSpy = sandbox.spy(element, 'focus');
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
- flushAsynchronousOperations();
-
- assert.isFalse(commitSpy.called);
- assert.isFalse(focusSpy.called);
- assert.equal(element._suggestions.length, 1);
- });
-
- test('tab on suggestion, tabComplete = false', () => {
- element._suggestions = [{name: 'sugar bombs'}];
- element._focused = true;
- // When tabComplete is false, do not focus.
- element.tabComplete = false;
- focusSpy = sandbox.spy(element, 'focus');
- Polymer.dom.flush();
- assert.isFalse(element.$.suggestions.isHidden);
-
- MockInteractions.pressAndReleaseKeyOn(
- element.$.suggestions.shadowRoot
- .querySelector('li:first-child'), 9, null, 'tab');
- flushAsynchronousOperations();
- assert.isFalse(commitSpy.called);
- assert.isFalse(element._focused);
- });
-
- test('tab on suggestion, tabComplete = true', () => {
- element._suggestions = [{name: 'sugar bombs'}];
- element._focused = true;
- // When tabComplete is true, focus.
- element.tabComplete = true;
- focusSpy = sandbox.spy(element, 'focus');
- Polymer.dom.flush();
- assert.isFalse(element.$.suggestions.isHidden);
-
- MockInteractions.pressAndReleaseKeyOn(
- element.$.suggestions.shadowRoot
- .querySelector('li:first-child'), 9, null, 'tab');
- flushAsynchronousOperations();
-
- assert.isTrue(commitSpy.called);
- assert.isTrue(element._focused);
- });
-
- test('tap on suggestion commits, does not call focus', () => {
- focusSpy = sandbox.spy(element, 'focus');
- element._focused = true;
- element._suggestions = [{name: 'first suggestion'}];
- Polymer.dom.flush();
- assert.isFalse(element.$.suggestions.isHidden);
- MockInteractions.tap(element.$.suggestions.shadowRoot
- .querySelector('li:first-child'));
- flushAsynchronousOperations();
-
- assert.isFalse(focusSpy.called);
- assert.isTrue(commitSpy.called);
- assert.isTrue(element.$.suggestions.isHidden);
- });
- });
-
- test('input-keydown event fired', () => {
- const listener = sandbox.spy();
- element.addEventListener('input-keydown', listener);
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
+ element._suggestions = [{name: 'first suggestion'}];
+ flush$0();
+ assert.isFalse(element.$.suggestions.isHidden);
+ MockInteractions.tap(element.$.suggestions.shadowRoot
+ .querySelector('li:first-child'));
flushAsynchronousOperations();
- assert.isTrue(listener.called);
- });
- test('enter with modifier does not complete', () => {
- const handleSpy = sandbox.spy(element, '_handleKeydown');
- const commitStub = sandbox.stub(element, '_handleInputCommit');
- MockInteractions.pressAndReleaseKeyOn(
- element.$.input, 13, 'ctrl', 'enter');
- assert.isTrue(handleSpy.called);
- assert.isFalse(commitStub.called);
- MockInteractions.pressAndReleaseKeyOn(
- element.$.input, 13, null, 'enter');
- assert.isTrue(commitStub.called);
- });
-
- suite('warnUncommitted', () => {
- let inputClassList;
- setup(() => {
- inputClassList = element.$.input.classList;
- });
-
- test('enabled', () => {
- element.warnUncommitted = true;
- element.text = 'blah blah blah';
- MockInteractions.blur(element.$.input);
- assert.isTrue(inputClassList.contains('warnUncommitted'));
- MockInteractions.focus(element.$.input);
- assert.isFalse(inputClassList.contains('warnUncommitted'));
- });
-
- test('disabled', () => {
- element.warnUncommitted = false;
- element.text = 'blah blah blah';
- MockInteractions.blur(element.$.input);
- assert.isFalse(inputClassList.contains('warnUncommitted'));
- });
-
- test('no text', () => {
- element.warnUncommitted = true;
- element.text = '';
- MockInteractions.blur(element.$.input);
- assert.isFalse(inputClassList.contains('warnUncommitted'));
- });
+ assert.isFalse(focusSpy.called);
+ assert.isTrue(commitSpy.called);
+ assert.isTrue(element.$.suggestions.isHidden);
});
});
+
+ test('input-keydown event fired', () => {
+ const listener = sandbox.spy();
+ element.addEventListener('input-keydown', listener);
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
+ flushAsynchronousOperations();
+ assert.isTrue(listener.called);
+ });
+
+ test('enter with modifier does not complete', () => {
+ const handleSpy = sandbox.spy(element, '_handleKeydown');
+ const commitStub = sandbox.stub(element, '_handleInputCommit');
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.input, 13, 'ctrl', 'enter');
+ assert.isTrue(handleSpy.called);
+ assert.isFalse(commitStub.called);
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.input, 13, null, 'enter');
+ assert.isTrue(commitStub.called);
+ });
+
+ suite('warnUncommitted', () => {
+ let inputClassList;
+ setup(() => {
+ inputClassList = element.$.input.classList;
+ });
+
+ test('enabled', () => {
+ element.warnUncommitted = true;
+ element.text = 'blah blah blah';
+ MockInteractions.blur(element.$.input);
+ assert.isTrue(inputClassList.contains('warnUncommitted'));
+ MockInteractions.focus(element.$.input);
+ assert.isFalse(inputClassList.contains('warnUncommitted'));
+ });
+
+ test('disabled', () => {
+ element.warnUncommitted = false;
+ element.text = 'blah blah blah';
+ MockInteractions.blur(element.$.input);
+ assert.isFalse(inputClassList.contains('warnUncommitted'));
+ });
+
+ test('no text', () => {
+ element.warnUncommitted = true;
+ element.text = '';
+ MockInteractions.blur(element.$.input);
+ assert.isFalse(inputClassList.contains('warnUncommitted'));
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
deleted file mode 100644
index 1daffa2..0000000
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-avatar">
- <template>
- <style include="shared-styles">
- :host {
- display: inline-block;
- border-radius: 50%;
- background-size: cover;
- background-color: var(--avatar-background-color, #f1f2f3);
- }
- </style>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-avatar.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
index efa97cf..857f1b4 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,89 +14,99 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @extends Polymer.Element
- */
- class GrAvatar extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-avatar'; }
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../gr-js-api-interface/gr-js-api-interface.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-avatar_html.js';
- static get properties() {
- return {
- account: {
- type: Object,
- observer: '_accountChanged',
- },
- imageSize: {
- type: Number,
- value: 16,
- },
- _hasAvatars: {
- type: Boolean,
- value: false,
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @extends Polymer.Element
+ */
+class GrAvatar extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** @override */
- attached() {
- super.attached();
- Promise.all([
- this._getConfig(),
- Gerrit.awaitPluginsLoaded(),
- ]).then(([cfg]) => {
- this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
+ static get is() { return 'gr-avatar'; }
- this._updateAvatarURL();
- });
- }
+ static get properties() {
+ return {
+ account: {
+ type: Object,
+ observer: '_accountChanged',
+ },
+ imageSize: {
+ type: Number,
+ value: 16,
+ },
+ _hasAvatars: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
- _getConfig() {
- return this.$.restAPI.getConfig();
- }
+ /** @override */
+ attached() {
+ super.attached();
+ Promise.all([
+ this._getConfig(),
+ Gerrit.awaitPluginsLoaded(),
+ ]).then(([cfg]) => {
+ this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
- _accountChanged(account) {
this._updateAvatarURL();
+ });
+ }
+
+ _getConfig() {
+ return this.$.restAPI.getConfig();
+ }
+
+ _accountChanged(account) {
+ this._updateAvatarURL();
+ }
+
+ _updateAvatarURL() {
+ if (!this._hasAvatars || !this.account) {
+ this.hidden = true;
+ return;
}
+ this.hidden = false;
- _updateAvatarURL() {
- if (!this._hasAvatars || !this.account) {
- this.hidden = true;
- return;
- }
- this.hidden = false;
-
- const url = this._buildAvatarURL(this.account);
- if (url) {
- this.style.backgroundImage = 'url("' + url + '")';
- }
- }
-
- _getAccounts(account) {
- return account._account_id || account.email || account.username ||
- account.name;
- }
-
- _buildAvatarURL(account) {
- if (!account) { return ''; }
- const avatars = account.avatars || [];
- for (let i = 0; i < avatars.length; i++) {
- if (avatars[i].height === this.imageSize) {
- return avatars[i].url;
- }
- }
- return this.getBaseUrl() + '/accounts/' +
- encodeURIComponent(this._getAccounts(account)) +
- '/avatar?s=' + this.imageSize;
+ const url = this._buildAvatarURL(this.account);
+ if (url) {
+ this.style.backgroundImage = 'url("' + url + '")';
}
}
- customElements.define(GrAvatar.is, GrAvatar);
-})();
+ _getAccounts(account) {
+ return account._account_id || account.email || account.username ||
+ account.name;
+ }
+
+ _buildAvatarURL(account) {
+ if (!account) { return ''; }
+ const avatars = account.avatars || [];
+ for (let i = 0; i < avatars.length; i++) {
+ if (avatars[i].height === this.imageSize) {
+ return avatars[i].url;
+ }
+ }
+ return this.getBaseUrl() + '/accounts/' +
+ encodeURIComponent(this._getAccounts(account)) +
+ '/avatar?s=' + this.imageSize;
+ }
+}
+
+customElements.define(GrAvatar.is, GrAvatar);
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.js
new file mode 100644
index 0000000..be4c350
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.js
@@ -0,0 +1,29 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: inline-block;
+ border-radius: 50%;
+ background-size: cover;
+ background-color: var(--avatar-background-color, #f1f2f3);
+ }
+ </style>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
index bd3f805..f64f163 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-avatar</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-avatar.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,14 +30,116 @@
</template>
</test-fixture>
-<script>
- suite('gr-avatar tests', async () => {
- await readyToTest();
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-avatar.js';
+suite('gr-avatar tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('methods', () => {
+ assert.equal(
+ element._buildAvatarURL({
+ _account_id: 123,
+ }),
+ '/accounts/123/avatar?s=16');
+ assert.equal(
+ element._buildAvatarURL({
+ email: 'test@example.com',
+ }),
+ '/accounts/test%40example.com/avatar?s=16');
+ assert.equal(
+ element._buildAvatarURL({
+ name: 'John Doe',
+ }),
+ '/accounts/John%20Doe/avatar?s=16');
+ assert.equal(
+ element._buildAvatarURL({
+ username: 'John_Doe',
+ }),
+ '/accounts/John_Doe/avatar?s=16');
+ assert.equal(
+ element._buildAvatarURL({
+ _account_id: 123,
+ avatars: [
+ {
+ url: 'https://cdn.example.com/s12-p/photo.jpg',
+ height: 12,
+ },
+ {
+ url: 'https://cdn.example.com/s16-p/photo.jpg',
+ height: 16,
+ },
+ {
+ url: 'https://cdn.example.com/s100-p/photo.jpg',
+ height: 100,
+ },
+ ],
+ }),
+ 'https://cdn.example.com/s16-p/photo.jpg');
+ assert.equal(
+ element._buildAvatarURL({
+ _account_id: 123,
+ avatars: [
+ {
+ url: 'https://cdn.example.com/s95-p/photo.jpg',
+ height: 95,
+ },
+ ],
+ }),
+ '/accounts/123/avatar?s=16');
+ assert.equal(element._buildAvatarURL(undefined), '');
+ });
+
+ test('dom for existing account', () => {
+ assert.isFalse(element.hasAttribute('hidden'));
+
+ sandbox.stub(
+ element,
+ '_getConfig',
+ () => Promise.resolve({plugin: {has_avatars: true}}));
+
+ element.imageSize = 64;
+ element.account = {
+ _account_id: 123,
+ };
+
+ assert.strictEqual(element.style.backgroundImage, '');
+
+ // Emulate plugins loaded.
+ Gerrit._loadPlugins([]);
+
+ Promise.all([
+ element.$.restAPI.getConfig(),
+ Gerrit.awaitPluginsLoaded(),
+ ]).then(() => {
+ assert.isFalse(element.hasAttribute('hidden'));
+
+ assert.isTrue(
+ element.style.backgroundImage.includes('/accounts/123/avatar?s=64'));
+ });
+ });
+
+ suite('plugin has avatars', () => {
let element;
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
+
+ stub('gr-avatar', {
+ _getConfig: () => Promise.resolve({plugin: {has_avatars: true}}),
+ });
+
element = fixture('basic');
});
@@ -50,110 +147,49 @@
sandbox.restore();
});
- test('methods', () => {
- assert.equal(
- element._buildAvatarURL({
- _account_id: 123,
- }),
- '/accounts/123/avatar?s=16');
- assert.equal(
- element._buildAvatarURL({
- email: 'test@example.com',
- }),
- '/accounts/test%40example.com/avatar?s=16');
- assert.equal(
- element._buildAvatarURL({
- name: 'John Doe',
- }),
- '/accounts/John%20Doe/avatar?s=16');
- assert.equal(
- element._buildAvatarURL({
- username: 'John_Doe',
- }),
- '/accounts/John_Doe/avatar?s=16');
- assert.equal(
- element._buildAvatarURL({
- _account_id: 123,
- avatars: [
- {
- url: 'https://cdn.example.com/s12-p/photo.jpg',
- height: 12,
- },
- {
- url: 'https://cdn.example.com/s16-p/photo.jpg',
- height: 16,
- },
- {
- url: 'https://cdn.example.com/s100-p/photo.jpg',
- height: 100,
- },
- ],
- }),
- 'https://cdn.example.com/s16-p/photo.jpg');
- assert.equal(
- element._buildAvatarURL({
- _account_id: 123,
- avatars: [
- {
- url: 'https://cdn.example.com/s95-p/photo.jpg',
- height: 95,
- },
- ],
- }),
- '/accounts/123/avatar?s=16');
- assert.equal(element._buildAvatarURL(undefined), '');
- });
-
- test('dom for existing account', () => {
+ test('dom for non available account', () => {
assert.isFalse(element.hasAttribute('hidden'));
- sandbox.stub(
- element,
- '_getConfig',
- () => Promise.resolve({plugin: {has_avatars: true}}));
-
- element.imageSize = 64;
- element.account = {
- _account_id: 123,
- };
-
- assert.strictEqual(element.style.backgroundImage, '');
-
// Emulate plugins loaded.
Gerrit._loadPlugins([]);
- Promise.all([
+ return Promise.all([
element.$.restAPI.getConfig(),
Gerrit.awaitPluginsLoaded(),
]).then(() => {
- assert.isFalse(element.hasAttribute('hidden'));
+ assert.isTrue(element.hasAttribute('hidden'));
- assert.isTrue(
- element.style.backgroundImage.includes('/accounts/123/avatar?s=64'));
+ assert.strictEqual(element.style.backgroundImage, '');
});
});
+ });
- suite('plugin has avatars', () => {
- let element;
- let sandbox;
+ suite('config not set', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ sandbox = sinon.sandbox.create();
- stub('gr-avatar', {
- _getConfig: () => Promise.resolve({plugin: {has_avatars: true}}),
- });
-
- element = fixture('basic');
+ stub('gr-avatar', {
+ _getConfig: () => Promise.resolve({}),
});
- teardown(() => {
- sandbox.restore();
- });
+ element = fixture('basic');
+ });
- test('dom for non available account', () => {
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('avatar hidden when account set', () => {
+ flush(() => {
assert.isFalse(element.hasAttribute('hidden'));
+ element.imageSize = 64;
+ element.account = {
+ _account_id: 123,
+ };
// Emulate plugins loaded.
Gerrit._loadPlugins([]);
@@ -162,49 +198,9 @@
Gerrit.awaitPluginsLoaded(),
]).then(() => {
assert.isTrue(element.hasAttribute('hidden'));
-
- assert.strictEqual(element.style.backgroundImage, '');
- });
- });
- });
-
- suite('config not set', () => {
- let element;
- let sandbox;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
-
- stub('gr-avatar', {
- _getConfig: () => Promise.resolve({}),
- });
-
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('avatar hidden when account set', () => {
- flush(() => {
- assert.isFalse(element.hasAttribute('hidden'));
-
- element.imageSize = 64;
- element.account = {
- _account_id: 123,
- };
- // Emulate plugins loaded.
- Gerrit._loadPlugins([]);
-
- return Promise.all([
- element.$.restAPI.getConfig(),
- Gerrit.awaitPluginsLoaded(),
- ]).then(() => {
- assert.isTrue(element.hasAttribute('hidden'));
- });
});
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
index 9d96038..41d958fd 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -14,120 +14,121 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Gerrit.TooltipMixin
- * @extends Polymer.Element
- */
- class GrButton extends Polymer.mixinBehaviors( [
- Gerrit.KeyboardShortcutBehavior,
- Gerrit.TooltipBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-button'; }
+import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '@polymer/paper-button/paper-button.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-button_html.js';
+import '../../../scripts/util.js';
- static get properties() {
- return {
- tooltip: String,
- downArrow: {
- type: Boolean,
- reflectToAttribute: true,
- },
- link: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- disabled: {
- type: Boolean,
- observer: '_disabledChanged',
- reflectToAttribute: true,
- },
- noUppercase: {
- type: Boolean,
- value: false,
- },
- loading: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
+/**
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.TooltipMixin
+ * @extends Polymer.Element
+ */
+class GrButton extends mixinBehaviors( [
+ Gerrit.KeyboardShortcutBehavior,
+ Gerrit.TooltipBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _disabled: {
- type: Boolean,
- computed: '_computeDisabled(disabled, loading)',
- },
+ static get is() { return 'gr-button'; }
- _initialTabindex: {
- type: String,
- value: '0',
- },
- };
- }
+ static get properties() {
+ return {
+ tooltip: String,
+ downArrow: {
+ type: Boolean,
+ reflectToAttribute: true,
+ },
+ link: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ disabled: {
+ type: Boolean,
+ observer: '_disabledChanged',
+ reflectToAttribute: true,
+ },
+ noUppercase: {
+ type: Boolean,
+ value: false,
+ },
+ loading: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
- /** @override */
- created() {
- super.created();
- this._initialTabindex = this.getAttribute('tabindex') || '0';
- this.addEventListener('click', e => this._handleAction(e));
- this.addEventListener('keydown',
- e => this._handleKeydown(e));
- }
+ _disabled: {
+ type: Boolean,
+ computed: '_computeDisabled(disabled, loading)',
+ },
- /** @override */
- ready() {
- super.ready();
- this._ensureAttribute('role', 'button');
- this._ensureAttribute('tabindex', '0');
- }
-
- _handleAction(e) {
- if (this._disabled) {
- e.preventDefault();
- e.stopPropagation();
- e.stopImmediatePropagation();
- return;
- }
-
- let el = this.root;
- let path = '';
- while (el = el.parentNode || el.host) {
- if (el.tagName && el.tagName.startsWith('GR-APP')) {
- break;
- }
- if (el.tagName) {
- const idString = el.id ? '#' + el.id : '';
- path = el.tagName + idString + ' ' + path;
- }
- }
- this.$.reporting.reportInteraction('button-click',
- {path: path.trim().toLowerCase()});
- }
-
- _disabledChanged(disabled) {
- this.setAttribute('tabindex', disabled ? '-1' : this._initialTabindex);
- this.updateStyles();
- }
-
- _computeDisabled(disabled, loading) {
- return disabled || loading;
- }
-
- _handleKeydown(e) {
- if (this.modifierPressed(e)) { return; }
- e = this.getKeyboardEvent(e);
- // Handle `enter`, `space`.
- if (e.keyCode === 13 || e.keyCode === 32) {
- e.preventDefault();
- e.stopPropagation();
- this.click();
- }
- }
+ _initialTabindex: {
+ type: String,
+ value: '0',
+ },
+ };
}
- customElements.define(GrButton.is, GrButton);
-})();
+ /** @override */
+ created() {
+ super.created();
+ this._initialTabindex = this.getAttribute('tabindex') || '0';
+ this.addEventListener('click', e => this._handleAction(e));
+ this.addEventListener('keydown',
+ e => this._handleKeydown(e));
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ this._ensureAttribute('role', 'button');
+ this._ensureAttribute('tabindex', '0');
+ }
+
+ _handleAction(e) {
+ if (this._disabled) {
+ e.preventDefault();
+ e.stopPropagation();
+ e.stopImmediatePropagation();
+ return;
+ }
+
+ this.$.reporting.reportInteraction('button-click',
+ {path: util.getEventPath(e)});
+ }
+
+ _disabledChanged(disabled) {
+ this.setAttribute('tabindex', disabled ? '-1' : this._initialTabindex);
+ this.updateStyles();
+ }
+
+ _computeDisabled(disabled, loading) {
+ return disabled || loading;
+ }
+
+ _handleKeydown(e) {
+ if (this.modifierPressed(e)) { return; }
+ e = this.getKeyboardEvent(e);
+ // Handle `enter`, `space`.
+ if (e.keyCode === 13 || e.keyCode === 32) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.click();
+ }
+ }
+}
+
+customElements.define(GrButton.is, GrButton);
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js
similarity index 80%
rename from polygerrit-ui/app/elements/shared/gr-button/gr-button.html
rename to polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js
index 729f87d..ad5c00d 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js
@@ -1,30 +1,22 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.js';
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="/bower_components/paper-button/paper-button.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-
-<dom-module id="gr-button">
- <template strip-whitespace>
+export const htmlTemplate = html`
<style include="shared-styles">
/* general styles for all buttons */
:host {
@@ -172,10 +164,7 @@
border-top-color: var(--deemphasized-text-color);
}
</style>
- <paper-button
- raised="[[!link]]"
- disabled="[[_disabled]]"
- tabindex="-1">
+ <paper-button raised="[[!link]]" disabled="[[_disabled]]" tabindex="-1">
<template is="dom-if" if="[[loading]]">
<span class="loadingSpin"></span>
</template>
@@ -183,6 +172,4 @@
<i class="downArrow"></i>
</paper-button>
<gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="gr-button.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
index cfac37f..9454d15 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-button</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-button.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,152 +30,193 @@
</template>
</test-fixture>
+<test-fixture id="nested">
+ <template>
+ <div id="test">
+ <gr-button class="testBtn"></gr-button>
+ </div>
+ </template>
+</test-fixture>
+
<test-fixture id="tabindex">
<template>
<gr-button tabindex="3"></gr-button>
</template>
</test-fixture>
-<script>
- suite('gr-button tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-button.js';
+import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
+suite('gr-button tests', () => {
+ let element;
+ let sandbox;
- const addSpyOn = function(eventName) {
- const spy = sandbox.spy();
- if (eventName == 'tap') {
- Polymer.Gestures.addListener(element, eventName, spy);
- } else {
- element.addEventListener(eventName, spy);
- }
- return spy;
- };
+ const addSpyOn = function(eventName) {
+ const spy = sandbox.spy();
+ if (eventName == 'tap') {
+ addListener(element, eventName, spy);
+ } else {
+ element.addEventListener(eventName, spy);
+ }
+ return spy;
+ };
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('disabled is set by disabled', () => {
+ const paperBtn = element.shadowRoot.querySelector('paper-button');
+ assert.isFalse(paperBtn.disabled);
+ element.disabled = true;
+ assert.isTrue(paperBtn.disabled);
+ element.disabled = false;
+ assert.isFalse(paperBtn.disabled);
+ });
+
+ test('loading set from listener', done => {
+ let resolve;
+ element.addEventListener('click', e => {
+ e.target.loading = true;
+ resolve = () => e.target.loading = false;
+ });
+ const paperBtn = element.shadowRoot.querySelector('paper-button');
+ assert.isFalse(paperBtn.disabled);
+ MockInteractions.tap(element);
+ assert.isTrue(paperBtn.disabled);
+ assert.isTrue(element.hasAttribute('loading'));
+ resolve();
+ flush(() => {
+ assert.isFalse(paperBtn.disabled);
+ assert.isFalse(element.hasAttribute('loading'));
+ done();
+ });
+ });
+
+ test('tabindex should be -1 if disabled', () => {
+ element.disabled = true;
+ assert.isTrue(element.getAttribute('tabindex') === '-1');
+ });
+
+ // Regression tests for Issue: 11969
+ test('tabindex should be reset to 0 if enabled', () => {
+ element.disabled = false;
+ assert.equal(element.getAttribute('tabindex'), '0');
+ element.disabled = true;
+ assert.equal(element.getAttribute('tabindex'), '-1');
+ element.disabled = false;
+ assert.equal(element.getAttribute('tabindex'), '0');
+ });
+
+ test('tabindex should be preserved', () => {
+ element = fixture('tabindex');
+ element.disabled = false;
+ assert.equal(element.getAttribute('tabindex'), '3');
+ element.disabled = true;
+ assert.equal(element.getAttribute('tabindex'), '-1');
+ element.disabled = false;
+ assert.equal(element.getAttribute('tabindex'), '3');
+ });
+
+ // 'tap' event is tested so we don't loose backward compatibility with older
+ // plugins who didn't move to on-click which is faster and well supported.
+ test('dispatches click event', () => {
+ const spy = addSpyOn('click');
+ MockInteractions.click(element);
+ assert.isTrue(spy.calledOnce);
+ });
+
+ test('dispatches tap event', () => {
+ const spy = addSpyOn('tap');
+ MockInteractions.tap(element);
+ assert.isTrue(spy.calledOnce);
+ });
+
+ test('dispatches click from tap event', () => {
+ const spy = addSpyOn('click');
+ MockInteractions.tap(element);
+ assert.isTrue(spy.calledOnce);
+ });
+
+ // Keycodes: 32 for Space, 13 for Enter.
+ for (const key of [32, 13]) {
+ test('dispatches click event on keycode ' + key, () => {
+ const tapSpy = sandbox.spy();
+ element.addEventListener('click', tapSpy);
+ MockInteractions.pressAndReleaseKeyOn(element, key);
+ assert.isTrue(tapSpy.calledOnce);
+ });
+
+ test('dispatches no click event with modifier on keycode ' + key, () => {
+ const tapSpy = sandbox.spy();
+ element.addEventListener('click', tapSpy);
+ MockInteractions.pressAndReleaseKeyOn(element, key, 'shift');
+ MockInteractions.pressAndReleaseKeyOn(element, key, 'ctrl');
+ MockInteractions.pressAndReleaseKeyOn(element, key, 'meta');
+ MockInteractions.pressAndReleaseKeyOn(element, key, 'alt');
+ assert.isFalse(tapSpy.calledOnce);
+ });
+ }
+
+ suite('disabled', () => {
setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('disabled is set by disabled', () => {
- const paperBtn = element.shadowRoot.querySelector('paper-button');
- assert.isFalse(paperBtn.disabled);
element.disabled = true;
- assert.isTrue(paperBtn.disabled);
- element.disabled = false;
- assert.isFalse(paperBtn.disabled);
});
- test('loading set from listener', done => {
- let resolve;
- element.addEventListener('click', e => {
- e.target.loading = true;
- resolve = () => e.target.loading = false;
- });
- const paperBtn = element.shadowRoot.querySelector('paper-button');
- assert.isFalse(paperBtn.disabled);
- MockInteractions.tap(element);
- assert.isTrue(paperBtn.disabled);
- assert.isTrue(element.hasAttribute('loading'));
- resolve();
- flush(() => {
- assert.isFalse(paperBtn.disabled);
- assert.isFalse(element.hasAttribute('loading'));
- done();
- });
- });
-
- test('tabindex should be -1 if disabled', () => {
- element.disabled = true;
- assert.isTrue(element.getAttribute('tabindex') === '-1');
- });
-
- // Regression tests for Issue: 11969
- test('tabindex should be reset to 0 if enabled', () => {
- element.disabled = false;
- assert.equal(element.getAttribute('tabindex'), '0');
- element.disabled = true;
- assert.equal(element.getAttribute('tabindex'), '-1');
- element.disabled = false;
- assert.equal(element.getAttribute('tabindex'), '0');
- });
-
- test('tabindex should be preserved', () => {
- element = fixture('tabindex');
- element.disabled = false;
- assert.equal(element.getAttribute('tabindex'), '3');
- element.disabled = true;
- assert.equal(element.getAttribute('tabindex'), '-1');
- element.disabled = false;
- assert.equal(element.getAttribute('tabindex'), '3');
- });
-
- // 'tap' event is tested so we don't loose backward compatibility with older
- // plugins who didn't move to on-click which is faster and well supported.
- test('dispatches click event', () => {
- const spy = addSpyOn('click');
- MockInteractions.click(element);
- assert.isTrue(spy.calledOnce);
- });
-
- test('dispatches tap event', () => {
- const spy = addSpyOn('tap');
- MockInteractions.tap(element);
- assert.isTrue(spy.calledOnce);
- });
-
- test('dispatches click from tap event', () => {
- const spy = addSpyOn('click');
- MockInteractions.tap(element);
- assert.isTrue(spy.calledOnce);
- });
-
- // Keycodes: 32 for Space, 13 for Enter.
- for (const key of [32, 13]) {
- test('dispatches click event on keycode ' + key, () => {
- const tapSpy = sandbox.spy();
- element.addEventListener('click', tapSpy);
- MockInteractions.pressAndReleaseKeyOn(element, key);
- assert.isTrue(tapSpy.calledOnce);
- });
-
- test('dispatches no click event with modifier on keycode ' + key, () => {
- const tapSpy = sandbox.spy();
- element.addEventListener('click', tapSpy);
- MockInteractions.pressAndReleaseKeyOn(element, key, 'shift');
- MockInteractions.pressAndReleaseKeyOn(element, key, 'ctrl');
- MockInteractions.pressAndReleaseKeyOn(element, key, 'meta');
- MockInteractions.pressAndReleaseKeyOn(element, key, 'alt');
- assert.isFalse(tapSpy.calledOnce);
+ for (const eventName of ['tap', 'click']) {
+ test('stops ' + eventName + ' event', () => {
+ const spy = addSpyOn(eventName);
+ MockInteractions.tap(element);
+ assert.isFalse(spy.called);
});
}
- suite('disabled', () => {
- setup(() => {
- element.disabled = true;
+ // Keycodes: 32 for Space, 13 for Enter.
+ for (const key of [32, 13]) {
+ test('stops click event on keycode ' + key, () => {
+ const tapSpy = sandbox.spy();
+ element.addEventListener('click', tapSpy);
+ MockInteractions.pressAndReleaseKeyOn(element, key);
+ assert.isFalse(tapSpy.called);
});
+ }
+ });
- for (const eventName of ['tap', 'click']) {
- test('stops ' + eventName + ' event', () => {
- const spy = addSpyOn(eventName);
- MockInteractions.tap(element);
- assert.isFalse(spy.called);
- });
- }
+ suite('reporting', () => {
+ const reportStub = sinon.stub();
+ setup(() => {
+ stub('gr-reporting', {
+ reportInteraction: (...args) => {
+ reportStub(...args);
+ },
+ });
+ reportStub.reset();
+ });
- // Keycodes: 32 for Space, 13 for Enter.
- for (const key of [32, 13]) {
- test('stops click event on keycode ' + key, () => {
- const tapSpy = sandbox.spy();
- element.addEventListener('click', tapSpy);
- MockInteractions.pressAndReleaseKeyOn(element, key);
- assert.isFalse(tapSpy.called);
- });
- }
+ test('report event after click', () => {
+ MockInteractions.click(element);
+ assert.isTrue(reportStub.calledOnce);
+ assert.equal(reportStub.lastCall.args[0], 'button-click');
+ assert.deepEqual(reportStub.lastCall.args[1], {
+ path: 'html>body>test-fixture#basic>gr-button',
+ });
+ });
+
+ test('report event after click on nested', () => {
+ element = fixture('nested');
+ MockInteractions.click(element.querySelector('gr-button'));
+ assert.isTrue(reportStub.calledOnce);
+ assert.equal(reportStub.lastCall.args[0], 'button-click');
+ assert.deepEqual(reportStub.lastCall.args[1], {
+ path: 'html>body>test-fixture#nested>div#test>gr-button.testBtn',
+ });
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
deleted file mode 100644
index dc8ba34..0000000
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
+++ /dev/null
@@ -1,45 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-change-star">
- <template>
- <style include="shared-styles">
- button {
- background-color: transparent;
- cursor: pointer;
- }
- iron-icon.active {
- fill: var(--link-color);
- }
- iron-icon {
- vertical-align: top;
- --iron-icon-height: var(--gr-change-star-size, var(--line-height-normal, 20px));
- --iron-icon-width: var(--gr-change-star-size, var(--line-height-normal, 20px));
- }
- </style>
- <button aria-label="Change star" on-click="toggleStar">
- <iron-icon
- class$="[[_computeStarClass(change.starred)]]"
- icon$="[[_computeStarIcon(change.starred)]]"></iron-icon>
- </button>
- </template>
- <script src="gr-change-star.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
index 001632f..10e06dd 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,49 +14,56 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrChangeStar extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-change-star'; }
- /**
- * Fired when star state is toggled.
- *
- * @event toggle-star
- */
+import '../gr-icons/gr-icons.js';
+import '../../../styles/shared-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-change-star_html.js';
- static get properties() {
- return {
- /** @type {?} */
- change: {
- type: Object,
- notify: true,
- },
- };
- }
+/** @extends Polymer.Element */
+class GrChangeStar extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _computeStarClass(starred) {
- return starred ? 'active' : '';
- }
+ static get is() { return 'gr-change-star'; }
+ /**
+ * Fired when star state is toggled.
+ *
+ * @event toggle-star
+ */
- _computeStarIcon(starred) {
- // Hollow star is used to indicate inactive state.
- return `gr-icons:star${starred ? '' : '-border'}`;
- }
-
- toggleStar() {
- const newVal = !this.change.starred;
- this.set('change.starred', newVal);
- this.dispatchEvent(new CustomEvent('toggle-star', {
- bubbles: true,
- composed: true,
- detail: {change: this.change, starred: newVal},
- }));
- }
+ static get properties() {
+ return {
+ /** @type {?} */
+ change: {
+ type: Object,
+ notify: true,
+ },
+ };
}
- customElements.define(GrChangeStar.is, GrChangeStar);
-})();
+ _computeStarClass(starred) {
+ return starred ? 'active' : '';
+ }
+
+ _computeStarIcon(starred) {
+ // Hollow star is used to indicate inactive state.
+ return `gr-icons:star${starred ? '' : '-border'}`;
+ }
+
+ toggleStar() {
+ const newVal = !this.change.starred;
+ this.set('change.starred', newVal);
+ this.dispatchEvent(new CustomEvent('toggle-star', {
+ bubbles: true,
+ composed: true,
+ detail: {change: this.change, starred: newVal},
+ }));
+ }
+}
+
+customElements.define(GrChangeStar.is, GrChangeStar);
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js
new file mode 100644
index 0000000..a4925aa
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js
@@ -0,0 +1,37 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ button {
+ background-color: transparent;
+ cursor: pointer;
+ }
+ iron-icon.active {
+ fill: var(--link-color);
+ }
+ iron-icon {
+ vertical-align: top;
+ --iron-icon-height: var(--gr-change-star-size, var(--line-height-normal, 20px));
+ --iron-icon-width: var(--gr-change-star-size, var(--line-height-normal, 20px));
+ }
+ </style>
+ <button aria-label="Change star" on-click="toggleStar">
+ <iron-icon class\$="[[_computeStarClass(change.starred)]]" icon\$="[[_computeStarIcon(change.starred)]]"></iron-icon>
+ </button>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
index b76ce4d..66bbd36 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-star</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-change-star.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,51 +30,52 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-star tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-change-star.js';
+suite('gr-change-star tests', () => {
+ let element;
- setup(() => {
- element = fixture('basic');
- element.change = {
- _number: 2,
- starred: true,
- };
- });
-
- test('star visibility states', () => {
- element.set('change.starred', true);
- let icon = element.shadowRoot
- .querySelector('iron-icon');
- assert.isTrue(icon.classList.contains('active'));
- assert.equal(icon.icon, 'gr-icons:star');
-
- element.set('change.starred', false);
- icon = element.shadowRoot
- .querySelector('iron-icon');
- assert.isFalse(icon.classList.contains('active'));
- assert.equal(icon.icon, 'gr-icons:star-border');
- });
-
- test('starring', done => {
- element.addEventListener('toggle-star', () => {
- assert.equal(element.change.starred, true);
- done();
- });
- element.set('change.starred', false);
- MockInteractions.tap(element.shadowRoot
- .querySelector('button'));
- });
-
- test('unstarring', done => {
- element.addEventListener('toggle-star', () => {
- assert.equal(element.change.starred, false);
- done();
- });
- element.set('change.starred', true);
- MockInteractions.tap(element.shadowRoot
- .querySelector('button'));
- });
+ setup(() => {
+ element = fixture('basic');
+ element.change = {
+ _number: 2,
+ starred: true,
+ };
});
+
+ test('star visibility states', () => {
+ element.set('change.starred', true);
+ let icon = element.shadowRoot
+ .querySelector('iron-icon');
+ assert.isTrue(icon.classList.contains('active'));
+ assert.equal(icon.icon, 'gr-icons:star');
+
+ element.set('change.starred', false);
+ icon = element.shadowRoot
+ .querySelector('iron-icon');
+ assert.isFalse(icon.classList.contains('active'));
+ assert.equal(icon.icon, 'gr-icons:star-border');
+ });
+
+ test('starring', done => {
+ element.addEventListener('toggle-star', () => {
+ assert.equal(element.change.starred, true);
+ done();
+ });
+ element.set('change.starred', false);
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('button'));
+ });
+
+ test('unstarring', done => {
+ element.addEventListener('toggle-star', () => {
+ assert.equal(element.change.starred, false);
+ done();
+ });
+ element.set('change.starred', true);
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('button'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html
deleted file mode 100644
index eaca593..0000000
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html
+++ /dev/null
@@ -1,86 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-change-status">
- <template>
- <style include="shared-styles">
- .chip {
- border-radius: var(--border-radius);
- background-color: var(--chip-background-color);
- padding: 0 var(--spacing-m);
- white-space: nowrap;
- }
- :host(.merged) .chip {
- background-color: #5b9d52;
- color: #5b9d52;
- }
- :host(.abandoned) .chip {
- background-color: #afafaf;
- color: #afafaf;
- }
- :host(.wip) .chip {
- background-color: #8f756c;
- color: #8f756c;
- }
- :host(.private) .chip {
- background-color: #c17ccf;
- color: #c17ccf;
- }
- :host(.merge-conflict) .chip {
- background-color: #dc5c60;
- color: #dc5c60;
- }
- :host(.active) .chip {
- background-color: #29b6f6;
- color: #29b6f6;
- }
- :host(.ready-to-submit) .chip {
- background-color: #e10ca3;
- color: #e10ca3;
- }
- :host(.custom) .chip {
- background-color: #825cc2;
- color: #825cc2;
- }
- :host([flat]) .chip {
- background-color: transparent;
- padding: 0;
- }
- :host(:not([flat])) .chip {
- color: white;
- }
- </style>
- <gr-tooltip-content
- has-tooltip
- position-below
- title="[[tooltipText]]"
- max-width="40em">
- <div
- class="chip"
- aria-label$="Label: [[status]]">
- [[_computeStatusString(status)]]
- </div>
- </gr-tooltip-content>
- </template>
- <script src="gr-change-status.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
index 7052a6a..b99612e 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
@@ -14,85 +14,93 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const ChangeStates = {
- MERGED: 'Merged',
- ABANDONED: 'Abandoned',
- MERGE_CONFLICT: 'Merge Conflict',
- WIP: 'WIP',
- PRIVATE: 'Private',
- };
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-tooltip-content/gr-tooltip-content.js';
+import '../../../styles/shared-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-change-status_html.js';
- const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' +
- 'It will not appear on dashboards unless you are CC\'ed or assigned, ' +
- 'and email notifications will be silenced until the review is started.';
+const ChangeStates = {
+ MERGED: 'Merged',
+ ABANDONED: 'Abandoned',
+ MERGE_CONFLICT: 'Merge Conflict',
+ WIP: 'WIP',
+ PRIVATE: 'Private',
+};
- const MERGE_CONFLICT_TOOLTIP = 'This change has merge conflicts. ' +
- 'Download the patch and run "git rebase master". ' +
- 'Upload a new patchset after resolving all merge conflicts.';
+const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' +
+ 'It will not appear on dashboards unless you are CC\'ed or assigned, ' +
+ 'and email notifications will be silenced until the review is started.';
- const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' +
- 'current reviewers (or anyone with "View Private Changes" permission).';
+const MERGE_CONFLICT_TOOLTIP = 'This change has merge conflicts. ' +
+ 'Download the patch and run "git rebase master". ' +
+ 'Upload a new patchset after resolving all merge conflicts.';
- /** @extends Polymer.Element */
- class GrChangeStatus extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-change-status'; }
+const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' +
+ 'current reviewers (or anyone with "View Private Changes" permission).';
- static get properties() {
- return {
- flat: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- status: {
- type: String,
- observer: '_updateChipDetails',
- },
- tooltipText: {
- type: String,
- value: '',
- },
- };
- }
+/** @extends Polymer.Element */
+class GrChangeStatus extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _computeStatusString(status) {
- if (status === ChangeStates.WIP && !this.flat) {
- return 'Work in Progress';
- }
- return status;
- }
+ static get is() { return 'gr-change-status'; }
- _toClassName(str) {
- return str.toLowerCase().replace(/\s/g, '-');
- }
-
- _updateChipDetails(status, previousStatus) {
- if (previousStatus) {
- this.classList.remove(this._toClassName(previousStatus));
- }
- this.classList.add(this._toClassName(status));
-
- switch (status) {
- case ChangeStates.WIP:
- this.tooltipText = WIP_TOOLTIP;
- break;
- case ChangeStates.PRIVATE:
- this.tooltipText = PRIVATE_TOOLTIP;
- break;
- case ChangeStates.MERGE_CONFLICT:
- this.tooltipText = MERGE_CONFLICT_TOOLTIP;
- break;
- default:
- this.tooltipText = '';
- break;
- }
- }
+ static get properties() {
+ return {
+ flat: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ status: {
+ type: String,
+ observer: '_updateChipDetails',
+ },
+ tooltipText: {
+ type: String,
+ value: '',
+ },
+ };
}
- customElements.define(GrChangeStatus.is, GrChangeStatus);
-})();
+ _computeStatusString(status) {
+ if (status === ChangeStates.WIP && !this.flat) {
+ return 'Work in Progress';
+ }
+ return status;
+ }
+
+ _toClassName(str) {
+ return str.toLowerCase().replace(/\s/g, '-');
+ }
+
+ _updateChipDetails(status, previousStatus) {
+ if (previousStatus) {
+ this.classList.remove(this._toClassName(previousStatus));
+ }
+ this.classList.add(this._toClassName(status));
+
+ switch (status) {
+ case ChangeStates.WIP:
+ this.tooltipText = WIP_TOOLTIP;
+ break;
+ case ChangeStates.PRIVATE:
+ this.tooltipText = PRIVATE_TOOLTIP;
+ break;
+ case ChangeStates.MERGE_CONFLICT:
+ this.tooltipText = MERGE_CONFLICT_TOOLTIP;
+ break;
+ default:
+ this.tooltipText = '';
+ break;
+ }
+ }
+}
+
+customElements.define(GrChangeStatus.is, GrChangeStatus);
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js
new file mode 100644
index 0000000..1a1bc1b8
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js
@@ -0,0 +1,72 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ .chip {
+ border-radius: var(--border-radius);
+ background-color: var(--chip-background-color);
+ padding: 0 var(--spacing-m);
+ white-space: nowrap;
+ }
+ :host(.merged) .chip {
+ background-color: #5b9d52;
+ color: #5b9d52;
+ }
+ :host(.abandoned) .chip {
+ background-color: #afafaf;
+ color: #afafaf;
+ }
+ :host(.wip) .chip {
+ background-color: #8f756c;
+ color: #8f756c;
+ }
+ :host(.private) .chip {
+ background-color: #c17ccf;
+ color: #c17ccf;
+ }
+ :host(.merge-conflict) .chip {
+ background-color: #dc5c60;
+ color: #dc5c60;
+ }
+ :host(.active) .chip {
+ background-color: #29b6f6;
+ color: #29b6f6;
+ }
+ :host(.ready-to-submit) .chip {
+ background-color: #e10ca3;
+ color: #e10ca3;
+ }
+ :host(.custom) .chip {
+ background-color: #825cc2;
+ color: #825cc2;
+ }
+ :host([flat]) .chip {
+ background-color: transparent;
+ padding: 0;
+ }
+ :host(:not([flat])) .chip {
+ color: white;
+ }
+ </style>
+ <gr-tooltip-content has-tooltip="" position-below="" title="[[tooltipText]]" max-width="40em">
+ <div class="chip" aria-label\$="Label: [[status]]">
+ [[_computeStatusString(status)]]
+ </div>
+ </gr-tooltip-content>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
index d78cc3a..48c1495 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-status</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-change-status.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,106 +30,107 @@
</template>
</test-fixture>
-<script>
- const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' +
- 'It will not appear on dashboards unless you are CC\'ed or assigned, ' +
- 'and email notifications will be silenced until the review is started.';
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-change-status.js';
+const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' +
+ 'It will not appear on dashboards unless you are CC\'ed or assigned, ' +
+ 'and email notifications will be silenced until the review is started.';
- const MERGE_CONFLICT_TOOLTIP = 'This change has merge conflicts. ' +
- 'Download the patch and run "git rebase master". ' +
- 'Upload a new patchset after resolving all merge conflicts.';
+const MERGE_CONFLICT_TOOLTIP = 'This change has merge conflicts. ' +
+ 'Download the patch and run "git rebase master". ' +
+ 'Upload a new patchset after resolving all merge conflicts.';
- const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' +
- 'current reviewers (or anyone with "View Private Changes" permission).';
+const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' +
+ 'current reviewers (or anyone with "View Private Changes" permission).';
- suite('gr-change-status tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+suite('gr-change-status tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('WIP', () => {
- element.status = 'WIP';
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, 'Work in Progress');
- assert.equal(element.tooltipText, WIP_TOOLTIP);
- assert.isTrue(element.classList.contains('wip'));
- });
-
- test('WIP flat', () => {
- element.flat = true;
- element.status = 'WIP';
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, 'WIP');
- assert.isDefined(element.tooltipText);
- assert.isTrue(element.classList.contains('wip'));
- assert.isTrue(element.hasAttribute('flat'));
- });
-
- test('merged', () => {
- element.status = 'Merged';
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, '');
- assert.isTrue(element.classList.contains('merged'));
- });
-
- test('abandoned', () => {
- element.status = 'Abandoned';
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, '');
- assert.isTrue(element.classList.contains('abandoned'));
- });
-
- test('merge conflict', () => {
- element.status = 'Merge Conflict';
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, MERGE_CONFLICT_TOOLTIP);
- assert.isTrue(element.classList.contains('merge-conflict'));
- });
-
- test('private', () => {
- element.status = 'Private';
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, PRIVATE_TOOLTIP);
- assert.isTrue(element.classList.contains('private'));
- });
-
- test('active', () => {
- element.status = 'Active';
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, '');
- assert.isTrue(element.classList.contains('active'));
- });
-
- test('ready to submit', () => {
- element.status = 'Ready to submit';
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, '');
- assert.isTrue(element.classList.contains('ready-to-submit'));
- });
-
- test('updating status removes the previous class', () => {
- element.status = 'Private';
- assert.isTrue(element.classList.contains('private'));
- assert.isFalse(element.classList.contains('wip'));
-
- element.status = 'WIP';
- assert.isFalse(element.classList.contains('private'));
- assert.isTrue(element.classList.contains('wip'));
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('WIP', () => {
+ element.status = 'WIP';
+ assert.equal(element.shadowRoot
+ .querySelector('.chip').innerText, 'Work in Progress');
+ assert.equal(element.tooltipText, WIP_TOOLTIP);
+ assert.isTrue(element.classList.contains('wip'));
+ });
+
+ test('WIP flat', () => {
+ element.flat = true;
+ element.status = 'WIP';
+ assert.equal(element.shadowRoot
+ .querySelector('.chip').innerText, 'WIP');
+ assert.isDefined(element.tooltipText);
+ assert.isTrue(element.classList.contains('wip'));
+ assert.isTrue(element.hasAttribute('flat'));
+ });
+
+ test('merged', () => {
+ element.status = 'Merged';
+ assert.equal(element.shadowRoot
+ .querySelector('.chip').innerText, element.status);
+ assert.equal(element.tooltipText, '');
+ assert.isTrue(element.classList.contains('merged'));
+ });
+
+ test('abandoned', () => {
+ element.status = 'Abandoned';
+ assert.equal(element.shadowRoot
+ .querySelector('.chip').innerText, element.status);
+ assert.equal(element.tooltipText, '');
+ assert.isTrue(element.classList.contains('abandoned'));
+ });
+
+ test('merge conflict', () => {
+ element.status = 'Merge Conflict';
+ assert.equal(element.shadowRoot
+ .querySelector('.chip').innerText, element.status);
+ assert.equal(element.tooltipText, MERGE_CONFLICT_TOOLTIP);
+ assert.isTrue(element.classList.contains('merge-conflict'));
+ });
+
+ test('private', () => {
+ element.status = 'Private';
+ assert.equal(element.shadowRoot
+ .querySelector('.chip').innerText, element.status);
+ assert.equal(element.tooltipText, PRIVATE_TOOLTIP);
+ assert.isTrue(element.classList.contains('private'));
+ });
+
+ test('active', () => {
+ element.status = 'Active';
+ assert.equal(element.shadowRoot
+ .querySelector('.chip').innerText, element.status);
+ assert.equal(element.tooltipText, '');
+ assert.isTrue(element.classList.contains('active'));
+ });
+
+ test('ready to submit', () => {
+ element.status = 'Ready to submit';
+ assert.equal(element.shadowRoot
+ .querySelector('.chip').innerText, element.status);
+ assert.equal(element.tooltipText, '');
+ assert.isTrue(element.classList.contains('ready-to-submit'));
+ });
+
+ test('updating status removes the previous class', () => {
+ element.status = 'Private';
+ assert.isTrue(element.classList.contains('private'));
+ assert.isFalse(element.classList.contains('wip'));
+
+ element.status = 'WIP';
+ assert.isFalse(element.classList.contains('private'));
+ assert.isTrue(element.classList.contains('wip'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html
deleted file mode 100644
index d615a7f..0000000
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html
+++ /dev/null
@@ -1,140 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-storage/gr-storage.html">
-<link rel="import" href="../gr-comment/gr-comment.html">
-
-<dom-module id="gr-comment-thread">
- <template>
- <style include="shared-styles">
- :host {
- font-family: var(--font-family);
- font-size: var(--font-size-normal);
- font-weight: var(--font-weight-normal);
- line-height: var(--line-height-normal);
- }
- gr-button {
- margin-left: var(--spacing-m);
- }
- #actions {
- margin-left: auto;
- padding: var(--spacing-m);
- }
- #container {
- background-color: var(--comment-background-color);
- color: var(--comment-text-color);
- display: block;
- margin: 0 var(--spacing-s) var(--spacing-s);
- white-space: normal;
- box-shadow: var(--elevation-level-2);
- border-radius: var(--border-radius);
- /** This is required for firefox to continue the inheritance */
- -webkit-user-select: inherit;
- -moz-user-select: inherit;
- -ms-user-select: inherit;
- user-select: inherit;
- }
- #container.unresolved {
- background-color: var(--unresolved-comment-background-color);
- }
- #container.robotComment {
- background-color: var(--robot-comment-background-color);
- }
- #commentInfoContainer {
- border-top: 1px dotted var(--border-color);
- display: flex;
- }
- #unresolvedLabel {
- font-family: var(--font-family);
- margin: auto 0;
- padding: var(--spacing-m);
- }
- .pathInfo {
- display: flex;
- align-items: baseline;
- justify-content: space-between;
- padding: 0 var(--spacing-s) var(--spacing-s);
- }
- .descriptionText {
- margin-left: var(--spacing-m);
- font-style: italic;
- }
- </style>
- <template is="dom-if" if="[[showFilePath]]">
- <div class="pathInfo">
- <a href$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]">[[_computeDisplayPath(path)]]</a>
- <span class="descriptionText">Patchset [[patchNum]]</span>
- </div>
- </template>
- <div id="container" class$="[[_computeHostClass(unresolved, isRobotComment)]]">
- <template id="commentList" is="dom-repeat" items="[[_orderedComments]]"
- as="comment">
- <gr-comment
- comment="{{comment}}"
- comments="{{comments}}"
- robot-button-disabled="[[_shouldDisableAction(_showActions, _lastComment)]]"
- change-num="[[changeNum]]"
- patch-num="[[patchNum]]"
- draft="[[_isDraft(comment)]]"
- show-actions="[[_showActions]]"
- comment-side="[[comment.__commentSide]]"
- side="[[comment.side]]"
- project-config="[[_projectConfig]]"
- on-create-fix-comment="_handleCommentFix"
- on-comment-discard="_handleCommentDiscard"
- on-comment-save="_handleCommentSavedOrDiscarded"></gr-comment>
- </template>
- <div id="commentInfoContainer"
- hidden$="[[_hideActions(_showActions, _lastComment)]]">
- <span id="unresolvedLabel" hidden$="[[!unresolved]]">Unresolved</span>
- <div id="actions">
- <gr-button
- id="replyBtn"
- link
- class="action reply"
- on-click="_handleCommentReply">Reply</gr-button>
- <gr-button
- id="quoteBtn"
- link
- class="action quote"
- on-click="_handleCommentQuote">Quote</gr-button>
- <gr-button
- id="ackBtn"
- link
- class="action ack"
- on-click="_handleCommentAck">Ack</gr-button>
- <gr-button
- id="doneBtn"
- link
- class="action done"
- on-click="_handleCommentDone">Done</gr-button>
- </div>
- </div>
- </div>
- <gr-reporting id="reporting"></gr-reporting>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- <gr-storage id="storage"></gr-storage>
- </template>
- <script src="gr-comment-thread.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
index d8a56f8..765c5cc 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,517 +14,532 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const UNRESOLVED_EXPAND_COUNT = 5;
- const NEWLINE_PATTERN = /\n/g;
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-storage/gr-storage.js';
+import '../gr-comment/gr-comment.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-comment-thread_html.js';
+
+const UNRESOLVED_EXPAND_COUNT = 5;
+const NEWLINE_PATTERN = /\n/g;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.PathListMixin
+ * @extends Polymer.Element
+ */
+class GrCommentThread extends mixinBehaviors( [
+ /**
+ * Not used in this element rather other elements tests
+ */
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+ Gerrit.PathListBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-comment-thread'; }
+ /**
+ * Fired when the thread should be discarded.
+ *
+ * @event thread-discard
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Gerrit.PathListMixin
- * @extends Polymer.Element
+ * Fired when a comment in the thread is permanently modified.
+ *
+ * @event thread-changed
*/
- class GrCommentThread extends Polymer.mixinBehaviors( [
- /**
- * Not used in this element rather other elements tests
- */
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- Gerrit.PathListBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-comment-thread'; }
- /**
- * Fired when the thread should be discarded.
- *
- * @event thread-discard
- */
- /**
- * Fired when a comment in the thread is permanently modified.
- *
- * @event thread-changed
- */
+ /**
+ * gr-comment-thread exposes the following attributes that allow a
+ * diff widget like gr-diff to show the thread in the right location:
+ *
+ * line-num:
+ * 1-based line number or undefined if it refers to the entire file.
+ *
+ * comment-side:
+ * "left" or "right". These indicate which of the two diffed versions
+ * the comment relates to. In the case of unified diff, the left
+ * version is the one whose line number column is further to the left.
+ *
+ * range:
+ * The range of text that the comment refers to (start_line,
+ * start_character, end_line, end_character), serialized as JSON. If
+ * set, range's end_line will have the same value as line-num. Line
+ * numbers are 1-based, char numbers are 0-based. The start position
+ * (start_line, start_character) is inclusive, and the end position
+ * (end_line, end_character) is exclusive.
+ */
+ static get properties() {
+ return {
+ changeNum: String,
+ comments: {
+ type: Array,
+ value() { return []; },
+ },
+ /**
+ * @type {?{start_line: number, start_character: number, end_line: number,
+ * end_character: number}}
+ */
+ range: {
+ type: Object,
+ reflectToAttribute: true,
+ },
+ keyEventTarget: {
+ type: Object,
+ value() { return document.body; },
+ },
+ commentSide: {
+ type: String,
+ reflectToAttribute: true,
+ },
+ patchNum: String,
+ path: String,
+ projectName: {
+ type: String,
+ observer: '_projectNameChanged',
+ },
+ hasDraft: {
+ type: Boolean,
+ notify: true,
+ reflectToAttribute: true,
+ },
+ isOnParent: {
+ type: Boolean,
+ value: false,
+ },
+ parentIndex: {
+ type: Number,
+ value: null,
+ },
+ rootId: {
+ type: String,
+ notify: true,
+ computed: '_computeRootId(comments.*)',
+ },
+ /**
+ * If this is true, the comment thread also needs to have the change and
+ * line properties property set
+ */
+ showFilePath: {
+ type: Boolean,
+ value: false,
+ },
+ /** Necessary only if showFilePath is true or when used with gr-diff */
+ lineNum: {
+ type: Number,
+ reflectToAttribute: true,
+ },
+ unresolved: {
+ type: Boolean,
+ notify: true,
+ reflectToAttribute: true,
+ },
+ _showActions: Boolean,
+ _lastComment: Object,
+ _orderedComments: Array,
+ _projectConfig: Object,
+ isRobotComment: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ };
+ }
- /**
- * gr-comment-thread exposes the following attributes that allow a
- * diff widget like gr-diff to show the thread in the right location:
- *
- * line-num:
- * 1-based line number or undefined if it refers to the entire file.
- *
- * comment-side:
- * "left" or "right". These indicate which of the two diffed versions
- * the comment relates to. In the case of unified diff, the left
- * version is the one whose line number column is further to the left.
- *
- * range:
- * The range of text that the comment refers to (start_line,
- * start_character, end_line, end_character), serialized as JSON. If
- * set, range's end_line will have the same value as line-num. Line
- * numbers are 1-based, char numbers are 0-based. The start position
- * (start_line, start_character) is inclusive, and the end position
- * (end_line, end_character) is exclusive.
- */
- static get properties() {
- return {
- changeNum: String,
- comments: {
- type: Array,
- value() { return []; },
- },
- /**
- * @type {?{start_line: number, start_character: number, end_line: number,
- * end_character: number}}
- */
- range: {
- type: Object,
- reflectToAttribute: true,
- },
- keyEventTarget: {
- type: Object,
- value() { return document.body; },
- },
- commentSide: {
- type: String,
- reflectToAttribute: true,
- },
- patchNum: String,
- path: String,
- projectName: {
- type: String,
- observer: '_projectNameChanged',
- },
- hasDraft: {
- type: Boolean,
- notify: true,
- reflectToAttribute: true,
- },
- isOnParent: {
- type: Boolean,
- value: false,
- },
- parentIndex: {
- type: Number,
- value: null,
- },
- rootId: {
- type: String,
- notify: true,
- computed: '_computeRootId(comments.*)',
- },
- /**
- * If this is true, the comment thread also needs to have the change and
- * line properties property set
- */
- showFilePath: {
- type: Boolean,
- value: false,
- },
- /** Necessary only if showFilePath is true or when used with gr-diff */
- lineNum: {
- type: Number,
- reflectToAttribute: true,
- },
- unresolved: {
- type: Boolean,
- notify: true,
- reflectToAttribute: true,
- },
- _showActions: Boolean,
- _lastComment: Object,
- _orderedComments: Array,
- _projectConfig: Object,
- isRobotComment: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- };
- }
+ static get observers() {
+ return [
+ '_commentsChanged(comments.*)',
+ ];
+ }
- static get observers() {
- return [
- '_commentsChanged(comments.*)',
- ];
- }
+ get keyBindings() {
+ return {
+ 'e shift+e': '_handleEKey',
+ };
+ }
- get keyBindings() {
- return {
- 'e shift+e': '_handleEKey',
- };
- }
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('comment-update',
+ e => this._handleCommentUpdate(e));
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('comment-update',
- e => this._handleCommentUpdate(e));
- }
+ /** @override */
+ attached() {
+ super.attached();
+ this._getLoggedIn().then(loggedIn => {
+ this._showActions = loggedIn;
+ });
+ this._setInitialExpandedState();
+ }
- /** @override */
- attached() {
- super.attached();
- this._getLoggedIn().then(loggedIn => {
- this._showActions = loggedIn;
- });
- this._setInitialExpandedState();
- }
+ addOrEditDraft(opt_lineNum, opt_range) {
+ const lastComment = this.comments[this.comments.length - 1] || {};
+ if (lastComment.__draft) {
+ const commentEl = this._commentElWithDraftID(
+ lastComment.id || lastComment.__draftID);
+ commentEl.editing = true;
- addOrEditDraft(opt_lineNum, opt_range) {
- const lastComment = this.comments[this.comments.length - 1] || {};
- if (lastComment.__draft) {
- const commentEl = this._commentElWithDraftID(
- lastComment.id || lastComment.__draftID);
- commentEl.editing = true;
-
- // If the comment was collapsed, re-open it to make it clear which
- // actions are available.
- commentEl.collapsed = false;
- } else {
- const range = opt_range ? opt_range :
- lastComment ? lastComment.range : undefined;
- const unresolved = lastComment ? lastComment.unresolved : undefined;
- this.addDraft(opt_lineNum, range, unresolved);
- }
- }
-
- addDraft(opt_lineNum, opt_range, opt_unresolved) {
- const draft = this._newDraft(opt_lineNum, opt_range);
- draft.__editing = true;
- draft.unresolved = opt_unresolved === false ? opt_unresolved : true;
- this.push('comments', draft);
- }
-
- fireRemoveSelf() {
- this.dispatchEvent(new CustomEvent('thread-discard',
- {detail: {rootId: this.rootId}, bubbles: false}));
- }
-
- _getDiffUrlForComment(projectName, changeNum, path, patchNum) {
- return Gerrit.Nav.getUrlForDiffById(changeNum,
- projectName, path, patchNum,
- null, this.lineNum);
- }
-
- _computeDisplayPath(path) {
- const lineString = this.lineNum ? `#${this.lineNum}` : '';
- return this.computeDisplayPath(path) + lineString;
- }
-
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- _commentsChanged() {
- this._orderedComments = this._sortedComments(this.comments);
- this.updateThreadProperties();
- }
-
- updateThreadProperties() {
- if (this._orderedComments.length) {
- this._lastComment = this._getLastComment();
- this.unresolved = this._lastComment.unresolved;
- this.hasDraft = this._lastComment.__draft;
- this.isRobotComment = !!(this._lastComment.robot_id);
- }
- }
-
- _shouldDisableAction(_showActions, _lastComment) {
- return !_showActions || !_lastComment || !!_lastComment.__draft;
- }
-
- _hideActions(_showActions, _lastComment) {
- return this._shouldDisableAction(_showActions, _lastComment) ||
- !!_lastComment.robot_id;
- }
-
- _getLastComment() {
- return this._orderedComments[this._orderedComments.length - 1] || {};
- }
-
- _handleEKey(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- // Don’t preventDefault in this case because it will render the event
- // useless for other handlers (other gr-comment-thread elements).
- if (e.detail.keyboardEvent.shiftKey) {
- this._expandCollapseComments(true);
- } else {
- if (this.modifierPressed(e)) { return; }
- this._expandCollapseComments(false);
- }
- }
-
- _expandCollapseComments(actionIsCollapse) {
- const comments =
- Polymer.dom(this.root).querySelectorAll('gr-comment');
- for (const comment of comments) {
- comment.collapsed = actionIsCollapse;
- }
- }
-
- /**
- * Sets the initial state of the comment thread.
- * Expands the thread if one of the following is true:
- * - last {UNRESOLVED_EXPAND_COUNT} comments expanded by default if the
- * thread is unresolved,
- * - it's a robot comment.
- */
- _setInitialExpandedState() {
- if (this._orderedComments) {
- for (let i = 0; i < this._orderedComments.length; i++) {
- const comment = this._orderedComments[i];
- const isRobotComment = !!comment.robot_id;
- // False if it's an unresolved comment under UNRESOLVED_EXPAND_COUNT.
- const resolvedThread = !this.unresolved ||
- this._orderedComments.length - i - 1 >= UNRESOLVED_EXPAND_COUNT;
- comment.collapsed = !isRobotComment && resolvedThread;
- }
- }
- }
-
- _sortedComments(comments) {
- return comments.slice().sort((c1, c2) => {
- const c1Date = c1.__date || util.parseDate(c1.updated);
- const c2Date = c2.__date || util.parseDate(c2.updated);
- const dateCompare = c1Date - c2Date;
- // Ensure drafts are at the end. There should only be one but in edge
- // cases could be more. In the unlikely event two drafts are being
- // compared, use the typical date compare.
- if (c2.__draft && !c1.__draft ) { return -1; }
- if (c1.__draft && !c2.__draft ) { return 1; }
- if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) { return 0; }
- // If same date, fall back to sorting by id.
- return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
- });
- }
-
- _createReplyComment(parent, content, opt_isEditing,
- opt_unresolved) {
- this.$.reporting.recordDraftInteraction();
- const reply = this._newReply(
- this._orderedComments[this._orderedComments.length - 1].id,
- parent.line,
- content,
- opt_unresolved,
- parent.range);
-
- // If there is currently a comment in an editing state, add an attribute
- // so that the gr-comment knows not to populate the draft text.
- for (let i = 0; i < this.comments.length; i++) {
- if (this.comments[i].__editing) {
- reply.__otherEditing = true;
- break;
- }
- }
-
- if (opt_isEditing) {
- reply.__editing = true;
- }
-
- this.push('comments', reply);
-
- if (!opt_isEditing) {
- // Allow the reply to render in the dom-repeat.
- this.async(() => {
- const commentEl = this._commentElWithDraftID(reply.__draftID);
- commentEl.save();
- }, 1);
- }
- }
-
- _isDraft(comment) {
- return !!comment.__draft;
- }
-
- /**
- * @param {boolean=} opt_quote
- */
- _processCommentReply(opt_quote) {
- const comment = this._lastComment;
- let quoteStr;
- if (opt_quote) {
- const msg = comment.message;
- quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
- }
- this._createReplyComment(comment, quoteStr, true, comment.unresolved);
- }
-
- _handleCommentReply(e) {
- this._processCommentReply();
- }
-
- _handleCommentQuote(e) {
- this._processCommentReply(true);
- }
-
- _handleCommentAck(e) {
- const comment = this._lastComment;
- this._createReplyComment(comment, 'Ack', false, false);
- }
-
- _handleCommentDone(e) {
- const comment = this._lastComment;
- this._createReplyComment(comment, 'Done', false, false);
- }
-
- _handleCommentFix(e) {
- const comment = e.detail.comment;
- const msg = comment.message;
- const quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
- const response = quoteStr + 'Please fix.';
- this._createReplyComment(comment, response, false, true);
- }
-
- _commentElWithDraftID(id) {
- const els = Polymer.dom(this.root).querySelectorAll('gr-comment');
- for (const el of els) {
- if (el.comment.id === id || el.comment.__draftID === id) {
- return el;
- }
- }
- return null;
- }
-
- _newReply(inReplyTo, opt_lineNum, opt_message, opt_unresolved,
- opt_range) {
- const d = this._newDraft(opt_lineNum);
- d.in_reply_to = inReplyTo;
- d.range = opt_range;
- if (opt_message != null) {
- d.message = opt_message;
- }
- if (opt_unresolved !== undefined) {
- d.unresolved = opt_unresolved;
- }
- return d;
- }
-
- /**
- * @param {number=} opt_lineNum
- * @param {!Object=} opt_range
- */
- _newDraft(opt_lineNum, opt_range) {
- const d = {
- __draft: true,
- __draftID: Math.random().toString(36),
- __date: new Date(),
- path: this.path,
- patchNum: this.patchNum,
- side: this._getSide(this.isOnParent),
- __commentSide: this.commentSide,
- };
- if (opt_lineNum) {
- d.line = opt_lineNum;
- }
- if (opt_range) {
- d.range = opt_range;
- }
- if (this.parentIndex) {
- d.parent = this.parentIndex;
- }
- return d;
- }
-
- _getSide(isOnParent) {
- if (isOnParent) { return 'PARENT'; }
- return 'REVISION';
- }
-
- _computeRootId(comments) {
- // Keep the root ID even if the comment was removed, so that notification
- // to sync will know which thread to remove.
- if (!comments.base.length) { return this.rootId; }
- const rootComment = comments.base[0];
- return rootComment.id || rootComment.__draftID;
- }
-
- _handleCommentDiscard(e) {
- const diffCommentEl = Polymer.dom(e).rootTarget;
- const comment = diffCommentEl.comment;
- const idx = this._indexOf(comment, this.comments);
- if (idx == -1) {
- throw Error('Cannot find comment ' +
- JSON.stringify(diffCommentEl.comment));
- }
- this.splice('comments', idx, 1);
- if (this.comments.length === 0) {
- this.fireRemoveSelf();
- }
- this._handleCommentSavedOrDiscarded(e);
-
- // Check to see if there are any other open comments getting edited and
- // set the local storage value to its message value.
- for (const changeComment of this.comments) {
- if (changeComment.__editing) {
- const commentLocation = {
- changeNum: this.changeNum,
- patchNum: this.patchNum,
- path: changeComment.path,
- line: changeComment.line,
- };
- return this.$.storage.setDraftComment(commentLocation,
- changeComment.message);
- }
- }
- }
-
- _handleCommentSavedOrDiscarded(e) {
- this.dispatchEvent(new CustomEvent('thread-changed',
- {detail: {rootId: this.rootId, path: this.path},
- bubbles: false}));
- }
-
- _handleCommentUpdate(e) {
- const comment = e.detail.comment;
- const index = this._indexOf(comment, this.comments);
- if (index === -1) {
- // This should never happen: comment belongs to another thread.
- console.warn('Comment update for another comment thread.');
- return;
- }
- this.set(['comments', index], comment);
- // Because of the way we pass these comment objects around by-ref, in
- // combination with the fact that Polymer does dirty checking in
- // observers, the this.set() call above will not cause a thread update in
- // some situations.
- this.updateThreadProperties();
- }
-
- _indexOf(comment, arr) {
- for (let i = 0; i < arr.length; i++) {
- const c = arr[i];
- if ((c.__draftID != null && c.__draftID == comment.__draftID) ||
- (c.id != null && c.id == comment.id)) {
- return i;
- }
- }
- return -1;
- }
-
- _computeHostClass(unresolved) {
- if (this.isRobotComment) {
- return 'robotComment';
- }
- return unresolved ? 'unresolved' : '';
- }
-
- /**
- * Load the project config when a project name has been provided.
- *
- * @param {string} name The project name.
- */
- _projectNameChanged(name) {
- if (!name) { return; }
- this.$.restAPI.getProjectConfig(name).then(config => {
- this._projectConfig = config;
- });
+ // If the comment was collapsed, re-open it to make it clear which
+ // actions are available.
+ commentEl.collapsed = false;
+ } else {
+ const range = opt_range ? opt_range :
+ lastComment ? lastComment.range : undefined;
+ const unresolved = lastComment ? lastComment.unresolved : undefined;
+ this.addDraft(opt_lineNum, range, unresolved);
}
}
- customElements.define(GrCommentThread.is, GrCommentThread);
-})();
+ addDraft(opt_lineNum, opt_range, opt_unresolved) {
+ const draft = this._newDraft(opt_lineNum, opt_range);
+ draft.__editing = true;
+ draft.unresolved = opt_unresolved === false ? opt_unresolved : true;
+ this.push('comments', draft);
+ }
+
+ fireRemoveSelf() {
+ this.dispatchEvent(new CustomEvent('thread-discard',
+ {detail: {rootId: this.rootId}, bubbles: false}));
+ }
+
+ _getDiffUrlForComment(projectName, changeNum, path, patchNum) {
+ return Gerrit.Nav.getUrlForDiffById(changeNum,
+ projectName, path, patchNum,
+ null, this.lineNum);
+ }
+
+ _computeDisplayPath(path) {
+ const lineString = this.lineNum ? `#${this.lineNum}` : '';
+ return this.computeDisplayPath(path) + lineString;
+ }
+
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _commentsChanged() {
+ this._orderedComments = this._sortedComments(this.comments);
+ this.updateThreadProperties();
+ }
+
+ updateThreadProperties() {
+ if (this._orderedComments.length) {
+ this._lastComment = this._getLastComment();
+ this.unresolved = this._lastComment.unresolved;
+ this.hasDraft = this._lastComment.__draft;
+ this.isRobotComment = !!(this._lastComment.robot_id);
+ }
+ }
+
+ _shouldDisableAction(_showActions, _lastComment) {
+ return !_showActions || !_lastComment || !!_lastComment.__draft;
+ }
+
+ _hideActions(_showActions, _lastComment) {
+ return this._shouldDisableAction(_showActions, _lastComment) ||
+ !!_lastComment.robot_id;
+ }
+
+ _getLastComment() {
+ return this._orderedComments[this._orderedComments.length - 1] || {};
+ }
+
+ _handleEKey(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ // Don’t preventDefault in this case because it will render the event
+ // useless for other handlers (other gr-comment-thread elements).
+ if (e.detail.keyboardEvent.shiftKey) {
+ this._expandCollapseComments(true);
+ } else {
+ if (this.modifierPressed(e)) { return; }
+ this._expandCollapseComments(false);
+ }
+ }
+
+ _expandCollapseComments(actionIsCollapse) {
+ const comments =
+ dom(this.root).querySelectorAll('gr-comment');
+ for (const comment of comments) {
+ comment.collapsed = actionIsCollapse;
+ }
+ }
+
+ /**
+ * Sets the initial state of the comment thread.
+ * Expands the thread if one of the following is true:
+ * - last {UNRESOLVED_EXPAND_COUNT} comments expanded by default if the
+ * thread is unresolved,
+ * - it's a robot comment.
+ */
+ _setInitialExpandedState() {
+ if (this._orderedComments) {
+ for (let i = 0; i < this._orderedComments.length; i++) {
+ const comment = this._orderedComments[i];
+ const isRobotComment = !!comment.robot_id;
+ // False if it's an unresolved comment under UNRESOLVED_EXPAND_COUNT.
+ const resolvedThread = !this.unresolved ||
+ this._orderedComments.length - i - 1 >= UNRESOLVED_EXPAND_COUNT;
+ comment.collapsed = !isRobotComment && resolvedThread;
+ }
+ }
+ }
+
+ _sortedComments(comments) {
+ return comments.slice().sort((c1, c2) => {
+ const c1Date = c1.__date || util.parseDate(c1.updated);
+ const c2Date = c2.__date || util.parseDate(c2.updated);
+ const dateCompare = c1Date - c2Date;
+ // Ensure drafts are at the end. There should only be one but in edge
+ // cases could be more. In the unlikely event two drafts are being
+ // compared, use the typical date compare.
+ if (c2.__draft && !c1.__draft ) { return -1; }
+ if (c1.__draft && !c2.__draft ) { return 1; }
+ if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) { return 0; }
+ // If same date, fall back to sorting by id.
+ return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
+ });
+ }
+
+ _createReplyComment(parent, content, opt_isEditing,
+ opt_unresolved) {
+ this.$.reporting.recordDraftInteraction();
+ const reply = this._newReply(
+ this._orderedComments[this._orderedComments.length - 1].id,
+ parent.line,
+ content,
+ opt_unresolved,
+ parent.range);
+
+ // If there is currently a comment in an editing state, add an attribute
+ // so that the gr-comment knows not to populate the draft text.
+ for (let i = 0; i < this.comments.length; i++) {
+ if (this.comments[i].__editing) {
+ reply.__otherEditing = true;
+ break;
+ }
+ }
+
+ if (opt_isEditing) {
+ reply.__editing = true;
+ }
+
+ this.push('comments', reply);
+
+ if (!opt_isEditing) {
+ // Allow the reply to render in the dom-repeat.
+ this.async(() => {
+ const commentEl = this._commentElWithDraftID(reply.__draftID);
+ commentEl.save();
+ }, 1);
+ }
+ }
+
+ _isDraft(comment) {
+ return !!comment.__draft;
+ }
+
+ /**
+ * @param {boolean=} opt_quote
+ */
+ _processCommentReply(opt_quote) {
+ const comment = this._lastComment;
+ let quoteStr;
+ if (opt_quote) {
+ const msg = comment.message;
+ quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
+ }
+ this._createReplyComment(comment, quoteStr, true, comment.unresolved);
+ }
+
+ _handleCommentReply(e) {
+ this._processCommentReply();
+ }
+
+ _handleCommentQuote(e) {
+ this._processCommentReply(true);
+ }
+
+ _handleCommentAck(e) {
+ const comment = this._lastComment;
+ this._createReplyComment(comment, 'Ack', false, false);
+ }
+
+ _handleCommentDone(e) {
+ const comment = this._lastComment;
+ this._createReplyComment(comment, 'Done', false, false);
+ }
+
+ _handleCommentFix(e) {
+ const comment = e.detail.comment;
+ const msg = comment.message;
+ const quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
+ const response = quoteStr + 'Please fix.';
+ this._createReplyComment(comment, response, false, true);
+ }
+
+ _commentElWithDraftID(id) {
+ const els = dom(this.root).querySelectorAll('gr-comment');
+ for (const el of els) {
+ if (el.comment.id === id || el.comment.__draftID === id) {
+ return el;
+ }
+ }
+ return null;
+ }
+
+ _newReply(inReplyTo, opt_lineNum, opt_message, opt_unresolved,
+ opt_range) {
+ const d = this._newDraft(opt_lineNum);
+ d.in_reply_to = inReplyTo;
+ d.range = opt_range;
+ if (opt_message != null) {
+ d.message = opt_message;
+ }
+ if (opt_unresolved !== undefined) {
+ d.unresolved = opt_unresolved;
+ }
+ return d;
+ }
+
+ /**
+ * @param {number=} opt_lineNum
+ * @param {!Object=} opt_range
+ */
+ _newDraft(opt_lineNum, opt_range) {
+ const d = {
+ __draft: true,
+ __draftID: Math.random().toString(36),
+ __date: new Date(),
+ path: this.path,
+ patchNum: this.patchNum,
+ side: this._getSide(this.isOnParent),
+ __commentSide: this.commentSide,
+ };
+ if (opt_lineNum) {
+ d.line = opt_lineNum;
+ }
+ if (opt_range) {
+ d.range = opt_range;
+ }
+ if (this.parentIndex) {
+ d.parent = this.parentIndex;
+ }
+ return d;
+ }
+
+ _getSide(isOnParent) {
+ if (isOnParent) { return 'PARENT'; }
+ return 'REVISION';
+ }
+
+ _computeRootId(comments) {
+ // Keep the root ID even if the comment was removed, so that notification
+ // to sync will know which thread to remove.
+ if (!comments.base.length) { return this.rootId; }
+ const rootComment = comments.base[0];
+ return rootComment.id || rootComment.__draftID;
+ }
+
+ _handleCommentDiscard(e) {
+ const diffCommentEl = dom(e).rootTarget;
+ const comment = diffCommentEl.comment;
+ const idx = this._indexOf(comment, this.comments);
+ if (idx == -1) {
+ throw Error('Cannot find comment ' +
+ JSON.stringify(diffCommentEl.comment));
+ }
+ this.splice('comments', idx, 1);
+ if (this.comments.length === 0) {
+ this.fireRemoveSelf();
+ }
+ this._handleCommentSavedOrDiscarded(e);
+
+ // Check to see if there are any other open comments getting edited and
+ // set the local storage value to its message value.
+ for (const changeComment of this.comments) {
+ if (changeComment.__editing) {
+ const commentLocation = {
+ changeNum: this.changeNum,
+ patchNum: this.patchNum,
+ path: changeComment.path,
+ line: changeComment.line,
+ };
+ return this.$.storage.setDraftComment(commentLocation,
+ changeComment.message);
+ }
+ }
+ }
+
+ _handleCommentSavedOrDiscarded(e) {
+ this.dispatchEvent(new CustomEvent('thread-changed',
+ {detail: {rootId: this.rootId, path: this.path},
+ bubbles: false}));
+ }
+
+ _handleCommentUpdate(e) {
+ const comment = e.detail.comment;
+ const index = this._indexOf(comment, this.comments);
+ if (index === -1) {
+ // This should never happen: comment belongs to another thread.
+ console.warn('Comment update for another comment thread.');
+ return;
+ }
+ this.set(['comments', index], comment);
+ // Because of the way we pass these comment objects around by-ref, in
+ // combination with the fact that Polymer does dirty checking in
+ // observers, the this.set() call above will not cause a thread update in
+ // some situations.
+ this.updateThreadProperties();
+ }
+
+ _indexOf(comment, arr) {
+ for (let i = 0; i < arr.length; i++) {
+ const c = arr[i];
+ if ((c.__draftID != null && c.__draftID == comment.__draftID) ||
+ (c.id != null && c.id == comment.id)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ _computeHostClass(unresolved) {
+ if (this.isRobotComment) {
+ return 'robotComment';
+ }
+ return unresolved ? 'unresolved' : '';
+ }
+
+ /**
+ * Load the project config when a project name has been provided.
+ *
+ * @param {string} name The project name.
+ */
+ _projectNameChanged(name) {
+ if (!name) { return; }
+ this.$.restAPI.getProjectConfig(name).then(config => {
+ this._projectConfig = config;
+ });
+ }
+}
+
+customElements.define(GrCommentThread.is, GrCommentThread);
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
new file mode 100644
index 0000000..1d991cb
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
@@ -0,0 +1,97 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ font-family: var(--font-family);
+ font-size: var(--font-size-normal);
+ font-weight: var(--font-weight-normal);
+ line-height: var(--line-height-normal);
+ }
+ gr-button {
+ margin-left: var(--spacing-m);
+ }
+ #actions {
+ margin-left: auto;
+ padding: var(--spacing-m);
+ }
+ #container {
+ background-color: var(--comment-background-color);
+ color: var(--comment-text-color);
+ display: block;
+ margin: 0 var(--spacing-s) var(--spacing-s);
+ white-space: normal;
+ box-shadow: var(--elevation-level-2);
+ border-radius: var(--border-radius);
+ /** This is required for firefox to continue the inheritance */
+ -webkit-user-select: inherit;
+ -moz-user-select: inherit;
+ -ms-user-select: inherit;
+ user-select: inherit;
+ }
+ #container.unresolved {
+ background-color: var(--unresolved-comment-background-color);
+ }
+ #container.robotComment {
+ background-color: var(--robot-comment-background-color);
+ }
+ #commentInfoContainer {
+ border-top: 1px dotted var(--border-color);
+ display: flex;
+ }
+ #unresolvedLabel {
+ font-family: var(--font-family);
+ margin: auto 0;
+ padding: var(--spacing-m);
+ }
+ .pathInfo {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ padding: 0 var(--spacing-s) var(--spacing-s);
+ }
+ .descriptionText {
+ margin-left: var(--spacing-m);
+ font-style: italic;
+ }
+ </style>
+ <template is="dom-if" if="[[showFilePath]]">
+ <div class="pathInfo">
+ <a href\$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]">[[_computeDisplayPath(path)]]</a>
+ <span class="descriptionText">Patchset [[patchNum]]</span>
+ </div>
+ </template>
+ <div id="container" class\$="[[_computeHostClass(unresolved, isRobotComment)]]">
+ <template id="commentList" is="dom-repeat" items="[[_orderedComments]]" as="comment">
+ <gr-comment comment="{{comment}}" comments="{{comments}}" robot-button-disabled="[[_shouldDisableAction(_showActions, _lastComment)]]" change-num="[[changeNum]]" patch-num="[[patchNum]]" draft="[[_isDraft(comment)]]" show-actions="[[_showActions]]" comment-side="[[comment.__commentSide]]" side="[[comment.side]]" project-config="[[_projectConfig]]" on-create-fix-comment="_handleCommentFix" on-comment-discard="_handleCommentDiscard" on-comment-save="_handleCommentSavedOrDiscarded"></gr-comment>
+ </template>
+ <div id="commentInfoContainer" hidden\$="[[_hideActions(_showActions, _lastComment)]]">
+ <span id="unresolvedLabel" hidden\$="[[!unresolved]]">Unresolved</span>
+ <div id="actions">
+ <gr-button id="replyBtn" link="" class="action reply" on-click="_handleCommentReply">Reply</gr-button>
+ <gr-button id="quoteBtn" link="" class="action quote" on-click="_handleCommentQuote">Quote</gr-button>
+ <gr-button id="ackBtn" link="" class="action ack" on-click="_handleCommentAck">Ack</gr-button>
+ <gr-button id="doneBtn" link="" class="action done" on-click="_handleCommentDone">Done</gr-button>
+ </div>
+ </div>
+ </div>
+ <gr-reporting id="reporting"></gr-reporting>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ <gr-storage id="storage"></gr-storage>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
index a17a174..895866b 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
@@ -19,17 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-comment-thread</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="gr-comment-thread.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -43,197 +36,13 @@
</template>
</test-fixture>
-<script>
- suite('gr-comment-thread tests', async () => {
- await readyToTest();
-
- suite('basic test', () => {
- let element;
- let sandbox;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(false); },
- });
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('comments are sorted correctly', () => {
- const comments = [
- {
- message: 'i like you, too',
- in_reply_to: 'sallys_confession',
- __date: new Date('2015-12-25'),
- }, {
- id: 'sallys_confession',
- message: 'i like you, jack',
- updated: '2015-12-24 15:00:20.396000000',
- }, {
- id: 'sally_to_dr_finklestein',
- message: 'i’m running away',
- updated: '2015-10-31 09:00:20.396000000',
- }, {
- id: 'sallys_defiance',
- in_reply_to: 'sally_to_dr_finklestein',
- message: 'i will poison you so i can get away',
- updated: '2015-10-31 15:00:20.396000000',
- }, {
- id: 'dr_finklesteins_response',
- in_reply_to: 'sally_to_dr_finklestein',
- message: 'no i will pull a thread and your arm will fall off',
- updated: '2015-10-31 11:00:20.396000000',
- }, {
- id: 'sallys_mission',
- message: 'i have to find santa',
- updated: '2015-12-24 15:00:20.396000000',
- },
- ];
- const results = element._sortedComments(comments);
- assert.deepEqual(results, [
- {
- id: 'sally_to_dr_finklestein',
- message: 'i’m running away',
- updated: '2015-10-31 09:00:20.396000000',
- }, {
- id: 'dr_finklesteins_response',
- in_reply_to: 'sally_to_dr_finklestein',
- message: 'no i will pull a thread and your arm will fall off',
- updated: '2015-10-31 11:00:20.396000000',
- }, {
- id: 'sallys_defiance',
- in_reply_to: 'sally_to_dr_finklestein',
- message: 'i will poison you so i can get away',
- updated: '2015-10-31 15:00:20.396000000',
- }, {
- id: 'sallys_confession',
- message: 'i like you, jack',
- updated: '2015-12-24 15:00:20.396000000',
- }, {
- id: 'sallys_mission',
- message: 'i have to find santa',
- updated: '2015-12-24 15:00:20.396000000',
- }, {
- message: 'i like you, too',
- in_reply_to: 'sallys_confession',
- __date: new Date('2015-12-25'),
- },
- ]);
- });
-
- test('addOrEditDraft w/ edit draft', () => {
- element.comments = [{
- id: 'jacks_reply',
- message: 'i like you, too',
- in_reply_to: 'sallys_confession',
- updated: '2015-12-25 15:00:20.396000000',
- __draft: true,
- }];
- const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
- () => { return {}; });
- const addDraftStub = sandbox.stub(element, 'addDraft');
-
- element.addOrEditDraft(123);
-
- assert.isTrue(commentElStub.called);
- assert.isFalse(addDraftStub.called);
- });
-
- test('addOrEditDraft w/o edit draft', () => {
- element.comments = [];
- const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
- () => { return {}; });
- const addDraftStub = sandbox.stub(element, 'addDraft');
-
- element.addOrEditDraft(123);
-
- assert.isFalse(commentElStub.called);
- assert.isTrue(addDraftStub.called);
- });
-
- test('_shouldDisableAction', () => {
- let showActions = true;
- const lastComment = {};
- assert.equal(
- element._shouldDisableAction(showActions, lastComment), false);
- showActions = false;
- assert.equal(
- element._shouldDisableAction(showActions, lastComment), true);
- showActions = true;
- lastComment.__draft = true;
- assert.equal(
- element._shouldDisableAction(showActions, lastComment), true);
- const robotComment = {};
- robotComment.robot_id = true;
- assert.equal(
- element._shouldDisableAction(showActions, robotComment), false);
- });
-
- test('_hideActions', () => {
- let showActions = true;
- const lastComment = {};
- assert.equal(element._hideActions(showActions, lastComment), false);
- showActions = false;
- assert.equal(element._hideActions(showActions, lastComment), true);
- showActions = true;
- lastComment.__draft = true;
- assert.equal(element._hideActions(showActions, lastComment), true);
- const robotComment = {};
- robotComment.robot_id = true;
- assert.equal(element._hideActions(showActions, robotComment), true);
- });
-
- test('setting project name loads the project config', done => {
- const projectName = 'foo/bar/baz';
- const getProjectStub = sandbox.stub(element.$.restAPI, 'getProjectConfig')
- .returns(Promise.resolve({}));
- element.projectName = projectName;
- flush(() => {
- assert.isTrue(getProjectStub.calledWithExactly(projectName));
- done();
- });
- });
-
- test('optionally show file path', () => {
- // Path info doesn't exist when showFilePath is false. Because it's in a
- // dom-if it is not yet in the dom.
- assert.isNotOk(element.shadowRoot
- .querySelector('.pathInfo'));
-
- sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
- element.changeNum = 123;
- element.projectName = 'test project';
- element.path = 'path/to/file';
- element.patchNum = 3;
- element.lineNum = 5;
- element.showFilePath = true;
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('.pathInfo'));
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('.pathInfo')).display,
- 'none');
- assert.isTrue(Gerrit.Nav.getUrlForDiffById.lastCall.calledWithExactly(
- element.changeNum, element.projectName, element.path,
- element.patchNum, null, element.lineNum));
- });
-
- test('_computeDisplayPath', () => {
- const path = 'path/to/file';
- assert.equal(element._computeDisplayPath(path), 'path/to/file');
-
- element.lineNum = 5;
- assert.equal(element._computeDisplayPath(path), 'path/to/file#5');
- });
- });
- });
-
- suite('comment action tests', () => {
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-comment-thread.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-comment-thread tests', () => {
+ suite('basic test', () => {
let element;
let sandbox;
@@ -241,535 +50,721 @@
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(false); },
- saveDiffDraft() {
- return Promise.resolve({
- ok: true,
- text() {
- return Promise.resolve(')]}\'\n' +
- JSON.stringify({
- id: '7afa4931_de3d65bd',
- path: '/path/to/file.txt',
- line: 5,
- in_reply_to: 'baf0414d_60047215',
- updated: '2015-12-21 02:01:10.850000000',
- message: 'Done',
- }));
- },
- });
- },
- deleteDiffDraft() { return Promise.resolve({ok: true}); },
});
- element = fixture('withComment');
- element.comments = [{
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
- },
- id: 'baf0414d_60047215',
- line: 5,
- message: 'is this a crossover episode!?',
- updated: '2015-12-08 19:48:33.843000000',
- path: '/path/to/file.txt',
- }];
- flushAsynchronousOperations();
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
- test('reply', () => {
- const commentEl = element.shadowRoot
- .querySelector('gr-comment');
- const reportStub = sandbox.stub(element.$.reporting,
- 'recordDraftInteraction');
- assert.ok(commentEl);
-
- const replyBtn = element.$.replyBtn;
- MockInteractions.tap(replyBtn);
- flushAsynchronousOperations();
-
- const drafts = element._orderedComments.filter(c => c.__draft == true);
- assert.equal(drafts.length, 1);
- assert.notOk(drafts[0].message, 'message should be empty');
- assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
- assert.isTrue(reportStub.calledOnce);
- });
-
- test('quote reply', () => {
- const commentEl = element.shadowRoot
- .querySelector('gr-comment');
- const reportStub = sandbox.stub(element.$.reporting,
- 'recordDraftInteraction');
- assert.ok(commentEl);
-
- const quoteBtn = element.$.quoteBtn;
- MockInteractions.tap(quoteBtn);
- flushAsynchronousOperations();
-
- const drafts = element._orderedComments.filter(c => c.__draft == true);
- assert.equal(drafts.length, 1);
- assert.equal(drafts[0].message, '> is this a crossover episode!?\n\n');
- assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
- assert.isTrue(reportStub.calledOnce);
- });
-
- test('quote reply multiline', () => {
- const reportStub = sandbox.stub(element.$.reporting,
- 'recordDraftInteraction');
- element.comments = [{
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
+ test('comments are sorted correctly', () => {
+ const comments = [
+ {
+ message: 'i like you, too',
+ in_reply_to: 'sallys_confession',
+ __date: new Date('2015-12-25'),
+ }, {
+ id: 'sallys_confession',
+ message: 'i like you, jack',
+ updated: '2015-12-24 15:00:20.396000000',
+ }, {
+ id: 'sally_to_dr_finklestein',
+ message: 'i’m running away',
+ updated: '2015-10-31 09:00:20.396000000',
+ }, {
+ id: 'sallys_defiance',
+ in_reply_to: 'sally_to_dr_finklestein',
+ message: 'i will poison you so i can get away',
+ updated: '2015-10-31 15:00:20.396000000',
+ }, {
+ id: 'dr_finklesteins_response',
+ in_reply_to: 'sally_to_dr_finklestein',
+ message: 'no i will pull a thread and your arm will fall off',
+ updated: '2015-10-31 11:00:20.396000000',
+ }, {
+ id: 'sallys_mission',
+ message: 'i have to find santa',
+ updated: '2015-12-24 15:00:20.396000000',
},
- id: 'baf0414d_60047215',
- line: 5,
- message: 'is this a crossover episode!?\nIt might be!',
- updated: '2015-12-08 19:48:33.843000000',
- }];
- flushAsynchronousOperations();
-
- const commentEl = element.shadowRoot
- .querySelector('gr-comment');
- assert.ok(commentEl);
-
- const quoteBtn = element.$.quoteBtn;
- MockInteractions.tap(quoteBtn);
- flushAsynchronousOperations();
-
- const drafts = element._orderedComments.filter(c => c.__draft == true);
- assert.equal(drafts.length, 1);
- assert.equal(drafts[0].message,
- '> is this a crossover episode!?\n> It might be!\n\n');
- assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
- assert.isTrue(reportStub.calledOnce);
- });
-
- test('ack', done => {
- const reportStub = sandbox.stub(element.$.reporting,
- 'recordDraftInteraction');
- element.changeNum = '42';
- element.patchNum = '1';
-
- const commentEl = element.shadowRoot
- .querySelector('gr-comment');
- assert.ok(commentEl);
-
- const ackBtn = element.$.ackBtn;
- MockInteractions.tap(ackBtn);
- flush(() => {
- const drafts = element.comments.filter(c => c.__draft == true);
- assert.equal(drafts.length, 1);
- assert.equal(drafts[0].message, 'Ack');
- assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
- assert.equal(drafts[0].unresolved, false);
- assert.isTrue(reportStub.calledOnce);
- done();
- });
- });
-
- test('done', done => {
- const reportStub = sandbox.stub(element.$.reporting,
- 'recordDraftInteraction');
- element.changeNum = '42';
- element.patchNum = '1';
- const commentEl = element.shadowRoot
- .querySelector('gr-comment');
- assert.ok(commentEl);
-
- const doneBtn = element.$.doneBtn;
- MockInteractions.tap(doneBtn);
- flush(() => {
- const drafts = element.comments.filter(c => c.__draft == true);
- assert.equal(drafts.length, 1);
- assert.equal(drafts[0].message, 'Done');
- assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
- assert.isFalse(drafts[0].unresolved);
- assert.isTrue(reportStub.calledOnce);
- done();
- });
- });
-
- test('save', done => {
- element.changeNum = '42';
- element.patchNum = '1';
- element.path = '/path/to/file.txt';
- const commentEl = element.shadowRoot
- .querySelector('gr-comment');
- assert.ok(commentEl);
-
- const saveOrDiscardStub = sandbox.stub();
- element.addEventListener('thread-changed', saveOrDiscardStub);
- element.shadowRoot
- .querySelector('gr-comment')._fireSave();
-
- flush(() => {
- assert.isTrue(saveOrDiscardStub.called);
- assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
- 'baf0414d_60047215');
- assert.equal(element.rootId, 'baf0414d_60047215');
- assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
- '/path/to/file.txt');
- done();
- });
- });
-
- test('please fix', done => {
- element.changeNum = '42';
- element.patchNum = '1';
- const commentEl = element.shadowRoot
- .querySelector('gr-comment');
- assert.ok(commentEl);
- commentEl.addEventListener('create-fix-comment', () => {
- const drafts = element._orderedComments.filter(c => c.__draft == true);
- assert.equal(drafts.length, 1);
- assert.equal(
- drafts[0].message, '> is this a crossover episode!?\n\nPlease fix.');
- assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
- assert.isTrue(drafts[0].unresolved);
- done();
- });
- commentEl.fire('create-fix-comment', {comment: commentEl.comment},
- {bubbles: false});
- });
-
- test('discard', done => {
- element.changeNum = '42';
- element.patchNum = '1';
- element.path = '/path/to/file.txt';
- element.push('comments', element._newReply(
- element.comments[0].id,
- element.comments[0].line,
- element.comments[0].path,
- 'it’s pronouced jiff, not giff'));
- flushAsynchronousOperations();
-
- const saveOrDiscardStub = sandbox.stub();
- element.addEventListener('thread-changed', saveOrDiscardStub);
- const draftEl =
- Polymer.dom(element.root).querySelectorAll('gr-comment')[1];
- assert.ok(draftEl);
- draftEl.addEventListener('comment-discard', () => {
- const drafts = element.comments.filter(c => c.__draft == true);
- assert.equal(drafts.length, 0);
- assert.isTrue(saveOrDiscardStub.called);
- assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
- element.rootId);
- assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
- element.path);
- done();
- });
- draftEl.fire('comment-discard', {comment: draftEl.comment},
- {bubbles: false});
- });
-
- test('discard with a single comment still fires event with previous rootId',
- done => {
- element.changeNum = '42';
- element.patchNum = '1';
- element.path = '/path/to/file.txt';
- element.comments = [];
- element.addOrEditDraft('1');
- flushAsynchronousOperations();
- const rootId = element.rootId;
- assert.isOk(rootId);
-
- const saveOrDiscardStub = sandbox.stub();
- element.addEventListener('thread-changed', saveOrDiscardStub);
- const draftEl =
- Polymer.dom(element.root).querySelectorAll('gr-comment')[0];
- assert.ok(draftEl);
- draftEl.addEventListener('comment-discard', () => {
- assert.equal(element.comments.length, 0);
- assert.isTrue(saveOrDiscardStub.called);
- assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
- rootId);
- assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
- element.path);
- done();
- });
- draftEl.fire('comment-discard', {comment: draftEl.comment},
- {bubbles: false});
- });
-
- test('first editing comment does not add __otherEditing attribute', () => {
- element.comments = [{
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
+ ];
+ const results = element._sortedComments(comments);
+ assert.deepEqual(results, [
+ {
+ id: 'sally_to_dr_finklestein',
+ message: 'i’m running away',
+ updated: '2015-10-31 09:00:20.396000000',
+ }, {
+ id: 'dr_finklesteins_response',
+ in_reply_to: 'sally_to_dr_finklestein',
+ message: 'no i will pull a thread and your arm will fall off',
+ updated: '2015-10-31 11:00:20.396000000',
+ }, {
+ id: 'sallys_defiance',
+ in_reply_to: 'sally_to_dr_finklestein',
+ message: 'i will poison you so i can get away',
+ updated: '2015-10-31 15:00:20.396000000',
+ }, {
+ id: 'sallys_confession',
+ message: 'i like you, jack',
+ updated: '2015-12-24 15:00:20.396000000',
+ }, {
+ id: 'sallys_mission',
+ message: 'i have to find santa',
+ updated: '2015-12-24 15:00:20.396000000',
+ }, {
+ message: 'i like you, too',
+ in_reply_to: 'sallys_confession',
+ __date: new Date('2015-12-25'),
},
- id: 'baf0414d_60047215',
- line: 5,
- message: 'is this a crossover episode!?',
- updated: '2015-12-08 19:48:33.843000000',
+ ]);
+ });
+
+ test('addOrEditDraft w/ edit draft', () => {
+ element.comments = [{
+ id: 'jacks_reply',
+ message: 'i like you, too',
+ in_reply_to: 'sallys_confession',
+ updated: '2015-12-25 15:00:20.396000000',
__draft: true,
}];
+ const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
+ () => { return {}; });
+ const addDraftStub = sandbox.stub(element, 'addDraft');
- const replyBtn = element.$.replyBtn;
- MockInteractions.tap(replyBtn);
- flushAsynchronousOperations();
+ element.addOrEditDraft(123);
- const editing = element._orderedComments.filter(c => c.__editing == true);
- assert.equal(editing.length, 1);
- assert.equal(!!editing[0].__otherEditing, false);
+ assert.isTrue(commentElStub.called);
+ assert.isFalse(addDraftStub.called);
});
- test('When not editing other comments, local storage not set' +
- ' after discard', done => {
- element.changeNum = '42';
- element.patchNum = '1';
- element.comments = [{
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
- },
- id: 'baf0414d_60047215',
- line: 5,
- message: 'is this a crossover episode!?',
- updated: '2015-12-08 19:48:31.843000000',
- },
- {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
- },
- __draftID: '1',
- in_reply_to: 'baf0414d_60047215',
- line: 5,
- message: 'yes',
- updated: '2015-12-08 19:48:32.843000000',
- __draft: true,
- __editing: true,
- },
- {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
- },
- __draftID: '2',
- in_reply_to: 'baf0414d_60047215',
- line: 5,
- message: 'no',
- updated: '2015-12-08 19:48:33.843000000',
- __draft: true,
- }];
- const storageStub = sinon.stub(element.$.storage, 'setDraftComment');
- flushAsynchronousOperations();
+ test('addOrEditDraft w/o edit draft', () => {
+ element.comments = [];
+ const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
+ () => { return {}; });
+ const addDraftStub = sandbox.stub(element, 'addDraft');
- const draftEl =
- Polymer.dom(element.root).querySelectorAll('gr-comment')[1];
- assert.ok(draftEl);
- draftEl.addEventListener('comment-discard', () => {
- assert.isFalse(storageStub.called);
- storageStub.restore();
+ element.addOrEditDraft(123);
+
+ assert.isFalse(commentElStub.called);
+ assert.isTrue(addDraftStub.called);
+ });
+
+ test('_shouldDisableAction', () => {
+ let showActions = true;
+ const lastComment = {};
+ assert.equal(
+ element._shouldDisableAction(showActions, lastComment), false);
+ showActions = false;
+ assert.equal(
+ element._shouldDisableAction(showActions, lastComment), true);
+ showActions = true;
+ lastComment.__draft = true;
+ assert.equal(
+ element._shouldDisableAction(showActions, lastComment), true);
+ const robotComment = {};
+ robotComment.robot_id = true;
+ assert.equal(
+ element._shouldDisableAction(showActions, robotComment), false);
+ });
+
+ test('_hideActions', () => {
+ let showActions = true;
+ const lastComment = {};
+ assert.equal(element._hideActions(showActions, lastComment), false);
+ showActions = false;
+ assert.equal(element._hideActions(showActions, lastComment), true);
+ showActions = true;
+ lastComment.__draft = true;
+ assert.equal(element._hideActions(showActions, lastComment), true);
+ const robotComment = {};
+ robotComment.robot_id = true;
+ assert.equal(element._hideActions(showActions, robotComment), true);
+ });
+
+ test('setting project name loads the project config', done => {
+ const projectName = 'foo/bar/baz';
+ const getProjectStub = sandbox.stub(element.$.restAPI, 'getProjectConfig')
+ .returns(Promise.resolve({}));
+ element.projectName = projectName;
+ flush(() => {
+ assert.isTrue(getProjectStub.calledWithExactly(projectName));
done();
});
- draftEl.fire('comment-discard', {comment: draftEl.comment},
- {bubbles: false});
});
- test('comment-update', () => {
- const commentEl = element.shadowRoot
- .querySelector('gr-comment');
- const updatedComment = {
- id: element.comments[0].id,
- foo: 'bar',
- };
- commentEl.fire('comment-update', {comment: updatedComment});
- assert.strictEqual(element.comments[0], updatedComment);
- });
+ test('optionally show file path', () => {
+ // Path info doesn't exist when showFilePath is false. Because it's in a
+ // dom-if it is not yet in the dom.
+ assert.isNotOk(element.shadowRoot
+ .querySelector('.pathInfo'));
- suite('jack and sally comment data test consolidation', () => {
- setup(() => {
- element.comments = [
- {
- id: 'jacks_reply',
- message: 'i like you, too',
- in_reply_to: 'sallys_confession',
- updated: '2015-12-25 15:00:20.396000000',
- unresolved: false,
- }, {
- id: 'sallys_confession',
- in_reply_to: 'nonexistent_comment',
- message: 'i like you, jack',
- updated: '2015-12-24 15:00:20.396000000',
- }, {
- id: 'sally_to_dr_finklestein',
- in_reply_to: 'nonexistent_comment',
- message: 'i’m running away',
- updated: '2015-10-31 09:00:20.396000000',
- }, {
- id: 'sallys_defiance',
- message: 'i will poison you so i can get away',
- updated: '2015-10-31 15:00:20.396000000',
- }];
- });
-
- test('orphan replies', () => {
- assert.equal(4, element._orderedComments.length);
- });
-
- test('keyboard shortcuts', () => {
- const expandCollapseStub =
- sinon.stub(element, '_expandCollapseComments');
- MockInteractions.pressAndReleaseKeyOn(element, 69, null, 'e');
- assert.isTrue(expandCollapseStub.lastCall.calledWith(false));
-
- MockInteractions.pressAndReleaseKeyOn(element, 69, 'shift', 'e');
- assert.isTrue(expandCollapseStub.lastCall.calledWith(true));
- });
-
- test('comment in_reply_to is either null or most recent comment', () => {
- element._createReplyComment(element.comments[3], 'dummy', true);
- flushAsynchronousOperations();
- assert.equal(element._orderedComments.length, 5);
- assert.equal(element._orderedComments[4].in_reply_to, 'jacks_reply');
- });
-
- test('resolvable comments', () => {
- assert.isFalse(element.unresolved);
- element._createReplyComment(element.comments[3], 'dummy', true, true);
- flushAsynchronousOperations();
- assert.isTrue(element.unresolved);
- });
-
- test('_setInitialExpandedState', () => {
- element.unresolved = true;
- element._setInitialExpandedState();
- for (let i = 0; i < element.comments.length; i++) {
- assert.isFalse(element.comments[i].collapsed);
- }
- element.unresolved = false;
- element._setInitialExpandedState();
- for (let i = 0; i < element.comments.length; i++) {
- assert.isTrue(element.comments[i].collapsed);
- }
- for (let i = 0; i < element.comments.length; i++) {
- element.comments[i].robot_id = 123;
- }
- element._setInitialExpandedState();
- for (let i = 0; i < element.comments.length; i++) {
- assert.isFalse(element.comments[i].collapsed);
- }
- });
- });
-
- test('_computeHostClass', () => {
- assert.equal(element._computeHostClass(true), 'unresolved');
- assert.equal(element._computeHostClass(false), '');
- });
-
- test('addDraft sets unresolved state correctly', () => {
- let unresolved = true;
- element.comments = [];
- element.addDraft(null, null, unresolved);
- assert.equal(element.comments[0].unresolved, true);
-
- unresolved = false; // comment should get added as actually resolved.
- element.comments = [];
- element.addDraft(null, null, unresolved);
- assert.equal(element.comments[0].unresolved, false);
-
- element.comments = [];
- element.addDraft();
- assert.equal(element.comments[0].unresolved, true);
- });
-
- test('_newDraft', () => {
- element.commentSide = 'left';
+ sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
+ element.changeNum = 123;
+ element.projectName = 'test project';
+ element.path = 'path/to/file';
element.patchNum = 3;
- const draft = element._newDraft();
- assert.equal(draft.__commentSide, 'left');
- assert.equal(draft.patchNum, 3);
+ element.lineNum = 5;
+ element.showFilePath = true;
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('.pathInfo'));
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('.pathInfo')).display,
+ 'none');
+ assert.isTrue(Gerrit.Nav.getUrlForDiffById.lastCall.calledWithExactly(
+ element.changeNum, element.projectName, element.path,
+ element.patchNum, null, element.lineNum));
});
- test('new comment gets created', () => {
- element.comments = [];
- element.addOrEditDraft(1);
- assert.equal(element.comments.length, 1);
- // Mock a submitted comment.
- element.comments[0].id = element.comments[0].__draftID;
- element.comments[0].__draft = false;
- element.addOrEditDraft(1);
- assert.equal(element.comments.length, 2);
- });
+ test('_computeDisplayPath', () => {
+ const path = 'path/to/file';
+ assert.equal(element._computeDisplayPath(path), 'path/to/file');
- test('unresolved label', () => {
- element.unresolved = false;
- assert.isTrue(element.$.unresolvedLabel.hasAttribute('hidden'));
- element.unresolved = true;
- assert.isFalse(element.$.unresolvedLabel.hasAttribute('hidden'));
- });
-
- test('draft comments are at the end of orderedComments', () => {
- element.comments = [{
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
- },
- id: 2,
- line: 5,
- message: 'Earlier draft',
- updated: '2015-12-08 19:48:33.843000000',
- __draft: true,
- },
- {
- author: {
- name: 'Mr. Peanutbutter2',
- email: 'tenn1sballchaser@aol.com',
- },
- id: 1,
- line: 5,
- message: 'This comment was left last but is not a draft',
- updated: '2015-12-10 19:48:33.843000000',
- },
- {
- author: {
- name: 'Mr. Peanutbutter2',
- email: 'tenn1sballchaser@aol.com',
- },
- id: 3,
- line: 5,
- message: 'Later draft',
- updated: '2015-12-09 19:48:33.843000000',
- __draft: true,
- }];
- assert.equal(element._orderedComments[0].id, '1');
- assert.equal(element._orderedComments[1].id, '2');
- assert.equal(element._orderedComments[2].id, '3');
- });
-
- test('reflects lineNum and commentSide to attributes', () => {
- element.lineNum = 7;
- element.commentSide = 'left';
-
- assert.equal(element.getAttribute('line-num'), '7');
- assert.equal(element.getAttribute('comment-side'), 'left');
- });
-
- test('reflects range to JSON serialized attribute if set', () => {
- element.range = {
- start_line: 4,
- end_line: 5,
- start_character: 6,
- end_character: 7,
- };
-
- assert.deepEqual(
- JSON.parse(element.getAttribute('range')),
- {start_line: 4, end_line: 5, start_character: 6, end_character: 7});
- });
-
- test('removes range attribute if range is unset', () => {
- element.range = {
- start_line: 4,
- end_line: 5,
- start_character: 6,
- end_character: 7,
- };
- element.range = undefined;
-
- assert.notOk(element.hasAttribute('range'));
+ element.lineNum = 5;
+ assert.equal(element._computeDisplayPath(path), 'path/to/file#5');
});
});
-</script>
\ No newline at end of file
+});
+
+suite('comment action tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(false); },
+ saveDiffDraft() {
+ return Promise.resolve({
+ ok: true,
+ text() {
+ return Promise.resolve(')]}\'\n' +
+ JSON.stringify({
+ id: '7afa4931_de3d65bd',
+ path: '/path/to/file.txt',
+ line: 5,
+ in_reply_to: 'baf0414d_60047215',
+ updated: '2015-12-21 02:01:10.850000000',
+ message: 'Done',
+ }));
+ },
+ });
+ },
+ deleteDiffDraft() { return Promise.resolve({ok: true}); },
+ });
+ element = fixture('withComment');
+ element.comments = [{
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 'baf0414d_60047215',
+ line: 5,
+ message: 'is this a crossover episode!?',
+ updated: '2015-12-08 19:48:33.843000000',
+ path: '/path/to/file.txt',
+ }];
+ flushAsynchronousOperations();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('reply', () => {
+ const commentEl = element.shadowRoot
+ .querySelector('gr-comment');
+ const reportStub = sandbox.stub(element.$.reporting,
+ 'recordDraftInteraction');
+ assert.ok(commentEl);
+
+ const replyBtn = element.$.replyBtn;
+ MockInteractions.tap(replyBtn);
+ flushAsynchronousOperations();
+
+ const drafts = element._orderedComments.filter(c => c.__draft == true);
+ assert.equal(drafts.length, 1);
+ assert.notOk(drafts[0].message, 'message should be empty');
+ assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+ assert.isTrue(reportStub.calledOnce);
+ });
+
+ test('quote reply', () => {
+ const commentEl = element.shadowRoot
+ .querySelector('gr-comment');
+ const reportStub = sandbox.stub(element.$.reporting,
+ 'recordDraftInteraction');
+ assert.ok(commentEl);
+
+ const quoteBtn = element.$.quoteBtn;
+ MockInteractions.tap(quoteBtn);
+ flushAsynchronousOperations();
+
+ const drafts = element._orderedComments.filter(c => c.__draft == true);
+ assert.equal(drafts.length, 1);
+ assert.equal(drafts[0].message, '> is this a crossover episode!?\n\n');
+ assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+ assert.isTrue(reportStub.calledOnce);
+ });
+
+ test('quote reply multiline', () => {
+ const reportStub = sandbox.stub(element.$.reporting,
+ 'recordDraftInteraction');
+ element.comments = [{
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 'baf0414d_60047215',
+ line: 5,
+ message: 'is this a crossover episode!?\nIt might be!',
+ updated: '2015-12-08 19:48:33.843000000',
+ }];
+ flushAsynchronousOperations();
+
+ const commentEl = element.shadowRoot
+ .querySelector('gr-comment');
+ assert.ok(commentEl);
+
+ const quoteBtn = element.$.quoteBtn;
+ MockInteractions.tap(quoteBtn);
+ flushAsynchronousOperations();
+
+ const drafts = element._orderedComments.filter(c => c.__draft == true);
+ assert.equal(drafts.length, 1);
+ assert.equal(drafts[0].message,
+ '> is this a crossover episode!?\n> It might be!\n\n');
+ assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+ assert.isTrue(reportStub.calledOnce);
+ });
+
+ test('ack', done => {
+ const reportStub = sandbox.stub(element.$.reporting,
+ 'recordDraftInteraction');
+ element.changeNum = '42';
+ element.patchNum = '1';
+
+ const commentEl = element.shadowRoot
+ .querySelector('gr-comment');
+ assert.ok(commentEl);
+
+ const ackBtn = element.$.ackBtn;
+ MockInteractions.tap(ackBtn);
+ flush(() => {
+ const drafts = element.comments.filter(c => c.__draft == true);
+ assert.equal(drafts.length, 1);
+ assert.equal(drafts[0].message, 'Ack');
+ assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+ assert.equal(drafts[0].unresolved, false);
+ assert.isTrue(reportStub.calledOnce);
+ done();
+ });
+ });
+
+ test('done', done => {
+ const reportStub = sandbox.stub(element.$.reporting,
+ 'recordDraftInteraction');
+ element.changeNum = '42';
+ element.patchNum = '1';
+ const commentEl = element.shadowRoot
+ .querySelector('gr-comment');
+ assert.ok(commentEl);
+
+ const doneBtn = element.$.doneBtn;
+ MockInteractions.tap(doneBtn);
+ flush(() => {
+ const drafts = element.comments.filter(c => c.__draft == true);
+ assert.equal(drafts.length, 1);
+ assert.equal(drafts[0].message, 'Done');
+ assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+ assert.isFalse(drafts[0].unresolved);
+ assert.isTrue(reportStub.calledOnce);
+ done();
+ });
+ });
+
+ test('save', done => {
+ element.changeNum = '42';
+ element.patchNum = '1';
+ element.path = '/path/to/file.txt';
+ const commentEl = element.shadowRoot
+ .querySelector('gr-comment');
+ assert.ok(commentEl);
+
+ const saveOrDiscardStub = sandbox.stub();
+ element.addEventListener('thread-changed', saveOrDiscardStub);
+ element.shadowRoot
+ .querySelector('gr-comment')._fireSave();
+
+ flush(() => {
+ assert.isTrue(saveOrDiscardStub.called);
+ assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
+ 'baf0414d_60047215');
+ assert.equal(element.rootId, 'baf0414d_60047215');
+ assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
+ '/path/to/file.txt');
+ done();
+ });
+ });
+
+ test('please fix', done => {
+ element.changeNum = '42';
+ element.patchNum = '1';
+ const commentEl = element.shadowRoot
+ .querySelector('gr-comment');
+ assert.ok(commentEl);
+ commentEl.addEventListener('create-fix-comment', () => {
+ const drafts = element._orderedComments.filter(c => c.__draft == true);
+ assert.equal(drafts.length, 1);
+ assert.equal(
+ drafts[0].message, '> is this a crossover episode!?\n\nPlease fix.');
+ assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+ assert.isTrue(drafts[0].unresolved);
+ done();
+ });
+ commentEl.fire('create-fix-comment', {comment: commentEl.comment},
+ {bubbles: false});
+ });
+
+ test('discard', done => {
+ element.changeNum = '42';
+ element.patchNum = '1';
+ element.path = '/path/to/file.txt';
+ element.push('comments', element._newReply(
+ element.comments[0].id,
+ element.comments[0].line,
+ element.comments[0].path,
+ 'it’s pronouced jiff, not giff'));
+ flushAsynchronousOperations();
+
+ const saveOrDiscardStub = sandbox.stub();
+ element.addEventListener('thread-changed', saveOrDiscardStub);
+ const draftEl =
+ dom(element.root).querySelectorAll('gr-comment')[1];
+ assert.ok(draftEl);
+ draftEl.addEventListener('comment-discard', () => {
+ const drafts = element.comments.filter(c => c.__draft == true);
+ assert.equal(drafts.length, 0);
+ assert.isTrue(saveOrDiscardStub.called);
+ assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
+ element.rootId);
+ assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
+ element.path);
+ done();
+ });
+ draftEl.fire('comment-discard', {comment: draftEl.comment},
+ {bubbles: false});
+ });
+
+ test('discard with a single comment still fires event with previous rootId',
+ done => {
+ element.changeNum = '42';
+ element.patchNum = '1';
+ element.path = '/path/to/file.txt';
+ element.comments = [];
+ element.addOrEditDraft('1');
+ flushAsynchronousOperations();
+ const rootId = element.rootId;
+ assert.isOk(rootId);
+
+ const saveOrDiscardStub = sandbox.stub();
+ element.addEventListener('thread-changed', saveOrDiscardStub);
+ const draftEl =
+ dom(element.root).querySelectorAll('gr-comment')[0];
+ assert.ok(draftEl);
+ draftEl.addEventListener('comment-discard', () => {
+ assert.equal(element.comments.length, 0);
+ assert.isTrue(saveOrDiscardStub.called);
+ assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
+ rootId);
+ assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
+ element.path);
+ done();
+ });
+ draftEl.fire('comment-discard', {comment: draftEl.comment},
+ {bubbles: false});
+ });
+
+ test('first editing comment does not add __otherEditing attribute', () => {
+ element.comments = [{
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 'baf0414d_60047215',
+ line: 5,
+ message: 'is this a crossover episode!?',
+ updated: '2015-12-08 19:48:33.843000000',
+ __draft: true,
+ }];
+
+ const replyBtn = element.$.replyBtn;
+ MockInteractions.tap(replyBtn);
+ flushAsynchronousOperations();
+
+ const editing = element._orderedComments.filter(c => c.__editing == true);
+ assert.equal(editing.length, 1);
+ assert.equal(!!editing[0].__otherEditing, false);
+ });
+
+ test('When not editing other comments, local storage not set' +
+ ' after discard', done => {
+ element.changeNum = '42';
+ element.patchNum = '1';
+ element.comments = [{
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 'baf0414d_60047215',
+ line: 5,
+ message: 'is this a crossover episode!?',
+ updated: '2015-12-08 19:48:31.843000000',
+ },
+ {
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ __draftID: '1',
+ in_reply_to: 'baf0414d_60047215',
+ line: 5,
+ message: 'yes',
+ updated: '2015-12-08 19:48:32.843000000',
+ __draft: true,
+ __editing: true,
+ },
+ {
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ __draftID: '2',
+ in_reply_to: 'baf0414d_60047215',
+ line: 5,
+ message: 'no',
+ updated: '2015-12-08 19:48:33.843000000',
+ __draft: true,
+ }];
+ const storageStub = sinon.stub(element.$.storage, 'setDraftComment');
+ flushAsynchronousOperations();
+
+ const draftEl =
+ dom(element.root).querySelectorAll('gr-comment')[1];
+ assert.ok(draftEl);
+ draftEl.addEventListener('comment-discard', () => {
+ assert.isFalse(storageStub.called);
+ storageStub.restore();
+ done();
+ });
+ draftEl.fire('comment-discard', {comment: draftEl.comment},
+ {bubbles: false});
+ });
+
+ test('comment-update', () => {
+ const commentEl = element.shadowRoot
+ .querySelector('gr-comment');
+ const updatedComment = {
+ id: element.comments[0].id,
+ foo: 'bar',
+ };
+ commentEl.fire('comment-update', {comment: updatedComment});
+ assert.strictEqual(element.comments[0], updatedComment);
+ });
+
+ suite('jack and sally comment data test consolidation', () => {
+ setup(() => {
+ element.comments = [
+ {
+ id: 'jacks_reply',
+ message: 'i like you, too',
+ in_reply_to: 'sallys_confession',
+ updated: '2015-12-25 15:00:20.396000000',
+ unresolved: false,
+ }, {
+ id: 'sallys_confession',
+ in_reply_to: 'nonexistent_comment',
+ message: 'i like you, jack',
+ updated: '2015-12-24 15:00:20.396000000',
+ }, {
+ id: 'sally_to_dr_finklestein',
+ in_reply_to: 'nonexistent_comment',
+ message: 'i’m running away',
+ updated: '2015-10-31 09:00:20.396000000',
+ }, {
+ id: 'sallys_defiance',
+ message: 'i will poison you so i can get away',
+ updated: '2015-10-31 15:00:20.396000000',
+ }];
+ });
+
+ test('orphan replies', () => {
+ assert.equal(4, element._orderedComments.length);
+ });
+
+ test('keyboard shortcuts', () => {
+ const expandCollapseStub =
+ sinon.stub(element, '_expandCollapseComments');
+ MockInteractions.pressAndReleaseKeyOn(element, 69, null, 'e');
+ assert.isTrue(expandCollapseStub.lastCall.calledWith(false));
+
+ MockInteractions.pressAndReleaseKeyOn(element, 69, 'shift', 'e');
+ assert.isTrue(expandCollapseStub.lastCall.calledWith(true));
+ });
+
+ test('comment in_reply_to is either null or most recent comment', () => {
+ element._createReplyComment(element.comments[3], 'dummy', true);
+ flushAsynchronousOperations();
+ assert.equal(element._orderedComments.length, 5);
+ assert.equal(element._orderedComments[4].in_reply_to, 'jacks_reply');
+ });
+
+ test('resolvable comments', () => {
+ assert.isFalse(element.unresolved);
+ element._createReplyComment(element.comments[3], 'dummy', true, true);
+ flushAsynchronousOperations();
+ assert.isTrue(element.unresolved);
+ });
+
+ test('_setInitialExpandedState', () => {
+ element.unresolved = true;
+ element._setInitialExpandedState();
+ for (let i = 0; i < element.comments.length; i++) {
+ assert.isFalse(element.comments[i].collapsed);
+ }
+ element.unresolved = false;
+ element._setInitialExpandedState();
+ for (let i = 0; i < element.comments.length; i++) {
+ assert.isTrue(element.comments[i].collapsed);
+ }
+ for (let i = 0; i < element.comments.length; i++) {
+ element.comments[i].robot_id = 123;
+ }
+ element._setInitialExpandedState();
+ for (let i = 0; i < element.comments.length; i++) {
+ assert.isFalse(element.comments[i].collapsed);
+ }
+ });
+ });
+
+ test('_computeHostClass', () => {
+ assert.equal(element._computeHostClass(true), 'unresolved');
+ assert.equal(element._computeHostClass(false), '');
+ });
+
+ test('addDraft sets unresolved state correctly', () => {
+ let unresolved = true;
+ element.comments = [];
+ element.addDraft(null, null, unresolved);
+ assert.equal(element.comments[0].unresolved, true);
+
+ unresolved = false; // comment should get added as actually resolved.
+ element.comments = [];
+ element.addDraft(null, null, unresolved);
+ assert.equal(element.comments[0].unresolved, false);
+
+ element.comments = [];
+ element.addDraft();
+ assert.equal(element.comments[0].unresolved, true);
+ });
+
+ test('_newDraft', () => {
+ element.commentSide = 'left';
+ element.patchNum = 3;
+ const draft = element._newDraft();
+ assert.equal(draft.__commentSide, 'left');
+ assert.equal(draft.patchNum, 3);
+ });
+
+ test('new comment gets created', () => {
+ element.comments = [];
+ element.addOrEditDraft(1);
+ assert.equal(element.comments.length, 1);
+ // Mock a submitted comment.
+ element.comments[0].id = element.comments[0].__draftID;
+ element.comments[0].__draft = false;
+ element.addOrEditDraft(1);
+ assert.equal(element.comments.length, 2);
+ });
+
+ test('unresolved label', () => {
+ element.unresolved = false;
+ assert.isTrue(element.$.unresolvedLabel.hasAttribute('hidden'));
+ element.unresolved = true;
+ assert.isFalse(element.$.unresolvedLabel.hasAttribute('hidden'));
+ });
+
+ test('draft comments are at the end of orderedComments', () => {
+ element.comments = [{
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 2,
+ line: 5,
+ message: 'Earlier draft',
+ updated: '2015-12-08 19:48:33.843000000',
+ __draft: true,
+ },
+ {
+ author: {
+ name: 'Mr. Peanutbutter2',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 1,
+ line: 5,
+ message: 'This comment was left last but is not a draft',
+ updated: '2015-12-10 19:48:33.843000000',
+ },
+ {
+ author: {
+ name: 'Mr. Peanutbutter2',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 3,
+ line: 5,
+ message: 'Later draft',
+ updated: '2015-12-09 19:48:33.843000000',
+ __draft: true,
+ }];
+ assert.equal(element._orderedComments[0].id, '1');
+ assert.equal(element._orderedComments[1].id, '2');
+ assert.equal(element._orderedComments[2].id, '3');
+ });
+
+ test('reflects lineNum and commentSide to attributes', () => {
+ element.lineNum = 7;
+ element.commentSide = 'left';
+
+ assert.equal(element.getAttribute('line-num'), '7');
+ assert.equal(element.getAttribute('comment-side'), 'left');
+ });
+
+ test('reflects range to JSON serialized attribute if set', () => {
+ element.range = {
+ start_line: 4,
+ end_line: 5,
+ start_character: 6,
+ end_character: 7,
+ };
+
+ assert.deepEqual(
+ JSON.parse(element.getAttribute('range')),
+ {start_line: 4, end_line: 5, start_character: 6, end_character: 7});
+ });
+
+ test('removes range attribute if range is unset', () => {
+ element.range = {
+ start_line: 4,
+ end_line: 5,
+ start_character: 6,
+ end_character: 7,
+ };
+ element.range = undefined;
+
+ assert.notOk(element.hasAttribute('range'));
+ });
+});
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
deleted file mode 100644
index 18ffc0e..0000000
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
+++ /dev/null
@@ -1,433 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-storage/gr-storage.html">
-<link rel="import" href="../../shared/gr-textarea/gr-textarea.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html">
-<script src="../../../scripts/rootElement.js"></script>
-
-<dom-module id="gr-comment">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- font-family: var(--font-family);
- padding: var(--spacing-m);
- }
- :host([disabled]) {
- pointer-events: none;
- }
- :host([disabled]) .actions,
- :host([disabled]) .robotActions,
- :host([disabled]) .date {
- opacity: .5;
- }
- :host([discarding]) {
- display: none;
- }
- .header {
- align-items: center;
- cursor: pointer;
- display: flex;
- margin: calc(0px - var(--spacing-m)) calc(0px - var(--spacing-m)) 0 calc(0px - var(--spacing-m));
- padding: var(--spacing-m);
- }
- .container.collapsed .header {
- margin-bottom: calc(0 - var(--spacing-m));
- }
- .headerMiddle {
- color: var(--deemphasized-text-color);
- flex: 1;
- overflow: hidden;
- }
- .draftLabel,
- .draftTooltip {
- color: var(--deemphasized-text-color);
- display: none;
- }
- .date {
- justify-content: flex-end;
- margin-left: 5px;
- min-width: 4.5em;
- text-align: right;
- white-space: nowrap;
- }
- span.date {
- color: var(--deemphasized-text-color);
- }
- span.date:hover {
- text-decoration: underline;
- }
- .actions, .robotActions {
- display: flex;
- justify-content: flex-end;
- padding-top: 0;
- }
- .action {
- margin-left: var(--spacing-l);
- }
- .rightActions {
- display: flex;
- justify-content: flex-end;
- }
- .rightActions gr-button {
- --gr-button: {
- height: 20px;
- padding: 0 var(--spacing-s);
- }
- }
- .editMessage {
- display: none;
- margin: var(--spacing-m) 0;
- width: 100%;
- }
- .container:not(.draft) .actions .hideOnPublished {
- display: none;
- }
- .draft .reply,
- .draft .quote,
- .draft .ack,
- .draft .done {
- display: none;
- }
- .draft .draftLabel,
- .draft .draftTooltip {
- display: inline;
- }
- .draft:not(.editing) .save,
- .draft:not(.editing) .cancel {
- display: none;
- }
- .editing .message,
- .editing .reply,
- .editing .quote,
- .editing .ack,
- .editing .done,
- .editing .edit,
- .editing .discard,
- .editing .unresolved {
- display: none;
- }
- .editing .editMessage {
- display: block;
- }
- .show-hide {
- margin-left: var(--spacing-s);
- }
- .robotId {
- color: var(--deemphasized-text-color);
- margin-bottom: var(--spacing-m);
- margin-top: -.4em;
- }
- .robotIcon {
- margin-right: var(--spacing-xs);
- /* because of the antenna of the robot, it looks off center even when it
- is centered. artificially adjust margin to account for this. */
- margin-top: -4px;
- }
- .runIdInformation {
- margin: var(--spacing-m) 0;
- }
- .robotRun {
- margin-left: var(--spacing-m);
- }
- .robotRunLink {
- margin-left: var(--spacing-m);
- }
- input.show-hide {
- display: none;
- }
- label.show-hide {
- cursor: pointer;
- display: block;
- }
- label.show-hide iron-icon {
- vertical-align: top;
- }
- #container .collapsedContent {
- display: none;
- }
- #container.collapsed {
- padding-bottom: 3px;
- }
- #container.collapsed .collapsedContent {
- display: block;
- overflow: hidden;
- padding-left: 5px;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- #container.collapsed .actions,
- #container.collapsed gr-formatted-text,
- #container.collapsed gr-textarea,
- #container.collapsed .respectfulReviewTip{
- display: none;
- }
- .resolve,
- .unresolved {
- align-items: center;
- display: flex;
- flex: 1;
- margin: 0;
- }
- .resolve label {
- color: var(--comment-text-color);
- }
- gr-dialog .main {
- display: flex;
- flex-direction: column;
- width: 100%;
- }
- #deleteBtn {
- display: none;
- --gr-button: {
- color: var(--deemphasized-text-color);
- padding: 0;
- }
- }
- #deleteBtn.showDeleteButtons {
- display: block;
- }
-
- /** Disable select for the caret and actions */
- .actions,
- .show-hide {
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- }
-
- .respectfulReviewTip {
- justify-content: space-between;
- display: flex;
- padding: var(--spacing-m);
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius);
- margin-bottom: var(--spacing-m);
- }
- .respectfulReviewTip div {
- display: flex;
- }
- .respectfulReviewTip div iron-icon {
- margin-right: var(--spacing-s);
- }
- .respectfulReviewTip a {
- white-space: nowrap;
- margin-right: var(--spacing-s);
- padding-left: var(--spacing-m);
- text-decoration: none;
- }
- .pointer {
- cursor: pointer;
- }
- </style>
- <div id="container" class="container">
- <div class="header" id="header" on-click="_handleToggleCollapsed">
- <div class="headerLeft">
- <span class="authorName">[[_computeAuthorName(comment)]]</span>
- <span class="draftLabel">DRAFT</span>
- <gr-tooltip-content class="draftTooltip"
- has-tooltip
- title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'A' key."
- max-width="20em"
- show-icon></gr-tooltip-content>
- </div>
- <div class="headerMiddle">
- <span class="collapsedContent">[[comment.message]]</span>
- </div>
- <div hidden$="[[_computeHideRunDetails(comment, collapsed)]]" class="runIdMessage message">
- <div class="runIdInformation">
- <a class="robotRunLink" href$="[[comment.url]]">
- <span class="robotRun link">Run Details</span>
- </a>
- </div>
- </div>
- <gr-button
- id="deleteBtn"
- link
- class$="action delete [[_computeDeleteButtonClass(_isAdmin, draft)]]"
- hidden$="[[isRobotComment]]"
- on-click="_handleCommentDelete">
- <iron-icon id="icon" icon="gr-icons:delete"></iron-icon>
- </gr-button>
- <span class="date" on-click="_handleAnchorClick">
- <gr-date-formatter
- has-tooltip
- date-str="[[comment.updated]]"></gr-date-formatter>
- </span>
- <div class="show-hide">
- <label class="show-hide">
- <input type="checkbox" class="show-hide"
- checked$="[[collapsed]]"
- on-change="_handleToggleCollapsed">
- <iron-icon
- id="icon"
- icon="[[_computeShowHideIcon(collapsed)]]">
- </iron-icon>
- </label>
- </div>
- </div>
- <div class="body">
- <template is="dom-if" if="[[isRobotComment]]">
- <div class="robotId" hidden$="[[collapsed]]">
- [[comment.author.name]]
- </div>
- </template>
- <template is="dom-if" if="[[editing]]">
- <gr-textarea
- id="editTextarea"
- class="editMessage"
- autocomplete="on"
- code
- disabled="{{disabled}}"
- rows="4"
- text="{{_messageText}}"></gr-textarea>
- <template is="dom-if" if="[[_computeVisibilityOfTip(_showRespectfulTip, _respectfulTipDismissed)]]">
- <div class="respectfulReviewTip">
- <div>
- <gr-tooltip-content
- has-tooltip
- title="Tips for respectful code reviews.">
- <iron-icon class="pointer" icon="gr-icons:lightbulb-outline"></iron-icon>
- </gr-tooltip-content>
- [[_respectfulReviewTip]]
- </div>
- <div>
- <a
- tabIndex="-1"
- on-click="_onRespectfulReadMoreClick"
- href="https://testing.googleblog.com/2019/11/code-health-respectful-reviews-useful.html"
- target="_blank">
- Read more
- </a>
- <iron-icon
- class="close pointer"
- on-click="_dismissRespectfulTip"
- icon="gr-icons:close"></iron-icon>
- </div>
- </div>
- </template>
- </template>
- <!--The message class is needed to ensure selectability from
- gr-diff-selection.-->
- <gr-formatted-text class="message"
- content="[[comment.message]]"
- no-trailing-margin="[[!comment.__draft]]"
- config="[[projectConfig.commentlinks]]"></gr-formatted-text>
- <div class="actions humanActions" hidden$="[[!_showHumanActions]]">
- <div class="action resolve hideOnPublished">
- <label>
- <input type="checkbox"
- id="resolvedCheckbox"
- checked="[[resolved]]"
- on-change="_handleToggleResolved">
- Resolved
- </label>
- </div>
- <div class="rightActions">
- <gr-button
- link
- class="action cancel hideOnPublished"
- on-click="_handleCancel">Cancel</gr-button>
- <gr-button
- link
- class="action discard hideOnPublished"
- on-click="_handleDiscard">Discard</gr-button>
- <gr-button
- link
- class="action edit hideOnPublished"
- on-click="_handleEdit">Edit</gr-button>
- <gr-button
- link
- disabled$="[[_computeSaveDisabled(_messageText, comment, resolved)]]"
- class="action save hideOnPublished"
- on-click="_handleSave">Save</gr-button>
- </div>
- </div>
- <div class="robotActions" hidden$="[[!_showRobotActions]]">
- <template is="dom-if" if="[[isRobotComment]]">
- <gr-endpoint-decorator name="robot-comment-controls">
- <gr-endpoint-param name="comment" value="[[comment]]">
- </gr-endpoint-param>
- </gr-endpoint-decorator>
- <gr-button
- link
- secondary
- class="action show-fix"
- hidden$="[[_hasNoFix(comment)]]"
- on-click="_handleShowFix">
- Show Fix
- </gr-button>
- <template is="dom-if" if="[[!_hasHumanReply]]">
- <gr-button
- link
- class="action fix"
- on-click="_handleFix"
- disabled="[[robotButtonDisabled]]">
- Please Fix
- </gr-button>
- </template>
- </template>
- </div>
- </div>
- </div>
- <template is="dom-if" if="[[_enableOverlay]]">
- <gr-overlay id="confirmDeleteOverlay" with-backdrop>
- <gr-confirm-delete-comment-dialog id="confirmDeleteComment"
- on-confirm="_handleConfirmDeleteComment"
- on-cancel="_handleCancelDeleteComment">
- </gr-confirm-delete-comment-dialog>
- </gr-overlay>
- <gr-overlay id="confirmDiscardOverlay" with-backdrop>
- <gr-dialog
- id="confirmDiscardDialog"
- confirm-label="Discard"
- confirm-on-enter
- on-confirm="_handleConfirmDiscard"
- on-cancel="_closeConfirmDiscardOverlay">
- <div class="header" slot="header">
- Discard comment
- </div>
- <div class="main" slot="main">
- Are you sure you want to discard this draft comment?
- </div>
- </gr-dialog>
- </gr-overlay>
- </template>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- <gr-storage id="storage"></gr-storage>
- <gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="gr-comment.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
index 9880e88..252f409 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,797 +14,824 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const STORAGE_DEBOUNCE_INTERVAL = 400;
- const TOAST_DEBOUNCE_INTERVAL = 200;
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
+import '../gr-button/gr-button.js';
+import '../gr-dialog/gr-dialog.js';
+import '../gr-date-formatter/gr-date-formatter.js';
+import '../gr-formatted-text/gr-formatted-text.js';
+import '../gr-icons/gr-icons.js';
+import '../gr-overlay/gr-overlay.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-storage/gr-storage.js';
+import '../gr-textarea/gr-textarea.js';
+import '../gr-tooltip-content/gr-tooltip-content.js';
+import '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js';
+import '../../../scripts/rootElement.js';
+import {flush, dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-comment_html.js';
- const SAVING_MESSAGE = 'Saving';
- const DRAFT_SINGULAR = 'draft...';
- const DRAFT_PLURAL = 'drafts...';
- const SAVED_MESSAGE = 'All changes saved';
+const STORAGE_DEBOUNCE_INTERVAL = 400;
+const TOAST_DEBOUNCE_INTERVAL = 200;
- const REPORT_CREATE_DRAFT = 'CreateDraftComment';
- const REPORT_UPDATE_DRAFT = 'UpdateDraftComment';
- const REPORT_DISCARD_DRAFT = 'DiscardDraftComment';
+const SAVING_MESSAGE = 'Saving';
+const DRAFT_SINGULAR = 'draft...';
+const DRAFT_PLURAL = 'drafts...';
+const SAVED_MESSAGE = 'All changes saved';
- const FILE = 'FILE';
+const REPORT_CREATE_DRAFT = 'CreateDraftComment';
+const REPORT_UPDATE_DRAFT = 'UpdateDraftComment';
+const REPORT_DISCARD_DRAFT = 'DiscardDraftComment';
+
+const FILE = 'FILE';
+
+/**
+ * All candidates tips to show, will pick randomly.
+ */
+const RESPECTFUL_REVIEW_TIPS= [
+ 'Assume competence.',
+ 'Provide rationale or context.',
+ 'Consider how comments may be interpreted.',
+ 'Avoid harsh language.',
+ 'Make your comments specific and actionable.',
+ 'When disagreeing, explain the advantage of your approach.',
+];
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrComment extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-comment'; }
+ /**
+ * Fired when the create fix comment action is triggered.
+ *
+ * @event create-fix-comment
+ */
/**
- * All candidates tips to show, will pick randomly.
+ * Fired when the show fix preview action is triggered.
+ *
+ * @event open-fix-preview
*/
- const RESPECTFUL_REVIEW_TIPS= [
- 'DO: Assume competence.',
- 'DO: Provide rationale or context.',
- 'DO: Consider how comments may be interpreted.',
- 'DON’T: Criticize the person.',
- 'DON’T: Use harsh language.',
- ];
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * Fired when this comment is discarded.
+ *
+ * @event comment-discard
*/
- class GrComment extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-comment'; }
- /**
- * Fired when the create fix comment action is triggered.
- *
- * @event create-fix-comment
- */
- /**
- * Fired when the show fix preview action is triggered.
- *
- * @event open-fix-preview
- */
+ /**
+ * Fired when this comment is saved.
+ *
+ * @event comment-save
+ */
- /**
- * Fired when this comment is discarded.
- *
- * @event comment-discard
- */
+ /**
+ * Fired when this comment is updated.
+ *
+ * @event comment-update
+ */
- /**
- * Fired when this comment is saved.
- *
- * @event comment-save
- */
+ /**
+ * Fired when the comment's timestamp is tapped.
+ *
+ * @event comment-anchor-tap
+ */
- /**
- * Fired when this comment is updated.
- *
- * @event comment-update
- */
+ static get properties() {
+ return {
+ changeNum: String,
+ /** @type {!Gerrit.Comment} */
+ comment: {
+ type: Object,
+ notify: true,
+ observer: '_commentChanged',
+ },
+ comments: {
+ type: Array,
+ },
+ isRobotComment: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ disabled: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ draft: {
+ type: Boolean,
+ value: false,
+ observer: '_draftChanged',
+ },
+ editing: {
+ type: Boolean,
+ value: false,
+ observer: '_editingChanged',
+ },
+ discarding: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ hasChildren: Boolean,
+ patchNum: String,
+ showActions: Boolean,
+ _showHumanActions: Boolean,
+ _showRobotActions: Boolean,
+ collapsed: {
+ type: Boolean,
+ value: true,
+ observer: '_toggleCollapseClass',
+ },
+ /** @type {?} */
+ projectConfig: Object,
+ robotButtonDisabled: Boolean,
+ _hasHumanReply: Boolean,
+ _isAdmin: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * Fired when the comment's timestamp is tapped.
- *
- * @event comment-anchor-tap
- */
+ _xhrPromise: Object, // Used for testing.
+ _messageText: {
+ type: String,
+ value: '',
+ observer: '_messageTextChanged',
+ },
+ commentSide: String,
+ side: String,
- static get properties() {
- return {
- changeNum: String,
- /** @type {!Gerrit.Comment} */
- comment: {
- type: Object,
- notify: true,
- observer: '_commentChanged',
- },
- comments: {
- type: Array,
- },
- isRobotComment: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- disabled: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- draft: {
- type: Boolean,
- value: false,
- observer: '_draftChanged',
- },
- editing: {
- type: Boolean,
- value: false,
- observer: '_editingChanged',
- },
- discarding: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- hasChildren: Boolean,
- patchNum: String,
- showActions: Boolean,
- _showHumanActions: Boolean,
- _showRobotActions: Boolean,
- collapsed: {
- type: Boolean,
- value: true,
- observer: '_toggleCollapseClass',
- },
- /** @type {?} */
- projectConfig: Object,
- robotButtonDisabled: Boolean,
- _hasHumanReply: Boolean,
- _isAdmin: {
- type: Boolean,
- value: false,
- },
+ resolved: Boolean,
- _xhrPromise: Object, // Used for testing.
- _messageText: {
- type: String,
- value: '',
- observer: '_messageTextChanged',
- },
- commentSide: String,
- side: String,
+ _numPendingDraftRequests: {
+ type: Object,
+ value:
+ {number: 0}, // Intentional to share the object across instances.
+ },
- resolved: Boolean,
+ _enableOverlay: {
+ type: Boolean,
+ value: false,
+ },
- _numPendingDraftRequests: {
- type: Object,
- value:
- {number: 0}, // Intentional to share the object across instances.
- },
+ /**
+ * Property for storing references to overlay elements. When the overlays
+ * are moved to Gerrit.getRootElement() to be shown they are no-longer
+ * children, so they can't be queried along the tree, so they are stored
+ * here.
+ */
+ _overlays: {
+ type: Object,
+ value: () => { return {}; },
+ },
- _enableOverlay: {
- type: Boolean,
- value: false,
- },
+ _showRespectfulTip: {
+ type: Boolean,
+ value: false,
+ },
+ _respectfulReviewTip: String,
+ _respectfulTipDismissed: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
- /**
- * Property for storing references to overlay elements. When the overlays
- * are moved to Gerrit.getRootElement() to be shown they are no-longer
- * children, so they can't be queried along the tree, so they are stored
- * here.
- */
- _overlays: {
- type: Object,
- value: () => { return {}; },
- },
+ static get observers() {
+ return [
+ '_commentMessageChanged(comment.message)',
+ '_loadLocalDraft(changeNum, patchNum, comment)',
+ '_isRobotComment(comment)',
+ '_calculateActionstoShow(showActions, isRobotComment)',
+ '_computeHasHumanReply(comment, comments.*)',
+ '_onEditingChange(editing)',
+ ];
+ }
- _showRespectfulTip: {
- type: Boolean,
- value: false,
- },
- _respectfulReviewTip: String,
- _respectfulTipDismissed: {
- type: Boolean,
- value: false,
- },
- };
+ get keyBindings() {
+ return {
+ 'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey',
+ 'esc': '_handleEsc',
+ };
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ if (this.editing) {
+ this.collapsed = false;
+ } else if (this.comment) {
+ this.collapsed = this.comment.collapsed;
}
+ this._getIsAdmin().then(isAdmin => {
+ this._isAdmin = isAdmin;
+ });
+ }
- static get observers() {
- return [
- '_commentMessageChanged(comment.message)',
- '_loadLocalDraft(changeNum, patchNum, comment)',
- '_isRobotComment(comment)',
- '_calculateActionstoShow(showActions, isRobotComment)',
- '_computeHasHumanReply(comment, comments.*)',
- '_onEditingChange(editing)',
- ];
+ /** @override */
+ detached() {
+ super.detached();
+ this.cancelDebouncer('fire-update');
+ if (this.textarea) {
+ this.textarea.closeDropdown();
}
+ }
- get keyBindings() {
- return {
- 'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey',
- 'esc': '_handleEsc',
- };
- }
-
- /** @override */
- attached() {
- super.attached();
- if (this.editing) {
- this.collapsed = false;
- } else if (this.comment) {
- this.collapsed = this.comment.collapsed;
- }
- this._getIsAdmin().then(isAdmin => {
- this._isAdmin = isAdmin;
- });
- }
-
- /** @override */
- detached() {
- super.detached();
- this.cancelDebouncer('fire-update');
- if (this.textarea) {
- this.textarea.closeDropdown();
- }
- }
-
- _onEditingChange(editing) {
- if (!editing) return;
- // visibility based on cache this will make sure we only and always show
- // a tip once every Math.max(a day, period between creating comments)
- const cachedVisibilityOfRespectfulTip =
- this.$.storage.getRespectfulTipVisibility();
- if (!cachedVisibilityOfRespectfulTip) {
- // we still want to show the tip with a probability of 30%
- if (this.getRandomNum(0, 3) >= 1) return;
- this._showRespectfulTip = true;
- const randomIdx = this.getRandomNum(0, RESPECTFUL_REVIEW_TIPS.length);
- this._respectfulReviewTip = RESPECTFUL_REVIEW_TIPS[randomIdx];
- this.$.reporting.reportInteraction(
- 'respectful-tip-appeared',
- {tip: this._respectfulReviewTip}
- );
- // update cache
- this.$.storage.setRespectfulTipVisibility();
- }
- }
-
- /** Set as a separate method so easy to stub. */
- getRandomNum(min, max) {
- return Math.floor(Math.random() * (max - min) + min);
- }
-
- _computeVisibilityOfTip(showTip, tipDismissed) {
- return showTip && !tipDismissed;
- }
-
- _dismissRespectfulTip() {
- this._respectfulTipDismissed = true;
+ _onEditingChange(editing) {
+ if (!editing) return;
+ // visibility based on cache this will make sure we only and always show
+ // a tip once every Math.max(a day, period between creating comments)
+ const cachedVisibilityOfRespectfulTip =
+ this.$.storage.getRespectfulTipVisibility();
+ if (!cachedVisibilityOfRespectfulTip) {
+ // we still want to show the tip with a probability of 30%
+ if (this.getRandomNum(0, 3) >= 1) return;
+ this._showRespectfulTip = true;
+ const randomIdx = this.getRandomNum(0, RESPECTFUL_REVIEW_TIPS.length);
+ this._respectfulReviewTip = RESPECTFUL_REVIEW_TIPS[randomIdx];
this.$.reporting.reportInteraction(
- 'respectful-tip-dismissed',
+ 'respectful-tip-appeared',
{tip: this._respectfulReviewTip}
);
- // add a 3 day delay to the tip cache
- this.$.storage.setRespectfulTipVisibility(/* delayDays= */ 3);
+ // update cache
+ this.$.storage.setRespectfulTipVisibility();
+ }
+ }
+
+ /** Set as a separate method so easy to stub. */
+ getRandomNum(min, max) {
+ return Math.floor(Math.random() * (max - min) + min);
+ }
+
+ _computeVisibilityOfTip(showTip, tipDismissed) {
+ return showTip && !tipDismissed;
+ }
+
+ _dismissRespectfulTip() {
+ this._respectfulTipDismissed = true;
+ this.$.reporting.reportInteraction(
+ 'respectful-tip-dismissed',
+ {tip: this._respectfulReviewTip}
+ );
+ // add a 14-day delay to the tip cache
+ this.$.storage.setRespectfulTipVisibility(/* delayDays= */ 14);
+ }
+
+ _onRespectfulReadMoreClick() {
+ this.$.reporting.reportInteraction('respectful-read-more-clicked');
+ }
+
+ get textarea() {
+ return this.shadowRoot.querySelector('#editTextarea');
+ }
+
+ get confirmDeleteOverlay() {
+ if (!this._overlays.confirmDelete) {
+ this._enableOverlay = true;
+ flush();
+ this._overlays.confirmDelete = this.shadowRoot
+ .querySelector('#confirmDeleteOverlay');
+ }
+ return this._overlays.confirmDelete;
+ }
+
+ get confirmDiscardOverlay() {
+ if (!this._overlays.confirmDiscard) {
+ this._enableOverlay = true;
+ flush();
+ this._overlays.confirmDiscard = this.shadowRoot
+ .querySelector('#confirmDiscardOverlay');
+ }
+ return this._overlays.confirmDiscard;
+ }
+
+ _computeShowHideIcon(collapsed) {
+ return collapsed ? 'gr-icons:expand-more' : 'gr-icons:expand-less';
+ }
+
+ _calculateActionstoShow(showActions, isRobotComment) {
+ // Polymer 2: check for undefined
+ if ([showActions, isRobotComment].some(arg => arg === undefined)) {
+ return;
}
- _onRespectfulReadMoreClick() {
- this.$.reporting.reportInteraction('respectful-read-more-clicked');
+ this._showHumanActions = showActions && !isRobotComment;
+ this._showRobotActions = showActions && isRobotComment;
+ }
+
+ _isRobotComment(comment) {
+ this.isRobotComment = !!comment.robot_id;
+ }
+
+ isOnParent() {
+ return this.side === 'PARENT';
+ }
+
+ _getIsAdmin() {
+ return this.$.restAPI.getIsAdmin();
+ }
+
+ /**
+ * @param {*=} opt_comment
+ */
+ save(opt_comment) {
+ let comment = opt_comment;
+ if (!comment) {
+ comment = this.comment;
}
- get textarea() {
- return this.shadowRoot.querySelector('#editTextarea');
+ this.set('comment.message', this._messageText);
+ this.editing = false;
+ this.disabled = true;
+
+ if (!this._messageText) {
+ return this._discardDraft();
}
- get confirmDeleteOverlay() {
- if (!this._overlays.confirmDelete) {
- this._enableOverlay = true;
- Polymer.dom.flush();
- this._overlays.confirmDelete = this.shadowRoot
- .querySelector('#confirmDeleteOverlay');
- }
- return this._overlays.confirmDelete;
- }
+ this._xhrPromise = this._saveDraft(comment).then(response => {
+ this.disabled = false;
+ if (!response.ok) { return response; }
- get confirmDiscardOverlay() {
- if (!this._overlays.confirmDiscard) {
- this._enableOverlay = true;
- Polymer.dom.flush();
- this._overlays.confirmDiscard = this.shadowRoot
- .querySelector('#confirmDiscardOverlay');
- }
- return this._overlays.confirmDiscard;
- }
-
- _computeShowHideIcon(collapsed) {
- return collapsed ? 'gr-icons:expand-more' : 'gr-icons:expand-less';
- }
-
- _calculateActionstoShow(showActions, isRobotComment) {
- // Polymer 2: check for undefined
- if ([showActions, isRobotComment].some(arg => arg === undefined)) {
- return;
- }
-
- this._showHumanActions = showActions && !isRobotComment;
- this._showRobotActions = showActions && isRobotComment;
- }
-
- _isRobotComment(comment) {
- this.isRobotComment = !!comment.robot_id;
- }
-
- isOnParent() {
- return this.side === 'PARENT';
- }
-
- _getIsAdmin() {
- return this.$.restAPI.getIsAdmin();
- }
-
- /**
- * @param {*=} opt_comment
- */
- save(opt_comment) {
- let comment = opt_comment;
- if (!comment) {
- comment = this.comment;
- }
-
- this.set('comment.message', this._messageText);
- this.editing = false;
- this.disabled = true;
-
- if (!this._messageText) {
- return this._discardDraft();
- }
-
- this._xhrPromise = this._saveDraft(comment).then(response => {
- this.disabled = false;
- if (!response.ok) { return response; }
-
- this._eraseDraftComment();
- return this.$.restAPI.getResponseObject(response).then(obj => {
- const resComment = obj;
- resComment.__draft = true;
- // Maintain the ephemeral draft ID for identification by other
- // elements.
- if (this.comment.__draftID) {
- resComment.__draftID = this.comment.__draftID;
- }
- resComment.__commentSide = this.commentSide;
- this.comment = resComment;
- this._fireSave();
- return obj;
+ this._eraseDraftComment();
+ return this.$.restAPI.getResponseObject(response).then(obj => {
+ const resComment = obj;
+ resComment.__draft = true;
+ // Maintain the ephemeral draft ID for identification by other
+ // elements.
+ if (this.comment.__draftID) {
+ resComment.__draftID = this.comment.__draftID;
+ }
+ resComment.__commentSide = this.commentSide;
+ this.comment = resComment;
+ this._fireSave();
+ return obj;
+ });
+ })
+ .catch(err => {
+ this.disabled = false;
+ throw err;
});
- })
- .catch(err => {
- this.disabled = false;
- throw err;
- });
- return this._xhrPromise;
+ return this._xhrPromise;
+ }
+
+ _eraseDraftComment() {
+ // Prevents a race condition in which removing the draft comment occurs
+ // prior to it being saved.
+ this.cancelDebouncer('store');
+
+ this.$.storage.eraseDraftComment({
+ changeNum: this.changeNum,
+ patchNum: this._getPatchNum(),
+ path: this.comment.path,
+ line: this.comment.line,
+ range: this.comment.range,
+ });
+ }
+
+ _commentChanged(comment) {
+ this.editing = !!comment.__editing;
+ this.resolved = !comment.unresolved;
+ if (this.editing) { // It's a new draft/reply, notify.
+ this._fireUpdate();
+ }
+ }
+
+ _computeHasHumanReply() {
+ if (!this.comment || !this.comments) return;
+ // hide please fix button for robot comment that has human reply
+ this._hasHumanReply = this.comments
+ .some(c => c.in_reply_to && c.in_reply_to === this.comment.id &&
+ !c.robot_id);
+ }
+
+ /**
+ * @param {!Object=} opt_mixin
+ *
+ * @return {!Object}
+ */
+ _getEventPayload(opt_mixin) {
+ return Object.assign({}, opt_mixin, {
+ comment: this.comment,
+ patchNum: this.patchNum,
+ });
+ }
+
+ _fireSave() {
+ this.fire('comment-save', this._getEventPayload());
+ }
+
+ _fireUpdate() {
+ this.debounce('fire-update', () => {
+ this.fire('comment-update', this._getEventPayload());
+ });
+ }
+
+ _draftChanged(draft) {
+ this.$.container.classList.toggle('draft', draft);
+ }
+
+ _editingChanged(editing, previousValue) {
+ // Polymer 2: observer fires when at least one property is defined.
+ // Do nothing to prevent comment.__editing being overwritten
+ // if previousValue is undefined
+ if (previousValue === undefined) return;
+
+ this.$.container.classList.toggle('editing', editing);
+ if (this.comment && this.comment.id) {
+ this.shadowRoot.querySelector('.cancel').hidden = !editing;
+ }
+ if (this.comment) {
+ this.comment.__editing = this.editing;
+ }
+ if (editing != !!previousValue) {
+ // To prevent event firing on comment creation.
+ this._fireUpdate();
+ }
+ if (editing) {
+ this.async(() => {
+ flush();
+ this.textarea && this.textarea.putCursorAtEnd();
+ }, 1);
+ }
+ }
+
+ _computeDeleteButtonClass(isAdmin, draft) {
+ return isAdmin && !draft ? 'showDeleteButtons' : '';
+ }
+
+ _computeSaveDisabled(draft, comment, resolved) {
+ // If resolved state has changed and a msg exists, save should be enabled.
+ if (!comment || comment.unresolved === resolved && draft) {
+ return false;
+ }
+ return !draft || draft.trim() === '';
+ }
+
+ _handleSaveKey(e) {
+ if (!this._computeSaveDisabled(this._messageText, this.comment,
+ this.resolved)) {
+ e.preventDefault();
+ this._handleSave(e);
+ }
+ }
+
+ _handleEsc(e) {
+ if (!this._messageText.length) {
+ e.preventDefault();
+ this._handleCancel(e);
+ }
+ }
+
+ _handleToggleCollapsed() {
+ this.collapsed = !this.collapsed;
+ }
+
+ _toggleCollapseClass(collapsed) {
+ if (collapsed) {
+ this.$.container.classList.add('collapsed');
+ } else {
+ this.$.container.classList.remove('collapsed');
+ }
+ }
+
+ _commentMessageChanged(message) {
+ this._messageText = message || '';
+ }
+
+ _messageTextChanged(newValue, oldValue) {
+ if (!this.comment || (this.comment && this.comment.id)) {
+ return;
}
- _eraseDraftComment() {
- // Prevents a race condition in which removing the draft comment occurs
- // prior to it being saved.
- this.cancelDebouncer('store');
-
- this.$.storage.eraseDraftComment({
+ this.debounce('store', () => {
+ const message = this._messageText;
+ const commentLocation = {
changeNum: this.changeNum,
patchNum: this._getPatchNum(),
path: this.comment.path,
line: this.comment.line,
range: this.comment.range,
- });
- }
+ };
- _commentChanged(comment) {
- this.editing = !!comment.__editing;
- this.resolved = !comment.unresolved;
- if (this.editing) { // It's a new draft/reply, notify.
- this._fireUpdate();
- }
- }
-
- _computeHasHumanReply() {
- if (!this.comment || !this.comments) return;
- // hide please fix button for robot comment that has human reply
- this._hasHumanReply = this.comments
- .some(c => c.in_reply_to && c.in_reply_to === this.comment.id &&
- !c.robot_id);
- }
-
- /**
- * @param {!Object=} opt_mixin
- *
- * @return {!Object}
- */
- _getEventPayload(opt_mixin) {
- return Object.assign({}, opt_mixin, {
- comment: this.comment,
- patchNum: this.patchNum,
- });
- }
-
- _fireSave() {
- this.fire('comment-save', this._getEventPayload());
- }
-
- _fireUpdate() {
- this.debounce('fire-update', () => {
- this.fire('comment-update', this._getEventPayload());
- });
- }
-
- _draftChanged(draft) {
- this.$.container.classList.toggle('draft', draft);
- }
-
- _editingChanged(editing, previousValue) {
- // Polymer 2: observer fires when at least one property is defined.
- // Do nothing to prevent comment.__editing being overwritten
- // if previousValue is undefined
- if (previousValue === undefined) return;
-
- this.$.container.classList.toggle('editing', editing);
- if (this.comment && this.comment.id) {
- this.shadowRoot.querySelector('.cancel').hidden = !editing;
- }
- if (this.comment) {
- this.comment.__editing = this.editing;
- }
- if (editing != !!previousValue) {
- // To prevent event firing on comment creation.
- this._fireUpdate();
- }
- if (editing) {
- this.async(() => {
- Polymer.dom.flush();
- this.textarea && this.textarea.putCursorAtEnd();
- }, 1);
- }
- }
-
- _computeDeleteButtonClass(isAdmin, draft) {
- return isAdmin && !draft ? 'showDeleteButtons' : '';
- }
-
- _computeSaveDisabled(draft, comment, resolved) {
- // If resolved state has changed and a msg exists, save should be enabled.
- if (!comment || comment.unresolved === resolved && draft) {
- return false;
- }
- return !draft || draft.trim() === '';
- }
-
- _handleSaveKey(e) {
- if (!this._computeSaveDisabled(this._messageText, this.comment,
- this.resolved)) {
- e.preventDefault();
- this._handleSave(e);
- }
- }
-
- _handleEsc(e) {
- if (!this._messageText.length) {
- e.preventDefault();
- this._handleCancel(e);
- }
- }
-
- _handleToggleCollapsed() {
- this.collapsed = !this.collapsed;
- }
-
- _toggleCollapseClass(collapsed) {
- if (collapsed) {
- this.$.container.classList.add('collapsed');
+ if ((!this._messageText || !this._messageText.length) && oldValue) {
+ // If the draft has been modified to be empty, then erase the storage
+ // entry.
+ this.$.storage.eraseDraftComment(commentLocation);
} else {
- this.$.container.classList.remove('collapsed');
+ this.$.storage.setDraftComment(commentLocation, message);
}
+ }, STORAGE_DEBOUNCE_INTERVAL);
+ }
+
+ _handleAnchorClick(e) {
+ e.preventDefault();
+ if (!this.comment.line) {
+ return;
+ }
+ this.dispatchEvent(new CustomEvent('comment-anchor-tap', {
+ bubbles: true,
+ composed: true,
+ detail: {
+ number: this.comment.line || FILE,
+ side: this.side,
+ },
+ }));
+ }
+
+ _handleEdit(e) {
+ e.preventDefault();
+ this._messageText = this.comment.message;
+ this.editing = true;
+ this.$.reporting.recordDraftInteraction();
+ }
+
+ _handleSave(e) {
+ e.preventDefault();
+
+ // Ignore saves started while already saving.
+ if (this.disabled) {
+ return;
+ }
+ const timingLabel = this.comment.id ?
+ REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT;
+ const timer = this.$.reporting.getTimer(timingLabel);
+ this.set('comment.__editing', false);
+ return this.save().then(() => { timer.end(); });
+ }
+
+ _handleCancel(e) {
+ e.preventDefault();
+
+ if (!this.comment.message ||
+ this.comment.message.trim().length === 0 ||
+ !this.comment.id) {
+ this._fireDiscard();
+ return;
+ }
+ this._messageText = this.comment.message;
+ this.editing = false;
+ }
+
+ _fireDiscard() {
+ this.cancelDebouncer('fire-update');
+ this.fire('comment-discard', this._getEventPayload());
+ }
+
+ _handleFix() {
+ this.dispatchEvent(new CustomEvent('create-fix-comment', {
+ bubbles: true,
+ composed: true,
+ detail: this._getEventPayload(),
+ }));
+ }
+
+ _handleShowFix() {
+ this.dispatchEvent(new CustomEvent('open-fix-preview', {
+ bubbles: true,
+ composed: true,
+ detail: this._getEventPayload(),
+ }));
+ }
+
+ _hasNoFix(comment) {
+ return !comment || !comment.fix_suggestions;
+ }
+
+ _handleDiscard(e) {
+ e.preventDefault();
+ this.$.reporting.recordDraftInteraction();
+
+ if (!this._messageText) {
+ this._discardDraft();
+ return;
}
- _commentMessageChanged(message) {
- this._messageText = message || '';
+ this._openOverlay(this.confirmDiscardOverlay).then(() => {
+ this.confirmDiscardOverlay.querySelector('#confirmDiscardDialog')
+ .resetFocus();
+ });
+ }
+
+ _handleConfirmDiscard(e) {
+ e.preventDefault();
+ const timer = this.$.reporting.getTimer(REPORT_DISCARD_DRAFT);
+ this._closeConfirmDiscardOverlay();
+ return this._discardDraft().then(() => { timer.end(); });
+ }
+
+ _discardDraft() {
+ if (!this.comment.__draft) {
+ throw Error('Cannot discard a non-draft comment.');
+ }
+ this.discarding = true;
+ this.editing = false;
+ this.disabled = true;
+ this._eraseDraftComment();
+
+ if (!this.comment.id) {
+ this.disabled = false;
+ this._fireDiscard();
+ return;
}
- _messageTextChanged(newValue, oldValue) {
- if (!this.comment || (this.comment && this.comment.id)) {
- return;
+ this._xhrPromise = this._deleteDraft(this.comment).then(response => {
+ this.disabled = false;
+ if (!response.ok) {
+ this.discarding = false;
+ return response;
}
- this.debounce('store', () => {
- const message = this._messageText;
- const commentLocation = {
- changeNum: this.changeNum,
- patchNum: this._getPatchNum(),
- path: this.comment.path,
- line: this.comment.line,
- range: this.comment.range,
- };
+ this._fireDiscard();
+ })
+ .catch(err => {
+ this.disabled = false;
+ throw err;
+ });
- if ((!this._messageText || !this._messageText.length) && oldValue) {
- // If the draft has been modified to be empty, then erase the storage
- // entry.
- this.$.storage.eraseDraftComment(commentLocation);
- } else {
- this.$.storage.setDraftComment(commentLocation, message);
- }
- }, STORAGE_DEBOUNCE_INTERVAL);
+ return this._xhrPromise;
+ }
+
+ _closeConfirmDiscardOverlay() {
+ this._closeOverlay(this.confirmDiscardOverlay);
+ }
+
+ _getSavingMessage(numPending) {
+ if (numPending === 0) {
+ return SAVED_MESSAGE;
}
+ return [
+ SAVING_MESSAGE,
+ numPending,
+ numPending === 1 ? DRAFT_SINGULAR : DRAFT_PLURAL,
+ ].join(' ');
+ }
- _handleAnchorClick(e) {
- e.preventDefault();
- if (!this.comment.line) {
- return;
+ _showStartRequest() {
+ const numPending = ++this._numPendingDraftRequests.number;
+ this._updateRequestToast(numPending);
+ }
+
+ _showEndRequest() {
+ const numPending = --this._numPendingDraftRequests.number;
+ this._updateRequestToast(numPending);
+ }
+
+ _handleFailedDraftRequest() {
+ this._numPendingDraftRequests.number--;
+
+ // Cancel the debouncer so that error toasts from the error-manager will
+ // not be overridden.
+ this.cancelDebouncer('draft-toast');
+ }
+
+ _updateRequestToast(numPending) {
+ const message = this._getSavingMessage(numPending);
+ this.debounce('draft-toast', () => {
+ // Note: the event is fired on the body rather than this element because
+ // this element may not be attached by the time this executes, in which
+ // case the event would not bubble.
+ document.body.dispatchEvent(new CustomEvent(
+ 'show-alert', {detail: {message}, bubbles: true, composed: true}));
+ }, TOAST_DEBOUNCE_INTERVAL);
+ }
+
+ _saveDraft(draft) {
+ this._showStartRequest();
+ return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft)
+ .then(result => {
+ if (result.ok) {
+ this._showEndRequest();
+ } else {
+ this._handleFailedDraftRequest();
+ }
+ return result;
+ });
+ }
+
+ _deleteDraft(draft) {
+ this._showStartRequest();
+ return this.$.restAPI.deleteDiffDraft(this.changeNum, this.patchNum,
+ draft).then(result => {
+ if (result.ok) {
+ this._showEndRequest();
+ } else {
+ this._handleFailedDraftRequest();
}
- this.dispatchEvent(new CustomEvent('comment-anchor-tap', {
- bubbles: true,
- composed: true,
- detail: {
- number: this.comment.line || FILE,
- side: this.side,
- },
- }));
+ return result;
+ });
+ }
+
+ _getPatchNum() {
+ return this.isOnParent() ? 'PARENT' : this.patchNum;
+ }
+
+ _loadLocalDraft(changeNum, patchNum, comment) {
+ // Polymer 2: check for undefined
+ if ([changeNum, patchNum, comment].some(arg => arg === undefined)) {
+ return;
}
- _handleEdit(e) {
- e.preventDefault();
- this._messageText = this.comment.message;
- this.editing = true;
- this.$.reporting.recordDraftInteraction();
+ // Only apply local drafts to comments that haven't been saved
+ // remotely, and haven't been given a default message already.
+ //
+ // Don't get local draft if there is another comment that is currently
+ // in an editing state.
+ if (!comment || comment.id || comment.message || comment.__otherEditing) {
+ delete comment.__otherEditing;
+ return;
}
- _handleSave(e) {
- e.preventDefault();
+ const draft = this.$.storage.getDraftComment({
+ changeNum,
+ patchNum: this._getPatchNum(),
+ path: comment.path,
+ line: comment.line,
+ range: comment.range,
+ });
- // Ignore saves started while already saving.
- if (this.disabled) {
- return;
- }
- const timingLabel = this.comment.id ?
- REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT;
- const timer = this.$.reporting.getTimer(timingLabel);
- this.set('comment.__editing', false);
- return this.save().then(() => { timer.end(); });
- }
-
- _handleCancel(e) {
- e.preventDefault();
-
- if (!this.comment.message ||
- this.comment.message.trim().length === 0 ||
- !this.comment.id) {
- this._fireDiscard();
- return;
- }
- this._messageText = this.comment.message;
- this.editing = false;
- }
-
- _fireDiscard() {
- this.cancelDebouncer('fire-update');
- this.fire('comment-discard', this._getEventPayload());
- }
-
- _handleFix() {
- this.dispatchEvent(new CustomEvent('create-fix-comment', {
- bubbles: true,
- composed: true,
- detail: this._getEventPayload(),
- }));
- }
-
- _handleShowFix() {
- this.dispatchEvent(new CustomEvent('open-fix-preview', {
- bubbles: true,
- composed: true,
- detail: this._getEventPayload(),
- }));
- }
-
- _hasNoFix(comment) {
- return !comment || !comment.fix_suggestions;
- }
-
- _handleDiscard(e) {
- e.preventDefault();
- this.$.reporting.recordDraftInteraction();
-
- if (!this._messageText) {
- this._discardDraft();
- return;
- }
-
- this._openOverlay(this.confirmDiscardOverlay).then(() => {
- this.confirmDiscardOverlay.querySelector('#confirmDiscardDialog')
- .resetFocus();
- });
- }
-
- _handleConfirmDiscard(e) {
- e.preventDefault();
- const timer = this.$.reporting.getTimer(REPORT_DISCARD_DRAFT);
- this._closeConfirmDiscardOverlay();
- return this._discardDraft().then(() => { timer.end(); });
- }
-
- _discardDraft() {
- if (!this.comment.__draft) {
- throw Error('Cannot discard a non-draft comment.');
- }
- this.discarding = true;
- this.editing = false;
- this.disabled = true;
- this._eraseDraftComment();
-
- if (!this.comment.id) {
- this.disabled = false;
- this._fireDiscard();
- return;
- }
-
- this._xhrPromise = this._deleteDraft(this.comment).then(response => {
- this.disabled = false;
- if (!response.ok) {
- this.discarding = false;
- return response;
- }
-
- this._fireDiscard();
- })
- .catch(err => {
- this.disabled = false;
- throw err;
- });
-
- return this._xhrPromise;
- }
-
- _closeConfirmDiscardOverlay() {
- this._closeOverlay(this.confirmDiscardOverlay);
- }
-
- _getSavingMessage(numPending) {
- if (numPending === 0) {
- return SAVED_MESSAGE;
- }
- return [
- SAVING_MESSAGE,
- numPending,
- numPending === 1 ? DRAFT_SINGULAR : DRAFT_PLURAL,
- ].join(' ');
- }
-
- _showStartRequest() {
- const numPending = ++this._numPendingDraftRequests.number;
- this._updateRequestToast(numPending);
- }
-
- _showEndRequest() {
- const numPending = --this._numPendingDraftRequests.number;
- this._updateRequestToast(numPending);
- }
-
- _handleFailedDraftRequest() {
- this._numPendingDraftRequests.number--;
-
- // Cancel the debouncer so that error toasts from the error-manager will
- // not be overridden.
- this.cancelDebouncer('draft-toast');
- }
-
- _updateRequestToast(numPending) {
- const message = this._getSavingMessage(numPending);
- this.debounce('draft-toast', () => {
- // Note: the event is fired on the body rather than this element because
- // this element may not be attached by the time this executes, in which
- // case the event would not bubble.
- document.body.dispatchEvent(new CustomEvent(
- 'show-alert', {detail: {message}, bubbles: true, composed: true}));
- }, TOAST_DEBOUNCE_INTERVAL);
- }
-
- _saveDraft(draft) {
- this._showStartRequest();
- return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft)
- .then(result => {
- if (result.ok) {
- this._showEndRequest();
- } else {
- this._handleFailedDraftRequest();
- }
- return result;
- });
- }
-
- _deleteDraft(draft) {
- this._showStartRequest();
- return this.$.restAPI.deleteDiffDraft(this.changeNum, this.patchNum,
- draft).then(result => {
- if (result.ok) {
- this._showEndRequest();
- } else {
- this._handleFailedDraftRequest();
- }
- return result;
- });
- }
-
- _getPatchNum() {
- return this.isOnParent() ? 'PARENT' : this.patchNum;
- }
-
- _loadLocalDraft(changeNum, patchNum, comment) {
- // Polymer 2: check for undefined
- if ([changeNum, patchNum, comment].some(arg => arg === undefined)) {
- return;
- }
-
- // Only apply local drafts to comments that haven't been saved
- // remotely, and haven't been given a default message already.
- //
- // Don't get local draft if there is another comment that is currently
- // in an editing state.
- if (!comment || comment.id || comment.message || comment.__otherEditing) {
- delete comment.__otherEditing;
- return;
- }
-
- const draft = this.$.storage.getDraftComment({
- changeNum,
- patchNum: this._getPatchNum(),
- path: comment.path,
- line: comment.line,
- range: comment.range,
- });
-
- if (draft) {
- this.set('comment.message', draft.message);
- }
- }
-
- _handleToggleResolved() {
- this.$.reporting.recordDraftInteraction();
- this.resolved = !this.resolved;
- // Modify payload instead of this.comment, as this.comment is passed from
- // the parent by ref.
- const payload = this._getEventPayload();
- payload.comment.unresolved = !this.$.resolvedCheckbox.checked;
- this.fire('comment-update', payload);
- if (!this.editing) {
- // Save the resolved state immediately.
- this.save(payload.comment);
- }
- }
-
- _handleCommentDelete() {
- this._openOverlay(this.confirmDeleteOverlay);
- }
-
- _handleCancelDeleteComment() {
- this._closeOverlay(this.confirmDeleteOverlay);
- }
-
- _openOverlay(overlay) {
- Polymer.dom(Gerrit.getRootElement()).appendChild(overlay);
- return overlay.open();
- }
-
- _computeAuthorName(comment) {
- if (!comment) return '';
- if (comment.robot_id) {
- return comment.robot_id;
- }
- return comment.author && comment.author.name;
- }
-
- _computeHideRunDetails(comment, collapsed) {
- if (!comment) return true;
- return !(comment.robot_id && comment.url && !collapsed);
- }
-
- _closeOverlay(overlay) {
- Polymer.dom(Gerrit.getRootElement()).removeChild(overlay);
- overlay.close();
- }
-
- _handleConfirmDeleteComment() {
- const dialog =
- this.confirmDeleteOverlay.querySelector('#confirmDeleteComment');
- this.$.restAPI.deleteComment(
- this.changeNum, this.patchNum, this.comment.id, dialog.message)
- .then(newComment => {
- this._handleCancelDeleteComment();
- this.comment = newComment;
- });
+ if (draft) {
+ this.set('comment.message', draft.message);
}
}
- customElements.define(GrComment.is, GrComment);
-})();
+ _handleToggleResolved() {
+ this.$.reporting.recordDraftInteraction();
+ this.resolved = !this.resolved;
+ // Modify payload instead of this.comment, as this.comment is passed from
+ // the parent by ref.
+ const payload = this._getEventPayload();
+ payload.comment.unresolved = !this.$.resolvedCheckbox.checked;
+ this.fire('comment-update', payload);
+ if (!this.editing) {
+ // Save the resolved state immediately.
+ this.save(payload.comment);
+ }
+ }
+
+ _handleCommentDelete() {
+ this._openOverlay(this.confirmDeleteOverlay);
+ }
+
+ _handleCancelDeleteComment() {
+ this._closeOverlay(this.confirmDeleteOverlay);
+ }
+
+ _openOverlay(overlay) {
+ dom(Gerrit.getRootElement()).appendChild(overlay);
+ return overlay.open();
+ }
+
+ _computeAuthorName(comment) {
+ if (!comment) return '';
+ if (comment.robot_id) {
+ return comment.robot_id;
+ }
+ return comment.author && comment.author.name;
+ }
+
+ _computeHideRunDetails(comment, collapsed) {
+ if (!comment) return true;
+ return !(comment.robot_id && comment.url && !collapsed);
+ }
+
+ _closeOverlay(overlay) {
+ dom(Gerrit.getRootElement()).removeChild(overlay);
+ overlay.close();
+ }
+
+ _handleConfirmDeleteComment() {
+ const dialog =
+ this.confirmDeleteOverlay.querySelector('#confirmDeleteComment');
+ this.$.restAPI.deleteComment(
+ this.changeNum, this.patchNum, this.comment.id, dialog.message)
+ .then(newComment => {
+ this._handleCancelDeleteComment();
+ this.comment = newComment;
+ });
+ }
+}
+
+customElements.define(GrComment.is, GrComment);
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
new file mode 100644
index 0000000..6c9ebee
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
@@ -0,0 +1,344 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ font-family: var(--font-family);
+ padding: var(--spacing-m);
+ }
+ :host([disabled]) {
+ pointer-events: none;
+ }
+ :host([disabled]) .actions,
+ :host([disabled]) .robotActions,
+ :host([disabled]) .date {
+ opacity: .5;
+ }
+ :host([discarding]) {
+ display: none;
+ }
+ .header {
+ align-items: center;
+ cursor: pointer;
+ display: flex;
+ margin: calc(0px - var(--spacing-m)) calc(0px - var(--spacing-m)) 0 calc(0px - var(--spacing-m));
+ padding: var(--spacing-m);
+ }
+ .container.collapsed .header {
+ margin-bottom: calc(0 - var(--spacing-m));
+ }
+ .headerMiddle {
+ color: var(--deemphasized-text-color);
+ flex: 1;
+ overflow: hidden;
+ }
+ .draftLabel,
+ .draftTooltip {
+ color: var(--deemphasized-text-color);
+ display: none;
+ }
+ .date {
+ justify-content: flex-end;
+ margin-left: 5px;
+ min-width: 4.5em;
+ text-align: right;
+ white-space: nowrap;
+ }
+ span.date {
+ color: var(--deemphasized-text-color);
+ }
+ span.date:hover {
+ text-decoration: underline;
+ }
+ .actions, .robotActions {
+ display: flex;
+ justify-content: flex-end;
+ padding-top: 0;
+ }
+ .action {
+ margin-left: var(--spacing-l);
+ }
+ .rightActions {
+ display: flex;
+ justify-content: flex-end;
+ }
+ .rightActions gr-button {
+ --gr-button: {
+ height: 20px;
+ padding: 0 var(--spacing-s);
+ }
+ }
+ .editMessage {
+ display: none;
+ margin: var(--spacing-m) 0;
+ width: 100%;
+ }
+ .container:not(.draft) .actions .hideOnPublished {
+ display: none;
+ }
+ .draft .reply,
+ .draft .quote,
+ .draft .ack,
+ .draft .done {
+ display: none;
+ }
+ .draft .draftLabel,
+ .draft .draftTooltip {
+ display: inline;
+ }
+ .draft:not(.editing) .save,
+ .draft:not(.editing) .cancel {
+ display: none;
+ }
+ .editing .message,
+ .editing .reply,
+ .editing .quote,
+ .editing .ack,
+ .editing .done,
+ .editing .edit,
+ .editing .discard,
+ .editing .unresolved {
+ display: none;
+ }
+ .editing .editMessage {
+ display: block;
+ }
+ .show-hide {
+ margin-left: var(--spacing-s);
+ }
+ .robotId {
+ color: var(--deemphasized-text-color);
+ margin-bottom: var(--spacing-m);
+ margin-top: -.4em;
+ }
+ .robotIcon {
+ margin-right: var(--spacing-xs);
+ /* because of the antenna of the robot, it looks off center even when it
+ is centered. artificially adjust margin to account for this. */
+ margin-top: -4px;
+ }
+ .runIdInformation {
+ margin: var(--spacing-m) 0;
+ }
+ .robotRun {
+ margin-left: var(--spacing-m);
+ }
+ .robotRunLink {
+ margin-left: var(--spacing-m);
+ }
+ input.show-hide {
+ display: none;
+ }
+ label.show-hide {
+ cursor: pointer;
+ display: block;
+ }
+ label.show-hide iron-icon {
+ vertical-align: top;
+ }
+ #container .collapsedContent {
+ display: none;
+ }
+ #container.collapsed {
+ padding-bottom: 3px;
+ }
+ #container.collapsed .collapsedContent {
+ display: block;
+ overflow: hidden;
+ padding-left: 5px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ #container.collapsed .actions,
+ #container.collapsed gr-formatted-text,
+ #container.collapsed gr-textarea,
+ #container.collapsed .respectfulReviewTip{
+ display: none;
+ }
+ .resolve,
+ .unresolved {
+ align-items: center;
+ display: flex;
+ flex: 1;
+ margin: 0;
+ }
+ .resolve label {
+ color: var(--comment-text-color);
+ }
+ gr-dialog .main {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ }
+ #deleteBtn {
+ display: none;
+ --gr-button: {
+ color: var(--deemphasized-text-color);
+ padding: 0;
+ }
+ }
+ #deleteBtn.showDeleteButtons {
+ display: block;
+ }
+
+ /** Disable select for the caret and actions */
+ .actions,
+ .show-hide {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ }
+
+ .respectfulReviewTip {
+ justify-content: space-between;
+ display: flex;
+ padding: var(--spacing-m);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ margin-bottom: var(--spacing-m);
+ }
+ .respectfulReviewTip div {
+ display: flex;
+ }
+ .respectfulReviewTip div iron-icon {
+ margin-right: var(--spacing-s);
+ }
+ .respectfulReviewTip a {
+ white-space: nowrap;
+ margin-right: var(--spacing-s);
+ padding-left: var(--spacing-m);
+ text-decoration: none;
+ }
+ .pointer {
+ cursor: pointer;
+ }
+ </style>
+ <div id="container" class="container">
+ <div class="header" id="header" on-click="_handleToggleCollapsed">
+ <div class="headerLeft">
+ <span class="authorName">[[_computeAuthorName(comment)]]</span>
+ <span class="draftLabel">DRAFT</span>
+ <gr-tooltip-content class="draftTooltip" has-tooltip="" title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'A' key." max-width="20em" show-icon=""></gr-tooltip-content>
+ </div>
+ <div class="headerMiddle">
+ <span class="collapsedContent">[[comment.message]]</span>
+ </div>
+ <div hidden\$="[[_computeHideRunDetails(comment, collapsed)]]" class="runIdMessage message">
+ <div class="runIdInformation">
+ <a class="robotRunLink" href\$="[[comment.url]]">
+ <span class="robotRun link">Run Details</span>
+ </a>
+ </div>
+ </div>
+ <gr-button id="deleteBtn" link="" class\$="action delete [[_computeDeleteButtonClass(_isAdmin, draft)]]" hidden\$="[[isRobotComment]]" on-click="_handleCommentDelete">
+ <iron-icon id="icon" icon="gr-icons:delete"></iron-icon>
+ </gr-button>
+ <span class="date" on-click="_handleAnchorClick">
+ <gr-date-formatter has-tooltip="" date-str="[[comment.updated]]"></gr-date-formatter>
+ </span>
+ <div class="show-hide">
+ <label class="show-hide">
+ <input type="checkbox" class="show-hide" checked\$="[[collapsed]]" on-change="_handleToggleCollapsed">
+ <iron-icon id="icon" icon="[[_computeShowHideIcon(collapsed)]]">
+ </iron-icon>
+ </label>
+ </div>
+ </div>
+ <div class="body">
+ <template is="dom-if" if="[[isRobotComment]]">
+ <div class="robotId" hidden\$="[[collapsed]]">
+ [[comment.author.name]]
+ </div>
+ </template>
+ <template is="dom-if" if="[[editing]]">
+ <gr-textarea id="editTextarea" class="editMessage" autocomplete="on" code="" disabled="{{disabled}}" rows="4" text="{{_messageText}}"></gr-textarea>
+ <template is="dom-if" if="[[_computeVisibilityOfTip(_showRespectfulTip, _respectfulTipDismissed)]]">
+ <div class="respectfulReviewTip">
+ <div>
+ <gr-tooltip-content has-tooltip="" title="Tips for respectful code reviews.">
+ <iron-icon class="pointer" icon="gr-icons:lightbulb-outline"></iron-icon>
+ </gr-tooltip-content>
+ [[_respectfulReviewTip]]
+ </div>
+ <div>
+ <a tabindex="-1" on-click="_onRespectfulReadMoreClick" href="https://testing.googleblog.com/2019/11/code-health-respectful-reviews-useful.html" target="_blank">
+ Read more
+ </a>
+ <a tabindex="-1" class="close pointer" on-click="_dismissRespectfulTip">Not helpful</a>
+ </div>
+ </div>
+ </template>
+ </template>
+ <!--The message class is needed to ensure selectability from
+ gr-diff-selection.-->
+ <gr-formatted-text class="message" content="[[comment.message]]" no-trailing-margin="[[!comment.__draft]]" config="[[projectConfig.commentlinks]]"></gr-formatted-text>
+ <div class="actions humanActions" hidden\$="[[!_showHumanActions]]">
+ <div class="action resolve hideOnPublished">
+ <label>
+ <input type="checkbox" id="resolvedCheckbox" checked="[[resolved]]" on-change="_handleToggleResolved">
+ Resolved
+ </label>
+ </div>
+ <div class="rightActions">
+ <gr-button link="" class="action cancel hideOnPublished" on-click="_handleCancel">Cancel</gr-button>
+ <gr-button link="" class="action discard hideOnPublished" on-click="_handleDiscard">Discard</gr-button>
+ <gr-button link="" class="action edit hideOnPublished" on-click="_handleEdit">Edit</gr-button>
+ <gr-button link="" disabled\$="[[_computeSaveDisabled(_messageText, comment, resolved)]]" class="action save hideOnPublished" on-click="_handleSave">Save</gr-button>
+ </div>
+ </div>
+ <div class="robotActions" hidden\$="[[!_showRobotActions]]">
+ <template is="dom-if" if="[[isRobotComment]]">
+ <gr-endpoint-decorator name="robot-comment-controls">
+ <gr-endpoint-param name="comment" value="[[comment]]">
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ <gr-button link="" secondary="" class="action show-fix" hidden\$="[[_hasNoFix(comment)]]" on-click="_handleShowFix">
+ Show Fix
+ </gr-button>
+ <template is="dom-if" if="[[!_hasHumanReply]]">
+ <gr-button link="" class="action fix" on-click="_handleFix" disabled="[[robotButtonDisabled]]">
+ Please Fix
+ </gr-button>
+ </template>
+ </template>
+ </div>
+ </div>
+ </div>
+ <template is="dom-if" if="[[_enableOverlay]]">
+ <gr-overlay id="confirmDeleteOverlay" with-backdrop="">
+ <gr-confirm-delete-comment-dialog id="confirmDeleteComment" on-confirm="_handleConfirmDeleteComment" on-cancel="_handleCancelDeleteComment">
+ </gr-confirm-delete-comment-dialog>
+ </gr-overlay>
+ <gr-overlay id="confirmDiscardOverlay" with-backdrop="">
+ <gr-dialog id="confirmDiscardDialog" confirm-label="Discard" confirm-on-enter="" on-confirm="_handleConfirmDiscard" on-cancel="_closeConfirmDiscardOverlay">
+ <div class="header" slot="header">
+ Discard comment
+ </div>
+ <div class="main" slot="main">
+ Are you sure you want to discard this draft comment?
+ </div>
+ </gr-dialog>
+ </gr-overlay>
+ </template>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ <gr-storage id="storage"></gr-storage>
+ <gr-reporting id="reporting"></gr-reporting>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
index 5e9d37a..33f19c5 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
@@ -19,18 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-comment</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="/bower_components/page/page.js"></script>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="gr-comment.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script src="/node_modules/page/page.js"></script>
<test-fixture id="basic">
<template>
@@ -44,1119 +37,1203 @@
</template>
</test-fixture>
-<script>
- function isVisible(el) {
- assert.ok(el);
- return getComputedStyle(el).getPropertyValue('display') !== 'none';
- }
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-comment.js';
+function isVisible(el) {
+ assert.ok(el);
+ return getComputedStyle(el).getPropertyValue('display') !== 'none';
+}
- suite('gr-comment tests', async () => {
- await readyToTest();
+suite('gr-comment tests', () => {
+ suite('basic tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve(null); },
+ });
+ element = fixture('basic');
+ element.comment = {
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 'baf0414d_60047215',
+ line: 5,
+ message: 'is this a crossover episode!?',
+ updated: '2015-12-08 19:48:33.843000000',
+ };
+ sandbox = sinon.sandbox.create();
+ });
- suite('basic tests', () => {
- let element;
- let sandbox;
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('collapsible comments', () => {
+ // When a comment (not draft) is loaded, it should be collapsed
+ assert.isTrue(element.collapsed);
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are not visible');
+ assert.isNotOk(element.textarea, 'textarea is not visible');
+
+ // The header middle content is only visible when comments are collapsed.
+ // It shows the message in a condensed way, and limits to a single line.
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is visible');
+
+ // When the header row is clicked, the comment should expand
+ MockInteractions.tap(element.$.header);
+ assert.isFalse(element.collapsed);
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are visible');
+ assert.isNotOk(element.textarea, 'textarea is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is not visible');
+ });
+
+ test('clicking on date link fires event', () => {
+ element.side = 'PARENT';
+ const stub = sinon.stub();
+ element.addEventListener('comment-anchor-tap', stub);
+ const dateEl = element.shadowRoot
+ .querySelector('.date');
+ assert.ok(dateEl);
+ MockInteractions.tap(dateEl);
+
+ assert.isTrue(stub.called);
+ assert.deepEqual(stub.lastCall.args[0].detail,
+ {side: element.side, number: element.comment.line});
+ });
+
+ test('message is not retrieved from storage when other edits', done => {
+ const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
+ const loadSpy = sandbox.spy(element, '_loadLocalDraft');
+
+ element.changeNum = 1;
+ element.patchNum = 1;
+ element.comment = {
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ line: 5,
+ __otherEditing: true,
+ };
+ flush(() => {
+ assert.isTrue(loadSpy.called);
+ assert.isFalse(storageStub.called);
+ done();
+ });
+ });
+
+ test('message is retrieved from storage when no other edits', done => {
+ const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
+ const loadSpy = sandbox.spy(element, '_loadLocalDraft');
+
+ element.changeNum = 1;
+ element.patchNum = 1;
+ element.comment = {
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ line: 5,
+ };
+ flush(() => {
+ assert.isTrue(loadSpy.called);
+ assert.isTrue(storageStub.called);
+ done();
+ });
+ });
+
+ test('_getPatchNum', () => {
+ element.side = 'PARENT';
+ element.patchNum = 1;
+ assert.equal(element._getPatchNum(), 'PARENT');
+ element.side = 'REVISION';
+ assert.equal(element._getPatchNum(), 1);
+ });
+
+ test('comment expand and collapse', () => {
+ element.collapsed = true;
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are not visible');
+ assert.isNotOk(element.textarea, 'textarea is not visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is visible');
+
+ element.collapsed = false;
+ assert.isFalse(element.collapsed);
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are visible');
+ assert.isNotOk(element.textarea, 'textarea is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is is not visible');
+ });
+
+ suite('while editing', () => {
setup(() => {
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve(null); },
- });
- element = fixture('basic');
- element.comment = {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
- },
- id: 'baf0414d_60047215',
- line: 5,
- message: 'is this a crossover episode!?',
- updated: '2015-12-08 19:48:33.843000000',
- };
- sandbox = sinon.sandbox.create();
+ element.editing = true;
+ element._messageText = 'test';
+ sandbox.stub(element, '_handleCancel');
+ sandbox.stub(element, '_handleSave');
+ flushAsynchronousOperations();
});
- teardown(() => {
- sandbox.restore();
- });
-
- test('collapsible comments', () => {
- // When a comment (not draft) is loaded, it should be collapsed
- assert.isTrue(element.collapsed);
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are not visible');
- assert.isNotOk(element.textarea, 'textarea is not visible');
-
- // The header middle content is only visible when comments are collapsed.
- // It shows the message in a condensed way, and limits to a single line.
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is visible');
-
- // When the header row is clicked, the comment should expand
- MockInteractions.tap(element.$.header);
- assert.isFalse(element.collapsed);
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are visible');
- assert.isNotOk(element.textarea, 'textarea is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is not visible');
- });
-
- test('clicking on date link fires event', () => {
- element.side = 'PARENT';
- const stub = sinon.stub();
- element.addEventListener('comment-anchor-tap', stub);
- const dateEl = element.shadowRoot
- .querySelector('.date');
- assert.ok(dateEl);
- MockInteractions.tap(dateEl);
-
- assert.isTrue(stub.called);
- assert.deepEqual(stub.lastCall.args[0].detail,
- {side: element.side, number: element.comment.line});
- });
-
- test('message is not retrieved from storage when other edits', done => {
- const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
- const loadSpy = sandbox.spy(element, '_loadLocalDraft');
-
- element.changeNum = 1;
- element.patchNum = 1;
- element.comment = {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
- },
- line: 5,
- __otherEditing: true,
- };
- flush(() => {
- assert.isTrue(loadSpy.called);
- assert.isFalse(storageStub.called);
- done();
- });
- });
-
- test('message is retrieved from storage when no other edits', done => {
- const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
- const loadSpy = sandbox.spy(element, '_loadLocalDraft');
-
- element.changeNum = 1;
- element.patchNum = 1;
- element.comment = {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
- },
- line: 5,
- };
- flush(() => {
- assert.isTrue(loadSpy.called);
- assert.isTrue(storageStub.called);
- done();
- });
- });
-
- test('_getPatchNum', () => {
- element.side = 'PARENT';
- element.patchNum = 1;
- assert.equal(element._getPatchNum(), 'PARENT');
- element.side = 'REVISION';
- assert.equal(element._getPatchNum(), 1);
- });
-
- test('comment expand and collapse', () => {
- element.collapsed = true;
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are not visible');
- assert.isNotOk(element.textarea, 'textarea is not visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is visible');
-
- element.collapsed = false;
- assert.isFalse(element.collapsed);
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are visible');
- assert.isNotOk(element.textarea, 'textarea is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is is not visible');
- });
-
- suite('while editing', () => {
+ suite('when text is empty', () => {
setup(() => {
- element.editing = true;
- element._messageText = 'test';
- sandbox.stub(element, '_handleCancel');
- sandbox.stub(element, '_handleSave');
- flushAsynchronousOperations();
+ element._messageText = '';
+ element.comment = {};
});
- suite('when text is empty', () => {
- setup(() => {
- element._messageText = '';
- element.comment = {};
- });
-
- test('esc closes comment when text is empty', () => {
- MockInteractions.pressAndReleaseKeyOn(
- element.textarea, 27); // esc
- assert.isTrue(element._handleCancel.called);
- });
-
- test('ctrl+enter does not save', () => {
- MockInteractions.pressAndReleaseKeyOn(
- element.textarea, 13, 'ctrl'); // ctrl + enter
- assert.isFalse(element._handleSave.called);
- });
-
- test('meta+enter does not save', () => {
- MockInteractions.pressAndReleaseKeyOn(
- element.textarea, 13, 'meta'); // meta + enter
- assert.isFalse(element._handleSave.called);
- });
-
- test('ctrl+s does not save', () => {
- MockInteractions.pressAndReleaseKeyOn(
- element.textarea, 83, 'ctrl'); // ctrl + s
- assert.isFalse(element._handleSave.called);
- });
- });
-
- test('esc does not close comment that has content', () => {
+ test('esc closes comment when text is empty', () => {
MockInteractions.pressAndReleaseKeyOn(
element.textarea, 27); // esc
- assert.isFalse(element._handleCancel.called);
+ assert.isTrue(element._handleCancel.called);
});
- test('ctrl+enter saves', () => {
+ test('ctrl+enter does not save', () => {
MockInteractions.pressAndReleaseKeyOn(
element.textarea, 13, 'ctrl'); // ctrl + enter
- assert.isTrue(element._handleSave.called);
+ assert.isFalse(element._handleSave.called);
});
- test('meta+enter saves', () => {
+ test('meta+enter does not save', () => {
MockInteractions.pressAndReleaseKeyOn(
element.textarea, 13, 'meta'); // meta + enter
- assert.isTrue(element._handleSave.called);
+ assert.isFalse(element._handleSave.called);
});
- test('ctrl+s saves', () => {
+ test('ctrl+s does not save', () => {
MockInteractions.pressAndReleaseKeyOn(
element.textarea, 83, 'ctrl'); // ctrl + s
- assert.isTrue(element._handleSave.called);
- });
- });
- test('delete comment button for non-admins is hidden', () => {
- element._isAdmin = false;
- assert.isFalse(element.shadowRoot
- .querySelector('.action.delete')
- .classList.contains('showDeleteButtons'));
- });
-
- test('delete comment button for admins with draft is hidden', () => {
- element._isAdmin = false;
- element.draft = true;
- assert.isFalse(element.shadowRoot
- .querySelector('.action.delete')
- .classList.contains('showDeleteButtons'));
- });
-
- test('delete comment', done => {
- sandbox.stub(
- element.$.restAPI, 'deleteComment').returns(Promise.resolve({}));
- sandbox.spy(element.confirmDeleteOverlay, 'open');
- element.changeNum = 42;
- element.patchNum = 0xDEADBEEF;
- element._isAdmin = true;
- assert.isTrue(element.shadowRoot
- .querySelector('.action.delete')
- .classList.contains('showDeleteButtons'));
- MockInteractions.tap(element.shadowRoot
- .querySelector('.action.delete'));
- flush(() => {
- element.confirmDeleteOverlay.open.lastCall.returnValue.then(() => {
- const dialog =
- window.confirmDeleteOverlay
- .querySelector('#confirmDeleteComment');
- dialog.message = 'removal reason';
- element._handleConfirmDeleteComment();
- assert.isTrue(element.$.restAPI.deleteComment.calledWith(
- 42, 0xDEADBEEF, 'baf0414d_60047215', 'removal reason'));
- done();
- });
+ assert.isFalse(element._handleSave.called);
});
});
- suite('draft update reporting', () => {
- let endStub;
- let getTimerStub;
- let mockEvent;
-
- setup(() => {
- mockEvent = {preventDefault() {}};
- sandbox.stub(element, 'save')
- .returns(Promise.resolve({}));
- sandbox.stub(element, '_discardDraft')
- .returns(Promise.resolve({}));
- endStub = sinon.stub();
- getTimerStub = sandbox.stub(element.$.reporting, 'getTimer')
- .returns({end: endStub});
- });
-
- test('create', () => {
- element.comment = {};
- return element._handleSave(mockEvent).then(() => {
- assert.isTrue(endStub.calledOnce);
- assert.isTrue(getTimerStub.calledOnce);
- assert.equal(getTimerStub.lastCall.args[0], 'CreateDraftComment');
- });
- });
-
- test('update', () => {
- element.comment = {id: 'abc_123'};
- return element._handleSave(mockEvent).then(() => {
- assert.isTrue(endStub.calledOnce);
- assert.isTrue(getTimerStub.calledOnce);
- assert.equal(getTimerStub.lastCall.args[0], 'UpdateDraftComment');
- });
- });
-
- test('discard', () => {
- element.comment = {id: 'abc_123'};
- sandbox.stub(element, '_closeConfirmDiscardOverlay');
- return element._handleConfirmDiscard(mockEvent).then(() => {
- assert.isTrue(endStub.calledOnce);
- assert.isTrue(getTimerStub.calledOnce);
- assert.equal(getTimerStub.lastCall.args[0], 'DiscardDraftComment');
- });
- });
+ test('esc does not close comment that has content', () => {
+ MockInteractions.pressAndReleaseKeyOn(
+ element.textarea, 27); // esc
+ assert.isFalse(element._handleCancel.called);
});
- test('edit reports interaction', () => {
- const reportStub = sandbox.stub(element.$.reporting,
- 'recordDraftInteraction');
- MockInteractions.tap(element.shadowRoot
- .querySelector('.edit'));
- assert.isTrue(reportStub.calledOnce);
+ test('ctrl+enter saves', () => {
+ MockInteractions.pressAndReleaseKeyOn(
+ element.textarea, 13, 'ctrl'); // ctrl + enter
+ assert.isTrue(element._handleSave.called);
});
- test('discard reports interaction', () => {
- const reportStub = sandbox.stub(element.$.reporting,
- 'recordDraftInteraction');
- element.draft = true;
- MockInteractions.tap(element.shadowRoot
- .querySelector('.discard'));
- assert.isTrue(reportStub.calledOnce);
+ test('meta+enter saves', () => {
+ MockInteractions.pressAndReleaseKeyOn(
+ element.textarea, 13, 'meta'); // meta + enter
+ assert.isTrue(element._handleSave.called);
+ });
+
+ test('ctrl+s saves', () => {
+ MockInteractions.pressAndReleaseKeyOn(
+ element.textarea, 83, 'ctrl'); // ctrl + s
+ assert.isTrue(element._handleSave.called);
+ });
+ });
+ test('delete comment button for non-admins is hidden', () => {
+ element._isAdmin = false;
+ assert.isFalse(element.shadowRoot
+ .querySelector('.action.delete')
+ .classList.contains('showDeleteButtons'));
+ });
+
+ test('delete comment button for admins with draft is hidden', () => {
+ element._isAdmin = false;
+ element.draft = true;
+ assert.isFalse(element.shadowRoot
+ .querySelector('.action.delete')
+ .classList.contains('showDeleteButtons'));
+ });
+
+ test('delete comment', done => {
+ sandbox.stub(
+ element.$.restAPI, 'deleteComment').returns(Promise.resolve({}));
+ sandbox.spy(element.confirmDeleteOverlay, 'open');
+ element.changeNum = 42;
+ element.patchNum = 0xDEADBEEF;
+ element._isAdmin = true;
+ assert.isTrue(element.shadowRoot
+ .querySelector('.action.delete')
+ .classList.contains('showDeleteButtons'));
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.action.delete'));
+ flush(() => {
+ element.confirmDeleteOverlay.open.lastCall.returnValue.then(() => {
+ const dialog =
+ window.confirmDeleteOverlay
+ .querySelector('#confirmDeleteComment');
+ dialog.message = 'removal reason';
+ element._handleConfirmDeleteComment();
+ assert.isTrue(element.$.restAPI.deleteComment.calledWith(
+ 42, 0xDEADBEEF, 'baf0414d_60047215', 'removal reason'));
+ done();
+ });
});
});
- suite('gr-comment draft tests', () => {
- let element;
- let sandbox;
+ suite('draft update reporting', () => {
+ let endStub;
+ let getTimerStub;
+ let mockEvent;
setup(() => {
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve(null); },
- saveDiffDraft() {
- return Promise.resolve({
- ok: true,
- text() {
- return Promise.resolve(
- ')]}\'\n{' +
- '"id": "baf0414d_40572e03",' +
- '"path": "/path/to/file",' +
- '"line": 5,' +
- '"updated": "2015-12-08 21:52:36.177000000",' +
- '"message": "saved!"' +
- '}'
- );
- },
- });
- },
- removeChangeReviewer() {
- return Promise.resolve({ok: true});
- },
- });
- stub('gr-storage', {
- getDraftComment() { return null; },
- });
- element = fixture('draft');
- element.changeNum = 42;
- element.patchNum = 1;
- element.editing = false;
- element.comment = {
- __commentSide: 'right',
- __draft: true,
- __draftID: 'temp_draft_id',
- path: '/path/to/file',
- line: 5,
- };
- element.commentSide = 'right';
- sandbox = sinon.sandbox.create();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('button visibility states', () => {
- element.showActions = false;
- assert.isTrue(element.shadowRoot
- .querySelector('.humanActions').hasAttribute('hidden'));
- assert.isTrue(element.shadowRoot
- .querySelector('.robotActions').hasAttribute('hidden'));
-
- element.showActions = true;
- assert.isFalse(element.shadowRoot
- .querySelector('.humanActions').hasAttribute('hidden'));
- assert.isTrue(element.shadowRoot
- .querySelector('.robotActions').hasAttribute('hidden'));
-
- element.draft = true;
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.edit')), 'edit is visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.discard')), 'discard is visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.save')), 'save is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.cancel')), 'cancel is not visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.resolve')), 'resolve is visible');
- assert.isFalse(element.shadowRoot
- .querySelector('.humanActions').hasAttribute('hidden'));
- assert.isTrue(element.shadowRoot
- .querySelector('.robotActions').hasAttribute('hidden'));
-
- element.editing = true;
- flushAsynchronousOperations();
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.edit')), 'edit is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.discard')), 'discard not visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.save')), 'save is visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.cancel')), 'cancel is visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.resolve')), 'resolve is visible');
- assert.isFalse(element.shadowRoot
- .querySelector('.humanActions').hasAttribute('hidden'));
- assert.isTrue(element.shadowRoot
- .querySelector('.robotActions').hasAttribute('hidden'));
-
- element.draft = false;
- element.editing = false;
- flushAsynchronousOperations();
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.edit')), 'edit is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.discard')),
- 'discard is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.save')), 'save is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.cancel')), 'cancel is not visible');
- assert.isFalse(element.shadowRoot
- .querySelector('.humanActions').hasAttribute('hidden'));
- assert.isTrue(element.shadowRoot
- .querySelector('.robotActions').hasAttribute('hidden'));
-
- element.comment.id = 'foo';
- element.draft = true;
- element.editing = true;
- flushAsynchronousOperations();
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.cancel')), 'cancel is visible');
- assert.isFalse(element.shadowRoot
- .querySelector('.humanActions').hasAttribute('hidden'));
- assert.isTrue(element.shadowRoot
- .querySelector('.robotActions').hasAttribute('hidden'));
-
- // Delete button is not hidden by default
- assert.isFalse(element.shadowRoot.querySelector('#deleteBtn').hidden);
-
- element.isRobotComment = true;
- element.draft = true;
- assert.isTrue(element.shadowRoot
- .querySelector('.humanActions').hasAttribute('hidden'));
- assert.isFalse(element.shadowRoot
- .querySelector('.robotActions').hasAttribute('hidden'));
-
- // It is not expected to see Robot comment drafts, but if they appear,
- // they will behave the same as non-drafts.
- element.draft = false;
- assert.isTrue(element.shadowRoot
- .querySelector('.humanActions').hasAttribute('hidden'));
- assert.isFalse(element.shadowRoot
- .querySelector('.robotActions').hasAttribute('hidden'));
-
- // A robot comment with run ID should display plain text.
- element.set(['comment', 'robot_run_id'], 'text');
- element.editing = false;
- element.collapsed = false;
- flushAsynchronousOperations();
- assert.isTrue(element.shadowRoot
- .querySelector('.robotRun.link').textContent === 'Run Details');
-
- // A robot comment with run ID and url should display a link.
- element.set(['comment', 'url'], '/path/to/run');
- flushAsynchronousOperations();
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('.robotRun.link')).display,
- 'none');
-
- // Delete button is hidden for robot comments
- assert.isTrue(element.shadowRoot.querySelector('#deleteBtn').hidden);
- });
-
- test('collapsible drafts', () => {
- assert.isTrue(element.collapsed);
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are not visible');
- assert.isNotOk(element.textarea, 'textarea is not visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is visible');
-
- MockInteractions.tap(element.$.header);
- assert.isFalse(element.collapsed);
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are visible');
- assert.isNotOk(element.textarea, 'textarea is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is is not visible');
-
- // When the edit button is pressed, should still see the actions
- // and also textarea
- MockInteractions.tap(element.shadowRoot
- .querySelector('.edit'));
- flushAsynchronousOperations();
- assert.isFalse(element.collapsed);
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is not visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are visible');
- assert.isTrue(isVisible(element.textarea), 'textarea is visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is not visible');
-
- // When toggle again, everything should be hidden except for textarea
- // and header middle content should be visible
- MockInteractions.tap(element.$.header);
- assert.isTrue(element.collapsed);
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('gr-textarea')),
- 'textarea is not visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is visible');
-
- // When toggle again, textarea should remain open in the state it was
- // before
- MockInteractions.tap(element.$.header);
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is not visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are visible');
- assert.isTrue(isVisible(element.textarea), 'textarea is visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is not visible');
- });
-
- test('robot comment layout', () => {
- const comment = Object.assign({
- robot_id: 'happy_robot_id',
- url: '/robot/comment',
- author: {
- name: 'Happy Robot',
- },
- }, element.comment);
- element.comment = comment;
- element.collapsed = false;
- flushAsynchronousOperations();
-
- let runIdMessage;
- runIdMessage = element.shadowRoot
- .querySelector('.runIdMessage');
- assert.isFalse(runIdMessage.hidden);
-
- const runDetailsLink = element.shadowRoot
- .querySelector('.robotRunLink');
- assert.isTrue(runDetailsLink.href.indexOf(element.comment.url) !== -1);
-
- const robotServiceName = element.shadowRoot
- .querySelector('.authorName');
- assert.isTrue(robotServiceName.textContent === 'happy_robot_id');
-
- const authorName = element.shadowRoot
- .querySelector('.robotId');
- assert.isTrue(authorName.innerText === 'Happy Robot');
-
- element.collapsed = true;
- flushAsynchronousOperations();
- runIdMessage = element.shadowRoot
- .querySelector('.runIdMessage');
- assert.isTrue(runIdMessage.hidden);
- });
-
- test('draft creation/cancellation', done => {
- assert.isFalse(element.editing);
- MockInteractions.tap(element.shadowRoot
- .querySelector('.edit'));
- assert.isTrue(element.editing);
-
- element._messageText = '';
- const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
-
- // Save should be disabled on an empty message.
- let disabled = element.shadowRoot
- .querySelector('.save').hasAttribute('disabled');
- assert.isTrue(disabled, 'save button should be disabled.');
- element._messageText = ' ';
- disabled = element.shadowRoot
- .querySelector('.save').hasAttribute('disabled');
- assert.isTrue(disabled, 'save button should be disabled.');
-
- const updateStub = sinon.stub();
- element.addEventListener('comment-update', updateStub);
-
- let numDiscardEvents = 0;
- element.addEventListener('comment-discard', e => {
- numDiscardEvents++;
- assert.isFalse(eraseMessageDraftSpy.called);
- if (numDiscardEvents === 2) {
- assert.isFalse(updateStub.called);
- done();
- }
- });
- MockInteractions.tap(element.shadowRoot
- .querySelector('.cancel'));
- element.flushDebouncer('fire-update');
- element._messageText = '';
- flushAsynchronousOperations();
- MockInteractions.pressAndReleaseKeyOn(element.textarea, 27); // esc
- });
-
- test('draft discard removes message from storage', done => {
- element._messageText = '';
- const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
- sandbox.stub(element, '_closeConfirmDiscardOverlay');
-
- element.addEventListener('comment-discard', e => {
- assert.isTrue(eraseMessageDraftSpy.called);
- done();
- });
- element._handleConfirmDiscard({preventDefault: sinon.stub()});
- });
-
- test('storage is cleared only after save success', () => {
- element._messageText = 'test';
- const eraseStub = sandbox.stub(element, '_eraseDraftComment');
- sandbox.stub(element.$.restAPI, 'getResponseObject')
+ mockEvent = {preventDefault() {}};
+ sandbox.stub(element, 'save')
.returns(Promise.resolve({}));
+ sandbox.stub(element, '_discardDraft')
+ .returns(Promise.resolve({}));
+ endStub = sinon.stub();
+ getTimerStub = sandbox.stub(element.$.reporting, 'getTimer')
+ .returns({end: endStub});
+ });
- sandbox.stub(element, '_saveDraft').returns(Promise.resolve({ok: false}));
+ test('create', () => {
+ element.comment = {};
+ return element._handleSave(mockEvent).then(() => {
+ assert.isTrue(endStub.calledOnce);
+ assert.isTrue(getTimerStub.calledOnce);
+ assert.equal(getTimerStub.lastCall.args[0], 'CreateDraftComment');
+ });
+ });
- const savePromise = element.save();
- assert.isFalse(eraseStub.called);
- return savePromise.then(() => {
- assert.isFalse(eraseStub.called);
+ test('update', () => {
+ element.comment = {id: 'abc_123'};
+ return element._handleSave(mockEvent).then(() => {
+ assert.isTrue(endStub.calledOnce);
+ assert.isTrue(getTimerStub.calledOnce);
+ assert.equal(getTimerStub.lastCall.args[0], 'UpdateDraftComment');
+ });
+ });
- element._saveDraft.restore();
- sandbox.stub(element, '_saveDraft')
- .returns(Promise.resolve({ok: true}));
- return element.save().then(() => {
- assert.isTrue(eraseStub.called);
+ test('discard', () => {
+ element.comment = {id: 'abc_123'};
+ sandbox.stub(element, '_closeConfirmDiscardOverlay');
+ return element._handleConfirmDiscard(mockEvent).then(() => {
+ assert.isTrue(endStub.calledOnce);
+ assert.isTrue(getTimerStub.calledOnce);
+ assert.equal(getTimerStub.lastCall.args[0], 'DiscardDraftComment');
+ });
+ });
+ });
+
+ test('edit reports interaction', () => {
+ const reportStub = sandbox.stub(element.$.reporting,
+ 'recordDraftInteraction');
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.edit'));
+ assert.isTrue(reportStub.calledOnce);
+ });
+
+ test('discard reports interaction', () => {
+ const reportStub = sandbox.stub(element.$.reporting,
+ 'recordDraftInteraction');
+ element.draft = true;
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.discard'));
+ assert.isTrue(reportStub.calledOnce);
+ });
+ });
+
+ suite('gr-comment draft tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve(null); },
+ saveDiffDraft() {
+ return Promise.resolve({
+ ok: true,
+ text() {
+ return Promise.resolve(
+ ')]}\'\n{' +
+ '"id": "baf0414d_40572e03",' +
+ '"path": "/path/to/file",' +
+ '"line": 5,' +
+ '"updated": "2015-12-08 21:52:36.177000000",' +
+ '"message": "saved!"' +
+ '}'
+ );
+ },
});
- });
+ },
+ removeChangeReviewer() {
+ return Promise.resolve({ok: true});
+ },
});
-
- test('_computeSaveDisabled', () => {
- const comment = {unresolved: true};
- const msgComment = {message: 'test', unresolved: true};
- assert.equal(element._computeSaveDisabled('', comment, false), true);
- assert.equal(element._computeSaveDisabled('test', comment, false), false);
- assert.equal(element._computeSaveDisabled('', msgComment, false), true);
- assert.equal(
- element._computeSaveDisabled('test', msgComment, false), false);
- assert.equal(
- element._computeSaveDisabled('test2', msgComment, false), false);
- assert.equal(element._computeSaveDisabled('test', comment, true), false);
- assert.equal(element._computeSaveDisabled('', comment, true), true);
- assert.equal(element._computeSaveDisabled('', comment, false), true);
+ stub('gr-storage', {
+ getDraftComment() { return null; },
});
+ element = fixture('draft');
+ element.changeNum = 42;
+ element.patchNum = 1;
+ element.editing = false;
+ element.comment = {
+ __commentSide: 'right',
+ __draft: true,
+ __draftID: 'temp_draft_id',
+ path: '/path/to/file',
+ line: 5,
+ };
+ element.commentSide = 'right';
+ sandbox = sinon.sandbox.create();
+ });
- suite('confirm discard', () => {
- let discardStub;
- let overlayStub;
- let mockEvent;
+ teardown(() => {
+ sandbox.restore();
+ });
- setup(() => {
- discardStub = sandbox.stub(element, '_discardDraft');
- overlayStub = sandbox.stub(element, '_openOverlay')
- .returns(Promise.resolve());
- mockEvent = {preventDefault: sinon.stub()};
- });
+ test('button visibility states', () => {
+ element.showActions = false;
+ assert.isTrue(element.shadowRoot
+ .querySelector('.humanActions').hasAttribute('hidden'));
+ assert.isTrue(element.shadowRoot
+ .querySelector('.robotActions').hasAttribute('hidden'));
- test('confirms discard of comments with message text', () => {
- element._messageText = 'test';
- element._handleDiscard(mockEvent);
- assert.isTrue(overlayStub.calledWith(element.confirmDiscardOverlay));
- assert.isFalse(discardStub.called);
- });
+ element.showActions = true;
+ assert.isFalse(element.shadowRoot
+ .querySelector('.humanActions').hasAttribute('hidden'));
+ assert.isTrue(element.shadowRoot
+ .querySelector('.robotActions').hasAttribute('hidden'));
- test('no confirmation for comments without message text', () => {
- element._messageText = '';
- element._handleDiscard(mockEvent);
- assert.isFalse(overlayStub.called);
- assert.isTrue(discardStub.calledOnce);
- });
- });
+ element.draft = true;
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.edit')), 'edit is visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.discard')), 'discard is visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.save')), 'save is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.cancel')), 'cancel is not visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.resolve')), 'resolve is visible');
+ assert.isFalse(element.shadowRoot
+ .querySelector('.humanActions').hasAttribute('hidden'));
+ assert.isTrue(element.shadowRoot
+ .querySelector('.robotActions').hasAttribute('hidden'));
- test('ctrl+s saves comment', done => {
- const stub = sinon.stub(element, 'save', () => {
- assert.isTrue(stub.called);
- stub.restore();
+ element.editing = true;
+ flushAsynchronousOperations();
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.edit')), 'edit is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.discard')), 'discard not visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.save')), 'save is visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.cancel')), 'cancel is visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.resolve')), 'resolve is visible');
+ assert.isFalse(element.shadowRoot
+ .querySelector('.humanActions').hasAttribute('hidden'));
+ assert.isTrue(element.shadowRoot
+ .querySelector('.robotActions').hasAttribute('hidden'));
+
+ element.draft = false;
+ element.editing = false;
+ flushAsynchronousOperations();
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.edit')), 'edit is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.discard')),
+ 'discard is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.save')), 'save is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.cancel')), 'cancel is not visible');
+ assert.isFalse(element.shadowRoot
+ .querySelector('.humanActions').hasAttribute('hidden'));
+ assert.isTrue(element.shadowRoot
+ .querySelector('.robotActions').hasAttribute('hidden'));
+
+ element.comment.id = 'foo';
+ element.draft = true;
+ element.editing = true;
+ flushAsynchronousOperations();
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.cancel')), 'cancel is visible');
+ assert.isFalse(element.shadowRoot
+ .querySelector('.humanActions').hasAttribute('hidden'));
+ assert.isTrue(element.shadowRoot
+ .querySelector('.robotActions').hasAttribute('hidden'));
+
+ // Delete button is not hidden by default
+ assert.isFalse(element.shadowRoot.querySelector('#deleteBtn').hidden);
+
+ element.isRobotComment = true;
+ element.draft = true;
+ assert.isTrue(element.shadowRoot
+ .querySelector('.humanActions').hasAttribute('hidden'));
+ assert.isFalse(element.shadowRoot
+ .querySelector('.robotActions').hasAttribute('hidden'));
+
+ // It is not expected to see Robot comment drafts, but if they appear,
+ // they will behave the same as non-drafts.
+ element.draft = false;
+ assert.isTrue(element.shadowRoot
+ .querySelector('.humanActions').hasAttribute('hidden'));
+ assert.isFalse(element.shadowRoot
+ .querySelector('.robotActions').hasAttribute('hidden'));
+
+ // A robot comment with run ID should display plain text.
+ element.set(['comment', 'robot_run_id'], 'text');
+ element.editing = false;
+ element.collapsed = false;
+ flushAsynchronousOperations();
+ assert.isTrue(element.shadowRoot
+ .querySelector('.robotRun.link').textContent === 'Run Details');
+
+ // A robot comment with run ID and url should display a link.
+ element.set(['comment', 'url'], '/path/to/run');
+ flushAsynchronousOperations();
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('.robotRun.link')).display,
+ 'none');
+
+ // Delete button is hidden for robot comments
+ assert.isTrue(element.shadowRoot.querySelector('#deleteBtn').hidden);
+ });
+
+ test('collapsible drafts', () => {
+ assert.isTrue(element.collapsed);
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are not visible');
+ assert.isNotOk(element.textarea, 'textarea is not visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is visible');
+
+ MockInteractions.tap(element.$.header);
+ assert.isFalse(element.collapsed);
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are visible');
+ assert.isNotOk(element.textarea, 'textarea is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is is not visible');
+
+ // When the edit button is pressed, should still see the actions
+ // and also textarea
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.edit'));
+ flushAsynchronousOperations();
+ assert.isFalse(element.collapsed);
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is not visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are visible');
+ assert.isTrue(isVisible(element.textarea), 'textarea is visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is not visible');
+
+ // When toggle again, everything should be hidden except for textarea
+ // and header middle content should be visible
+ MockInteractions.tap(element.$.header);
+ assert.isTrue(element.collapsed);
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('gr-textarea')),
+ 'textarea is not visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is visible');
+
+ // When toggle again, textarea should remain open in the state it was
+ // before
+ MockInteractions.tap(element.$.header);
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is not visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are visible');
+ assert.isTrue(isVisible(element.textarea), 'textarea is visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is not visible');
+ });
+
+ test('robot comment layout', () => {
+ const comment = Object.assign({
+ robot_id: 'happy_robot_id',
+ url: '/robot/comment',
+ author: {
+ name: 'Happy Robot',
+ },
+ }, element.comment);
+ element.comment = comment;
+ element.collapsed = false;
+ flushAsynchronousOperations();
+
+ let runIdMessage;
+ runIdMessage = element.shadowRoot
+ .querySelector('.runIdMessage');
+ assert.isFalse(runIdMessage.hidden);
+
+ const runDetailsLink = element.shadowRoot
+ .querySelector('.robotRunLink');
+ assert.isTrue(runDetailsLink.href.indexOf(element.comment.url) !== -1);
+
+ const robotServiceName = element.shadowRoot
+ .querySelector('.authorName');
+ assert.isTrue(robotServiceName.textContent === 'happy_robot_id');
+
+ const authorName = element.shadowRoot
+ .querySelector('.robotId');
+ assert.isTrue(authorName.innerText === 'Happy Robot');
+
+ element.collapsed = true;
+ flushAsynchronousOperations();
+ runIdMessage = element.shadowRoot
+ .querySelector('.runIdMessage');
+ assert.isTrue(runIdMessage.hidden);
+ });
+
+ test('draft creation/cancellation', done => {
+ assert.isFalse(element.editing);
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.edit'));
+ assert.isTrue(element.editing);
+
+ element._messageText = '';
+ const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
+
+ // Save should be disabled on an empty message.
+ let disabled = element.shadowRoot
+ .querySelector('.save').hasAttribute('disabled');
+ assert.isTrue(disabled, 'save button should be disabled.');
+ element._messageText = ' ';
+ disabled = element.shadowRoot
+ .querySelector('.save').hasAttribute('disabled');
+ assert.isTrue(disabled, 'save button should be disabled.');
+
+ const updateStub = sinon.stub();
+ element.addEventListener('comment-update', updateStub);
+
+ let numDiscardEvents = 0;
+ element.addEventListener('comment-discard', e => {
+ numDiscardEvents++;
+ assert.isFalse(eraseMessageDraftSpy.called);
+ if (numDiscardEvents === 2) {
+ assert.isFalse(updateStub.called);
done();
- return Promise.resolve();
+ }
+ });
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.cancel'));
+ element.flushDebouncer('fire-update');
+ element._messageText = '';
+ flushAsynchronousOperations();
+ MockInteractions.pressAndReleaseKeyOn(element.textarea, 27); // esc
+ });
+
+ test('draft discard removes message from storage', done => {
+ element._messageText = '';
+ const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
+ sandbox.stub(element, '_closeConfirmDiscardOverlay');
+
+ element.addEventListener('comment-discard', e => {
+ assert.isTrue(eraseMessageDraftSpy.called);
+ done();
+ });
+ element._handleConfirmDiscard({preventDefault: sinon.stub()});
+ });
+
+ test('storage is cleared only after save success', () => {
+ element._messageText = 'test';
+ const eraseStub = sandbox.stub(element, '_eraseDraftComment');
+ sandbox.stub(element.$.restAPI, 'getResponseObject')
+ .returns(Promise.resolve({}));
+
+ sandbox.stub(element, '_saveDraft').returns(Promise.resolve({ok: false}));
+
+ const savePromise = element.save();
+ assert.isFalse(eraseStub.called);
+ return savePromise.then(() => {
+ assert.isFalse(eraseStub.called);
+
+ element._saveDraft.restore();
+ sandbox.stub(element, '_saveDraft')
+ .returns(Promise.resolve({ok: true}));
+ return element.save().then(() => {
+ assert.isTrue(eraseStub.called);
});
- element._messageText = 'is that the horse from horsing around??';
- element.editing = true;
- flushAsynchronousOperations();
- MockInteractions.pressAndReleaseKeyOn(
- element.textarea.$.textarea.textarea,
- 83, 'ctrl'); // 'ctrl + s'
+ });
+ });
+
+ test('_computeSaveDisabled', () => {
+ const comment = {unresolved: true};
+ const msgComment = {message: 'test', unresolved: true};
+ assert.equal(element._computeSaveDisabled('', comment, false), true);
+ assert.equal(element._computeSaveDisabled('test', comment, false), false);
+ assert.equal(element._computeSaveDisabled('', msgComment, false), true);
+ assert.equal(
+ element._computeSaveDisabled('test', msgComment, false), false);
+ assert.equal(
+ element._computeSaveDisabled('test2', msgComment, false), false);
+ assert.equal(element._computeSaveDisabled('test', comment, true), false);
+ assert.equal(element._computeSaveDisabled('', comment, true), true);
+ assert.equal(element._computeSaveDisabled('', comment, false), true);
+ });
+
+ suite('confirm discard', () => {
+ let discardStub;
+ let overlayStub;
+ let mockEvent;
+
+ setup(() => {
+ discardStub = sandbox.stub(element, '_discardDraft');
+ overlayStub = sandbox.stub(element, '_openOverlay')
+ .returns(Promise.resolve());
+ mockEvent = {preventDefault: sinon.stub()};
});
- test('draft saving/editing', done => {
- const fireStub = sinon.stub(element, 'fire');
- const cancelDebounce = sandbox.stub(element, 'cancelDebouncer');
+ test('confirms discard of comments with message text', () => {
+ element._messageText = 'test';
+ element._handleDiscard(mockEvent);
+ assert.isTrue(overlayStub.calledWith(element.confirmDiscardOverlay));
+ assert.isFalse(discardStub.called);
+ });
- element.draft = true;
+ test('no confirmation for comments without message text', () => {
+ element._messageText = '';
+ element._handleDiscard(mockEvent);
+ assert.isFalse(overlayStub.called);
+ assert.isTrue(discardStub.calledOnce);
+ });
+ });
+
+ test('ctrl+s saves comment', done => {
+ const stub = sinon.stub(element, 'save', () => {
+ assert.isTrue(stub.called);
+ stub.restore();
+ done();
+ return Promise.resolve();
+ });
+ element._messageText = 'is that the horse from horsing around??';
+ element.editing = true;
+ flushAsynchronousOperations();
+ MockInteractions.pressAndReleaseKeyOn(
+ element.textarea.$.textarea.textarea,
+ 83, 'ctrl'); // 'ctrl + s'
+ });
+
+ test('draft saving/editing', done => {
+ const fireStub = sinon.stub(element, 'fire');
+ const cancelDebounce = sandbox.stub(element, 'cancelDebouncer');
+
+ element.draft = true;
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.edit'));
+ element._messageText = 'good news, everyone!';
+ element.flushDebouncer('fire-update');
+ element.flushDebouncer('store');
+ assert(fireStub.calledWith('comment-update'),
+ 'comment-update should be sent');
+ assert.isTrue(fireStub.calledOnce);
+
+ element._messageText = 'good news, everyone!';
+ element.flushDebouncer('fire-update');
+ element.flushDebouncer('store');
+ assert.isTrue(fireStub.calledOnce,
+ 'No events should fire for text editing');
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.save'));
+
+ assert.isTrue(element.disabled,
+ 'Element should be disabled when creating draft.');
+
+ element._xhrPromise.then(draft => {
+ assert(fireStub.calledWith('comment-save'),
+ 'comment-save should be sent');
+ assert(cancelDebounce.calledWith('store'));
+
+ assert.deepEqual(fireStub.lastCall.args[1], {
+ comment: {
+ __commentSide: 'right',
+ __draft: true,
+ __draftID: 'temp_draft_id',
+ id: 'baf0414d_40572e03',
+ line: 5,
+ message: 'saved!',
+ path: '/path/to/file',
+ updated: '2015-12-08 21:52:36.177000000',
+ },
+ patchNum: 1,
+ });
+ assert.isFalse(element.disabled,
+ 'Element should be enabled when done creating draft.');
+ assert.equal(draft.message, 'saved!');
+ assert.isFalse(element.editing);
+ }).then(() => {
MockInteractions.tap(element.shadowRoot
.querySelector('.edit'));
- element._messageText = 'good news, everyone!';
- element.flushDebouncer('fire-update');
- element.flushDebouncer('store');
- assert(fireStub.calledWith('comment-update'),
- 'comment-update should be sent');
- assert.isTrue(fireStub.calledOnce);
-
- element._messageText = 'good news, everyone!';
- element.flushDebouncer('fire-update');
- element.flushDebouncer('store');
- assert.isTrue(fireStub.calledOnce,
- 'No events should fire for text editing');
-
+ element._messageText = 'You’ll be delivering a package to Chapek 9, ' +
+ 'a world where humans are killed on sight.';
MockInteractions.tap(element.shadowRoot
.querySelector('.save'));
-
assert.isTrue(element.disabled,
- 'Element should be disabled when creating draft.');
+ 'Element should be disabled when updating draft.');
element._xhrPromise.then(draft => {
- assert(fireStub.calledWith('comment-save'),
- 'comment-save should be sent');
- assert(cancelDebounce.calledWith('store'));
-
- assert.deepEqual(fireStub.lastCall.args[1], {
- comment: {
- __commentSide: 'right',
- __draft: true,
- __draftID: 'temp_draft_id',
- id: 'baf0414d_40572e03',
- line: 5,
- message: 'saved!',
- path: '/path/to/file',
- updated: '2015-12-08 21:52:36.177000000',
- },
- patchNum: 1,
- });
assert.isFalse(element.disabled,
- 'Element should be enabled when done creating draft.');
+ 'Element should be enabled when done updating draft.');
assert.equal(draft.message, 'saved!');
assert.isFalse(element.editing);
- }).then(() => {
- MockInteractions.tap(element.shadowRoot
- .querySelector('.edit'));
- element._messageText = 'You’ll be delivering a package to Chapek 9, ' +
- 'a world where humans are killed on sight.';
- MockInteractions.tap(element.shadowRoot
- .querySelector('.save'));
- assert.isTrue(element.disabled,
- 'Element should be disabled when updating draft.');
-
- element._xhrPromise.then(draft => {
- assert.isFalse(element.disabled,
- 'Element should be enabled when done updating draft.');
- assert.equal(draft.message, 'saved!');
- assert.isFalse(element.editing);
- fireStub.restore();
- done();
- });
- });
- });
-
- test('draft prevent save when disabled', () => {
- const saveStub = sandbox.stub(element, 'save').returns(Promise.resolve());
- element.showActions = true;
- element.draft = true;
- MockInteractions.tap(element.$.header);
- MockInteractions.tap(element.shadowRoot
- .querySelector('.edit'));
- element._messageText = 'good news, everyone!';
- element.flushDebouncer('fire-update');
- element.flushDebouncer('store');
-
- element.disabled = true;
- MockInteractions.tap(element.shadowRoot
- .querySelector('.save'));
- assert.isFalse(saveStub.called);
-
- element.disabled = false;
- MockInteractions.tap(element.shadowRoot
- .querySelector('.save'));
- assert.isTrue(saveStub.calledOnce);
- });
-
- test('proper event fires on resolve, comment is not saved', done => {
- const save = sandbox.stub(element, 'save');
- element.addEventListener('comment-update', e => {
- assert.isTrue(e.detail.comment.unresolved);
- assert.isFalse(save.called);
+ fireStub.restore();
done();
});
- MockInteractions.tap(element.shadowRoot
- .querySelector('.resolve input'));
- });
-
- test('resolved comment state indicated by checkbox', () => {
- sandbox.stub(element, 'save');
- element.comment = {unresolved: false};
- assert.isTrue(element.shadowRoot
- .querySelector('.resolve input').checked);
- element.comment = {unresolved: true};
- assert.isFalse(element.shadowRoot
- .querySelector('.resolve input').checked);
- });
-
- test('resolved checkbox saves with tap when !editing', () => {
- element.editing = false;
- const save = sandbox.stub(element, 'save');
-
- element.comment = {unresolved: false};
- assert.isTrue(element.shadowRoot
- .querySelector('.resolve input').checked);
- element.comment = {unresolved: true};
- assert.isFalse(element.shadowRoot
- .querySelector('.resolve input').checked);
- assert.isFalse(save.called);
- MockInteractions.tap(element.$.resolvedCheckbox);
- assert.isTrue(element.shadowRoot
- .querySelector('.resolve input').checked);
- assert.isTrue(save.called);
- });
-
- suite('draft saving messages', () => {
- test('_getSavingMessage', () => {
- assert.equal(element._getSavingMessage(0), 'All changes saved');
- assert.equal(element._getSavingMessage(1), 'Saving 1 draft...');
- assert.equal(element._getSavingMessage(2), 'Saving 2 drafts...');
- assert.equal(element._getSavingMessage(3), 'Saving 3 drafts...');
- });
-
- test('_show{Start,End}Request', () => {
- const updateStub = sandbox.stub(element, '_updateRequestToast');
- element._numPendingDraftRequests.number = 1;
-
- element._showStartRequest();
- assert.isTrue(updateStub.calledOnce);
- assert.equal(updateStub.lastCall.args[0], 2);
- assert.equal(element._numPendingDraftRequests.number, 2);
-
- element._showEndRequest();
- assert.isTrue(updateStub.calledTwice);
- assert.equal(updateStub.lastCall.args[0], 1);
- assert.equal(element._numPendingDraftRequests.number, 1);
-
- element._showEndRequest();
- assert.isTrue(updateStub.calledThrice);
- assert.equal(updateStub.lastCall.args[0], 0);
- assert.equal(element._numPendingDraftRequests.number, 0);
- });
- });
-
- test('cancelling an unsaved draft discards, persists in storage', () => {
- const discardSpy = sandbox.spy(element, '_fireDiscard');
- const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
- const eraseStub = sandbox.stub(element.$.storage, 'eraseDraftComment');
- element._messageText = 'test text';
- flushAsynchronousOperations();
- element.flushDebouncer('store');
-
- assert.isTrue(storeStub.called);
- assert.equal(storeStub.lastCall.args[1], 'test text');
- element._handleCancel({preventDefault: () => {}});
- assert.isTrue(discardSpy.called);
- assert.isFalse(eraseStub.called);
- });
-
- test('cancelling edit on a saved draft does not store', () => {
- element.comment.id = 'foo';
- const discardSpy = sandbox.spy(element, '_fireDiscard');
- const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
- element._messageText = 'test text';
- flushAsynchronousOperations();
- element.flushDebouncer('store');
-
- assert.isFalse(storeStub.called);
- element._handleCancel({preventDefault: () => {}});
- assert.isTrue(discardSpy.called);
- });
-
- test('deleting text from saved draft and saving deletes the draft', () => {
- element.comment = {id: 'foo', message: 'test'};
- element._messageText = '';
- const discardStub = sandbox.stub(element, '_discardDraft');
-
- element.save();
- assert.isTrue(discardStub.called);
- });
-
- test('_handleFix fires create-fix event', done => {
- element.addEventListener('create-fix-comment', e => {
- assert.deepEqual(e.detail, element._getEventPayload());
- done();
- });
- element.isRobotComment = true;
- element.comments = [element.comment];
- flushAsynchronousOperations();
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('.fix'));
- });
-
- test('do not show Please Fix button if human reply exists', () => {
- element.comments = [
- {
- robot_id: 'happy_robot_id',
- robot_run_id: '5838406743490560',
- fix_suggestions: [
- {
- fix_id: '478ff847_3bf47aaf',
- description: 'Make the smiley happier by giving it a nose.',
- replacements: [
- {
- path: 'Documentation/config-gerrit.txt',
- range: {
- start_line: 10,
- start_character: 7,
- end_line: 10,
- end_character: 9,
- },
- replacement: ':-)',
- },
- ],
- },
- ],
- author: {
- _account_id: 1030912,
- name: 'Alice Kober-Sotzek',
- email: 'aliceks@google.com',
- avatars: [
- {
- url: '/s32-p/photo.jpg',
- height: 32,
- },
- {
- url: '/AaAdOFzPlFI/s56-p/photo.jpg',
- height: 56,
- },
- {
- url: '/AaAdOFzPlFI/s100-p/photo.jpg',
- height: 100,
- },
- {
- url: '/AaAdOFzPlFI/s120-p/photo.jpg',
- height: 120,
- },
- ],
- },
- patch_set: 1,
- id: 'eb0d03fd_5e95904f',
- line: 10,
- updated: '2017-04-04 15:36:17.000000000',
- message: 'This is a robot comment with a fix.',
- unresolved: false,
- __commentSide: 'right',
- collapsed: false,
- },
- {
- __draft: true,
- __draftID: '0.wbrfbwj89sa',
- __date: '2019-12-04T13:41:03.689Z',
- path: 'Documentation/config-gerrit.txt',
- patchNum: 1,
- side: 'REVISION',
- __commentSide: 'right',
- line: 10,
- in_reply_to: 'eb0d03fd_5e95904f',
- message: '> This is a robot comment with a fix.\n\nPlease fix.',
- unresolved: true,
- },
- ];
- element.comment = element.comments[0];
- flushAsynchronousOperations();
- assert.isNull(element.shadowRoot
- .querySelector('robotActions gr-button'));
- });
-
- test('show Please Fix if no human reply', () => {
- element.comments = [
- {
- robot_id: 'happy_robot_id',
- robot_run_id: '5838406743490560',
- fix_suggestions: [
- {
- fix_id: '478ff847_3bf47aaf',
- description: 'Make the smiley happier by giving it a nose.',
- replacements: [
- {
- path: 'Documentation/config-gerrit.txt',
- range: {
- start_line: 10,
- start_character: 7,
- end_line: 10,
- end_character: 9,
- },
- replacement: ':-)',
- },
- ],
- },
- ],
- author: {
- _account_id: 1030912,
- name: 'Alice Kober-Sotzek',
- email: 'aliceks@google.com',
- avatars: [
- {
- url: '/s32-p/photo.jpg',
- height: 32,
- },
- {
- url: '/AaAdOFzPlFI/s56-p/photo.jpg',
- height: 56,
- },
- {
- url: '/AaAdOFzPlFI/s100-p/photo.jpg',
- height: 100,
- },
- {
- url: '/AaAdOFzPlFI/s120-p/photo.jpg',
- height: 120,
- },
- ],
- },
- patch_set: 1,
- id: 'eb0d03fd_5e95904f',
- line: 10,
- updated: '2017-04-04 15:36:17.000000000',
- message: 'This is a robot comment with a fix.',
- unresolved: false,
- __commentSide: 'right',
- collapsed: false,
- },
- ];
- element.comment = element.comments[0];
- flushAsynchronousOperations();
- assert.isNotNull(element.shadowRoot
- .querySelector('.robotActions gr-button'));
- });
-
- test('_handleShowFix fires open-fix-preview event', done => {
- element.addEventListener('open-fix-preview', e => {
- assert.deepEqual(e.detail, element._getEventPayload());
- done();
- });
- element.comment = {fix_suggestions: [{}]};
- element.isRobotComment = true;
- flushAsynchronousOperations();
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('.show-fix'));
});
});
- suite('respectful tips', () => {
- let element;
- let sandbox;
- let clock;
- setup(() => {
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve(null); },
- });
- clock = sinon.useFakeTimers();
- sandbox = sinon.sandbox.create();
+ test('draft prevent save when disabled', () => {
+ const saveStub = sandbox.stub(element, 'save').returns(Promise.resolve());
+ element.showActions = true;
+ element.draft = true;
+ MockInteractions.tap(element.$.header);
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.edit'));
+ element._messageText = 'good news, everyone!';
+ element.flushDebouncer('fire-update');
+ element.flushDebouncer('store');
+
+ element.disabled = true;
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.save'));
+ assert.isFalse(saveStub.called);
+
+ element.disabled = false;
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.save'));
+ assert.isTrue(saveStub.calledOnce);
+ });
+
+ test('proper event fires on resolve, comment is not saved', done => {
+ const save = sandbox.stub(element, 'save');
+ element.addEventListener('comment-update', e => {
+ assert.isTrue(e.detail.comment.unresolved);
+ assert.isFalse(save.called);
+ done();
+ });
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.resolve input'));
+ });
+
+ test('resolved comment state indicated by checkbox', () => {
+ sandbox.stub(element, 'save');
+ element.comment = {unresolved: false};
+ assert.isTrue(element.shadowRoot
+ .querySelector('.resolve input').checked);
+ element.comment = {unresolved: true};
+ assert.isFalse(element.shadowRoot
+ .querySelector('.resolve input').checked);
+ });
+
+ test('resolved checkbox saves with tap when !editing', () => {
+ element.editing = false;
+ const save = sandbox.stub(element, 'save');
+
+ element.comment = {unresolved: false};
+ assert.isTrue(element.shadowRoot
+ .querySelector('.resolve input').checked);
+ element.comment = {unresolved: true};
+ assert.isFalse(element.shadowRoot
+ .querySelector('.resolve input').checked);
+ assert.isFalse(save.called);
+ MockInteractions.tap(element.$.resolvedCheckbox);
+ assert.isTrue(element.shadowRoot
+ .querySelector('.resolve input').checked);
+ assert.isTrue(save.called);
+ });
+
+ suite('draft saving messages', () => {
+ test('_getSavingMessage', () => {
+ assert.equal(element._getSavingMessage(0), 'All changes saved');
+ assert.equal(element._getSavingMessage(1), 'Saving 1 draft...');
+ assert.equal(element._getSavingMessage(2), 'Saving 2 drafts...');
+ assert.equal(element._getSavingMessage(3), 'Saving 3 drafts...');
});
- teardown(() => {
- clock.restore();
- sandbox.restore();
- });
+ test('_show{Start,End}Request', () => {
+ const updateStub = sandbox.stub(element, '_updateRequestToast');
+ element._numPendingDraftRequests.number = 1;
- test('show tip when no cached record', done => {
- // fake stub for storage
- const respectfulGetStub = sinon.stub();
- const respectfulSetStub = sinon.stub();
- stub('gr-storage', {
- getRespectfulTipVisibility() { return respectfulGetStub(); },
- setRespectfulTipVisibility() { return respectfulSetStub(); },
- });
- respectfulGetStub.returns(null);
- element = fixture('draft');
- // fake random
- element.getRandomNum = () => 0;
- element.comment = {__editing: true};
+ element._showStartRequest();
+ assert.isTrue(updateStub.calledOnce);
+ assert.equal(updateStub.lastCall.args[0], 2);
+ assert.equal(element._numPendingDraftRequests.number, 2);
+
+ element._showEndRequest();
+ assert.isTrue(updateStub.calledTwice);
+ assert.equal(updateStub.lastCall.args[0], 1);
+ assert.equal(element._numPendingDraftRequests.number, 1);
+
+ element._showEndRequest();
+ assert.isTrue(updateStub.calledThrice);
+ assert.equal(updateStub.lastCall.args[0], 0);
+ assert.equal(element._numPendingDraftRequests.number, 0);
+ });
+ });
+
+ test('cancelling an unsaved draft discards, persists in storage', () => {
+ const discardSpy = sandbox.spy(element, '_fireDiscard');
+ const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
+ const eraseStub = sandbox.stub(element.$.storage, 'eraseDraftComment');
+ element._messageText = 'test text';
+ flushAsynchronousOperations();
+ element.flushDebouncer('store');
+
+ assert.isTrue(storeStub.called);
+ assert.equal(storeStub.lastCall.args[1], 'test text');
+ element._handleCancel({preventDefault: () => {}});
+ assert.isTrue(discardSpy.called);
+ assert.isFalse(eraseStub.called);
+ });
+
+ test('cancelling edit on a saved draft does not store', () => {
+ element.comment.id = 'foo';
+ const discardSpy = sandbox.spy(element, '_fireDiscard');
+ const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
+ element._messageText = 'test text';
+ flushAsynchronousOperations();
+ element.flushDebouncer('store');
+
+ assert.isFalse(storeStub.called);
+ element._handleCancel({preventDefault: () => {}});
+ assert.isTrue(discardSpy.called);
+ });
+
+ test('deleting text from saved draft and saving deletes the draft', () => {
+ element.comment = {id: 'foo', message: 'test'};
+ element._messageText = '';
+ const discardStub = sandbox.stub(element, '_discardDraft');
+
+ element.save();
+ assert.isTrue(discardStub.called);
+ });
+
+ test('_handleFix fires create-fix event', done => {
+ element.addEventListener('create-fix-comment', e => {
+ assert.deepEqual(e.detail, element._getEventPayload());
+ done();
+ });
+ element.isRobotComment = true;
+ element.comments = [element.comment];
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.fix'));
+ });
+
+ test('do not show Please Fix button if human reply exists', () => {
+ element.comments = [
+ {
+ robot_id: 'happy_robot_id',
+ robot_run_id: '5838406743490560',
+ fix_suggestions: [
+ {
+ fix_id: '478ff847_3bf47aaf',
+ description: 'Make the smiley happier by giving it a nose.',
+ replacements: [
+ {
+ path: 'Documentation/config-gerrit.txt',
+ range: {
+ start_line: 10,
+ start_character: 7,
+ end_line: 10,
+ end_character: 9,
+ },
+ replacement: ':-)',
+ },
+ ],
+ },
+ ],
+ author: {
+ _account_id: 1030912,
+ name: 'Alice Kober-Sotzek',
+ email: 'aliceks@google.com',
+ avatars: [
+ {
+ url: '/s32-p/photo.jpg',
+ height: 32,
+ },
+ {
+ url: '/AaAdOFzPlFI/s56-p/photo.jpg',
+ height: 56,
+ },
+ {
+ url: '/AaAdOFzPlFI/s100-p/photo.jpg',
+ height: 100,
+ },
+ {
+ url: '/AaAdOFzPlFI/s120-p/photo.jpg',
+ height: 120,
+ },
+ ],
+ },
+ patch_set: 1,
+ id: 'eb0d03fd_5e95904f',
+ line: 10,
+ updated: '2017-04-04 15:36:17.000000000',
+ message: 'This is a robot comment with a fix.',
+ unresolved: false,
+ __commentSide: 'right',
+ collapsed: false,
+ },
+ {
+ __draft: true,
+ __draftID: '0.wbrfbwj89sa',
+ __date: '2019-12-04T13:41:03.689Z',
+ path: 'Documentation/config-gerrit.txt',
+ patchNum: 1,
+ side: 'REVISION',
+ __commentSide: 'right',
+ line: 10,
+ in_reply_to: 'eb0d03fd_5e95904f',
+ message: '> This is a robot comment with a fix.\n\nPlease fix.',
+ unresolved: true,
+ },
+ ];
+ element.comment = element.comments[0];
+ flushAsynchronousOperations();
+ assert.isNull(element.shadowRoot
+ .querySelector('robotActions gr-button'));
+ });
+
+ test('show Please Fix if no human reply', () => {
+ element.comments = [
+ {
+ robot_id: 'happy_robot_id',
+ robot_run_id: '5838406743490560',
+ fix_suggestions: [
+ {
+ fix_id: '478ff847_3bf47aaf',
+ description: 'Make the smiley happier by giving it a nose.',
+ replacements: [
+ {
+ path: 'Documentation/config-gerrit.txt',
+ range: {
+ start_line: 10,
+ start_character: 7,
+ end_line: 10,
+ end_character: 9,
+ },
+ replacement: ':-)',
+ },
+ ],
+ },
+ ],
+ author: {
+ _account_id: 1030912,
+ name: 'Alice Kober-Sotzek',
+ email: 'aliceks@google.com',
+ avatars: [
+ {
+ url: '/s32-p/photo.jpg',
+ height: 32,
+ },
+ {
+ url: '/AaAdOFzPlFI/s56-p/photo.jpg',
+ height: 56,
+ },
+ {
+ url: '/AaAdOFzPlFI/s100-p/photo.jpg',
+ height: 100,
+ },
+ {
+ url: '/AaAdOFzPlFI/s120-p/photo.jpg',
+ height: 120,
+ },
+ ],
+ },
+ patch_set: 1,
+ id: 'eb0d03fd_5e95904f',
+ line: 10,
+ updated: '2017-04-04 15:36:17.000000000',
+ message: 'This is a robot comment with a fix.',
+ unresolved: false,
+ __commentSide: 'right',
+ collapsed: false,
+ },
+ ];
+ element.comment = element.comments[0];
+ flushAsynchronousOperations();
+ assert.isNotNull(element.shadowRoot
+ .querySelector('.robotActions gr-button'));
+ });
+
+ test('_handleShowFix fires open-fix-preview event', done => {
+ element.addEventListener('open-fix-preview', e => {
+ assert.deepEqual(e.detail, element._getEventPayload());
+ done();
+ });
+ element.comment = {fix_suggestions: [{}]};
+ element.isRobotComment = true;
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.show-fix'));
+ });
+ });
+
+ suite('respectful tips', () => {
+ let element;
+ let sandbox;
+ let clock;
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve(null); },
+ });
+ clock = sinon.useFakeTimers();
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ clock.restore();
+ sandbox.restore();
+ });
+
+ test('show tip when no cached record', done => {
+ // fake stub for storage
+ const respectfulGetStub = sinon.stub();
+ const respectfulSetStub = sinon.stub();
+ stub('gr-storage', {
+ getRespectfulTipVisibility() { return respectfulGetStub(); },
+ setRespectfulTipVisibility() { return respectfulSetStub(); },
+ });
+ respectfulGetStub.returns(null);
+ element = fixture('draft');
+ // fake random
+ element.getRandomNum = () => 0;
+ element.comment = {__editing: true};
+ flush(() => {
+ assert.isTrue(respectfulGetStub.called);
+ assert.isTrue(respectfulSetStub.called);
+ assert.isTrue(
+ !!element.shadowRoot.querySelector('.respectfulReviewTip')
+ );
+ done();
+ });
+ });
+
+ test('add 14-day delays once dismissed', done => {
+ // fake stub for storage
+ const respectfulGetStub = sinon.stub();
+ const respectfulSetStub = sinon.stub();
+ stub('gr-storage', {
+ getRespectfulTipVisibility() { return respectfulGetStub(); },
+ setRespectfulTipVisibility(days) { return respectfulSetStub(days); },
+ });
+ respectfulGetStub.returns(null);
+ element = fixture('draft');
+ // fake random
+ element.getRandomNum = () => 0;
+ element.comment = {__editing: true};
+ flush(() => {
+ assert.isTrue(respectfulGetStub.called);
+ assert.isTrue(respectfulSetStub.called);
+ assert.isTrue(respectfulSetStub.lastCall.args[0] === undefined);
+ assert.isTrue(
+ !!element.shadowRoot.querySelector('.respectfulReviewTip')
+ );
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.respectfulReviewTip .close'));
+ flushAsynchronousOperations();
+ assert.isTrue(respectfulSetStub.lastCall.args[0] === 14);
+ done();
+ });
+ });
+
+ test('do not show tip when fall out of probability', done => {
+ // fake stub for storage
+ const respectfulGetStub = sinon.stub();
+ const respectfulSetStub = sinon.stub();
+ stub('gr-storage', {
+ getRespectfulTipVisibility() { return respectfulGetStub(); },
+ setRespectfulTipVisibility() { return respectfulSetStub(); },
+ });
+ respectfulGetStub.returns(null);
+ element = fixture('draft');
+ // fake random
+ element.getRandomNum = () => 3;
+ element.comment = {__editing: true};
+ flush(() => {
+ assert.isTrue(respectfulGetStub.called);
+ assert.isFalse(respectfulSetStub.called);
+ assert.isFalse(
+ !!element.shadowRoot.querySelector('.respectfulReviewTip')
+ );
+ done();
+ });
+ });
+
+ test('show tip when editing changed to true', done => {
+ // fake stub for storage
+ const respectfulGetStub = sinon.stub();
+ const respectfulSetStub = sinon.stub();
+ stub('gr-storage', {
+ getRespectfulTipVisibility() { return respectfulGetStub(); },
+ setRespectfulTipVisibility() { return respectfulSetStub(); },
+ });
+ respectfulGetStub.returns(null);
+ element = fixture('draft');
+ // fake random
+ element.getRandomNum = () => 0;
+ element.comment = {__editing: false};
+ flush(() => {
+ assert.isFalse(respectfulGetStub.called);
+ assert.isFalse(respectfulSetStub.called);
+ assert.isFalse(
+ !!element.shadowRoot.querySelector('.respectfulReviewTip')
+ );
+
+ element.editing = true;
flush(() => {
assert.isTrue(respectfulGetStub.called);
assert.isTrue(respectfulSetStub.called);
@@ -1166,113 +1243,30 @@
done();
});
});
+ });
- test('add 3 day delays once dismissed', done => {
- // fake stub for storage
- const respectfulGetStub = sinon.stub();
- const respectfulSetStub = sinon.stub();
- stub('gr-storage', {
- getRespectfulTipVisibility() { return respectfulGetStub(); },
- setRespectfulTipVisibility(days) { return respectfulSetStub(days); },
- });
- respectfulGetStub.returns(null);
- element = fixture('draft');
- // fake random
- element.getRandomNum = () => 0;
- element.comment = {__editing: true};
- flush(() => {
- assert.isTrue(respectfulGetStub.called);
- assert.isTrue(respectfulSetStub.called);
- assert.isTrue(respectfulSetStub.lastCall.args[0] === undefined);
- assert.isTrue(
- !!element.shadowRoot.querySelector('.respectfulReviewTip')
- );
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('.respectfulReviewTip .close'));
- flushAsynchronousOperations();
- assert.isTrue(respectfulSetStub.lastCall.args[0] === 3);
- done();
- });
+ test('no tip when cached record', done => {
+ // fake stub for storage
+ const respectfulGetStub = sinon.stub();
+ const respectfulSetStub = sinon.stub();
+ stub('gr-storage', {
+ getRespectfulTipVisibility() { return respectfulGetStub(); },
+ setRespectfulTipVisibility() { return respectfulSetStub(); },
});
-
- test('do not show tip when fall out of probability', done => {
- // fake stub for storage
- const respectfulGetStub = sinon.stub();
- const respectfulSetStub = sinon.stub();
- stub('gr-storage', {
- getRespectfulTipVisibility() { return respectfulGetStub(); },
- setRespectfulTipVisibility() { return respectfulSetStub(); },
- });
- respectfulGetStub.returns(null);
- element = fixture('draft');
- // fake random
- element.getRandomNum = () => 3;
- element.comment = {__editing: true};
- flush(() => {
- assert.isTrue(respectfulGetStub.called);
- assert.isFalse(respectfulSetStub.called);
- assert.isFalse(
- !!element.shadowRoot.querySelector('.respectfulReviewTip')
- );
- done();
- });
- });
-
- test('show tip when editing changed to true', done => {
- // fake stub for storage
- const respectfulGetStub = sinon.stub();
- const respectfulSetStub = sinon.stub();
- stub('gr-storage', {
- getRespectfulTipVisibility() { return respectfulGetStub(); },
- setRespectfulTipVisibility() { return respectfulSetStub(); },
- });
- respectfulGetStub.returns(null);
- element = fixture('draft');
- // fake random
- element.getRandomNum = () => 0;
- element.comment = {__editing: false};
- flush(() => {
- assert.isFalse(respectfulGetStub.called);
- assert.isFalse(respectfulSetStub.called);
- assert.isFalse(
- !!element.shadowRoot.querySelector('.respectfulReviewTip')
- );
-
- element.editing = true;
- flush(() => {
- assert.isTrue(respectfulGetStub.called);
- assert.isTrue(respectfulSetStub.called);
- assert.isTrue(
- !!element.shadowRoot.querySelector('.respectfulReviewTip')
- );
- done();
- });
- });
- });
-
- test('no tip when cached record', done => {
- // fake stub for storage
- const respectfulGetStub = sinon.stub();
- const respectfulSetStub = sinon.stub();
- stub('gr-storage', {
- getRespectfulTipVisibility() { return respectfulGetStub(); },
- setRespectfulTipVisibility() { return respectfulSetStub(); },
- });
- respectfulGetStub.returns({});
- element = fixture('draft');
- // fake random
- element.getRandomNum = () => 0;
- element.comment = {__editing: true};
- flush(() => {
- assert.isTrue(respectfulGetStub.called);
- assert.isFalse(respectfulSetStub.called);
- assert.isFalse(
- !!element.shadowRoot.querySelector('.respectfulReviewTip')
- );
- done();
- });
+ respectfulGetStub.returns({});
+ element = fixture('draft');
+ // fake random
+ element.getRandomNum = () => 0;
+ element.comment = {__editing: true};
+ flush(() => {
+ assert.isTrue(respectfulGetStub.called);
+ assert.isFalse(respectfulSetStub.called);
+ assert.isFalse(
+ !!element.shadowRoot.querySelector('.respectfulReviewTip')
+ );
+ done();
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
deleted file mode 100644
index e92bddb..0000000
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
+++ /dev/null
@@ -1,72 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-confirm-delete-comment-dialog">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- }
- :host([disabled]) {
- opacity: .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>
- </template>
- <script src="gr-confirm-delete-comment-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
index 8d50fe0..7db24c2 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
@@ -14,54 +14,64 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../gr-dialog/gr-dialog.js';
+import '../../../styles/shared-styles.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-confirm-delete-comment-dialog_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrConfirmDeleteCommentDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-delete-comment-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
*/
- class GrConfirmDeleteCommentDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-confirm-delete-comment-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
- message: String,
- };
- }
-
- resetFocus() {
- this.$.messageInput.textarea.focus();
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('confirm', {reason: this.message}, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', null, {bubbles: false});
- }
+ static get properties() {
+ return {
+ message: String,
+ };
}
- customElements.define(GrConfirmDeleteCommentDialog.is,
- GrConfirmDeleteCommentDialog);
-})();
+ resetFocus() {
+ this.$.messageInput.textarea.focus();
+ }
+
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('confirm', {reason: this.message}, {bubbles: false});
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', null, {bubbles: false});
+ }
+}
+
+customElements.define(GrConfirmDeleteCommentDialog.is,
+ GrConfirmDeleteCommentDialog);
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.js b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.js
new file mode 100644
index 0000000..f6caaa1
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.js
@@ -0,0 +1,56 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ }
+ :host([disabled]) {
+ opacity: .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.html b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html
deleted file mode 100644
index c344bf64..0000000
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html
+++ /dev/null
@@ -1,90 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-
-<dom-module id="gr-copy-clipboard">
- <template>
- <style include="shared-styles">
- .text {
- align-items: center;
- display: flex;
- flex-wrap: wrap;
- }
- .copyText {
- flex-grow: 1;
- margin-right: var(--spacing-s);
- }
- .hideInput {
- display: none;
- }
- input#input {
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-mono);
- line-height: var(--line-height-mono);
- @apply --text-container-style;
- width: 100%;
- }
- /*
- * Typically icons are 20px, which is the normal line-height.
- * The copy icon is too prominent at 20px, so we choose 16px
- * here, but add 2x2px padding below, so the entire
- * component should still fit nicely into a normal inline
- * layout flow.
- */
- #icon {
- height: 16px;
- width: 16px;
- }
- gr-button {
- --gr-button: {
- padding: 2px;
- }
- }
- </style>
- <div class="text">
- <iron-input
- class="copyText"
- type="text"
- bind-value="[[text]]"
- on-tap="_handleInputClick"
- readonly>
- <input
- id="input"
- is="iron-input"
- class$="[[_computeInputClass(hideInput)]]"
- type="text"
- bind-value="[[text]]"
- on-click="_handleInputClick"
- readonly>
- </iron-input>
- <gr-button id="button"
- link
- has-tooltip="[[hasTooltip]]"
- class="copyToClipboard"
- title="[[buttonTitle]]"
- on-click="_copyToClipboard">
- <iron-icon id="icon" icon="gr-icons:content-copy"></iron-icon>
- </gr-button>
- </div>
- </template>
- <script src="gr-copy-clipboard.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
index 9be6852..0f6168e 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
@@ -14,64 +14,74 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const COPY_TIMEOUT_MS = 1000;
+import '@polymer/iron-input/iron-input.js';
+import '../../../styles/shared-styles.js';
+import '../gr-button/gr-button.js';
+import '../gr-icons/gr-icons.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-copy-clipboard_html.js';
- /** @extends Polymer.Element */
- class GrCopyClipboard extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-copy-clipboard'; }
+const COPY_TIMEOUT_MS = 1000;
- static get properties() {
- return {
- text: String,
- buttonTitle: String,
- hasTooltip: {
- type: Boolean,
- value: false,
- },
- hideInput: {
- type: Boolean,
- value: false,
- },
- };
- }
+/** @extends Polymer.Element */
+class GrCopyClipboard extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- focusOnCopy() {
- this.$.button.focus();
- }
+ static get is() { return 'gr-copy-clipboard'; }
- _computeInputClass(hideInput) {
- return hideInput ? 'hideInput' : '';
- }
-
- _handleInputClick(e) {
- e.preventDefault();
- Polymer.dom(e).rootTarget.select();
- }
-
- _copyToClipboard(e) {
- e.preventDefault();
- e.stopPropagation();
-
- if (this.hideInput) {
- this.$.input.style.display = 'block';
- }
- this.$.input.focus();
- this.$.input.select();
- document.execCommand('copy');
- if (this.hideInput) {
- this.$.input.style.display = 'none';
- }
- this.$.icon.icon = 'gr-icons:check';
- this.async(
- () => this.$.icon.icon = 'gr-icons:content-copy',
- COPY_TIMEOUT_MS);
- }
+ static get properties() {
+ return {
+ text: String,
+ buttonTitle: String,
+ hasTooltip: {
+ type: Boolean,
+ value: false,
+ },
+ hideInput: {
+ type: Boolean,
+ value: false,
+ },
+ };
}
- customElements.define(GrCopyClipboard.is, GrCopyClipboard);
-})();
+ focusOnCopy() {
+ this.$.button.focus();
+ }
+
+ _computeInputClass(hideInput) {
+ return hideInput ? 'hideInput' : '';
+ }
+
+ _handleInputClick(e) {
+ e.preventDefault();
+ dom(e).rootTarget.select();
+ }
+
+ _copyToClipboard(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (this.hideInput) {
+ this.$.input.style.display = 'block';
+ }
+ this.$.input.focus();
+ this.$.input.select();
+ document.execCommand('copy');
+ if (this.hideInput) {
+ this.$.input.style.display = 'none';
+ }
+ this.$.icon.icon = 'gr-icons:check';
+ this.async(
+ () => this.$.icon.icon = 'gr-icons:content-copy',
+ COPY_TIMEOUT_MS);
+ }
+}
+
+customElements.define(GrCopyClipboard.is, GrCopyClipboard);
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.js b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.js
new file mode 100644
index 0000000..29becbb
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.js
@@ -0,0 +1,65 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ .text {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ }
+ .copyText {
+ flex-grow: 1;
+ margin-right: var(--spacing-s);
+ }
+ .hideInput {
+ display: none;
+ }
+ input#input {
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-mono);
+ line-height: var(--line-height-mono);
+ @apply --text-container-style;
+ width: 100%;
+ }
+ /*
+ * Typically icons are 20px, which is the normal line-height.
+ * The copy icon is too prominent at 20px, so we choose 16px
+ * here, but add 2x2px padding below, so the entire
+ * component should still fit nicely into a normal inline
+ * layout flow.
+ */
+ #icon {
+ height: 16px;
+ width: 16px;
+ }
+ gr-button {
+ --gr-button: {
+ padding: 2px;
+ }
+ }
+ </style>
+ <div class="text">
+ <iron-input class="copyText" type="text" bind-value="[[text]]" on-tap="_handleInputClick" readonly="">
+ <input id="input" is="iron-input" class\$="[[_computeInputClass(hideInput)]]" type="text" bind-value="[[text]]" on-click="_handleInputClick" readonly="">
+ </iron-input>
+ <gr-button id="button" link="" has-tooltip="[[hasTooltip]]" class="copyToClipboard" title="[[buttonTitle]]" on-click="_copyToClipboard">
+ <iron-icon id="icon" icon="gr-icons:content-copy"></iron-icon>
+ </gr-button>
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
index 45ade85..84cb166 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-copy-clipboard</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-copy-clipboard.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,73 +30,75 @@
</template>
</test-fixture>
-<script>
- suite('gr-copy-clipboard tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-copy-clipboard.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-copy-clipboard tests', () => {
+ let element;
+ let sandbox;
- setup(done => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.text = `git fetch http://gerrit@localhost:8080/a/test-project
- refs/changes/05/5/1 && git checkout FETCH_HEAD`;
- flushAsynchronousOperations();
- flush(done);
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('copy to clipboard', () => {
- const clipboardSpy = sandbox.spy(element, '_copyToClipboard');
- const copyBtn = element.shadowRoot
- .querySelector('.copyToClipboard');
- MockInteractions.tap(copyBtn);
- assert.isTrue(clipboardSpy.called);
- });
-
- test('focusOnCopy', () => {
- element.focusOnCopy();
- assert.deepEqual(Polymer.dom(element.root).activeElement,
- element.shadowRoot
- .querySelector('.copyToClipboard'));
- });
-
- test('_handleInputClick', () => {
- // iron-input as parent should never be hidden as copy won't work
- // on nested hidden elements
- const ironInputElement = element.shadowRoot.querySelector('iron-input');
- assert.notEqual(getComputedStyle(ironInputElement).display, 'none');
-
- const inputElement = element.shadowRoot.querySelector('input');
- MockInteractions.tap(inputElement);
- assert.equal(inputElement.selectionStart, 0);
- assert.equal(inputElement.selectionEnd, element.text.length - 1);
- });
-
- test('hideInput', () => {
- // iron-input as parent should never be hidden as copy won't work
- // on nested hidden elements
- const ironInputElement = element.shadowRoot.querySelector('iron-input');
- assert.notEqual(getComputedStyle(ironInputElement).display, 'none');
-
- assert.notEqual(getComputedStyle(element.$.input).display, 'none');
- element.hideInput = true;
- flushAsynchronousOperations();
- assert.equal(getComputedStyle(element.$.input).display, 'none');
- });
-
- test('stop events propagation', () => {
- const divParent = document.createElement('div');
- divParent.appendChild(element);
- const clickStub = sinon.stub();
- divParent.addEventListener('click', clickStub);
- element.stopPropagation = true;
- const copyBtn = element.shadowRoot.querySelector('.copyToClipboard');
- MockInteractions.tap(copyBtn);
- assert.isFalse(clickStub.called);
- });
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.text = `git fetch http://gerrit@localhost:8080/a/test-project
+ refs/changes/05/5/1 && git checkout FETCH_HEAD`;
+ flushAsynchronousOperations();
+ flush(done);
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('copy to clipboard', () => {
+ const clipboardSpy = sandbox.spy(element, '_copyToClipboard');
+ const copyBtn = element.shadowRoot
+ .querySelector('.copyToClipboard');
+ MockInteractions.tap(copyBtn);
+ assert.isTrue(clipboardSpy.called);
+ });
+
+ test('focusOnCopy', () => {
+ element.focusOnCopy();
+ assert.deepEqual(dom(element.root).activeElement,
+ element.shadowRoot
+ .querySelector('.copyToClipboard'));
+ });
+
+ test('_handleInputClick', () => {
+ // iron-input as parent should never be hidden as copy won't work
+ // on nested hidden elements
+ const ironInputElement = element.shadowRoot.querySelector('iron-input');
+ assert.notEqual(getComputedStyle(ironInputElement).display, 'none');
+
+ const inputElement = element.shadowRoot.querySelector('input');
+ MockInteractions.tap(inputElement);
+ assert.equal(inputElement.selectionStart, 0);
+ assert.equal(inputElement.selectionEnd, element.text.length - 1);
+ });
+
+ test('hideInput', () => {
+ // iron-input as parent should never be hidden as copy won't work
+ // on nested hidden elements
+ const ironInputElement = element.shadowRoot.querySelector('iron-input');
+ assert.notEqual(getComputedStyle(ironInputElement).display, 'none');
+
+ assert.notEqual(getComputedStyle(element.$.input).display, 'none');
+ element.hideInput = true;
+ flushAsynchronousOperations();
+ assert.equal(getComputedStyle(element.$.input).display, 'none');
+ });
+
+ test('stop events propagation', () => {
+ const divParent = document.createElement('div');
+ divParent.appendChild(element);
+ const clickStub = sinon.stub();
+ divParent.addEventListener('click', clickStub);
+ element.stopPropagation = true;
+ const copyBtn = element.shadowRoot.querySelector('.copyToClipboard');
+ MockInteractions.tap(copyBtn);
+ assert.isFalse(clickStub.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.html b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.html
deleted file mode 100644
index b69c61aa..0000000
--- a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.html
+++ /dev/null
@@ -1,58 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<script>
- (function(window) {
- 'use strict';
- const GrCountStringFormatter = window.GrCountStringFormatter || {};
-
- /**
- * Returns a count plus string that is pluralized when necessary.
- *
- * @param {number} count
- * @param {string} noun
- * @return {string}
- */
- GrCountStringFormatter.computePluralString = function(count, noun) {
- return this.computeString(count, noun) + (count > 1 ? 's' : '');
- };
-
- /**
- * Returns a count plus string that is not pluralized.
- *
- * @param {number} count
- * @param {string} noun
- * @return {string}
- */
- GrCountStringFormatter.computeString = function(count, noun) {
- if (count === 0) { return ''; }
- return count + ' ' + noun;
- };
-
- /**
- * Returns a count plus arbitrary text.
- *
- * @param {number} count
- * @param {string} text
- * @return {string}
- */
- GrCountStringFormatter.computeShortString = function(count, text) {
- if (count === 0) { return ''; }
- return count + text;
- };
- window.GrCountStringFormatter = GrCountStringFormatter;
- })(window);
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.js b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.js
new file mode 100644
index 0000000..02c57e0
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.js
@@ -0,0 +1,56 @@
+/**
+ * @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.
+ */
+(function(window) {
+ 'use strict';
+ const GrCountStringFormatter = window.GrCountStringFormatter || {};
+
+ /**
+ * Returns a count plus string that is pluralized when necessary.
+ *
+ * @param {number} count
+ * @param {string} noun
+ * @return {string}
+ */
+ GrCountStringFormatter.computePluralString = function(count, noun) {
+ return this.computeString(count, noun) + (count > 1 ? 's' : '');
+ };
+
+ /**
+ * Returns a count plus string that is not pluralized.
+ *
+ * @param {number} count
+ * @param {string} noun
+ * @return {string}
+ */
+ GrCountStringFormatter.computeString = function(count, noun) {
+ if (count === 0) { return ''; }
+ return count + ' ' + noun;
+ };
+
+ /**
+ * Returns a count plus arbitrary text.
+ *
+ * @param {number} count
+ * @param {string} text
+ * @return {string}
+ */
+ GrCountStringFormatter.computeShortString = function(count, text) {
+ if (count === 0) { return ''; }
+ return count + text;
+ };
+ window.GrCountStringFormatter = GrCountStringFormatter;
+})(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
index 64dff6a..ead0191 100644
--- a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
@@ -19,41 +19,38 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-count-string-formatter</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-count-string-formatter.html"/>
-
-<script>
- suite('gr-count-string-formatter tests', async () => {
- await readyToTest();
- test('computeString', () => {
- const noun = 'unresolved';
- assert.equal(GrCountStringFormatter.computeString(0, noun), '');
- assert.equal(GrCountStringFormatter.computeString(1, noun),
- '1 unresolved');
- assert.equal(GrCountStringFormatter.computeString(2, noun),
- '2 unresolved');
- });
-
- test('computeShortString', () => {
- const noun = 'c';
- assert.equal(GrCountStringFormatter.computeShortString(0, noun), '');
- assert.equal(GrCountStringFormatter.computeShortString(1, noun), '1c');
- assert.equal(GrCountStringFormatter.computeShortString(2, noun), '2c');
- });
-
- test('computePluralString', () => {
- const noun = 'comment';
- assert.equal(GrCountStringFormatter.computePluralString(0, noun), '');
- assert.equal(GrCountStringFormatter.computePluralString(1, noun),
- '1 comment');
- assert.equal(GrCountStringFormatter.computePluralString(2, noun),
- '2 comments');
- });
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-count-string-formatter.js';
+suite('gr-count-string-formatter tests', () => {
+ test('computeString', () => {
+ const noun = 'unresolved';
+ assert.equal(GrCountStringFormatter.computeString(0, noun), '');
+ assert.equal(GrCountStringFormatter.computeString(1, noun),
+ '1 unresolved');
+ assert.equal(GrCountStringFormatter.computeString(2, noun),
+ '2 unresolved');
});
+
+ test('computeShortString', () => {
+ const noun = 'c';
+ assert.equal(GrCountStringFormatter.computeShortString(0, noun), '');
+ assert.equal(GrCountStringFormatter.computeShortString(1, noun), '1c');
+ assert.equal(GrCountStringFormatter.computeShortString(2, noun), '2c');
+ });
+
+ test('computePluralString', () => {
+ const noun = 'comment';
+ assert.equal(GrCountStringFormatter.computePluralString(0, noun), '');
+ assert.equal(GrCountStringFormatter.computePluralString(1, noun),
+ '1 comment');
+ assert.equal(GrCountStringFormatter.computePluralString(2, noun),
+ '2 comments');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html
deleted file mode 100644
index 94d7aaa..0000000
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html
+++ /dev/null
@@ -1,23 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<dom-module id="gr-cursor-manager">
- <template></template>
- <script src="gr-cursor-manager.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
index 4f98e88..f184b6c 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -14,422 +14,426 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-cursor-manager_html.js';
- const ScrollBehavior = {
- NEVER: 'never',
- KEEP_VISIBLE: 'keep-visible',
- };
+const ScrollBehavior = {
+ NEVER: 'never',
+ KEEP_VISIBLE: 'keep-visible',
+};
- /** @extends Polymer.Element */
- class GrCursorManager extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-cursor-manager'; }
+/** @extends Polymer.Element */
+class GrCursorManager extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- static get properties() {
- return {
- stops: {
- type: Array,
- value() {
- return [];
- },
- observer: '_updateIndex',
+ static get is() { return 'gr-cursor-manager'; }
+
+ static get properties() {
+ return {
+ stops: {
+ type: Array,
+ value() {
+ return [];
},
- /**
- * @type {?Object}
- */
- target: {
- type: Object,
- notify: true,
- observer: '_scrollToTarget',
- },
- /**
- * The height of content intended to be included with the target.
- *
- * @type {?number}
- */
- _targetHeight: Number,
+ observer: '_updateIndex',
+ },
+ /**
+ * @type {?Object}
+ */
+ target: {
+ type: Object,
+ notify: true,
+ observer: '_scrollToTarget',
+ },
+ /**
+ * The height of content intended to be included with the target.
+ *
+ * @type {?number}
+ */
+ _targetHeight: Number,
- /**
- * The index of the current target (if any). -1 otherwise.
- */
- index: {
- type: Number,
- value: -1,
- notify: true,
- },
+ /**
+ * The index of the current target (if any). -1 otherwise.
+ */
+ index: {
+ type: Number,
+ value: -1,
+ notify: true,
+ },
- /**
- * The class to apply to the current target. Use null for no class.
- */
- cursorTargetClass: {
- type: String,
- value: null,
- },
+ /**
+ * The class to apply to the current target. Use null for no class.
+ */
+ cursorTargetClass: {
+ type: String,
+ value: null,
+ },
- /**
- * The scroll behavior for the cursor. Values are 'never' and
- * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
- * the viewport.
- * TODO (beckysiegel) figure out why it can be undefined
- *
- * @type {string|undefined}
- */
- scrollBehavior: {
- type: String,
- value: ScrollBehavior.NEVER,
- },
+ /**
+ * The scroll behavior for the cursor. Values are 'never' and
+ * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
+ * the viewport.
+ * TODO (beckysiegel) figure out why it can be undefined
+ *
+ * @type {string|undefined}
+ */
+ scrollBehavior: {
+ type: String,
+ value: ScrollBehavior.NEVER,
+ },
- /**
- * When true, will call element.focus() during scrolling.
- */
- focusOnMove: {
- type: Boolean,
- value: false,
- },
+ /**
+ * When true, will call element.focus() during scrolling.
+ */
+ focusOnMove: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * The scrollTopMargin defines height of invisible area at the top
- * of the page. If cursor locates inside this margin - it is
- * not visible, because it is covered by some other element.
- */
- scrollTopMargin: {
- type: Number,
- value: 0,
- },
- };
+ /**
+ * The scrollTopMargin defines height of invisible area at the top
+ * of the page. If cursor locates inside this margin - it is
+ * not visible, because it is covered by some other element.
+ */
+ scrollTopMargin: {
+ type: Number,
+ value: 0,
+ },
+ };
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this.unsetCursor();
+ }
+
+ /**
+ * Move the cursor forward. Clipped to the ends of the stop list.
+ *
+ * @param {!Function=} opt_condition Optional stop condition. If a condition
+ * is passed the cursor will continue to move in the specified direction
+ * until the condition is met.
+ * @param {!Function=} opt_getTargetHeight Optional function to calculate the
+ * height of the target's 'section'. The height of the target itself is
+ * sometimes different, used by the diff cursor.
+ * @param {boolean=} opt_clipToTop When none of the next indices match, move
+ * back to first instead of to last.
+ * @private
+ */
+
+ next(opt_condition, opt_getTargetHeight, opt_clipToTop) {
+ this._moveCursor(1, opt_condition, opt_getTargetHeight, opt_clipToTop);
+ }
+
+ previous(opt_condition) {
+ this._moveCursor(-1, opt_condition);
+ }
+
+ /**
+ * Move the cursor to the row which is the closest to the viewport center
+ * in vertical direction.
+ * The method uses IntersectionObservers API. If browser
+ * doesn't support this API the method does nothing
+ *
+ * @param {!Function=} opt_condition Optional condition. If a condition
+ * is passed only stops which meet conditions are taken into account.
+ */
+ moveToVisibleArea(opt_condition) {
+ if (!this.stops || !this._isIntersectionObserverSupported()) {
+ return;
}
+ const filteredStops = opt_condition ? this.stops.filter(opt_condition)
+ : this.stops;
+ const dims = this._getWindowDims();
+ const windowCenter =
+ Math.round((dims.innerHeight + this.scrollTopMargin) / 2);
- /** @override */
- detached() {
- super.detached();
- this.unsetCursor();
- }
+ let closestToTheCenter = null;
+ let minDistanceToCenter = null;
+ let unobservedCount = filteredStops.length;
- /**
- * Move the cursor forward. Clipped to the ends of the stop list.
- *
- * @param {!Function=} opt_condition Optional stop condition. If a condition
- * is passed the cursor will continue to move in the specified direction
- * until the condition is met.
- * @param {!Function=} opt_getTargetHeight Optional function to calculate the
- * height of the target's 'section'. The height of the target itself is
- * sometimes different, used by the diff cursor.
- * @param {boolean=} opt_clipToTop When none of the next indices match, move
- * back to first instead of to last.
- * @private
- */
+ const observer = new IntersectionObserver(entries => {
+ // This callback is called for the first time immediately.
+ // Typically it gets all observed stops at once, but
+ // sometimes can get them in several chunks.
+ entries.forEach(entry => {
+ observer.unobserve(entry.target);
- next(opt_condition, opt_getTargetHeight, opt_clipToTop) {
- this._moveCursor(1, opt_condition, opt_getTargetHeight, opt_clipToTop);
- }
-
- previous(opt_condition) {
- this._moveCursor(-1, opt_condition);
- }
-
- /**
- * Move the cursor to the row which is the closest to the viewport center
- * in vertical direction.
- * The method uses IntersectionObservers API. If browser
- * doesn't support this API the method does nothing
- *
- * @param {!Function=} opt_condition Optional condition. If a condition
- * is passed only stops which meet conditions are taken into account.
- */
- moveToVisibleArea(opt_condition) {
- if (!this.stops || !this._isIntersectionObserverSupported()) {
- return;
- }
- const filteredStops = opt_condition ? this.stops.filter(opt_condition)
- : this.stops;
- const dims = this._getWindowDims();
- const windowCenter =
- Math.round((dims.innerHeight + this.scrollTopMargin) / 2);
-
- let closestToTheCenter = null;
- let minDistanceToCenter = null;
- let unobservedCount = filteredStops.length;
-
- const observer = new IntersectionObserver(entries => {
- // This callback is called for the first time immediately.
- // Typically it gets all observed stops at once, but
- // sometimes can get them in several chunks.
- entries.forEach(entry => {
- observer.unobserve(entry.target);
-
- // In Edge it is recommended to use intersectionRatio instead of
- // isIntersecting.
- const isInsideViewport =
- entry.isIntersecting || entry.intersectionRatio > 0;
- if (!isInsideViewport) {
- return;
- }
- const center = entry.boundingClientRect.top + Math.round(
- entry.boundingClientRect.height / 2);
- const distanceToWindowCenter = Math.abs(center - windowCenter);
- if (minDistanceToCenter === null ||
- distanceToWindowCenter < minDistanceToCenter) {
- closestToTheCenter = entry.target;
- minDistanceToCenter = distanceToWindowCenter;
- }
- });
- unobservedCount -= entries.length;
- if (unobservedCount == 0 && closestToTheCenter) {
- // set cursor when all stops were observed.
- // In most cases the target is visible, so scroll is not
- // needed. But in rare cases the target can become invisible
- // at this point (due to some scrolling in window).
- // To avoid jumps set noScroll options.
- this.setCursor(closestToTheCenter, true);
- }
- });
- filteredStops.forEach(stop => {
- observer.observe(stop);
- });
- }
-
- _isIntersectionObserverSupported() {
- // The copy of this method exists in gr-app-element.js under the
- // name _isCursorManagerSupportMoveToVisibleLine
- // If you update this method, you must update gr-app-element.js
- // as well.
- return 'IntersectionObserver' in window;
- }
-
- /**
- * Set the cursor to an arbitrary element.
- *
- * @param {!HTMLElement} element
- * @param {boolean=} opt_noScroll prevent any potential scrolling in response
- * setting the cursor.
- */
- setCursor(element, opt_noScroll) {
- let behavior;
- if (opt_noScroll) {
- behavior = this.scrollBehavior;
- this.scrollBehavior = ScrollBehavior.NEVER;
- }
-
- this.unsetCursor();
- this.target = element;
- this._updateIndex();
- this._decorateTarget();
-
- if (opt_noScroll) { this.scrollBehavior = behavior; }
- }
-
- unsetCursor() {
- this._unDecorateTarget();
- this.index = -1;
- this.target = null;
- this._targetHeight = null;
- }
-
- isAtStart() {
- return this.index === 0;
- }
-
- isAtEnd() {
- return this.index === this.stops.length - 1;
- }
-
- moveToStart() {
- if (this.stops.length) {
- this.setCursor(this.stops[0]);
- }
- }
-
- moveToEnd() {
- if (this.stops.length) {
- this.setCursor(this.stops[this.stops.length - 1]);
- }
- }
-
- setCursorAtIndex(index, opt_noScroll) {
- this.setCursor(this.stops[index], opt_noScroll);
- }
-
- /**
- * Move the cursor forward or backward by delta. Clipped to the beginning or
- * end of stop list.
- *
- * @param {number} delta either -1 or 1.
- * @param {!Function=} opt_condition Optional stop condition. If a condition
- * is passed the cursor will continue to move in the specified direction
- * until the condition is met.
- * @param {!Function=} opt_getTargetHeight Optional function to calculate the
- * height of the target's 'section'. The height of the target itself is
- * sometimes different, used by the diff cursor.
- * @param {boolean=} opt_clipToTop When none of the next indices match, move
- * back to first instead of to last.
- * @private
- */
- _moveCursor(delta, opt_condition, opt_getTargetHeight, opt_clipToTop) {
- if (!this.stops.length) {
- this.unsetCursor();
- return;
- }
-
- this._unDecorateTarget();
-
- const newIndex = this._getNextindex(delta, opt_condition, opt_clipToTop);
-
- let newTarget = null;
- if (newIndex !== -1) {
- newTarget = this.stops[newIndex];
- }
-
- this.index = newIndex;
- this.target = newTarget;
-
- if (!this.target) { return; }
-
- if (opt_getTargetHeight) {
- this._targetHeight = opt_getTargetHeight(newTarget);
- } else {
- this._targetHeight = newTarget.scrollHeight;
- }
-
- if (this.focusOnMove) { this.target.focus(); }
-
- this._decorateTarget();
- }
-
- _decorateTarget() {
- if (this.target && this.cursorTargetClass) {
- this.target.classList.add(this.cursorTargetClass);
- }
- }
-
- _unDecorateTarget() {
- if (this.target && this.cursorTargetClass) {
- this.target.classList.remove(this.cursorTargetClass);
- }
- }
-
- /**
- * Get the next stop index indicated by the delta direction.
- *
- * @param {number} delta either -1 or 1.
- * @param {!Function=} opt_condition Optional stop condition.
- * @param {boolean=} opt_clipToTop When none of the next indices match, move
- * back to first instead of to last.
- * @return {number} the new index.
- * @private
- */
- _getNextindex(delta, opt_condition, opt_clipToTop) {
- if (!this.stops.length || this.index === -1) {
- return -1;
- }
-
- let newIndex = this.index;
- do {
- newIndex = newIndex + delta;
- } while (newIndex > 0 &&
- newIndex < this.stops.length - 1 &&
- opt_condition && !opt_condition(this.stops[newIndex]));
-
- newIndex = Math.max(0, Math.min(this.stops.length - 1, newIndex));
-
- // If we failed to satisfy the condition:
- if (opt_condition && !opt_condition(this.stops[newIndex])) {
- if (delta < 0 || opt_clipToTop) {
- return 0;
- } else if (delta > 0) {
- return this.stops.length - 1;
- }
- return this.index;
- }
-
- return newIndex;
- }
-
- _updateIndex() {
- if (!this.target) {
- this.index = -1;
- return;
- }
-
- const newIndex = Array.prototype.indexOf.call(this.stops, this.target);
- if (newIndex === -1) {
- this.unsetCursor();
- } else {
- this.index = newIndex;
- }
- }
-
- /**
- * Calculate where the element is relative to the window.
- *
- * @param {!Object} target Target to scroll to.
- * @return {number} Distance to top of the target.
- */
- _getTop(target) {
- let top = target.offsetTop;
- for (let offsetParent = target.offsetParent;
- offsetParent;
- offsetParent = offsetParent.offsetParent) {
- top += offsetParent.offsetTop;
- }
- return top;
- }
-
- /**
- * @return {boolean}
- */
- _targetIsVisible(top) {
- const dims = this._getWindowDims();
- return this.scrollBehavior === ScrollBehavior.KEEP_VISIBLE &&
- top > (dims.pageYOffset + this.scrollTopMargin) &&
- top < dims.pageYOffset + dims.innerHeight;
- }
-
- _calculateScrollToValue(top, target) {
- const dims = this._getWindowDims();
- return top + this.scrollTopMargin - (dims.innerHeight / 3) +
- (target.offsetHeight / 2);
- }
-
- _scrollToTarget() {
- if (!this.target || this.scrollBehavior === ScrollBehavior.NEVER) {
- return;
- }
-
- const dims = this._getWindowDims();
- const top = this._getTop(this.target);
- const bottomIsVisible = this._targetHeight ?
- this._targetIsVisible(top + this._targetHeight) : true;
- const scrollToValue = this._calculateScrollToValue(top, this.target);
-
- if (this._targetIsVisible(top)) {
- // Don't scroll if either the bottom is visible or if the position that
- // would get scrolled to is higher up than the current position. this
- // woulld cause less of the target content to be displayed than is
- // already.
- if (bottomIsVisible || scrollToValue < dims.scrollY) {
+ // In Edge it is recommended to use intersectionRatio instead of
+ // isIntersecting.
+ const isInsideViewport =
+ entry.isIntersecting || entry.intersectionRatio > 0;
+ if (!isInsideViewport) {
return;
}
+ const center = entry.boundingClientRect.top + Math.round(
+ entry.boundingClientRect.height / 2);
+ const distanceToWindowCenter = Math.abs(center - windowCenter);
+ if (minDistanceToCenter === null ||
+ distanceToWindowCenter < minDistanceToCenter) {
+ closestToTheCenter = entry.target;
+ minDistanceToCenter = distanceToWindowCenter;
+ }
+ });
+ unobservedCount -= entries.length;
+ if (unobservedCount == 0 && closestToTheCenter) {
+ // set cursor when all stops were observed.
+ // In most cases the target is visible, so scroll is not
+ // needed. But in rare cases the target can become invisible
+ // at this point (due to some scrolling in window).
+ // To avoid jumps set noScroll options.
+ this.setCursor(closestToTheCenter, true);
}
+ });
+ filteredStops.forEach(stop => {
+ observer.observe(stop);
+ });
+ }
- // Scroll the element to the middle of the window. Dividing by a third
- // instead of half the inner height feels a bit better otherwise the
- // element appears to be below the center of the window even when it
- // isn't.
- window.scrollTo(dims.scrollX, scrollToValue);
+ _isIntersectionObserverSupported() {
+ // The copy of this method exists in gr-app-element.js under the
+ // name _isCursorManagerSupportMoveToVisibleLine
+ // If you update this method, you must update gr-app-element.js
+ // as well.
+ return 'IntersectionObserver' in window;
+ }
+
+ /**
+ * Set the cursor to an arbitrary element.
+ *
+ * @param {!HTMLElement} element
+ * @param {boolean=} opt_noScroll prevent any potential scrolling in response
+ * setting the cursor.
+ */
+ setCursor(element, opt_noScroll) {
+ let behavior;
+ if (opt_noScroll) {
+ behavior = this.scrollBehavior;
+ this.scrollBehavior = ScrollBehavior.NEVER;
}
- _getWindowDims() {
- return {
- scrollX: window.scrollX,
- scrollY: window.scrollY,
- innerHeight: window.innerHeight,
- pageYOffset: window.pageYOffset,
- };
+ this.unsetCursor();
+ this.target = element;
+ this._updateIndex();
+ this._decorateTarget();
+
+ if (opt_noScroll) { this.scrollBehavior = behavior; }
+ }
+
+ unsetCursor() {
+ this._unDecorateTarget();
+ this.index = -1;
+ this.target = null;
+ this._targetHeight = null;
+ }
+
+ isAtStart() {
+ return this.index === 0;
+ }
+
+ isAtEnd() {
+ return this.index === this.stops.length - 1;
+ }
+
+ moveToStart() {
+ if (this.stops.length) {
+ this.setCursor(this.stops[0]);
}
}
- customElements.define(GrCursorManager.is, GrCursorManager);
-})();
+ moveToEnd() {
+ if (this.stops.length) {
+ this.setCursor(this.stops[this.stops.length - 1]);
+ }
+ }
+
+ setCursorAtIndex(index, opt_noScroll) {
+ this.setCursor(this.stops[index], opt_noScroll);
+ }
+
+ /**
+ * Move the cursor forward or backward by delta. Clipped to the beginning or
+ * end of stop list.
+ *
+ * @param {number} delta either -1 or 1.
+ * @param {!Function=} opt_condition Optional stop condition. If a condition
+ * is passed the cursor will continue to move in the specified direction
+ * until the condition is met.
+ * @param {!Function=} opt_getTargetHeight Optional function to calculate the
+ * height of the target's 'section'. The height of the target itself is
+ * sometimes different, used by the diff cursor.
+ * @param {boolean=} opt_clipToTop When none of the next indices match, move
+ * back to first instead of to last.
+ * @private
+ */
+ _moveCursor(delta, opt_condition, opt_getTargetHeight, opt_clipToTop) {
+ if (!this.stops.length) {
+ this.unsetCursor();
+ return;
+ }
+
+ this._unDecorateTarget();
+
+ const newIndex = this._getNextindex(delta, opt_condition, opt_clipToTop);
+
+ let newTarget = null;
+ if (newIndex !== -1) {
+ newTarget = this.stops[newIndex];
+ }
+
+ this.index = newIndex;
+ this.target = newTarget;
+
+ if (!this.target) { return; }
+
+ if (opt_getTargetHeight) {
+ this._targetHeight = opt_getTargetHeight(newTarget);
+ } else {
+ this._targetHeight = newTarget.scrollHeight;
+ }
+
+ if (this.focusOnMove) { this.target.focus(); }
+
+ this._decorateTarget();
+ }
+
+ _decorateTarget() {
+ if (this.target && this.cursorTargetClass) {
+ this.target.classList.add(this.cursorTargetClass);
+ }
+ }
+
+ _unDecorateTarget() {
+ if (this.target && this.cursorTargetClass) {
+ this.target.classList.remove(this.cursorTargetClass);
+ }
+ }
+
+ /**
+ * Get the next stop index indicated by the delta direction.
+ *
+ * @param {number} delta either -1 or 1.
+ * @param {!Function=} opt_condition Optional stop condition.
+ * @param {boolean=} opt_clipToTop When none of the next indices match, move
+ * back to first instead of to last.
+ * @return {number} the new index.
+ * @private
+ */
+ _getNextindex(delta, opt_condition, opt_clipToTop) {
+ if (!this.stops.length || this.index === -1) {
+ return -1;
+ }
+
+ let newIndex = this.index;
+ do {
+ newIndex = newIndex + delta;
+ } while (newIndex > 0 &&
+ newIndex < this.stops.length - 1 &&
+ opt_condition && !opt_condition(this.stops[newIndex]));
+
+ newIndex = Math.max(0, Math.min(this.stops.length - 1, newIndex));
+
+ // If we failed to satisfy the condition:
+ if (opt_condition && !opt_condition(this.stops[newIndex])) {
+ if (delta < 0 || opt_clipToTop) {
+ return 0;
+ } else if (delta > 0) {
+ return this.stops.length - 1;
+ }
+ return this.index;
+ }
+
+ return newIndex;
+ }
+
+ _updateIndex() {
+ if (!this.target) {
+ this.index = -1;
+ return;
+ }
+
+ const newIndex = Array.prototype.indexOf.call(this.stops, this.target);
+ if (newIndex === -1) {
+ this.unsetCursor();
+ } else {
+ this.index = newIndex;
+ }
+ }
+
+ /**
+ * Calculate where the element is relative to the window.
+ *
+ * @param {!Object} target Target to scroll to.
+ * @return {number} Distance to top of the target.
+ */
+ _getTop(target) {
+ let top = target.offsetTop;
+ for (let offsetParent = target.offsetParent;
+ offsetParent;
+ offsetParent = offsetParent.offsetParent) {
+ top += offsetParent.offsetTop;
+ }
+ return top;
+ }
+
+ /**
+ * @return {boolean}
+ */
+ _targetIsVisible(top) {
+ const dims = this._getWindowDims();
+ return this.scrollBehavior === ScrollBehavior.KEEP_VISIBLE &&
+ top > (dims.pageYOffset + this.scrollTopMargin) &&
+ top < dims.pageYOffset + dims.innerHeight;
+ }
+
+ _calculateScrollToValue(top, target) {
+ const dims = this._getWindowDims();
+ return top + this.scrollTopMargin - (dims.innerHeight / 3) +
+ (target.offsetHeight / 2);
+ }
+
+ _scrollToTarget() {
+ if (!this.target || this.scrollBehavior === ScrollBehavior.NEVER) {
+ return;
+ }
+
+ const dims = this._getWindowDims();
+ const top = this._getTop(this.target);
+ const bottomIsVisible = this._targetHeight ?
+ this._targetIsVisible(top + this._targetHeight) : true;
+ const scrollToValue = this._calculateScrollToValue(top, this.target);
+
+ if (this._targetIsVisible(top)) {
+ // Don't scroll if either the bottom is visible or if the position that
+ // would get scrolled to is higher up than the current position. this
+ // woulld cause less of the target content to be displayed than is
+ // already.
+ if (bottomIsVisible || scrollToValue < dims.scrollY) {
+ return;
+ }
+ }
+
+ // Scroll the element to the middle of the window. Dividing by a third
+ // instead of half the inner height feels a bit better otherwise the
+ // element appears to be below the center of the window even when it
+ // isn't.
+ window.scrollTo(dims.scrollX, scrollToValue);
+ }
+
+ _getWindowDims() {
+ return {
+ scrollX: window.scrollX,
+ scrollY: window.scrollY,
+ innerHeight: window.innerHeight,
+ pageYOffset: window.pageYOffset,
+ };
+ }
+}
+
+customElements.define(GrCursorManager.is, GrCursorManager);
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_html.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_html.js
new file mode 100644
index 0000000..29757e5
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_html.js
@@ -0,0 +1,21 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
index e7d5d74..2f56ea6 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-cursor-manager</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-cursor-manager.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -41,243 +36,244 @@
</template>
</test-fixture>
-<script>
- suite('gr-cursor-manager tests', async () => {
- await readyToTest();
- let sandbox;
- let element;
- let list;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-cursor-manager.js';
+suite('gr-cursor-manager tests', () => {
+ let sandbox;
+ let element;
+ let list;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ const fixtureElements = fixture('basic');
+ element = fixtureElements[0];
+ list = fixtureElements[1];
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('core cursor functionality', () => {
+ // The element is initialized into the proper state.
+ assert.isArray(element.stops);
+ assert.equal(element.stops.length, 0);
+ assert.equal(element.index, -1);
+ assert.isNotOk(element.target);
+
+ // Initialize the cursor with its stops.
+ element.stops = list.querySelectorAll('li');
+
+ // It should have the stops but it should not be targeting any of them.
+ assert.isNotNull(element.stops);
+ assert.equal(element.stops.length, 4);
+ assert.equal(element.index, -1);
+ assert.isNotOk(element.target);
+
+ // Select the third stop.
+ element.setCursor(list.children[2]);
+
+ // It should update its internal state and update the element's class.
+ assert.equal(element.index, 2);
+ assert.equal(element.target, list.children[2]);
+ assert.isTrue(list.children[2].classList.contains('targeted'));
+ assert.isFalse(element.isAtStart());
+ assert.isFalse(element.isAtEnd());
+
+ // Progress the cursor.
+ element.next();
+
+ // Confirm that the next stop is selected and that the previous stop is
+ // unselected.
+ assert.equal(element.index, 3);
+ assert.equal(element.target, list.children[3]);
+ assert.isTrue(element.isAtEnd());
+ assert.isFalse(list.children[2].classList.contains('targeted'));
+ assert.isTrue(list.children[3].classList.contains('targeted'));
+
+ // Progress the cursor.
+ element.next();
+
+ // We should still be at the end.
+ assert.equal(element.index, 3);
+ assert.equal(element.target, list.children[3]);
+ assert.isTrue(element.isAtEnd());
+
+ // Wind the cursor all the way back to the first stop.
+ element.previous();
+ element.previous();
+ element.previous();
+
+ // The element state should reflect the end of the list.
+ assert.equal(element.index, 0);
+ assert.equal(element.target, list.children[0]);
+ assert.isTrue(element.isAtStart());
+ assert.isTrue(list.children[0].classList.contains('targeted'));
+
+ const newLi = document.createElement('li');
+ newLi.textContent = 'Z';
+ list.insertBefore(newLi, list.children[0]);
+ element.stops = list.querySelectorAll('li');
+
+ assert.equal(element.index, 1);
+
+ // De-select all targets.
+ element.unsetCursor();
+
+ // There should now be no cursor target.
+ assert.isFalse(list.children[1].classList.contains('targeted'));
+ assert.isNotOk(element.target);
+ assert.equal(element.index, -1);
+ });
+
+ test('_moveCursor', () => {
+ // Initialize the cursor with its stops.
+ element.stops = list.querySelectorAll('li');
+ // Select the first stop.
+ element.setCursor(list.children[0]);
+ const getTargetHeight = sinon.stub();
+
+ // Move the cursor without an optional get target height function.
+ element._moveCursor(1);
+ assert.isFalse(getTargetHeight.called);
+
+ // Move the cursor with an optional get target height function.
+ element._moveCursor(1, null, getTargetHeight);
+ assert.isTrue(getTargetHeight.called);
+ });
+
+ test('_moveCursor from -1 does not check height', () => {
+ element.stops = list.querySelectorAll('li');
+ const getTargetHeight = sinon.stub();
+ element._moveCursor(1, () => false, getTargetHeight);
+ assert.isFalse(getTargetHeight.called);
+ });
+
+ test('opt_noScroll', () => {
+ sandbox.stub(element, '_targetIsVisible', () => false);
+ const scrollStub = sandbox.stub(window, 'scrollTo');
+ element.stops = list.querySelectorAll('li');
+ element.scrollBehavior = 'keep-visible';
+
+ element.setCursorAtIndex(1, true);
+ assert.isFalse(scrollStub.called);
+
+ element.setCursorAtIndex(2);
+ assert.isTrue(scrollStub.called);
+ });
+
+ test('_getNextindex', () => {
+ const isLetterB = function(row) {
+ return row.textContent === 'B';
+ };
+ element.stops = list.querySelectorAll('li');
+ // Start cursor at the first stop.
+ element.setCursor(list.children[0]);
+
+ // Move forward to meet the next condition.
+ assert.equal(element._getNextindex(1, isLetterB), 1);
+ element.index = 1;
+
+ // Nothing else meets the condition, should be at last stop.
+ assert.equal(element._getNextindex(1, isLetterB), 3);
+ element.index = 3;
+
+ // Should stay at last stop if try to proceed.
+ assert.equal(element._getNextindex(1, isLetterB), 3);
+
+ // Go back to the previous condition met. Should be back at.
+ // stop 1.
+ assert.equal(element._getNextindex(-1, isLetterB), 1);
+ element.index = 1;
+
+ // Go back. No more meet the condition. Should be at stop 0.
+ assert.equal(element._getNextindex(-1, isLetterB), 0);
+ });
+
+ test('focusOnMove prop', () => {
+ const listEls = list.querySelectorAll('li');
+ for (let i = 0; i < listEls.length; i++) {
+ sandbox.spy(listEls[i], 'focus');
+ }
+ element.stops = listEls;
+ element.setCursor(list.children[0]);
+
+ element.focusOnMove = false;
+ element.next();
+ assert.isFalse(element.target.focus.called);
+
+ element.focusOnMove = true;
+ element.next();
+ assert.isTrue(element.target.focus.called);
+ });
+
+ suite('_scrollToTarget', () => {
+ let scrollStub;
setup(() => {
- sandbox = sinon.sandbox.create();
- const fixtureElements = fixture('basic');
- element = fixtureElements[0];
- list = fixtureElements[1];
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('core cursor functionality', () => {
- // The element is initialized into the proper state.
- assert.isArray(element.stops);
- assert.equal(element.stops.length, 0);
- assert.equal(element.index, -1);
- assert.isNotOk(element.target);
-
- // Initialize the cursor with its stops.
- element.stops = list.querySelectorAll('li');
-
- // It should have the stops but it should not be targeting any of them.
- assert.isNotNull(element.stops);
- assert.equal(element.stops.length, 4);
- assert.equal(element.index, -1);
- assert.isNotOk(element.target);
-
- // Select the third stop.
- element.setCursor(list.children[2]);
-
- // It should update its internal state and update the element's class.
- assert.equal(element.index, 2);
- assert.equal(element.target, list.children[2]);
- assert.isTrue(list.children[2].classList.contains('targeted'));
- assert.isFalse(element.isAtStart());
- assert.isFalse(element.isAtEnd());
-
- // Progress the cursor.
- element.next();
-
- // Confirm that the next stop is selected and that the previous stop is
- // unselected.
- assert.equal(element.index, 3);
- assert.equal(element.target, list.children[3]);
- assert.isTrue(element.isAtEnd());
- assert.isFalse(list.children[2].classList.contains('targeted'));
- assert.isTrue(list.children[3].classList.contains('targeted'));
-
- // Progress the cursor.
- element.next();
-
- // We should still be at the end.
- assert.equal(element.index, 3);
- assert.equal(element.target, list.children[3]);
- assert.isTrue(element.isAtEnd());
-
- // Wind the cursor all the way back to the first stop.
- element.previous();
- element.previous();
- element.previous();
-
- // The element state should reflect the end of the list.
- assert.equal(element.index, 0);
- assert.equal(element.target, list.children[0]);
- assert.isTrue(element.isAtStart());
- assert.isTrue(list.children[0].classList.contains('targeted'));
-
- const newLi = document.createElement('li');
- newLi.textContent = 'Z';
- list.insertBefore(newLi, list.children[0]);
- element.stops = list.querySelectorAll('li');
-
- assert.equal(element.index, 1);
-
- // De-select all targets.
- element.unsetCursor();
-
- // There should now be no cursor target.
- assert.isFalse(list.children[1].classList.contains('targeted'));
- assert.isNotOk(element.target);
- assert.equal(element.index, -1);
- });
-
- test('_moveCursor', () => {
- // Initialize the cursor with its stops.
- element.stops = list.querySelectorAll('li');
- // Select the first stop.
- element.setCursor(list.children[0]);
- const getTargetHeight = sinon.stub();
-
- // Move the cursor without an optional get target height function.
- element._moveCursor(1);
- assert.isFalse(getTargetHeight.called);
-
- // Move the cursor with an optional get target height function.
- element._moveCursor(1, null, getTargetHeight);
- assert.isTrue(getTargetHeight.called);
- });
-
- test('_moveCursor from -1 does not check height', () => {
- element.stops = list.querySelectorAll('li');
- const getTargetHeight = sinon.stub();
- element._moveCursor(1, () => false, getTargetHeight);
- assert.isFalse(getTargetHeight.called);
- });
-
- test('opt_noScroll', () => {
- sandbox.stub(element, '_targetIsVisible', () => false);
- const scrollStub = sandbox.stub(window, 'scrollTo');
element.stops = list.querySelectorAll('li');
element.scrollBehavior = 'keep-visible';
- element.setCursorAtIndex(1, true);
- assert.isFalse(scrollStub.called);
+ // There is a target which has a targetNext
+ element.setCursor(list.children[0]);
+ element._moveCursor(1);
+ scrollStub = sandbox.stub(window, 'scrollTo');
+ window.innerHeight = 60;
+ });
- element.setCursorAtIndex(2);
+ test('Called when top and bottom not visible', () => {
+ sandbox.stub(element, '_targetIsVisible').returns(false);
+ element._scrollToTarget();
assert.isTrue(scrollStub.called);
});
- test('_getNextindex', () => {
- const isLetterB = function(row) {
- return row.textContent === 'B';
- };
- element.stops = list.querySelectorAll('li');
- // Start cursor at the first stop.
- element.setCursor(list.children[0]);
-
- // Move forward to meet the next condition.
- assert.equal(element._getNextindex(1, isLetterB), 1);
- element.index = 1;
-
- // Nothing else meets the condition, should be at last stop.
- assert.equal(element._getNextindex(1, isLetterB), 3);
- element.index = 3;
-
- // Should stay at last stop if try to proceed.
- assert.equal(element._getNextindex(1, isLetterB), 3);
-
- // Go back to the previous condition met. Should be back at.
- // stop 1.
- assert.equal(element._getNextindex(-1, isLetterB), 1);
- element.index = 1;
-
- // Go back. No more meet the condition. Should be at stop 0.
- assert.equal(element._getNextindex(-1, isLetterB), 0);
+ test('Not called when top and bottom visible', () => {
+ sandbox.stub(element, '_targetIsVisible').returns(true);
+ element._scrollToTarget();
+ assert.isFalse(scrollStub.called);
});
- test('focusOnMove prop', () => {
- const listEls = list.querySelectorAll('li');
- for (let i = 0; i < listEls.length; i++) {
- sandbox.spy(listEls[i], 'focus');
- }
- element.stops = listEls;
- element.setCursor(list.children[0]);
-
- element.focusOnMove = false;
- element.next();
- assert.isFalse(element.target.focus.called);
-
- element.focusOnMove = true;
- element.next();
- assert.isTrue(element.target.focus.called);
+ test('Called when top is visible, bottom is not, scroll is lower', () => {
+ const visibleStub = sandbox.stub(element, '_targetIsVisible',
+ () => visibleStub.callCount === 2);
+ sandbox.stub(element, '_getWindowDims').returns({
+ scrollX: 123,
+ scrollY: 15,
+ innerHeight: 1000,
+ pageYOffset: 0,
+ });
+ sandbox.stub(element, '_calculateScrollToValue').returns(20);
+ element._scrollToTarget();
+ assert.isTrue(scrollStub.called);
+ assert.isTrue(scrollStub.calledWithExactly(123, 20));
+ assert.equal(visibleStub.callCount, 2);
});
- suite('_scrollToTarget', () => {
- let scrollStub;
- setup(() => {
- element.stops = list.querySelectorAll('li');
- element.scrollBehavior = 'keep-visible';
-
- // There is a target which has a targetNext
- element.setCursor(list.children[0]);
- element._moveCursor(1);
- scrollStub = sandbox.stub(window, 'scrollTo');
- window.innerHeight = 60;
+ test('Called when top is visible, bottom not, scroll is higher', () => {
+ const visibleStub = sandbox.stub(element, '_targetIsVisible',
+ () => visibleStub.callCount === 2);
+ sandbox.stub(element, '_getWindowDims').returns({
+ scrollX: 123,
+ scrollY: 25,
+ innerHeight: 1000,
+ pageYOffset: 0,
});
+ sandbox.stub(element, '_calculateScrollToValue').returns(20);
+ element._scrollToTarget();
+ assert.isFalse(scrollStub.called);
+ assert.equal(visibleStub.callCount, 2);
+ });
- test('Called when top and bottom not visible', () => {
- sandbox.stub(element, '_targetIsVisible').returns(false);
- element._scrollToTarget();
- assert.isTrue(scrollStub.called);
+ test('_calculateScrollToValue', () => {
+ sandbox.stub(element, '_getWindowDims').returns({
+ scrollX: 123,
+ scrollY: 25,
+ innerHeight: 300,
+ pageYOffset: 0,
});
-
- test('Not called when top and bottom visible', () => {
- sandbox.stub(element, '_targetIsVisible').returns(true);
- element._scrollToTarget();
- assert.isFalse(scrollStub.called);
- });
-
- test('Called when top is visible, bottom is not, scroll is lower', () => {
- const visibleStub = sandbox.stub(element, '_targetIsVisible',
- () => visibleStub.callCount === 2);
- sandbox.stub(element, '_getWindowDims').returns({
- scrollX: 123,
- scrollY: 15,
- innerHeight: 1000,
- pageYOffset: 0,
- });
- sandbox.stub(element, '_calculateScrollToValue').returns(20);
- element._scrollToTarget();
- assert.isTrue(scrollStub.called);
- assert.isTrue(scrollStub.calledWithExactly(123, 20));
- assert.equal(visibleStub.callCount, 2);
- });
-
- test('Called when top is visible, bottom not, scroll is higher', () => {
- const visibleStub = sandbox.stub(element, '_targetIsVisible',
- () => visibleStub.callCount === 2);
- sandbox.stub(element, '_getWindowDims').returns({
- scrollX: 123,
- scrollY: 25,
- innerHeight: 1000,
- pageYOffset: 0,
- });
- sandbox.stub(element, '_calculateScrollToValue').returns(20);
- element._scrollToTarget();
- assert.isFalse(scrollStub.called);
- assert.equal(visibleStub.callCount, 2);
- });
-
- test('_calculateScrollToValue', () => {
- sandbox.stub(element, '_getWindowDims').returns({
- scrollX: 123,
- scrollY: 25,
- innerHeight: 300,
- pageYOffset: 0,
- });
- assert.equal(element._calculateScrollToValue(1000, {offsetHeight: 10}),
- 905);
- });
+ assert.equal(element._calculateScrollToValue(1000, {offsetHeight: 10}),
+ 905);
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
deleted file mode 100644
index ae5a945..0000000
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
-<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<script src="../../../scripts/util.js"></script>
-
-<dom-module id="gr-date-formatter">
- <template>
- <style include="shared-styles">
- :host {
- color: inherit;
- display: inline;
- }
- </style>
- <span>
- [[_computeDateStr(dateStr, _timeFormat, _dateFormat, _relative, showDateAndTime)]]
- </span>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-date-formatter.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
index 7be041b..c46bb17 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,243 +14,253 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const Duration = {
- HOUR: 1000 * 60 * 60,
- DAY: 1000 * 60 * 60 * 24,
- };
+import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import '../../../scripts/util.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-date-formatter_html.js';
- const TimeFormats = {
- TIME_12: 'h:mm A', // 2:14 PM
- TIME_12_WITH_SEC: 'h:mm:ss A', // 2:14:00 PM
- TIME_24: 'HH:mm', // 14:14
- TIME_24_WITH_SEC: 'HH:mm:ss', // 14:14:00
- };
+const Duration = {
+ HOUR: 1000 * 60 * 60,
+ DAY: 1000 * 60 * 60 * 24,
+};
- const DateFormats = {
- STD: {
- short: 'MMM DD', // Aug 29
- full: 'MMM DD, YYYY', // Aug 29, 1997
- },
- US: {
- short: 'MM/DD', // 08/29
- full: 'MM/DD/YY', // 08/29/97
- },
- ISO: {
- short: 'MM-DD', // 08-29
- full: 'YYYY-MM-DD', // 1997-08-29
- },
- EURO: {
- short: 'DD. MMM', // 29. Aug
- full: 'DD.MM.YYYY', // 29.08.1997
- },
- UK: {
- short: 'DD/MM', // 29/08
- full: 'DD/MM/YYYY', // 29/08/1997
- },
- };
+const TimeFormats = {
+ TIME_12: 'h:mm A', // 2:14 PM
+ TIME_12_WITH_SEC: 'h:mm:ss A', // 2:14:00 PM
+ TIME_24: 'HH:mm', // 14:14
+ TIME_24_WITH_SEC: 'HH:mm:ss', // 14:14:00
+};
- /**
- * @appliesMixin Gerrit.TooltipMixin
- * @extends Polymer.Element
- */
- class GrDateFormatter extends Polymer.mixinBehaviors( [
- Gerrit.TooltipBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-date-formatter'; }
+const DateFormats = {
+ STD: {
+ short: 'MMM DD', // Aug 29
+ full: 'MMM DD, YYYY', // Aug 29, 1997
+ },
+ US: {
+ short: 'MM/DD', // 08/29
+ full: 'MM/DD/YY', // 08/29/97
+ },
+ ISO: {
+ short: 'MM-DD', // 08-29
+ full: 'YYYY-MM-DD', // 1997-08-29
+ },
+ EURO: {
+ short: 'DD. MMM', // 29. Aug
+ full: 'DD.MM.YYYY', // 29.08.1997
+ },
+ UK: {
+ short: 'DD/MM', // 29/08
+ full: 'DD/MM/YYYY', // 29/08/1997
+ },
+};
- static get properties() {
- return {
- dateStr: {
- type: String,
- value: null,
- notify: true,
- },
- showDateAndTime: {
- type: Boolean,
- value: false,
- },
+/**
+ * @appliesMixin Gerrit.TooltipMixin
+ * @extends Polymer.Element
+ */
+class GrDateFormatter extends mixinBehaviors( [
+ Gerrit.TooltipBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /**
- * When true, the detailed date appears in a GR-TOOLTIP rather than in the
- * native browser tooltip.
- */
- hasTooltip: Boolean,
+ static get is() { return 'gr-date-formatter'; }
- /**
- * The title to be used as the native tooltip or by the tooltip behavior.
- */
- title: {
- type: String,
- reflectToAttribute: true,
- computed: '_computeFullDateStr(dateStr, _timeFormat, _dateFormat)',
- },
+ static get properties() {
+ return {
+ dateStr: {
+ type: String,
+ value: null,
+ notify: true,
+ },
+ showDateAndTime: {
+ type: Boolean,
+ value: false,
+ },
- /** @type {?{short: string, full: string}} */
- _dateFormat: Object,
- _timeFormat: String, // No default value to prevent flickering.
- _relative: Boolean, // No default value to prevent flickering.
- };
- }
+ /**
+ * When true, the detailed date appears in a GR-TOOLTIP rather than in the
+ * native browser tooltip.
+ */
+ hasTooltip: Boolean,
- /** @override */
- attached() {
- super.attached();
- this._loadPreferences();
- }
+ /**
+ * The title to be used as the native tooltip or by the tooltip behavior.
+ */
+ title: {
+ type: String,
+ reflectToAttribute: true,
+ computed: '_computeFullDateStr(dateStr, _timeFormat, _dateFormat)',
+ },
- _getUtcOffsetString() {
- return ' UTC' + moment().format('Z');
- }
+ /** @type {?{short: string, full: string}} */
+ _dateFormat: Object,
+ _timeFormat: String, // No default value to prevent flickering.
+ _relative: Boolean, // No default value to prevent flickering.
+ };
+ }
- _loadPreferences() {
- return this._getLoggedIn().then(loggedIn => {
- if (!loggedIn) {
- this._timeFormat = TimeFormats.TIME_24;
- this._dateFormat = DateFormats.STD;
- this._relative = false;
- return;
- }
- return Promise.all([
- this._loadTimeFormat(),
- this._loadRelative(),
- ]);
- });
- }
+ /** @override */
+ attached() {
+ super.attached();
+ this._loadPreferences();
+ }
- _loadTimeFormat() {
- return this._getPreferences().then(preferences => {
- const timeFormat = preferences && preferences.time_format;
- const dateFormat = preferences && preferences.date_format;
- this._decideTimeFormat(timeFormat);
- this._decideDateFormat(dateFormat);
- });
- }
+ _getUtcOffsetString() {
+ return ' UTC' + moment().format('Z');
+ }
- _decideTimeFormat(timeFormat) {
- switch (timeFormat) {
- case 'HHMM_12':
- this._timeFormat = TimeFormats.TIME_12;
- break;
- case 'HHMM_24':
- this._timeFormat = TimeFormats.TIME_24;
- break;
- default:
- throw Error('Invalid time format: ' + timeFormat);
+ _loadPreferences() {
+ return this._getLoggedIn().then(loggedIn => {
+ if (!loggedIn) {
+ this._timeFormat = TimeFormats.TIME_24;
+ this._dateFormat = DateFormats.STD;
+ this._relative = false;
+ return;
}
- }
+ return Promise.all([
+ this._loadTimeFormat(),
+ this._loadRelative(),
+ ]);
+ });
+ }
- _decideDateFormat(dateFormat) {
- switch (dateFormat) {
- case 'STD':
- this._dateFormat = DateFormats.STD;
- break;
- case 'US':
- this._dateFormat = DateFormats.US;
- break;
- case 'ISO':
- this._dateFormat = DateFormats.ISO;
- break;
- case 'EURO':
- this._dateFormat = DateFormats.EURO;
- break;
- case 'UK':
- this._dateFormat = DateFormats.UK;
- break;
- default:
- throw Error('Invalid date format: ' + dateFormat);
- }
- }
+ _loadTimeFormat() {
+ return this._getPreferences().then(preferences => {
+ const timeFormat = preferences && preferences.time_format;
+ const dateFormat = preferences && preferences.date_format;
+ this._decideTimeFormat(timeFormat);
+ this._decideDateFormat(dateFormat);
+ });
+ }
- _loadRelative() {
- return this._getPreferences().then(prefs => {
- // prefs.relative_date_in_change_table is not set when false.
- this._relative = !!(prefs && prefs.relative_date_in_change_table);
- });
- }
-
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- _getPreferences() {
- return this.$.restAPI.getPreferences();
- }
-
- /**
- * Return true if date is within 24 hours and on the same day.
- */
- _isWithinDay(now, date) {
- const diff = -date.diff(now);
- return diff < Duration.DAY && date.day() === now.getDay();
- }
-
- /**
- * Returns true if date is from one to six months.
- */
- _isWithinHalfYear(now, date) {
- const diff = -date.diff(now);
- return (date.day() !== now.getDay() || diff >= Duration.DAY) &&
- diff < 180 * Duration.DAY;
- }
-
- _computeDateStr(
- dateStr, timeFormat, dateFormat, relative, showDateAndTime
- ) {
- if (!dateStr || !timeFormat || !dateFormat) { return ''; }
- const date = moment(util.parseDate(dateStr));
- if (!date.isValid()) { return ''; }
- if (relative) {
- const dateFromNow = date.fromNow();
- if (dateFromNow === 'a few seconds ago') {
- return 'just now';
- } else {
- return dateFromNow;
- }
- }
- const now = new Date();
- let format = dateFormat.full;
- if (this._isWithinDay(now, date)) {
- format = timeFormat;
- } else {
- if (this._isWithinHalfYear(now, date)) {
- format = dateFormat.short;
- }
- if (this.showDateAndTime) {
- format = `${format} ${timeFormat}`;
- }
- }
- return date.format(format);
- }
-
- _timeToSecondsFormat(timeFormat) {
- return timeFormat === TimeFormats.TIME_12 ?
- TimeFormats.TIME_12_WITH_SEC :
- TimeFormats.TIME_24_WITH_SEC;
- }
-
- _computeFullDateStr(dateStr, timeFormat, dateFormat) {
- // Polymer 2: check for undefined
- if ([
- dateStr,
- timeFormat,
- dateFormat,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- if (!dateStr) { return ''; }
- const date = moment(util.parseDate(dateStr));
- if (!date.isValid()) { return ''; }
- let format = dateFormat.full + ', ';
- format += this._timeToSecondsFormat(timeFormat);
- return date.format(format) + this._getUtcOffsetString();
+ _decideTimeFormat(timeFormat) {
+ switch (timeFormat) {
+ case 'HHMM_12':
+ this._timeFormat = TimeFormats.TIME_12;
+ break;
+ case 'HHMM_24':
+ this._timeFormat = TimeFormats.TIME_24;
+ break;
+ default:
+ throw Error('Invalid time format: ' + timeFormat);
}
}
- customElements.define(GrDateFormatter.is, GrDateFormatter);
-})();
+ _decideDateFormat(dateFormat) {
+ switch (dateFormat) {
+ case 'STD':
+ this._dateFormat = DateFormats.STD;
+ break;
+ case 'US':
+ this._dateFormat = DateFormats.US;
+ break;
+ case 'ISO':
+ this._dateFormat = DateFormats.ISO;
+ break;
+ case 'EURO':
+ this._dateFormat = DateFormats.EURO;
+ break;
+ case 'UK':
+ this._dateFormat = DateFormats.UK;
+ break;
+ default:
+ throw Error('Invalid date format: ' + dateFormat);
+ }
+ }
+
+ _loadRelative() {
+ return this._getPreferences().then(prefs => {
+ // prefs.relative_date_in_change_table is not set when false.
+ this._relative = !!(prefs && prefs.relative_date_in_change_table);
+ });
+ }
+
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _getPreferences() {
+ return this.$.restAPI.getPreferences();
+ }
+
+ /**
+ * Return true if date is within 24 hours and on the same day.
+ */
+ _isWithinDay(now, date) {
+ const diff = -date.diff(now);
+ return diff < Duration.DAY && date.day() === now.getDay();
+ }
+
+ /**
+ * Returns true if date is from one to six months.
+ */
+ _isWithinHalfYear(now, date) {
+ const diff = -date.diff(now);
+ return (date.day() !== now.getDay() || diff >= Duration.DAY) &&
+ diff < 180 * Duration.DAY;
+ }
+
+ _computeDateStr(
+ dateStr, timeFormat, dateFormat, relative, showDateAndTime
+ ) {
+ if (!dateStr || !timeFormat || !dateFormat) { return ''; }
+ const date = moment(util.parseDate(dateStr));
+ if (!date.isValid()) { return ''; }
+ if (relative) {
+ const dateFromNow = date.fromNow();
+ if (dateFromNow === 'a few seconds ago') {
+ return 'just now';
+ } else {
+ return dateFromNow;
+ }
+ }
+ const now = new Date();
+ let format = dateFormat.full;
+ if (this._isWithinDay(now, date)) {
+ format = timeFormat;
+ } else {
+ if (this._isWithinHalfYear(now, date)) {
+ format = dateFormat.short;
+ }
+ if (this.showDateAndTime) {
+ format = `${format} ${timeFormat}`;
+ }
+ }
+ return date.format(format);
+ }
+
+ _timeToSecondsFormat(timeFormat) {
+ return timeFormat === TimeFormats.TIME_12 ?
+ TimeFormats.TIME_12_WITH_SEC :
+ TimeFormats.TIME_24_WITH_SEC;
+ }
+
+ _computeFullDateStr(dateStr, timeFormat, dateFormat) {
+ // Polymer 2: check for undefined
+ if ([
+ dateStr,
+ timeFormat,
+ dateFormat,
+ ].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ if (!dateStr) { return ''; }
+ const date = moment(util.parseDate(dateStr));
+ if (!date.isValid()) { return ''; }
+ let format = dateFormat.full + ', ';
+ format += this._timeToSecondsFormat(timeFormat);
+ return date.format(format) + this._getUtcOffsetString();
+ }
+}
+
+customElements.define(GrDateFormatter.is, GrDateFormatter);
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.js
new file mode 100644
index 0000000..19aa143
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.js
@@ -0,0 +1,30 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ color: inherit;
+ display: inline;
+ }
+ </style>
+ <span>
+ [[_computeDateStr(dateStr, _timeFormat, _dateFormat, _relative, showDateAndTime)]]
+ </span>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
index 0b572bf..867f5d38 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -19,17 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-date-formatter</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="gr-date-formatter.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -37,416 +30,418 @@
</template>
</test-fixture>
-<script>
- suite('gr-date-formatter tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-date-formatter.js';
+suite('gr-date-formatter tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ /**
+ * Parse server-formatter date and normalize into current timezone.
+ */
+ function normalizedDate(dateStr) {
+ const d = util.parseDate(dateStr);
+ d.setMinutes(d.getMinutes() + d.getTimezoneOffset());
+ return d;
+ }
+
+ function testDates(nowStr, dateStr, expected, expectedWithDateAndTime,
+ expectedTooltip, done) {
+ // Normalize and convert the date to mimic server response.
+ dateStr = normalizedDate(dateStr)
+ .toJSON()
+ .replace('T', ' ')
+ .slice(0, -1);
+ sandbox.useFakeTimers(normalizedDate(nowStr).getTime());
+ element.dateStr = dateStr;
+ flush(() => {
+ const span = element.shadowRoot
+ .querySelector('span');
+ assert.equal(span.textContent.trim(), expected);
+ assert.equal(element.title, expectedTooltip);
+ element.showDateAndTime = true;
+ flushAsynchronousOperations();
+ assert.equal(span.textContent.trim(), expectedWithDateAndTime);
+ done();
+ });
+ }
+
+ function stubRestAPI(preferences) {
+ const loggedInPromise = Promise.resolve(preferences !== null);
+ const preferencesPromise = Promise.resolve(preferences);
+ stub('gr-rest-api-interface', {
+ getLoggedIn: sinon.stub().returns(loggedInPromise),
+ getPreferences: sinon.stub().returns(preferencesPromise),
+ });
+ return Promise.all([loggedInPromise, preferencesPromise]);
+ }
+
+ suite('STD + 24 hours time format preference', () => {
+ setup(() => stubRestAPI({
+ time_format: 'HHMM_24',
+ date_format: 'STD',
+ relative_date_in_change_table: false,
+ }).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ }));
+
+ test('invalid dates are quietly rejected', () => {
+ assert.notOk((new Date('foo')).valueOf());
+ assert.equal(element._computeDateStr('foo', 'h:mm A'), '');
});
- teardown(() => {
- sandbox.restore();
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '15:34',
+ '15:34',
+ 'Jul 29, 2015, 15:34:14', done);
});
- /**
- * Parse server-formatter date and normalize into current timezone.
- */
- function normalizedDate(dateStr) {
- const d = util.parseDate(dateStr);
- d.setMinutes(d.getMinutes() + d.getTimezoneOffset());
- return d;
- }
-
- function testDates(nowStr, dateStr, expected, expectedWithDateAndTime,
- expectedTooltip, done) {
- // Normalize and convert the date to mimic server response.
- dateStr = normalizedDate(dateStr)
- .toJSON()
- .replace('T', ' ')
- .slice(0, -1);
- sandbox.useFakeTimers(normalizedDate(nowStr).getTime());
- element.dateStr = dateStr;
- flush(() => {
- const span = element.shadowRoot
- .querySelector('span');
- assert.equal(span.textContent.trim(), expected);
- assert.equal(element.title, expectedTooltip);
- element.showDateAndTime = true;
- flushAsynchronousOperations();
- assert.equal(span.textContent.trim(), expectedWithDateAndTime);
- done();
- });
- }
-
- function stubRestAPI(preferences) {
- const loggedInPromise = Promise.resolve(preferences !== null);
- const preferencesPromise = Promise.resolve(preferences);
- stub('gr-rest-api-interface', {
- getLoggedIn: sinon.stub().returns(loggedInPromise),
- getPreferences: sinon.stub().returns(preferencesPromise),
- });
- return Promise.all([loggedInPromise, preferencesPromise]);
- }
-
- suite('STD + 24 hours time format preference', () => {
- setup(() => stubRestAPI({
- time_format: 'HHMM_24',
- date_format: 'STD',
- relative_date_in_change_table: false,
- }).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- }));
-
- test('invalid dates are quietly rejected', () => {
- assert.notOk((new Date('foo')).valueOf());
- assert.equal(element._computeDateStr('foo', 'h:mm A'), '');
- });
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '15:34',
- '15:34',
- 'Jul 29, 2015, 15:34:14', done);
- });
-
- test('Within 24 hours on different days', done => {
- testDates('2015-07-29 03:34:14.985000000',
- '2015-07-28 20:25:14.985000000',
- 'Jul 28',
- 'Jul 28 20:25',
- 'Jul 28, 2015, 20:25:14', done);
- });
-
- test('More than 24 hours but less than six months', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-06-15 03:25:14.985000000',
- 'Jun 15',
- 'Jun 15 03:25',
- 'Jun 15, 2015, 03:25:14', done);
- });
-
- test('More than six months', done => {
- testDates('2015-09-15 20:34:00.000000000',
- '2015-01-15 03:25:00.000000000',
- 'Jan 15, 2015',
- 'Jan 15, 2015 03:25',
- 'Jan 15, 2015, 03:25:00', done);
- });
+ test('Within 24 hours on different days', done => {
+ testDates('2015-07-29 03:34:14.985000000',
+ '2015-07-28 20:25:14.985000000',
+ 'Jul 28',
+ 'Jul 28 20:25',
+ 'Jul 28, 2015, 20:25:14', done);
});
- suite('US + 24 hours time format preference', () => {
- setup(() => stubRestAPI({
- time_format: 'HHMM_24',
- date_format: 'US',
- relative_date_in_change_table: false,
- }).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- }));
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '15:34',
- '15:34',
- '07/29/15, 15:34:14', done);
- });
-
- test('Within 24 hours on different days', done => {
- testDates('2015-07-29 03:34:14.985000000',
- '2015-07-28 20:25:14.985000000',
- '07/28',
- '07/28 20:25',
- '07/28/15, 20:25:14', done);
- });
-
- test('More than 24 hours but less than six months', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-06-15 03:25:14.985000000',
- '06/15',
- '06/15 03:25',
- '06/15/15, 03:25:14', done);
- });
+ test('More than 24 hours but less than six months', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-06-15 03:25:14.985000000',
+ 'Jun 15',
+ 'Jun 15 03:25',
+ 'Jun 15, 2015, 03:25:14', done);
});
- suite('ISO + 24 hours time format preference', () => {
- setup(() => stubRestAPI({
- time_format: 'HHMM_24',
- date_format: 'ISO',
- relative_date_in_change_table: false,
- }).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- }));
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '15:34',
- '15:34',
- '2015-07-29, 15:34:14', done);
- });
-
- test('Within 24 hours on different days', done => {
- testDates('2015-07-29 03:34:14.985000000',
- '2015-07-28 20:25:14.985000000',
- '07-28',
- '07-28 20:25',
- '2015-07-28, 20:25:14', done);
- });
-
- test('More than 24 hours but less than six months', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-06-15 03:25:14.985000000',
- '06-15',
- '06-15 03:25',
- '2015-06-15, 03:25:14', done);
- });
- });
-
- suite('EURO + 24 hours time format preference', () => {
- setup(() => stubRestAPI({
- time_format: 'HHMM_24',
- date_format: 'EURO',
- relative_date_in_change_table: false,
- }).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- }));
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '15:34',
- '15:34',
- '29.07.2015, 15:34:14', done);
- });
-
- test('Within 24 hours on different days', done => {
- testDates('2015-07-29 03:34:14.985000000',
- '2015-07-28 20:25:14.985000000',
- '28. Jul',
- '28. Jul 20:25',
- '28.07.2015, 20:25:14', done);
- });
-
- test('More than 24 hours but less than six months', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-06-15 03:25:14.985000000',
- '15. Jun',
- '15. Jun 03:25',
- '15.06.2015, 03:25:14', done);
- });
- });
-
- suite('UK + 24 hours time format preference', () => {
- setup(() => stubRestAPI({
- time_format: 'HHMM_24',
- date_format: 'UK',
- relative_date_in_change_table: false,
- }).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- }));
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '15:34',
- '15:34',
- '29/07/2015, 15:34:14', done);
- });
-
- test('Within 24 hours on different days', done => {
- testDates('2015-07-29 03:34:14.985000000',
- '2015-07-28 20:25:14.985000000',
- '28/07',
- '28/07 20:25',
- '28/07/2015, 20:25:14', done);
- });
-
- test('More than 24 hours but less than six months', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-06-15 03:25:14.985000000',
- '15/06',
- '15/06 03:25',
- '15/06/2015, 03:25:14', done);
- });
- });
-
- suite('STD + 12 hours time format preference', () => {
- setup(() =>
- // relative_date_in_change_table is not set when false.
- stubRestAPI(
- {time_format: 'HHMM_12', date_format: 'STD'}
- ).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- })
- );
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '3:34 PM',
- '3:34 PM',
- 'Jul 29, 2015, 3:34:14 PM', done);
- });
- });
-
- suite('US + 12 hours time format preference', () => {
- setup(() =>
- // relative_date_in_change_table is not set when false.
- stubRestAPI(
- {time_format: 'HHMM_12', date_format: 'US'}
- ).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- })
- );
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '3:34 PM',
- '3:34 PM',
- '07/29/15, 3:34:14 PM', done);
- });
- });
-
- suite('ISO + 12 hours time format preference', () => {
- setup(() =>
- // relative_date_in_change_table is not set when false.
- stubRestAPI(
- {time_format: 'HHMM_12', date_format: 'ISO'}
- ).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- })
- );
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '3:34 PM',
- '3:34 PM',
- '2015-07-29, 3:34:14 PM', done);
- });
- });
-
- suite('EURO + 12 hours time format preference', () => {
- setup(() =>
- // relative_date_in_change_table is not set when false.
- stubRestAPI(
- {time_format: 'HHMM_12', date_format: 'EURO'}
- ).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- })
- );
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '3:34 PM',
- '3:34 PM',
- '29.07.2015, 3:34:14 PM', done);
- });
- });
-
- suite('UK + 12 hours time format preference', () => {
- setup(() =>
- // relative_date_in_change_table is not set when false.
- stubRestAPI(
- {time_format: 'HHMM_12', date_format: 'UK'}
- ).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- })
- );
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '3:34 PM',
- '3:34 PM',
- '29/07/2015, 3:34:14 PM', done);
- });
- });
-
- suite('relative date preference', () => {
- setup(() => stubRestAPI({
- time_format: 'HHMM_12',
- date_format: 'STD',
- relative_date_in_change_table: true,
- }).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- }));
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '5 hours ago',
- '5 hours ago',
- 'Jul 29, 2015, 3:34:14 PM', done);
- });
-
- test('More than six months', done => {
- testDates('2015-09-15 20:34:00.000000000',
- '2015-01-15 03:25:00.000000000',
- '8 months ago',
- '8 months ago',
- 'Jan 15, 2015, 3:25:00 AM', done);
- });
- });
-
- suite('logged in', () => {
- setup(() => stubRestAPI({
- time_format: 'HHMM_12',
- date_format: 'US',
- relative_date_in_change_table: true,
- }).then(() => {
- element = fixture('basic');
- return element._loadPreferences();
- }));
-
- test('Preferences are respected', () => {
- assert.equal(element._timeFormat, 'h:mm A');
- assert.equal(element._dateFormat.short, 'MM/DD');
- assert.equal(element._dateFormat.full, 'MM/DD/YY');
- assert.isTrue(element._relative);
- });
- });
-
- suite('logged out', () => {
- setup(() => stubRestAPI(null).then(() => {
- element = fixture('basic');
- return element._loadPreferences();
- }));
-
- test('Default preferences are respected', () => {
- assert.equal(element._timeFormat, 'HH:mm');
- assert.equal(element._dateFormat.short, 'MMM DD');
- assert.equal(element._dateFormat.full, 'MMM DD, YYYY');
- assert.isFalse(element._relative);
- });
+ test('More than six months', done => {
+ testDates('2015-09-15 20:34:00.000000000',
+ '2015-01-15 03:25:00.000000000',
+ 'Jan 15, 2015',
+ 'Jan 15, 2015 03:25',
+ 'Jan 15, 2015, 03:25:00', done);
});
});
+
+ suite('US + 24 hours time format preference', () => {
+ setup(() => stubRestAPI({
+ time_format: 'HHMM_24',
+ date_format: 'US',
+ relative_date_in_change_table: false,
+ }).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ }));
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '15:34',
+ '15:34',
+ '07/29/15, 15:34:14', done);
+ });
+
+ test('Within 24 hours on different days', done => {
+ testDates('2015-07-29 03:34:14.985000000',
+ '2015-07-28 20:25:14.985000000',
+ '07/28',
+ '07/28 20:25',
+ '07/28/15, 20:25:14', done);
+ });
+
+ test('More than 24 hours but less than six months', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-06-15 03:25:14.985000000',
+ '06/15',
+ '06/15 03:25',
+ '06/15/15, 03:25:14', done);
+ });
+ });
+
+ suite('ISO + 24 hours time format preference', () => {
+ setup(() => stubRestAPI({
+ time_format: 'HHMM_24',
+ date_format: 'ISO',
+ relative_date_in_change_table: false,
+ }).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ }));
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '15:34',
+ '15:34',
+ '2015-07-29, 15:34:14', done);
+ });
+
+ test('Within 24 hours on different days', done => {
+ testDates('2015-07-29 03:34:14.985000000',
+ '2015-07-28 20:25:14.985000000',
+ '07-28',
+ '07-28 20:25',
+ '2015-07-28, 20:25:14', done);
+ });
+
+ test('More than 24 hours but less than six months', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-06-15 03:25:14.985000000',
+ '06-15',
+ '06-15 03:25',
+ '2015-06-15, 03:25:14', done);
+ });
+ });
+
+ suite('EURO + 24 hours time format preference', () => {
+ setup(() => stubRestAPI({
+ time_format: 'HHMM_24',
+ date_format: 'EURO',
+ relative_date_in_change_table: false,
+ }).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ }));
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '15:34',
+ '15:34',
+ '29.07.2015, 15:34:14', done);
+ });
+
+ test('Within 24 hours on different days', done => {
+ testDates('2015-07-29 03:34:14.985000000',
+ '2015-07-28 20:25:14.985000000',
+ '28. Jul',
+ '28. Jul 20:25',
+ '28.07.2015, 20:25:14', done);
+ });
+
+ test('More than 24 hours but less than six months', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-06-15 03:25:14.985000000',
+ '15. Jun',
+ '15. Jun 03:25',
+ '15.06.2015, 03:25:14', done);
+ });
+ });
+
+ suite('UK + 24 hours time format preference', () => {
+ setup(() => stubRestAPI({
+ time_format: 'HHMM_24',
+ date_format: 'UK',
+ relative_date_in_change_table: false,
+ }).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ }));
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '15:34',
+ '15:34',
+ '29/07/2015, 15:34:14', done);
+ });
+
+ test('Within 24 hours on different days', done => {
+ testDates('2015-07-29 03:34:14.985000000',
+ '2015-07-28 20:25:14.985000000',
+ '28/07',
+ '28/07 20:25',
+ '28/07/2015, 20:25:14', done);
+ });
+
+ test('More than 24 hours but less than six months', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-06-15 03:25:14.985000000',
+ '15/06',
+ '15/06 03:25',
+ '15/06/2015, 03:25:14', done);
+ });
+ });
+
+ suite('STD + 12 hours time format preference', () => {
+ setup(() =>
+ // relative_date_in_change_table is not set when false.
+ stubRestAPI(
+ {time_format: 'HHMM_12', date_format: 'STD'}
+ ).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ })
+ );
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '3:34 PM',
+ '3:34 PM',
+ 'Jul 29, 2015, 3:34:14 PM', done);
+ });
+ });
+
+ suite('US + 12 hours time format preference', () => {
+ setup(() =>
+ // relative_date_in_change_table is not set when false.
+ stubRestAPI(
+ {time_format: 'HHMM_12', date_format: 'US'}
+ ).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ })
+ );
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '3:34 PM',
+ '3:34 PM',
+ '07/29/15, 3:34:14 PM', done);
+ });
+ });
+
+ suite('ISO + 12 hours time format preference', () => {
+ setup(() =>
+ // relative_date_in_change_table is not set when false.
+ stubRestAPI(
+ {time_format: 'HHMM_12', date_format: 'ISO'}
+ ).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ })
+ );
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '3:34 PM',
+ '3:34 PM',
+ '2015-07-29, 3:34:14 PM', done);
+ });
+ });
+
+ suite('EURO + 12 hours time format preference', () => {
+ setup(() =>
+ // relative_date_in_change_table is not set when false.
+ stubRestAPI(
+ {time_format: 'HHMM_12', date_format: 'EURO'}
+ ).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ })
+ );
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '3:34 PM',
+ '3:34 PM',
+ '29.07.2015, 3:34:14 PM', done);
+ });
+ });
+
+ suite('UK + 12 hours time format preference', () => {
+ setup(() =>
+ // relative_date_in_change_table is not set when false.
+ stubRestAPI(
+ {time_format: 'HHMM_12', date_format: 'UK'}
+ ).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ })
+ );
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '3:34 PM',
+ '3:34 PM',
+ '29/07/2015, 3:34:14 PM', done);
+ });
+ });
+
+ suite('relative date preference', () => {
+ setup(() => stubRestAPI({
+ time_format: 'HHMM_12',
+ date_format: 'STD',
+ relative_date_in_change_table: true,
+ }).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ }));
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '5 hours ago',
+ '5 hours ago',
+ 'Jul 29, 2015, 3:34:14 PM', done);
+ });
+
+ test('More than six months', done => {
+ testDates('2015-09-15 20:34:00.000000000',
+ '2015-01-15 03:25:00.000000000',
+ '8 months ago',
+ '8 months ago',
+ 'Jan 15, 2015, 3:25:00 AM', done);
+ });
+ });
+
+ suite('logged in', () => {
+ setup(() => stubRestAPI({
+ time_format: 'HHMM_12',
+ date_format: 'US',
+ relative_date_in_change_table: true,
+ }).then(() => {
+ element = fixture('basic');
+ return element._loadPreferences();
+ }));
+
+ test('Preferences are respected', () => {
+ assert.equal(element._timeFormat, 'h:mm A');
+ assert.equal(element._dateFormat.short, 'MM/DD');
+ assert.equal(element._dateFormat.full, 'MM/DD/YY');
+ assert.isTrue(element._relative);
+ });
+ });
+
+ suite('logged out', () => {
+ setup(() => stubRestAPI(null).then(() => {
+ element = fixture('basic');
+ return element._loadPreferences();
+ }));
+
+ test('Default preferences are respected', () => {
+ assert.equal(element._timeFormat, 'HH:mm');
+ assert.equal(element._dateFormat.short, 'MMM DD');
+ assert.equal(element._dateFormat.full, 'MMM DD, YYYY');
+ assert.isFalse(element._relative);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html
deleted file mode 100644
index 475f6e2..0000000
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html
+++ /dev/null
@@ -1,86 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../gr-button/gr-button.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-dialog">
- <template>
- <style include="shared-styles">
- :host {
- color: var(--primary-text-color);
- display: block;
- max-height: 90vh;
- overflow: auto;
- }
- .container {
- display: flex;
- flex-direction: column;
- max-height: 90vh;
- padding: var(--spacing-xl);
- }
- header {
- flex-shrink: 0;
- padding-bottom: var(--spacing-xl);
- }
- main {
- display: flex;
- flex-shrink: 1;
- width: 100%;
- flex: 1;
- /* IMPORTANT: required for firefox */
- min-height: 0px;
- }
- main .overflow-container {
- flex: 1;
- overflow: auto;
- }
- footer {
- display: flex;
- flex-shrink: 0;
- justify-content: flex-end;
- padding-top: var(--spacing-xl);
- }
- gr-button {
- margin-left: var(--spacing-l);
- }
- .hidden {
- display: none;
- }
- </style>
- <div class="container" on-keydown="_handleKeydown">
- <header class="font-h3"><slot name="header"></slot></header>
- <main>
- <div class="overflow-container">
- <slot name="main"></slot>
- </div>
- </main>
- <footer>
- <slot name="footer"></slot>
- <gr-button id="cancel" class$="[[_computeCancelClass(cancelLabel)]]" link on-click="_handleCancelTap">
- [[cancelLabel]]
- </gr-button>
- <gr-button id="confirm" link primary on-click="_handleConfirm" disabled="[[disabled]]">
- [[confirmLabel]]
- </gr-button>
- </footer>
- </div>
- </template>
- <script src="gr-dialog.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
index 8d00452..8141863 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
@@ -14,85 +14,94 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../gr-button/gr-button.js';
+import '../../../styles/shared-styles.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-dialog_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
*/
- class GrDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
- confirmLabel: {
- type: String,
- value: 'Confirm',
- },
- // Supplying an empty cancel label will hide the button completely.
- cancelLabel: {
- type: String,
- value: 'Cancel',
- },
- disabled: {
- type: Boolean,
- value: false,
- },
- confirmOnEnter: {
- type: Boolean,
- value: false,
- },
- };
- }
-
- /** @override */
- ready() {
- super.ready();
- this._ensureAttribute('role', 'dialog');
- }
-
- _handleConfirm(e) {
- if (this.disabled) { return; }
-
- e.preventDefault();
- e.stopPropagation();
- this.fire('confirm', null, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', null, {bubbles: false});
- }
-
- _handleKeydown(e) {
- if (this.confirmOnEnter && e.keyCode === 13) { this._handleConfirm(e); }
- }
-
- resetFocus() {
- this.$.confirm.focus();
- }
-
- _computeCancelClass(cancelLabel) {
- return cancelLabel.length ? '' : 'hidden';
- }
+ static get properties() {
+ return {
+ confirmLabel: {
+ type: String,
+ value: 'Confirm',
+ },
+ // Supplying an empty cancel label will hide the button completely.
+ cancelLabel: {
+ type: String,
+ value: 'Cancel',
+ },
+ disabled: {
+ type: Boolean,
+ value: false,
+ },
+ confirmOnEnter: {
+ type: Boolean,
+ value: false,
+ },
+ };
}
- customElements.define(GrDialog.is, GrDialog);
-})();
+ /** @override */
+ ready() {
+ super.ready();
+ this._ensureAttribute('role', 'dialog');
+ }
+
+ _handleConfirm(e) {
+ if (this.disabled) { return; }
+
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('confirm', null, {bubbles: false});
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', null, {bubbles: false});
+ }
+
+ _handleKeydown(e) {
+ if (this.confirmOnEnter && e.keyCode === 13) { this._handleConfirm(e); }
+ }
+
+ resetFocus() {
+ this.$.confirm.focus();
+ }
+
+ _computeCancelClass(cancelLabel) {
+ return cancelLabel.length ? '' : 'hidden';
+ }
+}
+
+customElements.define(GrDialog.is, GrDialog);
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js
new file mode 100644
index 0000000..ba85fd4
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js
@@ -0,0 +1,79 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ color: var(--primary-text-color);
+ display: block;
+ max-height: 90vh;
+ overflow: auto;
+ }
+ .container {
+ display: flex;
+ flex-direction: column;
+ max-height: 90vh;
+ padding: var(--spacing-xl);
+ }
+ header {
+ flex-shrink: 0;
+ padding-bottom: var(--spacing-xl);
+ }
+ main {
+ display: flex;
+ flex-shrink: 1;
+ width: 100%;
+ flex: 1;
+ /* IMPORTANT: required for firefox */
+ min-height: 0px;
+ }
+ main .overflow-container {
+ flex: 1;
+ overflow: auto;
+ }
+ footer {
+ display: flex;
+ flex-shrink: 0;
+ justify-content: flex-end;
+ padding-top: var(--spacing-xl);
+ }
+ gr-button {
+ margin-left: var(--spacing-l);
+ }
+ .hidden {
+ display: none;
+ }
+ </style>
+ <div class="container" on-keydown="_handleKeydown">
+ <header class="font-h3"><slot name="header"></slot></header>
+ <main>
+ <div class="overflow-container">
+ <slot name="main"></slot>
+ </div>
+ </main>
+ <footer>
+ <slot name="footer"></slot>
+ <gr-button id="cancel" class\$="[[_computeCancelClass(cancelLabel)]]" link="" on-click="_handleCancelTap">
+ [[cancelLabel]]
+ </gr-button>
+ <gr-button id="confirm" link="" primary="" on-click="_handleConfirm" disabled="[[disabled]]">
+ [[confirmLabel]]
+ </gr-button>
+ </footer>
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html
index ad93962..ded607f 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-dialog</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-dialog.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,65 +30,66 @@
</template>
</test-fixture>
-<script>
- suite('gr-dialog tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-dialog.js';
+suite('gr-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('events', done => {
- let numEvents = 0;
- function handler() { if (++numEvents == 2) { done(); } }
-
- element.addEventListener('confirm', handler);
- element.addEventListener('cancel', handler);
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button[primary]'));
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button:not([primary])'));
- });
-
- test('confirmOnEnter', () => {
- element.confirmOnEnter = false;
- const handleConfirmStub = sandbox.stub(element, '_handleConfirm');
- const handleKeydownSpy = sandbox.spy(element, '_handleKeydown');
- MockInteractions.pressAndReleaseKeyOn(element.shadowRoot
- .querySelector('main'),
- 13, null, 'enter');
- flushAsynchronousOperations();
-
- assert.isTrue(handleKeydownSpy.called);
- assert.isFalse(handleConfirmStub.called);
-
- element.confirmOnEnter = true;
- MockInteractions.pressAndReleaseKeyOn(element.shadowRoot
- .querySelector('main'),
- 13, null, 'enter');
- flushAsynchronousOperations();
-
- assert.isTrue(handleConfirmStub.called);
- });
-
- test('resetFocus', () => {
- const focusStub = sandbox.stub(element.$.confirm, 'focus');
- element.resetFocus();
- assert.isTrue(focusStub.calledOnce);
- });
-
- test('empty cancel label hides cancel btn', () => {
- assert.isFalse(isHidden(element.$.cancel));
- element.cancelLabel = '';
- flushAsynchronousOperations();
-
- assert.isTrue(isHidden(element.$.cancel));
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('events', done => {
+ let numEvents = 0;
+ function handler() { if (++numEvents == 2) { done(); } }
+
+ element.addEventListener('confirm', handler);
+ element.addEventListener('cancel', handler);
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button[primary]'));
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button:not([primary])'));
+ });
+
+ test('confirmOnEnter', () => {
+ element.confirmOnEnter = false;
+ const handleConfirmStub = sandbox.stub(element, '_handleConfirm');
+ const handleKeydownSpy = sandbox.spy(element, '_handleKeydown');
+ MockInteractions.pressAndReleaseKeyOn(element.shadowRoot
+ .querySelector('main'),
+ 13, null, 'enter');
+ flushAsynchronousOperations();
+
+ assert.isTrue(handleKeydownSpy.called);
+ assert.isFalse(handleConfirmStub.called);
+
+ element.confirmOnEnter = true;
+ MockInteractions.pressAndReleaseKeyOn(element.shadowRoot
+ .querySelector('main'),
+ 13, null, 'enter');
+ flushAsynchronousOperations();
+
+ assert.isTrue(handleConfirmStub.called);
+ });
+
+ test('resetFocus', () => {
+ const focusStub = sandbox.stub(element.$.confirm, 'focus');
+ element.resetFocus();
+ assert.isTrue(focusStub.calledOnce);
+ });
+
+ test('empty cancel label hides cancel btn', () => {
+ assert.isFalse(isHidden(element.$.cancel));
+ element.cancelLabel = '';
+ flushAsynchronousOperations();
+
+ assert.isTrue(isHidden(element.$.cancel));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html
deleted file mode 100644
index 367e30c..0000000
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.html
+++ /dev/null
@@ -1,191 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../gr-button/gr-button.html">
-<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-select/gr-select.html">
-
-<dom-module id="gr-diff-preferences">
- <template>
- <style include="shared-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>
- <div id="diffPreferences" class="gr-form-styles">
- <section>
- <span class="title">Context</span>
- <span class="value">
- <gr-select
- id="contextSelect"
- bind-value="{{diffPrefs.context}}">
- <select
- on-keypress="_handleDiffPrefsChanged"
- on-change="_handleDiffPrefsChanged">
- <option value="3">3 lines</option>
- <option value="10">10 lines</option>
- <option value="25">25 lines</option>
- <option value="50">50 lines</option>
- <option value="75">75 lines</option>
- <option value="100">100 lines</option>
- <option value="-1">Whole file</option>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Fit to screen</span>
- <span class="value">
- <input
- id="lineWrappingInput"
- type="checkbox"
- checked$="[[diffPrefs.line_wrapping]]"
- on-change="_handleLineWrappingTap">
- </span>
- </section>
- <section>
- <span class="title">Diff width</span>
- <span class="value">
- <iron-input
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{diffPrefs.line_length}}"
- on-keypress="_handleDiffPrefsChanged"
- on-change="_handleDiffPrefsChanged">
- <input
- is="iron-input"
- type="number"
- id="columnsInput"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{diffPrefs.line_length}}"
- on-keypress="_handleDiffPrefsChanged"
- on-change="_handleDiffPrefsChanged">
- </iron-input>
- </span>
- </section>
- <section>
- <span class="title">Tab width</span>
- <span class="value">
- <iron-input
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{diffPrefs.tab_size}}"
- on-keypress="_handleDiffPrefsChanged"
- on-change="_handleDiffPrefsChanged">
- <input
- is="iron-input"
- type="number"
- id="tabSizeInput"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{diffPrefs.tab_size}}"
- on-keypress="_handleDiffPrefsChanged"
- on-change="_handleDiffPrefsChanged">
- </iron-input>
- </span>
- </section>
- <section hidden$="[[!diffPrefs.font_size]]">
- <span class="title">Font size</span>
- <span class="value">
- <iron-input
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{diffPrefs.font_size}}"
- on-keypress="_handleDiffPrefsChanged"
- on-change="_handleDiffPrefsChanged">
- <input
- is="iron-input"
- type="number"
- id="fontSizeInput"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{diffPrefs.font_size}}"
- on-keypress="_handleDiffPrefsChanged"
- on-change="_handleDiffPrefsChanged">
- </iron-input>
- </span>
- </section>
- <section>
- <span class="title">Show tabs</span>
- <span class="value">
- <input
- id="showTabsInput"
- type="checkbox"
- checked$="[[diffPrefs.show_tabs]]"
- on-change="_handleShowTabsTap">
- </span>
- </section>
- <section>
- <span class="title">Show trailing whitespace</span>
- <span class="value">
- <input
- id="showTrailingWhitespaceInput"
- type="checkbox"
- checked$="[[diffPrefs.show_whitespace_errors]]"
- on-change="_handleShowTrailingWhitespaceTap">
- </span>
- </section>
- <section>
- <span class="title">Syntax highlighting</span>
- <span class="value">
- <input
- id="syntaxHighlightInput"
- type="checkbox"
- checked$="[[diffPrefs.syntax_highlighting]]"
- on-change="_handleSyntaxHighlightTap">
- </span>
- </section>
- <section>
- <span class="title">Automatically mark viewed files reviewed</span>
- <span class="value">
- <input
- id="automaticReviewInput"
- type="checkbox"
- checked$="[[!diffPrefs.manual_review]]"
- on-change="_handleAutomaticReviewTap">
- </span>
- </section>
- <section>
- <div class="pref">
- <span class="title">Ignore Whitespace</span>
- <span class="value">
- <gr-select bind-value="{{diffPrefs.ignore_whitespace}}">
- <select
- on-keypress="_handleDiffPrefsChanged"
- on-change="_handleDiffPrefsChanged">
- <option value="IGNORE_NONE">None</option>
- <option value="IGNORE_TRAILING">Trailing</option>
- <option value="IGNORE_LEADING_AND_TRAILING">Leading & trailing</option>
- <option value="IGNORE_ALL">All</option>
- </select>
- </gr-select>
- </span>
- </div>
- </section>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-diff-preferences.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
index c408e5a..00f9078 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
@@ -14,72 +14,82 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrDiffPreferences extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-diff-preferences'; }
+import '@polymer/iron-input/iron-input.js';
+import '../../../styles/shared-styles.js';
+import '../gr-button/gr-button.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-select/gr-select.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-diff-preferences_html.js';
- static get properties() {
- return {
- hasUnsavedChanges: {
- type: Boolean,
- notify: true,
- value: false,
- },
+/** @extends Polymer.Element */
+class GrDiffPreferences extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /** @type {?} */
- diffPrefs: Object,
- };
- }
+ static get is() { return 'gr-diff-preferences'; }
- loadData() {
- return this.$.restAPI.getDiffPreferences().then(prefs => {
- this.diffPrefs = prefs;
- });
- }
+ static get properties() {
+ return {
+ hasUnsavedChanges: {
+ type: Boolean,
+ notify: true,
+ value: false,
+ },
- _handleDiffPrefsChanged() {
- this.hasUnsavedChanges = true;
- }
-
- _handleLineWrappingTap() {
- this.set('diffPrefs.line_wrapping', this.$.lineWrappingInput.checked);
- this._handleDiffPrefsChanged();
- }
-
- _handleShowTabsTap() {
- this.set('diffPrefs.show_tabs', this.$.showTabsInput.checked);
- this._handleDiffPrefsChanged();
- }
-
- _handleShowTrailingWhitespaceTap() {
- this.set('diffPrefs.show_whitespace_errors',
- this.$.showTrailingWhitespaceInput.checked);
- this._handleDiffPrefsChanged();
- }
-
- _handleSyntaxHighlightTap() {
- this.set('diffPrefs.syntax_highlighting',
- this.$.syntaxHighlightInput.checked);
- this._handleDiffPrefsChanged();
- }
-
- _handleAutomaticReviewTap() {
- this.set('diffPrefs.manual_review',
- !this.$.automaticReviewInput.checked);
- this._handleDiffPrefsChanged();
- }
-
- save() {
- return this.$.restAPI.saveDiffPreferences(this.diffPrefs).then(res => {
- this.hasUnsavedChanges = false;
- });
- }
+ /** @type {?} */
+ diffPrefs: Object,
+ };
}
- customElements.define(GrDiffPreferences.is, GrDiffPreferences);
-})();
+ loadData() {
+ return this.$.restAPI.getDiffPreferences().then(prefs => {
+ this.diffPrefs = prefs;
+ });
+ }
+
+ _handleDiffPrefsChanged() {
+ this.hasUnsavedChanges = true;
+ }
+
+ _handleLineWrappingTap() {
+ this.set('diffPrefs.line_wrapping', this.$.lineWrappingInput.checked);
+ this._handleDiffPrefsChanged();
+ }
+
+ _handleShowTabsTap() {
+ this.set('diffPrefs.show_tabs', this.$.showTabsInput.checked);
+ this._handleDiffPrefsChanged();
+ }
+
+ _handleShowTrailingWhitespaceTap() {
+ this.set('diffPrefs.show_whitespace_errors',
+ this.$.showTrailingWhitespaceInput.checked);
+ this._handleDiffPrefsChanged();
+ }
+
+ _handleSyntaxHighlightTap() {
+ this.set('diffPrefs.syntax_highlighting',
+ this.$.syntaxHighlightInput.checked);
+ this._handleDiffPrefsChanged();
+ }
+
+ _handleAutomaticReviewTap() {
+ this.set('diffPrefs.manual_review',
+ !this.$.automaticReviewInput.checked);
+ this._handleDiffPrefsChanged();
+ }
+
+ save() {
+ return this.$.restAPI.saveDiffPreferences(this.diffPrefs).then(res => {
+ this.hasUnsavedChanges = false;
+ });
+ }
+}
+
+customElements.define(GrDiffPreferences.is, GrDiffPreferences);
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.js b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.js
new file mode 100644
index 0000000..7869c2c
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.js
@@ -0,0 +1,114 @@
+/**
+ * @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.js';
+
+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">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <div id="diffPreferences" class="gr-form-styles">
+ <section>
+ <span class="title">Context</span>
+ <span class="value">
+ <gr-select id="contextSelect" bind-value="{{diffPrefs.context}}">
+ <select on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged">
+ <option value="3">3 lines</option>
+ <option value="10">10 lines</option>
+ <option value="25">25 lines</option>
+ <option value="50">50 lines</option>
+ <option value="75">75 lines</option>
+ <option value="100">100 lines</option>
+ <option value="-1">Whole file</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title">Fit to screen</span>
+ <span class="value">
+ <input id="lineWrappingInput" type="checkbox" checked\$="[[diffPrefs.line_wrapping]]" on-change="_handleLineWrappingTap">
+ </span>
+ </section>
+ <section>
+ <span class="title">Diff width</span>
+ <span class="value">
+ <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.line_length}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged">
+ <input is="iron-input" type="number" id="columnsInput" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.line_length}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged">
+ </iron-input>
+ </span>
+ </section>
+ <section>
+ <span class="title">Tab width</span>
+ <span class="value">
+ <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.tab_size}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged">
+ <input is="iron-input" type="number" id="tabSizeInput" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.tab_size}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged">
+ </iron-input>
+ </span>
+ </section>
+ <section hidden\$="[[!diffPrefs.font_size]]">
+ <span class="title">Font size</span>
+ <span class="value">
+ <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.font_size}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged">
+ <input is="iron-input" type="number" id="fontSizeInput" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.font_size}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged">
+ </iron-input>
+ </span>
+ </section>
+ <section>
+ <span class="title">Show tabs</span>
+ <span class="value">
+ <input id="showTabsInput" type="checkbox" checked\$="[[diffPrefs.show_tabs]]" on-change="_handleShowTabsTap">
+ </span>
+ </section>
+ <section>
+ <span class="title">Show trailing whitespace</span>
+ <span class="value">
+ <input id="showTrailingWhitespaceInput" type="checkbox" checked\$="[[diffPrefs.show_whitespace_errors]]" on-change="_handleShowTrailingWhitespaceTap">
+ </span>
+ </section>
+ <section>
+ <span class="title">Syntax highlighting</span>
+ <span class="value">
+ <input id="syntaxHighlightInput" type="checkbox" checked\$="[[diffPrefs.syntax_highlighting]]" on-change="_handleSyntaxHighlightTap">
+ </span>
+ </section>
+ <section>
+ <span class="title">Automatically mark viewed files reviewed</span>
+ <span class="value">
+ <input id="automaticReviewInput" type="checkbox" checked\$="[[!diffPrefs.manual_review]]" on-change="_handleAutomaticReviewTap">
+ </span>
+ </section>
+ <section>
+ <div class="pref">
+ <span class="title">Ignore Whitespace</span>
+ <span class="value">
+ <gr-select bind-value="{{diffPrefs.ignore_whitespace}}">
+ <select on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged">
+ <option value="IGNORE_NONE">None</option>
+ <option value="IGNORE_TRAILING">Trailing</option>
+ <option value="IGNORE_LEADING_AND_TRAILING">Leading & trailing</option>
+ <option value="IGNORE_ALL">All</option>
+ </select>
+ </gr-select>
+ </span>
+ </div>
+ </section>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
index 3c2a7d1..5607a3d 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-preferences</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-diff-preferences.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,93 +30,94 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff-preferences tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let diffPreferences;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-diff-preferences.js';
+suite('gr-diff-preferences tests', () => {
+ let element;
+ let sandbox;
+ let diffPreferences;
- function valueOf(title, fieldsetid) {
- const sections = element.$[fieldsetid].querySelectorAll('section');
- let titleEl;
- for (let i = 0; i < sections.length; i++) {
- titleEl = sections[i].querySelector('.title');
- if (titleEl.textContent.trim() === title) {
- return sections[i].querySelector('.value');
- }
+ function valueOf(title, fieldsetid) {
+ const sections = element.$[fieldsetid].querySelectorAll('section');
+ let titleEl;
+ for (let i = 0; i < sections.length; i++) {
+ titleEl = sections[i].querySelector('.title');
+ if (titleEl.textContent.trim() === title) {
+ return sections[i].querySelector('.value');
}
}
+ }
- setup(() => {
- diffPreferences = {
- context: 10,
- line_wrapping: false,
- line_length: 100,
- tab_size: 8,
- font_size: 12,
- show_tabs: true,
- show_whitespace_errors: true,
- syntax_highlighting: true,
- manual_review: false,
- ignore_whitespace: 'IGNORE_NONE',
- };
+ setup(() => {
+ diffPreferences = {
+ context: 10,
+ line_wrapping: false,
+ line_length: 100,
+ tab_size: 8,
+ font_size: 12,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ manual_review: false,
+ ignore_whitespace: 'IGNORE_NONE',
+ };
- stub('gr-rest-api-interface', {
- getDiffPreferences() {
- return Promise.resolve(diffPreferences);
- },
- });
-
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- return element.loadData();
+ stub('gr-rest-api-interface', {
+ getDiffPreferences() {
+ return Promise.resolve(diffPreferences);
+ },
});
- teardown(() => { sandbox.restore(); });
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ return element.loadData();
+ });
- test('renders', () => {
- // Rendered with the expected preferences selected.
- assert.equal(valueOf('Context', 'diffPreferences')
- .firstElementChild.bindValue, diffPreferences.context);
- assert.equal(valueOf('Fit to screen', 'diffPreferences')
- .firstElementChild.checked, diffPreferences.line_wrapping);
- assert.equal(valueOf('Diff width', 'diffPreferences')
- .firstElementChild.bindValue, diffPreferences.line_length);
- assert.equal(valueOf('Tab width', 'diffPreferences')
- .firstElementChild.bindValue, diffPreferences.tab_size);
- assert.equal(valueOf('Font size', 'diffPreferences')
- .firstElementChild.bindValue, diffPreferences.font_size);
- assert.equal(valueOf('Show tabs', 'diffPreferences')
- .firstElementChild.checked, diffPreferences.show_tabs);
- assert.equal(valueOf('Show trailing whitespace', 'diffPreferences')
- .firstElementChild.checked, diffPreferences.show_whitespace_errors);
- assert.equal(valueOf('Syntax highlighting', 'diffPreferences')
- .firstElementChild.checked, diffPreferences.syntax_highlighting);
- assert.equal(
- valueOf('Automatically mark viewed files reviewed', 'diffPreferences')
- .firstElementChild.checked, !diffPreferences.manual_review);
- assert.equal(valueOf('Ignore Whitespace', 'diffPreferences')
- .firstElementChild.bindValue, diffPreferences.ignore_whitespace);
+ teardown(() => { sandbox.restore(); });
+ test('renders', () => {
+ // Rendered with the expected preferences selected.
+ assert.equal(valueOf('Context', 'diffPreferences')
+ .firstElementChild.bindValue, diffPreferences.context);
+ assert.equal(valueOf('Fit to screen', 'diffPreferences')
+ .firstElementChild.checked, diffPreferences.line_wrapping);
+ assert.equal(valueOf('Diff width', 'diffPreferences')
+ .firstElementChild.bindValue, diffPreferences.line_length);
+ assert.equal(valueOf('Tab width', 'diffPreferences')
+ .firstElementChild.bindValue, diffPreferences.tab_size);
+ assert.equal(valueOf('Font size', 'diffPreferences')
+ .firstElementChild.bindValue, diffPreferences.font_size);
+ assert.equal(valueOf('Show tabs', 'diffPreferences')
+ .firstElementChild.checked, diffPreferences.show_tabs);
+ assert.equal(valueOf('Show trailing whitespace', 'diffPreferences')
+ .firstElementChild.checked, diffPreferences.show_whitespace_errors);
+ assert.equal(valueOf('Syntax highlighting', 'diffPreferences')
+ .firstElementChild.checked, diffPreferences.syntax_highlighting);
+ assert.equal(
+ valueOf('Automatically mark viewed files reviewed', 'diffPreferences')
+ .firstElementChild.checked, !diffPreferences.manual_review);
+ assert.equal(valueOf('Ignore Whitespace', 'diffPreferences')
+ .firstElementChild.bindValue, diffPreferences.ignore_whitespace);
+
+ assert.isFalse(element.hasUnsavedChanges);
+ });
+
+ test('save changes', () => {
+ sandbox.stub(element.$.restAPI, 'saveDiffPreferences')
+ .returns(Promise.resolve());
+ const showTrailingWhitespaceCheckbox =
+ valueOf('Show trailing whitespace', 'diffPreferences')
+ .firstElementChild;
+ showTrailingWhitespaceCheckbox.checked = false;
+ element._handleShowTrailingWhitespaceTap();
+
+ assert.isTrue(element.hasUnsavedChanges);
+
+ // Save the change.
+ return element.save().then(() => {
assert.isFalse(element.hasUnsavedChanges);
});
-
- test('save changes', () => {
- sandbox.stub(element.$.restAPI, 'saveDiffPreferences')
- .returns(Promise.resolve());
- const showTrailingWhitespaceCheckbox =
- valueOf('Show trailing whitespace', 'diffPreferences')
- .firstElementChild;
- showTrailingWhitespaceCheckbox.checked = false;
- element._handleShowTrailingWhitespaceTap();
-
- assert.isTrue(element.hasUnsavedChanges);
-
- // Save the change.
- return element.save().then(() => {
- assert.isFalse(element.hasUnsavedChanges);
- });
- });
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
deleted file mode 100644
index 14a65b2..0000000
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
+++ /dev/null
@@ -1,86 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="/bower_components/paper-tabs/paper-tabs.html">
-<link rel="import" href="../../shared/gr-shell-command/gr-shell-command.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-download-commands">
- <template>
- <style include="shared-styles">
- paper-tabs {
- height: 3rem;
- margin-bottom: var(--spacing-m);
- --paper-tabs-selection-bar-color: var(--link-color);
- }
- paper-tab {
- max-width: 15rem;
- text-transform: uppercase;
- --paper-tab-ink: var(--link-color);
- }
- label,
- input {
- display: block;
- }
- label {
- font-weight: var(--font-weight-bold);
- }
- .schemes {
- display: flex;
- justify-content: space-between;
- }
- .commands {
- display: flex;
- flex-direction: column;
- }
- gr-shell-command {
- width: 60em;
- margin-bottom: var(--spacing-m);
- }
- .hidden {
- display: none;
- }
- </style>
- <div class="schemes">
- <paper-tabs
- id="downloadTabs"
- class$="[[_computeShowTabs(schemes)]]"
- selected="[[_computeSelected(schemes, selectedScheme)]]"
- on-selected-changed="_handleTabChange">
- <template is="dom-repeat" items="[[schemes]]" as="scheme">
- <paper-tab data-scheme$="[[scheme]]">[[scheme]]</paper-tab>
- </template>
- </paper-tabs>
- </div>
- <div class="commands" hidden$="[[!schemes.length]]" hidden>
- <template is="dom-repeat"
- items="[[commands]]"
- as="command">
- <gr-shell-command
- label=[[command.title]]
- command=[[command.command]]></gr-shell-command>
- </template>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-download-commands.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
index 04df531..e9befaf 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
@@ -14,82 +14,93 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
- */
- class GrDownloadCommands extends Polymer.mixinBehaviors( [
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-download-commands'; }
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '@polymer/paper-tabs/paper-tabs.js';
+import '../gr-shell-command/gr-shell-command.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-download-commands_html.js';
- static get properties() {
- return {
- commands: Array,
- _loggedIn: {
- type: Boolean,
- value: false,
- observer: '_loggedInChanged',
- },
- schemes: Array,
- selectedScheme: {
- type: String,
- notify: true,
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrDownloadCommands extends mixinBehaviors( [
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** @override */
- attached() {
- super.attached();
- this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- });
- }
+ static get is() { return 'gr-download-commands'; }
- focusOnCopy() {
- this.shadowRoot.querySelector('gr-shell-command').focusOnCopy();
- }
+ static get properties() {
+ return {
+ commands: Array,
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ observer: '_loggedInChanged',
+ },
+ schemes: Array,
+ selectedScheme: {
+ type: String,
+ notify: true,
+ },
+ };
+ }
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
+ /** @override */
+ attached() {
+ super.attached();
+ this._getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ });
+ }
- _loggedInChanged(loggedIn) {
- if (!loggedIn) { return; }
- return this.$.restAPI.getPreferences().then(prefs => {
- if (prefs.download_scheme) {
- // Note (issue 5180): normalize the download scheme with lower-case.
- this.selectedScheme = prefs.download_scheme.toLowerCase();
- }
- });
- }
+ focusOnCopy() {
+ this.shadowRoot.querySelector('gr-shell-command').focusOnCopy();
+ }
- _handleTabChange(e) {
- const scheme = this.schemes[e.detail.value];
- if (scheme && scheme !== this.selectedScheme) {
- this.set('selectedScheme', scheme);
- if (this._loggedIn) {
- this.$.restAPI.savePreferences(
- {download_scheme: this.selectedScheme});
- }
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _loggedInChanged(loggedIn) {
+ if (!loggedIn) { return; }
+ return this.$.restAPI.getPreferences().then(prefs => {
+ if (prefs.download_scheme) {
+ // Note (issue 5180): normalize the download scheme with lower-case.
+ this.selectedScheme = prefs.download_scheme.toLowerCase();
}
- }
+ });
+ }
- _computeSelected(schemes, selectedScheme) {
- return (schemes.findIndex(scheme => scheme === selectedScheme) || 0) +
- '';
- }
-
- _computeShowTabs(schemes) {
- return schemes.length > 1 ? '' : 'hidden';
+ _handleTabChange(e) {
+ const scheme = this.schemes[e.detail.value];
+ if (scheme && scheme !== this.selectedScheme) {
+ this.set('selectedScheme', scheme);
+ if (this._loggedIn) {
+ this.$.restAPI.savePreferences(
+ {download_scheme: this.selectedScheme});
+ }
}
}
- customElements.define(GrDownloadCommands.is, GrDownloadCommands);
-})();
+ _computeSelected(schemes, selectedScheme) {
+ return (schemes.findIndex(scheme => scheme === selectedScheme) || 0) +
+ '';
+ }
+
+ _computeShowTabs(schemes) {
+ return schemes.length > 1 ? '' : 'hidden';
+ }
+}
+
+customElements.define(GrDownloadCommands.is, GrDownloadCommands);
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.js b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.js
new file mode 100644
index 0000000..12a8d01
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.js
@@ -0,0 +1,67 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ paper-tabs {
+ height: 3rem;
+ margin-bottom: var(--spacing-m);
+ --paper-tabs-selection-bar-color: var(--link-color);
+ }
+ paper-tab {
+ max-width: 15rem;
+ text-transform: uppercase;
+ --paper-tab-ink: var(--link-color);
+ }
+ label,
+ input {
+ display: block;
+ }
+ label {
+ font-weight: var(--font-weight-bold);
+ }
+ .schemes {
+ display: flex;
+ justify-content: space-between;
+ }
+ .commands {
+ display: flex;
+ flex-direction: column;
+ }
+ gr-shell-command {
+ width: 60em;
+ margin-bottom: var(--spacing-m);
+ }
+ .hidden {
+ display: none;
+ }
+ </style>
+ <div class="schemes">
+ <paper-tabs id="downloadTabs" class\$="[[_computeShowTabs(schemes)]]" selected="[[_computeSelected(schemes, selectedScheme)]]" on-selected-changed="_handleTabChange">
+ <template is="dom-repeat" items="[[schemes]]" as="scheme">
+ <paper-tab data-scheme\$="[[scheme]]">[[scheme]]</paper-tab>
+ </template>
+ </paper-tabs>
+ </div>
+ <div class="commands" hidden\$="[[!schemes.length]]" hidden="">
+ <template is="dom-repeat" items="[[commands]]" as="command">
+ <gr-shell-command label="[[command.title]]" command="[[command.command]]"></gr-shell-command>
+ </template>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
index 4e37f9e..f63ac0c 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-download-commands</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-download-commands.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,123 +30,124 @@
</template>
</test-fixture>
-<script>
- suite('gr-download-commands', async () => {
- await readyToTest();
- let element;
- let sandbox;
- const SCHEMES = ['http', 'repo', 'ssh'];
- const COMMANDS = [{
- title: 'Checkout',
- command: `git fetch http://andybons@localhost:8080/a/test-project
- refs/changes/05/5/1 && git checkout FETCH_HEAD`,
- }, {
- title: 'Cherry Pick',
- command: `git fetch http://andybons@localhost:8080/a/test-project
- refs/changes/05/5/1 && git cherry-pick FETCH_HEAD`,
- }, {
- title: 'Format Patch',
- command: `git fetch http://andybons@localhost:8080/a/test-project
- refs/changes/05/5/1 && git format-patch -1 --stdout FETCH_HEAD`,
- }, {
- title: 'Pull',
- command: `git pull http://andybons@localhost:8080/a/test-project
- refs/changes/05/5/1`,
- }];
- const SELECTED_SCHEME = 'http';
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-download-commands.js';
+suite('gr-download-commands', () => {
+ let element;
+ let sandbox;
+ const SCHEMES = ['http', 'repo', 'ssh'];
+ const COMMANDS = [{
+ title: 'Checkout',
+ command: `git fetch http://andybons@localhost:8080/a/test-project
+ refs/changes/05/5/1 && git checkout FETCH_HEAD`,
+ }, {
+ title: 'Cherry Pick',
+ command: `git fetch http://andybons@localhost:8080/a/test-project
+ refs/changes/05/5/1 && git cherry-pick FETCH_HEAD`,
+ }, {
+ title: 'Format Patch',
+ command: `git fetch http://andybons@localhost:8080/a/test-project
+ refs/changes/05/5/1 && git format-patch -1 --stdout FETCH_HEAD`,
+ }, {
+ title: 'Pull',
+ command: `git pull http://andybons@localhost:8080/a/test-project
+ refs/changes/05/5/1`,
+ }];
+ const SELECTED_SCHEME = 'http';
- setup(() => {
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('unauthenticated', () => {
+ setup(done => {
+ element = fixture('basic');
+ element.schemes = SCHEMES;
+ element.commands = COMMANDS;
+ element.selectedScheme = SELECTED_SCHEME;
+ flushAsynchronousOperations();
+ flush(done);
});
- teardown(() => {
- sandbox.restore();
+ test('focusOnCopy', () => {
+ const focusStub = sandbox.stub(element.shadowRoot
+ .querySelector('gr-shell-command'),
+ 'focusOnCopy');
+ element.focusOnCopy();
+ assert.isTrue(focusStub.called);
});
- suite('unauthenticated', () => {
- setup(done => {
- element = fixture('basic');
- element.schemes = SCHEMES;
- element.commands = COMMANDS;
- element.selectedScheme = SELECTED_SCHEME;
- flushAsynchronousOperations();
- flush(done);
+ test('element visibility', () => {
+ assert.isFalse(isHidden(element.shadowRoot
+ .querySelector('paper-tabs')));
+ assert.isFalse(isHidden(element.shadowRoot
+ .querySelector('.commands')));
+
+ element.schemes = [];
+ assert.isTrue(isHidden(element.shadowRoot
+ .querySelector('paper-tabs')));
+ assert.isTrue(isHidden(element.shadowRoot
+ .querySelector('.commands')));
+ });
+
+ test('tab selection', done => {
+ assert.equal(element.$.downloadTabs.selected, '0');
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('[data-scheme="ssh"]'));
+ flushAsynchronousOperations();
+ assert.equal(element.selectedScheme, 'ssh');
+ assert.equal(element.$.downloadTabs.selected, '2');
+ done();
+ });
+
+ test('loads scheme from preferences', done => {
+ stub('gr-rest-api-interface', {
+ getPreferences() {
+ return Promise.resolve({download_scheme: 'repo'});
+ },
});
-
- test('focusOnCopy', () => {
- const focusStub = sandbox.stub(element.shadowRoot
- .querySelector('gr-shell-command'),
- 'focusOnCopy');
- element.focusOnCopy();
- assert.isTrue(focusStub.called);
- });
-
- test('element visibility', () => {
- assert.isFalse(isHidden(element.shadowRoot
- .querySelector('paper-tabs')));
- assert.isFalse(isHidden(element.shadowRoot
- .querySelector('.commands')));
-
- element.schemes = [];
- assert.isTrue(isHidden(element.shadowRoot
- .querySelector('paper-tabs')));
- assert.isTrue(isHidden(element.shadowRoot
- .querySelector('.commands')));
- });
-
- test('tab selection', done => {
- assert.equal(element.$.downloadTabs.selected, '0');
- MockInteractions.tap(element.shadowRoot
- .querySelector('[data-scheme="ssh"]'));
- flushAsynchronousOperations();
- assert.equal(element.selectedScheme, 'ssh');
- assert.equal(element.$.downloadTabs.selected, '2');
+ element._loggedIn = true;
+ assert.isTrue(element.$.restAPI.getPreferences.called);
+ element.$.restAPI.getPreferences.lastCall.returnValue.then(() => {
+ assert.equal(element.selectedScheme, 'repo');
done();
});
+ });
- test('loads scheme from preferences', done => {
- stub('gr-rest-api-interface', {
- getPreferences() {
- return Promise.resolve({download_scheme: 'repo'});
- },
- });
- element._loggedIn = true;
- assert.isTrue(element.$.restAPI.getPreferences.called);
- element.$.restAPI.getPreferences.lastCall.returnValue.then(() => {
- assert.equal(element.selectedScheme, 'repo');
- done();
- });
+ test('normalize scheme from preferences', done => {
+ stub('gr-rest-api-interface', {
+ getPreferences() {
+ return Promise.resolve({download_scheme: 'REPO'});
+ },
});
-
- test('normalize scheme from preferences', done => {
- stub('gr-rest-api-interface', {
- getPreferences() {
- return Promise.resolve({download_scheme: 'REPO'});
- },
- });
- element._loggedIn = true;
- element.$.restAPI.getPreferences.lastCall.returnValue.then(() => {
- assert.equal(element.selectedScheme, 'repo');
- done();
- });
- });
-
- test('saves scheme to preferences', () => {
- element._loggedIn = true;
- const savePrefsStub = sandbox.stub(element.$.restAPI, 'savePreferences',
- () => Promise.resolve());
-
- flushAsynchronousOperations();
-
- const repoTab = element.shadowRoot
- .querySelector('paper-tab[data-scheme="repo"]');
-
- MockInteractions.tap(repoTab);
-
- assert.isTrue(savePrefsStub.called);
- assert.equal(savePrefsStub.lastCall.args[0].download_scheme,
- repoTab.getAttribute('data-scheme'));
+ element._loggedIn = true;
+ element.$.restAPI.getPreferences.lastCall.returnValue.then(() => {
+ assert.equal(element.selectedScheme, 'repo');
+ done();
});
});
+
+ test('saves scheme to preferences', () => {
+ element._loggedIn = true;
+ const savePrefsStub = sandbox.stub(element.$.restAPI, 'savePreferences',
+ () => Promise.resolve());
+
+ flushAsynchronousOperations();
+
+ const repoTab = element.shadowRoot
+ .querySelector('paper-tab[data-scheme="repo"]');
+
+ MockInteractions.tap(repoTab);
+
+ assert.isTrue(savePrefsStub.called);
+ assert.equal(savePrefsStub.lastCall.args[0].download_scheme,
+ repoTab.getAttribute('data-scheme'));
+ });
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
index 06b4a72..6b250de 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
@@ -14,123 +14,135 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '@polymer/iron-dropdown/iron-dropdown.js';
+import '@polymer/paper-item/paper-item.js';
+import '@polymer/paper-listbox/paper-listbox.js';
+import '../../../styles/shared-styles.js';
+import '../gr-button/gr-button.js';
+import '../gr-date-formatter/gr-date-formatter.js';
+import '../gr-select/gr-select.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-dropdown-list_html.js';
+
+/**
+ * fired when the selected value of the dropdown changes
+ *
+ * @event {change}
+ */
+
+const Defs = {};
+
+/**
+ * Requred values are text and value. mobileText and triggerText will
+ * fall back to text if not provided.
+ *
+ * If bottomText is not provided, nothing will display on the second
+ * line.
+ *
+ * If date is not provided, nothing will be displayed in its place.
+ *
+ * @typedef {{
+ * text: string,
+ * value: (string|number),
+ * bottomText: (string|undefined),
+ * triggerText: (string|undefined),
+ * mobileText: (string|undefined),
+ * date: (!Date|undefined),
+ * }}
+ */
+Defs.item;
+
+/** @extends Polymer.Element */
+class GrDropdownList extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-dropdown-list'; }
/**
- * fired when the selected value of the dropdown changes
+ * Fired when the selected value changes
*
- * @event {change}
+ * @event value-change
+ *
+ * @property {string|number} value
*/
- const Defs = {};
-
- /**
- * Requred values are text and value. mobileText and triggerText will
- * fall back to text if not provided.
- *
- * If bottomText is not provided, nothing will display on the second
- * line.
- *
- * If date is not provided, nothing will be displayed in its place.
- *
- * @typedef {{
- * text: string,
- * value: (string|number),
- * bottomText: (string|undefined),
- * triggerText: (string|undefined),
- * mobileText: (string|undefined),
- * date: (!Date|undefined),
- * }}
- */
- Defs.item;
-
- /** @extends Polymer.Element */
- class GrDropdownList extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-dropdown-list'; }
- /**
- * Fired when the selected value changes
- *
- * @event value-change
- *
- * @property {string|number} value
- */
-
- static get properties() {
- return {
- initialCount: Number,
- /** @type {!Array<!Defs.item>} */
- items: Object,
- text: String,
- disabled: {
- type: Boolean,
- value: false,
- },
- value: {
- type: String,
- notify: true,
- },
- };
- }
-
- static get observers() {
- return [
- '_handleValueChange(value, items)',
- ];
- }
-
- /**
- * Handle a click on the iron-dropdown element.
- *
- * @param {!Event} e
- */
- _handleDropdownClick(e) {
- // async is needed so that that the click event is fired before the
- // dropdown closes (This was a bug for touch devices).
- this.async(() => {
- this.$.dropdown.close();
- }, 1);
- }
-
- /**
- * Handle a click on the button to open the dropdown.
- *
- * @param {!Event} e
- */
- _showDropdownTapHandler(e) {
- this._open();
- }
-
- /**
- * Open the dropdown.
- */
- _open() {
- this.$.dropdown.open();
- }
-
- _computeMobileText(item) {
- return item.mobileText ? item.mobileText : item.text;
- }
-
- _handleValueChange(value, items) {
- // Polymer 2: check for undefined
- if ([value, items].some(arg => arg === undefined)) {
- return;
- }
-
- if (!value) { return; }
- const selectedObj = items.find(item => item.value + '' === value + '');
- if (!selectedObj) { return; }
- this.text = selectedObj.triggerText? selectedObj.triggerText :
- selectedObj.text;
- this.dispatchEvent(new CustomEvent('value-change', {
- detail: {value},
- bubbles: false,
- }));
- }
+ static get properties() {
+ return {
+ initialCount: Number,
+ /** @type {!Array<!Defs.item>} */
+ items: Object,
+ text: String,
+ disabled: {
+ type: Boolean,
+ value: false,
+ },
+ value: {
+ type: String,
+ notify: true,
+ },
+ };
}
- customElements.define(GrDropdownList.is, GrDropdownList);
-})();
+ static get observers() {
+ return [
+ '_handleValueChange(value, items)',
+ ];
+ }
+
+ /**
+ * Handle a click on the iron-dropdown element.
+ *
+ * @param {!Event} e
+ */
+ _handleDropdownClick(e) {
+ // async is needed so that that the click event is fired before the
+ // dropdown closes (This was a bug for touch devices).
+ this.async(() => {
+ this.$.dropdown.close();
+ }, 1);
+ }
+
+ /**
+ * Handle a click on the button to open the dropdown.
+ *
+ * @param {!Event} e
+ */
+ _showDropdownTapHandler(e) {
+ this._open();
+ }
+
+ /**
+ * Open the dropdown.
+ */
+ _open() {
+ this.$.dropdown.open();
+ }
+
+ _computeMobileText(item) {
+ return item.mobileText ? item.mobileText : item.text;
+ }
+
+ _handleValueChange(value, items) {
+ // Polymer 2: check for undefined
+ if ([value, items].some(arg => arg === undefined)) {
+ return;
+ }
+
+ if (!value) { return; }
+ const selectedObj = items.find(item => item.value + '' === value + '');
+ if (!selectedObj) { return; }
+ this.text = selectedObj.triggerText? selectedObj.triggerText :
+ selectedObj.text;
+ this.dispatchEvent(new CustomEvent('value-change', {
+ detail: {value},
+ bubbles: false,
+ }));
+ }
+}
+
+customElements.define(GrDropdownList.is, GrDropdownList);
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.js
similarity index 60%
rename from polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
rename to polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.js
index 7586876..3b454c2 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.js
@@ -1,33 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @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.js';
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
-<link rel="import" href="/bower_components/paper-item/paper-item.html">
-<link rel="import" href="/bower_components/paper-listbox/paper-listbox.html">
-
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-
-
-<dom-module id="gr-dropdown-list">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: inline-block;
@@ -128,38 +117,17 @@
}
}
</style>
- <gr-button
- disabled="[[disabled]]"
- down-arrow
- link
- id="trigger"
- class="dropdown-trigger"
- on-click="_showDropdownTapHandler"
- slot="dropdown-trigger">
+ <gr-button disabled="[[disabled]]" down-arrow="" link="" id="trigger" class="dropdown-trigger" on-click="_showDropdownTapHandler" slot="dropdown-trigger">
<span id="triggerText">[[text]]</span>
</gr-button>
- <iron-dropdown
- id="dropdown"
- vertical-align="top"
- allow-outside-scroll="true"
- on-click="_handleDropdownClick">
- <paper-listbox
- class="dropdown-content"
- slot="dropdown-content"
- attr-for-selected="data-value"
- selected="{{value}}"
- on-tap="_handleDropdownTap">
- <template is="dom-repeat"
- items="[[items]]"
- initial-count="[[initialCount]]">
- <paper-item
- disabled="[[item.disabled]]"
- data-value$="[[item.value]]">
+ <iron-dropdown id="dropdown" vertical-align="top" allow-outside-scroll="true" on-click="_handleDropdownClick">
+ <paper-listbox class="dropdown-content" slot="dropdown-content" attr-for-selected="data-value" selected="{{value}}" on-tap="_handleDropdownTap">
+ <template is="dom-repeat" items="[[items]]" initial-count="[[initialCount]]">
+ <paper-item disabled="[[item.disabled]]" data-value\$="[[item.value]]">
<div class="topContent">
<div>[[item.text]]</div>
<template is="dom-if" if="[[item.date]]">
- <gr-date-formatter
- date-str="[[item.date]]"></gr-date-formatter>
+ <gr-date-formatter date-str="[[item.date]]"></gr-date-formatter>
</template>
</div>
<template is="dom-if" if="[[item.bottomText]]">
@@ -174,14 +142,10 @@
<gr-select bind-value="{{value}}">
<select>
<template is="dom-repeat" items="[[items]]">
- <option
- disabled$="[[item.disabled]]"
- value="[[item.value]]">
+ <option disabled\$="[[item.disabled]]" value="[[item.value]]">
[[_computeMobileText(item)]]
</option>
</template>
</select>
</gr-select>
- </template>
- <script src="gr-dropdown-list.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
index 40e43fd..7d39f9e 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-dropdown-list</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-dropdown-list.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,140 +30,142 @@
</template>
</test-fixture>
-<script>
- suite('gr-dropdown-list tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-dropdown-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-dropdown-list tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
});
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('tap on trigger opens menu', () => {
- sandbox.stub(element, '_open', () => { element.$.dropdown.open(); });
- assert.isFalse(element.$.dropdown.opened);
- MockInteractions.tap(element.$.trigger);
- assert.isTrue(element.$.dropdown.opened);
- });
+ test('tap on trigger opens menu', () => {
+ sandbox.stub(element, '_open', () => { element.$.dropdown.open(); });
+ assert.isFalse(element.$.dropdown.opened);
+ MockInteractions.tap(element.$.trigger);
+ assert.isTrue(element.$.dropdown.opened);
+ });
- test('_computeMobileText', () => {
- const item = {
+ test('_computeMobileText', () => {
+ const item = {
+ value: 1,
+ text: 'text',
+ };
+ assert.equal(element._computeMobileText(item), item.text);
+ item.mobileText = 'mobile text';
+ assert.equal(element._computeMobileText(item), item.mobileText);
+ });
+
+ test('options are selected and laid out correctly', done => {
+ element.value = 2;
+ element.items = [
+ {
value: 1,
- text: 'text',
- };
- assert.equal(element._computeMobileText(item), item.text);
- item.mobileText = 'mobile text';
- assert.equal(element._computeMobileText(item), item.mobileText);
- });
+ text: 'Top Text 1',
+ },
+ {
+ value: 2,
+ bottomText: 'Bottom Text 2',
+ triggerText: 'Button Text 2',
+ text: 'Top Text 2',
+ mobileText: 'Mobile Text 2',
+ },
+ {
+ value: 3,
+ disabled: true,
+ bottomText: 'Bottom Text 3',
+ triggerText: 'Button Text 3',
+ date: '2017-08-18 23:11:42.569000000',
+ text: 'Top Text 3',
+ mobileText: 'Mobile Text 3',
+ },
+ ];
+ assert.equal(element.shadowRoot
+ .querySelector('paper-listbox').selected, element.value);
+ assert.equal(element.text, 'Button Text 2');
+ flush(() => {
+ const items = dom(element.root).querySelectorAll('paper-item');
+ const mobileItems = dom(element.root).querySelectorAll('option');
+ assert.equal(items.length, 3);
+ assert.equal(mobileItems.length, 3);
- test('options are selected and laid out correctly', done => {
- element.value = 2;
- element.items = [
- {
- value: 1,
- text: 'Top Text 1',
- },
- {
- value: 2,
- bottomText: 'Bottom Text 2',
- triggerText: 'Button Text 2',
- text: 'Top Text 2',
- mobileText: 'Mobile Text 2',
- },
- {
- value: 3,
- disabled: true,
- bottomText: 'Bottom Text 3',
- triggerText: 'Button Text 3',
- date: '2017-08-18 23:11:42.569000000',
- text: 'Top Text 3',
- mobileText: 'Mobile Text 3',
- },
- ];
- assert.equal(element.shadowRoot
- .querySelector('paper-listbox').selected, element.value);
- assert.equal(element.text, 'Button Text 2');
- flush(() => {
- const items = Polymer.dom(element.root).querySelectorAll('paper-item');
- const mobileItems = Polymer.dom(element.root).querySelectorAll('option');
- assert.equal(items.length, 3);
- assert.equal(mobileItems.length, 3);
+ // First Item
+ // The first item should be disabled, has no bottom text, and no date.
+ assert.isFalse(!!items[0].disabled);
+ assert.isFalse(mobileItems[0].disabled);
+ assert.isFalse(items[0].classList.contains('iron-selected'));
+ assert.isFalse(mobileItems[0].selected);
- // First Item
- // The first item should be disabled, has no bottom text, and no date.
- assert.isFalse(!!items[0].disabled);
- assert.isFalse(mobileItems[0].disabled);
- assert.isFalse(items[0].classList.contains('iron-selected'));
- assert.isFalse(mobileItems[0].selected);
+ assert.isNotOk(dom(items[0]).querySelector('gr-date-formatter'));
+ assert.isNotOk(dom(items[0]).querySelector('.bottomContent'));
+ assert.equal(items[0].dataset.value, element.items[0].value);
+ assert.equal(mobileItems[0].value, element.items[0].value);
+ assert.equal(dom(items[0]).querySelector('.topContent div')
+ .innerText, element.items[0].text);
- assert.isNotOk(Polymer.dom(items[0]).querySelector('gr-date-formatter'));
- assert.isNotOk(Polymer.dom(items[0]).querySelector('.bottomContent'));
- assert.equal(items[0].dataset.value, element.items[0].value);
- assert.equal(mobileItems[0].value, element.items[0].value);
- assert.equal(Polymer.dom(items[0]).querySelector('.topContent div')
- .innerText, element.items[0].text);
+ // Since no mobile specific text, it should fall back to text.
+ assert.equal(mobileItems[0].text, element.items[0].text);
- // Since no mobile specific text, it should fall back to text.
- assert.equal(mobileItems[0].text, element.items[0].text);
+ // Second Item
+ // The second item should have top text, bottom text, and no date.
+ assert.isFalse(!!items[1].disabled);
+ assert.isFalse(mobileItems[1].disabled);
+ assert.isTrue(items[1].classList.contains('iron-selected'));
+ assert.isTrue(mobileItems[1].selected);
- // Second Item
- // The second item should have top text, bottom text, and no date.
- assert.isFalse(!!items[1].disabled);
- assert.isFalse(mobileItems[1].disabled);
- assert.isTrue(items[1].classList.contains('iron-selected'));
- assert.isTrue(mobileItems[1].selected);
+ assert.isNotOk(dom(items[1]).querySelector('gr-date-formatter'));
+ assert.isOk(dom(items[1]).querySelector('.bottomContent'));
+ assert.equal(items[1].dataset.value, element.items[1].value);
+ assert.equal(mobileItems[1].value, element.items[1].value);
+ assert.equal(dom(items[1]).querySelector('.topContent div')
+ .innerText, element.items[1].text);
- assert.isNotOk(Polymer.dom(items[1]).querySelector('gr-date-formatter'));
- assert.isOk(Polymer.dom(items[1]).querySelector('.bottomContent'));
- assert.equal(items[1].dataset.value, element.items[1].value);
- assert.equal(mobileItems[1].value, element.items[1].value);
- assert.equal(Polymer.dom(items[1]).querySelector('.topContent div')
- .innerText, element.items[1].text);
+ // Since there is mobile specific text, it should that.
+ assert.equal(mobileItems[1].text, element.items[1].mobileText);
- // Since there is mobile specific text, it should that.
- assert.equal(mobileItems[1].text, element.items[1].mobileText);
+ // Since this item is selected, and it has triggerText defined, that
+ // should be used.
+ assert.equal(element.text, element.items[1].triggerText);
- // Since this item is selected, and it has triggerText defined, that
- // should be used.
- assert.equal(element.text, element.items[1].triggerText);
+ // Third item
+ // The third item should be disabled, and have a date, and bottom content.
+ assert.isTrue(!!items[2].disabled);
+ assert.isTrue(mobileItems[2].disabled);
+ assert.isFalse(items[2].classList.contains('iron-selected'));
+ assert.isFalse(mobileItems[2].selected);
- // Third item
- // The third item should be disabled, and have a date, and bottom content.
- assert.isTrue(!!items[2].disabled);
- assert.isTrue(mobileItems[2].disabled);
- assert.isFalse(items[2].classList.contains('iron-selected'));
- assert.isFalse(mobileItems[2].selected);
+ assert.isOk(dom(items[2]).querySelector('gr-date-formatter'));
+ assert.isOk(dom(items[2]).querySelector('.bottomContent'));
+ assert.equal(items[2].dataset.value, element.items[2].value);
+ assert.equal(mobileItems[2].value, element.items[2].value);
+ assert.equal(dom(items[2]).querySelector('.topContent div')
+ .innerText, element.items[2].text);
- assert.isOk(Polymer.dom(items[2]).querySelector('gr-date-formatter'));
- assert.isOk(Polymer.dom(items[2]).querySelector('.bottomContent'));
- assert.equal(items[2].dataset.value, element.items[2].value);
- assert.equal(mobileItems[2].value, element.items[2].value);
- assert.equal(Polymer.dom(items[2]).querySelector('.topContent div')
- .innerText, element.items[2].text);
+ // Since there is mobile specific text, it should that.
+ assert.equal(mobileItems[2].text, element.items[2].mobileText);
- // Since there is mobile specific text, it should that.
- assert.equal(mobileItems[2].text, element.items[2].mobileText);
+ // Select a new item.
+ MockInteractions.tap(items[0]);
+ flushAsynchronousOperations();
+ assert.equal(element.value, 1);
+ assert.isTrue(items[0].classList.contains('iron-selected'));
+ assert.isTrue(mobileItems[0].selected);
- // Select a new item.
- MockInteractions.tap(items[0]);
- flushAsynchronousOperations();
- assert.equal(element.value, 1);
- assert.isTrue(items[0].classList.contains('iron-selected'));
- assert.isTrue(mobileItems[0].selected);
-
- // Since no triggerText, the fallback is used.
- assert.equal(element.text, element.items[0].text);
- done();
- });
+ // Since no triggerText, the fallback is used.
+ assert.equal(element.text, element.items[0].text);
+ done();
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
deleted file mode 100644
index 5d28390..0000000
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
+++ /dev/null
@@ -1,168 +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.
--->
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-dropdown">
- <template>
- <style include="shared-styles">
- :host {
- display: inline-block;
- }
- .dropdown-trigger {
- text-decoration: none;
- width: 100%;
- }
- .dropdown-content {
- background-color: var(--dropdown-background-color);
- box-shadow: var(--elevation-level-2);
- }
- gr-button {
- @apply --gr-button;
- }
- gr-avatar {
- height: 2em;
- width: 2em;
- vertical-align: middle;
- }
- gr-button[link]:focus {
- outline: 5px auto -webkit-focus-ring-color;
- }
- ul {
- list-style: none;
- }
- .topContent,
- li {
- border-bottom: 1px solid var(--border-color);
- }
- li:last-of-type {
- border: none;
- }
- li .itemAction {
- cursor: pointer;
- display: block;
- padding: var(--spacing-m) var(--spacing-l);
- }
- li .itemAction {
- @apply --gr-dropdown-item;
- }
- li .itemAction.disabled {
- color: var(--deemphasized-text-color);
- cursor: default;
- }
- li .itemAction:link,
- li .itemAction:visited {
- text-decoration: none;
- }
- li .itemAction:not(.disabled):hover {
- background-color: var(--hover-background-color);
- }
- li:focus,
- li.selected {
- background-color: var(--selection-background-color);
- outline: none;
- }
- li:focus .itemAction,
- li.selected .itemAction {
- background-color: transparent;
- }
- .topContent {
- display: block;
- padding: var(--spacing-m) var(--spacing-l);
- @apply --gr-dropdown-item;
- }
- .bold-text {
- font-weight: var(--font-weight-bold);
- }
- </style>
- <gr-button
- link="[[link]]"
- class="dropdown-trigger" id="trigger"
- down-arrow="[[downArrow]]"
- on-click="_dropdownTriggerTapHandler">
- <slot></slot>
- </gr-button>
- <iron-dropdown id="dropdown"
- vertical-align="top"
- vertical-offset="[[verticalOffset]]"
- allow-outside-scroll="true"
- horizontal-align="[[horizontalAlign]]"
- on-click="_handleDropdownClick">
- <div class="dropdown-content" slot="dropdown-content">
- <ul>
- <template is="dom-if" if="[[topContent]]">
- <div class="topContent">
- <template
- is="dom-repeat"
- items="[[topContent]]"
- as="item"
- initial-count="75">
- <div
- class$="[[_getClassIfBold(item.bold)]] top-item"
- tabindex="-1">
- [[item.text]]
- </div>
- </template>
- </div>
- </template>
- <template
- is="dom-repeat"
- items="[[items]]"
- as="link"
- initial-count="75">
- <li tabindex="-1">
- <gr-tooltip-content
- has-tooltip="[[_computeHasTooltip(link.tooltip)]]"
- title$="[[link.tooltip]]">
- <span
- class$="itemAction [[_computeDisabledClass(link.id, disabledIds.*)]]"
- data-id$="[[link.id]]"
- on-click="_handleItemTap"
- hidden$="[[link.url]]"
- tabindex="-1">[[link.name]]</span>
- <a
- class="itemAction"
- href$="[[_computeLinkURL(link)]]"
- download$="[[_computeIsDownload(link)]]"
- rel$="[[_computeLinkRel(link)]]"
- target$="[[link.target]]"
- hidden$="[[!link.url]]"
- tabindex="-1">[[link.name]]</a>
- </gr-tooltip-content>
- </li>
- </template>
- </ul>
- </div>
- </iron-dropdown>
- <gr-cursor-manager
- id="cursor"
- cursor-target-class="selected"
- scroll-behavior="never"
- focus-on-move
- stops="[[_listElements]]"></gr-cursor-manager>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-dropdown.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index 531f2e3..b4190dc 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -14,309 +14,324 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- const REL_NOOPENER = 'noopener';
- const REL_EXTERNAL = 'external';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../../scripts/bundled-polymer.js';
+import '@polymer/iron-dropdown/iron-dropdown.js';
+import '../gr-button/gr-button.js';
+import '../gr-cursor-manager/gr-cursor-manager.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-tooltip-content/gr-tooltip-content.js';
+import '../../../styles/shared-styles.js';
+import {flush, dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-dropdown_html.js';
+
+const REL_NOOPENER = 'noopener';
+const REL_EXTERNAL = 'external';
+
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrDropdown extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-dropdown'; }
+ /**
+ * Fired when a non-link dropdown item with the given ID is tapped.
+ *
+ * @event tap-item-<id>
+ */
/**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * Fired when a non-link dropdown item is tapped.
+ *
+ * @event tap-item
*/
- class GrDropdown extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-dropdown'; }
- /**
- * Fired when a non-link dropdown item with the given ID is tapped.
- *
- * @event tap-item-<id>
- */
- /**
- * Fired when a non-link dropdown item is tapped.
- *
- * @event tap-item
- */
+ static get properties() {
+ return {
+ items: {
+ type: Array,
+ observer: '_resetCursorStops',
+ },
+ downArrow: Boolean,
+ topContent: Object,
+ horizontalAlign: {
+ type: String,
+ value: 'left',
+ },
- static get properties() {
- return {
- items: {
- type: Array,
- observer: '_resetCursorStops',
- },
- downArrow: Boolean,
- topContent: Object,
- horizontalAlign: {
- type: String,
- value: 'left',
- },
+ /**
+ * Style the dropdown trigger as a link (rather than a button).
+ */
+ link: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * Style the dropdown trigger as a link (rather than a button).
- */
- link: {
- type: Boolean,
- value: false,
- },
+ verticalOffset: {
+ type: Number,
+ value: 40,
+ },
- verticalOffset: {
- type: Number,
- value: 40,
- },
+ /**
+ * List the IDs of dropdown buttons to be disabled. (Note this only
+ * diisables bittons and not link entries.)
+ */
+ disabledIds: {
+ type: Array,
+ value() { return []; },
+ },
- /**
- * List the IDs of dropdown buttons to be disabled. (Note this only
- * diisables bittons and not link entries.)
- */
- disabledIds: {
- type: Array,
- value() { return []; },
- },
+ /**
+ * The elements of the list.
+ */
+ _listElements: {
+ type: Array,
+ value() { return []; },
+ },
+ };
+ }
- /**
- * The elements of the list.
- */
- _listElements: {
- type: Array,
- value() { return []; },
- },
- };
- }
+ get keyBindings() {
+ return {
+ 'down': '_handleDown',
+ 'enter space': '_handleEnter',
+ 'tab': '_handleTab',
+ 'up': '_handleUp',
+ };
+ }
- get keyBindings() {
- return {
- 'down': '_handleDown',
- 'enter space': '_handleEnter',
- 'tab': '_handleTab',
- 'up': '_handleUp',
- };
- }
-
- /**
- * Handle the up key.
- *
- * @param {!Event} e
- */
- _handleUp(e) {
- if (this.$.dropdown.opened) {
- e.preventDefault();
- e.stopPropagation();
- this.$.cursor.previous();
- } else {
- this._open();
- }
- }
-
- /**
- * Handle the down key.
- *
- * @param {!Event} e
- */
- _handleDown(e) {
- if (this.$.dropdown.opened) {
- e.preventDefault();
- e.stopPropagation();
- this.$.cursor.next();
- } else {
- this._open();
- }
- }
-
- /**
- * Handle the tab key.
- *
- * @param {!Event} e
- */
- _handleTab(e) {
- if (this.$.dropdown.opened) {
- // Tab in a native select is a no-op. Emulate this.
- e.preventDefault();
- e.stopPropagation();
- }
- }
-
- /**
- * Handle the enter key.
- *
- * @param {!Event} e
- */
- _handleEnter(e) {
+ /**
+ * Handle the up key.
+ *
+ * @param {!Event} e
+ */
+ _handleUp(e) {
+ if (this.$.dropdown.opened) {
e.preventDefault();
e.stopPropagation();
- if (this.$.dropdown.opened) {
- // TODO(milutin): This solution is not particularly robust in general.
- // Since gr-tooltip-content click on shadow dom is not propagated down,
- // we have to target `a` inside it.
- const el = this.$.cursor.target.querySelector(':not([hidden]) a');
- if (el) { el.click(); }
- } else {
- this._open();
- }
- }
-
- /**
- * Handle a click on the iron-dropdown element.
- *
- * @param {!Event} e
- */
- _handleDropdownClick(e) {
- this._close();
- }
-
- /**
- * Hanlde a click on the button to open the dropdown.
- *
- * @param {!Event} e
- */
- _dropdownTriggerTapHandler(e) {
- e.preventDefault();
- e.stopPropagation();
- if (this.$.dropdown.opened) {
- this._close();
- } else {
- this._open();
- }
- }
-
- /**
- * Open the dropdown and initialize the cursor.
- */
- _open() {
- this.$.dropdown.open();
- this._resetCursorStops();
- this.$.cursor.setCursorAtIndex(0);
- this.$.cursor.target.focus();
- }
-
- _close() {
- // async is needed so that that the click event is fired before the
- // dropdown closes (This was a bug for touch devices).
- this.async(() => {
- this.$.dropdown.close();
- }, 1);
- }
-
- /**
- * Get the class for a top-content item based on the given boolean.
- *
- * @param {boolean} bold Whether the item is bold.
- * @return {string} The class for the top-content item.
- */
- _getClassIfBold(bold) {
- return bold ? 'bold-text' : '';
- }
-
- /**
- * Build a URL for the given host and path. The base URL will be only added,
- * if it is not already included in the path.
- *
- * @param {!string} host
- * @param {!string} path
- * @return {!string} The scheme-relative URL.
- */
- _computeURLHelper(host, path) {
- const base = path.startsWith(this.getBaseUrl()) ?
- '' : this.getBaseUrl();
- return '//' + host + base + path;
- }
-
- /**
- * Build a scheme-relative URL for the current host. Will include the base
- * URL if one is present. Note: the URL will be scheme-relative but absolute
- * with regard to the host.
- *
- * @param {!string} path The path for the URL.
- * @return {!string} The scheme-relative URL.
- */
- _computeRelativeURL(path) {
- const host = window.location.host;
- return this._computeURLHelper(host, path);
- }
-
- /**
- * Compute the URL for a link object.
- *
- * @param {!Object} link The object describing the link.
- * @return {!string} The URL.
- */
- _computeLinkURL(link) {
- if (typeof link.url === 'undefined') {
- return '';
- }
- if (link.target || !link.url.startsWith('/')) {
- return link.url;
- }
- return this._computeRelativeURL(link.url);
- }
-
- /**
- * Compute the value for the rel attribute of an anchor for the given link
- * object. If the link has a target value, then the rel must be "noopener"
- * for security reasons.
- *
- * @param {!Object} link The object describing the link.
- * @return {?string} The rel value for the link.
- */
- _computeLinkRel(link) {
- // Note: noopener takes precedence over external.
- if (link.target) { return REL_NOOPENER; }
- if (link.external) { return REL_EXTERNAL; }
- return null;
- }
-
- /**
- * Handle a click on an item of the dropdown.
- *
- * @param {!Event} e
- */
- _handleItemTap(e) {
- const id = e.target.getAttribute('data-id');
- const item = this.items.find(item => item.id === id);
- if (id && !this.disabledIds.includes(id)) {
- if (item) {
- this.dispatchEvent(new CustomEvent('tap-item', {detail: item}));
- }
- this.dispatchEvent(new CustomEvent('tap-item-' + id));
- }
- }
-
- /**
- * If a dropdown item is shown as a button, get the class for the button.
- *
- * @param {string} id
- * @param {!Object} disabledIdsRecord The change record for the disabled IDs
- * list.
- * @return {!string} The class for the item button.
- */
- _computeDisabledClass(id, disabledIdsRecord) {
- return disabledIdsRecord.base.includes(id) ? 'disabled' : '';
- }
-
- /**
- * Recompute the stops for the dropdown item cursor.
- */
- _resetCursorStops() {
- if (this.items && this.items.length > 0 && this.$.dropdown.opened) {
- Polymer.dom.flush();
- this._listElements = Array.from(
- Polymer.dom(this.root).querySelectorAll('li'));
- }
- }
-
- _computeHasTooltip(tooltip) {
- return !!tooltip;
- }
-
- _computeIsDownload(link) {
- return !!link.download;
+ this.$.cursor.previous();
+ } else {
+ this._open();
}
}
- customElements.define(GrDropdown.is, GrDropdown);
-})();
+ /**
+ * Handle the down key.
+ *
+ * @param {!Event} e
+ */
+ _handleDown(e) {
+ if (this.$.dropdown.opened) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.$.cursor.next();
+ } else {
+ this._open();
+ }
+ }
+
+ /**
+ * Handle the tab key.
+ *
+ * @param {!Event} e
+ */
+ _handleTab(e) {
+ if (this.$.dropdown.opened) {
+ // Tab in a native select is a no-op. Emulate this.
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+
+ /**
+ * Handle the enter key.
+ *
+ * @param {!Event} e
+ */
+ _handleEnter(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (this.$.dropdown.opened) {
+ // TODO(milutin): This solution is not particularly robust in general.
+ // Since gr-tooltip-content click on shadow dom is not propagated down,
+ // we have to target `a` inside it.
+ const el = this.$.cursor.target.querySelector(':not([hidden]) a');
+ if (el) { el.click(); }
+ } else {
+ this._open();
+ }
+ }
+
+ /**
+ * Handle a click on the iron-dropdown element.
+ *
+ * @param {!Event} e
+ */
+ _handleDropdownClick(e) {
+ this._close();
+ }
+
+ /**
+ * Hanlde a click on the button to open the dropdown.
+ *
+ * @param {!Event} e
+ */
+ _dropdownTriggerTapHandler(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (this.$.dropdown.opened) {
+ this._close();
+ } else {
+ this._open();
+ }
+ }
+
+ /**
+ * Open the dropdown and initialize the cursor.
+ */
+ _open() {
+ this.$.dropdown.open();
+ this._resetCursorStops();
+ this.$.cursor.setCursorAtIndex(0);
+ this.$.cursor.target.focus();
+ }
+
+ _close() {
+ // async is needed so that that the click event is fired before the
+ // dropdown closes (This was a bug for touch devices).
+ this.async(() => {
+ this.$.dropdown.close();
+ }, 1);
+ }
+
+ /**
+ * Get the class for a top-content item based on the given boolean.
+ *
+ * @param {boolean} bold Whether the item is bold.
+ * @return {string} The class for the top-content item.
+ */
+ _getClassIfBold(bold) {
+ return bold ? 'bold-text' : '';
+ }
+
+ /**
+ * Build a URL for the given host and path. The base URL will be only added,
+ * if it is not already included in the path.
+ *
+ * @param {!string} host
+ * @param {!string} path
+ * @return {!string} The scheme-relative URL.
+ */
+ _computeURLHelper(host, path) {
+ const base = path.startsWith(this.getBaseUrl()) ?
+ '' : this.getBaseUrl();
+ return '//' + host + base + path;
+ }
+
+ /**
+ * Build a scheme-relative URL for the current host. Will include the base
+ * URL if one is present. Note: the URL will be scheme-relative but absolute
+ * with regard to the host.
+ *
+ * @param {!string} path The path for the URL.
+ * @return {!string} The scheme-relative URL.
+ */
+ _computeRelativeURL(path) {
+ const host = window.location.host;
+ return this._computeURLHelper(host, path);
+ }
+
+ /**
+ * Compute the URL for a link object.
+ *
+ * @param {!Object} link The object describing the link.
+ * @return {!string} The URL.
+ */
+ _computeLinkURL(link) {
+ if (typeof link.url === 'undefined') {
+ return '';
+ }
+ if (link.target || !link.url.startsWith('/')) {
+ return link.url;
+ }
+ return this._computeRelativeURL(link.url);
+ }
+
+ /**
+ * Compute the value for the rel attribute of an anchor for the given link
+ * object. If the link has a target value, then the rel must be "noopener"
+ * for security reasons.
+ *
+ * @param {!Object} link The object describing the link.
+ * @return {?string} The rel value for the link.
+ */
+ _computeLinkRel(link) {
+ // Note: noopener takes precedence over external.
+ if (link.target) { return REL_NOOPENER; }
+ if (link.external) { return REL_EXTERNAL; }
+ return null;
+ }
+
+ /**
+ * Handle a click on an item of the dropdown.
+ *
+ * @param {!Event} e
+ */
+ _handleItemTap(e) {
+ const id = e.target.getAttribute('data-id');
+ const item = this.items.find(item => item.id === id);
+ if (id && !this.disabledIds.includes(id)) {
+ if (item) {
+ this.dispatchEvent(new CustomEvent('tap-item', {detail: item}));
+ }
+ this.dispatchEvent(new CustomEvent('tap-item-' + id));
+ }
+ }
+
+ /**
+ * If a dropdown item is shown as a button, get the class for the button.
+ *
+ * @param {string} id
+ * @param {!Object} disabledIdsRecord The change record for the disabled IDs
+ * list.
+ * @return {!string} The class for the item button.
+ */
+ _computeDisabledClass(id, disabledIdsRecord) {
+ return disabledIdsRecord.base.includes(id) ? 'disabled' : '';
+ }
+
+ /**
+ * Recompute the stops for the dropdown item cursor.
+ */
+ _resetCursorStops() {
+ if (this.items && this.items.length > 0 && this.$.dropdown.opened) {
+ flush();
+ this._listElements = Array.from(
+ dom(this.root).querySelectorAll('li'));
+ }
+ }
+
+ _computeHasTooltip(tooltip) {
+ return !!tooltip;
+ }
+
+ _computeIsDownload(link) {
+ return !!link.download;
+ }
+}
+
+customElements.define(GrDropdown.is, GrDropdown);
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js
new file mode 100644
index 0000000..99028af
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js
@@ -0,0 +1,118 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: inline-block;
+ }
+ .dropdown-trigger {
+ text-decoration: none;
+ width: 100%;
+ }
+ .dropdown-content {
+ background-color: var(--dropdown-background-color);
+ box-shadow: var(--elevation-level-2);
+ }
+ gr-button {
+ @apply --gr-button;
+ }
+ gr-avatar {
+ height: 2em;
+ width: 2em;
+ vertical-align: middle;
+ }
+ gr-button[link]:focus {
+ outline: 5px auto -webkit-focus-ring-color;
+ }
+ ul {
+ list-style: none;
+ }
+ .topContent,
+ li {
+ border-bottom: 1px solid var(--border-color);
+ }
+ li:last-of-type {
+ border: none;
+ }
+ li .itemAction {
+ cursor: pointer;
+ display: block;
+ padding: var(--spacing-m) var(--spacing-l);
+ }
+ li .itemAction {
+ @apply --gr-dropdown-item;
+ }
+ li .itemAction.disabled {
+ color: var(--deemphasized-text-color);
+ cursor: default;
+ }
+ li .itemAction:link,
+ li .itemAction:visited {
+ text-decoration: none;
+ }
+ li .itemAction:not(.disabled):hover {
+ background-color: var(--hover-background-color);
+ }
+ li:focus,
+ li.selected {
+ background-color: var(--selection-background-color);
+ outline: none;
+ }
+ li:focus .itemAction,
+ li.selected .itemAction {
+ background-color: transparent;
+ }
+ .topContent {
+ display: block;
+ padding: var(--spacing-m) var(--spacing-l);
+ @apply --gr-dropdown-item;
+ }
+ .bold-text {
+ font-weight: var(--font-weight-bold);
+ }
+ </style>
+ <gr-button link="[[link]]" class="dropdown-trigger" id="trigger" down-arrow="[[downArrow]]" on-click="_dropdownTriggerTapHandler">
+ <slot></slot>
+ </gr-button>
+ <iron-dropdown id="dropdown" vertical-align="top" vertical-offset="[[verticalOffset]]" allow-outside-scroll="true" horizontal-align="[[horizontalAlign]]" on-click="_handleDropdownClick">
+ <div class="dropdown-content" slot="dropdown-content">
+ <ul>
+ <template is="dom-if" if="[[topContent]]">
+ <div class="topContent">
+ <template is="dom-repeat" items="[[topContent]]" as="item" initial-count="75">
+ <div class\$="[[_getClassIfBold(item.bold)]] top-item" tabindex="-1">
+ [[item.text]]
+ </div>
+ </template>
+ </div>
+ </template>
+ <template is="dom-repeat" items="[[items]]" as="link" initial-count="75">
+ <li tabindex="-1">
+ <gr-tooltip-content has-tooltip="[[_computeHasTooltip(link.tooltip)]]" title\$="[[link.tooltip]]">
+ <span class\$="itemAction [[_computeDisabledClass(link.id, disabledIds.*)]]" data-id\$="[[link.id]]" on-click="_handleItemTap" hidden\$="[[link.url]]" tabindex="-1">[[link.name]]</span>
+ <a class="itemAction" href\$="[[_computeLinkURL(link)]]" download\$="[[_computeIsDownload(link)]]" rel\$="[[_computeLinkRel(link)]]" target\$="[[link.target]]" hidden\$="[[!link.url]]" tabindex="-1">[[link.name]]</a>
+ </gr-tooltip-content>
+ </li>
+ </template>
+ </ul>
+ </div>
+ </iron-dropdown>
+ <gr-cursor-manager id="cursor" cursor-target-class="selected" scroll-behavior="never" focus-on-move="" stops="[[_listElements]]"></gr-cursor-manager>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
index 2d7f090..a05634bce 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-dropdown</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-dropdown.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,176 +30,178 @@
</template>
</test-fixture>
-<script>
- suite('gr-dropdown tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-dropdown.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-dropdown tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ });
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_computeIsDownload', () => {
+ assert.isTrue(element._computeIsDownload({download: true}));
+ assert.isFalse(element._computeIsDownload({download: false}));
+ });
+
+ test('tap on trigger opens menu, then closes', () => {
+ sandbox.stub(element, '_open', () => { element.$.dropdown.open(); });
+ sandbox.stub(element, '_close', () => { element.$.dropdown.close(); });
+ assert.isFalse(element.$.dropdown.opened);
+ MockInteractions.tap(element.$.trigger);
+ assert.isTrue(element.$.dropdown.opened);
+ MockInteractions.tap(element.$.trigger);
+ assert.isFalse(element.$.dropdown.opened);
+ });
+
+ test('_computeURLHelper', () => {
+ const path = '/test';
+ const host = 'http://www.testsite.com';
+ const computedPath = element._computeURLHelper(host, path);
+ assert.equal(computedPath, '//http://www.testsite.com/test');
+ });
+
+ test('link URLs', () => {
+ assert.equal(
+ element._computeLinkURL({url: 'http://example.com/test'}),
+ 'http://example.com/test');
+ assert.equal(
+ element._computeLinkURL({url: 'https://example.com/test'}),
+ 'https://example.com/test');
+ assert.equal(
+ element._computeLinkURL({url: '/test'}),
+ '//' + window.location.host + '/test');
+ assert.equal(
+ element._computeLinkURL({url: '/test', target: '_blank'}),
+ '/test');
+ });
+
+ test('link rel', () => {
+ let link = {url: '/test'};
+ assert.isNull(element._computeLinkRel(link));
+
+ link = {url: '/test', target: '_blank'};
+ assert.equal(element._computeLinkRel(link), 'noopener');
+
+ link = {url: '/test', external: true};
+ assert.equal(element._computeLinkRel(link), 'external');
+
+ link = {url: '/test', target: '_blank', external: true};
+ assert.equal(element._computeLinkRel(link), 'noopener');
+ });
+
+ test('_getClassIfBold', () => {
+ let bold = true;
+ assert.equal(element._getClassIfBold(bold), 'bold-text');
+
+ bold = false;
+ assert.equal(element._getClassIfBold(bold), '');
+ });
+
+ test('Top text exists and is bolded correctly', () => {
+ element.topContent = [{text: 'User', bold: true}, {text: 'email'}];
+ flushAsynchronousOperations();
+ const topItems = dom(element.root).querySelectorAll('.top-item');
+ assert.equal(topItems.length, 2);
+ assert.isTrue(topItems[0].classList.contains('bold-text'));
+ assert.isFalse(topItems[1].classList.contains('bold-text'));
+ });
+
+ test('non link items', () => {
+ const item0 = {name: 'item one', id: 'foo'};
+ element.items = [item0, {name: 'item two', id: 'bar'}];
+ const fooTapped = sandbox.stub();
+ const tapped = sandbox.stub();
+ element.addEventListener('tap-item-foo', fooTapped);
+ element.addEventListener('tap-item', tapped);
+ flushAsynchronousOperations();
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.itemAction'));
+ assert.isTrue(fooTapped.called);
+ assert.isTrue(tapped.called);
+ assert.deepEqual(tapped.lastCall.args[0].detail, item0);
+ });
+
+ test('disabled non link item', () => {
+ element.items = [{name: 'item one', id: 'foo'}];
+ element.disabledIds = ['foo'];
+
+ const stub = sandbox.stub();
+ const tapped = sandbox.stub();
+ element.addEventListener('tap-item-foo', stub);
+ element.addEventListener('tap-item', tapped);
+ flushAsynchronousOperations();
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.itemAction'));
+ assert.isFalse(stub.called);
+ assert.isFalse(tapped.called);
+ });
+
+ test('properly sets tooltips', () => {
+ element.items = [
+ {name: 'item one', id: 'foo', tooltip: 'hello'},
+ {name: 'item two', id: 'bar'},
+ ];
+ element.disabledIds = [];
+ flushAsynchronousOperations();
+ const tooltipContents = dom(element.root)
+ .querySelectorAll('iron-dropdown li gr-tooltip-content');
+ assert.equal(tooltipContents.length, 2);
+ assert.isTrue(tooltipContents[0].hasTooltip);
+ assert.equal(tooltipContents[0].getAttribute('title'), 'hello');
+ assert.isFalse(tooltipContents[1].hasTooltip);
+ });
+
+ suite('keyboard navigation', () => {
setup(() => {
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('_computeIsDownload', () => {
- assert.isTrue(element._computeIsDownload({download: true}));
- assert.isFalse(element._computeIsDownload({download: false}));
- });
-
- test('tap on trigger opens menu, then closes', () => {
- sandbox.stub(element, '_open', () => { element.$.dropdown.open(); });
- sandbox.stub(element, '_close', () => { element.$.dropdown.close(); });
- assert.isFalse(element.$.dropdown.opened);
- MockInteractions.tap(element.$.trigger);
- assert.isTrue(element.$.dropdown.opened);
- MockInteractions.tap(element.$.trigger);
- assert.isFalse(element.$.dropdown.opened);
- });
-
- test('_computeURLHelper', () => {
- const path = '/test';
- const host = 'http://www.testsite.com';
- const computedPath = element._computeURLHelper(host, path);
- assert.equal(computedPath, '//http://www.testsite.com/test');
- });
-
- test('link URLs', () => {
- assert.equal(
- element._computeLinkURL({url: 'http://example.com/test'}),
- 'http://example.com/test');
- assert.equal(
- element._computeLinkURL({url: 'https://example.com/test'}),
- 'https://example.com/test');
- assert.equal(
- element._computeLinkURL({url: '/test'}),
- '//' + window.location.host + '/test');
- assert.equal(
- element._computeLinkURL({url: '/test', target: '_blank'}),
- '/test');
- });
-
- test('link rel', () => {
- let link = {url: '/test'};
- assert.isNull(element._computeLinkRel(link));
-
- link = {url: '/test', target: '_blank'};
- assert.equal(element._computeLinkRel(link), 'noopener');
-
- link = {url: '/test', external: true};
- assert.equal(element._computeLinkRel(link), 'external');
-
- link = {url: '/test', target: '_blank', external: true};
- assert.equal(element._computeLinkRel(link), 'noopener');
- });
-
- test('_getClassIfBold', () => {
- let bold = true;
- assert.equal(element._getClassIfBold(bold), 'bold-text');
-
- bold = false;
- assert.equal(element._getClassIfBold(bold), '');
- });
-
- test('Top text exists and is bolded correctly', () => {
- element.topContent = [{text: 'User', bold: true}, {text: 'email'}];
- flushAsynchronousOperations();
- const topItems = Polymer.dom(element.root).querySelectorAll('.top-item');
- assert.equal(topItems.length, 2);
- assert.isTrue(topItems[0].classList.contains('bold-text'));
- assert.isFalse(topItems[1].classList.contains('bold-text'));
- });
-
- test('non link items', () => {
- const item0 = {name: 'item one', id: 'foo'};
- element.items = [item0, {name: 'item two', id: 'bar'}];
- const fooTapped = sandbox.stub();
- const tapped = sandbox.stub();
- element.addEventListener('tap-item-foo', fooTapped);
- element.addEventListener('tap-item', tapped);
- flushAsynchronousOperations();
- MockInteractions.tap(element.shadowRoot
- .querySelector('.itemAction'));
- assert.isTrue(fooTapped.called);
- assert.isTrue(tapped.called);
- assert.deepEqual(tapped.lastCall.args[0].detail, item0);
- });
-
- test('disabled non link item', () => {
- element.items = [{name: 'item one', id: 'foo'}];
- element.disabledIds = ['foo'];
-
- const stub = sandbox.stub();
- const tapped = sandbox.stub();
- element.addEventListener('tap-item-foo', stub);
- element.addEventListener('tap-item', tapped);
- flushAsynchronousOperations();
- MockInteractions.tap(element.shadowRoot
- .querySelector('.itemAction'));
- assert.isFalse(stub.called);
- assert.isFalse(tapped.called);
- });
-
- test('properly sets tooltips', () => {
element.items = [
- {name: 'item one', id: 'foo', tooltip: 'hello'},
+ {name: 'item one', id: 'foo'},
{name: 'item two', id: 'bar'},
];
- element.disabledIds = [];
flushAsynchronousOperations();
- const tooltipContents = Polymer.dom(element.root)
- .querySelectorAll('iron-dropdown li gr-tooltip-content');
- assert.equal(tooltipContents.length, 2);
- assert.isTrue(tooltipContents[0].hasTooltip);
- assert.equal(tooltipContents[0].getAttribute('title'), 'hello');
- assert.isFalse(tooltipContents[1].hasTooltip);
});
- suite('keyboard navigation', () => {
- setup(() => {
- element.items = [
- {name: 'item one', id: 'foo'},
- {name: 'item two', id: 'bar'},
- ];
- flushAsynchronousOperations();
- });
+ test('down', () => {
+ const stub = sandbox.stub(element.$.cursor, 'next');
+ assert.isFalse(element.$.dropdown.opened);
+ MockInteractions.pressAndReleaseKeyOn(element, 40);
+ assert.isTrue(element.$.dropdown.opened);
+ MockInteractions.pressAndReleaseKeyOn(element, 40);
+ assert.isTrue(stub.called);
+ });
- test('down', () => {
- const stub = sandbox.stub(element.$.cursor, 'next');
- assert.isFalse(element.$.dropdown.opened);
- MockInteractions.pressAndReleaseKeyOn(element, 40);
- assert.isTrue(element.$.dropdown.opened);
- MockInteractions.pressAndReleaseKeyOn(element, 40);
- assert.isTrue(stub.called);
- });
+ test('up', () => {
+ const stub = sandbox.stub(element.$.cursor, 'previous');
+ assert.isFalse(element.$.dropdown.opened);
+ MockInteractions.pressAndReleaseKeyOn(element, 38);
+ assert.isTrue(element.$.dropdown.opened);
+ MockInteractions.pressAndReleaseKeyOn(element, 38);
+ assert.isTrue(stub.called);
+ });
- test('up', () => {
- const stub = sandbox.stub(element.$.cursor, 'previous');
- assert.isFalse(element.$.dropdown.opened);
- MockInteractions.pressAndReleaseKeyOn(element, 38);
- assert.isTrue(element.$.dropdown.opened);
- MockInteractions.pressAndReleaseKeyOn(element, 38);
- assert.isTrue(stub.called);
- });
+ test('enter/space', () => {
+ // Because enter and space are handled by the same fn, we need only to
+ // test one.
+ assert.isFalse(element.$.dropdown.opened);
+ MockInteractions.pressAndReleaseKeyOn(element, 32); // Space
+ assert.isTrue(element.$.dropdown.opened);
- test('enter/space', () => {
- // Because enter and space are handled by the same fn, we need only to
- // test one.
- assert.isFalse(element.$.dropdown.opened);
- MockInteractions.pressAndReleaseKeyOn(element, 32); // Space
- assert.isTrue(element.$.dropdown.opened);
-
- const el = element.$.cursor.target.querySelector(':not([hidden]) a');
- const stub = sandbox.stub(el, 'click');
- MockInteractions.pressAndReleaseKeyOn(element, 32); // Space
- assert.isTrue(stub.called);
- });
+ const el = element.$.cursor.target.querySelector(':not([hidden]) a');
+ const stub = sandbox.stub(el, 'click');
+ MockInteractions.pressAndReleaseKeyOn(element, 32); // Space
+ assert.isTrue(stub.called);
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html
deleted file mode 100644
index 627f948..0000000
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html
+++ /dev/null
@@ -1,83 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../gr-storage/gr-storage.html">
-<link rel="import" href="../gr-button/gr-button.html">
-
-<dom-module id="gr-editable-content">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- }
- :host([disabled]) iron-autogrow-textarea {
- opacity: .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 {
- max-height: 36em;
- overflow: hidden;
- }
- .editor iron-autogrow-textarea {
- background-color: var(--view-background-color);
- width: 100%;
-
- /* 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;
- }
- </style>
- <div class="viewer" hidden$="[[editing]]">
- <slot></slot>
- </div>
- <div class="editor" hidden$="[[!editing]]">
- <iron-autogrow-textarea
- autocomplete="on"
- bind-value="{{_newContent}}"
- disabled="[[disabled]]"></iron-autogrow-textarea>
- <div class="editButtons">
- <gr-button primary
- on-click="_handleSave"
- disabled="[[_saveDisabled]]">Save</gr-button>
- <gr-button
- on-click="_handleCancel"
- disabled="[[disabled]]">Cancel</gr-button>
- </div>
- </div>
- <gr-storage id="storage"></gr-storage>
- </template>
- <script src="gr-editable-content.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
index 4510d3f..a417e3f 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
@@ -14,152 +14,163 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const RESTORED_MESSAGE = 'Content restored from a previous edit.';
- const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../gr-storage/gr-storage.js';
+import '../gr-button/gr-button.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-editable-content_html.js';
+
+const RESTORED_MESSAGE = 'Content restored from a previous edit.';
+const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrEditableContent extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-editable-content'; }
+ /**
+ * Fired when the save button is pressed.
+ *
+ * @event editable-content-save
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event editable-content-cancel
*/
- class GrEditableContent extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-editable-content'; }
- /**
- * Fired when the save button is pressed.
- *
- * @event editable-content-save
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event editable-content-cancel
- */
+ /**
+ * Fired when content is restored from storage.
+ *
+ * @event show-alert
+ */
- /**
- * Fired when content is restored from storage.
- *
- * @event show-alert
- */
-
- static get properties() {
- return {
- content: {
- notify: true,
- type: String,
- },
- disabled: {
- reflectToAttribute: true,
- type: Boolean,
- value: false,
- },
- editing: {
- observer: '_editingChanged',
- type: Boolean,
- value: false,
- },
- removeZeroWidthSpace: Boolean,
- // If no storage key is provided, content is not stored.
- storageKey: String,
- _saveDisabled: {
- computed: '_computeSaveDisabled(disabled, content, _newContent)',
- type: Boolean,
- value: true,
- },
- _newContent: {
- type: String,
- observer: '_newContentChanged',
- },
- };
- }
-
- focusTextarea() {
- this.shadowRoot.querySelector('iron-autogrow-textarea').textarea.focus();
- }
-
- _newContentChanged(newContent, oldContent) {
- if (!this.storageKey) { return; }
-
- this.debounce('store', () => {
- if (newContent.length) {
- this.$.storage.setEditableContentItem(this.storageKey, 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
- // all the content in the editable textarea. But <gr-storage> cleans
- // up itself after one day, so we are not so concerned about leaving
- // some garbage behind.
- this.$.storage.eraseEditableContentItem(this.storageKey);
- }
- }, STORAGE_DEBOUNCE_INTERVAL_MS);
- }
-
- _editingChanged(editing) {
- // This method is for initializing _newContent when you start editing.
- // Restoring content from local storage is not perfect and has
- // some issues:
- //
- // 1. When you start editing in multiple tabs, then we are vulnerable to
- // race conditions between the tabs.
- // 2. The stored content is keyed by revision, so when you upload a new
- // patchset and click "reload" and then click "cancel" on the content-
- // editable, then you won't be able to recover the content anymore.
- //
- // Because of these issues we believe that it is better to only recover
- // 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;
- }
-
- let content;
- if (this.storageKey) {
- const storedContent =
- this.$.storage.getEditableContentItem(this.storageKey);
- if (storedContent && storedContent.message) {
- content = storedContent.message;
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {message: RESTORED_MESSAGE},
- bubbles: true,
- composed: true,
- }));
- }
- }
- if (!content) {
- content = this.content || '';
- }
-
- // TODO(wyatta) switch linkify sequence, see issue 5526.
- this._newContent = this.removeZeroWidthSpace ?
- content.replace(/^R=\u200B/gm, 'R=') :
- content;
- }
-
- _computeSaveDisabled(disabled, content, newContent) {
- return disabled || !newContent || content === newContent;
- }
-
- _handleSave(e) {
- e.preventDefault();
- this.fire('editable-content-save', {content: this._newContent});
- // 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) {
- e.preventDefault();
- this.editing = false;
- this.fire('editable-content-cancel');
- }
+ static get properties() {
+ return {
+ content: {
+ notify: true,
+ type: String,
+ },
+ disabled: {
+ reflectToAttribute: true,
+ type: Boolean,
+ value: false,
+ },
+ editing: {
+ observer: '_editingChanged',
+ type: Boolean,
+ value: false,
+ },
+ removeZeroWidthSpace: Boolean,
+ // If no storage key is provided, content is not stored.
+ storageKey: String,
+ _saveDisabled: {
+ computed: '_computeSaveDisabled(disabled, content, _newContent)',
+ type: Boolean,
+ value: true,
+ },
+ _newContent: {
+ type: String,
+ observer: '_newContentChanged',
+ },
+ };
}
- customElements.define(GrEditableContent.is, GrEditableContent);
-})();
+ focusTextarea() {
+ this.shadowRoot.querySelector('iron-autogrow-textarea').textarea.focus();
+ }
+
+ _newContentChanged(newContent, oldContent) {
+ if (!this.storageKey) { return; }
+
+ this.debounce('store', () => {
+ if (newContent.length) {
+ this.$.storage.setEditableContentItem(this.storageKey, 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
+ // all the content in the editable textarea. But <gr-storage> cleans
+ // up itself after one day, so we are not so concerned about leaving
+ // some garbage behind.
+ this.$.storage.eraseEditableContentItem(this.storageKey);
+ }
+ }, STORAGE_DEBOUNCE_INTERVAL_MS);
+ }
+
+ _editingChanged(editing) {
+ // This method is for initializing _newContent when you start editing.
+ // Restoring content from local storage is not perfect and has
+ // some issues:
+ //
+ // 1. When you start editing in multiple tabs, then we are vulnerable to
+ // race conditions between the tabs.
+ // 2. The stored content is keyed by revision, so when you upload a new
+ // patchset and click "reload" and then click "cancel" on the content-
+ // editable, then you won't be able to recover the content anymore.
+ //
+ // Because of these issues we believe that it is better to only recover
+ // 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;
+ }
+
+ let content;
+ if (this.storageKey) {
+ const storedContent =
+ this.$.storage.getEditableContentItem(this.storageKey);
+ if (storedContent && storedContent.message) {
+ content = storedContent.message;
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {message: RESTORED_MESSAGE},
+ bubbles: true,
+ composed: true,
+ }));
+ }
+ }
+ if (!content) {
+ content = this.content || '';
+ }
+
+ // TODO(wyatta) switch linkify sequence, see issue 5526.
+ this._newContent = this.removeZeroWidthSpace ?
+ content.replace(/^R=\u200B/gm, 'R=') :
+ content;
+ }
+
+ _computeSaveDisabled(disabled, content, newContent) {
+ return disabled || !newContent || content === newContent;
+ }
+
+ _handleSave(e) {
+ e.preventDefault();
+ this.fire('editable-content-save', {content: this._newContent});
+ // 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) {
+ e.preventDefault();
+ this.editing = false;
+ this.fire('editable-content-cancel');
+ }
+}
+
+customElements.define(GrEditableContent.is, GrEditableContent);
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.js
new file mode 100644
index 0000000..e0e5047
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.js
@@ -0,0 +1,67 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ }
+ :host([disabled]) iron-autogrow-textarea {
+ opacity: .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 {
+ max-height: 36em;
+ overflow: hidden;
+ }
+ .editor iron-autogrow-textarea {
+ background-color: var(--view-background-color);
+ width: 100%;
+
+ /* 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;
+ }
+ </style>
+ <div class="viewer" hidden\$="[[editing]]">
+ <slot></slot>
+ </div>
+ <div class="editor" hidden\$="[[!editing]]">
+ <iron-autogrow-textarea autocomplete="on" bind-value="{{_newContent}}" disabled="[[disabled]]"></iron-autogrow-textarea>
+ <div class="editButtons">
+ <gr-button primary="" on-click="_handleSave" disabled="[[_saveDisabled]]">Save</gr-button>
+ <gr-button on-click="_handleCancel" disabled="[[disabled]]">Cancel</gr-button>
+ </div>
+ </div>
+ <gr-storage id="storage"></gr-storage>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
index 87be1e9..8b9f39d 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
@@ -19,17 +19,17 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-editable-content</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="/bower_components/iron-test-helpers/mock-interactions.js"></script>
-
-<link rel="import" href="gr-editable-content.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<!-- Can't use absolute path below for mock-interaction.js.
+Web component tester(wct) has a built-in http server and it serves "/components" directory (which is
+actually /node_modules directory). Also, wct patches some files to load modules from /components.
+With the absolute path, browser tries to load dom-module from 2 different places (/component/... and
+/node_modules/...) though this is actually the same file. This leads to a run-time error.
+-->
+<script src="../../../node_modules/iron-test-helpers/mock-interactions.js"></script>
<test-fixture id="basic">
<template>
@@ -37,128 +37,129 @@
</template>
</test-fixture>
-<script>
- suite('gr-editable-content tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-editable-content.js';
+suite('gr-editable-content tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => { sandbox.restore(); });
+
+ test('save event', done => {
+ element.content = '';
+ element._newContent = 'foo';
+ element.addEventListener('editable-content-save', e => {
+ assert.equal(e.detail.content, 'foo');
+ done();
+ });
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button[primary]'));
+ });
+
+ test('cancel event', done => {
+ element.addEventListener('editable-content-cancel', () => {
+ done();
+ });
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button:not([primary])'));
+ });
+
+ test('enabling editing keeps old content', () => {
+ element.content = 'current content';
+ element._newContent = 'old content';
+ element.editing = true;
+ 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.editing = false;
+ assert.equal(element._newContent, 'stale content');
+ });
+
+ test('zero width spaces are removed properly', () => {
+ element.removeZeroWidthSpace = true;
+ element.content = 'R=\u200Btest@google.com';
+ element.editing = true;
+ assert.equal(element._newContent, 'R=test@google.com');
+ });
+
+ suite('editing', () => {
setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('save event', done => {
- element.content = '';
- element._newContent = 'foo';
- element.addEventListener('editable-content-save', e => {
- assert.equal(e.detail.content, 'foo');
- done();
- });
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button[primary]'));
- });
-
- test('cancel event', done => {
- element.addEventListener('editable-content-cancel', () => {
- done();
- });
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button:not([primary])'));
- });
-
- test('enabling editing keeps old content', () => {
- element.content = 'current content';
- element._newContent = 'old content';
- element.editing = true;
- 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.editing = false;
- assert.equal(element._newContent, 'stale content');
});
- test('zero width spaces are removed properly', () => {
- element.removeZeroWidthSpace = true;
- element.content = 'R=\u200Btest@google.com';
- element.editing = true;
- assert.equal(element._newContent, 'R=test@google.com');
+ test('save button is disabled initially', () => {
+ assert.isTrue(element.shadowRoot
+ .querySelector('gr-button[primary]').disabled);
});
- suite('editing', () => {
- setup(() => {
- element.content = 'current content';
- element.editing = true;
- });
-
- test('save button is disabled initially', () => {
- assert.isTrue(element.shadowRoot
- .querySelector('gr-button[primary]').disabled);
- });
-
- test('save button is enabled when content changes', () => {
- element._newContent = 'new content';
- assert.isFalse(element.shadowRoot
- .querySelector('gr-button[primary]').disabled);
- });
- });
-
- suite('storageKey and related behavior', () => {
- let dispatchSpy;
- setup(() => {
- element.content = 'current content';
- element.storageKey = 'test';
- dispatchSpy = sandbox.spy(element, 'dispatchEvent');
- });
-
- test('editing toggled to true, has stored data', () => {
- sandbox.stub(element.$.storage, 'getEditableContentItem')
- .returns({message: 'stored content'});
- element.editing = true;
-
- assert.equal(element._newContent, 'stored content');
- assert.isTrue(dispatchSpy.called);
- assert.equal(dispatchSpy.lastCall.args[0].type, 'show-alert');
- });
-
- test('editing toggled to true, has no stored data', () => {
- sandbox.stub(element.$.storage, 'getEditableContentItem')
- .returns({});
- element.editing = true;
-
- assert.equal(element._newContent, 'current content');
- assert.isFalse(dispatchSpy.called);
- });
-
- test('edits are cached', () => {
- const storeStub =
- sandbox.stub(element.$.storage, 'setEditableContentItem');
- const eraseStub =
- sandbox.stub(element.$.storage, 'eraseEditableContentItem');
- element.editing = true;
-
- element._newContent = 'new content';
- flushAsynchronousOperations();
- element.flushDebouncer('store');
-
- assert.isTrue(storeStub.called);
- assert.deepEqual(
- [element.storageKey, element._newContent],
- storeStub.lastCall.args);
-
- element._newContent = '';
- flushAsynchronousOperations();
- element.flushDebouncer('store');
-
- assert.isTrue(eraseStub.called);
- assert.deepEqual([element.storageKey], eraseStub.lastCall.args);
- });
+ test('save button is enabled when content changes', () => {
+ element._newContent = 'new content';
+ assert.isFalse(element.shadowRoot
+ .querySelector('gr-button[primary]').disabled);
});
});
+
+ suite('storageKey and related behavior', () => {
+ let dispatchSpy;
+ setup(() => {
+ element.content = 'current content';
+ element.storageKey = 'test';
+ dispatchSpy = sandbox.spy(element, 'dispatchEvent');
+ });
+
+ test('editing toggled to true, has stored data', () => {
+ sandbox.stub(element.$.storage, 'getEditableContentItem')
+ .returns({message: 'stored content'});
+ element.editing = true;
+
+ assert.equal(element._newContent, 'stored content');
+ assert.isTrue(dispatchSpy.called);
+ assert.equal(dispatchSpy.lastCall.args[0].type, 'show-alert');
+ });
+
+ test('editing toggled to true, has no stored data', () => {
+ sandbox.stub(element.$.storage, 'getEditableContentItem')
+ .returns({});
+ element.editing = true;
+
+ assert.equal(element._newContent, 'current content');
+ assert.isFalse(dispatchSpy.called);
+ });
+
+ test('edits are cached', () => {
+ const storeStub =
+ sandbox.stub(element.$.storage, 'setEditableContentItem');
+ const eraseStub =
+ sandbox.stub(element.$.storage, 'eraseEditableContentItem');
+ element.editing = true;
+
+ element._newContent = 'new content';
+ flushAsynchronousOperations();
+ element.flushDebouncer('store');
+
+ assert.isTrue(storeStub.called);
+ assert.deepEqual(
+ [element.storageKey, element._newContent],
+ storeStub.lastCall.args);
+
+ element._newContent = '';
+ flushAsynchronousOperations();
+ element.flushDebouncer('store');
+
+ assert.isTrue(eraseStub.called);
+ assert.deepEqual([element.storageKey], eraseStub.lastCall.args);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
deleted file mode 100644
index 8d0d1c37..0000000
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
+++ /dev/null
@@ -1,107 +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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="/bower_components/iron-overlay-behavior/iron-overlay-behavior.html">
-<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
-<link rel="import" href="/bower_components/paper-input/paper-input.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../gr-button/gr-button.html">
-
-<dom-module id="gr-editable-label">
- <template>
- <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;
- @apply --label-style;
- }
- 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);
- @apply --input-style;
- }
- .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);
- }
- </style>
- <label
- class$="[[_computeLabelClass(readOnly, value, placeholder)]]"
- title$="[[_computeLabel(value, placeholder)]]"
- on-click="_showDropdown">[[_computeLabel(value, placeholder)]]</label>
- <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">
- <paper-input
- id="input"
- label="[[labelText]]"
- maxlength="[[maxLength]]"
- value="{{_inputText}}"></paper-input>
- <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>
- </template>
- <script src="gr-editable-label.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
index ef5bb8c..3de5a64 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
@@ -14,193 +14,207 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const AWAIT_MAX_ITERS = 10;
- const AWAIT_STEP = 5;
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {IronOverlayBehaviorImpl} from '@polymer/iron-overlay-behavior/iron-overlay-behavior.js';
+import '@polymer/iron-dropdown/iron-dropdown.js';
+import '@polymer/paper-input/paper-input.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../gr-button/gr-button.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-editable-label_html.js';
+
+const AWAIT_MAX_ITERS = 10;
+const AWAIT_STEP = 5;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrEditableLabel extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-editable-label'; }
+ /**
+ * Fired when the value is changed.
+ *
+ * @event changed
+ */
+
+ static get properties() {
+ return {
+ labelText: String,
+ editing: {
+ type: Boolean,
+ value: false,
+ },
+ value: {
+ type: String,
+ notify: true,
+ value: '',
+ observer: '_updateTitle',
+ },
+ placeholder: {
+ type: String,
+ value: '',
+ },
+ readOnly: {
+ type: Boolean,
+ value: false,
+ },
+ uppercase: {
+ type: Boolean,
+ reflectToAttribute: true,
+ value: false,
+ },
+ maxLength: Number,
+ _inputText: String,
+ // This is used to push the iron-input element up on the page, so
+ // the input is placed in approximately the same position as the
+ // trigger.
+ _verticalOffset: {
+ type: Number,
+ readOnly: true,
+ value: -30,
+ },
+ };
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ this._ensureAttribute('tabindex', '0');
+ }
+
+ get keyBindings() {
+ return {
+ enter: '_handleEnter',
+ esc: '_handleEsc',
+ };
+ }
+
+ _usePlaceholder(value, placeholder) {
+ return (!value || !value.length) && placeholder;
+ }
+
+ _computeLabel(value, placeholder) {
+ if (this._usePlaceholder(value, placeholder)) {
+ return placeholder;
+ }
+ return value;
+ }
+
+ _showDropdown() {
+ if (this.readOnly || this.editing) { return; }
+ return this._open().then(() => {
+ this._nativeInput.focus();
+ if (!this.$.input.value) { return; }
+ this._nativeInput.setSelectionRange(0, this.$.input.value.length);
+ });
+ }
+
+ open() {
+ return this._open().then(() => {
+ this._nativeInput.focus();
+ });
+ }
+
+ _open(...args) {
+ this.$.dropdown.open();
+ this._inputText = this.value;
+ this.editing = true;
+
+ return new Promise(resolve => {
+ IronOverlayBehaviorImpl.open.apply(this.$.dropdown, args);
+ this._awaitOpen(resolve);
+ });
+ }
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually
+ * opening. Eventually replace with a direct way to listen to the overlay.
*/
- class GrEditableLabel extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-editable-label'; }
- /**
- * Fired when the value is changed.
- *
- * @event changed
- */
+ _awaitOpen(fn) {
+ let iters = 0;
+ const step = () => {
+ this.async(() => {
+ if (this.$.dropdown.style.display !== 'none') {
+ fn.call(this);
+ } else if (iters++ < AWAIT_MAX_ITERS) {
+ step.call(this);
+ }
+ }, AWAIT_STEP);
+ };
+ step.call(this);
+ }
- static get properties() {
- return {
- labelText: String,
- editing: {
- type: Boolean,
- value: false,
- },
- value: {
- type: String,
- notify: true,
- value: '',
- observer: '_updateTitle',
- },
- placeholder: {
- type: String,
- value: '',
- },
- readOnly: {
- type: Boolean,
- value: false,
- },
- uppercase: {
- type: Boolean,
- reflectToAttribute: true,
- value: false,
- },
- maxLength: Number,
- _inputText: String,
- // This is used to push the iron-input element up on the page, so
- // the input is placed in approximately the same position as the
- // trigger.
- _verticalOffset: {
- type: Number,
- readOnly: true,
- value: -30,
- },
- };
- }
+ _id() {
+ return this.getAttribute('id') || 'global';
+ }
- /** @override */
- ready() {
- super.ready();
- this._ensureAttribute('tabindex', '0');
- }
+ _save() {
+ if (!this.editing) { return; }
+ this.$.dropdown.close();
+ this.value = this._inputText;
+ this.editing = false;
+ this.fire('changed', this.value);
+ }
- get keyBindings() {
- return {
- enter: '_handleEnter',
- esc: '_handleEsc',
- };
- }
+ _cancel() {
+ if (!this.editing) { return; }
+ this.$.dropdown.close();
+ this.editing = false;
+ this._inputText = this.value;
+ }
- _usePlaceholder(value, placeholder) {
- return (!value || !value.length) && placeholder;
- }
+ get _nativeInput() {
+ // In Polymer 2, the namespace of nativeInput
+ // changed from input to nativeInput
+ return this.$.input.$.nativeInput || this.$.input.$.input;
+ }
- _computeLabel(value, placeholder) {
- if (this._usePlaceholder(value, placeholder)) {
- return placeholder;
- }
- return value;
- }
-
- _showDropdown() {
- if (this.readOnly || this.editing) { return; }
- return this._open().then(() => {
- this._nativeInput.focus();
- if (!this.$.input.value) { return; }
- this._nativeInput.setSelectionRange(0, this.$.input.value.length);
- });
- }
-
- open() {
- return this._open().then(() => {
- this._nativeInput.focus();
- });
- }
-
- _open(...args) {
- this.$.dropdown.open();
- this._inputText = this.value;
- this.editing = true;
-
- return new Promise(resolve => {
- Polymer.IronOverlayBehaviorImpl.open.apply(this.$.dropdown, args);
- this._awaitOpen(resolve);
- });
- }
-
- /**
- * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually
- * opening. Eventually replace with a direct way to listen to the overlay.
- */
- _awaitOpen(fn) {
- let iters = 0;
- const step = () => {
- this.async(() => {
- if (this.$.dropdown.style.display !== 'none') {
- fn.call(this);
- } else if (iters++ < AWAIT_MAX_ITERS) {
- step.call(this);
- }
- }, AWAIT_STEP);
- };
- step.call(this);
- }
-
- _id() {
- return this.getAttribute('id') || 'global';
- }
-
- _save() {
- if (!this.editing) { return; }
- this.$.dropdown.close();
- this.value = this._inputText;
- this.editing = false;
- this.fire('changed', this.value);
- }
-
- _cancel() {
- if (!this.editing) { return; }
- this.$.dropdown.close();
- this.editing = false;
- this._inputText = this.value;
- }
-
- get _nativeInput() {
- // In Polymer 2, the namespace of nativeInput
- // changed from input to nativeInput
- return this.$.input.$.nativeInput || this.$.input.$.input;
- }
-
- _handleEnter(e) {
- e = this.getKeyboardEvent(e);
- const target = Polymer.dom(e).rootTarget;
- if (target === this._nativeInput) {
- e.preventDefault();
- this._save();
- }
- }
-
- _handleEsc(e) {
- e = this.getKeyboardEvent(e);
- const target = Polymer.dom(e).rootTarget;
- if (target === this._nativeInput) {
- e.preventDefault();
- this._cancel();
- }
- }
-
- _computeLabelClass(readOnly, value, placeholder) {
- const classes = [];
- if (!readOnly) { classes.push('editable'); }
- if (this._usePlaceholder(value, placeholder)) {
- classes.push('placeholder');
- }
- return classes.join(' ');
- }
-
- _updateTitle(value) {
- this.setAttribute('title', this._computeLabel(value, this.placeholder));
+ _handleEnter(e) {
+ e = this.getKeyboardEvent(e);
+ const target = dom(e).rootTarget;
+ if (target === this._nativeInput) {
+ e.preventDefault();
+ this._save();
}
}
- customElements.define(GrEditableLabel.is, GrEditableLabel);
-})();
+ _handleEsc(e) {
+ e = this.getKeyboardEvent(e);
+ const target = dom(e).rootTarget;
+ if (target === this._nativeInput) {
+ e.preventDefault();
+ this._cancel();
+ }
+ }
+
+ _computeLabelClass(readOnly, value, placeholder) {
+ const classes = [];
+ if (!readOnly) { classes.push('editable'); }
+ if (this._usePlaceholder(value, placeholder)) {
+ classes.push('placeholder');
+ }
+ return classes.join(' ');
+ }
+
+ _updateTitle(value) {
+ this.setAttribute('title', this._computeLabel(value, this.placeholder));
+ }
+}
+
+customElements.define(GrEditableLabel.is, GrEditableLabel);
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.js
new file mode 100644
index 0000000..9bc31a3
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.js
@@ -0,0 +1,84 @@
+/**
+ * @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.js';
+
+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;
+ @apply --label-style;
+ }
+ 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);
+ @apply --input-style;
+ }
+ .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);
+ }
+ </style>
+ <label class\$="[[_computeLabelClass(readOnly, value, placeholder)]]" title\$="[[_computeLabel(value, placeholder)]]" on-click="_showDropdown">[[_computeLabel(value, placeholder)]]</label>
+ <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">
+ <paper-input id="input" label="[[labelText]]" maxlength="[[maxLength]]" value="{{_inputText}}"></paper-input>
+ <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.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
index 540c10d..ef392df 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
@@ -19,17 +19,17 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-editable-label</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="/bower_components/iron-test-helpers/mock-interactions.js"></script>
-
-<link rel="import" href="gr-editable-label.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<!-- Can't use absolute path below for mock-interaction.js.
+Web component tester(wct) has a built-in http server and it serves "/components" directory (which is
+actually /node_modules directory). Also, wct patches some files to load modules from /components.
+With the absolute path, browser tries to load dom-module from 2 different places (/component/... and
+/node_modules/...) though this is actually the same file. This leads to a run-time error.
+-->
+<script src="../../../node_modules/iron-test-helpers/mock-interactions.js"></script>
<test-fixture id="basic">
<template>
@@ -54,197 +54,199 @@
</template>
</test-fixture>
-<script>
- suite('gr-editable-label tests', async () => {
- await readyToTest();
- let element;
- let elementNoPlaceholder;
- let input;
- let label;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-editable-label.js';
+import {flush as flush$0} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-editable-label tests', () => {
+ let element;
+ let elementNoPlaceholder;
+ let input;
+ let label;
+ let sandbox;
- setup(done => {
- element = fixture('basic');
- elementNoPlaceholder = fixture('no-placeholder');
+ setup(done => {
+ element = fixture('basic');
+ elementNoPlaceholder = fixture('no-placeholder');
- label = element.shadowRoot
- .querySelector('label');
- sandbox = sinon.sandbox.create();
- flush(() => {
- // In Polymer 2 inputElement isn't nativeInput anymore
- input = element.$.input.$.nativeInput || element.$.input.inputElement;
- done();
- });
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('element render', () => {
- // The dropdown is closed and the label is visible:
- assert.isFalse(element.$.dropdown.opened);
- assert.isTrue(label.classList.contains('editable'));
- assert.equal(label.textContent, 'value text');
- const focusSpy = sandbox.spy(input, 'focus');
- const showSpy = sandbox.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');
- });
- });
-
- test('title with placeholder', done => {
- assert.equal(element.title, 'value text');
- element.value = '';
-
- element.async(() => {
- assert.equal(element.title, 'label text');
- done();
- });
- });
-
- test('title without placeholder', done => {
- assert.equal(elementNoPlaceholder.title, '');
- element.value = 'value text';
-
- element.async(() => {
- assert.equal(element.title, 'value text');
- done();
- });
- });
-
- test('edit value', done => {
- const editedStub = sandbox.stub();
- element.addEventListener('changed', editedStub);
- assert.isFalse(element.editing);
-
- MockInteractions.tap(label);
-
- Polymer.dom.flush();
-
- assert.isTrue(element.editing);
- element._inputText = 'new text';
-
- assert.isFalse(editedStub.called);
-
- element.async(() => {
- assert.isTrue(editedStub.called);
- assert.equal(input.value, 'new text');
- assert.isFalse(element.editing);
- done();
- });
-
- // Press enter:
- MockInteractions.keyDownOn(input, 13);
- });
-
- test('save button', done => {
- const editedStub = sandbox.stub();
- element.addEventListener('changed', editedStub);
- assert.isFalse(element.editing);
-
- MockInteractions.tap(label);
-
- Polymer.dom.flush();
-
- assert.isTrue(element.editing);
- element._inputText = 'new text';
-
- assert.isFalse(editedStub.called);
-
- element.async(() => {
- assert.isTrue(editedStub.called);
- assert.equal(input.value, 'new text');
- assert.isFalse(element.editing);
- done();
- });
-
- // Press enter:
- MockInteractions.tap(element.$.saveBtn, 13);
- });
-
- test('edit and then escape key', done => {
- const editedStub = sandbox.stub();
- element.addEventListener('changed', editedStub);
- assert.isFalse(element.editing);
-
- MockInteractions.tap(label);
-
- Polymer.dom.flush();
-
- assert.isTrue(element.editing);
- element._inputText = 'new text';
-
- assert.isFalse(editedStub.called);
-
- element.async(() => {
- assert.isFalse(editedStub.called);
- // Text changes sould be discarded.
- assert.equal(input.value, 'value text');
- assert.isFalse(element.editing);
- done();
- });
-
- // Press escape:
- MockInteractions.keyDownOn(input, 27);
- });
-
- test('cancel button', done => {
- const editedStub = sandbox.stub();
- element.addEventListener('changed', editedStub);
- assert.isFalse(element.editing);
-
- MockInteractions.tap(label);
-
- Polymer.dom.flush();
-
- assert.isTrue(element.editing);
- element._inputText = 'new text';
-
- assert.isFalse(editedStub.called);
-
- element.async(() => {
- assert.isFalse(editedStub.called);
- // Text changes sould be discarded.
- assert.equal(input.value, 'value text');
- assert.isFalse(element.editing);
- done();
- });
-
- // Press escape:
- MockInteractions.tap(element.$.cancelBtn);
- });
-
- suite('gr-editable-label read-only tests', () => {
- let element;
- let label;
-
- setup(() => {
- element = fixture('read-only');
- label = element.shadowRoot
- .querySelector('label');
- });
-
- test('disallows edit when read-only', () => {
- // The dropdown is closed.
- assert.isFalse(element.$.dropdown.opened);
- MockInteractions.tap(label);
-
- Polymer.dom.flush();
-
- // The dropdown is still closed.
- assert.isFalse(element.$.dropdown.opened);
- });
-
- test('label is not marked as editable', () => {
- assert.isFalse(label.classList.contains('editable'));
- });
+ label = element.shadowRoot
+ .querySelector('label');
+ sandbox = sinon.sandbox.create();
+ flush(() => {
+ // In Polymer 2 inputElement isn't nativeInput anymore
+ input = element.$.input.$.nativeInput || element.$.input.inputElement;
+ done();
});
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('element render', () => {
+ // The dropdown is closed and the label is visible:
+ assert.isFalse(element.$.dropdown.opened);
+ assert.isTrue(label.classList.contains('editable'));
+ assert.equal(label.textContent, 'value text');
+ const focusSpy = sandbox.spy(input, 'focus');
+ const showSpy = sandbox.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');
+ });
+ });
+
+ test('title with placeholder', done => {
+ assert.equal(element.title, 'value text');
+ element.value = '';
+
+ element.async(() => {
+ assert.equal(element.title, 'label text');
+ done();
+ });
+ });
+
+ test('title without placeholder', done => {
+ assert.equal(elementNoPlaceholder.title, '');
+ element.value = 'value text';
+
+ element.async(() => {
+ assert.equal(element.title, 'value text');
+ done();
+ });
+ });
+
+ test('edit value', done => {
+ const editedStub = sandbox.stub();
+ element.addEventListener('changed', editedStub);
+ assert.isFalse(element.editing);
+
+ MockInteractions.tap(label);
+
+ flush$0();
+
+ assert.isTrue(element.editing);
+ element._inputText = 'new text';
+
+ assert.isFalse(editedStub.called);
+
+ element.async(() => {
+ assert.isTrue(editedStub.called);
+ assert.equal(input.value, 'new text');
+ assert.isFalse(element.editing);
+ done();
+ });
+
+ // Press enter:
+ MockInteractions.keyDownOn(input, 13);
+ });
+
+ test('save button', done => {
+ const editedStub = sandbox.stub();
+ element.addEventListener('changed', editedStub);
+ assert.isFalse(element.editing);
+
+ MockInteractions.tap(label);
+
+ flush$0();
+
+ assert.isTrue(element.editing);
+ element._inputText = 'new text';
+
+ assert.isFalse(editedStub.called);
+
+ element.async(() => {
+ assert.isTrue(editedStub.called);
+ assert.equal(input.value, 'new text');
+ assert.isFalse(element.editing);
+ done();
+ });
+
+ // Press enter:
+ MockInteractions.tap(element.$.saveBtn, 13);
+ });
+
+ test('edit and then escape key', done => {
+ const editedStub = sandbox.stub();
+ element.addEventListener('changed', editedStub);
+ assert.isFalse(element.editing);
+
+ MockInteractions.tap(label);
+
+ flush$0();
+
+ assert.isTrue(element.editing);
+ element._inputText = 'new text';
+
+ assert.isFalse(editedStub.called);
+
+ element.async(() => {
+ assert.isFalse(editedStub.called);
+ // Text changes sould be discarded.
+ assert.equal(input.value, 'value text');
+ assert.isFalse(element.editing);
+ done();
+ });
+
+ // Press escape:
+ MockInteractions.keyDownOn(input, 27);
+ });
+
+ test('cancel button', done => {
+ const editedStub = sandbox.stub();
+ element.addEventListener('changed', editedStub);
+ assert.isFalse(element.editing);
+
+ MockInteractions.tap(label);
+
+ flush$0();
+
+ assert.isTrue(element.editing);
+ element._inputText = 'new text';
+
+ assert.isFalse(editedStub.called);
+
+ element.async(() => {
+ assert.isFalse(editedStub.called);
+ // Text changes sould be discarded.
+ assert.equal(input.value, 'value text');
+ assert.isFalse(element.editing);
+ done();
+ });
+
+ // Press escape:
+ MockInteractions.tap(element.$.cancelBtn);
+ });
+
+ suite('gr-editable-label read-only tests', () => {
+ let element;
+ let label;
+
+ setup(() => {
+ element = fixture('read-only');
+ label = element.shadowRoot
+ .querySelector('label');
+ });
+
+ test('disallows edit when read-only', () => {
+ // The dropdown is closed.
+ assert.isFalse(element.$.dropdown.opened);
+ MockInteractions.tap(label);
+
+ flush$0();
+
+ // The dropdown is still closed.
+ assert.isFalse(element.$.dropdown.opened);
+ });
+
+ test('label is not marked as editable', () => {
+ assert.isFalse(label.classList.contains('editable'));
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html
index 305702a..7201ba4 100644
--- a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html
@@ -19,13 +19,8 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-api-interface</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="../gr-js-api-interface/gr-js-api-interface.html">
-
-<script>void(0);</script>
+<script src="../../../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -33,118 +28,119 @@
</template>
</test-fixture>
-<script>
- suite('gr-event-interface tests', async () => {
- await readyToTest();
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../gr-js-api-interface/gr-js-api-interface.js';
+suite('gr-event-interface tests', () => {
+ let sandbox;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('test on Gerrit', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
+ fixture('basic');
+ Gerrit.removeAllListeners();
});
- teardown(() => {
- sandbox.restore();
+ test('communicate between plugin and Gerrit', done => {
+ const eventName = 'test-plugin-event';
+ let p;
+ Gerrit.on(eventName, e => {
+ assert.equal(e.value, 'test');
+ assert.equal(e.plugin, p);
+ done();
+ });
+ Gerrit.install(plugin => {
+ p = plugin;
+ Gerrit.emit(eventName, {value: 'test', plugin});
+ }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
});
- suite('test on Gerrit', () => {
- setup(() => {
- fixture('basic');
- Gerrit.removeAllListeners();
+ test('listen on events from core', done => {
+ const eventName = 'test-plugin-event';
+ Gerrit.on(eventName, e => {
+ assert.equal(e.value, 'test');
+ done();
});
- test('communicate between plugin and Gerrit', done => {
- const eventName = 'test-plugin-event';
- let p;
+ Gerrit.emit(eventName, {value: 'test'});
+ });
+
+ test('communicate across plugins', done => {
+ const eventName = 'test-plugin-event';
+ Gerrit.install(plugin => {
Gerrit.on(eventName, e => {
- assert.equal(e.value, 'test');
- assert.equal(e.plugin, p);
+ assert.equal(e.plugin.getPluginName(), 'testB');
done();
});
- Gerrit.install(plugin => {
- p = plugin;
- Gerrit.emit(eventName, {value: 'test', plugin});
- }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- });
+ }, '0.1',
+ 'http://test.com/plugins/testA/static/testA.js');
- test('listen on events from core', done => {
- const eventName = 'test-plugin-event';
- Gerrit.on(eventName, e => {
- assert.equal(e.value, 'test');
- done();
- });
-
- Gerrit.emit(eventName, {value: 'test'});
- });
-
- test('communicate across plugins', done => {
- const eventName = 'test-plugin-event';
- Gerrit.install(plugin => {
- Gerrit.on(eventName, e => {
- assert.equal(e.plugin.getPluginName(), 'testB');
- done();
- });
- }, '0.1',
- 'http://test.com/plugins/testA/static/testA.js');
-
- Gerrit.install(plugin => {
- Gerrit.emit(eventName, {plugin});
- }, '0.1',
- 'http://test.com/plugins/testB/static/testB.js');
- });
- });
-
- suite('test on interfaces', () => {
- let testObj;
-
- class TestClass extends EventEmitter {
- }
-
- setup(() => {
- testObj = new TestClass();
- });
-
- test('on', () => {
- const cbStub = sinon.stub();
- testObj.on('test', cbStub);
- testObj.emit('test');
- testObj.emit('test');
- assert.isTrue(cbStub.calledTwice);
- });
-
- test('once', () => {
- const cbStub = sinon.stub();
- testObj.once('test', cbStub);
- testObj.emit('test');
- testObj.emit('test');
- assert.isTrue(cbStub.calledOnce);
- });
-
- test('unsubscribe', () => {
- const cbStub = sinon.stub();
- const unsubscribe = testObj.on('test', cbStub);
- testObj.emit('test');
- unsubscribe();
- testObj.emit('test');
- assert.isTrue(cbStub.calledOnce);
- });
-
- test('off', () => {
- const cbStub = sinon.stub();
- testObj.on('test', cbStub);
- testObj.emit('test');
- testObj.off('test', cbStub);
- testObj.emit('test');
- assert.isTrue(cbStub.calledOnce);
- });
-
- test('removeAllListeners', () => {
- const cbStub = sinon.stub();
- testObj.on('test', cbStub);
- testObj.removeAllListeners('test');
- testObj.emit('test');
- assert.isTrue(cbStub.notCalled);
- });
+ Gerrit.install(plugin => {
+ Gerrit.emit(eventName, {plugin});
+ }, '0.1',
+ 'http://test.com/plugins/testB/static/testB.js');
});
});
+
+ suite('test on interfaces', () => {
+ let testObj;
+
+ class TestClass extends EventEmitter {
+ }
+
+ setup(() => {
+ testObj = new TestClass();
+ });
+
+ test('on', () => {
+ const cbStub = sinon.stub();
+ testObj.on('test', cbStub);
+ testObj.emit('test');
+ testObj.emit('test');
+ assert.isTrue(cbStub.calledTwice);
+ });
+
+ test('once', () => {
+ const cbStub = sinon.stub();
+ testObj.once('test', cbStub);
+ testObj.emit('test');
+ testObj.emit('test');
+ assert.isTrue(cbStub.calledOnce);
+ });
+
+ test('unsubscribe', () => {
+ const cbStub = sinon.stub();
+ const unsubscribe = testObj.on('test', cbStub);
+ testObj.emit('test');
+ unsubscribe();
+ testObj.emit('test');
+ assert.isTrue(cbStub.calledOnce);
+ });
+
+ test('off', () => {
+ const cbStub = sinon.stub();
+ testObj.on('test', cbStub);
+ testObj.emit('test');
+ testObj.off('test', cbStub);
+ testObj.emit('test');
+ assert.isTrue(cbStub.calledOnce);
+ });
+
+ test('removeAllListeners', () => {
+ const cbStub = sinon.stub();
+ testObj.on('test', cbStub);
+ testObj.removeAllListeners('test');
+ testObj.emit('test');
+ assert.isTrue(cbStub.notCalled);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html
deleted file mode 100644
index 14285b4..0000000
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html
+++ /dev/null
@@ -1,52 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-fixed-panel">
- <template>
- <style include="shared-styles">
- :host {
- box-sizing: border-box;
- display: block;
- min-height: var(--header-height);
- position: relative;
- }
- header {
- background: inherit;
- border: inherit;
- display: inline;
- height: inherit;
- }
- .floating {
- left: 0;
- position: fixed;
- width: 100%;
- will-change: top;
- }
- .fixedAtTop {
- border-bottom: 1px solid #a4a4a4;
- box-shadow: var(--elevation-level-2);
- }
- </style>
- <header id="header" class$="[[_computeHeaderClass(_headerFloating, _topLast)]]">
- <slot></slot>
- </header>
- </template>
- <script src="gr-fixed-panel.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
index d4995cc..0d19f00 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
@@ -14,225 +14,231 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrFixedPanel extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-fixed-panel'; }
+import '../../../styles/shared-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-fixed-panel_html.js';
- static get properties() {
- return {
- floatingDisabled: {
- type: Boolean,
- value: false,
- },
- readyForMeasure: {
- type: Boolean,
- observer: '_readyForMeasureObserver',
- },
- keepOnScroll: {
- type: Boolean,
- value: false,
- },
- _isMeasured: {
- type: Boolean,
- value: false,
- },
+/** @extends Polymer.Element */
+class GrFixedPanel extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /**
- * Initial offset from the top of the document, in pixels.
- */
- _topInitial: Number,
+ static get is() { return 'gr-fixed-panel'; }
- /**
- * Current offset from the top of the window, in pixels.
- */
- _topLast: Number,
+ static get properties() {
+ return {
+ floatingDisabled: {
+ type: Boolean,
+ value: false,
+ },
+ readyForMeasure: {
+ type: Boolean,
+ observer: '_readyForMeasureObserver',
+ },
+ keepOnScroll: {
+ type: Boolean,
+ value: false,
+ },
+ _isMeasured: {
+ type: Boolean,
+ value: false,
+ },
- _headerHeight: Number,
- _headerFloating: {
- type: Boolean,
- value: false,
- },
- _observer: {
- type: Object,
- value: null,
- },
- /**
- * If place before any other content defines how much
- * of the content below it is covered by this panel
- */
- floatingHeight: {
- type: Number,
- value: 0,
- notify: true,
- },
+ /**
+ * Initial offset from the top of the document, in pixels.
+ */
+ _topInitial: Number,
- _webComponentsReady: Boolean,
- };
+ /**
+ * Current offset from the top of the window, in pixels.
+ */
+ _topLast: Number,
+
+ _headerHeight: Number,
+ _headerFloating: {
+ type: Boolean,
+ value: false,
+ },
+ _observer: {
+ type: Object,
+ value: null,
+ },
+ /**
+ * If place before any other content defines how much
+ * of the content below it is covered by this panel
+ */
+ floatingHeight: {
+ type: Number,
+ value: 0,
+ notify: true,
+ },
+
+ _webComponentsReady: Boolean,
+ };
+ }
+
+ static get observers() {
+ return [
+ '_updateFloatingHeight(floatingDisabled, _isMeasured, _headerHeight)',
+ ];
+ }
+
+ _updateFloatingHeight(floatingDisabled, isMeasured, headerHeight) {
+ if ([
+ floatingDisabled,
+ isMeasured,
+ headerHeight,
+ ].some(arg => arg === undefined)) {
+ return;
}
+ this.floatingHeight =
+ (!floatingDisabled && isMeasured) ? headerHeight : 0;
+ }
- static get observers() {
- return [
- '_updateFloatingHeight(floatingDisabled, _isMeasured, _headerHeight)',
- ];
+ /** @override */
+ attached() {
+ super.attached();
+ if (this.floatingDisabled) {
+ return;
}
-
- _updateFloatingHeight(floatingDisabled, isMeasured, headerHeight) {
- if ([
- floatingDisabled,
- isMeasured,
- headerHeight,
- ].some(arg => arg === undefined)) {
- return;
- }
- this.floatingHeight =
- (!floatingDisabled && isMeasured) ? headerHeight : 0;
+ // Enable content measure unless blocked by param.
+ if (this.readyForMeasure !== false) {
+ this.readyForMeasure = true;
}
+ this.listen(window, 'resize', 'update');
+ this.listen(window, 'scroll', '_updateOnScroll');
+ this._observer = new MutationObserver(this.update.bind(this));
+ this._observer.observe(this.$.header, {childList: true, subtree: true});
+ }
- /** @override */
- attached() {
- super.attached();
- if (this.floatingDisabled) {
- return;
- }
- // Enable content measure unless blocked by param.
- if (this.readyForMeasure !== false) {
- this.readyForMeasure = true;
- }
- this.listen(window, 'resize', 'update');
- this.listen(window, 'scroll', '_updateOnScroll');
- this._observer = new MutationObserver(this.update.bind(this));
- this._observer.observe(this.$.header, {childList: true, subtree: true});
- }
-
- /** @override */
- detached() {
- super.detached();
- this.unlisten(window, 'scroll', '_updateOnScroll');
- this.unlisten(window, 'resize', 'update');
- if (this._observer) {
- this._observer.disconnect();
- }
- }
-
- _readyForMeasureObserver(readyForMeasure) {
- if (readyForMeasure) {
- this.update();
- }
- }
-
- _computeHeaderClass(headerFloating, topLast) {
- const fixedAtTop = this.keepOnScroll && topLast === 0;
- return [
- headerFloating ? 'floating' : '',
- fixedAtTop ? 'fixedAtTop' : '',
- ].join(' ');
- }
-
- unfloat() {
- if (this.floatingDisabled) {
- return;
- }
- this.$.header.style.top = '';
- this._headerFloating = false;
- this.updateStyles({'--header-height': ''});
- }
-
- update() {
- this.debounce('update', () => {
- this._updateDebounced();
- }, 100);
- }
-
- _updateOnScroll() {
- this.debounce('update', () => {
- this._updateDebounced();
- });
- }
-
- _updateDebounced() {
- if (this.floatingDisabled) {
- return;
- }
- this._isMeasured = false;
- this._maybeFloatHeader();
- this._reposition();
- }
-
- _getElementTop() {
- return this.getBoundingClientRect().top;
- }
-
- _reposition() {
- if (!this._headerFloating) {
- return;
- }
- const header = this.$.header;
- // Since the outer element is relative positioned, can use its top
- // to determine how to position the inner header element.
- const elemTop = this._getElementTop();
- let newTop;
- if (this.keepOnScroll && elemTop < 0) {
- // Should stick to the top.
- newTop = 0;
- } else {
- // Keep in line with the outer element.
- newTop = elemTop;
- }
- // Initialize top style if it doesn't exist yet.
- if (!header.style.top && this._topLast === newTop) {
- header.style.top = newTop;
- }
- if (this._topLast !== newTop) {
- if (newTop === undefined) {
- header.style.top = '';
- } else {
- header.style.top = newTop + 'px';
- }
- this._topLast = newTop;
- }
- }
-
- _measure() {
- if (this._isMeasured) {
- return; // Already measured.
- }
- const rect = this.$.header.getBoundingClientRect();
- if (rect.height === 0 && rect.width === 0) {
- return; // Not ready for measurement yet.
- }
- const top = document.body.scrollTop + rect.top;
- this._topLast = top;
- this._headerHeight = rect.height;
- this._topInitial =
- this.getBoundingClientRect().top + document.body.scrollTop;
- this._isMeasured = true;
- }
-
- _isFloatingNeeded() {
- return this.keepOnScroll ||
- document.body.scrollWidth > document.body.clientWidth;
- }
-
- _maybeFloatHeader() {
- if (!this._isFloatingNeeded()) {
- return;
- }
- this._measure();
- if (this._isMeasured) {
- this._floatHeader();
- }
- }
-
- _floatHeader() {
- this.updateStyles({'--header-height': this._headerHeight + 'px'});
- this._headerFloating = true;
+ /** @override */
+ detached() {
+ super.detached();
+ this.unlisten(window, 'scroll', '_updateOnScroll');
+ this.unlisten(window, 'resize', 'update');
+ if (this._observer) {
+ this._observer.disconnect();
}
}
- customElements.define(GrFixedPanel.is, GrFixedPanel);
-})();
+ _readyForMeasureObserver(readyForMeasure) {
+ if (readyForMeasure) {
+ this.update();
+ }
+ }
+
+ _computeHeaderClass(headerFloating, topLast) {
+ const fixedAtTop = this.keepOnScroll && topLast === 0;
+ return [
+ headerFloating ? 'floating' : '',
+ fixedAtTop ? 'fixedAtTop' : '',
+ ].join(' ');
+ }
+
+ unfloat() {
+ if (this.floatingDisabled) {
+ return;
+ }
+ this.$.header.style.top = '';
+ this._headerFloating = false;
+ this.updateStyles({'--header-height': ''});
+ }
+
+ update() {
+ this.debounce('update', () => {
+ this._updateDebounced();
+ }, 100);
+ }
+
+ _updateOnScroll() {
+ this.debounce('update', () => {
+ this._updateDebounced();
+ });
+ }
+
+ _updateDebounced() {
+ if (this.floatingDisabled) {
+ return;
+ }
+ this._isMeasured = false;
+ this._maybeFloatHeader();
+ this._reposition();
+ }
+
+ _getElementTop() {
+ return this.getBoundingClientRect().top;
+ }
+
+ _reposition() {
+ if (!this._headerFloating) {
+ return;
+ }
+ const header = this.$.header;
+ // Since the outer element is relative positioned, can use its top
+ // to determine how to position the inner header element.
+ const elemTop = this._getElementTop();
+ let newTop;
+ if (this.keepOnScroll && elemTop < 0) {
+ // Should stick to the top.
+ newTop = 0;
+ } else {
+ // Keep in line with the outer element.
+ newTop = elemTop;
+ }
+ // Initialize top style if it doesn't exist yet.
+ if (!header.style.top && this._topLast === newTop) {
+ header.style.top = newTop;
+ }
+ if (this._topLast !== newTop) {
+ if (newTop === undefined) {
+ header.style.top = '';
+ } else {
+ header.style.top = newTop + 'px';
+ }
+ this._topLast = newTop;
+ }
+ }
+
+ _measure() {
+ if (this._isMeasured) {
+ return; // Already measured.
+ }
+ const rect = this.$.header.getBoundingClientRect();
+ if (rect.height === 0 && rect.width === 0) {
+ return; // Not ready for measurement yet.
+ }
+ const top = document.body.scrollTop + rect.top;
+ this._topLast = top;
+ this._headerHeight = rect.height;
+ this._topInitial =
+ this.getBoundingClientRect().top + document.body.scrollTop;
+ this._isMeasured = true;
+ }
+
+ _isFloatingNeeded() {
+ return this.keepOnScroll ||
+ document.body.scrollWidth > document.body.clientWidth;
+ }
+
+ _maybeFloatHeader() {
+ if (!this._isFloatingNeeded()) {
+ return;
+ }
+ this._measure();
+ if (this._isMeasured) {
+ this._floatHeader();
+ }
+ }
+
+ _floatHeader() {
+ this.updateStyles({'--header-height': this._headerHeight + 'px'});
+ this._headerFloating = true;
+ }
+}
+
+customElements.define(GrFixedPanel.is, GrFixedPanel);
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_html.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_html.js
new file mode 100644
index 0000000..69ae735
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_html.js
@@ -0,0 +1,47 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ box-sizing: border-box;
+ display: block;
+ min-height: var(--header-height);
+ position: relative;
+ }
+ header {
+ background: inherit;
+ border: inherit;
+ display: inline;
+ height: inherit;
+ }
+ .floating {
+ left: 0;
+ position: fixed;
+ width: 100%;
+ will-change: top;
+ }
+ .fixedAtTop {
+ border-bottom: 1px solid #a4a4a4;
+ box-shadow: var(--elevation-level-2);
+ }
+ </style>
+ <header id="header" class\$="[[_computeHeaderClass(_headerFloating, _topLast)]]">
+ <slot></slot>
+ </header>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
index 7ae0265..cc2fb70 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
@@ -19,13 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-fixed-panel</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-fixed-panel.html">
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<style>
/* Prevent horizontal scrolling on page.
New version of web-component-tester creates body with margins */
@@ -35,8 +32,6 @@
}
</style>
-<script>void(0);</script>
-
<test-fixture id="basic">
<template>
<gr-fixed-panel>
@@ -45,83 +40,84 @@
</template>
</test-fixture>
-<script>
- suite('gr-fixed-panel', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-fixed-panel.js';
+suite('gr-fixed-panel', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.readyForMeasure = true;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('can be disabled with floatingDisabled', () => {
+ element.floatingDisabled = true;
+ sandbox.stub(element, '_reposition');
+ window.dispatchEvent(new CustomEvent('resize'));
+ element.flushDebouncer('update');
+ assert.isFalse(element._reposition.called);
+ });
+
+ test('header is the height of the content', () => {
+ assert.equal(element.getBoundingClientRect().height, 100);
+ });
+
+ test('scroll triggers _reposition', () => {
+ sandbox.stub(element, '_reposition');
+ window.dispatchEvent(new CustomEvent('scroll'));
+ element.flushDebouncer('update');
+ assert.isTrue(element._reposition.called);
+ });
+
+ suite('_reposition', () => {
+ const getHeaderTop = function() {
+ return element.$.header.style.top;
+ };
+
+ const emulateScrollY = function(distance) {
+ element._getElementTop.returns(element._headerTopInitial - distance);
+ element._updateDebounced();
+ element.flushDebouncer('scroll');
+ };
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.readyForMeasure = true;
+ element._headerTopInitial = 10;
+ sandbox.stub(element, '_getElementTop')
+ .returns(element._headerTopInitial);
});
- teardown(() => {
- sandbox.restore();
+ test('scrolls header along with document', () => {
+ emulateScrollY(20);
+ // No top property is set when !_headerFloating.
+ assert.equal(getHeaderTop(), '');
});
- test('can be disabled with floatingDisabled', () => {
- element.floatingDisabled = true;
- sandbox.stub(element, '_reposition');
- window.dispatchEvent(new CustomEvent('resize'));
- element.flushDebouncer('update');
- assert.isFalse(element._reposition.called);
+ test('does not stick to the top by default', () => {
+ emulateScrollY(150);
+ // No top property is set when !_headerFloating.
+ assert.equal(getHeaderTop(), '');
});
- test('header is the height of the content', () => {
- assert.equal(element.getBoundingClientRect().height, 100);
+ test('sticks to the top if enabled', () => {
+ element.keepOnScroll = true;
+ emulateScrollY(120);
+ assert.equal(getHeaderTop(), '0px');
});
- test('scroll triggers _reposition', () => {
- sandbox.stub(element, '_reposition');
- window.dispatchEvent(new CustomEvent('scroll'));
- element.flushDebouncer('update');
- assert.isTrue(element._reposition.called);
- });
-
- suite('_reposition', () => {
- const getHeaderTop = function() {
- return element.$.header.style.top;
- };
-
- const emulateScrollY = function(distance) {
- element._getElementTop.returns(element._headerTopInitial - distance);
- element._updateDebounced();
- element.flushDebouncer('scroll');
- };
-
- setup(() => {
- element._headerTopInitial = 10;
- sandbox.stub(element, '_getElementTop')
- .returns(element._headerTopInitial);
- });
-
- test('scrolls header along with document', () => {
- emulateScrollY(20);
- // No top property is set when !_headerFloating.
- assert.equal(getHeaderTop(), '');
- });
-
- test('does not stick to the top by default', () => {
- emulateScrollY(150);
- // No top property is set when !_headerFloating.
- assert.equal(getHeaderTop(), '');
- });
-
- test('sticks to the top if enabled', () => {
- element.keepOnScroll = true;
- emulateScrollY(120);
- assert.equal(getHeaderTop(), '0px');
- });
-
- test('drops a shadow when fixed to the top', () => {
- element.keepOnScroll = true;
- emulateScrollY(5);
- assert.isFalse(element.$.header.classList.contains('fixedAtTop'));
- emulateScrollY(120);
- assert.isTrue(element.$.header.classList.contains('fixedAtTop'));
- });
+ test('drops a shadow when fixed to the top', () => {
+ element.keepOnScroll = true;
+ emulateScrollY(5);
+ assert.isFalse(element.$.header.classList.contains('fixedAtTop'));
+ emulateScrollY(120);
+ assert.isTrue(element.$.header.classList.contains('fixedAtTop'));
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html
deleted file mode 100644
index 0c254ee..0000000
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html
+++ /dev/null
@@ -1,71 +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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../gr-linked-text/gr-linked-text.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-formatted-text">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- font-family: var(--font-family);
- }
- p,
- ul,
- code,
- blockquote,
- gr-linked-text.pre {
- margin: 0 0 var(--spacing-m) 0;
- }
- p,
- ul,
- code,
- blockquote {
- max-width: var(--gr-formatted-text-prose-max-width, none);
- }
- :host(.noTrailingMargin) p:last-child,
- :host(.noTrailingMargin) ul:last-child,
- :host(.noTrailingMargin) blockquote:last-child,
- :host(.noTrailingMargin) gr-linked-text.pre:last-child {
- margin: 0;
- }
- code,
- blockquote {
- border-left: 1px solid #aaa;
- padding: 0 var(--spacing-m);
- }
- code {
- display: block;
- white-space: pre;
- color: var(--deemphasized-text-color);
- }
- li {
- list-style-type: disc;
- margin-left: var(--spacing-xl);
- }
- gr-linked-text.pre {
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-code);
- line-height: var(--line-height-code);
- }
-
- </style>
- <div id="container"></div>
- </template>
- <script src="gr-formatted-text.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
index e62fcc8..139e09c 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
@@ -14,288 +14,296 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- // eslint-disable-next-line no-unused-vars
- const QUOTE_MARKER_PATTERN = /\n\s?>\s/g;
- const CODE_MARKER_PATTERN = /^(`{1,3})([^`]+?)\1$/;
+import '../gr-linked-text/gr-linked-text.js';
+import '../../../styles/shared-styles.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-formatted-text_html.js';
- /** @extends Polymer.Element */
- class GrFormattedText extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-formatted-text'; }
+// eslint-disable-next-line no-unused-vars
+const QUOTE_MARKER_PATTERN = /\n\s?>\s/g;
+const CODE_MARKER_PATTERN = /^(`{1,3})([^`]+?)\1$/;
- static get properties() {
- return {
- content: {
- type: String,
- observer: '_contentChanged',
- },
- config: Object,
- noTrailingMargin: {
- type: Boolean,
- value: false,
- },
- };
- }
+/** @extends Polymer.Element */
+class GrFormattedText extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- static get observers() {
- return [
- '_contentOrConfigChanged(content, config)',
- ];
- }
+ static get is() { return 'gr-formatted-text'; }
- /** @override */
- ready() {
- super.ready();
- if (this.noTrailingMargin) {
- this.classList.add('noTrailingMargin');
- }
- }
+ static get properties() {
+ return {
+ content: {
+ type: String,
+ observer: '_contentChanged',
+ },
+ config: Object,
+ noTrailingMargin: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
- _contentChanged(content) {
- // In the case where the config may not be set (perhaps due to the
- // request for it still being in flight), set the content anyway to
- // prevent waiting on the config to display the text.
- if (this.config) { return; }
- this._contentOrConfigChanged(content);
- }
+ static get observers() {
+ return [
+ '_contentOrConfigChanged(content, config)',
+ ];
+ }
- /**
- * Given a source string, update the DOM inside #container.
- */
- _contentOrConfigChanged(content) {
- const container = Polymer.dom(this.$.container);
-
- // Remove existing content.
- while (container.firstChild) {
- container.removeChild(container.firstChild);
- }
-
- // Add new content.
- for (const node of this._computeNodes(this._computeBlocks(content))) {
- container.appendChild(node);
- }
- }
-
- /**
- * Given a source string, parse into an array of block objects. Each block
- * has a `type` property which takes any of the follwoing values.
- * * 'paragraph'
- * * 'quote' (Block quote.)
- * * 'pre' (Pre-formatted text.)
- * * 'list' (Unordered list.)
- * * 'code' (code blocks.)
- *
- * For blocks of type 'paragraph', 'pre' and 'code' there is a `text`
- * property that maps to a string of the block's content.
- *
- * For blocks of type 'list', there is an `items` property that maps to a
- * list of strings representing the list items.
- *
- * For blocks of type 'quote', there is a `blocks` property that maps to a
- * list of blocks contained in the quote.
- *
- * NOTE: Strings appearing in all block objects are NOT escaped.
- *
- * @param {string} content
- * @return {!Array<!Object>}
- */
- _computeBlocks(content) {
- if (!content) { return []; }
-
- const result = [];
- const lines = content.replace(/[\s\n\r\t]+$/g, '').split('\n');
-
- for (let i = 0; i < lines.length; i++) {
- if (!lines[i].length) {
- continue;
- }
-
- if (this._isCodeMarkLine(lines[i])) {
- // handle multi-line code
- let nextI = i+1;
- while (!this._isCodeMarkLine(lines[nextI]) && nextI < lines.length) {
- nextI++;
- }
-
- if (this._isCodeMarkLine(lines[nextI])) {
- result.push({
- type: 'code',
- text: lines.slice(i+1, nextI).join('\n'),
- });
- i = nextI;
- continue;
- }
-
- // otherwise treat it as regular line and continue
- // check for other cases
- }
-
- if (this._isSingleLineCode(lines[i])) {
- // no guard check as _isSingleLineCode tested on the pattern
- const codeContent = lines[i].match(CODE_MARKER_PATTERN)[2];
- result.push({type: 'code', text: codeContent});
- } else if (this._isList(lines[i])) {
- let nextI = i + 1;
- while (this._isList(lines[nextI])) {
- nextI++;
- }
- result.push(this._makeList(lines.slice(i, nextI)));
- i = nextI - 1;
- } else if (this._isQuote(lines[i])) {
- let nextI = i + 1;
- while (this._isQuote(lines[nextI])) {
- nextI++;
- }
- const blockLines = lines.slice(i, nextI)
- .map(l => l.replace(/^[ ]?>[ ]?/, ''));
- result.push({
- type: 'quote',
- blocks: this._computeBlocks(blockLines.join('\n')),
- });
- i = nextI - 1;
- } else if (this._isPreFormat(lines[i])) {
- let nextI = i + 1;
- // include pre or all regular lines but stop at next new line
- while (this._isPreFormat(lines[nextI])
- || (this._isRegularLine(lines[nextI]) && lines[nextI].length)) {
- nextI++;
- }
- result.push({
- type: 'pre',
- text: lines.slice(i, nextI).join('\n'),
- });
- i = nextI - 1;
- } else {
- let nextI = i + 1;
- while (this._isRegularLine(lines[nextI])) {
- nextI++;
- }
- result.push({
- type: 'paragraph',
- text: lines.slice(i, nextI).join('\n'),
- });
- i = nextI - 1;
- }
- }
-
- return result;
- }
-
- /**
- * Take a block of comment text that contains a list, generate appropriate
- * block objects and append them to the output list.
- *
- * * Item one.
- * * Item two.
- * * item three.
- *
- * TODO(taoalpha): maybe we should also support nested list
- *
- * @param {!Array<string>} lines The block containing the list.
- */
- _makeList(lines) {
- const block = {type: 'list', items: []};
- let line;
-
- for (let i = 0; i < lines.length; i++) {
- line = lines[i];
- line = line.substring(1).trim();
- block.items.push(line);
- }
- return block;
- }
-
- _isRegularLine(line) {
- // line can not be recognized by existing patterns
- if (line === undefined) return false;
- return !this._isQuote(line) && !this._isCodeMarkLine(line)
- && !this._isSingleLineCode(line) && !this._isList(line) &&
- !this._isPreFormat(line);
- }
-
- _isQuote(line) {
- return line && (line.startsWith('> ') || line.startsWith(' > '));
- }
-
- _isCodeMarkLine(line) {
- return line && line.trim() === '```';
- }
-
- _isSingleLineCode(line) {
- return line && CODE_MARKER_PATTERN.test(line);
- }
-
- _isPreFormat(line) {
- return line && /^[ \t]/.test(line);
- }
-
- _isList(line) {
- return line && /^[-*] /.test(line);
- }
-
- /**
- * @param {string} content
- * @param {boolean=} opt_isPre
- */
- _makeLinkedText(content, opt_isPre) {
- const text = document.createElement('gr-linked-text');
- text.config = this.config;
- text.content = content;
- text.pre = true;
- if (opt_isPre) {
- text.classList.add('pre');
- }
- return text;
- }
-
- /**
- * Map an array of block objects to an array of DOM nodes.
- *
- * @param {!Array<!Object>} blocks
- * @return {!Array<!HTMLElement>}
- */
- _computeNodes(blocks) {
- return blocks.map(block => {
- if (block.type === 'paragraph') {
- const p = document.createElement('p');
- p.appendChild(this._makeLinkedText(block.text));
- return p;
- }
-
- if (block.type === 'quote') {
- const bq = document.createElement('blockquote');
- for (const node of this._computeNodes(block.blocks)) {
- bq.appendChild(node);
- }
- return bq;
- }
-
- if (block.type === 'code') {
- const code = document.createElement('code');
- code.textContent = block.text;
- return code;
- }
-
- if (block.type === 'pre') {
- return this._makeLinkedText(block.text, true);
- }
-
- if (block.type === 'list') {
- const ul = document.createElement('ul');
- for (const item of block.items) {
- const li = document.createElement('li');
- li.appendChild(this._makeLinkedText(item));
- ul.appendChild(li);
- }
- return ul;
- }
- });
+ /** @override */
+ ready() {
+ super.ready();
+ if (this.noTrailingMargin) {
+ this.classList.add('noTrailingMargin');
}
}
- customElements.define(GrFormattedText.is, GrFormattedText);
-})();
+ _contentChanged(content) {
+ // In the case where the config may not be set (perhaps due to the
+ // request for it still being in flight), set the content anyway to
+ // prevent waiting on the config to display the text.
+ if (this.config) { return; }
+ this._contentOrConfigChanged(content);
+ }
+
+ /**
+ * Given a source string, update the DOM inside #container.
+ */
+ _contentOrConfigChanged(content) {
+ const container = dom(this.$.container);
+
+ // Remove existing content.
+ while (container.firstChild) {
+ container.removeChild(container.firstChild);
+ }
+
+ // Add new content.
+ for (const node of this._computeNodes(this._computeBlocks(content))) {
+ container.appendChild(node);
+ }
+ }
+
+ /**
+ * Given a source string, parse into an array of block objects. Each block
+ * has a `type` property which takes any of the follwoing values.
+ * * 'paragraph'
+ * * 'quote' (Block quote.)
+ * * 'pre' (Pre-formatted text.)
+ * * 'list' (Unordered list.)
+ * * 'code' (code blocks.)
+ *
+ * For blocks of type 'paragraph', 'pre' and 'code' there is a `text`
+ * property that maps to a string of the block's content.
+ *
+ * For blocks of type 'list', there is an `items` property that maps to a
+ * list of strings representing the list items.
+ *
+ * For blocks of type 'quote', there is a `blocks` property that maps to a
+ * list of blocks contained in the quote.
+ *
+ * NOTE: Strings appearing in all block objects are NOT escaped.
+ *
+ * @param {string} content
+ * @return {!Array<!Object>}
+ */
+ _computeBlocks(content) {
+ if (!content) { return []; }
+
+ const result = [];
+ const lines = content.replace(/[\s\n\r\t]+$/g, '').split('\n');
+
+ for (let i = 0; i < lines.length; i++) {
+ if (!lines[i].length) {
+ continue;
+ }
+
+ if (this._isCodeMarkLine(lines[i])) {
+ // handle multi-line code
+ let nextI = i+1;
+ while (!this._isCodeMarkLine(lines[nextI]) && nextI < lines.length) {
+ nextI++;
+ }
+
+ if (this._isCodeMarkLine(lines[nextI])) {
+ result.push({
+ type: 'code',
+ text: lines.slice(i+1, nextI).join('\n'),
+ });
+ i = nextI;
+ continue;
+ }
+
+ // otherwise treat it as regular line and continue
+ // check for other cases
+ }
+
+ if (this._isSingleLineCode(lines[i])) {
+ // no guard check as _isSingleLineCode tested on the pattern
+ const codeContent = lines[i].match(CODE_MARKER_PATTERN)[2];
+ result.push({type: 'code', text: codeContent});
+ } else if (this._isList(lines[i])) {
+ let nextI = i + 1;
+ while (this._isList(lines[nextI])) {
+ nextI++;
+ }
+ result.push(this._makeList(lines.slice(i, nextI)));
+ i = nextI - 1;
+ } else if (this._isQuote(lines[i])) {
+ let nextI = i + 1;
+ while (this._isQuote(lines[nextI])) {
+ nextI++;
+ }
+ const blockLines = lines.slice(i, nextI)
+ .map(l => l.replace(/^[ ]?>[ ]?/, ''));
+ result.push({
+ type: 'quote',
+ blocks: this._computeBlocks(blockLines.join('\n')),
+ });
+ i = nextI - 1;
+ } else if (this._isPreFormat(lines[i])) {
+ let nextI = i + 1;
+ // include pre or all regular lines but stop at next new line
+ while (this._isPreFormat(lines[nextI])
+ || (this._isRegularLine(lines[nextI]) && lines[nextI].length)) {
+ nextI++;
+ }
+ result.push({
+ type: 'pre',
+ text: lines.slice(i, nextI).join('\n'),
+ });
+ i = nextI - 1;
+ } else {
+ let nextI = i + 1;
+ while (this._isRegularLine(lines[nextI])) {
+ nextI++;
+ }
+ result.push({
+ type: 'paragraph',
+ text: lines.slice(i, nextI).join('\n'),
+ });
+ i = nextI - 1;
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Take a block of comment text that contains a list, generate appropriate
+ * block objects and append them to the output list.
+ *
+ * * Item one.
+ * * Item two.
+ * * item three.
+ *
+ * TODO(taoalpha): maybe we should also support nested list
+ *
+ * @param {!Array<string>} lines The block containing the list.
+ */
+ _makeList(lines) {
+ const block = {type: 'list', items: []};
+ let line;
+
+ for (let i = 0; i < lines.length; i++) {
+ line = lines[i];
+ line = line.substring(1).trim();
+ block.items.push(line);
+ }
+ return block;
+ }
+
+ _isRegularLine(line) {
+ // line can not be recognized by existing patterns
+ if (line === undefined) return false;
+ return !this._isQuote(line) && !this._isCodeMarkLine(line)
+ && !this._isSingleLineCode(line) && !this._isList(line) &&
+ !this._isPreFormat(line);
+ }
+
+ _isQuote(line) {
+ return line && (line.startsWith('> ') || line.startsWith(' > '));
+ }
+
+ _isCodeMarkLine(line) {
+ return line && line.trim() === '```';
+ }
+
+ _isSingleLineCode(line) {
+ return line && CODE_MARKER_PATTERN.test(line);
+ }
+
+ _isPreFormat(line) {
+ return line && /^[ \t]/.test(line);
+ }
+
+ _isList(line) {
+ return line && /^[-*] /.test(line);
+ }
+
+ /**
+ * @param {string} content
+ * @param {boolean=} opt_isPre
+ */
+ _makeLinkedText(content, opt_isPre) {
+ const text = document.createElement('gr-linked-text');
+ text.config = this.config;
+ text.content = content;
+ text.pre = true;
+ if (opt_isPre) {
+ text.classList.add('pre');
+ }
+ return text;
+ }
+
+ /**
+ * Map an array of block objects to an array of DOM nodes.
+ *
+ * @param {!Array<!Object>} blocks
+ * @return {!Array<!HTMLElement>}
+ */
+ _computeNodes(blocks) {
+ return blocks.map(block => {
+ if (block.type === 'paragraph') {
+ const p = document.createElement('p');
+ p.appendChild(this._makeLinkedText(block.text));
+ return p;
+ }
+
+ if (block.type === 'quote') {
+ const bq = document.createElement('blockquote');
+ for (const node of this._computeNodes(block.blocks)) {
+ bq.appendChild(node);
+ }
+ return bq;
+ }
+
+ if (block.type === 'code') {
+ const code = document.createElement('code');
+ code.textContent = block.text;
+ return code;
+ }
+
+ if (block.type === 'pre') {
+ return this._makeLinkedText(block.text, true);
+ }
+
+ if (block.type === 'list') {
+ const ul = document.createElement('ul');
+ for (const item of block.items) {
+ const li = document.createElement('li');
+ li.appendChild(this._makeLinkedText(item));
+ ul.appendChild(li);
+ }
+ return ul;
+ }
+ });
+ }
+}
+
+customElements.define(GrFormattedText.is, GrFormattedText);
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js
new file mode 100644
index 0000000..2b50565
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js
@@ -0,0 +1,66 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ font-family: var(--font-family);
+ }
+ p,
+ ul,
+ code,
+ blockquote,
+ gr-linked-text.pre {
+ margin: 0 0 var(--spacing-m) 0;
+ }
+ p,
+ ul,
+ code,
+ blockquote {
+ max-width: var(--gr-formatted-text-prose-max-width, none);
+ }
+ :host(.noTrailingMargin) p:last-child,
+ :host(.noTrailingMargin) ul:last-child,
+ :host(.noTrailingMargin) blockquote:last-child,
+ :host(.noTrailingMargin) gr-linked-text.pre:last-child {
+ margin: 0;
+ }
+ code,
+ blockquote {
+ border-left: 1px solid #aaa;
+ padding: 0 var(--spacing-m);
+ }
+ code {
+ display: block;
+ white-space: pre-wrap;
+ color: var(--deemphasized-text-color);
+ }
+ li {
+ list-style-type: disc;
+ margin-left: var(--spacing-xl);
+ }
+ gr-linked-text.pre {
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-code);
+ line-height: var(--line-height-code);
+ }
+
+ </style>
+ <div id="container"></div>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
index a6d8524..c48ec8a 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-editable-label</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-formatted-text.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,395 +30,396 @@
</template>
</test-fixture>
-<script>
- suite('gr-formatted-text tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-formatted-text.js';
+suite('gr-formatted-text tests', () => {
+ let element;
+ let sandbox;
- function assertBlock(result, index, type, text) {
- assert.equal(result[index].type, type);
- assert.equal(result[index].text, text);
- }
+ function assertBlock(result, index, type, text) {
+ assert.equal(result[index].type, type);
+ assert.equal(result[index].text, text);
+ }
- function assertListBlock(result, resultIndex, itemIndex, text) {
- assert.equal(result[resultIndex].type, 'list');
- assert.equal(result[resultIndex].items[itemIndex], text);
- }
+ function assertListBlock(result, resultIndex, itemIndex, text) {
+ assert.equal(result[resultIndex].type, 'list');
+ assert.equal(result[resultIndex].items[itemIndex], text);
+ }
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('parse null undefined and empty', () => {
- assert.lengthOf(element._computeBlocks(null), 0);
- assert.lengthOf(element._computeBlocks(undefined), 0);
- assert.lengthOf(element._computeBlocks(''), 0);
- });
-
- test('parse simple', () => {
- const comment = 'Para1';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertBlock(result, 0, 'paragraph', comment);
- });
-
- test('parse multiline para', () => {
- const comment = 'Para 1\nStill para 1';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertBlock(result, 0, 'paragraph', comment);
- });
-
- test('parse para break without special blocks', () => {
- const comment = 'Para 1\n\nPara 2\n\nPara 3';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertBlock(result, 0, 'paragraph', comment);
- });
-
- test('parse quote', () => {
- const comment = '> Quote text';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assert.equal(result[0].type, 'quote');
- assert.lengthOf(result[0].blocks, 1);
- assertBlock(result[0].blocks, 0, 'paragraph', 'Quote text');
- });
-
- test('parse quote lead space', () => {
- const comment = ' > Quote text';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assert.equal(result[0].type, 'quote');
- assert.lengthOf(result[0].blocks, 1);
- assertBlock(result[0].blocks, 0, 'paragraph', 'Quote text');
- });
-
- test('parse multiline quote', () => {
- const comment = '> Quote line 1\n> Quote line 2\n > Quote line 3\n';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assert.equal(result[0].type, 'quote');
- assert.lengthOf(result[0].blocks, 1);
- assertBlock(result[0].blocks, 0, 'paragraph',
- 'Quote line 1\nQuote line 2\nQuote line 3');
- });
-
- test('parse pre', () => {
- const comment = ' Four space indent.';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertBlock(result, 0, 'pre', comment);
- });
-
- test('parse one space pre', () => {
- const comment = ' One space indent.\n Another line.';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertBlock(result, 0, 'pre', comment);
- });
-
- test('parse tab pre', () => {
- const comment = '\tOne tab indent.\n\tAnother line.\n Yet another!';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertBlock(result, 0, 'pre', comment);
- });
-
- test('parse star list', () => {
- const comment = '* Item 1\n* Item 2\n* Item 3';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertListBlock(result, 0, 0, 'Item 1');
- assertListBlock(result, 0, 1, 'Item 2');
- assertListBlock(result, 0, 2, 'Item 3');
- });
-
- test('parse dash list', () => {
- const comment = '- Item 1\n- Item 2\n- Item 3';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertListBlock(result, 0, 0, 'Item 1');
- assertListBlock(result, 0, 1, 'Item 2');
- assertListBlock(result, 0, 2, 'Item 3');
- });
-
- test('parse mixed list', () => {
- const comment = '- Item 1\n* Item 2\n- Item 3\n* Item 4';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertListBlock(result, 0, 0, 'Item 1');
- assertListBlock(result, 0, 1, 'Item 2');
- assertListBlock(result, 0, 2, 'Item 3');
- assertListBlock(result, 0, 3, 'Item 4');
- });
-
- test('parse mixed block types', () => {
- const comment = 'Paragraph\nacross\na\nfew\nlines.' +
- '\n\n' +
- '> Quote\n> across\n> not many lines.' +
- '\n\n' +
- 'Another paragraph' +
- '\n\n' +
- '* Series\n* of\n* list\n* items' +
- '\n\n' +
- 'Yet another paragraph' +
- '\n\n' +
- '\tPreformatted text.' +
- '\n\n' +
- 'Parting words.';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 7);
- assertBlock(result, 0, 'paragraph', 'Paragraph\nacross\na\nfew\nlines.\n');
-
- assert.equal(result[1].type, 'quote');
- assert.lengthOf(result[1].blocks, 1);
- assertBlock(result[1].blocks, 0, 'paragraph',
- 'Quote\nacross\nnot many lines.');
-
- assertBlock(result, 2, 'paragraph', 'Another paragraph\n');
- assertListBlock(result, 3, 0, 'Series');
- assertListBlock(result, 3, 1, 'of');
- assertListBlock(result, 3, 2, 'list');
- assertListBlock(result, 3, 3, 'items');
- assertBlock(result, 4, 'paragraph', 'Yet another paragraph\n');
- assertBlock(result, 5, 'pre', '\tPreformatted text.');
- assertBlock(result, 6, 'paragraph', 'Parting words.');
- });
-
- test('bullet list 1', () => {
- const comment = 'A\n\n* line 1\n* 2nd line';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertBlock(result, 0, 'paragraph', 'A\n');
- assertListBlock(result, 1, 0, 'line 1');
- assertListBlock(result, 1, 1, '2nd line');
- });
-
- test('bullet list 2', () => {
- const comment = 'A\n* line 1\n* 2nd line\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertBlock(result, 0, 'paragraph', 'A');
- assertListBlock(result, 1, 0, 'line 1');
- assertListBlock(result, 1, 1, '2nd line');
- assertBlock(result, 2, 'paragraph', 'B');
- });
-
- test('bullet list 3', () => {
- const comment = '* line 1\n* 2nd line\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertListBlock(result, 0, 0, 'line 1');
- assertListBlock(result, 0, 1, '2nd line');
- assertBlock(result, 1, 'paragraph', 'B');
- });
-
- test('bullet list 4', () => {
- const comment = 'To see this bug, you have to:\n' +
- '* Be on IMAP or EAS (not on POP)\n' +
- '* Be very unlucky\n';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertBlock(result, 0, 'paragraph', 'To see this bug, you have to:');
- assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)');
- assertListBlock(result, 1, 1, 'Be very unlucky');
- });
-
- test('bullet list 5', () => {
- const comment = 'To see this bug,\n' +
- 'you have to:\n' +
- '* Be on IMAP or EAS (not on POP)\n' +
- '* Be very unlucky\n';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertBlock(result, 0, 'paragraph', 'To see this bug,\nyou have to:');
- assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)');
- assertListBlock(result, 1, 1, 'Be very unlucky');
- });
-
- test('dash list 1', () => {
- const comment = 'A\n- line 1\n- 2nd line';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertBlock(result, 0, 'paragraph', 'A');
- assertListBlock(result, 1, 0, 'line 1');
- assertListBlock(result, 1, 1, '2nd line');
- });
-
- test('dash list 2', () => {
- const comment = 'A\n- line 1\n- 2nd line\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertBlock(result, 0, 'paragraph', 'A');
- assertListBlock(result, 1, 0, 'line 1');
- assertListBlock(result, 1, 1, '2nd line');
- assertBlock(result, 2, 'paragraph', 'B');
- });
-
- test('dash list 3', () => {
- const comment = '- line 1\n- 2nd line\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertListBlock(result, 0, 0, 'line 1');
- assertListBlock(result, 0, 1, '2nd line');
- assertBlock(result, 1, 'paragraph', 'B');
- });
-
- test('nested list will NOT be recognized', () => {
- // will be rendered as two separate lists
- const comment = '- line 1\n - line with indentation\n- line 2';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertListBlock(result, 0, 0, 'line 1');
- assert.equal(result[1].type, 'pre');
- assertListBlock(result, 2, 0, 'line 2');
- });
-
- test('pre format 1', () => {
- const comment = 'A\n This is pre\n formatted';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertBlock(result, 0, 'paragraph', 'A');
- assertBlock(result, 1, 'pre', ' This is pre\n formatted');
- });
-
- test('pre format 2', () => {
- const comment = 'A\n This is pre\n formatted\n\nbut this is not';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertBlock(result, 0, 'paragraph', 'A');
- assertBlock(result, 1, 'pre', ' This is pre\n formatted');
- assertBlock(result, 2, 'paragraph', 'but this is not');
- });
-
- test('pre format 3', () => {
- const comment = 'A\n Q\n <R>\n S\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertBlock(result, 0, 'paragraph', 'A');
- assertBlock(result, 1, 'pre', ' Q\n <R>\n S');
- assertBlock(result, 2, 'paragraph', 'B');
- });
-
- test('pre format 4', () => {
- const comment = ' Q\n <R>\n S\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertBlock(result, 0, 'pre', ' Q\n <R>\n S');
- assertBlock(result, 1, 'paragraph', 'B');
- });
-
- test('quote 1', () => {
- const comment = '> I\'m happy\n > with quotes!\n\nSee above.';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assert.equal(result[0].type, 'quote');
- assert.lengthOf(result[0].blocks, 1);
- assertBlock(result[0].blocks, 0, 'paragraph', 'I\'m happy\nwith quotes!');
- assertBlock(result, 1, 'paragraph', 'See above.');
- });
-
- test('quote 2', () => {
- const comment = 'See this said:\n > a quoted\n > string block\n\nOK?';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertBlock(result, 0, 'paragraph', 'See this said:');
- assert.equal(result[1].type, 'quote');
- assert.lengthOf(result[1].blocks, 1);
- assertBlock(result[1].blocks, 0, 'paragraph', 'a quoted\nstring block');
- assertBlock(result, 2, 'paragraph', 'OK?');
- });
-
- test('nested quotes', () => {
- const comment = ' > > prior\n > \n > next\n';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assert.equal(result[0].type, 'quote');
- assert.lengthOf(result[0].blocks, 2);
- assert.equal(result[0].blocks[0].type, 'quote');
- assert.lengthOf(result[0].blocks[0].blocks, 1);
- assertBlock(result[0].blocks[0].blocks, 0, 'paragraph', 'prior');
- assertBlock(result[0].blocks, 1, 'paragraph', 'next');
- });
-
- test('code 1', () => {
- const comment = '```\n// test code\n```';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assert.equal(result[0].type, 'code');
- assert.equal(result[0].text, '// test code');
- });
-
- test('code 2', () => {
- const comment = 'test code\n```// test code```';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assert.equal(result[0].type, 'paragraph');
- assert.equal(result[0].text, 'test code');
- assert.equal(result[1].type, 'code');
- assert.equal(result[1].text, '// test code');
- });
-
- test('code 3', () => {
- const comment = 'test code\n```// test code```';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assert.equal(result[0].type, 'paragraph');
- assert.equal(result[0].text, 'test code');
- assert.equal(result[1].type, 'code');
- assert.equal(result[1].text, '// test code');
- });
-
- test('not a code', () => {
- const comment = 'test code\n```// test code';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assert.equal(result[0].type, 'paragraph');
- assert.equal(result[0].text, 'test code\n```// test code');
- });
-
- test('not a code 2', () => {
- const comment = 'test code\n```\n// test code';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assert.equal(result[0].type, 'paragraph');
- assert.equal(result[0].text, 'test code');
- assert.equal(result[1].type, 'paragraph');
- assert.equal(result[1].text, '```\n// test code');
- });
-
- test('mix all 1', () => {
- const comment = ' bullets:\n- bullet 1\n- bullet 2\n\ncode example:\n' +
- '```// test code```\n\n> reference is here';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 5);
- assert.equal(result[0].type, 'pre');
- assert.equal(result[1].type, 'list');
- assert.equal(result[2].type, 'paragraph');
- assert.equal(result[3].type, 'code');
- assert.equal(result[4].type, 'quote');
- });
-
- test('_computeNodes called without config', () => {
- const computeNodesSpy = sandbox.spy(element, '_computeNodes');
- element.content = 'some text';
- assert.isTrue(computeNodesSpy.called);
- });
-
- test('_contentOrConfigChanged called with config', () => {
- const contentStub = sandbox.stub(element, '_contentChanged');
- const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged');
- element.content = 'some text';
- element.config = {};
- assert.isTrue(contentStub.called);
- assert.isTrue(contentConfigStub.called);
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('parse null undefined and empty', () => {
+ assert.lengthOf(element._computeBlocks(null), 0);
+ assert.lengthOf(element._computeBlocks(undefined), 0);
+ assert.lengthOf(element._computeBlocks(''), 0);
+ });
+
+ test('parse simple', () => {
+ const comment = 'Para1';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertBlock(result, 0, 'paragraph', comment);
+ });
+
+ test('parse multiline para', () => {
+ const comment = 'Para 1\nStill para 1';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertBlock(result, 0, 'paragraph', comment);
+ });
+
+ test('parse para break without special blocks', () => {
+ const comment = 'Para 1\n\nPara 2\n\nPara 3';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertBlock(result, 0, 'paragraph', comment);
+ });
+
+ test('parse quote', () => {
+ const comment = '> Quote text';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assert.equal(result[0].type, 'quote');
+ assert.lengthOf(result[0].blocks, 1);
+ assertBlock(result[0].blocks, 0, 'paragraph', 'Quote text');
+ });
+
+ test('parse quote lead space', () => {
+ const comment = ' > Quote text';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assert.equal(result[0].type, 'quote');
+ assert.lengthOf(result[0].blocks, 1);
+ assertBlock(result[0].blocks, 0, 'paragraph', 'Quote text');
+ });
+
+ test('parse multiline quote', () => {
+ const comment = '> Quote line 1\n> Quote line 2\n > Quote line 3\n';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assert.equal(result[0].type, 'quote');
+ assert.lengthOf(result[0].blocks, 1);
+ assertBlock(result[0].blocks, 0, 'paragraph',
+ 'Quote line 1\nQuote line 2\nQuote line 3');
+ });
+
+ test('parse pre', () => {
+ const comment = ' Four space indent.';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertBlock(result, 0, 'pre', comment);
+ });
+
+ test('parse one space pre', () => {
+ const comment = ' One space indent.\n Another line.';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertBlock(result, 0, 'pre', comment);
+ });
+
+ test('parse tab pre', () => {
+ const comment = '\tOne tab indent.\n\tAnother line.\n Yet another!';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertBlock(result, 0, 'pre', comment);
+ });
+
+ test('parse star list', () => {
+ const comment = '* Item 1\n* Item 2\n* Item 3';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertListBlock(result, 0, 0, 'Item 1');
+ assertListBlock(result, 0, 1, 'Item 2');
+ assertListBlock(result, 0, 2, 'Item 3');
+ });
+
+ test('parse dash list', () => {
+ const comment = '- Item 1\n- Item 2\n- Item 3';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertListBlock(result, 0, 0, 'Item 1');
+ assertListBlock(result, 0, 1, 'Item 2');
+ assertListBlock(result, 0, 2, 'Item 3');
+ });
+
+ test('parse mixed list', () => {
+ const comment = '- Item 1\n* Item 2\n- Item 3\n* Item 4';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertListBlock(result, 0, 0, 'Item 1');
+ assertListBlock(result, 0, 1, 'Item 2');
+ assertListBlock(result, 0, 2, 'Item 3');
+ assertListBlock(result, 0, 3, 'Item 4');
+ });
+
+ test('parse mixed block types', () => {
+ const comment = 'Paragraph\nacross\na\nfew\nlines.' +
+ '\n\n' +
+ '> Quote\n> across\n> not many lines.' +
+ '\n\n' +
+ 'Another paragraph' +
+ '\n\n' +
+ '* Series\n* of\n* list\n* items' +
+ '\n\n' +
+ 'Yet another paragraph' +
+ '\n\n' +
+ '\tPreformatted text.' +
+ '\n\n' +
+ 'Parting words.';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 7);
+ assertBlock(result, 0, 'paragraph', 'Paragraph\nacross\na\nfew\nlines.\n');
+
+ assert.equal(result[1].type, 'quote');
+ assert.lengthOf(result[1].blocks, 1);
+ assertBlock(result[1].blocks, 0, 'paragraph',
+ 'Quote\nacross\nnot many lines.');
+
+ assertBlock(result, 2, 'paragraph', 'Another paragraph\n');
+ assertListBlock(result, 3, 0, 'Series');
+ assertListBlock(result, 3, 1, 'of');
+ assertListBlock(result, 3, 2, 'list');
+ assertListBlock(result, 3, 3, 'items');
+ assertBlock(result, 4, 'paragraph', 'Yet another paragraph\n');
+ assertBlock(result, 5, 'pre', '\tPreformatted text.');
+ assertBlock(result, 6, 'paragraph', 'Parting words.');
+ });
+
+ test('bullet list 1', () => {
+ const comment = 'A\n\n* line 1\n* 2nd line';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assertBlock(result, 0, 'paragraph', 'A\n');
+ assertListBlock(result, 1, 0, 'line 1');
+ assertListBlock(result, 1, 1, '2nd line');
+ });
+
+ test('bullet list 2', () => {
+ const comment = 'A\n* line 1\n* 2nd line\n\nB';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 3);
+ assertBlock(result, 0, 'paragraph', 'A');
+ assertListBlock(result, 1, 0, 'line 1');
+ assertListBlock(result, 1, 1, '2nd line');
+ assertBlock(result, 2, 'paragraph', 'B');
+ });
+
+ test('bullet list 3', () => {
+ const comment = '* line 1\n* 2nd line\n\nB';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assertListBlock(result, 0, 0, 'line 1');
+ assertListBlock(result, 0, 1, '2nd line');
+ assertBlock(result, 1, 'paragraph', 'B');
+ });
+
+ test('bullet list 4', () => {
+ const comment = 'To see this bug, you have to:\n' +
+ '* Be on IMAP or EAS (not on POP)\n' +
+ '* Be very unlucky\n';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assertBlock(result, 0, 'paragraph', 'To see this bug, you have to:');
+ assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)');
+ assertListBlock(result, 1, 1, 'Be very unlucky');
+ });
+
+ test('bullet list 5', () => {
+ const comment = 'To see this bug,\n' +
+ 'you have to:\n' +
+ '* Be on IMAP or EAS (not on POP)\n' +
+ '* Be very unlucky\n';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assertBlock(result, 0, 'paragraph', 'To see this bug,\nyou have to:');
+ assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)');
+ assertListBlock(result, 1, 1, 'Be very unlucky');
+ });
+
+ test('dash list 1', () => {
+ const comment = 'A\n- line 1\n- 2nd line';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assertBlock(result, 0, 'paragraph', 'A');
+ assertListBlock(result, 1, 0, 'line 1');
+ assertListBlock(result, 1, 1, '2nd line');
+ });
+
+ test('dash list 2', () => {
+ const comment = 'A\n- line 1\n- 2nd line\n\nB';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 3);
+ assertBlock(result, 0, 'paragraph', 'A');
+ assertListBlock(result, 1, 0, 'line 1');
+ assertListBlock(result, 1, 1, '2nd line');
+ assertBlock(result, 2, 'paragraph', 'B');
+ });
+
+ test('dash list 3', () => {
+ const comment = '- line 1\n- 2nd line\n\nB';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assertListBlock(result, 0, 0, 'line 1');
+ assertListBlock(result, 0, 1, '2nd line');
+ assertBlock(result, 1, 'paragraph', 'B');
+ });
+
+ test('nested list will NOT be recognized', () => {
+ // will be rendered as two separate lists
+ const comment = '- line 1\n - line with indentation\n- line 2';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 3);
+ assertListBlock(result, 0, 0, 'line 1');
+ assert.equal(result[1].type, 'pre');
+ assertListBlock(result, 2, 0, 'line 2');
+ });
+
+ test('pre format 1', () => {
+ const comment = 'A\n This is pre\n formatted';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assertBlock(result, 0, 'paragraph', 'A');
+ assertBlock(result, 1, 'pre', ' This is pre\n formatted');
+ });
+
+ test('pre format 2', () => {
+ const comment = 'A\n This is pre\n formatted\n\nbut this is not';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 3);
+ assertBlock(result, 0, 'paragraph', 'A');
+ assertBlock(result, 1, 'pre', ' This is pre\n formatted');
+ assertBlock(result, 2, 'paragraph', 'but this is not');
+ });
+
+ test('pre format 3', () => {
+ const comment = 'A\n Q\n <R>\n S\n\nB';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 3);
+ assertBlock(result, 0, 'paragraph', 'A');
+ assertBlock(result, 1, 'pre', ' Q\n <R>\n S');
+ assertBlock(result, 2, 'paragraph', 'B');
+ });
+
+ test('pre format 4', () => {
+ const comment = ' Q\n <R>\n S\n\nB';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assertBlock(result, 0, 'pre', ' Q\n <R>\n S');
+ assertBlock(result, 1, 'paragraph', 'B');
+ });
+
+ test('quote 1', () => {
+ const comment = '> I\'m happy\n > with quotes!\n\nSee above.';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assert.equal(result[0].type, 'quote');
+ assert.lengthOf(result[0].blocks, 1);
+ assertBlock(result[0].blocks, 0, 'paragraph', 'I\'m happy\nwith quotes!');
+ assertBlock(result, 1, 'paragraph', 'See above.');
+ });
+
+ test('quote 2', () => {
+ const comment = 'See this said:\n > a quoted\n > string block\n\nOK?';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 3);
+ assertBlock(result, 0, 'paragraph', 'See this said:');
+ assert.equal(result[1].type, 'quote');
+ assert.lengthOf(result[1].blocks, 1);
+ assertBlock(result[1].blocks, 0, 'paragraph', 'a quoted\nstring block');
+ assertBlock(result, 2, 'paragraph', 'OK?');
+ });
+
+ test('nested quotes', () => {
+ const comment = ' > > prior\n > \n > next\n';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assert.equal(result[0].type, 'quote');
+ assert.lengthOf(result[0].blocks, 2);
+ assert.equal(result[0].blocks[0].type, 'quote');
+ assert.lengthOf(result[0].blocks[0].blocks, 1);
+ assertBlock(result[0].blocks[0].blocks, 0, 'paragraph', 'prior');
+ assertBlock(result[0].blocks, 1, 'paragraph', 'next');
+ });
+
+ test('code 1', () => {
+ const comment = '```\n// test code\n```';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assert.equal(result[0].type, 'code');
+ assert.equal(result[0].text, '// test code');
+ });
+
+ test('code 2', () => {
+ const comment = 'test code\n```// test code```';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assert.equal(result[0].type, 'paragraph');
+ assert.equal(result[0].text, 'test code');
+ assert.equal(result[1].type, 'code');
+ assert.equal(result[1].text, '// test code');
+ });
+
+ test('code 3', () => {
+ const comment = 'test code\n```// test code```';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assert.equal(result[0].type, 'paragraph');
+ assert.equal(result[0].text, 'test code');
+ assert.equal(result[1].type, 'code');
+ assert.equal(result[1].text, '// test code');
+ });
+
+ test('not a code', () => {
+ const comment = 'test code\n```// test code';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assert.equal(result[0].type, 'paragraph');
+ assert.equal(result[0].text, 'test code\n```// test code');
+ });
+
+ test('not a code 2', () => {
+ const comment = 'test code\n```\n// test code';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assert.equal(result[0].type, 'paragraph');
+ assert.equal(result[0].text, 'test code');
+ assert.equal(result[1].type, 'paragraph');
+ assert.equal(result[1].text, '```\n// test code');
+ });
+
+ test('mix all 1', () => {
+ const comment = ' bullets:\n- bullet 1\n- bullet 2\n\ncode example:\n' +
+ '```// test code```\n\n> reference is here';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 5);
+ assert.equal(result[0].type, 'pre');
+ assert.equal(result[1].type, 'list');
+ assert.equal(result[2].type, 'paragraph');
+ assert.equal(result[3].type, 'code');
+ assert.equal(result[4].type, 'quote');
+ });
+
+ test('_computeNodes called without config', () => {
+ const computeNodesSpy = sandbox.spy(element, '_computeNodes');
+ element.content = 'some text';
+ assert.isTrue(computeNodesSpy.called);
+ });
+
+ test('_contentOrConfigChanged called with config', () => {
+ const contentStub = sandbox.stub(element, '_contentChanged');
+ const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged');
+ element.content = 'some text';
+ element.config = {};
+ assert.isTrue(contentStub.called);
+ assert.isTrue(contentConfigStub.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.js
new file mode 100644
index 0000000..0bc9cb7
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.js
@@ -0,0 +1,50 @@
+/**
+ * @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 '../../../scripts/bundled-polymer.js';
+
+import '@polymer/iron-icon/iron-icon.js';
+import '../../../styles/shared-styles.js';
+import '../gr-avatar/gr-avatar.js';
+import '../gr-button/gr-button.js';
+import {hovercardBehaviorMixin} from '../gr-hovercard/gr-hovercard-behavior.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-hovercard-account_html.js';
+
+/** @extends Polymer.Element */
+class GrHovercardAccount extends GestureEventListeners(
+ hovercardBehaviorMixin(LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-hovercard-account'; }
+
+ static get properties() {
+ return {
+ account: Object,
+ voteableText: String,
+ attention: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ };
+ }
+}
+
+customElements.define(GrHovercardAccount.is, GrHovercardAccount);
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js
new file mode 100644
index 0000000..0763420
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js
@@ -0,0 +1,96 @@
+/**
+ * @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 '../gr-hovercard/gr-hovercard-shared-style.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+export const htmlTemplate = html`
+ <style include="gr-hovercard-shared-style">
+ .top,
+ .attention,
+ .status,
+ .voteable {
+ padding: var(--spacing-s) var(--spacing-l);
+ }
+ .top {
+ display: flex;
+ padding-top: var(--spacing-xl);
+ min-width: 300px;
+ }
+ gr-avatar {
+ height: 48px;
+ width: 48px;
+ margin-right: var(--spacing-l);
+ }
+ .title,
+ .email {
+ color: var(--deemphasized-text-color);
+ }
+ .status iron-icon {
+ width: 14px;
+ height: 14px;
+ vertical-align: top;
+ position: relative;
+ top: 2px;
+ }
+ .action {
+ border-top: 1px solid var(--border-color);
+ padding: var(--spacing-s) var(--spacing-l);
+ --gr-button: {
+ padding: var(--spacing-s) 0;
+ };
+ }
+ :host(:not([attention])) .attention {
+ display: none;
+ }
+ .attention {
+ background-color: var(--emphasis-color);
+ }
+ .attention iron-icon {
+ vertical-align: top;
+ }
+ </style>
+ <div id="container" role="tooltip" tabindex="-1">
+ <div class="top">
+ <div class="avatar">
+ <gr-avatar account="[[account]]" image-size="56"></gr-avatar>
+ </div>
+ <div class="account">
+ <h3 class="name">[[account.name]]</h3>
+ <div class="email">[[account.email]]</div>
+ </div>
+ </div>
+ <template is="dom-if" if="[[account.status]]">
+ <div class="status">
+ <span class="title">
+ <iron-icon icon="gr-icons:calendar"></iron-icon>
+ Status:
+ </span>
+ <span class="value">[[account.status]]</span>
+ </div>
+ </template>
+ <template is="dom-if" if="[[voteableText]]">
+ <div class="voteable">
+ <span class="title">Voteable:</span>
+ <span class="value">[[voteableText]]</span>
+ </div>
+ </template>
+ <div class="attention">
+ <iron-icon icon="gr-icons:attention"></iron-icon>
+ <span>It is this user's turn to take action.</span>
+ </div>
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html
new file mode 100644
index 0000000..be0f2b2
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<!--
+@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.
+-->
+
+<meta name="viewport"
+ content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-hovercard-account</title>
+
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script src="../../../node_modules/iron-test-helpers/mock-interactions.js" type="module"></script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-hovercard-account class="hovered"></gr-hovercard-account>
+ </template>
+</test-fixture>
+
+
+<script type="module">
+ import '../../../test/common-test-setup.js';
+ import './gr-hovercard-account.js';
+
+ suite('gr-hovercard-account tests', () => {
+ let element;
+ const ACCOUNT = {
+ email: 'kermit@gmail.com',
+ username: 'kermit',
+ name: 'Kermit The Frog',
+ _account_id: '31415926535',
+ };
+
+ setup(() => {
+ element = fixture('basic');
+ element.account = Object.assign({}, ACCOUNT);
+ });
+
+ test('account name is shown', () => {
+ assert.equal(element.shadowRoot.querySelector('.name').innerText,
+ 'Kermit The Frog');
+ });
+
+ test('account status is not shown if the property is not set', () => {
+ assert.isNull(element.shadowRoot.querySelector('.status'));
+ });
+
+ test('account status is displayed', () => {
+ element.account = Object.assign({status: 'OOO'}, ACCOUNT);
+ flushAsynchronousOperations();
+ assert.equal(element.shadowRoot.querySelector('.status .value').innerText,
+ 'OOO');
+ });
+
+ test('voteable div is not shown if the property is not set', () => {
+ assert.isNull(element.shadowRoot.querySelector('.voteable'));
+ });
+
+ test('voteable div is displayed', () => {
+ element.voteableText = 'CodeReview: +2';
+ flushAsynchronousOperations();
+ assert.equal(element.shadowRoot.querySelector('.voteable .value').innerText,
+ element.voteableText);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
new file mode 100644
index 0000000..1ba3c23
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
@@ -0,0 +1,383 @@
+/**
+ * @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 '../../../scripts/bundled-polymer.js';
+
+import '../../../styles/shared-styles.js';
+import '../../../scripts/rootElement.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const HOVER_CLASS = 'hovered';
+
+/**
+ * When the hovercard is positioned diagonally (bottom-left, bottom-right,
+ * top-left, or top-right), we add additional (invisible) padding so that the
+ * area that a user can hover over to access the hovercard is larger.
+ */
+const DIAGONAL_OVERFLOW = 15;
+
+/**
+ * How long should be wait before showing the hovercard when the user hovers
+ * over the element?
+ */
+const SHOW_DELAY_MS = 500;
+
+/**
+ * The mixin for gr-hovercard-behavior.
+ *
+ * @example
+ *
+ * // LegacyElementMixin is still needed to support the old lifecycles
+ * // TODO: Replace old life cycles with new ones.
+ *
+ * class YourComponent extends hovercardBehaviorMixin(
+ * LegacyElementMixin(PolymerElement)
+ * ) {
+ * static get is() { return ''; }
+ * static get template() { return html``; }
+ * }
+ *
+ * customElements.define(GrHovercard.is, GrHovercard);
+ *
+ * @see gr-hovercard.js
+ *
+ * // following annotations are required for polylint
+ * @polymer
+ * @mixinFunction
+ */
+export const hovercardBehaviorMixin = superClass => class extends superClass {
+ static get properties() {
+ return {
+ /**
+ * @type {?}
+ */
+ _target: Object,
+
+ /**
+ * Determines whether or not the hovercard is visible.
+ *
+ * @type {boolean}
+ */
+ _isShowing: {
+ type: Boolean,
+ value: false,
+ },
+ /**
+ * The `id` of the element that the hovercard is anchored to.
+ *
+ * @type {string}
+ */
+ for: {
+ type: String,
+ observer: '_forChanged',
+ },
+
+ /**
+ * The spacing between the top of the hovercard and the element it is
+ * anchored to.
+ *
+ * @type {number}
+ */
+ offset: {
+ type: Number,
+ value: 14,
+ },
+
+ /**
+ * Positions the hovercard to the top, right, bottom, left, bottom-left,
+ * bottom-right, top-left, or top-right of its content.
+ *
+ * @type {string}
+ */
+ position: {
+ type: String,
+ value: 'right',
+ },
+
+ container: Object,
+ /**
+ * ID for the container element.
+ *
+ * @type {string}
+ */
+ containerId: {
+ type: String,
+ value: 'gr-hovercard-container',
+ },
+ };
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ if (!this._target) { this._target = this.target; }
+ this.listen(this._target, 'mouseenter', 'showDelayed');
+ this.listen(this._target, 'focus', 'showDelayed');
+ this.listen(this._target, 'mouseleave', 'hide');
+ this.listen(this._target, 'blur', 'hide');
+ this.listen(this._target, 'click', 'hide');
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('mouseleave',
+ e => this.hide(e));
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ // First, check to see if the container has already been created.
+ this.container = Gerrit.getRootElement()
+ .querySelector('#' + this.containerId);
+
+ if (this.container) { return; }
+
+ // If it does not exist, create and initialize the hovercard container.
+ this.container = document.createElement('div');
+ this.container.setAttribute('id', this.containerId);
+ Gerrit.getRootElement().appendChild(this.container);
+ }
+
+ removeListeners() {
+ this.unlisten(this._target, 'mouseenter', 'show');
+ this.unlisten(this._target, 'focus', 'show');
+ this.unlisten(this._target, 'mouseleave', 'hide');
+ this.unlisten(this._target, 'blur', 'hide');
+ this.unlisten(this._target, 'click', 'hide');
+ }
+
+ /**
+ * Returns the target element that the hovercard is anchored to (the `id` of
+ * the `for` property).
+ *
+ * @type {HTMLElement}
+ */
+ get target() {
+ const parentNode = dom(this).parentNode;
+ // If the parentNode is a document fragment, then we need to use the host.
+ const ownerRoot = dom(this).getOwnerRoot();
+ let target;
+ if (this.for) {
+ target = dom(ownerRoot).querySelector('#' + this.for);
+ } else {
+ target = parentNode.nodeType == Node.DOCUMENT_FRAGMENT_NODE ?
+ ownerRoot.host :
+ parentNode;
+ }
+ return target;
+ }
+
+ /**
+ * Hides/closes the hovercard. This occurs when the user triggers the
+ * `mouseleave` event on the hovercard's `target` element (as long as the
+ * user is not hovering over the hovercard).
+ *
+ * @param {Event} e DOM Event (e.g. `mouseleave` event)
+ */
+ hide(e) {
+ this._isScheduledToShow = false;
+ if (!this._isShowing) {
+ return;
+ }
+ const targetRect = this._target.getBoundingClientRect();
+ const x = e.clientX;
+ const y = e.clientY;
+ if (x > targetRect.left && x < targetRect.right && y > targetRect.top &&
+ y < targetRect.bottom) {
+ // Sometimes the hovercard itself obscures the mouse pointer, and
+ // that generates a mouseleave event. We don't want to hide the hovercard
+ // in that situation.
+ return;
+ }
+
+ // If the user is now hovering over the hovercard or the user is returning
+ // from the hovercard but now hovering over the target (to stop an annoying
+ // flicker effect), just return.
+ if (e.toElement === this ||
+ (e.fromElement === this && e.toElement === this._target)) {
+ return;
+ }
+
+ // Mark that the hovercard is not visible and do not allow focusing
+ this._isShowing = false;
+
+ // Clear styles in preparation for the next time we need to show the card
+ this.classList.remove(HOVER_CLASS);
+
+ // Reset and remove the hovercard from the DOM
+ this.style.cssText = '';
+ this.$.container.setAttribute('tabindex', -1);
+
+ // Remove the hovercard from the container, given that it is still a child
+ // of the container.
+ if (this.container.contains(this)) {
+ this.container.removeChild(this);
+ }
+ }
+
+ /**
+ * Shows/opens the hovercard with a fixed delay.
+ */
+ showDelayed() {
+ this.showDelayedBy(SHOW_DELAY_MS);
+ }
+
+ /**
+ * Shows/opens the hovercard with the given delay.
+ */
+ showDelayedBy(delayMs) {
+ if (this._isShowing || this._isScheduledToShow) return;
+ this._isScheduledToShow = true;
+ setTimeout(() => {
+ // This happens when the mouse leaves the target before the delay is over.
+ if (!this._isScheduledToShow) return;
+ this._isScheduledToShow = false;
+ this.show();
+ }, delayMs);
+ }
+
+ /**
+ * Shows/opens the hovercard. This occurs when the user triggers the
+ * `mousenter` event on the hovercard's `target` element.
+ */
+ show() {
+ if (this._isShowing) {
+ return;
+ }
+
+ // Mark that the hovercard is now visible
+ this._isShowing = true;
+ this.setAttribute('tabindex', 0);
+
+ // Add it to the DOM and calculate its position
+ this.container.appendChild(this);
+ this.updatePosition();
+
+ // Trigger the transition
+ this.classList.add(HOVER_CLASS);
+ }
+
+ /**
+ * Updates the hovercard's position based on the `position` attribute
+ * and the current position of the `target` element.
+ *
+ * The hovercard is supposed to stay open if the user hovers over it.
+ * To keep it open when the user moves away from the target, the bounding
+ * rects of the target and hovercard must touch or overlap.
+ *
+ * NOTE: You do not need to directly call this method unless you need to
+ * update the position of the tooltip while it is already visible (the
+ * target element has moved and the tooltip is still open).
+ */
+ updatePosition() {
+ if (!this._target) { return; }
+
+ // Calculate the necessary measurements and positions
+ const parentRect = document.documentElement.getBoundingClientRect();
+ const targetRect = this._target.getBoundingClientRect();
+ const thisRect = this.getBoundingClientRect();
+
+ const targetLeft = targetRect.left - parentRect.left;
+ const targetTop = targetRect.top - parentRect.top;
+
+ let hovercardLeft;
+ let hovercardTop;
+ const diagonalPadding = this.offset + DIAGONAL_OVERFLOW;
+ let cssText = '';
+
+ // Find the top and left position values based on the position attribute
+ // of the hovercard.
+ switch (this.position) {
+ case 'top':
+ hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
+ hovercardTop = targetTop - thisRect.height - this.offset;
+ cssText += `padding-bottom:${this.offset
+ }px; margin-bottom:-${this.offset}px;`;
+ break;
+ case 'bottom':
+ hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
+ hovercardTop = targetTop + targetRect.height + this.offset;
+ cssText +=
+ `padding-top:${this.offset}px; margin-top:-${this.offset}px;`;
+ break;
+ case 'left':
+ hovercardLeft = targetLeft - thisRect.width - this.offset;
+ hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
+ cssText +=
+ `padding-right:${this.offset}px; margin-right:-${this.offset}px;`;
+ break;
+ case 'right':
+ hovercardLeft = targetRect.right + this.offset;
+ hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
+ cssText +=
+ `padding-left:${this.offset}px; margin-left:-${this.offset}px;`;
+ break;
+ case 'bottom-right':
+ hovercardLeft = targetRect.left + targetRect.width + this.offset;
+ hovercardTop = targetRect.top + targetRect.height + this.offset;
+ cssText += `padding-top:${diagonalPadding}px;`;
+ cssText += `padding-left:${diagonalPadding}px;`;
+ cssText += `margin-left:-${diagonalPadding}px;`;
+ cssText += `margin-top:-${diagonalPadding}px;`;
+ break;
+ case 'bottom-left':
+ hovercardLeft = targetRect.left - thisRect.width - this.offset;
+ hovercardTop = targetRect.top + targetRect.height + this.offset;
+ cssText += `padding-top:${diagonalPadding}px;`;
+ cssText += `padding-right:${diagonalPadding}px;`;
+ cssText += `margin-right:-${diagonalPadding}px;`;
+ cssText += `margin-top:-${diagonalPadding}px;`;
+ break;
+ case 'top-left':
+ hovercardLeft = targetRect.left - thisRect.width - this.offset;
+ hovercardTop = targetRect.top - thisRect.height - this.offset;
+ cssText += `padding-bottom:${diagonalPadding}px;`;
+ cssText += `padding-right:${diagonalPadding}px;`;
+ cssText += `margin-bottom:-${diagonalPadding}px;`;
+ cssText += `margin-right:-${diagonalPadding}px;`;
+ break;
+ case 'top-right':
+ hovercardLeft = targetRect.left + targetRect.width + this.offset;
+ hovercardTop = targetRect.top - thisRect.height - this.offset;
+ cssText += `padding-bottom:${diagonalPadding}px;`;
+ cssText += `padding-left:${diagonalPadding}px;`;
+ cssText += `margin-bottom:-${diagonalPadding}px;`;
+ cssText += `margin-left:-${diagonalPadding}px;`;
+ break;
+ }
+
+ // Prevent hovercard from appearing outside the viewport.
+ // TODO(kaspern): fix hovercard appearing outside viewport on bottom and
+ // right.
+ if (hovercardLeft < 0) { hovercardLeft = 0; }
+ if (hovercardTop < 0) { hovercardTop = 0; }
+ // Set the hovercard's position
+ cssText += `left:${hovercardLeft}px; top:${hovercardTop}px;`;
+ this.style.cssText = cssText;
+ }
+
+ /**
+ * Responds to a change in the `for` value and gets the updated `target`
+ * element for the hovercard.
+ *
+ * @private
+ */
+ _forChanged() {
+ this._target = this.target;
+ }
+};
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.js
new file mode 100644
index 0000000..a392691
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.js
@@ -0,0 +1,46 @@
+/**
+ * @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.
+ */
+
+/** The shared styles for all hover cards. */
+const GrHoverCardSharedStyle = document.createElement('dom-module');
+GrHoverCardSharedStyle.innerHTML =
+ `<template>
+ <style include="shared-styles">
+ :host {
+ box-sizing: border-box;
+ opacity: 0;
+ position: absolute;
+ transition: opacity 200ms;
+ visibility: hidden;
+ z-index: 200;
+ }
+ :host(.hovered) {
+ visibility: visible;
+ opacity: 1;
+ }
+ /* You have to use a <div class="container"> in your hovercard in order
+ to pick up this consistent styling. */
+ #container {
+ background: var(--dialog-background-color);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--elevation-level-5);
+ }
+ </style>
+ </template>`;
+
+GrHoverCardSharedStyle.register('gr-hovercard-shared-style');
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.html b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.html
deleted file mode 100644
index fcc04ba..0000000
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.html
+++ /dev/null
@@ -1,49 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-hovercard">
- <template>
- <style include="shared-styles">
- :host {
- box-sizing: border-box;
- opacity: 0;
- position: absolute;
- transition: opacity 200ms;
- visibility: hidden;
- z-index: 100;
- }
- :host(.hovered) {
- visibility: visible;
- opacity: 1;
- }
- #hovercard {
- background: var(--dialog-background-color);
- box-shadow: var(--elevation-level-2);
- padding: var(--spacing-l);
- }
- </style>
- <div id="hovercard" role="tooltip" tabindex="-1">
- <slot></slot>
- </div>
- </template>
- <script src="../../../scripts/rootElement.js"></script>
- <script src="gr-hovercard.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
index ce34d3a..3f936dd 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
@@ -14,322 +14,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
- const HOVER_CLASS = 'hovered';
+import '../../../scripts/bundled-polymer.js';
- /**
- * When the hovercard is positioned diagonally (bottom-left, bottom-right,
- * top-left, or top-right), we add additional (invisible) padding so that the
- * area that a user can hover over to access the hovercard is larger.
- */
- const DIAGONAL_OVERFLOW = 15;
+import '../../../styles/shared-styles.js';
+import '../../../scripts/rootElement.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-hovercard_html.js';
+import {hovercardBehaviorMixin} from './gr-hovercard-behavior.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import './gr-hovercard-shared-style.js';
- /** @extends Polymer.Element */
- class GrHovercard extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-hovercard'; }
+/** @extends Polymer.Element */
+class GrHovercard extends GestureEventListeners(
+ hovercardBehaviorMixin(LegacyElementMixin(PolymerElement))
+) {
+ static get template() { return htmlTemplate; }
- static get properties() {
- return {
- /**
- * @type {?}
- */
- _target: Object,
+ static get is() { return 'gr-hovercard'; }
+}
- /**
- * Determines whether or not the hovercard is visible.
- *
- * @type {boolean}
- */
- _isShowing: {
- type: Boolean,
- value: false,
- },
- /**
- * The `id` of the element that the hovercard is anchored to.
- *
- * @type {string}
- */
- for: {
- type: String,
- observer: '_forChanged',
- },
-
- /**
- * The spacing between the top of the hovercard and the element it is
- * anchored to.
- *
- * @type {number}
- */
- offset: {
- type: Number,
- value: 14,
- },
-
- /**
- * Positions the hovercard to the top, right, bottom, left, bottom-left,
- * bottom-right, top-left, or top-right of its content.
- *
- * @type {string}
- */
- position: {
- type: String,
- value: 'bottom',
- },
-
- container: Object,
- /**
- * ID for the container element.
- *
- * @type {string}
- */
- containerId: {
- type: String,
- value: 'gr-hovercard-container',
- },
- };
- }
-
- /** @override */
- attached() {
- super.attached();
- if (!this._target) { this._target = this.target; }
- this.listen(this._target, 'mouseenter', 'show');
- this.listen(this._target, 'focus', 'show');
- this.listen(this._target, 'mouseleave', 'hide');
- this.listen(this._target, 'blur', 'hide');
- this.listen(this._target, 'click', 'hide');
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('mouseleave',
- e => this.hide(e));
- }
-
- /** @override */
- ready() {
- super.ready();
- // First, check to see if the container has already been created.
- this.container = Gerrit.getRootElement()
- .querySelector('#' + this.containerId);
-
- if (this.container) { return; }
-
- // If it does not exist, create and initialize the hovercard container.
- this.container = document.createElement('div');
- this.container.setAttribute('id', this.containerId);
- Gerrit.getRootElement().appendChild(this.container);
- }
-
- removeListeners() {
- this.unlisten(this._target, 'mouseenter', 'show');
- this.unlisten(this._target, 'focus', 'show');
- this.unlisten(this._target, 'mouseleave', 'hide');
- this.unlisten(this._target, 'blur', 'hide');
- this.unlisten(this._target, 'click', 'hide');
- }
-
- /**
- * Returns the target element that the hovercard is anchored to (the `id` of
- * the `for` property).
- *
- * @type {HTMLElement}
- */
- get target() {
- const parentNode = Polymer.dom(this).parentNode;
- // If the parentNode is a document fragment, then we need to use the host.
- const ownerRoot = Polymer.dom(this).getOwnerRoot();
- let target;
- if (this.for) {
- target = Polymer.dom(ownerRoot).querySelector('#' + this.for);
- } else {
- target = parentNode.nodeType == Node.DOCUMENT_FRAGMENT_NODE ?
- ownerRoot.host :
- parentNode;
- }
- return target;
- }
-
- /**
- * Hides/closes the hovercard. This occurs when the user triggers the
- * `mouseleave` event on the hovercard's `target` element (as long as the
- * user is not hovering over the hovercard).
- *
- * @param {Event} e DOM Event (e.g. `mouseleave` event)
- */
- hide(e) {
- const targetRect = this._target.getBoundingClientRect();
- const x = e.clientX;
- const y = e.clientY;
- if (x > targetRect.left && x < targetRect.right && y > targetRect.top &&
- y < targetRect.bottom) {
- // Sometimes the hovercard itself obscures the mouse pointer, and
- // that generates a mouseleave event. We don't want to hide the hovercard
- // in that situation.
- return;
- }
-
- // If the hovercard is already hidden or the user is now hovering over the
- // hovercard or the user is returning from the hovercard but now hovering
- // over the target (to stop an annoying flicker effect), just return.
- if (!this._isShowing || e.toElement === this ||
- (e.fromElement === this && e.toElement === this._target)) {
- return;
- }
-
- // Mark that the hovercard is not visible and do not allow focusing
- this._isShowing = false;
-
- // Clear styles in preparation for the next time we need to show the card
- this.classList.remove(HOVER_CLASS);
-
- // Reset and remove the hovercard from the DOM
- this.style.cssText = '';
- this.$.hovercard.setAttribute('tabindex', -1);
-
- // Remove the hovercard from the container, given that it is still a child
- // of the container.
- if (this.container.contains(this)) {
- this.container.removeChild(this);
- }
- }
-
- /**
- * Shows/opens the hovercard. This occurs when the user triggers the
- * `mousenter` event on the hovercard's `target` element.
- *
- * @param {Event} e DOM Event (e.g., `mouseenter` event)
- */
- show(e) {
- if (this._isShowing) {
- return;
- }
-
- // Mark that the hovercard is now visible
- this._isShowing = true;
- this.setAttribute('tabindex', 0);
-
- // Add it to the DOM and calculate its position
- this.container.appendChild(this);
- this.updatePosition();
-
- // Trigger the transition
- this.classList.add(HOVER_CLASS);
- }
-
- /**
- * Updates the hovercard's position based on the `position` attribute
- * and the current position of the `target` element.
- *
- * The hovercard is supposed to stay open if the user hovers over it.
- * To keep it open when the user moves away from the target, the bounding
- * rects of the target and hovercard must touch or overlap.
- *
- * NOTE: You do not need to directly call this method unless you need to
- * update the position of the tooltip while it is already visible (the
- * target element has moved and the tooltip is still open).
- */
- updatePosition() {
- if (!this._target) { return; }
-
- // Calculate the necessary measurements and positions
- const parentRect = document.documentElement.getBoundingClientRect();
- const targetRect = this._target.getBoundingClientRect();
- const thisRect = this.getBoundingClientRect();
-
- const targetLeft = targetRect.left - parentRect.left;
- const targetTop = targetRect.top - parentRect.top;
-
- let hovercardLeft;
- let hovercardTop;
- const diagonalPadding = this.offset + DIAGONAL_OVERFLOW;
- let cssText = '';
-
- // Find the top and left position values based on the position attribute
- // of the hovercard.
- switch (this.position) {
- case 'top':
- hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
- hovercardTop = targetTop - thisRect.height - this.offset;
- cssText += `padding-bottom:${this.offset
- }px; margin-bottom:-${this.offset}px;`;
- break;
- case 'bottom':
- hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
- hovercardTop = targetTop + targetRect.height + this.offset;
- cssText +=
- `padding-top:${this.offset}px; margin-top:-${this.offset}px;`;
- break;
- case 'left':
- hovercardLeft = targetLeft - thisRect.width - this.offset;
- hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
- cssText +=
- `padding-right:${this.offset}px; margin-right:-${this.offset}px;`;
- break;
- case 'right':
- hovercardLeft = targetRect.right + this.offset;
- hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
- cssText +=
- `padding-left:${this.offset}px; margin-left:-${this.offset}px;`;
- break;
- case 'bottom-right':
- hovercardLeft = targetRect.left + targetRect.width + this.offset;
- hovercardTop = targetRect.top + targetRect.height + this.offset;
- cssText += `padding-top:${diagonalPadding}px;`;
- cssText += `padding-left:${diagonalPadding}px;`;
- cssText += `margin-left:-${diagonalPadding}px;`;
- cssText += `margin-top:-${diagonalPadding}px;`;
- break;
- case 'bottom-left':
- hovercardLeft = targetRect.left - thisRect.width - this.offset;
- hovercardTop = targetRect.top + targetRect.height + this.offset;
- cssText += `padding-top:${diagonalPadding}px;`;
- cssText += `padding-right:${diagonalPadding}px;`;
- cssText += `margin-right:-${diagonalPadding}px;`;
- cssText += `margin-top:-${diagonalPadding}px;`;
- break;
- case 'top-left':
- hovercardLeft = targetRect.left - thisRect.width - this.offset;
- hovercardTop = targetRect.top - thisRect.height - this.offset;
- cssText += `padding-bottom:${diagonalPadding}px;`;
- cssText += `padding-right:${diagonalPadding}px;`;
- cssText += `margin-bottom:-${diagonalPadding}px;`;
- cssText += `margin-right:-${diagonalPadding}px;`;
- break;
- case 'top-right':
- hovercardLeft = targetRect.left + targetRect.width + this.offset;
- hovercardTop = targetRect.top - thisRect.height - this.offset;
- cssText += `padding-bottom:${diagonalPadding}px;`;
- cssText += `padding-left:${diagonalPadding}px;`;
- cssText += `margin-bottom:-${diagonalPadding}px;`;
- cssText += `margin-left:-${diagonalPadding}px;`;
- break;
- }
-
- // Prevent hovercard from appearing outside the viewport.
- // TODO(kaspern): fix hovercard appearing outside viewport on bottom and
- // right.
- if (hovercardLeft < 0) { hovercardLeft = 0; }
- if (hovercardTop < 0) { hovercardTop = 0; }
- // Set the hovercard's position
- cssText += `left:${hovercardLeft}px; top:${hovercardTop}px;`;
- this.style.cssText = cssText;
- }
-
- /**
- * Responds to a change in the `for` value and gets the updated `target`
- * element for the hovercard.
- *
- * @private
- */
- _forChanged() {
- this._target = this.target;
- }
- }
-
- customElements.define(GrHovercard.is, GrHovercard);
-})();
+customElements.define(GrHovercard.is, GrHovercard);
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js
new file mode 100644
index 0000000..69fd4c5
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js
@@ -0,0 +1,28 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="gr-hovercard-shared-style">
+ #container {
+ padding: var(--spacing-l);
+ }
+ </style>
+ <div id="container" role="tooltip" tabindex="-1">
+ <slot></slot>
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
index 3c04853..99791e7 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
@@ -19,17 +19,17 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-hovercard</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="/bower_components/iron-test-helpers/mock-interactions.js"></script>
-
-<link rel="import" href="gr-hovercard.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<!-- Can't use absolute path below for mock-interaction.js.
+Web component tester(wct) has a built-in http server and it serves "/components" directory (which is
+actually /node_modules directory). Also, wct patches some files to load modules from /components.
+With the absolute path, browser tries to load dom-module from 2 different places (/component/... and
+/node_modules/...) though this is actually the same file. This leads to a run-time error.
+-->
+<script src="../../../node_modules/iron-test-helpers/mock-interactions.js"></script>
<button id="foo">Hello</button>
<test-fixture id="basic">
@@ -38,87 +38,106 @@
</template>
</test-fixture>
-<script>
- suite('gr-hovercard tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- // For css animations
- const TRANSITION_TIME = 500;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-hovercard.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-hovercard tests', () => {
+ let element;
+ let sandbox;
+ // For css animations
+ const TRANSITION_TIME = 500;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('updatePosition', () => {
- // Test that the correct style properties have at least been set.
- element.position = 'bottom';
- element.updatePosition();
- assert.typeOf(element.style.getPropertyValue('left'), 'string');
- assert.typeOf(element.style.getPropertyValue('top'), 'string');
- assert.typeOf(element.style.getPropertyValue('paddingTop'), 'string');
- assert.typeOf(element.style.getPropertyValue('marginTop'), 'string');
-
- const parentRect = document.documentElement.getBoundingClientRect();
- const targetRect = element._target.getBoundingClientRect();
- const thisRect = element.getBoundingClientRect();
-
- const targetLeft = targetRect.left - parentRect.left;
- const targetTop = targetRect.top - parentRect.top;
-
- const pixelCompare = pixel =>
- Math.round(parseInt(pixel.substring(0, pixel.length - 1)), 10);
-
- assert.equal(
- pixelCompare(element.style.left),
- pixelCompare(
- (targetLeft + (targetRect.width - thisRect.width) / 2) + 'px'));
- assert.equal(
- pixelCompare(element.style.top),
- pixelCompare(
- (targetTop + targetRect.height + element.offset) + 'px'));
- });
-
- test('hide', done => {
- element.hide({});
- setTimeout(() => {
- const style = getComputedStyle(element);
- assert.isFalse(element._isShowing);
- assert.isFalse(element.classList.contains('hovered'));
- assert.equal(style.opacity, '0');
- assert.equal(style.visibility, 'hidden');
- assert.notEqual(element.container, Polymer.dom(element).parentNode);
- done();
- }, TRANSITION_TIME);
- });
-
- test('show', done => {
- element.show({});
- setTimeout(() => {
- const style = getComputedStyle(element);
- assert.isTrue(element._isShowing);
- assert.isTrue(element.classList.contains('hovered'));
- assert.equal(style.opacity, '1');
- assert.equal(style.visibility, 'visible');
- done();
- }, TRANSITION_TIME);
- });
-
- test('card shows on enter and hides on leave', done => {
- const button = Polymer.dom(document).querySelector('button');
- assert.isFalse(element._isShowing);
- button.addEventListener('mouseenter', event => {
- assert.isTrue(element._isShowing);
- button.dispatchEvent(new CustomEvent('mouseleave'));
- });
- button.addEventListener('mouseleave', event => {
- assert.isFalse(element._isShowing);
- done();
- });
- button.dispatchEvent(new CustomEvent('mouseenter'));
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('updatePosition', () => {
+ // Test that the correct style properties have at least been set.
+ element.position = 'bottom';
+ element.updatePosition();
+ assert.typeOf(element.style.getPropertyValue('left'), 'string');
+ assert.typeOf(element.style.getPropertyValue('top'), 'string');
+ assert.typeOf(element.style.getPropertyValue('paddingTop'), 'string');
+ assert.typeOf(element.style.getPropertyValue('marginTop'), 'string');
+
+ const parentRect = document.documentElement.getBoundingClientRect();
+ const targetRect = element._target.getBoundingClientRect();
+ const thisRect = element.getBoundingClientRect();
+
+ const targetLeft = targetRect.left - parentRect.left;
+ const targetTop = targetRect.top - parentRect.top;
+
+ const pixelCompare = pixel =>
+ Math.round(parseInt(pixel.substring(0, pixel.length - 1)), 10);
+
+ assert.equal(
+ pixelCompare(element.style.left),
+ pixelCompare(
+ (targetLeft + (targetRect.width - thisRect.width) / 2) + 'px'));
+ assert.equal(
+ pixelCompare(element.style.top),
+ pixelCompare(
+ (targetTop + targetRect.height + element.offset) + 'px'));
+ });
+
+ test('hide', done => {
+ element.hide({});
+ setTimeout(() => {
+ const style = getComputedStyle(element);
+ assert.isFalse(element._isShowing);
+ assert.isFalse(element.classList.contains('hovered'));
+ assert.equal(style.opacity, '0');
+ assert.equal(style.visibility, 'hidden');
+ assert.notEqual(element.container, dom(element).parentNode);
+ done();
+ }, TRANSITION_TIME);
+ });
+
+ test('show', done => {
+ element.show({});
+ setTimeout(() => {
+ const style = getComputedStyle(element);
+ assert.isTrue(element._isShowing);
+ assert.isTrue(element.classList.contains('hovered'));
+ assert.equal(style.opacity, '1');
+ assert.equal(style.visibility, 'visible');
+ done();
+ }, TRANSITION_TIME);
+ });
+
+ test('showDelayed does not show immediately', done => {
+ element.showDelayedBy(100);
+ setTimeout(() => {
+ assert.isFalse(element._isShowing);
+ done();
+ }, 0);
+ });
+
+ test('showDelayed shows after delay', done => {
+ element.showDelayedBy(1);
+ setTimeout(() => {
+ assert.isTrue(element._isShowing);
+ done();
+ }, 10);
+ });
+
+ test('card is scheduled to show on enter and hides on leave', done => {
+ const button = dom(document).querySelector('button');
+ assert.isFalse(element._isShowing);
+ button.addEventListener('mouseenter', event => {
+ assert.isTrue(element._isScheduledToShow);
+ button.dispatchEvent(new CustomEvent('mouseleave'));
+ });
+ button.addEventListener('mouseleave', event => {
+ assert.isFalse(element._isScheduledToShow);
+ assert.isFalse(element._isShowing);
+ done();
+ });
+ button.dispatchEvent(new CustomEvent('mouseenter'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
similarity index 83%
rename from polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
rename to polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
index 2245b98..f7b8b63 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
@@ -1,75 +1,78 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '@polymer/iron-icon/iron-icon.js';
+import '@polymer/iron-iconset-svg/iron-iconset-svg.js';
+const $_documentContainer = document.createElement('template');
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
-<link rel="import" href="/bower_components/iron-iconset-svg/iron-iconset-svg.html">
-
-<iron-iconset-svg name="gr-icons" size="24">
+$_documentContainer.innerHTML = `<iron-iconset-svg name="gr-icons" size="24">
<svg>
<defs>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="expand-less"><path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"/></g>
+ <g id="expand-less"><path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="expand-more"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/></g>
+ <g id="expand-more"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"></path></g>
<!-- This SVG is a copy from material.io https://material.io/icons/#unfold_more -->
- <g id="unfold-more"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 5.83L15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15 12 18.17z"/></g>
+ <g id="unfold-more"><path d="M0 0h24v24H0z" fill="none"></path><path d="M12 5.83L15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15 12 18.17z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="search"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></g>
+ <g id="search"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="settings"><path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/></g>
+ <g id="settings"><path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="create"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></g>
+ <g id="create"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="star"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></g>
+ <g id="star"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="star-border"><path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"/></g>
+ <g id="star-border"><path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="close"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></g>
+ <g id="close"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="chevron-left"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></g>
+ <g id="chevron-left"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="chevron-right"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></g>
+ <g id="chevron-right"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="more-vert"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></g>
+ <g id="more-vert"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="deleteEdit"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></g>
+ <g id="deleteEdit"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/editor-icons.html -->
- <g id="publishEdit"><path d="M5 4v2h14V4H5zm0 10h4v6h6v-6h4l-7-7-7 7z"/></g>
+ <g id="publishEdit"><path d="M5 4v2h14V4H5zm0 10h4v6h6v-6h4l-7-7-7 7z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/editor-icons.html -->
- <g id="delete"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></g>
+ <g id="delete"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="help"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="info"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"></path></g>
<!-- This SVG is a copy from material.io https://material.io/icons/#ic_hourglass_full-->
- <g id="hourglass"><path d="M6 2v6h.01L6 8.01 10 12l-4 4 .01.01H6V22h12v-5.99h-.01L18 16l-4-4 4-3.99-.01-.01H18V2H6z"/><path d="M0 0h24v24H0V0z" fill="none"/></g>
+ <g id="hourglass"><path d="M6 2v6h.01L6 8.01 10 12l-4 4 .01.01H6V22h12v-5.99h-.01L18 16l-4-4 4-3.99-.01-.01H18V2H6z"></path><path d="M0 0h24v24H0V0z" fill="none"></path></g>
<!-- This SVG is a copy from material.io https://material.io/icons/#mode_comment-->
- <g id="comment"><path d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18z"/><path d="M0 0h24v24H0z" fill="none"/></g>
+ <g id="comment"><path d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18z"></path><path d="M0 0h24v24H0z" fill="none"></path></g>
+ <!-- This SVG is a copy from material.io https://material.io/icons/#calendar_today-->
+ <g id="calendar"><path d="M20 3h-1V1h-2v2H7V1H5v2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 18H4V8h16v13z"></path><path d="M0 0h24v24H0z" fill="none"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="error"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="lightbulb-outline"><path d="M9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7zm2.85 11.1l-.85.6V16h-4v-2.3l-.85-.6C7.8 12.16 7 10.63 7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z"></path></g>
<!-- This is a custom PolyGerrit SVG -->
- <g id="side-by-side"><path d="M17.1578947,10.8888889 L2.84210526,10.8888889 C2.37894737,10.8888889 2,11.2888889 2,11.7777778 L2,17.1111111 C2,17.6 2.37894737,18 2.84210526,18 L17.1578947,18 C17.6210526,18 18,17.6 18,17.1111111 L18,11.7777778 C18,11.2888889 17.6210526,10.8888889 17.1578947,10.8888889 Z M17.1578947,2 L2.84210526,2 C2.37894737,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.37894737,9.11111111 2.84210526,9.11111111 L17.1578947,9.11111111 C17.6210526,9.11111111 18,8.71111111 18,8.22222222 L18,2.88888889 C18,2.4 17.6210526,2 17.1578947,2 Z M16.1973628,2 L2.78874238,2 C2.35493407,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.35493407,9.11111111 2.78874238,9.11111111 L16.1973628,9.11111111 C16.6311711,9.11111111 16.9861052,8.71111111 16.9861052,8.22222222 L16.9861052,2.88888889 C16.9861052,2.4 16.6311711,2 16.1973628,2 Z" id="Shape" transform="scale(1.2) translate(10.000000, 10.000000) rotate(-90.000000) translate(-10.000000, -10.000000)"/></g>
+ <g id="side-by-side"><path d="M17.1578947,10.8888889 L2.84210526,10.8888889 C2.37894737,10.8888889 2,11.2888889 2,11.7777778 L2,17.1111111 C2,17.6 2.37894737,18 2.84210526,18 L17.1578947,18 C17.6210526,18 18,17.6 18,17.1111111 L18,11.7777778 C18,11.2888889 17.6210526,10.8888889 17.1578947,10.8888889 Z M17.1578947,2 L2.84210526,2 C2.37894737,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.37894737,9.11111111 2.84210526,9.11111111 L17.1578947,9.11111111 C17.6210526,9.11111111 18,8.71111111 18,8.22222222 L18,2.88888889 C18,2.4 17.6210526,2 17.1578947,2 Z M16.1973628,2 L2.78874238,2 C2.35493407,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.35493407,9.11111111 2.78874238,9.11111111 L16.1973628,9.11111111 C16.6311711,9.11111111 16.9861052,8.71111111 16.9861052,8.22222222 L16.9861052,2.88888889 C16.9861052,2.4 16.6311711,2 16.1973628,2 Z" id="Shape" transform="scale(1.2) translate(10.000000, 10.000000) rotate(-90.000000) translate(-10.000000, -10.000000)"></path></g>
<!-- This is a custom PolyGerrit SVG -->
- <g id="unified"><path d="M4,2 L17,2 C18.1045695,2 19,2.8954305 19,4 L19,16 C19,17.1045695 18.1045695,18 17,18 L4,18 C2.8954305,18 2,17.1045695 2,16 L2,4 L2,4 C2,2.8954305 2.8954305,2 4,2 L4,2 Z M4,7 L4,9 L17,9 L17,7 L4,7 Z M4,11 L4,13 L17,13 L17,11 L4,11 Z" id="Combined-Shape" transform="scale(1.12, 1.2)"/></g>
+ <g id="unified"><path d="M4,2 L17,2 C18.1045695,2 19,2.8954305 19,4 L19,16 C19,17.1045695 18.1045695,18 17,18 L4,18 C2.8954305,18 2,17.1045695 2,16 L2,4 L2,4 C2,2.8954305 2.8954305,2 4,2 L4,2 Z M4,7 L4,9 L17,9 L17,7 L4,7 Z M4,11 L4,13 L17,13 L17,11 L4,11 Z" id="Combined-Shape" transform="scale(1.12, 1.2)"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="content-copy"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></g>
+ <g id="content-copy"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"></path></g>
<!-- This is a custom PolyGerrit SVG -->
- <g id="check"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></g>
+ <g id="check"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"></path></g>
<!-- This is a custom PolyGerrit SVG -->
<g id="robot"><path d="M4.137453,5.61015591 L4.54835569,1.5340419 C4.5717665,1.30180904 4.76724872,1.12504213 5.00065859,1.12504213 C5.23327176,1.12504213 5.42730868,1.30282046 5.44761309,1.53454578 L5.76084628,5.10933916 C6.16304484,5.03749412 6.57714381,5 7,5 L17,5 C20.8659932,5 24,8.13400675 24,12 L24,15.1250421 C24,18.9910354 20.8659932,22.1250421 17,22.1250421 L7,22.1250421 C3.13400675,22.1250421 2.19029351e-15,18.9910354 0,15.1250421 L0,12 C-3.48556243e-16,9.15382228 1.69864167,6.70438358 4.137453,5.61015591 Z M5.77553049,6.12504213 C3.04904264,6.69038358 1,9.10590202 1,12 L1,15.1250421 C1,18.4387506 3.6862915,21.1250421 7,21.1250421 L17,21.1250421 C20.3137085,21.1250421 23,18.4387506 23,15.1250421 L23,12 C23,8.6862915 20.3137085,6 17,6 L7,6 C6.60617231,6 6.2212068,6.03794347 5.84855971,6.11037415 L5.84984496,6.12504213 L5.77553049,6.12504213 Z M6.93003717,6.95027711 L17.1232083,6.95027711 C19.8638332,6.95027711 22.0855486,9.17199258 22.0855486,11.9126175 C22.0855486,14.6532424 19.8638332,16.8749579 17.1232083,16.8749579 L6.93003717,16.8749579 C4.18941226,16.8749579 1.9676968,14.6532424 1.9676968,11.9126175 C1.9676968,9.17199258 4.18941226,6.95027711 6.93003717,6.95027711 Z M7.60124392,14.0779303 C9.03787127,14.0779303 10.2024878,12.9691885 10.2024878,11.6014862 C10.2024878,10.2337839 9.03787127,9.12504213 7.60124392,9.12504213 C6.16461657,9.12504213 5,10.2337839 5,11.6014862 C5,12.9691885 6.16461657,14.0779303 7.60124392,14.0779303 Z M16.617997,14.1098288 C18.0638768,14.1098288 19.2359939,12.9939463 19.2359939,11.6174355 C19.2359939,10.2409246 18.0638768,9.12504213 16.617997,9.12504213 C15.1721172,9.12504213 14,10.2409246 14,11.6174355 C14,12.9939463 15.1721172,14.1098288 16.617997,14.1098288 Z M9.79751216,18.1250421 L15,18.1250421 L15,19.1250421 C15,19.6773269 14.5522847,20.1250421 14,20.1250421 L10.7975122,20.1250421 C10.2452274,20.1250421 9.79751216,19.6773269 9.79751216,19.1250421 L9.79751216,18.1250421 Z"></path></g>
<!-- This is a custom PolyGerrit SVG -->
@@ -90,9 +93,13 @@
<!-- This is a custom PolyGerrit SVG -->
<g id="submit"><path d="M22.23,5 L11.65,15.58 L7.47000001,11.41 L6.06000001,12.82 L11.65,18.41 L23.649,6.41 L22.23,5 Z M16.58,5 L10.239,11.34 L11.65,12.75 L17.989,6.41 L16.58,5 Z M0.400000006,12.82 L5.99000001,18.41 L7.40000001,17 L1.82000001,11.41 L0.400000006,12.82 Z"></path></g>
<!-- This is a custom PolyGerrit SVG -->
- <g id="review"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></g>
+ <g id="review"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"></path></g>
<!-- This is a custom PolyGerrit SVG -->
- <g id="zeroState"><path d="M22 9V7h-2V5c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-2h2v-2h-2v-2h2v-2h-2V9h2zm-4 10H4V5h14v14zM6 13h5v4H6zm6-6h4v3h-4zM6 7h5v5H6zm6 4h4v6h-4z"/></g>
+ <g id="zeroState"><path d="M22 9V7h-2V5c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-2h2v-2h-2v-2h2v-2h-2V9h2zm-4 10H4V5h14v14zM6 13h5v4H6zm6-6h4v3h-4zM6 7h5v5H6zm6 4h4v6h-4z"></path></g>
+ <!-- This SVG is an adaptation of material.io https://material.io/icons/#label_important-->
+ <g id="attention"><path d="M5.5 19 l9 0 c.67 0 1.27 -.33 1.63 -.84 L20.5 12 l-4.37 -6.16 c-.36 -.51 -.96 -.84 -1.63 -.84 l-9 0 L9 12 z"></path></g>
</defs>
</svg>
-</iron-iconset-svg>
+</iron-iconset-svg>`;
+
+document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
index ea5740f..c4c9043 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
@@ -19,17 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-annotation-actions-context</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../diff/gr-diff-highlight/gr-annotation.js"></script>
-
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-js-api-interface.html"/>
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -37,68 +30,70 @@
</template>
</test-fixture>
-<script>
- suite('gr-annotation-actions-context tests', async () => {
- await readyToTest();
- let instance;
- let sandbox;
- let el;
- let lineNumberEl;
- let plugin;
+<script type="module">
+import '../../diff/gr-diff-highlight/gr-annotation.js';
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+suite('gr-annotation-actions-context tests', () => {
+ let instance;
+ let sandbox;
+ let el;
+ let lineNumberEl;
+ let plugin;
- setup(() => {
- sandbox = sinon.sandbox.create();
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
- const str = 'lorem ipsum blah blah';
- const line = {text: str};
- el = document.createElement('div');
- el.textContent = str;
- el.setAttribute('data-side', 'right');
- lineNumberEl = document.createElement('td');
- lineNumberEl.classList.add('right');
- document.body.appendChild(el);
- instance = new GrAnnotationActionsContext(
- el, lineNumberEl, line, 'dummy/path', '123', '1');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('test annotateRange', () => {
- const annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement');
- const start = 0;
- const end = 100;
- const cssStyleObject = plugin.styles().css('background-color: #000000');
-
- // Assert annotateElement is not called when side is different.
- instance.annotateRange(start, end, cssStyleObject, 'left');
- assert.equal(annotateElementSpy.callCount, 0);
-
- // Assert annotateElement is called once when side is the same.
- instance.annotateRange(start, end, cssStyleObject, 'right');
- assert.equal(annotateElementSpy.callCount, 1);
- const args = annotateElementSpy.getCalls()[0].args;
- assert.equal(args[0], el);
- assert.equal(args[1], start);
- assert.equal(args[2], end);
- assert.equal(args[3], cssStyleObject.getClassName(el));
- });
-
- test('test annotateLineNumber', () => {
- const cssStyleObject = plugin.styles().css('background-color: #000000');
-
- const className = cssStyleObject.getClassName(lineNumberEl);
-
- // Assert that css class is *not* applied when side is different.
- instance.annotateLineNumber(cssStyleObject, 'left');
- assert.isFalse(lineNumberEl.classList.contains(className));
-
- // Assert that css class is applied when side is the same.
- instance.annotateLineNumber(cssStyleObject, 'right');
- assert.isTrue(lineNumberEl.classList.contains(className));
- });
+ const str = 'lorem ipsum blah blah';
+ const line = {text: str};
+ el = document.createElement('div');
+ el.textContent = str;
+ el.setAttribute('data-side', 'right');
+ lineNumberEl = document.createElement('td');
+ lineNumberEl.classList.add('right');
+ document.body.appendChild(el);
+ instance = new GrAnnotationActionsContext(
+ el, lineNumberEl, line, 'dummy/path', '123', '1');
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('test annotateRange', () => {
+ const annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+ const start = 0;
+ const end = 100;
+ const cssStyleObject = plugin.styles().css('background-color: #000000');
+
+ // Assert annotateElement is not called when side is different.
+ instance.annotateRange(start, end, cssStyleObject, 'left');
+ assert.equal(annotateElementSpy.callCount, 0);
+
+ // Assert annotateElement is called once when side is the same.
+ instance.annotateRange(start, end, cssStyleObject, 'right');
+ assert.equal(annotateElementSpy.callCount, 1);
+ const args = annotateElementSpy.getCalls()[0].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], start);
+ assert.equal(args[2], end);
+ assert.equal(args[3], cssStyleObject.getClassName(el));
+ });
+
+ test('test annotateLineNumber', () => {
+ const cssStyleObject = plugin.styles().css('background-color: #000000');
+
+ const className = cssStyleObject.getClassName(lineNumberEl);
+
+ // Assert that css class is *not* applied when side is different.
+ instance.annotateLineNumber(cssStyleObject, 'left');
+ assert.isFalse(lineNumberEl.classList.contains(className));
+
+ // Assert that css class is applied when side is the same.
+ instance.annotateLineNumber(cssStyleObject, 'right');
+ assert.isTrue(lineNumberEl.classList.contains(className));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
index b8c4f83..13aa6f4 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
@@ -19,14 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-annotation-actions-js-api-js-api</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="../../change/gr-change-actions/gr-change-actions.html">
-
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
<span hidden id="annotation-span">
@@ -38,153 +34,154 @@
</template>
</test-fixture>
-<script>
- suite('gr-annotation-actions-js-api tests', async () => {
- await readyToTest();
- let annotationActions;
- let sandbox;
- let plugin;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../change/gr-change-actions/gr-change-actions.js';
+suite('gr-annotation-actions-js-api tests', () => {
+ let annotationActions;
+ let sandbox;
+ let plugin;
- setup(() => {
- sandbox = sinon.sandbox.create();
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- annotationActions = plugin.annotationApi();
- });
-
- teardown(() => {
- annotationActions = null;
- sandbox.restore();
- });
-
- test('add/get layer', () => {
- const str = 'lorem ipsum blah blah';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const changeNum = 1234;
- const patchNum = 2;
- let testLayerFuncCalled = false;
-
- const testLayerFunc = context => {
- testLayerFuncCalled = true;
- assert.equal(context.line, line);
- assert.equal(context.changeNum, changeNum);
- assert.equal(context.patchNum, 2);
- };
- annotationActions.addLayer(testLayerFunc);
-
- const annotationLayer = annotationActions.getLayer(
- '/dummy/path', changeNum, patchNum);
-
- const lineNumberEl = document.createElement('td');
- annotationLayer.annotate(el, lineNumberEl, line);
- assert.isTrue(testLayerFuncCalled);
- });
-
- test('add notifier', () => {
- const path1 = '/dummy/path1';
- const path2 = '/dummy/path2';
- const annotationLayer1 = annotationActions.getLayer(path1, 1, 2);
- const annotationLayer2 = annotationActions.getLayer(path2, 1, 2);
- const layer1Spy = sandbox.spy(annotationLayer1, 'notifyListeners');
- const layer2Spy = sandbox.spy(annotationLayer2, 'notifyListeners');
-
- let notify;
- let notifyFuncCalled;
- const notifyFunc = n => {
- notifyFuncCalled = true;
- notify = n;
- };
- annotationActions.addNotifier(notifyFunc);
- assert.isTrue(notifyFuncCalled);
-
- // Assert that no layers are invoked with a different path.
- notify('/dummy/path3', 0, 10, 'right');
- assert.isFalse(layer1Spy.called);
- assert.isFalse(layer2Spy.called);
-
- // Assert that only the 1st layer is invoked with path1.
- notify(path1, 0, 10, 'right');
- assert.isTrue(layer1Spy.called);
- assert.isFalse(layer2Spy.called);
-
- // Reset spies.
- layer1Spy.reset();
- layer2Spy.reset();
-
- // Assert that only the 2nd layer is invoked with path2.
- notify(path2, 0, 20, 'left');
- assert.isFalse(layer1Spy.called);
- assert.isTrue(layer2Spy.called);
- });
-
- test('toggle checkbox', () => {
- const fakeEl = {content: fixture('basic')};
- const hookStub = {onAttached: sandbox.stub()};
- sandbox.stub(plugin, 'hook').returns(hookStub);
-
- let checkbox;
- let onAttachedFuncCalled = false;
- const onAttachedFunc = c => {
- checkbox = c;
- onAttachedFuncCalled = true;
- };
- annotationActions.enableToggleCheckbox('test label', onAttachedFunc);
- const emulateAttached = () => hookStub.onAttached.callArgWith(0, fakeEl);
- emulateAttached();
-
- // Assert that onAttachedFunc is called and HTML elements have the
- // expected state.
- assert.isTrue(onAttachedFuncCalled);
- assert.equal(checkbox.id, 'annotation-checkbox');
- assert.isTrue(checkbox.disabled);
- assert.equal(document.getElementById('annotation-label').textContent,
- 'test label');
- assert.isFalse(document.getElementById('annotation-span').hidden);
-
- // Assert that error is shown if we try to enable checkbox again.
- onAttachedFuncCalled = false;
- annotationActions.enableToggleCheckbox('test label2', onAttachedFunc);
- const errorStub = sandbox.stub(
- console, 'error', (msg, err) => undefined);
- emulateAttached();
- assert.isTrue(
- errorStub.calledWith(
- 'annotation-span is already enabled. Cannot re-enable.'));
- // Assert that onAttachedFunc is not called and the label has not changed.
- assert.isFalse(onAttachedFuncCalled);
- assert.equal(document.getElementById('annotation-label').textContent,
- 'test label');
- });
-
- test('layer notify listeners', () => {
- const annotationLayer = annotationActions.getLayer(
- '/dummy/path', 1, 2);
- let listenerCalledTimes = 0;
- const startRange = 10;
- const endRange = 20;
- const side = 'right';
- const listener = (st, end, s) => {
- listenerCalledTimes++;
- assert.equal(st, startRange);
- assert.equal(end, endRange);
- assert.equal(s, side);
- };
-
- // Notify with 0 listeners added.
- annotationLayer.notifyListeners(startRange, endRange, side);
- assert.equal(listenerCalledTimes, 0);
-
- // Add 1 listener.
- annotationLayer.addListener(listener);
- annotationLayer.notifyListeners(startRange, endRange, side);
- assert.equal(listenerCalledTimes, 1);
-
- // Add 1 more listener. Total 2 listeners.
- annotationLayer.addListener(listener);
- annotationLayer.notifyListeners(startRange, endRange, side);
- assert.equal(listenerCalledTimes, 3);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ annotationActions = plugin.annotationApi();
});
+
+ teardown(() => {
+ annotationActions = null;
+ sandbox.restore();
+ });
+
+ test('add/get layer', () => {
+ const str = 'lorem ipsum blah blah';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const changeNum = 1234;
+ const patchNum = 2;
+ let testLayerFuncCalled = false;
+
+ const testLayerFunc = context => {
+ testLayerFuncCalled = true;
+ assert.equal(context.line, line);
+ assert.equal(context.changeNum, changeNum);
+ assert.equal(context.patchNum, 2);
+ };
+ annotationActions.addLayer(testLayerFunc);
+
+ const annotationLayer = annotationActions.getLayer(
+ '/dummy/path', changeNum, patchNum);
+
+ const lineNumberEl = document.createElement('td');
+ annotationLayer.annotate(el, lineNumberEl, line);
+ assert.isTrue(testLayerFuncCalled);
+ });
+
+ test('add notifier', () => {
+ const path1 = '/dummy/path1';
+ const path2 = '/dummy/path2';
+ const annotationLayer1 = annotationActions.getLayer(path1, 1, 2);
+ const annotationLayer2 = annotationActions.getLayer(path2, 1, 2);
+ const layer1Spy = sandbox.spy(annotationLayer1, 'notifyListeners');
+ const layer2Spy = sandbox.spy(annotationLayer2, 'notifyListeners');
+
+ let notify;
+ let notifyFuncCalled;
+ const notifyFunc = n => {
+ notifyFuncCalled = true;
+ notify = n;
+ };
+ annotationActions.addNotifier(notifyFunc);
+ assert.isTrue(notifyFuncCalled);
+
+ // Assert that no layers are invoked with a different path.
+ notify('/dummy/path3', 0, 10, 'right');
+ assert.isFalse(layer1Spy.called);
+ assert.isFalse(layer2Spy.called);
+
+ // Assert that only the 1st layer is invoked with path1.
+ notify(path1, 0, 10, 'right');
+ assert.isTrue(layer1Spy.called);
+ assert.isFalse(layer2Spy.called);
+
+ // Reset spies.
+ layer1Spy.reset();
+ layer2Spy.reset();
+
+ // Assert that only the 2nd layer is invoked with path2.
+ notify(path2, 0, 20, 'left');
+ assert.isFalse(layer1Spy.called);
+ assert.isTrue(layer2Spy.called);
+ });
+
+ test('toggle checkbox', () => {
+ const fakeEl = {content: fixture('basic')};
+ const hookStub = {onAttached: sandbox.stub()};
+ sandbox.stub(plugin, 'hook').returns(hookStub);
+
+ let checkbox;
+ let onAttachedFuncCalled = false;
+ const onAttachedFunc = c => {
+ checkbox = c;
+ onAttachedFuncCalled = true;
+ };
+ annotationActions.enableToggleCheckbox('test label', onAttachedFunc);
+ const emulateAttached = () => hookStub.onAttached.callArgWith(0, fakeEl);
+ emulateAttached();
+
+ // Assert that onAttachedFunc is called and HTML elements have the
+ // expected state.
+ assert.isTrue(onAttachedFuncCalled);
+ assert.equal(checkbox.id, 'annotation-checkbox');
+ assert.isTrue(checkbox.disabled);
+ assert.equal(document.getElementById('annotation-label').textContent,
+ 'test label');
+ assert.isFalse(document.getElementById('annotation-span').hidden);
+
+ // Assert that error is shown if we try to enable checkbox again.
+ onAttachedFuncCalled = false;
+ annotationActions.enableToggleCheckbox('test label2', onAttachedFunc);
+ const errorStub = sandbox.stub(
+ console, 'error', (msg, err) => undefined);
+ emulateAttached();
+ assert.isTrue(
+ errorStub.calledWith(
+ 'annotation-span is already enabled. Cannot re-enable.'));
+ // Assert that onAttachedFunc is not called and the label has not changed.
+ assert.isFalse(onAttachedFuncCalled);
+ assert.equal(document.getElementById('annotation-label').textContent,
+ 'test label');
+ });
+
+ test('layer notify listeners', () => {
+ const annotationLayer = annotationActions.getLayer(
+ '/dummy/path', 1, 2);
+ let listenerCalledTimes = 0;
+ const startRange = 10;
+ const endRange = 20;
+ const side = 'right';
+ const listener = (st, end, s) => {
+ listenerCalledTimes++;
+ assert.equal(st, startRange);
+ assert.equal(end, endRange);
+ assert.equal(s, side);
+ };
+
+ // Notify with 0 listeners added.
+ annotationLayer.notifyListeners(startRange, endRange, side);
+ assert.equal(listenerCalledTimes, 0);
+
+ // Add 1 listener.
+ annotationLayer.addListener(listener);
+ annotationLayer.notifyListeners(startRange, endRange, side);
+ assert.equal(listenerCalledTimes, 1);
+
+ // Add 1 more listener. Total 2 listeners.
+ annotationLayer.addListener(listener);
+ annotationLayer.notifyListeners(startRange, endRange, side);
+ assert.equal(listenerCalledTimes, 3);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
index d70a8d2..9586ccb 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
@@ -19,70 +19,66 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-api-interface</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-js-api-interface.html">
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+const PRELOADED_PROTOCOL = 'preloaded:';
-<script>
- const PRELOADED_PROTOCOL = 'preloaded:';
+suite('gr-api-utils tests', () => {
+ suite('test getPluginNameFromUrl', () => {
+ const {getPluginNameFromUrl} = window._apiUtils;
- suite('gr-api-utils tests', async () => {
- await readyToTest();
- suite('test getPluginNameFromUrl', () => {
- const {getPluginNameFromUrl} = window._apiUtils;
+ test('with empty string', () => {
+ assert.equal(getPluginNameFromUrl(''), null);
+ });
- test('with empty string', () => {
- assert.equal(getPluginNameFromUrl(''), null);
- });
+ test('with invalid url', () => {
+ assert.equal(getPluginNameFromUrl('test'), null);
+ });
- test('with invalid url', () => {
- assert.equal(getPluginNameFromUrl('test'), null);
- });
+ test('with random invalid url', () => {
+ assert.equal(getPluginNameFromUrl('http://example.com'), null);
+ assert.equal(
+ getPluginNameFromUrl('http://example.com/static/a.html'),
+ null
+ );
+ });
- test('with random invalid url', () => {
- assert.equal(getPluginNameFromUrl('http://example.com'), null);
- assert.equal(
- getPluginNameFromUrl('http://example.com/static/a.html'),
- null
- );
- });
+ test('with valid urls', () => {
+ assert.equal(
+ getPluginNameFromUrl('http://example.com/plugins/a.html'),
+ 'a'
+ );
+ assert.equal(
+ getPluginNameFromUrl('http://example.com/plugins/a/static/t.html'),
+ 'a'
+ );
+ });
- test('with valid urls', () => {
- assert.equal(
- getPluginNameFromUrl('http://example.com/plugins/a.html'),
- 'a'
- );
- assert.equal(
- getPluginNameFromUrl('http://example.com/plugins/a/static/t.html'),
- 'a'
- );
- });
+ test('with preloaded urls', () => {
+ assert.equal(getPluginNameFromUrl(`${PRELOADED_PROTOCOL}a`), 'a');
+ });
- test('with preloaded urls', () => {
- assert.equal(getPluginNameFromUrl(`${PRELOADED_PROTOCOL}a`), 'a');
- });
+ test('with gerrit-theme override', () => {
+ assert.equal(
+ getPluginNameFromUrl('http://example.com/static/gerrit-theme.html'),
+ 'gerrit-theme'
+ );
+ });
- test('with gerrit-theme override', () => {
- assert.equal(
- getPluginNameFromUrl('http://example.com/static/gerrit-theme.html'),
- 'gerrit-theme'
- );
- });
-
- test('with ASSETS_PATH', () => {
- window.ASSETS_PATH = 'http://cdn.com/2';
- assert.equal(
- getPluginNameFromUrl(`${window.ASSETS_PATH}/plugins/a.html`),
- 'a'
- );
- window.ASSETS_PATH = undefined;
- });
+ test('with ASSETS_PATH', () => {
+ window.ASSETS_PATH = 'http://cdn.com/2';
+ assert.equal(
+ getPluginNameFromUrl(`${window.ASSETS_PATH}/plugins/a.html`),
+ 'a'
+ );
+ window.ASSETS_PATH = undefined;
});
});
-</script>
\ No newline at end of file
+});
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
index 91e1a49..b5cbd97 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -19,19 +19,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-actions-js-api</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<!--
This must refer to the element this interface is wrapping around. Otherwise
breaking changes to gr-change-actions won’t be noticed.
-->
-<link rel="import" href="../../change/gr-change-actions/gr-change-actions.html">
-
-<script>void(0);</script>
<test-fixture id="basic">
<template>
@@ -39,191 +34,193 @@
</template>
</test-fixture>
-<script>
- suite('gr-js-api-interface tests', async () => {
- await readyToTest();
- let element;
- let changeActions;
- let plugin;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../change/gr-change-actions/gr-change-actions.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-js-api-interface tests', () => {
+ let element;
+ let changeActions;
+ let plugin;
- // Because deepEqual doesn’t behave in Safari.
- function assertArraysEqual(actual, expected) {
- assert.equal(actual.length, expected.length);
- for (let i = 0; i < actual.length; i++) {
- assert.equal(actual[i], expected[i]);
- }
+ // Because deepEqual doesn’t behave in Safari.
+ function assertArraysEqual(actual, expected) {
+ assert.equal(actual.length, expected.length);
+ for (let i = 0; i < actual.length; i++) {
+ assert.equal(actual[i], expected[i]);
}
+ }
- suite('early init', () => {
- setup(() => {
- Gerrit._testOnly_resetPlugins();
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- // Mimic all plugins loaded.
- Gerrit._loadPlugins([]);
- changeActions = plugin.changeActions();
- element = fixture('basic');
+ suite('early init', () => {
+ setup(() => {
+ Gerrit._testOnly_resetPlugins();
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ // Mimic all plugins loaded.
+ Gerrit._loadPlugins([]);
+ changeActions = plugin.changeActions();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ changeActions = null;
+ Gerrit._testOnly_resetPlugins();
+ });
+
+ test('does not throw', ()=> {
+ assert.doesNotThrow(() => {
+ changeActions.add('change', 'foo');
});
+ });
+ });
- teardown(() => {
- changeActions = null;
- Gerrit._testOnly_resetPlugins();
- });
+ suite('normal init', () => {
+ setup(() => {
+ Gerrit._testOnly_resetPlugins();
+ element = fixture('basic');
+ sinon.stub(element, '_editStatusChanged');
+ element.change = {};
+ element._hasKnownChainState = false;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ changeActions = plugin.changeActions();
+ // Mimic all plugins loaded.
+ Gerrit._loadPlugins([]);
+ });
- test('does not throw', ()=> {
- assert.doesNotThrow(() => {
- changeActions.add('change', 'foo');
+ teardown(() => {
+ changeActions = null;
+ Gerrit._testOnly_resetPlugins();
+ });
+
+ test('property existence', () => {
+ const properties = [
+ 'ActionType',
+ 'ChangeActions',
+ 'RevisionActions',
+ ];
+ for (const p of properties) {
+ assertArraysEqual(changeActions[p], element[p]);
+ }
+ });
+
+ test('add/remove primary action keys', () => {
+ element.primaryActionKeys = [];
+ changeActions.addPrimaryActionKey('foo');
+ assertArraysEqual(element.primaryActionKeys, ['foo']);
+ changeActions.addPrimaryActionKey('foo');
+ assertArraysEqual(element.primaryActionKeys, ['foo']);
+ changeActions.addPrimaryActionKey('bar');
+ assertArraysEqual(element.primaryActionKeys, ['foo', 'bar']);
+ changeActions.removePrimaryActionKey('foo');
+ assertArraysEqual(element.primaryActionKeys, ['bar']);
+ changeActions.removePrimaryActionKey('baz');
+ assertArraysEqual(element.primaryActionKeys, ['bar']);
+ changeActions.removePrimaryActionKey('bar');
+ assertArraysEqual(element.primaryActionKeys, []);
+ });
+
+ test('action buttons', done => {
+ const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+ const handler = sinon.spy();
+ changeActions.addTapListener(key, handler);
+ flush(() => {
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('[data-action-key="' + key + '"]'));
+ assert(handler.calledOnce);
+ changeActions.removeTapListener(key, handler);
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('[data-action-key="' + key + '"]'));
+ assert(handler.calledOnce);
+ changeActions.remove(key);
+ flush(() => {
+ assert.isNull(element.shadowRoot
+ .querySelector('[data-action-key="' + key + '"]'));
+ done();
});
});
});
- suite('normal init', () => {
- setup(() => {
- Gerrit._testOnly_resetPlugins();
- element = fixture('basic');
- sinon.stub(element, '_editStatusChanged');
- element.change = {};
- element._hasKnownChainState = false;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- changeActions = plugin.changeActions();
- // Mimic all plugins loaded.
- Gerrit._loadPlugins([]);
- });
-
- teardown(() => {
- changeActions = null;
- Gerrit._testOnly_resetPlugins();
- });
-
- test('property existence', () => {
- const properties = [
- 'ActionType',
- 'ChangeActions',
- 'RevisionActions',
- ];
- for (const p of properties) {
- assertArraysEqual(changeActions[p], element[p]);
- }
- });
-
- test('add/remove primary action keys', () => {
- element.primaryActionKeys = [];
- changeActions.addPrimaryActionKey('foo');
- assertArraysEqual(element.primaryActionKeys, ['foo']);
- changeActions.addPrimaryActionKey('foo');
- assertArraysEqual(element.primaryActionKeys, ['foo']);
- changeActions.addPrimaryActionKey('bar');
- assertArraysEqual(element.primaryActionKeys, ['foo', 'bar']);
- changeActions.removePrimaryActionKey('foo');
- assertArraysEqual(element.primaryActionKeys, ['bar']);
- changeActions.removePrimaryActionKey('baz');
- assertArraysEqual(element.primaryActionKeys, ['bar']);
- changeActions.removePrimaryActionKey('bar');
- assertArraysEqual(element.primaryActionKeys, []);
- });
-
- test('action buttons', done => {
- const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
- const handler = sinon.spy();
- changeActions.addTapListener(key, handler);
+ test('action button properties', done => {
+ const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+ flush(() => {
+ const button = element.shadowRoot
+ .querySelector('[data-action-key="' + key + '"]');
+ assert.isOk(button);
+ assert.equal(button.getAttribute('data-label'), 'Bork!');
+ assert.isNotOk(button.disabled);
+ changeActions.setLabel(key, 'Yo');
+ changeActions.setTitle(key, 'Yo hint');
+ changeActions.setEnabled(key, false);
+ changeActions.setIcon(key, 'pupper');
flush(() => {
- MockInteractions.tap(element.shadowRoot
- .querySelector('[data-action-key="' + key + '"]'));
- assert(handler.calledOnce);
- changeActions.removeTapListener(key, handler);
- MockInteractions.tap(element.shadowRoot
- .querySelector('[data-action-key="' + key + '"]'));
- assert(handler.calledOnce);
- changeActions.remove(key);
- flush(() => {
- assert.isNull(element.shadowRoot
- .querySelector('[data-action-key="' + key + '"]'));
- done();
- });
+ assert.equal(button.getAttribute('data-label'), 'Yo');
+ assert.equal(button.getAttribute('title'), 'Yo hint');
+ assert.isTrue(button.disabled);
+ assert.equal(dom(button).querySelector('iron-icon').icon,
+ 'gr-icons:pupper');
+ done();
});
});
+ });
- test('action button properties', done => {
- const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+ test('hide action buttons', done => {
+ const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+ flush(() => {
+ const button = element.shadowRoot
+ .querySelector('[data-action-key="' + key + '"]');
+ assert.isOk(button);
+ assert.isFalse(button.hasAttribute('hidden'));
+ changeActions.setActionHidden(
+ changeActions.ActionType.REVISION, key, true);
flush(() => {
const button = element.shadowRoot
.querySelector('[data-action-key="' + key + '"]');
- assert.isOk(button);
- assert.equal(button.getAttribute('data-label'), 'Bork!');
- assert.isNotOk(button.disabled);
- changeActions.setLabel(key, 'Yo');
- changeActions.setTitle(key, 'Yo hint');
- changeActions.setEnabled(key, false);
- changeActions.setIcon(key, 'pupper');
- flush(() => {
- assert.equal(button.getAttribute('data-label'), 'Yo');
- assert.equal(button.getAttribute('title'), 'Yo hint');
- assert.isTrue(button.disabled);
- assert.equal(Polymer.dom(button).querySelector('iron-icon').icon,
- 'gr-icons:pupper');
- done();
- });
+ assert.isNotOk(button);
+ done();
});
});
+ });
- test('hide action buttons', done => {
- const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+ test('move action button to overflow', done => {
+ const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+ flush(() => {
+ assert.isTrue(element.$.moreActions.hidden);
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="' + key + '"]'));
+ changeActions.setActionOverflow(
+ changeActions.ActionType.REVISION, key, true);
flush(() => {
- const button = element.shadowRoot
- .querySelector('[data-action-key="' + key + '"]');
- assert.isOk(button);
- assert.isFalse(button.hasAttribute('hidden'));
- changeActions.setActionHidden(
- changeActions.ActionType.REVISION, key, true);
- flush(() => {
- const button = element.shadowRoot
- .querySelector('[data-action-key="' + key + '"]');
- assert.isNotOk(button);
- done();
- });
- });
- });
-
- test('move action button to overflow', done => {
- const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
- flush(() => {
- assert.isTrue(element.$.moreActions.hidden);
- assert.isOk(element.shadowRoot
+ assert.isNotOk(element.shadowRoot
.querySelector('[data-action-key="' + key + '"]'));
- changeActions.setActionOverflow(
- changeActions.ActionType.REVISION, key, true);
- flush(() => {
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="' + key + '"]'));
- assert.isFalse(element.$.moreActions.hidden);
- assert.strictEqual(element.$.moreActions.items[0].name, 'Bork!');
- done();
- });
+ assert.isFalse(element.$.moreActions.hidden);
+ assert.strictEqual(element.$.moreActions.items[0].name, 'Bork!');
+ done();
});
});
+ });
- test('change actions priority', done => {
- const key1 =
- changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
- const key2 =
- changeActions.add(changeActions.ActionType.CHANGE, 'Squanch?');
+ test('change actions priority', done => {
+ const key1 =
+ changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+ const key2 =
+ changeActions.add(changeActions.ActionType.CHANGE, 'Squanch?');
+ flush(() => {
+ let buttons =
+ dom(element.root).querySelectorAll('[data-action-key]');
+ assert.equal(buttons[0].getAttribute('data-action-key'), key1);
+ assert.equal(buttons[1].getAttribute('data-action-key'), key2);
+ changeActions.setActionPriority(
+ changeActions.ActionType.REVISION, key1, 10);
flush(() => {
- let buttons =
- Polymer.dom(element.root).querySelectorAll('[data-action-key]');
- assert.equal(buttons[0].getAttribute('data-action-key'), key1);
- assert.equal(buttons[1].getAttribute('data-action-key'), key2);
- changeActions.setActionPriority(
- changeActions.ActionType.REVISION, key1, 10);
- flush(() => {
- buttons =
- Polymer.dom(element.root).querySelectorAll('[data-action-key]');
- assert.equal(buttons[0].getAttribute('data-action-key'), key2);
- assert.equal(buttons[1].getAttribute('data-action-key'), key1);
- done();
- });
+ buttons =
+ dom(element.root).querySelectorAll('[data-action-key]');
+ assert.equal(buttons[0].getAttribute('data-action-key'), key2);
+ assert.equal(buttons[1].getAttribute('data-action-key'), key1);
+ done();
});
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
index 3147746..edc31b8 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
@@ -19,19 +19,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-reply-js-api</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<!--
This must refer to the element this interface is wrapping around. Otherwise
breaking changes to gr-reply-dialog won’t be noticed.
-->
-<link rel="import" href="../../change/gr-reply-dialog/gr-reply-dialog.html">
-
-<script>void(0);</script>
<test-fixture id="basic">
<template>
@@ -39,86 +34,87 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-reply-js-api tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let changeReply;
- let plugin;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../change/gr-reply-dialog/gr-reply-dialog.js';
+suite('gr-change-reply-js-api tests', () => {
+ let element;
+ let sandbox;
+ let changeReply;
+ let plugin;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getAccount() { return Promise.resolve(null); },
+ });
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('early init', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getAccount() { return Promise.resolve(null); },
- });
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ changeReply = plugin.changeReply();
+ element = fixture('basic');
});
teardown(() => {
- sandbox.restore();
+ changeReply = null;
});
- suite('early init', () => {
- setup(() => {
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- changeReply = plugin.changeReply();
- element = fixture('basic');
- });
+ test('works', () => {
+ sandbox.stub(element, 'getLabelValue').returns('+123');
+ assert.equal(changeReply.getLabelValue('My-Label'), '+123');
- teardown(() => {
- changeReply = null;
- });
+ sandbox.stub(element, 'setLabelValue');
+ changeReply.setLabelValue('My-Label', '+1337');
+ assert.isTrue(
+ element.setLabelValue.calledWithExactly('My-Label', '+1337'));
- test('works', () => {
- sandbox.stub(element, 'getLabelValue').returns('+123');
- assert.equal(changeReply.getLabelValue('My-Label'), '+123');
+ sandbox.stub(element, 'send');
+ changeReply.send(false);
+ assert.isTrue(element.send.calledWithExactly(false));
- sandbox.stub(element, 'setLabelValue');
- changeReply.setLabelValue('My-Label', '+1337');
- assert.isTrue(
- element.setLabelValue.calledWithExactly('My-Label', '+1337'));
-
- sandbox.stub(element, 'send');
- changeReply.send(false);
- assert.isTrue(element.send.calledWithExactly(false));
-
- sandbox.stub(element, 'setPluginMessage');
- changeReply.showMessage('foobar');
- assert.isTrue(element.setPluginMessage.calledWithExactly('foobar'));
- });
- });
-
- suite('normal init', () => {
- setup(() => {
- element = fixture('basic');
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- changeReply = plugin.changeReply();
- });
-
- teardown(() => {
- changeReply = null;
- });
-
- test('works', () => {
- sandbox.stub(element, 'getLabelValue').returns('+123');
- assert.equal(changeReply.getLabelValue('My-Label'), '+123');
-
- sandbox.stub(element, 'setLabelValue');
- changeReply.setLabelValue('My-Label', '+1337');
- assert.isTrue(
- element.setLabelValue.calledWithExactly('My-Label', '+1337'));
-
- sandbox.stub(element, 'send');
- changeReply.send(false);
- assert.isTrue(element.send.calledWithExactly(false));
-
- sandbox.stub(element, 'setPluginMessage');
- changeReply.showMessage('foobar');
- assert.isTrue(element.setPluginMessage.calledWithExactly('foobar'));
- });
+ sandbox.stub(element, 'setPluginMessage');
+ changeReply.showMessage('foobar');
+ assert.isTrue(element.setPluginMessage.calledWithExactly('foobar'));
});
});
+
+ suite('normal init', () => {
+ setup(() => {
+ element = fixture('basic');
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ changeReply = plugin.changeReply();
+ });
+
+ teardown(() => {
+ changeReply = null;
+ });
+
+ test('works', () => {
+ sandbox.stub(element, 'getLabelValue').returns('+123');
+ assert.equal(changeReply.getLabelValue('My-Label'), '+123');
+
+ sandbox.stub(element, 'setLabelValue');
+ changeReply.setLabelValue('My-Label', '+1337');
+ assert.isTrue(
+ element.setLabelValue.calledWithExactly('My-Label', '+1337'));
+
+ sandbox.stub(element, 'send');
+ changeReply.send(false);
+ assert.isTrue(element.send.calledWithExactly(false));
+
+ sandbox.stub(element, 'setPluginMessage');
+ changeReply.showMessage('foobar');
+ assert.isTrue(element.setPluginMessage.calledWithExactly('foobar'));
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html
index ee95a5e..d289368 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-api-interface</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-js-api-interface.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,68 +30,69 @@
</template>
</test-fixture>
-<script>
- suite('gr-gerrit tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let sendStub;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+suite('gr-gerrit tests', () => {
+ let element;
+ let sandbox;
+ let sendStub;
- setup(() => {
- window.clock = sinon.useFakeTimers();
- sandbox = sinon.sandbox.create();
- sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
- stub('gr-rest-api-interface', {
- getAccount() {
- return Promise.resolve({name: 'Judy Hopps'});
- },
- send(...args) {
- return sendStub(...args);
- },
- });
- element = fixture('basic');
+ setup(() => {
+ window.clock = sinon.useFakeTimers();
+ sandbox = sinon.sandbox.create();
+ sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
+ stub('gr-rest-api-interface', {
+ getAccount() {
+ return Promise.resolve({name: 'Judy Hopps'});
+ },
+ send(...args) {
+ return sendStub(...args);
+ },
+ });
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ window.clock.restore();
+ sandbox.restore();
+ element._removeEventCallbacks();
+ Gerrit._testOnly_resetPlugins();
+ });
+
+ suite('proxy methods', () => {
+ test('Gerrit._isPluginEnabled proxy to pluginLoader', () => {
+ const stubFn = sandbox.stub();
+ sandbox.stub(
+ Gerrit._pluginLoader,
+ 'isPluginEnabled',
+ (...args) => stubFn(...args)
+ );
+ Gerrit._isPluginEnabled('test_plugin');
+ assert.isTrue(stubFn.calledWith('test_plugin'));
});
- teardown(() => {
- window.clock.restore();
- sandbox.restore();
- element._removeEventCallbacks();
- Gerrit._testOnly_resetPlugins();
+ test('Gerrit._isPluginLoaded proxy to pluginLoader', () => {
+ const stubFn = sandbox.stub();
+ sandbox.stub(
+ Gerrit._pluginLoader,
+ 'isPluginLoaded',
+ (...args) => stubFn(...args)
+ );
+ Gerrit._isPluginLoaded('test_plugin');
+ assert.isTrue(stubFn.calledWith('test_plugin'));
});
- suite('proxy methods', () => {
- test('Gerrit._isPluginEnabled proxy to pluginLoader', () => {
- const stubFn = sandbox.stub();
- sandbox.stub(
- Gerrit._pluginLoader,
- 'isPluginEnabled',
- (...args) => stubFn(...args)
- );
- Gerrit._isPluginEnabled('test_plugin');
- assert.isTrue(stubFn.calledWith('test_plugin'));
- });
-
- test('Gerrit._isPluginLoaded proxy to pluginLoader', () => {
- const stubFn = sandbox.stub();
- sandbox.stub(
- Gerrit._pluginLoader,
- 'isPluginLoaded',
- (...args) => stubFn(...args)
- );
- Gerrit._isPluginLoaded('test_plugin');
- assert.isTrue(stubFn.calledWith('test_plugin'));
- });
-
- test('Gerrit._isPluginPreloaded proxy to pluginLoader', () => {
- const stubFn = sandbox.stub();
- sandbox.stub(
- Gerrit._pluginLoader,
- 'isPluginPreloaded',
- (...args) => stubFn(...args)
- );
- Gerrit._isPluginPreloaded('test_plugin');
- assert.isTrue(stubFn.calledWith('test_plugin'));
- });
+ test('Gerrit._isPluginPreloaded proxy to pluginLoader', () => {
+ const stubFn = sandbox.stub();
+ sandbox.stub(
+ Gerrit._pluginLoader,
+ 'isPluginPreloaded',
+ (...args) => stubFn(...args)
+ );
+ Gerrit._isPluginPreloaded('test_plugin');
+ assert.isTrue(stubFn.calledWith('test_plugin'));
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js
new file mode 100644
index 0000000..2523d47
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js
@@ -0,0 +1,324 @@
+/**
+ * @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 '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+
+// Note: for new events, naming convention should be: `a-b`
+const EventType = {
+ HISTORY: 'history',
+ LABEL_CHANGE: 'labelchange',
+ SHOW_CHANGE: 'showchange',
+ SUBMIT_CHANGE: 'submitchange',
+ SHOW_REVISION_ACTIONS: 'show-revision-actions',
+ COMMIT_MSG_EDIT: 'commitmsgedit',
+ COMMENT: 'comment',
+ REVERT: 'revert',
+ REVERT_SUBMISSION: 'revert_submission',
+ POST_REVERT: 'postrevert',
+ ANNOTATE_DIFF: 'annotatediff',
+ ADMIN_MENU_LINKS: 'admin-menu-links',
+ HIGHLIGHTJS_LOADED: 'highlightjs-loaded',
+};
+
+const Element = {
+ CHANGE_ACTIONS: 'changeactions',
+ REPLY_DIALOG: 'replydialog',
+};
+
+/**
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @extends Polymer.Element
+ */
+class GrJsApiInterface extends mixinBehaviors( [
+ Gerrit.PatchSetBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get is() { return 'gr-js-api-interface'; }
+
+ constructor() {
+ super();
+ this.Element = Element;
+ this.EventType = EventType;
+ }
+
+ static get properties() {
+ return {
+ _elements: {
+ type: Object,
+ value: {}, // Shared across all instances.
+ },
+ _eventCallbacks: {
+ type: Object,
+ value: {}, // Shared across all instances.
+ },
+ };
+ }
+
+ handleEvent(type, detail) {
+ Gerrit.awaitPluginsLoaded().then(() => {
+ switch (type) {
+ case EventType.HISTORY:
+ this._handleHistory(detail);
+ break;
+ case EventType.SHOW_CHANGE:
+ this._handleShowChange(detail);
+ break;
+ case EventType.COMMENT:
+ this._handleComment(detail);
+ break;
+ case EventType.LABEL_CHANGE:
+ this._handleLabelChange(detail);
+ break;
+ case EventType.SHOW_REVISION_ACTIONS:
+ this._handleShowRevisionActions(detail);
+ break;
+ case EventType.HIGHLIGHTJS_LOADED:
+ this._handleHighlightjsLoaded(detail);
+ break;
+ default:
+ console.warn('handleEvent called with unsupported event type:',
+ type);
+ break;
+ }
+ });
+ }
+
+ addElement(key, el) {
+ this._elements[key] = el;
+ }
+
+ getElement(key) {
+ return this._elements[key];
+ }
+
+ addEventCallback(eventName, callback) {
+ if (!this._eventCallbacks[eventName]) {
+ this._eventCallbacks[eventName] = [];
+ }
+ this._eventCallbacks[eventName].push(callback);
+ }
+
+ canSubmitChange(change, revision) {
+ const submitCallbacks = this._getEventCallbacks(EventType.SUBMIT_CHANGE);
+ const cancelSubmit = submitCallbacks.some(callback => {
+ try {
+ return callback(change, revision) === false;
+ } catch (err) {
+ console.error(err);
+ }
+ return false;
+ });
+
+ return !cancelSubmit;
+ }
+
+ _removeEventCallbacks() {
+ for (const k in EventType) {
+ if (!EventType.hasOwnProperty(k)) { continue; }
+ this._eventCallbacks[EventType[k]] = [];
+ }
+ }
+
+ _handleHistory(detail) {
+ for (const cb of this._getEventCallbacks(EventType.HISTORY)) {
+ try {
+ cb(detail.path);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ }
+
+ _handleShowChange(detail) {
+ // Note (issue 8221) Shallow clone the change object and add a mergeable
+ // getter with deprecation warning. This makes the change detail appear as
+ // though SKIP_MERGEABLE was not set, so that plugins that expect it can
+ // still access.
+ //
+ // This clone and getter can be removed after plugins migrate to use
+ // info.mergeable.
+ //
+ // assign on getter with existing property will report error
+ // see Issue: 12286
+ const change = Object.assign({}, detail.change, {
+ get mergeable() {
+ console.warn('Accessing change.mergeable from SHOW_CHANGE is ' +
+ 'deprecated! Use info.mergeable instead.');
+ return detail.info && detail.info.mergeable;
+ },
+ });
+ const patchNum = detail.patchNum;
+ const info = detail.info;
+
+ let revision;
+ for (const rev of Object.values(change.revisions || {})) {
+ if (this.patchNumEquals(rev._number, patchNum)) {
+ revision = rev;
+ break;
+ }
+ }
+
+ for (const cb of this._getEventCallbacks(EventType.SHOW_CHANGE)) {
+ try {
+ cb(change, revision, info);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ }
+
+ /**
+ * @param {!{change: !Object, revisionActions: !Object}} detail
+ */
+ _handleShowRevisionActions(detail) {
+ const registeredCallbacks = this._getEventCallbacks(
+ EventType.SHOW_REVISION_ACTIONS
+ );
+ for (const cb of registeredCallbacks) {
+ try {
+ cb(detail.revisionActions, detail.change);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ }
+
+ handleCommitMessage(change, msg) {
+ for (const cb of this._getEventCallbacks(EventType.COMMIT_MSG_EDIT)) {
+ try {
+ cb(change, msg);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ }
+
+ _handleComment(detail) {
+ for (const cb of this._getEventCallbacks(EventType.COMMENT)) {
+ try {
+ cb(detail.node);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ }
+
+ _handleLabelChange(detail) {
+ for (const cb of this._getEventCallbacks(EventType.LABEL_CHANGE)) {
+ try {
+ cb(detail.change);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ }
+
+ _handleHighlightjsLoaded(detail) {
+ for (const cb of this._getEventCallbacks(EventType.HIGHLIGHTJS_LOADED)) {
+ try {
+ cb(detail.hljs);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ }
+
+ modifyRevertMsg(change, revertMsg, origMsg) {
+ for (const cb of this._getEventCallbacks(EventType.REVERT)) {
+ try {
+ revertMsg = cb(change, revertMsg, origMsg);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ return revertMsg;
+ }
+
+ modifyRevertSubmissionMsg(change, revertSubmissionMsg, origMsg) {
+ for (const cb of this._getEventCallbacks(EventType.REVERT_SUBMISSION)) {
+ try {
+ revertSubmissionMsg = cb(change, revertSubmissionMsg, origMsg);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ return revertSubmissionMsg;
+ }
+
+ getDiffLayers(path, changeNum, patchNum) {
+ const layers = [];
+ for (const annotationApi of
+ this._getEventCallbacks(EventType.ANNOTATE_DIFF)) {
+ try {
+ const layer = annotationApi.getLayer(path, changeNum, patchNum);
+ layers.push(layer);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ return layers;
+ }
+
+ /**
+ * Retrieves coverage data possibly provided by a plugin.
+ *
+ * Will wait for plugins to be loaded. If multiple plugins offer a coverage
+ * provider, the first one is returned. If no plugin offers a coverage provider,
+ * will resolve to null.
+ *
+ * @return {!Promise<?GrAnnotationActionsInterface>}
+ */
+ getCoverageAnnotationApi() {
+ return Gerrit.awaitPluginsLoaded()
+ .then(() => this._getEventCallbacks(EventType.ANNOTATE_DIFF)
+ .find(api => api.getCoverageProvider()));
+ }
+
+ getAdminMenuLinks() {
+ const links = [];
+ for (const adminApi of
+ this._getEventCallbacks(EventType.ADMIN_MENU_LINKS)) {
+ links.push(...adminApi.getMenuLinks());
+ }
+ return links;
+ }
+
+ getLabelValuesPostRevert(change) {
+ let labels = {};
+ for (const cb of this._getEventCallbacks(EventType.POST_REVERT)) {
+ try {
+ labels = cb(change);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ return labels;
+ }
+
+ _getEventCallbacks(type) {
+ return this._eventCallbacks[type] || [];
+ }
+}
+
+customElements.define(GrJsApiInterface.is, GrJsApiInterface);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
deleted file mode 100644
index 72fc3d0..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
+++ /dev/null
@@ -1,54 +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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../plugins/gr-admin-api/gr-admin-api.html">
-<link rel="import" href="../../plugins/gr-attribute-helper/gr-attribute-helper.html">
-<link rel="import" href="../../plugins/gr-change-metadata-api/gr-change-metadata-api.html">
-<link rel="import" href="../../plugins/gr-dom-hooks/gr-dom-hooks.html">
-<link rel="import" href="../../plugins/gr-event-helper/gr-event-helper.html">
-<link rel="import" href="../../plugins/gr-popup-interface/gr-popup-interface.html">
-<link rel="import" href="../../plugins/gr-repo-api/gr-repo-api.html">
-<link rel="import" href="../../plugins/gr-settings-api/gr-settings-api.html">
-<link rel="import" href="../../plugins/gr-styles-api/gr-styles-api.html">
-<link rel="import" href="../../plugins/gr-theme-api/gr-theme-api.html">
-<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-js-api-interface">
- <!--
- Note: the order matters as files depend on each other.
- 1. gr-api-utils will be used in multiple files below.
- 2. gr-gerrit depends on gr-plugin-loader, gr-public-js-api and
- also gr-plugin-endpoints
- 3. gr-public-js-api depends on gr-plugin-rest-api
- -->
- <script src="gr-api-utils.js"></script>
- <script src="../gr-event-interface/gr-event-interface.js"></script>
- <script src="gr-annotation-actions-context.js"></script>
- <script src="gr-annotation-actions-js-api.js"></script>
- <script src="gr-change-actions-js-api.js"></script>
- <script src="gr-change-reply-js-api.js"></script>
- <script src="gr-js-api-interface.js"></script>
- <script src="gr-plugin-endpoints.js"></script>
- <script src="gr-plugin-action-context.js"></script>
- <script src="gr-plugin-rest-api.js"></script>
- <script src="gr-public-js-api.js"></script>
- <script src="gr-plugin-loader.js"></script>
- <script src="gr-gerrit.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
index 393dc77..6b7b13e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -14,306 +14,45 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../plugins/gr-admin-api/gr-admin-api.js';
+import '../../plugins/gr-attribute-helper/gr-attribute-helper.js';
+import '../../plugins/gr-change-metadata-api/gr-change-metadata-api.js';
+import '../../plugins/gr-dom-hooks/gr-dom-hooks.js';
+import '../../plugins/gr-event-helper/gr-event-helper.js';
+import '../../plugins/gr-popup-interface/gr-popup-interface.js';
+import '../../plugins/gr-repo-api/gr-repo-api.js';
+import '../../plugins/gr-settings-api/gr-settings-api.js';
+import '../../plugins/gr-styles-api/gr-styles-api.js';
+import '../../plugins/gr-theme-api/gr-theme-api.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import './gr-api-utils.js';
+import '../gr-event-interface/gr-event-interface.js';
+import './gr-annotation-actions-context.js';
+import './gr-annotation-actions-js-api.js';
+import './gr-change-actions-js-api.js';
+import './gr-change-reply-js-api.js';
+import './gr-js-api-interface-element.js';
+import './gr-plugin-endpoints.js';
+import './gr-plugin-action-context.js';
+import './gr-plugin-rest-api.js';
+import './gr-public-js-api.js';
+import './gr-plugin-loader.js';
+import './gr-gerrit.js';
- // Note: for new events, naming convention should be: `a-b`
- const EventType = {
- HISTORY: 'history',
- LABEL_CHANGE: 'labelchange',
- SHOW_CHANGE: 'showchange',
- SUBMIT_CHANGE: 'submitchange',
- SHOW_REVISION_ACTIONS: 'show-revision-actions',
- COMMIT_MSG_EDIT: 'commitmsgedit',
- COMMENT: 'comment',
- REVERT: 'revert',
- REVERT_SUBMISSION: 'revert_submission',
- POST_REVERT: 'postrevert',
- ANNOTATE_DIFF: 'annotatediff',
- ADMIN_MENU_LINKS: 'admin-menu-links',
- HIGHLIGHTJS_LOADED: 'highlightjs-loaded',
- };
+/*
+ Note: the order matters as files depend on each other.
+ 1. gr-api-utils will be used in multiple files below.
+ 2. gr-gerrit depends on gr-plugin-loader, gr-public-js-api and
+ also gr-plugin-endpoints
+ 3. gr-public-js-api depends on gr-plugin-rest-api
+*/
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
- const Element = {
- CHANGE_ACTIONS: 'changeactions',
- REPLY_DIALOG: 'replydialog',
- };
-
- /**
- * @appliesMixin Gerrit.PatchSetMixin
- * @extends Polymer.Element
- */
- class GrJsApiInterface extends Polymer.mixinBehaviors( [
- Gerrit.PatchSetBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-js-api-interface'; }
-
- constructor() {
- super();
- this.Element = Element;
- this.EventType = EventType;
- }
-
- static get properties() {
- return {
- _elements: {
- type: Object,
- value: {}, // Shared across all instances.
- },
- _eventCallbacks: {
- type: Object,
- value: {}, // Shared across all instances.
- },
- };
- }
-
- handleEvent(type, detail) {
- Gerrit.awaitPluginsLoaded().then(() => {
- switch (type) {
- case EventType.HISTORY:
- this._handleHistory(detail);
- break;
- case EventType.SHOW_CHANGE:
- this._handleShowChange(detail);
- break;
- case EventType.COMMENT:
- this._handleComment(detail);
- break;
- case EventType.LABEL_CHANGE:
- this._handleLabelChange(detail);
- break;
- case EventType.SHOW_REVISION_ACTIONS:
- this._handleShowRevisionActions(detail);
- break;
- case EventType.HIGHLIGHTJS_LOADED:
- this._handleHighlightjsLoaded(detail);
- break;
- default:
- console.warn('handleEvent called with unsupported event type:',
- type);
- break;
- }
- });
- }
-
- addElement(key, el) {
- this._elements[key] = el;
- }
-
- getElement(key) {
- return this._elements[key];
- }
-
- addEventCallback(eventName, callback) {
- if (!this._eventCallbacks[eventName]) {
- this._eventCallbacks[eventName] = [];
- }
- this._eventCallbacks[eventName].push(callback);
- }
-
- canSubmitChange(change, revision) {
- const submitCallbacks = this._getEventCallbacks(EventType.SUBMIT_CHANGE);
- const cancelSubmit = submitCallbacks.some(callback => {
- try {
- return callback(change, revision) === false;
- } catch (err) {
- console.error(err);
- }
- return false;
- });
-
- return !cancelSubmit;
- }
-
- _removeEventCallbacks() {
- for (const k in EventType) {
- if (!EventType.hasOwnProperty(k)) { continue; }
- this._eventCallbacks[EventType[k]] = [];
- }
- }
-
- _handleHistory(detail) {
- for (const cb of this._getEventCallbacks(EventType.HISTORY)) {
- try {
- cb(detail.path);
- } catch (err) {
- console.error(err);
- }
- }
- }
-
- _handleShowChange(detail) {
- // Note (issue 8221) Shallow clone the change object and add a mergeable
- // getter with deprecation warning. This makes the change detail appear as
- // though SKIP_MERGEABLE was not set, so that plugins that expect it can
- // still access.
- //
- // This clone and getter can be removed after plugins migrate to use
- // info.mergeable.
- //
- // assign on getter with existing property will report error
- // see Issue: 12286
- const change = Object.assign({}, detail.change, {
- get mergeable() {
- console.warn('Accessing change.mergeable from SHOW_CHANGE is ' +
- 'deprecated! Use info.mergeable instead.');
- return detail.info && detail.info.mergeable;
- },
- });
- const patchNum = detail.patchNum;
- const info = detail.info;
-
- let revision;
- for (const rev of Object.values(change.revisions || {})) {
- if (this.patchNumEquals(rev._number, patchNum)) {
- revision = rev;
- break;
- }
- }
-
- for (const cb of this._getEventCallbacks(EventType.SHOW_CHANGE)) {
- try {
- cb(change, revision, info);
- } catch (err) {
- console.error(err);
- }
- }
- }
-
- /**
- * @param {!{change: !Object, revisionActions: !Object}} detail
- */
- _handleShowRevisionActions(detail) {
- const registeredCallbacks = this._getEventCallbacks(
- EventType.SHOW_REVISION_ACTIONS
- );
- for (const cb of registeredCallbacks) {
- try {
- cb(detail.revisionActions, detail.change);
- } catch (err) {
- console.error(err);
- }
- }
- }
-
- handleCommitMessage(change, msg) {
- for (const cb of this._getEventCallbacks(EventType.COMMIT_MSG_EDIT)) {
- try {
- cb(change, msg);
- } catch (err) {
- console.error(err);
- }
- }
- }
-
- _handleComment(detail) {
- for (const cb of this._getEventCallbacks(EventType.COMMENT)) {
- try {
- cb(detail.node);
- } catch (err) {
- console.error(err);
- }
- }
- }
-
- _handleLabelChange(detail) {
- for (const cb of this._getEventCallbacks(EventType.LABEL_CHANGE)) {
- try {
- cb(detail.change);
- } catch (err) {
- console.error(err);
- }
- }
- }
-
- _handleHighlightjsLoaded(detail) {
- for (const cb of this._getEventCallbacks(EventType.HIGHLIGHTJS_LOADED)) {
- try {
- cb(detail.hljs);
- } catch (err) {
- console.error(err);
- }
- }
- }
-
- modifyRevertMsg(change, revertMsg, origMsg) {
- for (const cb of this._getEventCallbacks(EventType.REVERT)) {
- try {
- revertMsg = cb(change, revertMsg, origMsg);
- } catch (err) {
- console.error(err);
- }
- }
- return revertMsg;
- }
-
- modifyRevertSubmissionMsg(change, revertSubmissionMsg, origMsg) {
- for (const cb of this._getEventCallbacks(EventType.REVERT_SUBMISSION)) {
- try {
- revertSubmissionMsg = cb(change, revertSubmissionMsg, origMsg);
- } catch (err) {
- console.error(err);
- }
- }
- return revertSubmissionMsg;
- }
-
- getDiffLayers(path, changeNum, patchNum) {
- const layers = [];
- for (const annotationApi of
- this._getEventCallbacks(EventType.ANNOTATE_DIFF)) {
- try {
- const layer = annotationApi.getLayer(path, changeNum, patchNum);
- layers.push(layer);
- } catch (err) {
- console.error(err);
- }
- }
- return layers;
- }
-
- /**
- * Retrieves coverage data possibly provided by a plugin.
- *
- * Will wait for plugins to be loaded. If multiple plugins offer a coverage
- * provider, the first one is returned. If no plugin offers a coverage provider,
- * will resolve to null.
- *
- * @return {!Promise<?GrAnnotationActionsInterface>}
- */
- getCoverageAnnotationApi() {
- return Gerrit.awaitPluginsLoaded()
- .then(() => this._getEventCallbacks(EventType.ANNOTATE_DIFF)
- .find(api => api.getCoverageProvider()));
- }
-
- getAdminMenuLinks() {
- const links = [];
- for (const adminApi of
- this._getEventCallbacks(EventType.ADMIN_MENU_LINKS)) {
- links.push(...adminApi.getMenuLinks());
- }
- return links;
- }
-
- getLabelValuesPostRevert(change) {
- let labels = {};
- for (const cb of this._getEventCallbacks(EventType.POST_REVERT)) {
- try {
- labels = cb(change);
- } catch (err) {
- console.error(err);
- }
- }
- return labels;
- }
-
- _getEventCallbacks(type) {
- return this._eventCallbacks[type] || [];
- }
- }
-
- customElements.define(GrJsApiInterface.is, GrJsApiInterface);
-})();
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index efc7206..e626e33 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-api-interface</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-js-api-interface.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,539 +30,545 @@
</template>
</test-fixture>
-<script>
- suite('gr-js-api-interface tests', async () => {
- await readyToTest();
- const {PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils;
- let element;
- let plugin;
- let errorStub;
- let sandbox;
- let getResponseObjectStub;
- let sendStub;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+suite('gr-js-api-interface tests', () => {
+ const {PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils;
+ let element;
+ let plugin;
+ let errorStub;
+ let sandbox;
+ let getResponseObjectStub;
+ let sendStub;
- const throwErrFn = function() {
- throw Error('Unfortunately, this handler has stopped');
+ const throwErrFn = function() {
+ throw Error('Unfortunately, this handler has stopped');
+ };
+
+ setup(() => {
+ window.clock = sinon.useFakeTimers();
+ sandbox = sinon.sandbox.create();
+ getResponseObjectStub = sandbox.stub().returns(Promise.resolve());
+ sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
+ stub('gr-rest-api-interface', {
+ getAccount() {
+ return Promise.resolve({name: 'Judy Hopps'});
+ },
+ getResponseObject: getResponseObjectStub,
+ send(...args) {
+ return sendStub(...args);
+ },
+ });
+ element = fixture('basic');
+ errorStub = sandbox.stub(console, 'error');
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ Gerrit._loadPlugins([]);
+ });
+
+ teardown(() => {
+ window.clock.restore();
+ sandbox.restore();
+ element._removeEventCallbacks();
+ plugin = null;
+ });
+
+ test('url', () => {
+ assert.equal(plugin.url(), 'http://test.com/plugins/testplugin/');
+ assert.equal(plugin.url('/static/test.js'),
+ 'http://test.com/plugins/testplugin/static/test.js');
+ });
+
+ test('url for preloaded plugin without ASSETS_PATH', () => {
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'preloaded:testpluginB');
+ assert.equal(plugin.url(),
+ `${window.location.origin}/plugins/testpluginB/`);
+ assert.equal(plugin.url('/static/test.js'),
+ `${window.location.origin}/plugins/testpluginB/static/test.js`);
+ });
+
+ test('url for preloaded plugin without ASSETS_PATH', () => {
+ const oldAssetsPath = window.ASSETS_PATH;
+ window.ASSETS_PATH = 'http://test.com';
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'preloaded:testpluginC');
+ assert.equal(plugin.url(), `${window.ASSETS_PATH}/plugins/testpluginC/`);
+ assert.equal(plugin.url('/static/test.js'),
+ `${window.ASSETS_PATH}/plugins/testpluginC/static/test.js`);
+ window.ASSETS_PATH = oldAssetsPath;
+ });
+
+ test('_send on failure rejects with response text', () => {
+ sendStub.returns(Promise.resolve(
+ {status: 400, text() { return Promise.resolve('text'); }}));
+ return plugin._send().catch(r => {
+ assert.equal(r.message, 'text');
+ });
+ });
+
+ test('_send on failure without text rejects with code', () => {
+ sendStub.returns(Promise.resolve(
+ {status: 400, text() { return Promise.resolve(null); }}));
+ return plugin._send().catch(r => {
+ assert.equal(r.message, '400');
+ });
+ });
+
+ test('get', () => {
+ const response = {foo: 'foo'};
+ getResponseObjectStub.returns(Promise.resolve(response));
+ return plugin.get('/url', r => {
+ assert.isTrue(sendStub.calledWith(
+ 'GET', 'http://test.com/plugins/testplugin/url'));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('get using Promise', () => {
+ const response = {foo: 'foo'};
+ getResponseObjectStub.returns(Promise.resolve(response));
+ return plugin.get('/url', r => 'rubbish').then(r => {
+ assert.isTrue(sendStub.calledWith(
+ 'GET', 'http://test.com/plugins/testplugin/url'));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('post', () => {
+ const payload = {foo: 'foo'};
+ const response = {bar: 'bar'};
+ getResponseObjectStub.returns(Promise.resolve(response));
+ return plugin.post('/url', payload, r => {
+ assert.isTrue(sendStub.calledWith(
+ 'POST', 'http://test.com/plugins/testplugin/url', payload));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('put', () => {
+ const payload = {foo: 'foo'};
+ const response = {bar: 'bar'};
+ getResponseObjectStub.returns(Promise.resolve(response));
+ return plugin.put('/url', payload, r => {
+ assert.isTrue(sendStub.calledWith(
+ 'PUT', 'http://test.com/plugins/testplugin/url', payload));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('delete works', () => {
+ const response = {status: 204};
+ sendStub.returns(Promise.resolve(response));
+ return plugin.delete('/url', r => {
+ assert.isTrue(sendStub.calledWithExactly(
+ 'DELETE', 'http://test.com/plugins/testplugin/url'));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('delete fails', () => {
+ sendStub.returns(Promise.resolve(
+ {status: 400, text() { return Promise.resolve('text'); }}));
+ return plugin.delete('/url', r => {
+ throw new Error('Should not resolve');
+ }).catch(err => {
+ assert.isTrue(sendStub.calledWith(
+ 'DELETE', 'http://test.com/plugins/testplugin/url'));
+ assert.equal('text', err.message);
+ });
+ });
+
+ test('history event', done => {
+ plugin.on(element.EventType.HISTORY, throwErrFn);
+ plugin.on(element.EventType.HISTORY, path => {
+ assert.equal(path, '/path/to/awesomesauce');
+ assert.isTrue(errorStub.calledOnce);
+ done();
+ });
+ element.handleEvent(element.EventType.HISTORY,
+ {path: '/path/to/awesomesauce'});
+ });
+
+ test('showchange event', done => {
+ const testChange = {
+ _number: 42,
+ revisions: {def: {_number: 2}, abc: {_number: 1}},
};
+ const expectedChange = Object.assign({mergeable: false}, testChange);
+ plugin.on(element.EventType.SHOW_CHANGE, throwErrFn);
+ plugin.on(element.EventType.SHOW_CHANGE, (change, revision, info) => {
+ assert.deepEqual(change, expectedChange);
+ assert.deepEqual(revision, testChange.revisions.abc);
+ assert.deepEqual(info, {mergeable: false});
+ assert.isTrue(errorStub.calledOnce);
+ done();
+ });
+ element.handleEvent(element.EventType.SHOW_CHANGE,
+ {change: testChange, patchNum: 1, info: {mergeable: false}});
+ });
+
+ test('show-revision-actions event', done => {
+ const testChange = {
+ _number: 42,
+ revisions: {def: {_number: 2}, abc: {_number: 1}},
+ };
+ plugin.on(element.EventType.SHOW_REVISION_ACTIONS, throwErrFn);
+ plugin.on(element.EventType.SHOW_REVISION_ACTIONS, (actions, change) => {
+ assert.deepEqual(change, testChange);
+ assert.deepEqual(actions, {test: {}});
+ assert.isTrue(errorStub.calledOnce);
+ done();
+ });
+ element.handleEvent(element.EventType.SHOW_REVISION_ACTIONS,
+ {change: testChange, revisionActions: {test: {}}});
+ });
+
+ test('handleEvent awaits plugins load', done => {
+ const testChange = {
+ _number: 42,
+ revisions: {def: {_number: 2}, abc: {_number: 1}},
+ };
+ const spy = sandbox.spy();
+ Gerrit._loadPlugins(['plugins/test.html']);
+ plugin.on(element.EventType.SHOW_CHANGE, spy);
+ element.handleEvent(element.EventType.SHOW_CHANGE,
+ {change: testChange, patchNum: 1});
+ assert.isFalse(spy.called);
+
+ // Timeout on loading plugins
+ window.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
+
+ flush(() => {
+ assert.isTrue(spy.called);
+ done();
+ });
+ });
+
+ test('comment event', done => {
+ const testCommentNode = {foo: 'bar'};
+ plugin.on(element.EventType.COMMENT, throwErrFn);
+ plugin.on(element.EventType.COMMENT, commentNode => {
+ assert.deepEqual(commentNode, testCommentNode);
+ assert.isTrue(errorStub.calledOnce);
+ done();
+ });
+ element.handleEvent(element.EventType.COMMENT, {node: testCommentNode});
+ });
+
+ test('revert event', () => {
+ function appendToRevertMsg(c, revertMsg, originalMsg) {
+ return revertMsg + '\n' + originalMsg.replace(/^/gm, '> ') + '\ninfo';
+ }
+
+ assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'), 'test');
+ assert.equal(errorStub.callCount, 0);
+
+ plugin.on(element.EventType.REVERT, throwErrFn);
+ plugin.on(element.EventType.REVERT, appendToRevertMsg);
+ assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'),
+ 'test\n> origTest\ninfo');
+ assert.isTrue(errorStub.calledOnce);
+
+ plugin.on(element.EventType.REVERT, appendToRevertMsg);
+ assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'),
+ 'test\n> origTest\ninfo\n> origTest\ninfo');
+ assert.isTrue(errorStub.calledTwice);
+ });
+
+ test('postrevert event', () => {
+ function getLabels(c) {
+ return {'Code-Review': 1};
+ }
+
+ assert.deepEqual(element.getLabelValuesPostRevert(null), {});
+ assert.equal(errorStub.callCount, 0);
+
+ plugin.on(element.EventType.POST_REVERT, throwErrFn);
+ plugin.on(element.EventType.POST_REVERT, getLabels);
+ assert.deepEqual(
+ element.getLabelValuesPostRevert(null), {'Code-Review': 1});
+ assert.isTrue(errorStub.calledOnce);
+ });
+
+ test('commitmsgedit event', done => {
+ const testMsg = 'Test CL commit message';
+ plugin.on(element.EventType.COMMIT_MSG_EDIT, throwErrFn);
+ plugin.on(element.EventType.COMMIT_MSG_EDIT, (change, msg) => {
+ assert.deepEqual(msg, testMsg);
+ assert.isTrue(errorStub.calledOnce);
+ done();
+ });
+ element.handleCommitMessage(null, testMsg);
+ });
+
+ test('labelchange event', done => {
+ const testChange = {_number: 42};
+ plugin.on(element.EventType.LABEL_CHANGE, throwErrFn);
+ plugin.on(element.EventType.LABEL_CHANGE, change => {
+ assert.deepEqual(change, testChange);
+ assert.isTrue(errorStub.calledOnce);
+ done();
+ });
+ element.handleEvent(element.EventType.LABEL_CHANGE, {change: testChange});
+ });
+
+ test('submitchange', () => {
+ plugin.on(element.EventType.SUBMIT_CHANGE, throwErrFn);
+ plugin.on(element.EventType.SUBMIT_CHANGE, () => true);
+ assert.isTrue(element.canSubmitChange());
+ assert.isTrue(errorStub.calledOnce);
+ plugin.on(element.EventType.SUBMIT_CHANGE, () => false);
+ plugin.on(element.EventType.SUBMIT_CHANGE, () => true);
+ assert.isFalse(element.canSubmitChange());
+ assert.isTrue(errorStub.calledTwice);
+ });
+
+ test('highlightjs-loaded event', done => {
+ const testHljs = {_number: 42};
+ plugin.on(element.EventType.HIGHLIGHTJS_LOADED, throwErrFn);
+ plugin.on(element.EventType.HIGHLIGHTJS_LOADED, hljs => {
+ assert.deepEqual(hljs, testHljs);
+ assert.isTrue(errorStub.calledOnce);
+ done();
+ });
+ element.handleEvent(element.EventType.HIGHLIGHTJS_LOADED, {hljs: testHljs});
+ });
+
+ test('getLoggedIn', done => {
+ // fake fetch for authCheck
+ sandbox.stub(window, 'fetch', () => Promise.resolve({status: 204}));
+ plugin.restApi().getLoggedIn()
+ .then(loggedIn => {
+ assert.isTrue(loggedIn);
+ done();
+ });
+ });
+
+ test('attributeHelper', () => {
+ assert.isOk(plugin.attributeHelper());
+ });
+
+ test('deprecated.install', () => {
+ plugin.deprecated.install();
+ assert.strictEqual(plugin.popup, plugin.deprecated.popup);
+ assert.strictEqual(plugin.onAction, plugin.deprecated.onAction);
+ assert.notStrictEqual(plugin.install, plugin.deprecated.install);
+ });
+
+ test('getAdminMenuLinks', () => {
+ const links = [{text: 'a', url: 'b'}, {text: 'c', url: 'd'}];
+ const getCallbacksStub = sandbox.stub(element, '_getEventCallbacks')
+ .returns([
+ {getMenuLinks: () => [links[0]]},
+ {getMenuLinks: () => [links[1]]},
+ ]);
+ const result = element.getAdminMenuLinks();
+ assert.deepEqual(result, links);
+ assert.isTrue(getCallbacksStub.calledOnce);
+ assert.equal(getCallbacksStub.lastCall.args[0],
+ element.EventType.ADMIN_MENU_LINKS);
+ });
+
+ suite('test plugin with base url', () => {
+ let baseUrlPlugin;
setup(() => {
- window.clock = sinon.useFakeTimers();
- sandbox = sinon.sandbox.create();
- getResponseObjectStub = sandbox.stub().returns(Promise.resolve());
- sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
- stub('gr-rest-api-interface', {
- getAccount() {
- return Promise.resolve({name: 'Judy Hopps'});
- },
- getResponseObject: getResponseObjectStub,
- send(...args) {
- return sendStub(...args);
- },
- });
- element = fixture('basic');
- errorStub = sandbox.stub(console, 'error');
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- Gerrit._loadPlugins([]);
- });
+ sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/r');
- teardown(() => {
- window.clock.restore();
- sandbox.restore();
- element._removeEventCallbacks();
- plugin = null;
+ Gerrit.install(p => { baseUrlPlugin = p; }, '0.1',
+ 'http://test.com/r/plugins/baseurlplugin/static/test.js');
});
test('url', () => {
- assert.equal(plugin.url(), 'http://test.com/plugins/testplugin/');
- assert.equal(plugin.url('/static/test.js'),
- 'http://test.com/plugins/testplugin/static/test.js');
+ assert.notEqual(baseUrlPlugin.url(),
+ 'http://test.com/plugins/baseurlplugin/');
+ assert.equal(baseUrlPlugin.url(),
+ 'http://test.com/r/plugins/baseurlplugin/');
+ assert.equal(baseUrlPlugin.url('/static/test.js'),
+ 'http://test.com/r/plugins/baseurlplugin/static/test.js');
+ });
+ });
+
+ suite('popup', () => {
+ test('popup(element) is deprecated', () => {
+ plugin.popup(document.createElement('div'));
+ assert.isTrue(console.error.calledOnce);
});
- test('url for preloaded plugin without ASSETS_PATH', () => {
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'preloaded:testpluginB');
- assert.equal(plugin.url(),
- `${window.location.origin}/plugins/testpluginB/`);
- assert.equal(plugin.url('/static/test.js'),
- `${window.location.origin}/plugins/testpluginB/static/test.js`);
+ test('popup(moduleName) creates popup with component', () => {
+ const openStub = sandbox.stub();
+ sandbox.stub(window, 'GrPopupInterface').returns({
+ open: openStub,
+ });
+ plugin.popup('some-name');
+ assert.isTrue(openStub.calledOnce);
+ assert.isTrue(GrPopupInterface.calledWith(plugin, 'some-name'));
});
- test('url for preloaded plugin without ASSETS_PATH', () => {
- const oldAssetsPath = window.ASSETS_PATH;
- window.ASSETS_PATH = 'http://test.com';
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'preloaded:testpluginC');
- assert.equal(plugin.url(), `${window.ASSETS_PATH}/plugins/testpluginC/`);
- assert.equal(plugin.url('/static/test.js'),
- `${window.ASSETS_PATH}/plugins/testpluginC/static/test.js`);
- window.ASSETS_PATH = oldAssetsPath;
+ test('deprecated.popup(element) creates popup with element', () => {
+ const el = document.createElement('div');
+ el.textContent = 'some text here';
+ const openStub = sandbox.stub(GrPopupInterface.prototype, 'open');
+ openStub.returns(Promise.resolve({
+ _getElement() {
+ return document.createElement('div');
+ }}));
+ plugin.deprecated.popup(el);
+ assert.isTrue(openStub.calledOnce);
});
+ });
- test('_send on failure rejects with response text', () => {
- sendStub.returns(Promise.resolve(
- {status: 400, text() { return Promise.resolve('text'); }}));
- return plugin._send().catch(r => {
- assert.equal(r.message, 'text');
+ suite('onAction', () => {
+ let change;
+ let revision;
+ let actionDetails;
+
+ setup(() => {
+ change = {};
+ revision = {};
+ actionDetails = {__key: 'some'};
+ sandbox.stub(plugin, 'on').callsArgWith(1, change, revision);
+ sandbox.stub(plugin, 'changeActions').returns({
+ addTapListener: sandbox.stub().callsArg(1),
+ getActionDetails: () => actionDetails,
});
});
- test('_send on failure without text rejects with code', () => {
- sendStub.returns(Promise.resolve(
- {status: 400, text() { return Promise.resolve(null); }}));
- return plugin._send().catch(r => {
- assert.equal(r.message, '400');
+ test('returns GrPluginActionContext', () => {
+ const stub = sandbox.stub();
+ plugin.deprecated.onAction('change', 'foo', ctx => {
+ assert.isTrue(ctx instanceof GrPluginActionContext);
+ assert.strictEqual(ctx.change, change);
+ assert.strictEqual(ctx.revision, revision);
+ assert.strictEqual(ctx.action, actionDetails);
+ assert.strictEqual(ctx.plugin, plugin);
+ stub();
});
+ assert.isTrue(stub.called);
});
- test('get', () => {
- const response = {foo: 'foo'};
- getResponseObjectStub.returns(Promise.resolve(response));
- return plugin.get('/url', r => {
- assert.isTrue(sendStub.calledWith(
- 'GET', 'http://test.com/plugins/testplugin/url'));
- assert.strictEqual(r, response);
- });
+ test('other actions', () => {
+ const stub = sandbox.stub();
+ plugin.deprecated.onAction('project', 'foo', stub);
+ plugin.deprecated.onAction('edit', 'foo', stub);
+ plugin.deprecated.onAction('branch', 'foo', stub);
+ assert.isFalse(stub.called);
+ });
+ });
+
+ suite('screen', () => {
+ test('screenUrl()', () => {
+ sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/base');
+ assert.equal(
+ plugin.screenUrl(),
+ `${location.origin}/base/x/testplugin`
+ );
+ assert.equal(
+ plugin.screenUrl('foo'),
+ `${location.origin}/base/x/testplugin/foo`
+ );
});
- test('get using Promise', () => {
- const response = {foo: 'foo'};
- getResponseObjectStub.returns(Promise.resolve(response));
- return plugin.get('/url', r => 'rubbish').then(r => {
- assert.isTrue(sendStub.calledWith(
- 'GET', 'http://test.com/plugins/testplugin/url'));
- assert.strictEqual(r, response);
- });
+ test('deprecated works', () => {
+ const stub = sandbox.stub();
+ const hookStub = {onAttached: sandbox.stub()};
+ sandbox.stub(plugin, 'hook').returns(hookStub);
+ plugin.deprecated.screen('foo', stub);
+ assert.isTrue(plugin.hook.calledWith('testplugin-screen-foo'));
+ const fakeEl = {style: {display: ''}};
+ hookStub.onAttached.callArgWith(0, fakeEl);
+ assert.isTrue(stub.called);
+ assert.equal(fakeEl.style.display, 'none');
});
- test('post', () => {
- const payload = {foo: 'foo'};
- const response = {bar: 'bar'};
- getResponseObjectStub.returns(Promise.resolve(response));
- return plugin.post('/url', payload, r => {
- assert.isTrue(sendStub.calledWith(
- 'POST', 'http://test.com/plugins/testplugin/url', payload));
- assert.strictEqual(r, response);
- });
+ test('works', () => {
+ sandbox.stub(plugin, 'registerCustomComponent');
+ plugin.screen('foo', 'some-module');
+ assert.isTrue(plugin.registerCustomComponent.calledWith(
+ 'testplugin-screen-foo', 'some-module'));
+ });
+ });
+
+ suite('panel', () => {
+ let fakeEl;
+ let emulateAttached;
+
+ setup(()=> {
+ fakeEl = {change: {}, revision: {}};
+ const hookStub = {onAttached: sandbox.stub()};
+ sandbox.stub(plugin, 'hook').returns(hookStub);
+ emulateAttached = () => hookStub.onAttached.callArgWith(0, fakeEl);
});
- test('put', () => {
- const payload = {foo: 'foo'};
- const response = {bar: 'bar'};
- getResponseObjectStub.returns(Promise.resolve(response));
- return plugin.put('/url', payload, r => {
- assert.isTrue(sendStub.calledWith(
- 'PUT', 'http://test.com/plugins/testplugin/url', payload));
- assert.strictEqual(r, response);
- });
+ test('plugin.panel is deprecated', () => {
+ plugin.panel('rubbish');
+ assert.isTrue(console.error.called);
});
- test('delete works', () => {
- const response = {status: 204};
- sendStub.returns(Promise.resolve(response));
- return plugin.delete('/url', r => {
- assert.isTrue(sendStub.calledWithExactly(
- 'DELETE', 'http://test.com/plugins/testplugin/url'));
- assert.strictEqual(r, response);
- });
- });
-
- test('delete fails', () => {
- sendStub.returns(Promise.resolve(
- {status: 400, text() { return Promise.resolve('text'); }}));
- return plugin.delete('/url', r => {
- throw new Error('Should not resolve');
- }).catch(err => {
- assert.isTrue(sendStub.calledWith(
- 'DELETE', 'http://test.com/plugins/testplugin/url'));
- assert.equal('text', err.message);
- });
- });
-
- test('history event', done => {
- plugin.on(element.EventType.HISTORY, throwErrFn);
- plugin.on(element.EventType.HISTORY, path => {
- assert.equal(path, '/path/to/awesomesauce');
- assert.isTrue(errorStub.calledOnce);
- done();
- });
- element.handleEvent(element.EventType.HISTORY,
- {path: '/path/to/awesomesauce'});
- });
-
- test('showchange event', done => {
- const testChange = {
- _number: 42,
- revisions: {def: {_number: 2}, abc: {_number: 1}},
- };
- const expectedChange = Object.assign({mergeable: false}, testChange);
- plugin.on(element.EventType.SHOW_CHANGE, throwErrFn);
- plugin.on(element.EventType.SHOW_CHANGE, (change, revision, info) => {
- assert.deepEqual(change, expectedChange);
- assert.deepEqual(revision, testChange.revisions.abc);
- assert.deepEqual(info, {mergeable: false});
- assert.isTrue(errorStub.calledOnce);
- done();
- });
- element.handleEvent(element.EventType.SHOW_CHANGE,
- {change: testChange, patchNum: 1, info: {mergeable: false}});
- });
-
- test('show-revision-actions event', done => {
- const testChange = {
- _number: 42,
- revisions: {def: {_number: 2}, abc: {_number: 1}},
- };
- plugin.on(element.EventType.SHOW_REVISION_ACTIONS, throwErrFn);
- plugin.on(element.EventType.SHOW_REVISION_ACTIONS, (actions, change) => {
- assert.deepEqual(change, testChange);
- assert.deepEqual(actions, {test: {}});
- assert.isTrue(errorStub.calledOnce);
- done();
- });
- element.handleEvent(element.EventType.SHOW_REVISION_ACTIONS,
- {change: testChange, revisionActions: {test: {}}});
- });
-
- test('handleEvent awaits plugins load', done => {
- const testChange = {
- _number: 42,
- revisions: {def: {_number: 2}, abc: {_number: 1}},
- };
- const spy = sandbox.spy();
- Gerrit._loadPlugins(['plugins/test.html']);
- plugin.on(element.EventType.SHOW_CHANGE, spy);
- element.handleEvent(element.EventType.SHOW_CHANGE,
- {change: testChange, patchNum: 1});
- assert.isFalse(spy.called);
-
- // Timeout on loading plugins
- window.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
-
- flush(() => {
- assert.isTrue(spy.called);
- done();
- });
- });
-
- test('comment event', done => {
- const testCommentNode = {foo: 'bar'};
- plugin.on(element.EventType.COMMENT, throwErrFn);
- plugin.on(element.EventType.COMMENT, commentNode => {
- assert.deepEqual(commentNode, testCommentNode);
- assert.isTrue(errorStub.calledOnce);
- done();
- });
- element.handleEvent(element.EventType.COMMENT, {node: testCommentNode});
- });
-
- test('revert event', () => {
- function appendToRevertMsg(c, revertMsg, originalMsg) {
- return revertMsg + '\n' + originalMsg.replace(/^/gm, '> ') + '\ninfo';
- }
-
- assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'), 'test');
- assert.equal(errorStub.callCount, 0);
-
- plugin.on(element.EventType.REVERT, throwErrFn);
- plugin.on(element.EventType.REVERT, appendToRevertMsg);
- assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'),
- 'test\n> origTest\ninfo');
- assert.isTrue(errorStub.calledOnce);
-
- plugin.on(element.EventType.REVERT, appendToRevertMsg);
- assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'),
- 'test\n> origTest\ninfo\n> origTest\ninfo');
- assert.isTrue(errorStub.calledTwice);
- });
-
- test('postrevert event', () => {
- function getLabels(c) {
- return {'Code-Review': 1};
- }
-
- assert.deepEqual(element.getLabelValuesPostRevert(null), {});
- assert.equal(errorStub.callCount, 0);
-
- plugin.on(element.EventType.POST_REVERT, throwErrFn);
- plugin.on(element.EventType.POST_REVERT, getLabels);
- assert.deepEqual(
- element.getLabelValuesPostRevert(null), {'Code-Review': 1});
- assert.isTrue(errorStub.calledOnce);
- });
-
- test('commitmsgedit event', done => {
- const testMsg = 'Test CL commit message';
- plugin.on(element.EventType.COMMIT_MSG_EDIT, throwErrFn);
- plugin.on(element.EventType.COMMIT_MSG_EDIT, (change, msg) => {
- assert.deepEqual(msg, testMsg);
- assert.isTrue(errorStub.calledOnce);
- done();
- });
- element.handleCommitMessage(null, testMsg);
- });
-
- test('labelchange event', done => {
- const testChange = {_number: 42};
- plugin.on(element.EventType.LABEL_CHANGE, throwErrFn);
- plugin.on(element.EventType.LABEL_CHANGE, change => {
- assert.deepEqual(change, testChange);
- assert.isTrue(errorStub.calledOnce);
- done();
- });
- element.handleEvent(element.EventType.LABEL_CHANGE, {change: testChange});
- });
-
- test('submitchange', () => {
- plugin.on(element.EventType.SUBMIT_CHANGE, throwErrFn);
- plugin.on(element.EventType.SUBMIT_CHANGE, () => true);
- assert.isTrue(element.canSubmitChange());
- assert.isTrue(errorStub.calledOnce);
- plugin.on(element.EventType.SUBMIT_CHANGE, () => false);
- plugin.on(element.EventType.SUBMIT_CHANGE, () => true);
- assert.isFalse(element.canSubmitChange());
- assert.isTrue(errorStub.calledTwice);
- });
-
- test('highlightjs-loaded event', done => {
- const testHljs = {_number: 42};
- plugin.on(element.EventType.HIGHLIGHTJS_LOADED, throwErrFn);
- plugin.on(element.EventType.HIGHLIGHTJS_LOADED, hljs => {
- assert.deepEqual(hljs, testHljs);
- assert.isTrue(errorStub.calledOnce);
- done();
- });
- element.handleEvent(element.EventType.HIGHLIGHTJS_LOADED, {hljs: testHljs});
- });
-
- test('getLoggedIn', done => {
- // fake fetch for authCheck
- sandbox.stub(window, 'fetch', () => Promise.resolve({status: 204}));
- plugin.restApi().getLoggedIn()
- .then(loggedIn => {
- assert.isTrue(loggedIn);
- done();
- });
- });
-
- test('attributeHelper', () => {
- assert.isOk(plugin.attributeHelper());
- });
-
- test('deprecated.install', () => {
- plugin.deprecated.install();
- assert.strictEqual(plugin.popup, plugin.deprecated.popup);
- assert.strictEqual(plugin.onAction, plugin.deprecated.onAction);
- assert.notStrictEqual(plugin.install, plugin.deprecated.install);
- });
-
- test('getAdminMenuLinks', () => {
- const links = [{text: 'a', url: 'b'}, {text: 'c', url: 'd'}];
- const getCallbacksStub = sandbox.stub(element, '_getEventCallbacks')
- .returns([
- {getMenuLinks: () => [links[0]]},
- {getMenuLinks: () => [links[1]]},
- ]);
- const result = element.getAdminMenuLinks();
- assert.deepEqual(result, links);
- assert.isTrue(getCallbacksStub.calledOnce);
- assert.equal(getCallbacksStub.lastCall.args[0],
- element.EventType.ADMIN_MENU_LINKS);
- });
-
- suite('test plugin with base url', () => {
- let baseUrlPlugin;
-
- setup(() => {
- sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/r');
-
- Gerrit.install(p => { baseUrlPlugin = p; }, '0.1',
- 'http://test.com/r/plugins/baseurlplugin/static/test.js');
- });
-
- test('url', () => {
- assert.notEqual(baseUrlPlugin.url(),
- 'http://test.com/plugins/baseurlplugin/');
- assert.equal(baseUrlPlugin.url(),
- 'http://test.com/r/plugins/baseurlplugin/');
- assert.equal(baseUrlPlugin.url('/static/test.js'),
- 'http://test.com/r/plugins/baseurlplugin/static/test.js');
- });
- });
-
- suite('popup', () => {
- test('popup(element) is deprecated', () => {
- plugin.popup(document.createElement('div'));
- assert.isTrue(console.error.calledOnce);
- });
-
- test('popup(moduleName) creates popup with component', () => {
- const openStub = sandbox.stub();
- sandbox.stub(window, 'GrPopupInterface').returns({
- open: openStub,
- });
- plugin.popup('some-name');
- assert.isTrue(openStub.calledOnce);
- assert.isTrue(GrPopupInterface.calledWith(plugin, 'some-name'));
- });
-
- test('deprecated.popup(element) creates popup with element', () => {
- const el = document.createElement('div');
- el.textContent = 'some text here';
- const openStub = sandbox.stub(GrPopupInterface.prototype, 'open');
- openStub.returns(Promise.resolve({
- _getElement() {
- return document.createElement('div');
- }}));
- plugin.deprecated.popup(el);
- assert.isTrue(openStub.calledOnce);
- });
- });
-
- suite('onAction', () => {
- let change;
- let revision;
- let actionDetails;
-
- setup(() => {
- change = {};
- revision = {};
- actionDetails = {__key: 'some'};
- sandbox.stub(plugin, 'on').callsArgWith(1, change, revision);
- sandbox.stub(plugin, 'changeActions').returns({
- addTapListener: sandbox.stub().callsArg(1),
- getActionDetails: () => actionDetails,
- });
- });
-
- test('returns GrPluginActionContext', () => {
- const stub = sandbox.stub();
- plugin.deprecated.onAction('change', 'foo', ctx => {
- assert.isTrue(ctx instanceof GrPluginActionContext);
- assert.strictEqual(ctx.change, change);
- assert.strictEqual(ctx.revision, revision);
- assert.strictEqual(ctx.action, actionDetails);
- assert.strictEqual(ctx.plugin, plugin);
- stub();
- });
- assert.isTrue(stub.called);
- });
-
- test('other actions', () => {
- const stub = sandbox.stub();
- plugin.deprecated.onAction('project', 'foo', stub);
- plugin.deprecated.onAction('edit', 'foo', stub);
- plugin.deprecated.onAction('branch', 'foo', stub);
- assert.isFalse(stub.called);
- });
- });
-
- suite('screen', () => {
- test('screenUrl()', () => {
- sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/base');
- assert.equal(plugin.screenUrl(), 'http://test.com/base/x/testplugin');
- assert.equal(
- plugin.screenUrl('foo'), 'http://test.com/base/x/testplugin/foo');
- });
-
- test('deprecated works', () => {
- const stub = sandbox.stub();
- const hookStub = {onAttached: sandbox.stub()};
- sandbox.stub(plugin, 'hook').returns(hookStub);
- plugin.deprecated.screen('foo', stub);
- assert.isTrue(plugin.hook.calledWith('testplugin-screen-foo'));
- const fakeEl = {style: {display: ''}};
- hookStub.onAttached.callArgWith(0, fakeEl);
- assert.isTrue(stub.called);
- assert.equal(fakeEl.style.display, 'none');
- });
-
- test('works', () => {
- sandbox.stub(plugin, 'registerCustomComponent');
- plugin.screen('foo', 'some-module');
- assert.isTrue(plugin.registerCustomComponent.calledWith(
- 'testplugin-screen-foo', 'some-module'));
- });
- });
-
- suite('panel', () => {
- let fakeEl;
- let emulateAttached;
-
- setup(()=> {
- fakeEl = {change: {}, revision: {}};
- const hookStub = {onAttached: sandbox.stub()};
- sandbox.stub(plugin, 'hook').returns(hookStub);
- emulateAttached = () => hookStub.onAttached.callArgWith(0, fakeEl);
- });
-
- test('plugin.panel is deprecated', () => {
- plugin.panel('rubbish');
- assert.isTrue(console.error.called);
- });
-
- [
- ['CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK', 'change-view-integration'],
- ['CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK', 'change-metadata-item'],
- ].forEach(([panelName, endpointName]) => {
- test(`deprecated.panel works for ${panelName}`, () => {
- const callback = sandbox.stub();
- plugin.deprecated.panel(panelName, callback);
- assert.isTrue(plugin.hook.calledWith(endpointName));
- emulateAttached();
- assert.isTrue(callback.called);
- const args = callback.args[0][0];
- assert.strictEqual(args.body, fakeEl);
- assert.strictEqual(args.p.CHANGE_INFO, fakeEl.change);
- assert.strictEqual(args.p.REVISION_INFO, fakeEl.revision);
- });
- });
- });
-
- suite('settingsScreen', () => {
- test('plugin.settingsScreen is deprecated', () => {
- plugin.settingsScreen('rubbish');
- assert.isTrue(console.error.called);
- });
-
- test('plugin.settings() returns GrSettingsApi', () => {
- assert.isOk(plugin.settings());
- assert.isTrue(plugin.settings() instanceof GrSettingsApi);
- });
-
- test('plugin.deprecated.settingsScreen() works', () => {
- const hookStub = {onAttached: sandbox.stub()};
- sandbox.stub(plugin, 'hook').returns(hookStub);
- const fakeSettings = {};
- fakeSettings.title = sandbox.stub().returns(fakeSettings);
- fakeSettings.token = sandbox.stub().returns(fakeSettings);
- fakeSettings.module = sandbox.stub().returns(fakeSettings);
- fakeSettings.build = sandbox.stub().returns(hookStub);
- sandbox.stub(plugin, 'settings').returns(fakeSettings);
+ [
+ ['CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK', 'change-view-integration'],
+ ['CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK', 'change-metadata-item'],
+ ].forEach(([panelName, endpointName]) => {
+ test(`deprecated.panel works for ${panelName}`, () => {
const callback = sandbox.stub();
-
- plugin.deprecated.settingsScreen('path', 'menu', callback);
- assert.isTrue(fakeSettings.title.calledWith('menu'));
- assert.isTrue(fakeSettings.token.calledWith('path'));
- assert.isTrue(fakeSettings.module.calledWith('div'));
- assert.equal(fakeSettings.build.callCount, 1);
-
- const fakeBody = {};
- const fakeEl = {
- style: {
- display: '',
- },
- querySelector: sandbox.stub().returns(fakeBody),
- };
- // Emulate settings screen attached
- hookStub.onAttached.callArgWith(0, fakeEl);
+ plugin.deprecated.panel(panelName, callback);
+ assert.isTrue(plugin.hook.calledWith(endpointName));
+ emulateAttached();
assert.isTrue(callback.called);
const args = callback.args[0][0];
- assert.strictEqual(args.body, fakeBody);
- assert.equal(fakeEl.style.display, 'none');
+ assert.strictEqual(args.body, fakeEl);
+ assert.strictEqual(args.p.CHANGE_INFO, fakeEl.change);
+ assert.strictEqual(args.p.REVISION_INFO, fakeEl.revision);
});
});
});
+
+ suite('settingsScreen', () => {
+ test('plugin.settingsScreen is deprecated', () => {
+ plugin.settingsScreen('rubbish');
+ assert.isTrue(console.error.called);
+ });
+
+ test('plugin.settings() returns GrSettingsApi', () => {
+ assert.isOk(plugin.settings());
+ assert.isTrue(plugin.settings() instanceof GrSettingsApi);
+ });
+
+ test('plugin.deprecated.settingsScreen() works', () => {
+ const hookStub = {onAttached: sandbox.stub()};
+ sandbox.stub(plugin, 'hook').returns(hookStub);
+ const fakeSettings = {};
+ fakeSettings.title = sandbox.stub().returns(fakeSettings);
+ fakeSettings.token = sandbox.stub().returns(fakeSettings);
+ fakeSettings.module = sandbox.stub().returns(fakeSettings);
+ fakeSettings.build = sandbox.stub().returns(hookStub);
+ sandbox.stub(plugin, 'settings').returns(fakeSettings);
+ const callback = sandbox.stub();
+
+ plugin.deprecated.settingsScreen('path', 'menu', callback);
+ assert.isTrue(fakeSettings.title.calledWith('menu'));
+ assert.isTrue(fakeSettings.token.calledWith('path'));
+ assert.isTrue(fakeSettings.module.calledWith('div'));
+ assert.equal(fakeSettings.build.callCount, 1);
+
+ const fakeBody = {};
+ const fakeEl = {
+ style: {
+ display: '',
+ },
+ querySelector: sandbox.stub().returns(fakeBody),
+ };
+ // Emulate settings screen attached
+ hookStub.onAttached.callArgWith(0, fakeEl);
+ assert.isTrue(callback.called);
+ const args = callback.args[0][0];
+ assert.strictEqual(args.body, fakeBody);
+ assert.equal(fakeEl.style.display, 'none');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
index c8fb9b1..48b2438 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-plugin-action-context</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-js-api-interface.html"/>
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,126 +30,128 @@
</template>
</test-fixture>
-<script>
- suite('gr-plugin-action-context tests', async () => {
- await readyToTest();
- let instance;
- let sandbox;
- let plugin;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-plugin-action-context tests', () => {
+ let instance;
+ let sandbox;
+ let plugin;
- setup(() => {
- sandbox = sinon.sandbox.create();
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- instance = new GrPluginActionContext(plugin);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ instance = new GrPluginActionContext(plugin);
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('popup() and hide()', () => {
- const popupApiStub = {
- close: sandbox.stub(),
- };
- sandbox.stub(plugin.deprecated, 'popup').returns(popupApiStub);
- const el = {};
- instance.popup(el);
- assert.isTrue(instance.plugin.deprecated.popup.calledWith(el));
+ test('popup() and hide()', () => {
+ const popupApiStub = {
+ close: sandbox.stub(),
+ };
+ sandbox.stub(plugin.deprecated, 'popup').returns(popupApiStub);
+ const el = {};
+ instance.popup(el);
+ assert.isTrue(instance.plugin.deprecated.popup.calledWith(el));
- instance.hide();
- assert.isTrue(popupApiStub.close.called);
- });
+ instance.hide();
+ assert.isTrue(popupApiStub.close.called);
+ });
- test('textfield', () => {
- assert.equal(instance.textfield().tagName, 'PAPER-INPUT');
- });
+ test('textfield', () => {
+ assert.equal(instance.textfield().tagName, 'PAPER-INPUT');
+ });
- test('br', () => {
- assert.equal(instance.br().tagName, 'BR');
- });
+ test('br', () => {
+ assert.equal(instance.br().tagName, 'BR');
+ });
- test('msg', () => {
- const el = instance.msg('foobar');
- assert.equal(el.tagName, 'GR-LABEL');
- assert.equal(el.textContent, 'foobar');
- });
+ test('msg', () => {
+ const el = instance.msg('foobar');
+ assert.equal(el.tagName, 'GR-LABEL');
+ assert.equal(el.textContent, 'foobar');
+ });
- test('div', () => {
- const el1 = document.createElement('span');
- el1.textContent = 'foo';
- const el2 = document.createElement('div');
- el2.textContent = 'bar';
- const div = instance.div(el1, el2);
- assert.equal(div.tagName, 'DIV');
- assert.equal(div.textContent, 'foobar');
- });
+ test('div', () => {
+ const el1 = document.createElement('span');
+ el1.textContent = 'foo';
+ const el2 = document.createElement('div');
+ el2.textContent = 'bar';
+ const div = instance.div(el1, el2);
+ assert.equal(div.tagName, 'DIV');
+ assert.equal(div.textContent, 'foobar');
+ });
- test('button', done => {
- const clickStub = sandbox.stub();
- const button = instance.button('foo', {onclick: clickStub});
- // If you don't attach a Polymer element to the DOM, then the ready()
- // callback will not be called and then e.g. this.$ is undefined.
- Polymer.dom(document.body).appendChild(button);
- MockInteractions.tap(button);
- flush(() => {
- assert.isTrue(clickStub.called);
- assert.equal(button.textContent, 'foo');
- done();
- });
- });
-
- test('checkbox', () => {
- const el = instance.checkbox();
- assert.equal(el.tagName, 'INPUT');
- assert.equal(el.type, 'checkbox');
- });
-
- test('label', () => {
- const fakeMsg = {};
- const fakeCheckbox = {};
- sandbox.stub(instance, 'div');
- sandbox.stub(instance, 'msg').returns(fakeMsg);
- instance.label(fakeCheckbox, 'foo');
- assert.isTrue(instance.div.calledWithExactly(fakeCheckbox, fakeMsg));
- });
-
- test('call', () => {
- instance.action = {
- method: 'METHOD',
- __key: 'key',
- __url: '/changes/1/revisions/2/foo~bar',
- };
- const sendStub = sandbox.stub().returns(Promise.resolve());
- sandbox.stub(plugin, 'restApi').returns({
- send: sendStub,
- });
- const payload = {foo: 'foo'};
- const successStub = sandbox.stub();
- instance.call(payload, successStub);
- assert.isTrue(sendStub.calledWith(
- 'METHOD', '/changes/1/revisions/2/foo~bar', payload));
- });
-
- test('call error', done => {
- instance.action = {
- method: 'METHOD',
- __key: 'key',
- __url: '/changes/1/revisions/2/foo~bar',
- };
- const sendStub = sandbox.stub().returns(Promise.reject(new Error('boom')));
- sandbox.stub(plugin, 'restApi').returns({
- send: sendStub,
- });
- const errorStub = sandbox.stub();
- document.addEventListener('show-alert', errorStub);
- instance.call();
- flush(() => {
- assert.isTrue(errorStub.calledOnce);
- assert.equal(errorStub.args[0][0].detail.message,
- 'Plugin network error: Error: boom');
- done();
- });
+ test('button', done => {
+ const clickStub = sandbox.stub();
+ const button = instance.button('foo', {onclick: clickStub});
+ // If you don't attach a Polymer element to the DOM, then the ready()
+ // callback will not be called and then e.g. this.$ is undefined.
+ dom(document.body).appendChild(button);
+ MockInteractions.tap(button);
+ flush(() => {
+ assert.isTrue(clickStub.called);
+ assert.equal(button.textContent, 'foo');
+ done();
});
});
+
+ test('checkbox', () => {
+ const el = instance.checkbox();
+ assert.equal(el.tagName, 'INPUT');
+ assert.equal(el.type, 'checkbox');
+ });
+
+ test('label', () => {
+ const fakeMsg = {};
+ const fakeCheckbox = {};
+ sandbox.stub(instance, 'div');
+ sandbox.stub(instance, 'msg').returns(fakeMsg);
+ instance.label(fakeCheckbox, 'foo');
+ assert.isTrue(instance.div.calledWithExactly(fakeCheckbox, fakeMsg));
+ });
+
+ test('call', () => {
+ instance.action = {
+ method: 'METHOD',
+ __key: 'key',
+ __url: '/changes/1/revisions/2/foo~bar',
+ };
+ const sendStub = sandbox.stub().returns(Promise.resolve());
+ sandbox.stub(plugin, 'restApi').returns({
+ send: sendStub,
+ });
+ const payload = {foo: 'foo'};
+ const successStub = sandbox.stub();
+ instance.call(payload, successStub);
+ assert.isTrue(sendStub.calledWith(
+ 'METHOD', '/changes/1/revisions/2/foo~bar', payload));
+ });
+
+ test('call error', done => {
+ instance.action = {
+ method: 'METHOD',
+ __key: 'key',
+ __url: '/changes/1/revisions/2/foo~bar',
+ };
+ const sendStub = sandbox.stub().returns(Promise.reject(new Error('boom')));
+ sandbox.stub(plugin, 'restApi').returns({
+ send: sendStub,
+ });
+ const errorStub = sandbox.stub();
+ document.addEventListener('show-alert', errorStub);
+ instance.call();
+ flush(() => {
+ assert.isTrue(errorStub.calledOnce);
+ assert.equal(errorStub.args[0][0].detail.message,
+ 'Plugin network error: Error: boom');
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
index 5c0d69a..39c3385 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
@@ -19,132 +19,128 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-plugin-endpoints</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-js-api-interface.html"/>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+suite('gr-plugin-endpoints tests', () => {
+ let sandbox;
+ let instance;
+ let pluginFoo;
+ let pluginBar;
+ let domHook;
-<script>
- suite('gr-plugin-endpoints tests', async () => {
- await readyToTest();
- let sandbox;
- let instance;
- let pluginFoo;
- let pluginBar;
- let domHook;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ domHook = {};
+ instance = new GrPluginEndpoints();
+ Gerrit.install(p => { pluginFoo = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/foo.html');
+ instance.registerModule(
+ pluginFoo, 'a-place', 'decorate', 'foo-module', domHook);
+ Gerrit.install(p => { pluginBar = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/bar.html');
+ instance.registerModule(
+ pluginBar, 'a-place', 'style', 'bar-module', domHook);
+ sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
+ });
- setup(() => {
- sandbox = sinon.sandbox.create();
- domHook = {};
- instance = new GrPluginEndpoints();
- Gerrit.install(p => { pluginFoo = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/foo.html');
- instance.registerModule(
- pluginFoo, 'a-place', 'decorate', 'foo-module', domHook);
- Gerrit.install(p => { pluginBar = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/bar.html');
- instance.registerModule(
- pluginBar, 'a-place', 'style', 'bar-module', domHook);
- sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- teardown(() => {
- sandbox.restore();
- });
-
- test('getDetails all', () => {
- assert.deepEqual(instance.getDetails('a-place'), [
- {
- moduleName: 'foo-module',
- plugin: pluginFoo,
- pluginUrl: pluginFoo._url,
- type: 'decorate',
- domHook,
- },
- {
- moduleName: 'bar-module',
- plugin: pluginBar,
- pluginUrl: pluginBar._url,
- type: 'style',
- domHook,
- },
- ]);
- });
-
- test('getDetails by type', () => {
- assert.deepEqual(instance.getDetails('a-place', {type: 'style'}), [
- {
- moduleName: 'bar-module',
- plugin: pluginBar,
- pluginUrl: pluginBar._url,
- type: 'style',
- domHook,
- },
- ]);
- });
-
- test('getDetails by module', () => {
- assert.deepEqual(
- instance.getDetails('a-place', {moduleName: 'foo-module'}),
- [
- {
- moduleName: 'foo-module',
- plugin: pluginFoo,
- pluginUrl: pluginFoo._url,
- type: 'decorate',
- domHook,
- },
- ]);
- });
-
- test('getModules', () => {
- assert.deepEqual(
- instance.getModules('a-place'), ['foo-module', 'bar-module']);
- });
-
- test('getPlugins', () => {
- assert.deepEqual(
- instance.getPlugins('a-place'), [pluginFoo._url]);
- });
-
- test('onNewEndpoint', () => {
- const newModuleStub = sandbox.stub();
- instance.onNewEndpoint('a-place', newModuleStub);
- instance.registerModule(
- pluginFoo, 'a-place', 'replace', 'zaz-module', domHook);
- assert.deepEqual(newModuleStub.lastCall.args[0], {
- moduleName: 'zaz-module',
+ test('getDetails all', () => {
+ assert.deepEqual(instance.getDetails('a-place'), [
+ {
+ moduleName: 'foo-module',
plugin: pluginFoo,
pluginUrl: pluginFoo._url,
- type: 'replace',
+ type: 'decorate',
domHook,
- });
- });
+ },
+ {
+ moduleName: 'bar-module',
+ plugin: pluginBar,
+ pluginUrl: pluginBar._url,
+ type: 'style',
+ domHook,
+ },
+ ]);
+ });
- test('reuse dom hooks', () => {
- instance.registerModule(
- pluginFoo, 'a-place', 'decorate', 'foo-module', domHook);
- assert.deepEqual(instance.getDetails('a-place'), [
- {
- moduleName: 'foo-module',
- plugin: pluginFoo,
- pluginUrl: pluginFoo._url,
- type: 'decorate',
- domHook,
- },
- {
- moduleName: 'bar-module',
- plugin: pluginBar,
- pluginUrl: pluginBar._url,
- type: 'style',
- domHook,
- },
- ]);
+ test('getDetails by type', () => {
+ assert.deepEqual(instance.getDetails('a-place', {type: 'style'}), [
+ {
+ moduleName: 'bar-module',
+ plugin: pluginBar,
+ pluginUrl: pluginBar._url,
+ type: 'style',
+ domHook,
+ },
+ ]);
+ });
+
+ test('getDetails by module', () => {
+ assert.deepEqual(
+ instance.getDetails('a-place', {moduleName: 'foo-module'}),
+ [
+ {
+ moduleName: 'foo-module',
+ plugin: pluginFoo,
+ pluginUrl: pluginFoo._url,
+ type: 'decorate',
+ domHook,
+ },
+ ]);
+ });
+
+ test('getModules', () => {
+ assert.deepEqual(
+ instance.getModules('a-place'), ['foo-module', 'bar-module']);
+ });
+
+ test('getPlugins', () => {
+ assert.deepEqual(
+ instance.getPlugins('a-place'), [pluginFoo._url]);
+ });
+
+ test('onNewEndpoint', () => {
+ const newModuleStub = sandbox.stub();
+ instance.onNewEndpoint('a-place', newModuleStub);
+ instance.registerModule(
+ pluginFoo, 'a-place', 'replace', 'zaz-module', domHook);
+ assert.deepEqual(newModuleStub.lastCall.args[0], {
+ moduleName: 'zaz-module',
+ plugin: pluginFoo,
+ pluginUrl: pluginFoo._url,
+ type: 'replace',
+ domHook,
});
});
+
+ test('reuse dom hooks', () => {
+ instance.registerModule(
+ pluginFoo, 'a-place', 'decorate', 'foo-module', domHook);
+ assert.deepEqual(instance.getDetails('a-place'), [
+ {
+ moduleName: 'foo-module',
+ plugin: pluginFoo,
+ pluginUrl: pluginFoo._url,
+ type: 'decorate',
+ domHook,
+ },
+ {
+ moduleName: 'bar-module',
+ plugin: pluginBar,
+ pluginUrl: pluginBar._url,
+ type: 'style',
+ domHook,
+ },
+ ]);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
index 1a9174f..4440fc8 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-plugin-host</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-js-api-interface.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,531 +30,532 @@
</template>
</test-fixture>
-<script>
- suite('gr-plugin-loader tests', async () => {
- await readyToTest();
- const {PRELOADED_PROTOCOL, PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils;
- let plugin;
- let sandbox;
- let url;
- let sendStub;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+suite('gr-plugin-loader tests', () => {
+ const {PRELOADED_PROTOCOL, PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils;
+ let plugin;
+ let sandbox;
+ let url;
+ let sendStub;
+ setup(() => {
+ window.clock = sinon.useFakeTimers();
+ sandbox = sinon.sandbox.create();
+ sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
+ stub('gr-rest-api-interface', {
+ getAccount() {
+ return Promise.resolve({name: 'Judy Hopps'});
+ },
+ send(...args) {
+ return sendStub(...args);
+ },
+ });
+ sandbox.stub(document.body, 'appendChild');
+ fixture('basic');
+ url = window.location.origin;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ window.clock.restore();
+ Gerrit._testOnly_resetPlugins();
+ });
+
+ test('reuse plugin for install calls', () => {
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+
+ let otherPlugin;
+ Gerrit.install(p => { otherPlugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ assert.strictEqual(plugin, otherPlugin);
+ });
+
+ test('flushes preinstalls if provided', () => {
+ assert.doesNotThrow(() => {
+ Gerrit._testOnly_flushPreinstalls();
+ });
+ window.Gerrit.flushPreinstalls = sandbox.stub();
+ Gerrit._testOnly_flushPreinstalls();
+ assert.isTrue(window.Gerrit.flushPreinstalls.calledOnce);
+ delete window.Gerrit.flushPreinstalls;
+ });
+
+ test('versioning', () => {
+ const callback = sandbox.spy();
+ Gerrit.install(callback, '0.0pre-alpha');
+ assert(callback.notCalled);
+ });
+
+ test('report pluginsLoaded', done => {
+ stub('gr-reporting', {
+ pluginsLoaded() {
+ done();
+ },
+ });
+ Gerrit._loadPlugins([]);
+ });
+
+ test('arePluginsLoaded', done => {
+ assert.isFalse(Gerrit._arePluginsLoaded());
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+
+ Gerrit._loadPlugins(plugins);
+ assert.isFalse(Gerrit._arePluginsLoaded());
+ // Timeout on loading plugins
+ window.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
+
+ flush(() => {
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ done();
+ });
+ });
+
+ test('plugins installed successfully', done => {
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => void 0, undefined, url);
+ });
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+ Gerrit._loadPlugins(plugins);
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar']));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ done();
+ });
+ });
+
+ test('isPluginEnabled and isPluginLoaded', done => {
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => void 0, undefined, url);
+ });
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ 'bar/static/test.js',
+ ];
+ Gerrit._loadPlugins(plugins);
+ assert.isTrue(
+ plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin))
+ );
+
+ flush(() => {
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ assert.isTrue(
+ plugins.every(plugin => Gerrit._pluginLoader.isPluginLoaded(plugin))
+ );
+
+ done();
+ });
+ });
+
+ test('plugins installed mixed result, 1 fail 1 succeed', done => {
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+
+ const alertStub = sandbox.stub();
+ document.addEventListener('show-alert', alertStub);
+
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => {
+ if (url === plugins[0]) {
+ throw new Error('failed');
+ }
+ }, undefined, url);
+ });
+
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ Gerrit._loadPlugins(plugins);
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar']));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ assert.isTrue(alertStub.calledOnce);
+ done();
+ });
+ });
+
+ test('isPluginEnabled and isPluginLoaded for mixed results', done => {
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+
+ const alertStub = sandbox.stub();
+ document.addEventListener('show-alert', alertStub);
+
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => {
+ if (url === plugins[0]) {
+ throw new Error('failed');
+ }
+ }, undefined, url);
+ });
+
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ Gerrit._loadPlugins(plugins);
+ assert.isTrue(
+ plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin))
+ );
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar']));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ assert.isTrue(alertStub.calledOnce);
+ assert.isTrue(Gerrit._pluginLoader.isPluginLoaded(plugins[1]));
+ assert.isFalse(Gerrit._pluginLoader.isPluginLoaded(plugins[0]));
+ done();
+ });
+ });
+
+ test('plugins installed all failed', done => {
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+
+ const alertStub = sandbox.stub();
+ document.addEventListener('show-alert', alertStub);
+
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => {
+ throw new Error('failed');
+ }, undefined, url);
+ });
+
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ Gerrit._loadPlugins(plugins);
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly([]));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ assert.isTrue(alertStub.calledTwice);
+ done();
+ });
+ });
+
+ test('plugins installed failed becasue of wrong version', done => {
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+
+ const alertStub = sandbox.stub();
+ document.addEventListener('show-alert', alertStub);
+
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => {
+ }, url === plugins[0] ? '' : 'alpha', url);
+ });
+
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ Gerrit._loadPlugins(plugins);
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo']));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ assert.isTrue(alertStub.calledOnce);
+ done();
+ });
+ });
+
+ test('multiple assets for same plugin installed successfully', done => {
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => void 0, undefined, url);
+ });
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/foo/static/test2.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+ Gerrit._loadPlugins(plugins);
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar']));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ done();
+ });
+ });
+
+ suite('plugin path and url', () => {
+ let importHtmlPluginStub;
+ let loadJsPluginStub;
setup(() => {
- window.clock = sinon.useFakeTimers();
- sandbox = sinon.sandbox.create();
- sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
- stub('gr-rest-api-interface', {
- getAccount() {
- return Promise.resolve({name: 'Judy Hopps'});
- },
- send(...args) {
- return sendStub(...args);
- },
+ importHtmlPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => {
+ importHtmlPluginStub(url);
});
- sandbox.stub(document.body, 'appendChild');
- fixture('basic');
- url = window.location.origin;
+ loadJsPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => {
+ loadJsPluginStub(url);
+ });
+ });
+
+ test('invalid plugin path', () => {
+ const failToLoadStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_failToLoad', (...args) => {
+ failToLoadStub(...args);
+ });
+
+ Gerrit._loadPlugins([
+ 'foo/bar',
+ ]);
+
+ assert.isTrue(failToLoadStub.calledOnce);
+ assert.isTrue(failToLoadStub.calledWithExactly(
+ 'Unrecognized plugin path foo/bar',
+ 'foo/bar'
+ ));
+ });
+
+ test('relative path for plugins', () => {
+ Gerrit._loadPlugins([
+ 'foo/bar.js',
+ 'foo/bar.html',
+ ]);
+
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(`${url}/foo/bar.html`)
+ );
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`${url}/foo/bar.js`)
+ );
+ });
+
+ test('relative path should honor getBaseUrl', () => {
+ const testUrl = '/test';
+ sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl', () => testUrl);
+
+ Gerrit._loadPlugins([
+ 'foo/bar.js',
+ 'foo/bar.html',
+ ]);
+
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(
+ `${url}${testUrl}/foo/bar.html`
+ )
+ );
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`${url}${testUrl}/foo/bar.js`)
+ );
+ });
+
+ test('absolute path for plugins', () => {
+ Gerrit._loadPlugins([
+ 'http://e.com/foo/bar.js',
+ 'http://e.com/foo/bar.html',
+ ]);
+
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`)
+ );
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`)
+ );
+ });
+ });
+
+ suite('With ASSETS_PATH', () => {
+ let importHtmlPluginStub;
+ let loadJsPluginStub;
+ setup(() => {
+ window.ASSETS_PATH = 'https://cdn.com';
+ importHtmlPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => {
+ importHtmlPluginStub(url);
+ });
+ loadJsPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => {
+ loadJsPluginStub(url);
+ });
});
teardown(() => {
- sandbox.restore();
- window.clock.restore();
- Gerrit._testOnly_resetPlugins();
+ window.ASSETS_PATH = '';
});
- test('reuse plugin for install calls', () => {
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
-
- let otherPlugin;
- Gerrit.install(p => { otherPlugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- assert.strictEqual(plugin, otherPlugin);
- });
-
- test('flushes preinstalls if provided', () => {
- assert.doesNotThrow(() => {
- Gerrit._testOnly_flushPreinstalls();
- });
- window.Gerrit.flushPreinstalls = sandbox.stub();
- Gerrit._testOnly_flushPreinstalls();
- assert.isTrue(window.Gerrit.flushPreinstalls.calledOnce);
- delete window.Gerrit.flushPreinstalls;
- });
-
- test('versioning', () => {
- const callback = sandbox.spy();
- Gerrit.install(callback, '0.0pre-alpha');
- assert(callback.notCalled);
- });
-
- test('report pluginsLoaded', done => {
- stub('gr-reporting', {
- pluginsLoaded() {
- done();
- },
- });
- Gerrit._loadPlugins([]);
- });
-
- test('arePluginsLoaded', done => {
- assert.isFalse(Gerrit._arePluginsLoaded());
- const plugins = [
- 'http://test.com/plugins/foo/static/test.js',
- 'http://test.com/plugins/bar/static/test.js',
- ];
-
- Gerrit._loadPlugins(plugins);
- assert.isFalse(Gerrit._arePluginsLoaded());
- // Timeout on loading plugins
- window.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
-
- flush(() => {
- assert.isTrue(Gerrit._arePluginsLoaded());
- done();
- });
- });
-
- test('plugins installed successfully', done => {
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- Gerrit.install(() => void 0, undefined, url);
- });
- const pluginsLoadedStub = sandbox.stub();
- stub('gr-reporting', {
- pluginsLoaded: (...args) => pluginsLoadedStub(...args),
- });
-
- const plugins = [
- 'http://test.com/plugins/foo/static/test.js',
- 'http://test.com/plugins/bar/static/test.js',
- ];
- Gerrit._loadPlugins(plugins);
-
- flush(() => {
- assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar']));
- assert.isTrue(Gerrit._arePluginsLoaded());
- done();
- });
- });
-
- test('isPluginEnabled and isPluginLoaded', done => {
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- Gerrit.install(() => void 0, undefined, url);
- });
- const pluginsLoadedStub = sandbox.stub();
- stub('gr-reporting', {
- pluginsLoaded: (...args) => pluginsLoadedStub(...args),
- });
-
- const plugins = [
- 'http://test.com/plugins/foo/static/test.js',
- 'http://test.com/plugins/bar/static/test.js',
- 'bar/static/test.js',
- ];
- Gerrit._loadPlugins(plugins);
- assert.isTrue(
- plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin))
- );
-
- flush(() => {
- assert.isTrue(Gerrit._arePluginsLoaded());
- assert.isTrue(
- plugins.every(plugin => Gerrit._pluginLoader.isPluginLoaded(plugin))
- );
-
- done();
- });
- });
-
- test('plugins installed mixed result, 1 fail 1 succeed', done => {
- const plugins = [
- 'http://test.com/plugins/foo/static/test.js',
- 'http://test.com/plugins/bar/static/test.js',
- ];
-
- const alertStub = sandbox.stub();
- document.addEventListener('show-alert', alertStub);
-
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- Gerrit.install(() => {
- if (url === plugins[0]) {
- throw new Error('failed');
- }
- }, undefined, url);
- });
-
- const pluginsLoadedStub = sandbox.stub();
- stub('gr-reporting', {
- pluginsLoaded: (...args) => pluginsLoadedStub(...args),
- });
-
- Gerrit._loadPlugins(plugins);
-
- flush(() => {
- assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar']));
- assert.isTrue(Gerrit._arePluginsLoaded());
- assert.isTrue(alertStub.calledOnce);
- done();
- });
- });
-
- test('isPluginEnabled and isPluginLoaded for mixed results', done => {
- const plugins = [
- 'http://test.com/plugins/foo/static/test.js',
- 'http://test.com/plugins/bar/static/test.js',
- ];
-
- const alertStub = sandbox.stub();
- document.addEventListener('show-alert', alertStub);
-
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- Gerrit.install(() => {
- if (url === plugins[0]) {
- throw new Error('failed');
- }
- }, undefined, url);
- });
-
- const pluginsLoadedStub = sandbox.stub();
- stub('gr-reporting', {
- pluginsLoaded: (...args) => pluginsLoadedStub(...args),
- });
-
- Gerrit._loadPlugins(plugins);
- assert.isTrue(
- plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin))
- );
-
- flush(() => {
- assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar']));
- assert.isTrue(Gerrit._arePluginsLoaded());
- assert.isTrue(alertStub.calledOnce);
- assert.isTrue(Gerrit._pluginLoader.isPluginLoaded(plugins[1]));
- assert.isFalse(Gerrit._pluginLoader.isPluginLoaded(plugins[0]));
- done();
- });
- });
-
- test('plugins installed all failed', done => {
- const plugins = [
- 'http://test.com/plugins/foo/static/test.js',
- 'http://test.com/plugins/bar/static/test.js',
- ];
-
- const alertStub = sandbox.stub();
- document.addEventListener('show-alert', alertStub);
-
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- Gerrit.install(() => {
- throw new Error('failed');
- }, undefined, url);
- });
-
- const pluginsLoadedStub = sandbox.stub();
- stub('gr-reporting', {
- pluginsLoaded: (...args) => pluginsLoadedStub(...args),
- });
-
- Gerrit._loadPlugins(plugins);
-
- flush(() => {
- assert.isTrue(pluginsLoadedStub.calledWithExactly([]));
- assert.isTrue(Gerrit._arePluginsLoaded());
- assert.isTrue(alertStub.calledTwice);
- done();
- });
- });
-
- test('plugins installed failed becasue of wrong version', done => {
- const plugins = [
- 'http://test.com/plugins/foo/static/test.js',
- 'http://test.com/plugins/bar/static/test.js',
- ];
-
- const alertStub = sandbox.stub();
- document.addEventListener('show-alert', alertStub);
-
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- Gerrit.install(() => {
- }, url === plugins[0] ? '' : 'alpha', url);
- });
-
- const pluginsLoadedStub = sandbox.stub();
- stub('gr-reporting', {
- pluginsLoaded: (...args) => pluginsLoadedStub(...args),
- });
-
- Gerrit._loadPlugins(plugins);
-
- flush(() => {
- assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo']));
- assert.isTrue(Gerrit._arePluginsLoaded());
- assert.isTrue(alertStub.calledOnce);
- done();
- });
- });
-
- test('multiple assets for same plugin installed successfully', done => {
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- Gerrit.install(() => void 0, undefined, url);
- });
- const pluginsLoadedStub = sandbox.stub();
- stub('gr-reporting', {
- pluginsLoaded: (...args) => pluginsLoadedStub(...args),
- });
-
- const plugins = [
- 'http://test.com/plugins/foo/static/test.js',
- 'http://test.com/plugins/foo/static/test2.js',
- 'http://test.com/plugins/bar/static/test.js',
- ];
- Gerrit._loadPlugins(plugins);
-
- flush(() => {
- assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar']));
- assert.isTrue(Gerrit._arePluginsLoaded());
- done();
- });
- });
-
- suite('plugin path and url', () => {
- let importHtmlPluginStub;
- let loadJsPluginStub;
- setup(() => {
- importHtmlPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => {
- importHtmlPluginStub(url);
- });
- loadJsPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => {
- loadJsPluginStub(url);
- });
- });
-
- test('invalid plugin path', () => {
- const failToLoadStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_failToLoad', (...args) => {
- failToLoadStub(...args);
- });
-
- Gerrit._loadPlugins([
- 'foo/bar',
- ]);
-
- assert.isTrue(failToLoadStub.calledOnce);
- assert.isTrue(failToLoadStub.calledWithExactly(
- 'Unrecognized plugin path foo/bar',
- 'foo/bar'
- ));
- });
-
- test('relative path for plugins', () => {
- Gerrit._loadPlugins([
- 'foo/bar.js',
- 'foo/bar.html',
- ]);
-
- assert.isTrue(importHtmlPluginStub.calledOnce);
- assert.isTrue(
- importHtmlPluginStub.calledWithExactly(`${url}/foo/bar.html`)
- );
- assert.isTrue(loadJsPluginStub.calledOnce);
- assert.isTrue(
- loadJsPluginStub.calledWithExactly(`${url}/foo/bar.js`)
- );
- });
-
- test('relative path should honor getBaseUrl', () => {
- const testUrl = '/test';
- sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl', () => testUrl);
-
- Gerrit._loadPlugins([
- 'foo/bar.js',
- 'foo/bar.html',
- ]);
-
- assert.isTrue(importHtmlPluginStub.calledOnce);
- assert.isTrue(loadJsPluginStub.calledOnce);
- assert.isTrue(
- importHtmlPluginStub.calledWithExactly(
- `${url}${testUrl}/foo/bar.html`
- )
- );
- assert.isTrue(
- loadJsPluginStub.calledWithExactly(`${url}${testUrl}/foo/bar.js`)
- );
- });
-
- test('absolute path for plugins', () => {
- Gerrit._loadPlugins([
- 'http://e.com/foo/bar.js',
- 'http://e.com/foo/bar.html',
- ]);
-
- assert.isTrue(importHtmlPluginStub.calledOnce);
- assert.isTrue(
- importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`)
- );
- assert.isTrue(loadJsPluginStub.calledOnce);
- assert.isTrue(
- loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`)
- );
- });
- });
-
- suite('With ASSETS_PATH', () => {
- let importHtmlPluginStub;
- let loadJsPluginStub;
- setup(() => {
- window.ASSETS_PATH = 'https://cdn.com';
- importHtmlPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => {
- importHtmlPluginStub(url);
- });
- loadJsPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => {
- loadJsPluginStub(url);
- });
- });
-
- teardown(() => {
- window.ASSETS_PATH = '';
- });
-
- test('Should try load plugins from assets path instead', () => {
- Gerrit._loadPlugins([
- 'foo/bar.js',
- 'foo/bar.html',
- ]);
-
- assert.isTrue(importHtmlPluginStub.calledOnce);
- assert.isTrue(
- importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`)
- );
- assert.isTrue(loadJsPluginStub.calledOnce);
- assert.isTrue(
- loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`));
- });
-
- test('Should honor original path if exists', () => {
- Gerrit._loadPlugins([
- 'http://e.com/foo/bar.html',
- 'http://e.com/foo/bar.js',
- ]);
-
- assert.isTrue(importHtmlPluginStub.calledOnce);
- assert.isTrue(
- importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`)
- );
- assert.isTrue(loadJsPluginStub.calledOnce);
- assert.isTrue(
- loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`));
- });
-
- test('Should try replace current host with assetsPath', () => {
- const host = window.location.origin;
- Gerrit._loadPlugins([
- `${host}/foo/bar.html`,
- `${host}/foo/bar.js`,
- ]);
-
- assert.isTrue(importHtmlPluginStub.calledOnce);
- assert.isTrue(
- importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`)
- );
- assert.isTrue(loadJsPluginStub.calledOnce);
- assert.isTrue(
- loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`));
- });
- });
-
- test('adds js plugins will call the body', () => {
+ test('Should try load plugins from assets path instead', () => {
Gerrit._loadPlugins([
- 'http://e.com/foo/bar.js',
- 'http://e.com/bar/foo.js',
+ 'foo/bar.js',
+ 'foo/bar.html',
]);
- assert.isTrue(document.body.appendChild.calledTwice);
+
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`)
+ );
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`));
});
- test('can call awaitPluginsLoaded multiple times', done => {
- const plugins = [
+ test('Should honor original path if exists', () => {
+ Gerrit._loadPlugins([
+ 'http://e.com/foo/bar.html',
'http://e.com/foo/bar.js',
- 'http://e.com/bar/foo.js',
- ];
+ ]);
- let installed = false;
- function pluginCallback(url) {
- if (url === plugins[1]) {
- installed = true;
- }
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`)
+ );
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`));
+ });
+
+ test('Should try replace current host with assetsPath', () => {
+ const host = window.location.origin;
+ Gerrit._loadPlugins([
+ `${host}/foo/bar.html`,
+ `${host}/foo/bar.js`,
+ ]);
+
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`)
+ );
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`));
+ });
+ });
+
+ test('adds js plugins will call the body', () => {
+ Gerrit._loadPlugins([
+ 'http://e.com/foo/bar.js',
+ 'http://e.com/bar/foo.js',
+ ]);
+ assert.isTrue(document.body.appendChild.calledTwice);
+ });
+
+ test('can call awaitPluginsLoaded multiple times', done => {
+ const plugins = [
+ 'http://e.com/foo/bar.js',
+ 'http://e.com/bar/foo.js',
+ ];
+
+ let installed = false;
+ function pluginCallback(url) {
+ if (url === plugins[1]) {
+ installed = true;
}
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- Gerrit.install(() => pluginCallback(url), undefined, url);
- });
+ }
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => pluginCallback(url), undefined, url);
+ });
- Gerrit._loadPlugins(plugins);
+ Gerrit._loadPlugins(plugins);
+
+ Gerrit.awaitPluginsLoaded().then(() => {
+ assert.isTrue(installed);
Gerrit.awaitPluginsLoaded().then(() => {
- assert.isTrue(installed);
-
- Gerrit.awaitPluginsLoaded().then(() => {
- done();
- });
- });
- });
-
- suite('preloaded plugins', () => {
- test('skips preloaded plugins when load plugins', () => {
- const importHtmlPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_importHtmlPlugin', url => {
- importHtmlPluginStub(url);
- });
- const loadJsPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- loadJsPluginStub(url);
- });
-
- Gerrit._preloadedPlugins = {
- foo: () => void 0,
- bar: () => void 0,
- };
-
- Gerrit._loadPlugins([
- 'http://e.com/plugins/foo.js',
- 'plugins/bar.html',
- 'http://e.com/plugins/test/foo.js',
- ]);
-
- assert.isTrue(importHtmlPluginStub.notCalled);
- assert.isTrue(loadJsPluginStub.calledOnce);
- });
-
- test('isPluginPreloaded', () => {
- Gerrit._preloadedPlugins = {baz: ()=>{}};
- assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('plugins/foo/bar'));
- assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('http://a.com/42'));
- assert.isTrue(
- Gerrit._pluginLoader.isPluginPreloaded(PRELOADED_PROTOCOL + 'baz')
- );
- Gerrit._preloadedPlugins = null;
- });
-
- test('preloaded plugins are installed', () => {
- const installStub = sandbox.stub();
- Gerrit._preloadedPlugins = {foo: installStub};
- Gerrit._pluginLoader.installPreloadedPlugins();
- assert.isTrue(installStub.called);
- const pluginApi = installStub.lastCall.args[0];
- assert.strictEqual(pluginApi.getPluginName(), 'foo');
- });
-
- test('installing preloaded plugin', () => {
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1', 'preloaded:foo');
- assert.strictEqual(plugin.getPluginName(), 'foo');
- assert.strictEqual(plugin.url('/some/thing.html'),
- `${window.location.origin}/plugins/foo/some/thing.html`);
+ done();
});
});
});
+
+ suite('preloaded plugins', () => {
+ test('skips preloaded plugins when load plugins', () => {
+ const importHtmlPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_importHtmlPlugin', url => {
+ importHtmlPluginStub(url);
+ });
+ const loadJsPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ loadJsPluginStub(url);
+ });
+
+ Gerrit._preloadedPlugins = {
+ foo: () => void 0,
+ bar: () => void 0,
+ };
+
+ Gerrit._loadPlugins([
+ 'http://e.com/plugins/foo.js',
+ 'plugins/bar.html',
+ 'http://e.com/plugins/test/foo.js',
+ ]);
+
+ assert.isTrue(importHtmlPluginStub.notCalled);
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ });
+
+ test('isPluginPreloaded', () => {
+ Gerrit._preloadedPlugins = {baz: ()=>{}};
+ assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('plugins/foo/bar'));
+ assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('http://a.com/42'));
+ assert.isTrue(
+ Gerrit._pluginLoader.isPluginPreloaded(PRELOADED_PROTOCOL + 'baz')
+ );
+ Gerrit._preloadedPlugins = null;
+ });
+
+ test('preloaded plugins are installed', () => {
+ const installStub = sandbox.stub();
+ Gerrit._preloadedPlugins = {foo: installStub};
+ Gerrit._pluginLoader.installPreloadedPlugins();
+ assert.isTrue(installStub.called);
+ const pluginApi = installStub.lastCall.args[0];
+ assert.strictEqual(pluginApi.getPluginName(), 'foo');
+ });
+
+ test('installing preloaded plugin', () => {
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1', 'preloaded:foo');
+ assert.strictEqual(plugin.getPluginName(), 'foo');
+ assert.strictEqual(plugin.url('/some/thing.html'),
+ `${window.location.origin}/plugins/foo/some/thing.html`);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
index a486bf1..8963821 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
@@ -19,139 +19,136 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-plugin-rest-api</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-js-api-interface.html"/>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+suite('gr-plugin-rest-api tests', () => {
+ let instance;
+ let sandbox;
+ let getResponseObjectStub;
+ let sendStub;
+ let restApiStub;
-<script>
- suite('gr-plugin-rest-api tests', async () => {
- await readyToTest();
- let instance;
- let sandbox;
- let getResponseObjectStub;
- let sendStub;
- let restApiStub;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ getResponseObjectStub = sandbox.stub().returns(Promise.resolve());
+ sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
+ restApiStub = {
+ getAccount: () => Promise.resolve({name: 'Judy Hopps'}),
+ getResponseObject: getResponseObjectStub,
+ send: sendStub,
+ getLoggedIn: sandbox.stub(),
+ getVersion: sandbox.stub(),
+ getConfig: sandbox.stub(),
+ };
+ stub('gr-rest-api-interface', Object.keys(restApiStub).reduce((a, k) => {
+ a[k] = (...args) => restApiStub[k](...args);
+ return a;
+ }, {}));
+ Gerrit.install(p => {}, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ instance = new GrPluginRestApi();
+ });
- setup(() => {
- sandbox = sinon.sandbox.create();
- getResponseObjectStub = sandbox.stub().returns(Promise.resolve());
- sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
- restApiStub = {
- getAccount: () => Promise.resolve({name: 'Judy Hopps'}),
- getResponseObject: getResponseObjectStub,
- send: sendStub,
- getLoggedIn: sandbox.stub(),
- getVersion: sandbox.stub(),
- getConfig: sandbox.stub(),
- };
- stub('gr-rest-api-interface', Object.keys(restApiStub).reduce((a, k) => {
- a[k] = (...args) => restApiStub[k](...args);
- return a;
- }, {}));
- Gerrit.install(p => {}, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- instance = new GrPluginRestApi();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- teardown(() => {
- sandbox.restore();
- });
-
- test('fetch', () => {
- const payload = {foo: 'foo'};
- return instance.fetch('HTTP_METHOD', '/url', payload).then(r => {
- assert.isTrue(sendStub.calledWith('HTTP_METHOD', '/url', payload));
- assert.equal(r.status, 200);
- assert.isFalse(getResponseObjectStub.called);
- });
- });
-
- test('send', () => {
- const payload = {foo: 'foo'};
- const response = {bar: 'bar'};
- getResponseObjectStub.returns(Promise.resolve(response));
- return instance.send('HTTP_METHOD', '/url', payload).then(r => {
- assert.isTrue(sendStub.calledWith('HTTP_METHOD', '/url', payload));
- assert.strictEqual(r, response);
- });
- });
-
- test('get', () => {
- const response = {foo: 'foo'};
- getResponseObjectStub.returns(Promise.resolve(response));
- return instance.get('/url').then(r => {
- assert.isTrue(sendStub.calledWith('GET', '/url'));
- assert.strictEqual(r, response);
- });
- });
-
- test('post', () => {
- const payload = {foo: 'foo'};
- const response = {bar: 'bar'};
- getResponseObjectStub.returns(Promise.resolve(response));
- return instance.post('/url', payload).then(r => {
- assert.isTrue(sendStub.calledWith('POST', '/url', payload));
- assert.strictEqual(r, response);
- });
- });
-
- test('put', () => {
- const payload = {foo: 'foo'};
- const response = {bar: 'bar'};
- getResponseObjectStub.returns(Promise.resolve(response));
- return instance.put('/url', payload).then(r => {
- assert.isTrue(sendStub.calledWith('PUT', '/url', payload));
- assert.strictEqual(r, response);
- });
- });
-
- test('delete works', () => {
- const response = {status: 204};
- sendStub.returns(Promise.resolve(response));
- return instance.delete('/url').then(r => {
- assert.isTrue(sendStub.calledWith('DELETE', '/url'));
- assert.strictEqual(r, response);
- });
- });
-
- test('delete fails', () => {
- sendStub.returns(Promise.resolve(
- {status: 400, text() { return Promise.resolve('text'); }}));
- return instance.delete('/url').then(r => {
- throw new Error('Should not resolve');
- })
- .catch(err => {
- assert.isTrue(sendStub.calledWith('DELETE', '/url'));
- assert.equal('text', err.message);
- });
- });
-
- test('getLoggedIn', () => {
- restApiStub.getLoggedIn.returns(Promise.resolve(true));
- return instance.getLoggedIn().then(result => {
- assert.isTrue(restApiStub.getLoggedIn.calledOnce);
- assert.isTrue(result);
- });
- });
-
- test('getVersion', () => {
- restApiStub.getVersion.returns(Promise.resolve('foo bar'));
- return instance.getVersion().then(result => {
- assert.isTrue(restApiStub.getVersion.calledOnce);
- assert.equal(result, 'foo bar');
- });
- });
-
- test('getConfig', () => {
- restApiStub.getConfig.returns(Promise.resolve('foo bar'));
- return instance.getConfig().then(result => {
- assert.isTrue(restApiStub.getConfig.calledOnce);
- assert.equal(result, 'foo bar');
- });
+ test('fetch', () => {
+ const payload = {foo: 'foo'};
+ return instance.fetch('HTTP_METHOD', '/url', payload).then(r => {
+ assert.isTrue(sendStub.calledWith('HTTP_METHOD', '/url', payload));
+ assert.equal(r.status, 200);
+ assert.isFalse(getResponseObjectStub.called);
});
});
+
+ test('send', () => {
+ const payload = {foo: 'foo'};
+ const response = {bar: 'bar'};
+ getResponseObjectStub.returns(Promise.resolve(response));
+ return instance.send('HTTP_METHOD', '/url', payload).then(r => {
+ assert.isTrue(sendStub.calledWith('HTTP_METHOD', '/url', payload));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('get', () => {
+ const response = {foo: 'foo'};
+ getResponseObjectStub.returns(Promise.resolve(response));
+ return instance.get('/url').then(r => {
+ assert.isTrue(sendStub.calledWith('GET', '/url'));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('post', () => {
+ const payload = {foo: 'foo'};
+ const response = {bar: 'bar'};
+ getResponseObjectStub.returns(Promise.resolve(response));
+ return instance.post('/url', payload).then(r => {
+ assert.isTrue(sendStub.calledWith('POST', '/url', payload));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('put', () => {
+ const payload = {foo: 'foo'};
+ const response = {bar: 'bar'};
+ getResponseObjectStub.returns(Promise.resolve(response));
+ return instance.put('/url', payload).then(r => {
+ assert.isTrue(sendStub.calledWith('PUT', '/url', payload));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('delete works', () => {
+ const response = {status: 204};
+ sendStub.returns(Promise.resolve(response));
+ return instance.delete('/url').then(r => {
+ assert.isTrue(sendStub.calledWith('DELETE', '/url'));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('delete fails', () => {
+ sendStub.returns(Promise.resolve(
+ {status: 400, text() { return Promise.resolve('text'); }}));
+ return instance.delete('/url').then(r => {
+ throw new Error('Should not resolve');
+ })
+ .catch(err => {
+ assert.isTrue(sendStub.calledWith('DELETE', '/url'));
+ assert.equal('text', err.message);
+ });
+ });
+
+ test('getLoggedIn', () => {
+ restApiStub.getLoggedIn.returns(Promise.resolve(true));
+ return instance.getLoggedIn().then(result => {
+ assert.isTrue(restApiStub.getLoggedIn.calledOnce);
+ assert.isTrue(result);
+ });
+ });
+
+ test('getVersion', () => {
+ restApiStub.getVersion.returns(Promise.resolve('foo bar'));
+ return instance.getVersion().then(result => {
+ assert.isTrue(restApiStub.getVersion.calledOnce);
+ assert.equal(result, 'foo bar');
+ });
+ });
+
+ test('getConfig', () => {
+ restApiStub.getConfig.returns(Promise.resolve('foo bar'));
+ return instance.getConfig().then(result => {
+ assert.isTrue(restApiStub.getConfig.calledOnce);
+ assert.equal(result, 'foo bar');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index 069aa7f..a31e5dc 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -151,7 +151,7 @@
};
Plugin.prototype.screenUrl = function(opt_screenName) {
- const origin = this._url.origin;
+ const origin = location.origin;
const base = Gerrit.BaseUrlBehavior.getBaseUrl();
const tokenPart = opt_screenName ? '/' + opt_screenName : '';
return `${origin}${base}/x/${this.getPluginName()}${tokenPart}`;
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html
deleted file mode 100644
index b17b251..0000000
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html
+++ /dev/null
@@ -1,132 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../styles/gr-voting-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../gr-account-label/gr-account-label.html">
-<link rel="import" href="../gr-account-chip/gr-account-chip.html">
-<link rel="import" href="../gr-button/gr-button.html">
-<link rel="import" href="../gr-icons/gr-icons.html">
-<link rel="import" href="../gr-label/gr-label.html">
-<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-label-info">
- <template strip-whitespace>
- <style include="gr-voting-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- .placeholder {
- color: var(--deemphasized-text-color);
- padding-top: var(--spacing-xs);
- }
- .hidden {
- display: none;
- }
- .voteChip {
- display: flex;
- justify-content: center;
- margin-right: var(--spacing-s);
- padding: 0;
- @apply --vote-chip-styles;
- border-width: 0;
- }
- .max {
- background-color: var(--vote-color-approved);
- }
- .min {
- background-color: var(--vote-color-rejected);
- }
- .positive {
- background-color: var(--vote-color-recommended);
- }
- .negative {
- background-color: var(--vote-color-disliked);
- }
- .hidden {
- display: none;
- }
- td {
- vertical-align: top;
- }
- tr {
- min-height: var(--line-height-normal);
- }
- gr-button {
- vertical-align: top;
- --gr-button: {
- height: var(--line-height-normal);
- width: var(--line-height-normal);
- padding: 0;
- }
- }
- gr-button[disabled] iron-icon {
- color: var(--border-color);
- }
- gr-account-chip {
- margin-right: var(--spacing-xs);
- }
- iron-icon {
- height: calc(var(--line-height-normal) - 2px);
- width: calc(var(--line-height-normal) - 2px);
- }
- .labelValueContainer:not(:first-of-type) td {
- padding-top: var(--spacing-s);
- }
- </style>
- <table>
- <p class$="placeholder [[_computeShowPlaceholder(labelInfo, change.labels.*)]]">
- No votes.
- </p>
- <template
- is="dom-repeat"
- items="[[_mapLabelInfo(labelInfo, account, change.labels.*)]]"
- as="mappedLabel">
- <tr class="labelValueContainer">
- <td>
- <gr-label
- has-tooltip
- title="[[_computeValueTooltip(labelInfo, mappedLabel.value)]]"
- class$="[[mappedLabel.className]] voteChip">
- [[mappedLabel.value]]
- </gr-label>
- </td>
- <td>
- <gr-account-chip
- account="[[mappedLabel.account]]"
- transparent-background></gr-account-chip>
- </td>
- <td>
- <gr-button
- link
- aria-label="Remove"
- on-click="_onDeleteVote"
- tooltip="Remove vote"
- data-account-id$="[[mappedLabel.account._account_id]]"
- class$="deleteBtn [[_computeDeleteClass(mappedLabel.account, mutable, change)]]">
- <iron-icon icon="gr-icons:delete"></iron-icon>
- </gr-button>
- </td>
- </tr>
- </template>
- </table>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-label-info.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
index 6e99e01..ad9b852 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
@@ -14,156 +14,170 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrLabelInfo extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-label-info'; }
+import '../../../styles/gr-voting-styles.js';
+import '../../../styles/shared-styles.js';
+import '../gr-account-label/gr-account-label.js';
+import '../gr-account-chip/gr-account-chip.js';
+import '../gr-button/gr-button.js';
+import '../gr-icons/gr-icons.js';
+import '../gr-label/gr-label.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-label-info_html.js';
- static get properties() {
- return {
- labelInfo: Object,
- label: String,
- /** @type {?} */
- change: Object,
- account: Object,
- mutable: Boolean,
- };
- }
+/** @extends Polymer.Element */
+class GrLabelInfo extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /**
- * @param {!Object} labelInfo
- * @param {!Object} account
- * @param {Object} changeLabelsRecord not used, but added as a parameter in
- * order to trigger computation when a label is removed from the change.
- */
- _mapLabelInfo(labelInfo, account, changeLabelsRecord) {
- const result = [];
- if (!labelInfo || !account) { return result; }
- if (!labelInfo.values) {
- if (labelInfo.rejected || labelInfo.approved) {
- const ok = labelInfo.approved || !labelInfo.rejected;
- return [{
- value: ok ? '👍️' : '👎️',
- className: ok ? 'positive' : 'negative',
- account: ok ? labelInfo.approved : labelInfo.rejected,
- }];
- }
- return result;
- }
- // Sort votes by positivity.
- const votes = (labelInfo.all || []).sort((a, b) => a.value - b.value);
- const values = Object.keys(labelInfo.values);
- for (const label of votes) {
- if (label.value && label.value != labelInfo.default_value) {
- let labelClassName;
- let labelValPrefix = '';
- if (label.value > 0) {
- labelValPrefix = '+';
- if (parseInt(label.value, 10) ===
- parseInt(values[values.length - 1], 10)) {
- labelClassName = 'max';
- } else {
- labelClassName = 'positive';
- }
- } else if (label.value < 0) {
- if (parseInt(label.value, 10) === parseInt(values[0], 10)) {
- labelClassName = 'min';
- } else {
- labelClassName = 'negative';
- }
- }
- const formattedLabel = {
- value: labelValPrefix + label.value,
- className: labelClassName,
- account: label,
- };
- if (label._account_id === account._account_id) {
- // Put self-votes at the top.
- result.unshift(formattedLabel);
- } else {
- result.push(formattedLabel);
- }
- }
+ static get is() { return 'gr-label-info'; }
+
+ static get properties() {
+ return {
+ labelInfo: Object,
+ label: String,
+ /** @type {?} */
+ change: Object,
+ account: Object,
+ mutable: Boolean,
+ };
+ }
+
+ /**
+ * @param {!Object} labelInfo
+ * @param {!Object} account
+ * @param {Object} changeLabelsRecord not used, but added as a parameter in
+ * order to trigger computation when a label is removed from the change.
+ */
+ _mapLabelInfo(labelInfo, account, changeLabelsRecord) {
+ const result = [];
+ if (!labelInfo || !account) { return result; }
+ if (!labelInfo.values) {
+ if (labelInfo.rejected || labelInfo.approved) {
+ const ok = labelInfo.approved || !labelInfo.rejected;
+ return [{
+ value: ok ? '👍️' : '👎️',
+ className: ok ? 'positive' : 'negative',
+ account: ok ? labelInfo.approved : labelInfo.rejected,
+ }];
}
return result;
}
-
- /**
- * A user is able to delete a vote iff the mutable property is true and the
- * reviewer that left the vote exists in the list of removable_reviewers
- * received from the backend.
- *
- * @param {!Object} reviewer An object describing the reviewer that left the
- * vote.
- * @param {boolean} mutable
- * @param {!Object} change
- */
- _computeDeleteClass(reviewer, mutable, change) {
- if (!mutable || !change || !change.removable_reviewers) {
- return 'hidden';
- }
- const removable = change.removable_reviewers;
- if (removable.find(r => r._account_id === reviewer._account_id)) {
- return '';
- }
- return 'hidden';
- }
-
- /**
- * Closure annotation for Polymer.prototype.splice is off.
- * For now, supressing annotations.
- *
- * @suppress {checkTypes} */
- _onDeleteVote(e) {
- e.preventDefault();
- let target = Polymer.dom(e).rootTarget;
- while (!target.classList.contains('deleteBtn')) {
- if (!target.parentElement) { return; }
- target = target.parentElement;
- }
-
- target.disabled = true;
- const accountID = parseInt(target.getAttribute('data-account-id'), 10);
- this._xhrPromise =
- this.$.restAPI.deleteVote(this.change._number, accountID, this.label)
- .then(response => {
- target.disabled = false;
- if (!response.ok) { return; }
- Gerrit.Nav.navigateToChange(this.change);
- })
- .catch(err => {
- target.disabled = false;
- return;
- });
- }
-
- _computeValueTooltip(labelInfo, score) {
- if (!labelInfo || !labelInfo.values || !labelInfo.values[score]) {
- return '';
- }
- return labelInfo.values[score];
- }
-
- /**
- * @param {!Object} labelInfo
- * @param {Object} changeLabelsRecord not used, but added as a parameter in
- * order to trigger computation when a label is removed from the change.
- */
- _computeShowPlaceholder(labelInfo, changeLabelsRecord) {
- if (labelInfo && labelInfo.all) {
- for (const label of labelInfo.all) {
- if (label.value && label.value != labelInfo.default_value) {
- return 'hidden';
+ // Sort votes by positivity.
+ const votes = (labelInfo.all || []).sort((a, b) => a.value - b.value);
+ const values = Object.keys(labelInfo.values);
+ for (const label of votes) {
+ if (label.value && label.value != labelInfo.default_value) {
+ let labelClassName;
+ let labelValPrefix = '';
+ if (label.value > 0) {
+ labelValPrefix = '+';
+ if (parseInt(label.value, 10) ===
+ parseInt(values[values.length - 1], 10)) {
+ labelClassName = 'max';
+ } else {
+ labelClassName = 'positive';
+ }
+ } else if (label.value < 0) {
+ if (parseInt(label.value, 10) === parseInt(values[0], 10)) {
+ labelClassName = 'min';
+ } else {
+ labelClassName = 'negative';
}
}
+ const formattedLabel = {
+ value: labelValPrefix + label.value,
+ className: labelClassName,
+ account: label,
+ };
+ if (label._account_id === account._account_id) {
+ // Put self-votes at the top.
+ result.unshift(formattedLabel);
+ } else {
+ result.push(formattedLabel);
+ }
}
- return '';
}
+ return result;
}
- customElements.define(GrLabelInfo.is, GrLabelInfo);
-})();
+ /**
+ * A user is able to delete a vote iff the mutable property is true and the
+ * reviewer that left the vote exists in the list of removable_reviewers
+ * received from the backend.
+ *
+ * @param {!Object} reviewer An object describing the reviewer that left the
+ * vote.
+ * @param {boolean} mutable
+ * @param {!Object} change
+ */
+ _computeDeleteClass(reviewer, mutable, change) {
+ if (!mutable || !change || !change.removable_reviewers) {
+ return 'hidden';
+ }
+ const removable = change.removable_reviewers;
+ if (removable.find(r => r._account_id === reviewer._account_id)) {
+ return '';
+ }
+ return 'hidden';
+ }
+
+ /**
+ * Closure annotation for Polymer.prototype.splice is off.
+ * For now, supressing annotations.
+ *
+ * @suppress {checkTypes} */
+ _onDeleteVote(e) {
+ e.preventDefault();
+ let target = dom(e).rootTarget;
+ while (!target.classList.contains('deleteBtn')) {
+ if (!target.parentElement) { return; }
+ target = target.parentElement;
+ }
+
+ target.disabled = true;
+ const accountID = parseInt(target.getAttribute('data-account-id'), 10);
+ this._xhrPromise =
+ this.$.restAPI.deleteVote(this.change._number, accountID, this.label)
+ .then(response => {
+ target.disabled = false;
+ if (!response.ok) { return; }
+ Gerrit.Nav.navigateToChange(this.change);
+ })
+ .catch(err => {
+ target.disabled = false;
+ return;
+ });
+ }
+
+ _computeValueTooltip(labelInfo, score) {
+ if (!labelInfo || !labelInfo.values || !labelInfo.values[score]) {
+ return '';
+ }
+ return labelInfo.values[score];
+ }
+
+ /**
+ * @param {!Object} labelInfo
+ * @param {Object} changeLabelsRecord not used, but added as a parameter in
+ * order to trigger computation when a label is removed from the change.
+ */
+ _computeShowPlaceholder(labelInfo, changeLabelsRecord) {
+ if (labelInfo && labelInfo.all) {
+ for (const label of labelInfo.all) {
+ if (label.value && label.value != labelInfo.default_value) {
+ return 'hidden';
+ }
+ }
+ }
+ return '';
+ }
+}
+
+customElements.define(GrLabelInfo.is, GrLabelInfo);
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.js b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.js
new file mode 100644
index 0000000..d19467b
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.js
@@ -0,0 +1,105 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="gr-voting-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="shared-styles">
+ .placeholder {
+ color: var(--deemphasized-text-color);
+ padding-top: var(--spacing-xs);
+ }
+ .hidden {
+ display: none;
+ }
+ .voteChip {
+ display: flex;
+ justify-content: center;
+ margin-right: var(--spacing-s);
+ padding: 0;
+ @apply --vote-chip-styles;
+ border-width: 0;
+ }
+ .max {
+ background-color: var(--vote-color-approved);
+ }
+ .min {
+ background-color: var(--vote-color-rejected);
+ }
+ .positive {
+ background-color: var(--vote-color-recommended);
+ }
+ .negative {
+ background-color: var(--vote-color-disliked);
+ }
+ .hidden {
+ display: none;
+ }
+ td {
+ vertical-align: top;
+ }
+ tr {
+ min-height: var(--line-height-normal);
+ }
+ gr-button {
+ vertical-align: top;
+ --gr-button: {
+ height: var(--line-height-normal);
+ width: var(--line-height-normal);
+ padding: 0;
+ }
+ }
+ gr-button[disabled] iron-icon {
+ color: var(--border-color);
+ }
+ gr-account-chip {
+ margin-right: var(--spacing-xs);
+ }
+ iron-icon {
+ height: calc(var(--line-height-normal) - 2px);
+ width: calc(var(--line-height-normal) - 2px);
+ }
+ .labelValueContainer:not(:first-of-type) td {
+ padding-top: var(--spacing-s);
+ }
+ </style>
+ <p class\$="placeholder [[_computeShowPlaceholder(labelInfo, change.labels.*)]]">
+ No votes.
+ </p><table>
+
+ <template is="dom-repeat" items="[[_mapLabelInfo(labelInfo, account, change.labels.*)]]" as="mappedLabel">
+ <tr class="labelValueContainer">
+ <td>
+ <gr-label has-tooltip="" title="[[_computeValueTooltip(labelInfo, mappedLabel.value)]]" class\$="[[mappedLabel.className]] voteChip">
+ [[mappedLabel.value]]
+ </gr-label>
+ </td>
+ <td>
+ <gr-account-chip account="[[mappedLabel.account]]" transparent-background=""></gr-account-chip>
+ </td>
+ <td>
+ <gr-button link="" aria-label="Remove" on-click="_onDeleteVote" tooltip="Remove vote" data-account-id\$="[[mappedLabel.account._account_id]]" class\$="deleteBtn [[_computeDeleteClass(mappedLabel.account, mutable, change)]]">
+ <iron-icon icon="gr-icons:delete"></iron-icon>
+ </gr-button>
+ </td>
+ </tr>
+ </template>
+ </table>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
index 013a6ee..f7e9a70 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
@@ -18,15 +18,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-label-info</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-label-info.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -34,205 +29,207 @@
</template>
</test-fixture>
-<script>
- suite('gr-account-link tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-label-info.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-account-link tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ // Needed to trigger computed bindings.
+ element.account = {};
+ element.change = {labels: {}};
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('remove reviewer votes', () => {
setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- // Needed to trigger computed bindings.
- element.account = {};
- element.change = {labels: {}};
+ sandbox.stub(element, '_computeValueTooltip').returns('');
+ element.account = {
+ _account_id: 1,
+ name: 'bojack',
+ };
+ const test = {
+ all: [{_account_id: 1, name: 'bojack', value: 1}],
+ default_value: 0,
+ values: [],
+ };
+ element.change = {
+ _number: 42,
+ change_id: 'the id',
+ actions: [],
+ topic: 'the topic',
+ status: 'NEW',
+ submit_type: 'CHERRY_PICK',
+ labels: {test},
+ removable_reviewers: [],
+ };
+ element.labelInfo = test;
+ element.label = 'test';
+
+ flushAsynchronousOperations();
});
- teardown(() => {
- sandbox.restore();
+ test('_computeCanDeleteVote', () => {
+ element.mutable = false;
+ const button = element.shadowRoot
+ .querySelector('gr-button');
+ assert.isTrue(isHidden(button));
+ element.change.removable_reviewers = [element.account];
+ element.mutable = true;
+ assert.isFalse(isHidden(button));
});
- suite('remove reviewer votes', () => {
- setup(() => {
- sandbox.stub(element, '_computeValueTooltip').returns('');
- element.account = {
- _account_id: 1,
- name: 'bojack',
- };
- const test = {
- all: [{_account_id: 1, name: 'bojack', value: 1}],
- default_value: 0,
- values: [],
- };
- element.change = {
- _number: 42,
- change_id: 'the id',
- actions: [],
- topic: 'the topic',
- status: 'NEW',
- submit_type: 'CHERRY_PICK',
- labels: {test},
- removable_reviewers: [],
- };
- element.labelInfo = test;
- element.label = 'test';
+ test('deletes votes', () => {
+ const deleteResponse = Promise.resolve({ok: true});
+ const deleteStub = sandbox.stub(
+ element.$.restAPI, 'deleteVote').returns(deleteResponse);
- flushAsynchronousOperations();
+ element.change.removable_reviewers = [element.account];
+ element.change.labels.test.recommended = {_account_id: 1};
+ element.mutable = true;
+ const button = element.shadowRoot
+ .querySelector('gr-button');
+ MockInteractions.tap(button);
+ assert.isTrue(button.disabled);
+ return deleteResponse.then(() => {
+ assert.isFalse(button.disabled);
+ assert.isTrue(deleteStub.calledWithExactly(42, 1, 'test'));
});
-
- test('_computeCanDeleteVote', () => {
- element.mutable = false;
- const button = element.shadowRoot
- .querySelector('gr-button');
- assert.isTrue(isHidden(button));
- element.change.removable_reviewers = [element.account];
- element.mutable = true;
- assert.isFalse(isHidden(button));
- });
-
- test('deletes votes', () => {
- const deleteResponse = Promise.resolve({ok: true});
- const deleteStub = sandbox.stub(
- element.$.restAPI, 'deleteVote').returns(deleteResponse);
-
- element.change.removable_reviewers = [element.account];
- element.change.labels.test.recommended = {_account_id: 1};
- element.mutable = true;
- const button = element.shadowRoot
- .querySelector('gr-button');
- MockInteractions.tap(button);
- assert.isTrue(button.disabled);
- return deleteResponse.then(() => {
- assert.isFalse(button.disabled);
- assert.isTrue(deleteStub.calledWithExactly(42, 1, 'test'));
- });
- });
- });
-
- suite('label color and order', () => {
- test('valueless label rejected', () => {
- element.labelInfo = {rejected: {name: 'someone'}};
- flushAsynchronousOperations();
- const labels = Polymer.dom(element.root).querySelectorAll('gr-label');
- assert.isTrue(labels[0].classList.contains('negative'));
- });
-
- test('valueless label approved', () => {
- element.labelInfo = {approved: {name: 'someone'}};
- flushAsynchronousOperations();
- const labels = Polymer.dom(element.root).querySelectorAll('gr-label');
- assert.isTrue(labels[0].classList.contains('positive'));
- });
-
- test('-2 to +2', () => {
- element.labelInfo = {
- all: [
- {value: 2, name: 'user 2'},
- {value: 1, name: 'user 1'},
- {value: -1, name: 'user 3'},
- {value: -2, name: 'user 4'},
- ],
- values: {
- '-2': 'Awful',
- '-1': 'Don\'t submit as-is',
- ' 0': 'No score',
- '+1': 'Looks good to me',
- '+2': 'Ready to submit',
- },
- };
- flushAsynchronousOperations();
- const labels = Polymer.dom(element.root).querySelectorAll('gr-label');
- assert.isTrue(labels[0].classList.contains('max'));
- assert.isTrue(labels[1].classList.contains('positive'));
- assert.isTrue(labels[2].classList.contains('negative'));
- assert.isTrue(labels[3].classList.contains('min'));
- });
-
- test('-1 to +1', () => {
- element.labelInfo = {
- all: [
- {value: 1, name: 'user 1'},
- {value: -1, name: 'user 2'},
- ],
- values: {
- '-1': 'Don\'t submit as-is',
- ' 0': 'No score',
- '+1': 'Looks good to me',
- },
- };
- flushAsynchronousOperations();
- const labels = Polymer.dom(element.root).querySelectorAll('gr-label');
- assert.isTrue(labels[0].classList.contains('max'));
- assert.isTrue(labels[1].classList.contains('min'));
- });
-
- test('0 to +2', () => {
- element.labelInfo = {
- all: [
- {value: 1, name: 'user 2'},
- {value: 2, name: 'user '},
- ],
- values: {
- ' 0': 'Don\'t submit as-is',
- '+1': 'No score',
- '+2': 'Looks good to me',
- },
- };
- flushAsynchronousOperations();
- const labels = Polymer.dom(element.root).querySelectorAll('gr-label');
- assert.isTrue(labels[0].classList.contains('max'));
- assert.isTrue(labels[1].classList.contains('positive'));
- });
-
- test('self votes at top', () => {
- element.account = {
- _account_id: 1,
- name: 'bojack',
- };
- element.labelInfo = {
- all: [
- {value: 1, name: 'user 1', _account_id: 2},
- {value: -1, name: 'bojack', _account_id: 1},
- ],
- values: {
- '-1': 'Don\'t submit as-is',
- ' 0': 'No score',
- '+1': 'Looks good to me',
- },
- };
- flushAsynchronousOperations();
- const chips =
- Polymer.dom(element.root).querySelectorAll('gr-account-chip');
- assert.equal(chips[0].account._account_id, element.account._account_id);
- });
- });
-
- test('_computeValueTooltip', () => {
- // Existing label.
- let labelInfo = {values: {0: 'Baz'}};
- let score = '0';
- assert.equal(element._computeValueTooltip(labelInfo, score), 'Baz');
-
- // Non-exsistent score.
- score = '2';
- assert.equal(element._computeValueTooltip(labelInfo, score), '');
-
- // No values on label.
- labelInfo = {values: {}};
- score = '0';
- assert.equal(element._computeValueTooltip(labelInfo, score), '');
- });
-
- test('placeholder', () => {
- element.labelInfo = {};
- assert.isFalse(isHidden(element.shadowRoot
- .querySelector('.placeholder')));
- element.labelInfo = {all: []};
- assert.isFalse(isHidden(element.shadowRoot
- .querySelector('.placeholder')));
- element.labelInfo = {all: [{value: 1}]};
- assert.isTrue(isHidden(element.shadowRoot
- .querySelector('.placeholder')));
});
});
+
+ suite('label color and order', () => {
+ test('valueless label rejected', () => {
+ element.labelInfo = {rejected: {name: 'someone'}};
+ flushAsynchronousOperations();
+ const labels = dom(element.root).querySelectorAll('gr-label');
+ assert.isTrue(labels[0].classList.contains('negative'));
+ });
+
+ test('valueless label approved', () => {
+ element.labelInfo = {approved: {name: 'someone'}};
+ flushAsynchronousOperations();
+ const labels = dom(element.root).querySelectorAll('gr-label');
+ assert.isTrue(labels[0].classList.contains('positive'));
+ });
+
+ test('-2 to +2', () => {
+ element.labelInfo = {
+ all: [
+ {value: 2, name: 'user 2'},
+ {value: 1, name: 'user 1'},
+ {value: -1, name: 'user 3'},
+ {value: -2, name: 'user 4'},
+ ],
+ values: {
+ '-2': 'Awful',
+ '-1': 'Don\'t submit as-is',
+ ' 0': 'No score',
+ '+1': 'Looks good to me',
+ '+2': 'Ready to submit',
+ },
+ };
+ flushAsynchronousOperations();
+ const labels = dom(element.root).querySelectorAll('gr-label');
+ assert.isTrue(labels[0].classList.contains('max'));
+ assert.isTrue(labels[1].classList.contains('positive'));
+ assert.isTrue(labels[2].classList.contains('negative'));
+ assert.isTrue(labels[3].classList.contains('min'));
+ });
+
+ test('-1 to +1', () => {
+ element.labelInfo = {
+ all: [
+ {value: 1, name: 'user 1'},
+ {value: -1, name: 'user 2'},
+ ],
+ values: {
+ '-1': 'Don\'t submit as-is',
+ ' 0': 'No score',
+ '+1': 'Looks good to me',
+ },
+ };
+ flushAsynchronousOperations();
+ const labels = dom(element.root).querySelectorAll('gr-label');
+ assert.isTrue(labels[0].classList.contains('max'));
+ assert.isTrue(labels[1].classList.contains('min'));
+ });
+
+ test('0 to +2', () => {
+ element.labelInfo = {
+ all: [
+ {value: 1, name: 'user 2'},
+ {value: 2, name: 'user '},
+ ],
+ values: {
+ ' 0': 'Don\'t submit as-is',
+ '+1': 'No score',
+ '+2': 'Looks good to me',
+ },
+ };
+ flushAsynchronousOperations();
+ const labels = dom(element.root).querySelectorAll('gr-label');
+ assert.isTrue(labels[0].classList.contains('max'));
+ assert.isTrue(labels[1].classList.contains('positive'));
+ });
+
+ test('self votes at top', () => {
+ element.account = {
+ _account_id: 1,
+ name: 'bojack',
+ };
+ element.labelInfo = {
+ all: [
+ {value: 1, name: 'user 1', _account_id: 2},
+ {value: -1, name: 'bojack', _account_id: 1},
+ ],
+ values: {
+ '-1': 'Don\'t submit as-is',
+ ' 0': 'No score',
+ '+1': 'Looks good to me',
+ },
+ };
+ flushAsynchronousOperations();
+ const chips =
+ dom(element.root).querySelectorAll('gr-account-chip');
+ assert.equal(chips[0].account._account_id, element.account._account_id);
+ });
+ });
+
+ test('_computeValueTooltip', () => {
+ // Existing label.
+ let labelInfo = {values: {0: 'Baz'}};
+ let score = '0';
+ assert.equal(element._computeValueTooltip(labelInfo, score), 'Baz');
+
+ // Non-exsistent score.
+ score = '2';
+ assert.equal(element._computeValueTooltip(labelInfo, score), '');
+
+ // No values on label.
+ labelInfo = {values: {}};
+ score = '0';
+ assert.equal(element._computeValueTooltip(labelInfo, score), '');
+ });
+
+ test('placeholder', () => {
+ element.labelInfo = {};
+ assert.isFalse(isHidden(element.shadowRoot
+ .querySelector('.placeholder')));
+ element.labelInfo = {all: []};
+ assert.isFalse(isHidden(element.shadowRoot
+ .querySelector('.placeholder')));
+ element.labelInfo = {all: [{value: 1}]};
+ assert.isTrue(isHidden(element.shadowRoot
+ .querySelector('.placeholder')));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label.html b/polygerrit-ui/app/elements/shared/gr-label/gr-label.html
deleted file mode 100644
index 55ecc98..0000000
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.html
+++ /dev/null
@@ -1,24 +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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
-<dom-module id="gr-label">
- <template strip-whitespace>
- <slot></slot>
- </template>
- <script src="gr-label.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
index b594757..c797919 100644
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
@@ -14,20 +14,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.TooltipMixin
- * @extends Polymer.Element
- */
- class GrLabel extends Polymer.mixinBehaviors( [
- Gerrit.TooltipBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-label'; }
- }
+import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-label_html.js';
- customElements.define(GrLabel.is, GrLabel);
-})();
+/**
+ * @appliesMixin Gerrit.TooltipMixin
+ * @extends Polymer.Element
+ */
+class GrLabel extends mixinBehaviors( [
+ Gerrit.TooltipBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-label'; }
+}
+
+customElements.define(GrLabel.is, GrLabel);
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.js b/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.js
new file mode 100644
index 0000000..1644c07
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.js
@@ -0,0 +1,21 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <slot></slot>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html
deleted file mode 100644
index 47be6f7..0000000
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html
+++ /dev/null
@@ -1,65 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-labeled-autocomplete">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- width: 12em;
- }
- #container {
- background: var(--chip-background-color);
- border-radius: 1em;
- padding: var(--spacing-m);
- }
- #header {
- color: var(--deemphasized-text-color);
- font-weight: var(--font-weight-bold);
- font-size: var(--font-size-small);
- }
- #body {
- display: flex;
- }
- #trigger {
- color: var(--deemphasized-text-color);
- cursor: pointer;
- padding-left: var(--spacing-s);
- }
- #trigger:hover {
- color: var(--primary-text-color);
- }
- </style>
- <div id="container">
- <div id="header">[[label]]</div>
- <div id="body">
- <gr-autocomplete
- id="autocomplete"
- threshold="[[_autocompleteThreshold]]"
- query="[[query]]"
- disabled="[[disabled]]"
- placeholder="[[placeholder]]"
- borderless></gr-autocomplete>
- <div id="trigger" on-click="_handleTriggerClick">▼</div>
- </div>
- </div>
- </template>
- <script src="gr-labeled-autocomplete.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
index cb5ad7c..f585347 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
@@ -14,69 +14,76 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrLabeledAutocomplete extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-labeled-autocomplete'; }
- /**
- * Fired when a value is chosen.
- *
- * @event commit
- */
+import '../gr-autocomplete/gr-autocomplete.js';
+import '../../../styles/shared-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-labeled-autocomplete_html.js';
- static get properties() {
- return {
+/** @extends Polymer.Element */
+class GrLabeledAutocomplete extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /**
- * Used just like the query property of gr-autocomplete.
- *
- * @type {function(string): Promise<?>}
- */
- query: {
- type: Function,
- value() {
- return function() {
- return Promise.resolve([]);
- };
- },
+ static get is() { return 'gr-labeled-autocomplete'; }
+ /**
+ * Fired when a value is chosen.
+ *
+ * @event commit
+ */
+
+ static get properties() {
+ return {
+
+ /**
+ * Used just like the query property of gr-autocomplete.
+ *
+ * @type {function(string): Promise<?>}
+ */
+ query: {
+ type: Function,
+ value() {
+ return function() {
+ return Promise.resolve([]);
+ };
},
+ },
- text: {
- type: String,
- value: '',
- notify: true,
- },
- label: String,
- placeholder: String,
- disabled: Boolean,
+ text: {
+ type: String,
+ value: '',
+ notify: true,
+ },
+ label: String,
+ placeholder: String,
+ disabled: Boolean,
- _autocompleteThreshold: {
- type: Number,
- value: 0,
- readOnly: true,
- },
- };
- }
-
- _handleTriggerClick(e) {
- // Stop propagation here so we don't confuse gr-autocomplete, which
- // listens for taps on body to try to determine when it's blurred.
- e.stopPropagation();
- this.$.autocomplete.focus();
- }
-
- setText(text) {
- this.$.autocomplete.setText(text);
- }
-
- clear() {
- this.setText('');
- }
+ _autocompleteThreshold: {
+ type: Number,
+ value: 0,
+ readOnly: true,
+ },
+ };
}
- customElements.define(GrLabeledAutocomplete.is, GrLabeledAutocomplete);
-})();
+ _handleTriggerClick(e) {
+ // Stop propagation here so we don't confuse gr-autocomplete, which
+ // listens for taps on body to try to determine when it's blurred.
+ e.stopPropagation();
+ this.$.autocomplete.focus();
+ }
+
+ setText(text) {
+ this.$.autocomplete.setText(text);
+ }
+
+ clear() {
+ this.setText('');
+ }
+}
+
+customElements.define(GrLabeledAutocomplete.is, GrLabeledAutocomplete);
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.js b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.js
new file mode 100644
index 0000000..fe0b03c
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.js
@@ -0,0 +1,54 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ width: 12em;
+ }
+ #container {
+ background: var(--chip-background-color);
+ border-radius: 1em;
+ padding: var(--spacing-m);
+ }
+ #header {
+ color: var(--deemphasized-text-color);
+ font-weight: var(--font-weight-bold);
+ font-size: var(--font-size-small);
+ }
+ #body {
+ display: flex;
+ }
+ #trigger {
+ color: var(--deemphasized-text-color);
+ cursor: pointer;
+ padding-left: var(--spacing-s);
+ }
+ #trigger:hover {
+ color: var(--primary-text-color);
+ }
+ </style>
+ <div id="container">
+ <div id="header">[[label]]</div>
+ <div id="body">
+ <gr-autocomplete id="autocomplete" threshold="[[_autocompleteThreshold]]" query="[[query]]" disabled="[[disabled]]" placeholder="[[placeholder]]" borderless=""></gr-autocomplete>
+ <div id="trigger" on-click="_handleTriggerClick">▼</div>
+ </div>
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html
index e79e741..7e83aaf 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html
@@ -18,15 +18,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-labeled-autocomplete</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-labeled-autocomplete.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -34,32 +29,33 @@
</template>
</test-fixture>
-<script>
- suite('gr-labeled-autocomplete tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-labeled-autocomplete.js';
+suite('gr-labeled-autocomplete tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('tapping trigger focuses autocomplete', () => {
- const e = {stopPropagation: () => undefined};
- sandbox.stub(e, 'stopPropagation');
- sandbox.stub(element.$.autocomplete, 'focus');
- element._handleTriggerClick(e);
- assert.isTrue(e.stopPropagation.calledOnce);
- assert.isTrue(element.$.autocomplete.focus.calledOnce);
- });
-
- test('setText', () => {
- sandbox.stub(element.$.autocomplete, 'setText');
- element.setText('foo-bar');
- assert.isTrue(element.$.autocomplete.setText.calledWith('foo-bar'));
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('tapping trigger focuses autocomplete', () => {
+ const e = {stopPropagation: () => undefined};
+ sandbox.stub(e, 'stopPropagation');
+ sandbox.stub(element.$.autocomplete, 'focus');
+ element._handleTriggerClick(e);
+ assert.isTrue(e.stopPropagation.calledOnce);
+ assert.isTrue(element.$.autocomplete.focus.calledOnce);
+ });
+
+ test('setText', () => {
+ sandbox.stub(element.$.autocomplete, 'setText');
+ element.setText('foo-bar');
+ assert.isTrue(element.$.autocomplete.setText.calledWith('foo-bar'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
deleted file mode 100644
index fb55c67..0000000
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
+++ /dev/null
@@ -1,25 +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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-
-<dom-module id="gr-lib-loader">
- <template>
- <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
- </template>
- <script src="gr-lib-loader.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
index 81126ec..9bd8a11 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
@@ -14,155 +14,163 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js';
- const DARK_THEME_PATH = 'styles/themes/dark-theme.html';
+import '../gr-js-api-interface/gr-js-api-interface.js';
+import {importHref} from '../../../scripts/import-href.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-lib-loader_html.js';
- /** @extends Polymer.Element */
- class GrLibLoader extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-lib-loader'; }
+const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js';
+const DARK_THEME_PATH = 'styles/themes/dark-theme.html';
- static get properties() {
- return {
- _hljsState: {
- type: Object,
+/** @extends Polymer.Element */
+class GrLibLoader extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- // NOTE: intended singleton.
- value: {
- configured: false,
- loading: false,
- callbacks: [],
- },
+ static get is() { return 'gr-lib-loader'; }
+
+ static get properties() {
+ return {
+ _hljsState: {
+ type: Object,
+
+ // NOTE: intended singleton.
+ value: {
+ configured: false,
+ loading: false,
+ callbacks: [],
},
- };
- }
-
- /**
- * Get the HLJS library. Returns a promise that resolves with a reference to
- * the library after it's been loaded. The promise resolves immediately if
- * it's already been loaded.
- *
- * @return {!Promise<Object>}
- */
- getHLJS() {
- return new Promise((resolve, reject) => {
- // If the lib is totally loaded, resolve immediately.
- if (this._getHighlightLib()) {
- resolve(this._getHighlightLib());
- return;
- }
-
- // If the library is not currently being loaded, then start loading it.
- if (!this._hljsState.loading) {
- this._hljsState.loading = true;
- this._loadScript(this._getHLJSUrl())
- .then(this._onHLJSLibLoaded.bind(this))
- .catch(reject);
- }
-
- this._hljsState.callbacks.push(resolve);
- });
- }
-
- /**
- * Loads the dark theme document. Returns a promise that resolves with a
- * custom-style DOM element.
- *
- * @return {!Promise<Element>}
- * @suppress {checkTypes}
- */
- getDarkTheme() {
- return new Promise((resolve, reject) => {
- Polymer.importHref(
- this._getLibRoot() + DARK_THEME_PATH, () => {
- const module = document.createElement('style');
- module.setAttribute('include', 'dark-theme');
- const cs = document.createElement('custom-style');
- cs.appendChild(module);
-
- resolve(cs);
- },
- reject);
- });
- }
-
- /**
- * Execute callbacks awaiting the HLJS lib load.
- */
- _onHLJSLibLoaded() {
- const lib = this._getHighlightLib();
- this._hljsState.loading = false;
- this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.HIGHLIGHTJS_LOADED, {
- hljs: lib,
- });
- for (const cb of this._hljsState.callbacks) {
- cb(lib);
- }
- this._hljsState.callbacks = [];
- }
-
- /**
- * Get the HLJS library, assuming it has been loaded. Configure the library
- * if it hasn't already been configured.
- *
- * @return {!Object}
- */
- _getHighlightLib() {
- const lib = window.hljs;
- if (lib && !this._hljsState.configured) {
- this._hljsState.configured = true;
-
- lib.configure({classPrefix: 'gr-diff gr-syntax gr-syntax-'});
- }
- return lib;
- }
-
- /**
- * Get the resource path used to load the application. If the application
- * was loaded through a CDN, then this will be the path to CDN resources.
- *
- * @return {string}
- */
- _getLibRoot() {
- if (window.STATIC_RESOURCE_PATH) {
- return window.STATIC_RESOURCE_PATH + '/';
- }
- return '/';
- }
-
- /**
- * Load and execute a JS file from the lib root.
- *
- * @param {string} src The path to the JS file without the lib root.
- * @return {Promise} a promise that resolves when the script's onload
- * executes.
- */
- _loadScript(src) {
- return new Promise((resolve, reject) => {
- const script = document.createElement('script');
-
- if (!src) {
- reject(new Error('Unable to load blank script url.'));
- return;
- }
-
- script.setAttribute('src', src);
- script.onload = resolve;
- script.onerror = reject;
- Polymer.dom(document.head).appendChild(script);
- });
- }
-
- _getHLJSUrl() {
- const root = this._getLibRoot();
- if (!root) { return null; }
- return root + HLJS_PATH;
- }
+ },
+ };
}
- customElements.define(GrLibLoader.is, GrLibLoader);
-})();
+ /**
+ * Get the HLJS library. Returns a promise that resolves with a reference to
+ * the library after it's been loaded. The promise resolves immediately if
+ * it's already been loaded.
+ *
+ * @return {!Promise<Object>}
+ */
+ getHLJS() {
+ return new Promise((resolve, reject) => {
+ // If the lib is totally loaded, resolve immediately.
+ if (this._getHighlightLib()) {
+ resolve(this._getHighlightLib());
+ return;
+ }
+
+ // If the library is not currently being loaded, then start loading it.
+ if (!this._hljsState.loading) {
+ this._hljsState.loading = true;
+ this._loadScript(this._getHLJSUrl())
+ .then(this._onHLJSLibLoaded.bind(this))
+ .catch(reject);
+ }
+
+ this._hljsState.callbacks.push(resolve);
+ });
+ }
+
+ /**
+ * Loads the dark theme document. Returns a promise that resolves with a
+ * custom-style DOM element.
+ *
+ * @return {!Promise<Element>}
+ * @suppress {checkTypes}
+ */
+ getDarkTheme() {
+ return new Promise((resolve, reject) => {
+ importHref(
+ this._getLibRoot() + DARK_THEME_PATH, () => {
+ const module = document.createElement('style');
+ module.setAttribute('include', 'dark-theme');
+ const cs = document.createElement('custom-style');
+ cs.appendChild(module);
+
+ resolve(cs);
+ },
+ reject);
+ });
+ }
+
+ /**
+ * Execute callbacks awaiting the HLJS lib load.
+ */
+ _onHLJSLibLoaded() {
+ const lib = this._getHighlightLib();
+ this._hljsState.loading = false;
+ this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.HIGHLIGHTJS_LOADED, {
+ hljs: lib,
+ });
+ for (const cb of this._hljsState.callbacks) {
+ cb(lib);
+ }
+ this._hljsState.callbacks = [];
+ }
+
+ /**
+ * Get the HLJS library, assuming it has been loaded. Configure the library
+ * if it hasn't already been configured.
+ *
+ * @return {!Object}
+ */
+ _getHighlightLib() {
+ const lib = window.hljs;
+ if (lib && !this._hljsState.configured) {
+ this._hljsState.configured = true;
+
+ lib.configure({classPrefix: 'gr-diff gr-syntax gr-syntax-'});
+ }
+ return lib;
+ }
+
+ /**
+ * Get the resource path used to load the application. If the application
+ * was loaded through a CDN, then this will be the path to CDN resources.
+ *
+ * @return {string}
+ */
+ _getLibRoot() {
+ if (window.STATIC_RESOURCE_PATH) {
+ return window.STATIC_RESOURCE_PATH + '/';
+ }
+ return '/';
+ }
+
+ /**
+ * Load and execute a JS file from the lib root.
+ *
+ * @param {string} src The path to the JS file without the lib root.
+ * @return {Promise} a promise that resolves when the script's onload
+ * executes.
+ */
+ _loadScript(src) {
+ return new Promise((resolve, reject) => {
+ const script = document.createElement('script');
+
+ if (!src) {
+ reject(new Error('Unable to load blank script url.'));
+ return;
+ }
+
+ script.setAttribute('src', src);
+ script.onload = resolve;
+ script.onerror = reject;
+ dom(document.head).appendChild(script);
+ });
+ }
+
+ _getHLJSUrl() {
+ const root = this._getLibRoot();
+ if (!root) { return null; }
+ return root + HLJS_PATH;
+ }
+}
+
+customElements.define(GrLibLoader.is, GrLibLoader);
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_html.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_html.js
new file mode 100644
index 0000000..3bc0d72
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_html.js
@@ -0,0 +1,21 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
index a1d9c0f..89672a1 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-lib-loader</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-lib-loader.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,117 +30,118 @@
</template>
</test-fixture>
-<script>
- suite('gr-lib-loader tests', async () => {
- await readyToTest();
- let sandbox;
- let element;
- let resolveLoad;
- let loadStub;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-lib-loader.js';
+suite('gr-lib-loader tests', () => {
+ let sandbox;
+ let element;
+ let resolveLoad;
+ let loadStub;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+
+ loadStub = sandbox.stub(element, '_loadScript', () =>
+ new Promise(resolve => resolveLoad = resolve)
+ );
+
+ // Assert preconditions:
+ assert.isFalse(element._hljsState.loading);
+ });
+
+ teardown(() => {
+ if (window.hljs) {
+ delete window.hljs;
+ }
+ sandbox.restore();
+
+ // Because the element state is a singleton, clean it up.
+ element._hljsState.configured = false;
+ element._hljsState.loading = false;
+ element._hljsState.callbacks = [];
+ });
+
+ test('only load once', done => {
+ sandbox.stub(element, '_getHLJSUrl').returns('');
+ const firstCallHandler = sinon.stub();
+ element.getHLJS().then(firstCallHandler);
+
+ // It should now be in the loading state.
+ assert.isTrue(loadStub.called);
+ assert.isTrue(element._hljsState.loading);
+ assert.isFalse(firstCallHandler.called);
+
+ const secondCallHandler = sinon.stub();
+ element.getHLJS().then(secondCallHandler);
+
+ // No change in state.
+ assert.isTrue(element._hljsState.loading);
+ assert.isFalse(firstCallHandler.called);
+ assert.isFalse(secondCallHandler.called);
+
+ // Now load the library.
+ resolveLoad();
+ flush(() => {
+ // The state should be loaded and both handlers called.
+ assert.isFalse(element._hljsState.loading);
+ assert.isTrue(firstCallHandler.called);
+ assert.isTrue(secondCallHandler.called);
+ done();
+ });
+ });
+
+ suite('preloaded', () => {
+ let hljsStub;
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
-
- loadStub = sandbox.stub(element, '_loadScript', () =>
- new Promise(resolve => resolveLoad = resolve)
- );
-
- // Assert preconditions:
- assert.isFalse(element._hljsState.loading);
+ hljsStub = {
+ configure: sinon.stub(),
+ };
+ window.hljs = hljsStub;
});
teardown(() => {
- if (window.hljs) {
- delete window.hljs;
- }
- sandbox.restore();
-
- // Because the element state is a singleton, clean it up.
- element._hljsState.configured = false;
- element._hljsState.loading = false;
- element._hljsState.callbacks = [];
+ delete window.hljs;
});
- test('only load once', done => {
- sandbox.stub(element, '_getHLJSUrl').returns('');
+ test('returns hljs', done => {
const firstCallHandler = sinon.stub();
element.getHLJS().then(firstCallHandler);
-
- // It should now be in the loading state.
- assert.isTrue(loadStub.called);
- assert.isTrue(element._hljsState.loading);
- assert.isFalse(firstCallHandler.called);
-
- const secondCallHandler = sinon.stub();
- element.getHLJS().then(secondCallHandler);
-
- // No change in state.
- assert.isTrue(element._hljsState.loading);
- assert.isFalse(firstCallHandler.called);
- assert.isFalse(secondCallHandler.called);
-
- // Now load the library.
- resolveLoad();
flush(() => {
- // The state should be loaded and both handlers called.
- assert.isFalse(element._hljsState.loading);
assert.isTrue(firstCallHandler.called);
- assert.isTrue(secondCallHandler.called);
+ assert.isTrue(firstCallHandler.calledWith(hljsStub));
done();
});
});
- suite('preloaded', () => {
- let hljsStub;
-
- setup(() => {
- hljsStub = {
- configure: sinon.stub(),
- };
- window.hljs = hljsStub;
- });
-
- teardown(() => {
- delete window.hljs;
- });
-
- test('returns hljs', done => {
- const firstCallHandler = sinon.stub();
- element.getHLJS().then(firstCallHandler);
- flush(() => {
- assert.isTrue(firstCallHandler.called);
- assert.isTrue(firstCallHandler.calledWith(hljsStub));
- done();
- });
- });
-
- test('configures hljs', done => {
- element.getHLJS().then(() => {
- assert.isTrue(window.hljs.configure.calledOnce);
- done();
- });
- });
- });
-
- suite('_getHLJSUrl', () => {
- suite('checking _getLibRoot', () => {
- let root;
-
- setup(() => {
- sandbox.stub(element, '_getLibRoot', () => root);
- });
-
- test('with no root', () => {
- assert.isNull(element._getHLJSUrl());
- });
-
- test('with root', () => {
- root = 'test-root.com/';
- assert.equal(element._getHLJSUrl(),
- 'test-root.com/bower_components/highlightjs/highlight.min.js');
- });
+ test('configures hljs', done => {
+ element.getHLJS().then(() => {
+ assert.isTrue(window.hljs.configure.calledOnce);
+ done();
});
});
});
+
+ suite('_getHLJSUrl', () => {
+ suite('checking _getLibRoot', () => {
+ let root;
+
+ setup(() => {
+ sandbox.stub(element, '_getLibRoot', () => root);
+ });
+
+ test('with no root', () => {
+ assert.isNull(element._getHLJSUrl());
+ });
+
+ test('with root', () => {
+ root = 'test-root.com/';
+ assert.equal(element._getHLJSUrl(),
+ 'test-root.com/bower_components/highlightjs/highlight.min.js');
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.html b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.html
deleted file mode 100644
index d00416b..0000000
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.html
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
-
-<dom-module id="gr-limited-text">
- <template>[[_computeDisplayText(text, limit)]]</template>
- <script src="gr-limited-text.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
index ee032f6..d47dbbc 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
@@ -14,92 +14,99 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-limited-text_html.js';
+
+/**
+ * The gr-limited-text element is for displaying text with a maximum length
+ * (in number of characters) to display. If the length of the text exceeds the
+ * configured limit, then an ellipsis indicates that the text was truncated
+ * and a tooltip containing the full text is enabled.
+ *
+ * @appliesMixin Gerrit.TooltipMixin
+ * @extends Polymer.Element
+ */
+class GrLimitedText extends mixinBehaviors( [
+ Gerrit.TooltipBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-limited-text'; }
+
+ static get properties() {
+ return {
+ /** The un-truncated text to display. */
+ text: String,
+
+ /** The maximum length for the text to display before truncating. */
+ limit: {
+ type: Number,
+ value: null,
+ },
+
+ /** Boolean property used by Gerrit.TooltipBehavior. */
+ hasTooltip: {
+ type: Boolean,
+ value: false,
+ },
+
+ /**
+ * Disable the tooltip.
+ * When set to true, will not show tooltip even text is over limit
+ */
+ disableTooltip: {
+ type: Boolean,
+ value: false,
+ },
+
+ /**
+ * The maximum number of characters to display in the tooltop.
+ */
+ tooltipLimit: {
+ type: Number,
+ value: 1024,
+ },
+ };
+ }
+
+ static get observers() {
+ return [
+ '_updateTitle(text, limit, tooltipLimit)',
+ ];
+ }
/**
- * The gr-limited-text element is for displaying text with a maximum length
- * (in number of characters) to display. If the length of the text exceeds the
- * configured limit, then an ellipsis indicates that the text was truncated
- * and a tooltip containing the full text is enabled.
- *
- * @appliesMixin Gerrit.TooltipMixin
- * @extends Polymer.Element
+ * The text or limit have changed. Recompute whether a tooltip needs to be
+ * enabled.
*/
- class GrLimitedText extends Polymer.mixinBehaviors( [
- Gerrit.TooltipBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-limited-text'; }
-
- static get properties() {
- return {
- /** The un-truncated text to display. */
- text: String,
-
- /** The maximum length for the text to display before truncating. */
- limit: {
- type: Number,
- value: null,
- },
-
- /** Boolean property used by Gerrit.TooltipBehavior. */
- hasTooltip: {
- type: Boolean,
- value: false,
- },
-
- /**
- * Disable the tooltip.
- * When set to true, will not show tooltip even text is over limit
- */
- disableTooltip: {
- type: Boolean,
- value: false,
- },
-
- /**
- * The maximum number of characters to display in the tooltop.
- */
- tooltipLimit: {
- type: Number,
- value: 1024,
- },
- };
+ _updateTitle(text, limit, tooltipLimit) {
+ // Polymer 2: check for undefined
+ if ([text, limit, tooltipLimit].some(arg => arg === undefined)) {
+ return;
}
- static get observers() {
- return [
- '_updateTitle(text, limit, tooltipLimit)',
- ];
- }
-
- /**
- * The text or limit have changed. Recompute whether a tooltip needs to be
- * enabled.
- */
- _updateTitle(text, limit, tooltipLimit) {
- // Polymer 2: check for undefined
- if ([text, limit, tooltipLimit].some(arg => arg === undefined)) {
- return;
- }
-
- this.hasTooltip = !!limit && !!text && text.length > limit;
- if (this.hasTooltip && !this.disableTooltip) {
- this.setAttribute('title', text.substr(0, tooltipLimit));
- } else {
- this.removeAttribute('title');
- }
- }
-
- _computeDisplayText(text, limit) {
- if (!!limit && !!text && text.length > limit) {
- return text.substr(0, limit - 1) + '…';
- }
- return text;
+ this.hasTooltip = !!limit && !!text && text.length > limit;
+ if (this.hasTooltip && !this.disableTooltip) {
+ this.setAttribute('title', text.substr(0, tooltipLimit));
+ } else {
+ this.removeAttribute('title');
}
}
- customElements.define(GrLimitedText.is, GrLimitedText);
-})();
+ _computeDisplayText(text, limit) {
+ if (!!limit && !!text && text.length > limit) {
+ return text.substr(0, limit - 1) + '…';
+ }
+ return text;
+ }
+}
+
+customElements.define(GrLimitedText.is, GrLimitedText);
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.js b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.js
new file mode 100644
index 0000000..c14f9f9
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.js
@@ -0,0 +1,21 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+[[_computeDisplayText(text, limit)]]
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
index c34a348..1c6358c 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
@@ -19,16 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-limited-text</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-
-<link rel="import" href="gr-limited-text.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,72 +30,73 @@
</template>
</test-fixture>
-<script>
- suite('gr-limited-text tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-limited-text.js';
+suite('gr-limited-text tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('_updateTitle', () => {
- const updateSpy = sandbox.spy(element, '_updateTitle');
- element.text = 'abc 123';
- flushAsynchronousOperations();
- assert.isTrue(updateSpy.calledOnce);
- assert.isNotOk(element.getAttribute('title'));
- assert.isFalse(element.hasTooltip);
-
- element.limit = 10;
- flushAsynchronousOperations();
- assert.isTrue(updateSpy.calledTwice);
- assert.isNotOk(element.getAttribute('title'));
- assert.isFalse(element.hasTooltip);
-
- element.limit = 3;
- flushAsynchronousOperations();
- assert.isTrue(updateSpy.calledThrice);
- assert.equal(element.getAttribute('title'), 'abc 123');
- assert.isTrue(element.hasTooltip);
-
- element.tooltipLimit = 3;
- flushAsynchronousOperations();
- assert.equal(element.getAttribute('title'), 'abc');
-
- element.tooltipLimit = 1024;
- element.limit = 100;
- flushAsynchronousOperations();
- assert.equal(updateSpy.callCount, 6);
- assert.isNotOk(element.getAttribute('title'));
- assert.isFalse(element.hasTooltip);
-
- element.limit = null;
- flushAsynchronousOperations();
- assert.equal(updateSpy.callCount, 7);
- assert.isNotOk(element.getAttribute('title'));
- assert.isFalse(element.hasTooltip);
- });
-
- test('_computeDisplayText', () => {
- assert.equal(element._computeDisplayText('foo bar', 100), 'foo bar');
- assert.equal(element._computeDisplayText('foo bar', 4), 'foo…');
- assert.equal(element._computeDisplayText('foo bar', null), 'foo bar');
- });
-
- test('when disable tooltip', () => {
- sandbox.spy(element, '_updateTitle');
- element.text = 'abcdefghijklmn';
- element.disableTooltip = true;
- element.limit = 10;
- flushAsynchronousOperations();
- assert.equal(element.getAttribute('title'), null);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_updateTitle', () => {
+ const updateSpy = sandbox.spy(element, '_updateTitle');
+ element.text = 'abc 123';
+ flushAsynchronousOperations();
+ assert.isTrue(updateSpy.calledOnce);
+ assert.isNotOk(element.getAttribute('title'));
+ assert.isFalse(element.hasTooltip);
+
+ element.limit = 10;
+ flushAsynchronousOperations();
+ assert.isTrue(updateSpy.calledTwice);
+ assert.isNotOk(element.getAttribute('title'));
+ assert.isFalse(element.hasTooltip);
+
+ element.limit = 3;
+ flushAsynchronousOperations();
+ assert.isTrue(updateSpy.calledThrice);
+ assert.equal(element.getAttribute('title'), 'abc 123');
+ assert.isTrue(element.hasTooltip);
+
+ element.tooltipLimit = 3;
+ flushAsynchronousOperations();
+ assert.equal(element.getAttribute('title'), 'abc');
+
+ element.tooltipLimit = 1024;
+ element.limit = 100;
+ flushAsynchronousOperations();
+ assert.equal(updateSpy.callCount, 6);
+ assert.isNotOk(element.getAttribute('title'));
+ assert.isFalse(element.hasTooltip);
+
+ element.limit = null;
+ flushAsynchronousOperations();
+ assert.equal(updateSpy.callCount, 7);
+ assert.isNotOk(element.getAttribute('title'));
+ assert.isFalse(element.hasTooltip);
+ });
+
+ test('_computeDisplayText', () => {
+ assert.equal(element._computeDisplayText('foo bar', 100), 'foo bar');
+ assert.equal(element._computeDisplayText('foo bar', 4), 'foo…');
+ assert.equal(element._computeDisplayText('foo bar', null), 'foo bar');
+ });
+
+ test('when disable tooltip', () => {
+ sandbox.spy(element, '_updateTitle');
+ element.text = 'abcdefghijklmn';
+ element.disableTooltip = true;
+ element.limit = 10;
+ flushAsynchronousOperations();
+ assert.equal(element.getAttribute('title'), null);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
deleted file mode 100644
index 844b8be..0000000
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
+++ /dev/null
@@ -1,99 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
-<link rel="import" href="../gr-button/gr-button.html">
-<link rel="import" href="../gr-icons/gr-icons.html">
-<link rel="import" href="../gr-limited-text/gr-limited-text.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-linked-chip">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- overflow: hidden;
- }
- .container {
- align-items: center;
- background: var(--chip-background-color);
- border-radius: .75em;
- display: inline-flex;
- padding: 0 var(--spacing-m);
- }
- gr-button.remove {
- --gr-remove-button-style: {
- border: 0;
- color: var(--deemphasized-text-color);
- font-weight: var(--font-weight-normal);
- height: .6em;
- line-height: 10px;
- margin-left: var(--spacing-xs);
- padding: 0;
- text-decoration: none;
- }
- }
-
- gr-button.remove:hover,
- gr-button.remove:focus {
- --gr-button: {
- @apply --gr-remove-button-style;
- color: #333;
- }
- }
- gr-button.remove {
- --gr-button: {
- @apply --gr-remove-button-style;
- }
- }
- .transparentBackground,
- gr-button.transparentBackground {
- background-color: transparent;
- }
- :host([disabled]) {
- opacity: .6;
- pointer-events: none;
- }
- a {
- color: var(--linked-chip-text-color);
- }
- iron-icon {
- height: 1.2rem;
- width: 1.2rem;
- }
- </style>
- <div class$="container [[_getBackgroundClass(transparentBackground)]]">
- <a href$="[[href]]">
- <gr-limited-text
- limit="[[limit]]"
- text="[[text]]"></gr-limited-text>
- </a>
- <gr-button
- id="remove"
- link
- hidden$="[[!removable]]"
- hidden
- class$="remove [[_getBackgroundClass(transparentBackground)]]"
- on-click="_handleRemoveTap">
- <iron-icon icon="gr-icons:close"></iron-icon>
- </gr-button>
- </div>
- </template>
- <script src="gr-linked-chip.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
index ccab685..2957c9f 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
@@ -14,52 +14,64 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
- */
- class GrLinkedChip extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-linked-chip'; }
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
+import '../gr-button/gr-button.js';
+import '../gr-icons/gr-icons.js';
+import '../gr-limited-text/gr-limited-text.js';
+import '../../../styles/shared-styles.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-linked-chip_html.js';
- static get properties() {
- return {
- href: String,
- disabled: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- removable: {
- type: Boolean,
- value: false,
- },
- text: String,
- transparentBackground: {
- type: Boolean,
- value: false,
- },
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrLinkedChip extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** If provided, sets the maximum length of the content. */
- limit: Number,
- };
- }
+ static get is() { return 'gr-linked-chip'; }
- _getBackgroundClass(transparent) {
- return transparent ? 'transparentBackground' : '';
- }
+ static get properties() {
+ return {
+ href: String,
+ disabled: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ removable: {
+ type: Boolean,
+ value: false,
+ },
+ text: String,
+ transparentBackground: {
+ type: Boolean,
+ value: false,
+ },
- _handleRemoveTap(e) {
- e.preventDefault();
- this.fire('remove');
- }
+ /** If provided, sets the maximum length of the content. */
+ limit: Number,
+ };
}
- customElements.define(GrLinkedChip.is, GrLinkedChip);
-})();
+ _getBackgroundClass(transparent) {
+ return transparent ? 'transparentBackground' : '';
+ }
+
+ _handleRemoveTap(e) {
+ e.preventDefault();
+ this.fire('remove');
+ }
+}
+
+customElements.define(GrLinkedChip.is, GrLinkedChip);
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.js b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.js
new file mode 100644
index 0000000..c028d02
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.js
@@ -0,0 +1,81 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ overflow: hidden;
+ }
+ .container {
+ align-items: center;
+ background: var(--chip-background-color);
+ border-radius: .75em;
+ display: inline-flex;
+ padding: 0 var(--spacing-m);
+ }
+ gr-button.remove {
+ --gr-remove-button-style: {
+ border: 0;
+ color: var(--deemphasized-text-color);
+ font-weight: var(--font-weight-normal);
+ height: .6em;
+ line-height: 10px;
+ margin-left: var(--spacing-xs);
+ padding: 0;
+ text-decoration: none;
+ }
+ }
+
+ gr-button.remove:hover,
+ gr-button.remove:focus {
+ --gr-button: {
+ @apply --gr-remove-button-style;
+ color: #333;
+ }
+ }
+ gr-button.remove {
+ --gr-button: {
+ @apply --gr-remove-button-style;
+ }
+ }
+ .transparentBackground,
+ gr-button.transparentBackground {
+ background-color: transparent;
+ }
+ :host([disabled]) {
+ opacity: .6;
+ pointer-events: none;
+ }
+ a {
+ color: var(--linked-chip-text-color);
+ }
+ iron-icon {
+ height: 1.2rem;
+ width: 1.2rem;
+ }
+ </style>
+ <div class\$="container [[_getBackgroundClass(transparentBackground)]]">
+ <a href\$="[[href]]">
+ <gr-limited-text limit="[[limit]]" text="[[text]]"></gr-limited-text>
+ </a>
+ <gr-button id="remove" link="" hidden\$="[[!removable]]" hidden="" class\$="remove [[_getBackgroundClass(transparentBackground)]]" on-click="_handleRemoveTap">
+ <iron-icon icon="gr-icons:close"></iron-icon>
+ </gr-button>
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
index 6eb93b4..0ba6194 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
@@ -19,17 +19,17 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-linked-chip</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="/bower_components/iron-test-helpers/mock-interactions.js"></script>
-
-<link rel="import" href="gr-linked-chip.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<!-- Can't use absolute path below for mock-interaction.js.
+Web component tester(wct) has a built-in http server and it serves "/components" directory (which is
+actually /node_modules directory). Also, wct patches some files to load modules from /components.
+With the absolute path, browser tries to load dom-module from 2 different places (/component/... and
+/node_modules/...) though this is actually the same file. This leads to a run-time error.
+-->
+<script src="../../../node_modules/iron-test-helpers/mock-interactions.js"></script>
<test-fixture id="basic">
<template>
@@ -37,27 +37,28 @@
</template>
</test-fixture>
-<script>
- suite('gr-linked-chip tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-linked-chip.js';
+suite('gr-linked-chip tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('remove fired', () => {
- const spy = sandbox.spy();
- element.addEventListener('remove', spy);
- flushAsynchronousOperations();
- MockInteractions.tap(element.$.remove);
- assert.isTrue(spy.called);
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('remove fired', () => {
+ const spy = sandbox.spy();
+ element.addEventListener('remove', spy);
+ flushAsynchronousOperations();
+ MockInteractions.tap(element.$.remove);
+ assert.isTrue(spy.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
deleted file mode 100644
index 61facc0..0000000
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
+++ /dev/null
@@ -1,44 +0,0 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-
-<script src="/bower_components/ba-linkify/ba-linkify.js"></script>
-<script src="link-text-parser.js"></script>
-<dom-module id="gr-linked-text">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- }
- :host([pre]) span {
- white-space: var(--linked-text-white-space, pre-wrap);
- word-wrap: var(--linked-text-word-wrap, break-word);
- }
- :host([disabled]) a {
- color: inherit;
- text-decoration: none;
- pointer-events: none;
- }
- </style>
- <span id="output"></span>
- </template>
- <script src="gr-linked-text.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
index f970734..07ce424 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,110 +14,121 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrLinkedText extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-linked-text'; }
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import './link-text-parser.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import 'ba-linkify/ba-linkify.js';
+import {htmlTemplate} from './gr-linked-text_html.js';
- static get properties() {
- return {
- removeZeroWidthSpace: Boolean,
- content: {
- type: String,
- observer: '_contentChanged',
- },
- pre: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- disabled: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- config: Object,
- };
- }
+/** @extends Polymer.Element */
+class GrLinkedText extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- static get observers() {
- return [
- '_contentOrConfigChanged(content, config)',
- ];
- }
+ static get is() { return 'gr-linked-text'; }
- _contentChanged(content) {
- // In the case where the config may not be set (perhaps due to the
- // request for it still being in flight), set the content anyway to
- // prevent waiting on the config to display the text.
- if (this.config != null) { return; }
- this.$.output.textContent = content;
- }
-
- /**
- * Because either the source text or the linkification config has changed,
- * the content should be re-parsed.
- *
- * @param {string|null|undefined} content The raw, un-linkified source
- * string to parse.
- * @param {Object|null|undefined} config The server config specifying
- * commentLink patterns
- */
- _contentOrConfigChanged(content, config) {
- if (!Gerrit.Nav || !Gerrit.Nav.mapCommentlinks) return;
- config = Gerrit.Nav.mapCommentlinks(config);
- const output = Polymer.dom(this.$.output);
- output.textContent = '';
- const parser = new GrLinkTextParser(config,
- this._handleParseResult.bind(this), this.removeZeroWidthSpace);
- parser.parse(content);
-
- // Ensure that external links originating from HTML commentlink configs
- // open in a new tab. @see Issue 5567
- // Ensure links to the same host originating from commentlink configs
- // open in the same tab. When target is not set - default is _self
- // @see Issue 4616
- output.querySelectorAll('a').forEach(anchor => {
- if (anchor.hostname === window.location.hostname) {
- anchor.removeAttribute('target');
- } else {
- anchor.setAttribute('target', '_blank');
- }
- anchor.setAttribute('rel', 'noopener');
- });
- }
-
- /**
- * This method is called when the GrLikTextParser emits a partial result
- * (used as the "callback" parameter). It will be called in either of two
- * ways:
- * - To create a link: when called with `text` and `href` arguments, a link
- * element should be created and attached to the resulting DOM.
- * - To attach an arbitrary fragment: when called with only the `fragment`
- * argument, the fragment should be attached to the resulting DOM as is.
- *
- * @param {string|null} text
- * @param {string|null} href
- * @param {DocumentFragment|undefined} fragment
- */
- _handleParseResult(text, href, fragment) {
- const output = Polymer.dom(this.$.output);
- if (href) {
- const a = document.createElement('a');
- a.href = href;
- a.textContent = text;
- a.target = '_blank';
- a.rel = 'noopener';
- output.appendChild(a);
- } else if (fragment) {
- output.appendChild(fragment);
- }
- }
+ static get properties() {
+ return {
+ removeZeroWidthSpace: Boolean,
+ content: {
+ type: String,
+ observer: '_contentChanged',
+ },
+ pre: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ disabled: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ config: Object,
+ };
}
- customElements.define(GrLinkedText.is, GrLinkedText);
-})();
+ static get observers() {
+ return [
+ '_contentOrConfigChanged(content, config)',
+ ];
+ }
+
+ _contentChanged(content) {
+ // In the case where the config may not be set (perhaps due to the
+ // request for it still being in flight), set the content anyway to
+ // prevent waiting on the config to display the text.
+ if (this.config != null) { return; }
+ this.$.output.textContent = content;
+ }
+
+ /**
+ * Because either the source text or the linkification config has changed,
+ * the content should be re-parsed.
+ *
+ * @param {string|null|undefined} content The raw, un-linkified source
+ * string to parse.
+ * @param {Object|null|undefined} config The server config specifying
+ * commentLink patterns
+ */
+ _contentOrConfigChanged(content, config) {
+ if (!Gerrit.Nav || !Gerrit.Nav.mapCommentlinks) return;
+ config = Gerrit.Nav.mapCommentlinks(config);
+ const output = dom(this.$.output);
+ output.textContent = '';
+ const parser = new GrLinkTextParser(config,
+ this._handleParseResult.bind(this), this.removeZeroWidthSpace);
+ parser.parse(content);
+
+ // Ensure that external links originating from HTML commentlink configs
+ // open in a new tab. @see Issue 5567
+ // Ensure links to the same host originating from commentlink configs
+ // open in the same tab. When target is not set - default is _self
+ // @see Issue 4616
+ output.querySelectorAll('a').forEach(anchor => {
+ if (anchor.hostname === window.location.hostname) {
+ anchor.removeAttribute('target');
+ } else {
+ anchor.setAttribute('target', '_blank');
+ }
+ anchor.setAttribute('rel', 'noopener');
+ });
+ }
+
+ /**
+ * This method is called when the GrLikTextParser emits a partial result
+ * (used as the "callback" parameter). It will be called in either of two
+ * ways:
+ * - To create a link: when called with `text` and `href` arguments, a link
+ * element should be created and attached to the resulting DOM.
+ * - To attach an arbitrary fragment: when called with only the `fragment`
+ * argument, the fragment should be attached to the resulting DOM as is.
+ *
+ * @param {string|null} text
+ * @param {string|null} href
+ * @param {DocumentFragment|undefined} fragment
+ */
+ _handleParseResult(text, href, fragment) {
+ const output = dom(this.$.output);
+ if (href) {
+ const a = document.createElement('a');
+ a.href = href;
+ a.textContent = text;
+ a.target = '_blank';
+ a.rel = 'noopener';
+ output.appendChild(a);
+ } else if (fragment) {
+ output.appendChild(fragment);
+ }
+ }
+}
+
+customElements.define(GrLinkedText.is, GrLinkedText);
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.js
new file mode 100644
index 0000000..43d7144
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.js
@@ -0,0 +1,35 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ }
+ :host([pre]) span {
+ white-space: var(--linked-text-white-space, pre-wrap);
+ word-wrap: var(--linked-text-word-wrap, break-word);
+ }
+ :host([disabled]) a {
+ color: inherit;
+ text-decoration: none;
+ pointer-events: none;
+ }
+ </style>
+ <span id="output"></span>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
index 9e373b7..381b332 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -19,17 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-linked-text</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="gr-linked-text.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -39,340 +32,343 @@
</template>
</test-fixture>
-<script>
- suite('gr-linked-text tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-linked-text.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-linked-text tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- sandbox.stub(Gerrit.Nav, 'mapCommentlinks', x => x);
- element.config = {
- ph: {
- match: '([Bb]ug|[Ii]ssue)\\s*#?(\\d+)',
- link: 'https://bugs.chromium.org/p/gerrit/issues/detail?id=$2',
- },
- prefixsameinlinkandpattern: {
- match: '([Hh][Tt][Tt][Pp]example)\\s*#?(\\d+)',
- link: 'https://bugs.chromium.org/p/gerrit/issues/detail?id=$2',
- },
- changeid: {
- match: '(I[0-9a-f]{8,40})',
- link: '#/q/$1',
- },
- changeid2: {
- match: 'Change-Id: +(I[0-9a-f]{8,40})',
- link: '#/q/$1',
- },
- googlesearch: {
- match: 'google:(.+)',
- link: 'https://bing.com/search?q=$1', // html should supercede link.
- html: '<a href="https://google.com/search?q=$1">$1</a>',
- },
- hashedhtml: {
- match: 'hash:(.+)',
- html: '<a href="#/awesomesauce">$1</a>',
- },
- baseurl: {
- match: 'test (.+)',
- html: '<a href="/r/awesomesauce">$1</a>',
- },
- anotatstartwithbaseurl: {
- match: 'a test (.+)',
- html: '[Lookup: <a href="/r/awesomesauce">$1</a>]',
- },
- disabledconfig: {
- match: 'foo:(.+)',
- link: 'https://google.com/search?q=$1',
- enabled: false,
- },
- };
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('URL pattern was parsed and linked.', () => {
- // Regular inline link.
- const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
- element.content = url;
- const linkEl = element.$.output.childNodes[0];
- assert.equal(linkEl.target, '_blank');
- assert.equal(linkEl.rel, 'noopener');
- assert.equal(linkEl.href, url);
- assert.equal(linkEl.textContent, url);
- });
-
- test('Bug pattern was parsed and linked', () => {
- // "Issue/Bug" pattern.
- element.content = 'Issue 3650';
-
- let linkEl = element.$.output.childNodes[0];
- const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
- assert.equal(linkEl.target, '_blank');
- assert.equal(linkEl.href, url);
- assert.equal(linkEl.textContent, 'Issue 3650');
-
- element.content = 'Bug 3650';
- linkEl = element.$.output.childNodes[0];
- assert.equal(linkEl.target, '_blank');
- assert.equal(linkEl.rel, 'noopener');
- assert.equal(linkEl.href, url);
- assert.equal(linkEl.textContent, 'Bug 3650');
- });
-
- test('Pattern with same prefix as link was correctly parsed', () => {
- // Pattern starts with the same prefix (`http`) as the url.
- element.content = 'httpexample 3650';
-
- assert.equal(element.$.output.childNodes.length, 1);
- const linkEl = element.$.output.childNodes[0];
- const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
- assert.equal(linkEl.target, '_blank');
- assert.equal(linkEl.href, url);
- assert.equal(linkEl.textContent, 'httpexample 3650');
- });
-
- test('Change-Id pattern was parsed and linked', () => {
- // "Change-Id:" pattern.
- const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
- const prefix = 'Change-Id: ';
- element.content = prefix + changeID;
-
- const textNode = element.$.output.childNodes[0];
- const linkEl = element.$.output.childNodes[1];
- assert.equal(textNode.textContent, prefix);
- const url = '/q/' + changeID;
- assert.isFalse(linkEl.hasAttribute('target'));
- // Since url is a path, the host is added automatically.
- assert.isTrue(linkEl.href.endsWith(url));
- assert.equal(linkEl.textContent, changeID);
- });
-
- test('Change-Id pattern was parsed and linked with base url', () => {
- window.CANONICAL_PATH = '/r';
-
- // "Change-Id:" pattern.
- const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
- const prefix = 'Change-Id: ';
- element.content = prefix + changeID;
-
- const textNode = element.$.output.childNodes[0];
- const linkEl = element.$.output.childNodes[1];
- assert.equal(textNode.textContent, prefix);
- const url = '/r/q/' + changeID;
- assert.isFalse(linkEl.hasAttribute('target'));
- // Since url is a path, the host is added automatically.
- assert.isTrue(linkEl.href.endsWith(url));
- assert.equal(linkEl.textContent, changeID);
- });
-
- test('Multiple matches', () => {
- element.content = 'Issue 3650\nIssue 3450';
- const linkEl1 = element.$.output.childNodes[0];
- const linkEl2 = element.$.output.childNodes[2];
-
- assert.equal(linkEl1.target, '_blank');
- assert.equal(linkEl1.href,
- 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650');
- assert.equal(linkEl1.textContent, 'Issue 3650');
-
- assert.equal(linkEl2.target, '_blank');
- assert.equal(linkEl2.href,
- 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3450');
- assert.equal(linkEl2.textContent, 'Issue 3450');
- });
-
- test('Change-Id pattern parsed before bug pattern', () => {
- // "Change-Id:" pattern.
- const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
- const prefix = 'Change-Id: ';
-
- // "Issue/Bug" pattern.
- const bug = 'Issue 3650';
-
- const changeUrl = '/q/' + changeID;
- const bugUrl = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
-
- element.content = prefix + changeID + bug;
-
- const textNode = element.$.output.childNodes[0];
- const changeLinkEl = element.$.output.childNodes[1];
- const bugLinkEl = element.$.output.childNodes[2];
-
- assert.equal(textNode.textContent, prefix);
-
- assert.isFalse(changeLinkEl.hasAttribute('target'));
- assert.isTrue(changeLinkEl.href.endsWith(changeUrl));
- assert.equal(changeLinkEl.textContent, changeID);
-
- assert.equal(bugLinkEl.target, '_blank');
- assert.equal(bugLinkEl.href, bugUrl);
- assert.equal(bugLinkEl.textContent, 'Issue 3650');
- });
-
- test('html field in link config', () => {
- element.content = 'google:do a barrel roll';
- const linkEl = element.$.output.childNodes[0];
- assert.equal(linkEl.getAttribute('href'),
- 'https://google.com/search?q=do a barrel roll');
- assert.equal(linkEl.textContent, 'do a barrel roll');
- });
-
- test('removing hash from links', () => {
- element.content = 'hash:foo';
- const linkEl = element.$.output.childNodes[0];
- assert.isTrue(linkEl.href.endsWith('/awesomesauce'));
- assert.equal(linkEl.textContent, 'foo');
- });
-
- test('html with base url', () => {
- window.CANONICAL_PATH = '/r';
-
- element.content = 'test foo';
- const linkEl = element.$.output.childNodes[0];
- assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
- assert.equal(linkEl.textContent, 'foo');
- });
-
- test('a is not at start', () => {
- window.CANONICAL_PATH = '/r';
-
- element.content = 'a test foo';
- const linkEl = element.$.output.childNodes[1];
- assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
- assert.equal(linkEl.textContent, 'foo');
- });
-
- test('hash html with base url', () => {
- window.CANONICAL_PATH = '/r';
-
- element.content = 'hash:foo';
- const linkEl = element.$.output.childNodes[0];
- assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
- assert.equal(linkEl.textContent, 'foo');
- });
-
- test('disabled config', () => {
- element.content = 'foo:baz';
- assert.equal(element.$.output.innerHTML, 'foo:baz');
- });
-
- test('R=email labels link correctly', () => {
- element.removeZeroWidthSpace = true;
- element.content = 'R=\u200Btest@google.com';
- assert.equal(element.$.output.textContent, 'R=test@google.com');
- assert.equal(element.$.output.innerHTML.match(/(R=<a)/g).length, 1);
- });
-
- test('CC=email labels link correctly', () => {
- element.removeZeroWidthSpace = true;
- element.content = 'CC=\u200Btest@google.com';
- assert.equal(element.$.output.textContent, 'CC=test@google.com');
- assert.equal(element.$.output.innerHTML.match(/(CC=<a)/g).length, 1);
- });
-
- test('only {http,https,mailto} protocols are linkified', () => {
- element.content = 'xx mailto:test@google.com yy';
- let links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 1);
- assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
- assert.equal(links[0].innerHTML, 'mailto:test@google.com');
-
- element.content = 'xx http://google.com yy';
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 1);
- assert.equal(links[0].getAttribute('href'), 'http://google.com');
- assert.equal(links[0].innerHTML, 'http://google.com');
-
- element.content = 'xx https://google.com yy';
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 1);
- assert.equal(links[0].getAttribute('href'), 'https://google.com');
- assert.equal(links[0].innerHTML, 'https://google.com');
-
- element.content = 'xx ssh://google.com yy';
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 0);
-
- element.content = 'xx ftp://google.com yy';
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 0);
- });
-
- test('links without leading whitespace are linkified', () => {
- element.content = 'xx abcmailto:test@google.com yy';
- assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx abc');
- let links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 1);
- assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
- assert.equal(links[0].innerHTML, 'mailto:test@google.com');
-
- element.content = 'xx defhttp://google.com yy';
- assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx def');
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 1);
- assert.equal(links[0].getAttribute('href'), 'http://google.com');
- assert.equal(links[0].innerHTML, 'http://google.com');
-
- element.content = 'xx qwehttps://google.com yy';
- assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx qwe');
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 1);
- assert.equal(links[0].getAttribute('href'), 'https://google.com');
- assert.equal(links[0].innerHTML, 'https://google.com');
-
- // Non-latin character
- element.content = 'xx абвhttps://google.com yy';
- assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx абв');
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 1);
- assert.equal(links[0].getAttribute('href'), 'https://google.com');
- assert.equal(links[0].innerHTML, 'https://google.com');
-
- element.content = 'xx ssh://google.com yy';
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 0);
-
- element.content = 'xx ftp://google.com yy';
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 0);
- });
-
- test('overlapping links', () => {
- element.config = {
- b1: {
- match: '(B:\\s*)(\\d+)',
- html: '$1<a href="ftp://foo/$2">$2</a>',
- },
- b2: {
- match: '(B:\\s*\\d+\\s*,\\s*)(\\d+)',
- html: '$1<a href="ftp://foo/$2">$2</a>',
- },
- };
- element.content = '- B: 123, 45';
- const links = Polymer.dom(element.root).querySelectorAll('a');
-
- assert.equal(links.length, 2);
- assert.equal(element.shadowRoot
- .querySelector('span').textContent, '- B: 123, 45');
-
- assert.equal(links[0].href, 'ftp://foo/123');
- assert.equal(links[0].textContent, '123');
-
- assert.equal(links[1].href, 'ftp://foo/45');
- assert.equal(links[1].textContent, '45');
- });
-
- test('_contentOrConfigChanged called with config', () => {
- const contentStub = sandbox.stub(element, '_contentChanged');
- const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged');
- element.content = 'some text';
- assert.isTrue(contentStub.called);
- assert.isTrue(contentConfigStub.called);
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(Gerrit.Nav, 'mapCommentlinks', x => x);
+ element.config = {
+ ph: {
+ match: '([Bb]ug|[Ii]ssue)\\s*#?(\\d+)',
+ link: 'https://bugs.chromium.org/p/gerrit/issues/detail?id=$2',
+ },
+ prefixsameinlinkandpattern: {
+ match: '([Hh][Tt][Tt][Pp]example)\\s*#?(\\d+)',
+ link: 'https://bugs.chromium.org/p/gerrit/issues/detail?id=$2',
+ },
+ changeid: {
+ match: '(I[0-9a-f]{8,40})',
+ link: '#/q/$1',
+ },
+ changeid2: {
+ match: 'Change-Id: +(I[0-9a-f]{8,40})',
+ link: '#/q/$1',
+ },
+ googlesearch: {
+ match: 'google:(.+)',
+ link: 'https://bing.com/search?q=$1', // html should supercede link.
+ html: '<a href="https://google.com/search?q=$1">$1</a>',
+ },
+ hashedhtml: {
+ match: 'hash:(.+)',
+ html: '<a href="#/awesomesauce">$1</a>',
+ },
+ baseurl: {
+ match: 'test (.+)',
+ html: '<a href="/r/awesomesauce">$1</a>',
+ },
+ anotatstartwithbaseurl: {
+ match: 'a test (.+)',
+ html: '[Lookup: <a href="/r/awesomesauce">$1</a>]',
+ },
+ disabledconfig: {
+ match: 'foo:(.+)',
+ link: 'https://google.com/search?q=$1',
+ enabled: false,
+ },
+ };
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('URL pattern was parsed and linked.', () => {
+ // Regular inline link.
+ const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
+ element.content = url;
+ const linkEl = element.$.output.childNodes[0];
+ assert.equal(linkEl.target, '_blank');
+ assert.equal(linkEl.rel, 'noopener');
+ assert.equal(linkEl.href, url);
+ assert.equal(linkEl.textContent, url);
+ });
+
+ test('Bug pattern was parsed and linked', () => {
+ // "Issue/Bug" pattern.
+ element.content = 'Issue 3650';
+
+ let linkEl = element.$.output.childNodes[0];
+ const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
+ assert.equal(linkEl.target, '_blank');
+ assert.equal(linkEl.href, url);
+ assert.equal(linkEl.textContent, 'Issue 3650');
+
+ element.content = 'Bug 3650';
+ linkEl = element.$.output.childNodes[0];
+ assert.equal(linkEl.target, '_blank');
+ assert.equal(linkEl.rel, 'noopener');
+ assert.equal(linkEl.href, url);
+ assert.equal(linkEl.textContent, 'Bug 3650');
+ });
+
+ test('Pattern with same prefix as link was correctly parsed', () => {
+ // Pattern starts with the same prefix (`http`) as the url.
+ element.content = 'httpexample 3650';
+
+ assert.equal(element.$.output.childNodes.length, 1);
+ const linkEl = element.$.output.childNodes[0];
+ const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
+ assert.equal(linkEl.target, '_blank');
+ assert.equal(linkEl.href, url);
+ assert.equal(linkEl.textContent, 'httpexample 3650');
+ });
+
+ test('Change-Id pattern was parsed and linked', () => {
+ // "Change-Id:" pattern.
+ const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
+ const prefix = 'Change-Id: ';
+ element.content = prefix + changeID;
+
+ const textNode = element.$.output.childNodes[0];
+ const linkEl = element.$.output.childNodes[1];
+ assert.equal(textNode.textContent, prefix);
+ const url = '/q/' + changeID;
+ assert.isFalse(linkEl.hasAttribute('target'));
+ // Since url is a path, the host is added automatically.
+ assert.isTrue(linkEl.href.endsWith(url));
+ assert.equal(linkEl.textContent, changeID);
+ });
+
+ test('Change-Id pattern was parsed and linked with base url', () => {
+ window.CANONICAL_PATH = '/r';
+
+ // "Change-Id:" pattern.
+ const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
+ const prefix = 'Change-Id: ';
+ element.content = prefix + changeID;
+
+ const textNode = element.$.output.childNodes[0];
+ const linkEl = element.$.output.childNodes[1];
+ assert.equal(textNode.textContent, prefix);
+ const url = '/r/q/' + changeID;
+ assert.isFalse(linkEl.hasAttribute('target'));
+ // Since url is a path, the host is added automatically.
+ assert.isTrue(linkEl.href.endsWith(url));
+ assert.equal(linkEl.textContent, changeID);
+ });
+
+ test('Multiple matches', () => {
+ element.content = 'Issue 3650\nIssue 3450';
+ const linkEl1 = element.$.output.childNodes[0];
+ const linkEl2 = element.$.output.childNodes[2];
+
+ assert.equal(linkEl1.target, '_blank');
+ assert.equal(linkEl1.href,
+ 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650');
+ assert.equal(linkEl1.textContent, 'Issue 3650');
+
+ assert.equal(linkEl2.target, '_blank');
+ assert.equal(linkEl2.href,
+ 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3450');
+ assert.equal(linkEl2.textContent, 'Issue 3450');
+ });
+
+ test('Change-Id pattern parsed before bug pattern', () => {
+ // "Change-Id:" pattern.
+ const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
+ const prefix = 'Change-Id: ';
+
+ // "Issue/Bug" pattern.
+ const bug = 'Issue 3650';
+
+ const changeUrl = '/q/' + changeID;
+ const bugUrl = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
+
+ element.content = prefix + changeID + bug;
+
+ const textNode = element.$.output.childNodes[0];
+ const changeLinkEl = element.$.output.childNodes[1];
+ const bugLinkEl = element.$.output.childNodes[2];
+
+ assert.equal(textNode.textContent, prefix);
+
+ assert.isFalse(changeLinkEl.hasAttribute('target'));
+ assert.isTrue(changeLinkEl.href.endsWith(changeUrl));
+ assert.equal(changeLinkEl.textContent, changeID);
+
+ assert.equal(bugLinkEl.target, '_blank');
+ assert.equal(bugLinkEl.href, bugUrl);
+ assert.equal(bugLinkEl.textContent, 'Issue 3650');
+ });
+
+ test('html field in link config', () => {
+ element.content = 'google:do a barrel roll';
+ const linkEl = element.$.output.childNodes[0];
+ assert.equal(linkEl.getAttribute('href'),
+ 'https://google.com/search?q=do a barrel roll');
+ assert.equal(linkEl.textContent, 'do a barrel roll');
+ });
+
+ test('removing hash from links', () => {
+ element.content = 'hash:foo';
+ const linkEl = element.$.output.childNodes[0];
+ assert.isTrue(linkEl.href.endsWith('/awesomesauce'));
+ assert.equal(linkEl.textContent, 'foo');
+ });
+
+ test('html with base url', () => {
+ window.CANONICAL_PATH = '/r';
+
+ element.content = 'test foo';
+ const linkEl = element.$.output.childNodes[0];
+ assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
+ assert.equal(linkEl.textContent, 'foo');
+ });
+
+ test('a is not at start', () => {
+ window.CANONICAL_PATH = '/r';
+
+ element.content = 'a test foo';
+ const linkEl = element.$.output.childNodes[1];
+ assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
+ assert.equal(linkEl.textContent, 'foo');
+ });
+
+ test('hash html with base url', () => {
+ window.CANONICAL_PATH = '/r';
+
+ element.content = 'hash:foo';
+ const linkEl = element.$.output.childNodes[0];
+ assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
+ assert.equal(linkEl.textContent, 'foo');
+ });
+
+ test('disabled config', () => {
+ element.content = 'foo:baz';
+ assert.equal(element.$.output.innerHTML, 'foo:baz');
+ });
+
+ test('R=email labels link correctly', () => {
+ element.removeZeroWidthSpace = true;
+ element.content = 'R=\u200Btest@google.com';
+ assert.equal(element.$.output.textContent, 'R=test@google.com');
+ assert.equal(element.$.output.innerHTML.match(/(R=<a)/g).length, 1);
+ });
+
+ test('CC=email labels link correctly', () => {
+ element.removeZeroWidthSpace = true;
+ element.content = 'CC=\u200Btest@google.com';
+ assert.equal(element.$.output.textContent, 'CC=test@google.com');
+ assert.equal(element.$.output.innerHTML.match(/(CC=<a)/g).length, 1);
+ });
+
+ test('only {http,https,mailto} protocols are linkified', () => {
+ element.content = 'xx mailto:test@google.com yy';
+ let links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
+ assert.equal(links[0].innerHTML, 'mailto:test@google.com');
+
+ element.content = 'xx http://google.com yy';
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'http://google.com');
+ assert.equal(links[0].innerHTML, 'http://google.com');
+
+ element.content = 'xx https://google.com yy';
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'https://google.com');
+ assert.equal(links[0].innerHTML, 'https://google.com');
+
+ element.content = 'xx ssh://google.com yy';
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 0);
+
+ element.content = 'xx ftp://google.com yy';
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 0);
+ });
+
+ test('links without leading whitespace are linkified', () => {
+ element.content = 'xx abcmailto:test@google.com yy';
+ assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx abc');
+ let links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
+ assert.equal(links[0].innerHTML, 'mailto:test@google.com');
+
+ element.content = 'xx defhttp://google.com yy';
+ assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx def');
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'http://google.com');
+ assert.equal(links[0].innerHTML, 'http://google.com');
+
+ element.content = 'xx qwehttps://google.com yy';
+ assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx qwe');
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'https://google.com');
+ assert.equal(links[0].innerHTML, 'https://google.com');
+
+ // Non-latin character
+ element.content = 'xx абвhttps://google.com yy';
+ assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx абв');
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'https://google.com');
+ assert.equal(links[0].innerHTML, 'https://google.com');
+
+ element.content = 'xx ssh://google.com yy';
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 0);
+
+ element.content = 'xx ftp://google.com yy';
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 0);
+ });
+
+ test('overlapping links', () => {
+ element.config = {
+ b1: {
+ match: '(B:\\s*)(\\d+)',
+ html: '$1<a href="ftp://foo/$2">$2</a>',
+ },
+ b2: {
+ match: '(B:\\s*\\d+\\s*,\\s*)(\\d+)',
+ html: '$1<a href="ftp://foo/$2">$2</a>',
+ },
+ };
+ element.content = '- B: 123, 45';
+ const links = dom(element.root).querySelectorAll('a');
+
+ assert.equal(links.length, 2);
+ assert.equal(element.shadowRoot
+ .querySelector('span').textContent, '- B: 123, 45');
+
+ assert.equal(links[0].href, 'ftp://foo/123');
+ assert.equal(links[0].textContent, '123');
+
+ assert.equal(links[1].href, 'ftp://foo/45');
+ assert.equal(links[1].textContent, '45');
+ });
+
+ test('_contentOrConfigChanged called with config', () => {
+ const contentStub = sandbox.stub(element, '_contentChanged');
+ const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged');
+ element.content = 'some text';
+ assert.isTrue(contentStub.called);
+ assert.isTrue(contentConfigStub.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html
deleted file mode 100644
index 3d41a7c..0000000
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html
+++ /dev/null
@@ -1,106 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
-
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-
-<dom-module id="gr-list-view">
- <template>
- <style include="shared-styles">
- #filter {
- max-width: 25em;
- }
- #filter:focus {
- outline: none;
- }
- #topContainer {
- align-items: center;
- display: flex;
- height: 3rem;
- justify-content: space-between;
- margin: 0 var(--spacing-l);
- }
- #createNewContainer:not(.show) {
- display: none;
- }
- a {
- color: var(--primary-text-color);
- text-decoration: none;
- }
- a:hover {
- text-decoration: underline;
- }
- nav {
- align-items: center;
- display: flex;
- height: 3rem;
- justify-content: flex-end;
- margin-right: 20px;
- }
- nav,
- iron-icon {
- color: var(--deemphasized-text-color);
- }
- iron-icon {
- height: 1.85rem;
- margin-left: 16px;
- width: 1.85rem;
- }
- </style>
- <div id="topContainer">
- <div class="filterContainer">
- <label>Filter:</label>
- <iron-input
- type="text"
- bind-value="{{filter}}">
- <input
- is="iron-input"
- type="text"
- id="filter"
- bind-value="{{filter}}">
- </iron-input>
- </div>
- <div id="createNewContainer"
- class$="[[_computeCreateClass(createNew)]]">
- <gr-button primary link id="createNew" on-click="_createNewItem">
- Create New
- </gr-button>
- </div>
- </div>
- <slot></slot>
- <nav>
- Page [[_computePage(offset, itemsPerPage)]]
- <a id="prevArrow"
- href$="[[_computeNavLink(offset, -1, itemsPerPage, filter, path)]]"
- hidden$="[[_hidePrevArrow(loading, offset)]]" hidden>
- <iron-icon icon="gr-icons:chevron-left"></iron-icon>
- </a>
- <a id="nextArrow"
- href$="[[_computeNavLink(offset, 1, itemsPerPage, filter, path)]]"
- hidden$="[[_hideNextArrow(loading, items)]]" hidden>
- <iron-icon icon="gr-icons:chevron-right"></iron-icon>
- </a>
- </nav>
- </template>
- <script src="gr-list-view.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
index 8913cd8..d52a912 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
@@ -14,106 +14,119 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const REQUEST_DEBOUNCE_INTERVAL_MS = 200;
+import '@polymer/iron-input/iron-input.js';
+import '@polymer/iron-icon/iron-icon.js';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../gr-button/gr-button.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-list-view_html.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrListView extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.FireBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-list-view'; }
+const REQUEST_DEBOUNCE_INTERVAL_MS = 200;
- static get properties() {
- return {
- createNew: Boolean,
- items: Array,
- itemsPerPage: Number,
- filter: {
- type: String,
- observer: '_filterChanged',
- },
- offset: Number,
- loading: Boolean,
- path: String,
- };
- }
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrListView extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.FireBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** @override */
- detached() {
- super.detached();
- this.cancelDebouncer('reload');
- }
+ static get is() { return 'gr-list-view'; }
- _filterChanged(newFilter, oldFilter) {
- if (!newFilter && !oldFilter) {
- return;
- }
-
- this._debounceReload(newFilter);
- }
-
- _debounceReload(filter) {
- this.debounce('reload', () => {
- if (filter) {
- return page.show(`${this.path}/q/filter:` +
- this.encodeURL(filter, false));
- }
- page.show(this.path);
- }, REQUEST_DEBOUNCE_INTERVAL_MS);
- }
-
- _createNewItem() {
- this.fire('create-clicked');
- }
-
- _computeNavLink(offset, direction, itemsPerPage, filter, path) {
- // Offset could be a string when passed from the router.
- offset = +(offset || 0);
- const newOffset = Math.max(0, offset + (itemsPerPage * direction));
- let href = this.getBaseUrl() + path;
- if (filter) {
- href += '/q/filter:' + this.encodeURL(filter, false);
- }
- if (newOffset > 0) {
- href += ',' + newOffset;
- }
- return href;
- }
-
- _computeCreateClass(createNew) {
- return createNew ? 'show' : '';
- }
-
- _hidePrevArrow(loading, offset) {
- return loading || offset === 0;
- }
-
- _hideNextArrow(loading, items) {
- if (loading || !items || !items.length) {
- return true;
- }
- const lastPage = items.length < this.itemsPerPage + 1;
- return lastPage;
- }
-
- // TODO: fix offset (including itemsPerPage)
- // to either support a decimal or make it go to the nearest
- // whole number (e.g 3).
- _computePage(offset, itemsPerPage) {
- return offset / itemsPerPage + 1;
- }
+ static get properties() {
+ return {
+ createNew: Boolean,
+ items: Array,
+ itemsPerPage: Number,
+ filter: {
+ type: String,
+ observer: '_filterChanged',
+ },
+ offset: Number,
+ loading: Boolean,
+ path: String,
+ };
}
- customElements.define(GrListView.is, GrListView);
-})();
+ /** @override */
+ detached() {
+ super.detached();
+ this.cancelDebouncer('reload');
+ }
+
+ _filterChanged(newFilter, oldFilter) {
+ if (!newFilter && !oldFilter) {
+ return;
+ }
+
+ this._debounceReload(newFilter);
+ }
+
+ _debounceReload(filter) {
+ this.debounce('reload', () => {
+ if (filter) {
+ return page.show(`${this.path}/q/filter:` +
+ this.encodeURL(filter, false));
+ }
+ page.show(this.path);
+ }, REQUEST_DEBOUNCE_INTERVAL_MS);
+ }
+
+ _createNewItem() {
+ this.fire('create-clicked');
+ }
+
+ _computeNavLink(offset, direction, itemsPerPage, filter, path) {
+ // Offset could be a string when passed from the router.
+ offset = +(offset || 0);
+ const newOffset = Math.max(0, offset + (itemsPerPage * direction));
+ let href = this.getBaseUrl() + path;
+ if (filter) {
+ href += '/q/filter:' + this.encodeURL(filter, false);
+ }
+ if (newOffset > 0) {
+ href += ',' + newOffset;
+ }
+ return href;
+ }
+
+ _computeCreateClass(createNew) {
+ return createNew ? 'show' : '';
+ }
+
+ _hidePrevArrow(loading, offset) {
+ return loading || offset === 0;
+ }
+
+ _hideNextArrow(loading, items) {
+ if (loading || !items || !items.length) {
+ return true;
+ }
+ const lastPage = items.length < this.itemsPerPage + 1;
+ return lastPage;
+ }
+
+ // TODO: fix offset (including itemsPerPage)
+ // to either support a decimal or make it go to the nearest
+ // whole number (e.g 3).
+ _computePage(offset, itemsPerPage) {
+ return offset / itemsPerPage + 1;
+ }
+}
+
+customElements.define(GrListView.is, GrListView);
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.js
new file mode 100644
index 0000000..0d33dd0
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.js
@@ -0,0 +1,84 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ #filter {
+ max-width: 25em;
+ }
+ #filter:focus {
+ outline: none;
+ }
+ #topContainer {
+ align-items: center;
+ display: flex;
+ height: 3rem;
+ justify-content: space-between;
+ margin: 0 var(--spacing-l);
+ }
+ #createNewContainer:not(.show) {
+ display: none;
+ }
+ a {
+ color: var(--primary-text-color);
+ text-decoration: none;
+ }
+ a:hover {
+ text-decoration: underline;
+ }
+ nav {
+ align-items: center;
+ display: flex;
+ height: 3rem;
+ justify-content: flex-end;
+ margin-right: 20px;
+ }
+ nav,
+ iron-icon {
+ color: var(--deemphasized-text-color);
+ }
+ iron-icon {
+ height: 1.85rem;
+ margin-left: 16px;
+ width: 1.85rem;
+ }
+ </style>
+ <div id="topContainer">
+ <div class="filterContainer">
+ <label>Filter:</label>
+ <iron-input type="text" bind-value="{{filter}}">
+ <input is="iron-input" type="text" id="filter" bind-value="{{filter}}">
+ </iron-input>
+ </div>
+ <div id="createNewContainer" class\$="[[_computeCreateClass(createNew)]]">
+ <gr-button primary="" link="" id="createNew" on-click="_createNewItem">
+ Create New
+ </gr-button>
+ </div>
+ </div>
+ <slot></slot>
+ <nav>
+ Page [[_computePage(offset, itemsPerPage)]]
+ <a id="prevArrow" href\$="[[_computeNavLink(offset, -1, itemsPerPage, filter, path)]]" hidden\$="[[_hidePrevArrow(loading, offset)]]" hidden="">
+ <iron-icon icon="gr-icons:chevron-left"></iron-icon>
+ </a>
+ <a id="nextArrow" href\$="[[_computeNavLink(offset, 1, itemsPerPage, filter, path)]]" hidden\$="[[_hideNextArrow(loading, items)]]" hidden="">
+ <iron-icon icon="gr-icons:chevron-right"></iron-icon>
+ </a>
+ </nav>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
index 70605a1..accf73e 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
@@ -19,16 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-list-view</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-list-view.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -36,133 +30,134 @@
</template>
</test-fixture>
-<script>
- suite('gr-list-view tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-list-view.js';
+suite('gr-list-view tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_computeNavLink', () => {
+ const offset = 25;
+ const projectsPerPage = 25;
+ let filter = 'test';
+ const path = '/admin/projects';
+
+ sandbox.stub(element, 'getBaseUrl', () => '');
+
+ assert.equal(
+ element._computeNavLink(offset, 1, projectsPerPage, filter, path),
+ '/admin/projects/q/filter:test,50');
+
+ assert.equal(
+ element._computeNavLink(offset, -1, projectsPerPage, filter, path),
+ '/admin/projects/q/filter:test');
+
+ assert.equal(
+ element._computeNavLink(offset, 1, projectsPerPage, null, path),
+ '/admin/projects,50');
+
+ assert.equal(
+ element._computeNavLink(offset, -1, projectsPerPage, null, path),
+ '/admin/projects');
+
+ filter = 'plugins/';
+ assert.equal(
+ element._computeNavLink(offset, 1, projectsPerPage, filter, path),
+ '/admin/projects/q/filter:plugins%252F,50');
+ });
+
+ test('_onValueChange', done => {
+ element.path = '/admin/projects';
+ sandbox.stub(page, 'show', url => {
+ assert.equal(url, '/admin/projects/q/filter:test');
+ done();
});
+ element.filter = 'test';
+ });
- teardown(() => {
- sandbox.restore();
- });
+ test('_filterChanged not reload when swap between falsy values', () => {
+ sandbox.stub(element, '_debounceReload');
+ element.filter = null;
+ element.filter = undefined;
+ element.filter = '';
+ assert.isFalse(element._debounceReload.called);
+ });
- test('_computeNavLink', () => {
- const offset = 25;
- const projectsPerPage = 25;
- let filter = 'test';
- const path = '/admin/projects';
+ test('next button', done => {
+ element.itemsPerPage = 25;
+ let projects = new Array(26);
- sandbox.stub(element, 'getBaseUrl', () => '');
-
- assert.equal(
- element._computeNavLink(offset, 1, projectsPerPage, filter, path),
- '/admin/projects/q/filter:test,50');
-
- assert.equal(
- element._computeNavLink(offset, -1, projectsPerPage, filter, path),
- '/admin/projects/q/filter:test');
-
- assert.equal(
- element._computeNavLink(offset, 1, projectsPerPage, null, path),
- '/admin/projects,50');
-
- assert.equal(
- element._computeNavLink(offset, -1, projectsPerPage, null, path),
- '/admin/projects');
-
- filter = 'plugins/';
- assert.equal(
- element._computeNavLink(offset, 1, projectsPerPage, filter, path),
- '/admin/projects/q/filter:plugins%252F,50');
- });
-
- test('_onValueChange', done => {
- element.path = '/admin/projects';
- sandbox.stub(page, 'show', url => {
- assert.equal(url, '/admin/projects/q/filter:test');
- done();
- });
- element.filter = 'test';
- });
-
- test('_filterChanged not reload when swap between falsy values', () => {
- sandbox.stub(element, '_debounceReload');
- element.filter = null;
- element.filter = undefined;
- element.filter = '';
- assert.isFalse(element._debounceReload.called);
- });
-
- test('next button', done => {
- element.itemsPerPage = 25;
- let projects = new Array(26);
-
- flush(() => {
- let loading;
- assert.isFalse(element._hideNextArrow(loading, projects));
- loading = true;
- assert.isTrue(element._hideNextArrow(loading, projects));
- loading = false;
- assert.isFalse(element._hideNextArrow(loading, projects));
- element._projects = [];
- assert.isTrue(element._hideNextArrow(loading, element._projects));
- projects = new Array(4);
- assert.isTrue(element._hideNextArrow(loading, projects));
- done();
- });
- });
-
- test('prev button', () => {
- assert.isTrue(element._hidePrevArrow(true, 0));
- flush(() => {
- let offset = 0;
- assert.isTrue(element._hidePrevArrow(false, offset));
- offset = 5;
- assert.isFalse(element._hidePrevArrow(false, offset));
- });
- });
-
- test('createNew link appears correctly', () => {
- assert.isFalse(element.shadowRoot
- .querySelector('#createNewContainer').classList
- .contains('show'));
- element.createNew = true;
- flushAsynchronousOperations();
- assert.isTrue(element.shadowRoot
- .querySelector('#createNewContainer').classList
- .contains('show'));
- });
-
- test('fires create clicked event when button tapped', () => {
- const clickHandler = sandbox.stub();
- element.addEventListener('create-clicked', clickHandler);
- element.createNew = true;
- flushAsynchronousOperations();
- MockInteractions.tap(element.shadowRoot.querySelector('#createNew'));
- assert.isTrue(clickHandler.called);
- });
-
- test('next/prev links change when path changes', () => {
- const BRANCHES_PATH = '/path/to/branches';
- const TAGS_PATH = '/path/to/tags';
- sandbox.stub(element, '_computeNavLink');
- element.offset = 0;
- element.itemsPerPage = 25;
- element.filter = '';
- element.path = BRANCHES_PATH;
- assert.equal(element._computeNavLink.lastCall.args[4], BRANCHES_PATH);
- element.path = TAGS_PATH;
- assert.equal(element._computeNavLink.lastCall.args[4], TAGS_PATH);
- });
-
- test('_computePage', () => {
- assert.equal(element._computePage(0, 25), 1);
- assert.equal(element._computePage(50, 25), 3);
+ flush(() => {
+ let loading;
+ assert.isFalse(element._hideNextArrow(loading, projects));
+ loading = true;
+ assert.isTrue(element._hideNextArrow(loading, projects));
+ loading = false;
+ assert.isFalse(element._hideNextArrow(loading, projects));
+ element._projects = [];
+ assert.isTrue(element._hideNextArrow(loading, element._projects));
+ projects = new Array(4);
+ assert.isTrue(element._hideNextArrow(loading, projects));
+ done();
});
});
+
+ test('prev button', () => {
+ assert.isTrue(element._hidePrevArrow(true, 0));
+ flush(() => {
+ let offset = 0;
+ assert.isTrue(element._hidePrevArrow(false, offset));
+ offset = 5;
+ assert.isFalse(element._hidePrevArrow(false, offset));
+ });
+ });
+
+ test('createNew link appears correctly', () => {
+ assert.isFalse(element.shadowRoot
+ .querySelector('#createNewContainer').classList
+ .contains('show'));
+ element.createNew = true;
+ flushAsynchronousOperations();
+ assert.isTrue(element.shadowRoot
+ .querySelector('#createNewContainer').classList
+ .contains('show'));
+ });
+
+ test('fires create clicked event when button tapped', () => {
+ const clickHandler = sandbox.stub();
+ element.addEventListener('create-clicked', clickHandler);
+ element.createNew = true;
+ flushAsynchronousOperations();
+ MockInteractions.tap(element.shadowRoot.querySelector('#createNew'));
+ assert.isTrue(clickHandler.called);
+ });
+
+ test('next/prev links change when path changes', () => {
+ const BRANCHES_PATH = '/path/to/branches';
+ const TAGS_PATH = '/path/to/tags';
+ sandbox.stub(element, '_computeNavLink');
+ element.offset = 0;
+ element.itemsPerPage = 25;
+ element.filter = '';
+ element.path = BRANCHES_PATH;
+ assert.equal(element._computeNavLink.lastCall.args[4], BRANCHES_PATH);
+ element.path = TAGS_PATH;
+ assert.equal(element._computeNavLink.lastCall.args[4], TAGS_PATH);
+ });
+
+ test('_computePage', () => {
+ assert.equal(element._computePage(0, 25), 1);
+ assert.equal(element._computePage(50, 25), 3);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
deleted file mode 100644
index 1afd1c9..0000000
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
+++ /dev/null
@@ -1,47 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-overlay-behavior/iron-overlay-behavior.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-overlay">
- <template>
- <style include="shared-styles">
- :host {
- background: var(--dialog-background-color);
- border-radius: var(--border-radius);
- box-shadow: var(--elevation-level-5);
- }
-
- @media screen and (max-width: 50em) {
- :host {
- height: 100%;
- left: 0;
- position: fixed;
- right: 0;
- top: 0;
- border-radius: 0;
- box-shadow: none;
- }
- }
- </style>
- <slot></slot>
- </template>
- <script src="gr-overlay.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
index 3a957ef..fd68971 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -14,108 +14,117 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const AWAIT_MAX_ITERS = 10;
- const AWAIT_STEP = 5;
- const BREAKPOINT_FULLSCREEN_OVERLAY = '50em';
+import {IronOverlayBehaviorImpl, IronOverlayBehavior} from '@polymer/iron-overlay-behavior/iron-overlay-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-overlay_html.js';
+
+const AWAIT_MAX_ITERS = 10;
+const AWAIT_STEP = 5;
+const BREAKPOINT_FULLSCREEN_OVERLAY = '50em';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrOverlay extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ IronOverlayBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-overlay'; }
+ /**
+ * Fired when a fullscreen overlay is closed
+ *
+ * @event fullscreen-overlay-closed
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when an overlay is opened in full screen mode
+ *
+ * @event fullscreen-overlay-opened
*/
- class GrOverlay extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Polymer.IronOverlayBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-overlay'; }
- /**
- * Fired when a fullscreen overlay is closed
- *
- * @event fullscreen-overlay-closed
- */
- /**
- * Fired when an overlay is opened in full screen mode
- *
- * @event fullscreen-overlay-opened
- */
+ static get properties() {
+ return {
+ _fullScreenOpen: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
- static get properties() {
- return {
- _fullScreenOpen: {
- type: Boolean,
- value: false,
- },
- };
- }
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('iron-overlay-closed',
+ () => this._close());
+ this.addEventListener('iron-overlay-cancelled',
+ () => this._close());
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('iron-overlay-closed',
- () => this._close());
- this.addEventListener('iron-overlay-cancelled',
- () => this._close());
- }
-
- open(...args) {
- return new Promise((resolve, reject) => {
- Polymer.IronOverlayBehaviorImpl.open.apply(this, args);
- if (this._isMobile()) {
- this.fire('fullscreen-overlay-opened');
- this._fullScreenOpen = true;
- }
- this._awaitOpen(resolve, reject);
- });
- }
-
- _isMobile() {
- return window.matchMedia(`(max-width: ${BREAKPOINT_FULLSCREEN_OVERLAY})`);
- }
-
- _close() {
- if (this._fullScreenOpen) {
- this.fire('fullscreen-overlay-closed');
- this._fullScreenOpen = false;
+ open(...args) {
+ return new Promise((resolve, reject) => {
+ IronOverlayBehaviorImpl.open.apply(this, args);
+ if (this._isMobile()) {
+ this.fire('fullscreen-overlay-opened');
+ this._fullScreenOpen = true;
}
- }
+ this._awaitOpen(resolve, reject);
+ });
+ }
- /**
- * Override the focus stops that iron-overlay-behavior tries to find.
- */
- setFocusStops(stops) {
- this.__firstFocusableNode = stops.start;
- this.__lastFocusableNode = stops.end;
- }
+ _isMobile() {
+ return window.matchMedia(`(max-width: ${BREAKPOINT_FULLSCREEN_OVERLAY})`);
+ }
- /**
- * 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, reject) {
- let iters = 0;
- const step = () => {
- this.async(() => {
- if (this.style.display !== 'none') {
- fn.call(this);
- } else if (iters++ < AWAIT_MAX_ITERS) {
- step.call(this);
- } else {
- reject(new Error('gr-overlay _awaitOpen failed to resolve'));
- }
- }, AWAIT_STEP);
- };
- step.call(this);
- }
-
- _id() {
- return this.getAttribute('id') || 'global';
+ _close() {
+ if (this._fullScreenOpen) {
+ this.fire('fullscreen-overlay-closed');
+ this._fullScreenOpen = false;
}
}
- customElements.define(GrOverlay.is, GrOverlay);
-})();
+ /**
+ * Override the focus stops that iron-overlay-behavior tries to find.
+ */
+ setFocusStops(stops) {
+ this.__firstFocusableNode = stops.start;
+ this.__lastFocusableNode = stops.end;
+ }
+
+ /**
+ * 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, reject) {
+ let iters = 0;
+ const step = () => {
+ this.async(() => {
+ if (this.style.display !== 'none') {
+ fn.call(this);
+ } else if (iters++ < AWAIT_MAX_ITERS) {
+ step.call(this);
+ } else {
+ reject(new Error('gr-overlay _awaitOpen failed to resolve'));
+ }
+ }, AWAIT_STEP);
+ };
+ step.call(this);
+ }
+
+ _id() {
+ return this.getAttribute('id') || 'global';
+ }
+}
+
+customElements.define(GrOverlay.is, GrOverlay);
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.js
new file mode 100644
index 0000000..5fe000a
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.js
@@ -0,0 +1,40 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ background: var(--dialog-background-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--elevation-level-5);
+ }
+
+ @media screen and (max-width: 50em) {
+ :host {
+ height: 100%;
+ left: 0;
+ position: fixed;
+ right: 0;
+ top: 0;
+ border-radius: 0;
+ box-shadow: none;
+ }
+ }
+ </style>
+ <slot></slot>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
index e1218af3..52bba7a 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
@@ -19,18 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-overlay</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-
-<link rel="import" href="gr-overlay.html">
-
-<script>void(0);</script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -40,57 +33,58 @@
</template>
</test-fixture>
-<script>
- suite('gr-overlay tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-overlay.js';
+suite('gr-overlay tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('events are fired on fullscreen view', done => {
- sandbox.stub(element, '_isMobile').returns(true);
- const openHandler = sandbox.stub();
- const closeHandler = sandbox.stub();
- element.addEventListener('fullscreen-overlay-opened', openHandler);
- element.addEventListener('fullscreen-overlay-closed', closeHandler);
+ test('events are fired on fullscreen view', done => {
+ sandbox.stub(element, '_isMobile').returns(true);
+ const openHandler = sandbox.stub();
+ const closeHandler = sandbox.stub();
+ element.addEventListener('fullscreen-overlay-opened', openHandler);
+ element.addEventListener('fullscreen-overlay-closed', closeHandler);
- element.open().then(() => {
- assert.isTrue(element._isMobile.called);
- assert.isTrue(element._fullScreenOpen);
- assert.isTrue(openHandler.called);
+ element.open().then(() => {
+ assert.isTrue(element._isMobile.called);
+ assert.isTrue(element._fullScreenOpen);
+ assert.isTrue(openHandler.called);
- element._close();
- assert.isFalse(element._fullScreenOpen);
- assert.isTrue(closeHandler.called);
- done();
- });
- });
-
- test('events are not fired on desktop view', done => {
- sandbox.stub(element, '_isMobile').returns(false);
- const openHandler = sandbox.stub();
- const closeHandler = sandbox.stub();
- element.addEventListener('fullscreen-overlay-opened', openHandler);
- element.addEventListener('fullscreen-overlay-closed', closeHandler);
-
- element.open().then(() => {
- assert.isTrue(element._isMobile.called);
- assert.isFalse(element._fullScreenOpen);
- assert.isFalse(openHandler.called);
-
- element._close();
- assert.isFalse(element._fullScreenOpen);
- assert.isFalse(closeHandler.called);
- done();
- });
+ element._close();
+ assert.isFalse(element._fullScreenOpen);
+ assert.isTrue(closeHandler.called);
+ done();
});
});
+
+ test('events are not fired on desktop view', done => {
+ sandbox.stub(element, '_isMobile').returns(false);
+ const openHandler = sandbox.stub();
+ const closeHandler = sandbox.stub();
+ element.addEventListener('fullscreen-overlay-opened', openHandler);
+ element.addEventListener('fullscreen-overlay-closed', closeHandler);
+
+ element.open().then(() => {
+ assert.isTrue(element._isMobile.called);
+ assert.isFalse(element._fullScreenOpen);
+ assert.isFalse(openHandler.called);
+
+ element._close();
+ assert.isFalse(element._fullScreenOpen);
+ assert.isFalse(closeHandler.called);
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
deleted file mode 100644
index f1c3a6f..0000000
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
+++ /dev/null
@@ -1,47 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-page-nav">
- <template>
- <style include="shared-styles">
- #nav {
- background-color: var(--table-header-background-color);
- border: 1px solid var(--border-color);
- border-top: none;
- height: 100%;
- position: absolute;
- top: 0;
- width: 14em;
- }
- #nav.pinned {
- position: fixed;
- }
- @media only screen and (max-width: 53em) {
- #nav {
- display: none;
- }
- }
- </style>
- <nav id="nav">
- <slot></slot>
- </nav>
- </template>
- <script src="gr-page-nav.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
index ac876c4..23b284c 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
@@ -14,62 +14,69 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
- /** @extends Polymer.Element */
- class GrPageNav extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-page-nav'; }
+import '../../../scripts/bundled-polymer.js';
+import '../../../styles/shared-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-page-nav_html.js';
- static get properties() {
- return {
- _headerHeight: Number,
- };
- }
+/** @extends Polymer.Element */
+class GrPageNav extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /** @override */
- attached() {
- super.attached();
- this.listen(window, 'scroll', '_handleBodyScroll');
- }
+ static get is() { return 'gr-page-nav'; }
- /** @override */
- detached() {
- super.detached();
- this.unlisten(window, 'scroll', '_handleBodyScroll');
- }
-
- _handleBodyScroll() {
- if (this._headerHeight === undefined) {
- let top = this._getOffsetTop(this);
- for (let offsetParent = this.offsetParent;
- offsetParent;
- offsetParent = this._getOffsetParent(offsetParent)) {
- top += this._getOffsetTop(offsetParent);
- }
- this._headerHeight = top;
- }
-
- this.$.nav.classList.toggle('pinned',
- this._getScrollY() >= this._headerHeight);
- }
-
- /* Functions used for test purposes */
- _getOffsetParent(element) {
- if (!element || !element.offsetParent) { return ''; }
- return element.offsetParent;
- }
-
- _getOffsetTop(element) {
- return element.offsetTop;
- }
-
- _getScrollY() {
- return window.scrollY;
- }
+ static get properties() {
+ return {
+ _headerHeight: Number,
+ };
}
- customElements.define(GrPageNav.is, GrPageNav);
-})();
+ /** @override */
+ attached() {
+ super.attached();
+ this.listen(window, 'scroll', '_handleBodyScroll');
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this.unlisten(window, 'scroll', '_handleBodyScroll');
+ }
+
+ _handleBodyScroll() {
+ if (this._headerHeight === undefined) {
+ let top = this._getOffsetTop(this);
+ for (let offsetParent = this.offsetParent;
+ offsetParent;
+ offsetParent = this._getOffsetParent(offsetParent)) {
+ top += this._getOffsetTop(offsetParent);
+ }
+ this._headerHeight = top;
+ }
+
+ this.$.nav.classList.toggle('pinned',
+ this._getScrollY() >= this._headerHeight);
+ }
+
+ /* Functions used for test purposes */
+ _getOffsetParent(element) {
+ if (!element || !element.offsetParent) { return ''; }
+ return element.offsetParent;
+ }
+
+ _getOffsetTop(element) {
+ return element.offsetTop;
+ }
+
+ _getScrollY() {
+ return window.scrollY;
+ }
+}
+
+customElements.define(GrPageNav.is, GrPageNav);
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.js b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.js
new file mode 100644
index 0000000..fe17a1b
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.js
@@ -0,0 +1,42 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ #nav {
+ background-color: var(--table-header-background-color);
+ border: 1px solid var(--border-color);
+ border-top: none;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ width: 14em;
+ }
+ #nav.pinned {
+ position: fixed;
+ }
+ @media only screen and (max-width: 53em) {
+ #nav {
+ display: none;
+ }
+ }
+ </style>
+ <nav id="nav">
+ <slot></slot>
+ </nav>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html
index fdfe46c..c1f7ace 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html
@@ -19,18 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-page-nav</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-
-<link rel="import" href="gr-page-nav.html">
-
-<script>void(0);</script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -42,53 +35,54 @@
</template>
</test-fixture>
-<script>
- suite('gr-page-nav tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-page-nav.js';
+suite('gr-page-nav tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- flushAsynchronousOperations();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('header is not pinned just below top', () => {
- sandbox.stub(element, '_getOffsetParent', () => 0);
- sandbox.stub(element, '_getOffsetTop', () => 10);
- sandbox.stub(element, '_getScrollY', () => 5);
- element._handleBodyScroll();
- assert.isFalse(element.$.nav.classList.contains('pinned'));
- });
-
- test('header is pinned when scroll down the page', () => {
- sandbox.stub(element, '_getOffsetParent', () => 0);
- sandbox.stub(element, '_getOffsetTop', () => 10);
- sandbox.stub(element, '_getScrollY', () => 25);
- window.scrollY = 100;
- element._handleBodyScroll();
- assert.isTrue(element.$.nav.classList.contains('pinned'));
- });
-
- test('header is not pinned just below top with header set', () => {
- element._headerHeight = 20;
- sandbox.stub(element, '_getScrollY', () => 15);
- window.scrollY = 100;
- element._handleBodyScroll();
- assert.isFalse(element.$.nav.classList.contains('pinned'));
- });
-
- test('header is pinned when scroll down the page with header set', () => {
- element._headerHeight = 20;
- sandbox.stub(element, '_getScrollY', () => 25);
- window.scrollY = 100;
- element._handleBodyScroll();
- assert.isTrue(element.$.nav.classList.contains('pinned'));
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ flushAsynchronousOperations();
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('header is not pinned just below top', () => {
+ sandbox.stub(element, '_getOffsetParent', () => 0);
+ sandbox.stub(element, '_getOffsetTop', () => 10);
+ sandbox.stub(element, '_getScrollY', () => 5);
+ element._handleBodyScroll();
+ assert.isFalse(element.$.nav.classList.contains('pinned'));
+ });
+
+ test('header is pinned when scroll down the page', () => {
+ sandbox.stub(element, '_getOffsetParent', () => 0);
+ sandbox.stub(element, '_getOffsetTop', () => 10);
+ sandbox.stub(element, '_getScrollY', () => 25);
+ window.scrollY = 100;
+ element._handleBodyScroll();
+ assert.isTrue(element.$.nav.classList.contains('pinned'));
+ });
+
+ test('header is not pinned just below top with header set', () => {
+ element._headerHeight = 20;
+ sandbox.stub(element, '_getScrollY', () => 15);
+ window.scrollY = 100;
+ element._handleBodyScroll();
+ assert.isFalse(element.$.nav.classList.contains('pinned'));
+ });
+
+ test('header is pinned when scroll down the page with header set', () => {
+ element._headerHeight = 20;
+ sandbox.stub(element, '_getScrollY', () => 25);
+ window.scrollY = 100;
+ element._handleBodyScroll();
+ assert.isTrue(element.$.nav.classList.contains('pinned'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.html b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.html
deleted file mode 100644
index ce596f8..0000000
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.html
+++ /dev/null
@@ -1,60 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-repo-branch-picker">
- <template>
- <style include="shared-styles">
- :host {
- display: block;
- }
- gr-labeled-autocomplete,
- iron-icon {
- display: inline-block;
- }
- iron-icon {
- margin-bottom: var(--spacing-l);
- }
- </style>
- <div>
- <gr-labeled-autocomplete
- id="repoInput"
- label="Repository"
- placeholder="Select repo"
- on-commit="_repoCommitted"
- query="[[_repoQuery]]">
- </gr-labeled-autocomplete>
- <iron-icon icon="gr-icons:chevron-right"></iron-icon>
- <gr-labeled-autocomplete
- id="branchInput"
- label="Branch"
- placeholder="Select branch"
- disabled="[[_branchDisabled]]"
- on-commit="_branchCommitted"
- query="[[_query]]">
- </gr-labeled-autocomplete>
- </div>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-repo-branch-picker.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
index 18e2596..15d5f76 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
@@ -14,110 +14,122 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const SUGGESTIONS_LIMIT = 15;
- const REF_PREFIX = 'refs/heads/';
+import '@polymer/iron-icon/iron-icon.js';
+import '../../../styles/shared-styles.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../gr-icons/gr-icons.js';
+import '../gr-labeled-autocomplete/gr-labeled-autocomplete.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-repo-branch-picker_html.js';
- /**
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrRepoBranchPicker extends Polymer.mixinBehaviors( [
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-repo-branch-picker'; }
+const SUGGESTIONS_LIMIT = 15;
+const REF_PREFIX = 'refs/heads/';
- static get properties() {
- return {
- repo: {
- type: String,
- notify: true,
- observer: '_repoChanged',
+/**
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrRepoBranchPicker extends mixinBehaviors( [
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-repo-branch-picker'; }
+
+ static get properties() {
+ return {
+ repo: {
+ type: String,
+ notify: true,
+ observer: '_repoChanged',
+ },
+ branch: {
+ type: String,
+ notify: true,
+ },
+ _branchDisabled: Boolean,
+ _query: {
+ type: Function,
+ value() {
+ return this._getRepoBranchesSuggestions.bind(this);
},
- branch: {
- type: String,
- notify: true,
+ },
+ _repoQuery: {
+ type: Function,
+ value() {
+ return this._getRepoSuggestions.bind(this);
},
- _branchDisabled: Boolean,
- _query: {
- type: Function,
- value() {
- return this._getRepoBranchesSuggestions.bind(this);
- },
- },
- _repoQuery: {
- type: Function,
- value() {
- return this._getRepoSuggestions.bind(this);
- },
- },
- };
- }
+ },
+ };
+ }
- /** @override */
- attached() {
- super.attached();
- if (this.repo) {
- this.$.repoInput.setText(this.repo);
- }
- }
-
- /** @override */
- ready() {
- super.ready();
- this._branchDisabled = !this.repo;
- }
-
- _getRepoBranchesSuggestions(input) {
- if (!this.repo) { return Promise.resolve([]); }
- if (input.startsWith(REF_PREFIX)) {
- input = input.substring(REF_PREFIX.length);
- }
- return this.$.restAPI.getRepoBranches(input, this.repo, SUGGESTIONS_LIMIT)
- .then(this._branchResponseToSuggestions.bind(this));
- }
-
- _getRepoSuggestions(input) {
- return this.$.restAPI.getRepos(input, SUGGESTIONS_LIMIT)
- .then(this._repoResponseToSuggestions.bind(this));
- }
-
- _repoResponseToSuggestions(res) {
- return res.map(repo => {
- return {
- name: repo.name,
- value: this.singleDecodeURL(repo.id),
- };
- });
- }
-
- _branchResponseToSuggestions(res) {
- return Object.keys(res).map(key => {
- let branch = res[key].ref;
- if (branch.startsWith(REF_PREFIX)) {
- branch = branch.substring(REF_PREFIX.length);
- }
- return {name: branch, value: branch};
- });
- }
-
- _repoCommitted(e) {
- this.repo = e.detail.value;
- }
-
- _branchCommitted(e) {
- this.branch = e.detail.value;
- }
-
- _repoChanged() {
- this.$.branchInput.clear();
- this._branchDisabled = !this.repo;
+ /** @override */
+ attached() {
+ super.attached();
+ if (this.repo) {
+ this.$.repoInput.setText(this.repo);
}
}
- customElements.define(GrRepoBranchPicker.is, GrRepoBranchPicker);
-})();
+ /** @override */
+ ready() {
+ super.ready();
+ this._branchDisabled = !this.repo;
+ }
+
+ _getRepoBranchesSuggestions(input) {
+ if (!this.repo) { return Promise.resolve([]); }
+ if (input.startsWith(REF_PREFIX)) {
+ input = input.substring(REF_PREFIX.length);
+ }
+ return this.$.restAPI.getRepoBranches(input, this.repo, SUGGESTIONS_LIMIT)
+ .then(this._branchResponseToSuggestions.bind(this));
+ }
+
+ _getRepoSuggestions(input) {
+ return this.$.restAPI.getRepos(input, SUGGESTIONS_LIMIT)
+ .then(this._repoResponseToSuggestions.bind(this));
+ }
+
+ _repoResponseToSuggestions(res) {
+ return res.map(repo => {
+ return {
+ name: repo.name,
+ value: this.singleDecodeURL(repo.id),
+ };
+ });
+ }
+
+ _branchResponseToSuggestions(res) {
+ return Object.keys(res).map(key => {
+ let branch = res[key].ref;
+ if (branch.startsWith(REF_PREFIX)) {
+ branch = branch.substring(REF_PREFIX.length);
+ }
+ return {name: branch, value: branch};
+ });
+ }
+
+ _repoCommitted(e) {
+ this.repo = e.detail.value;
+ }
+
+ _branchCommitted(e) {
+ this.branch = e.detail.value;
+ }
+
+ _repoChanged() {
+ this.$.branchInput.clear();
+ this._branchDisabled = !this.repo;
+ }
+}
+
+customElements.define(GrRepoBranchPicker.is, GrRepoBranchPicker);
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.js b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.js
new file mode 100644
index 0000000..fe6b522
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.js
@@ -0,0 +1,40 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: block;
+ }
+ gr-labeled-autocomplete,
+ iron-icon {
+ display: inline-block;
+ }
+ iron-icon {
+ margin-bottom: var(--spacing-l);
+ }
+ </style>
+ <div>
+ <gr-labeled-autocomplete id="repoInput" label="Repository" placeholder="Select repo" on-commit="_repoCommitted" query="[[_repoQuery]]">
+ </gr-labeled-autocomplete>
+ <iron-icon icon="gr-icons:chevron-right"></iron-icon>
+ <gr-labeled-autocomplete id="branchInput" label="Branch" placeholder="Select branch" disabled="[[_branchDisabled]]" on-commit="_branchCommitted" query="[[_query]]">
+ </gr-labeled-autocomplete>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
index b068b25..8e50b09 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
@@ -18,15 +18,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-branch-picker</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-repo-branch-picker.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -34,113 +29,114 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo-branch-picker tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-repo-branch-picker.js';
+suite('gr-repo-branch-picker tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => { sandbox.restore(); });
+
+ suite('_getRepoSuggestions', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
+ sandbox.stub(element.$.restAPI, 'getRepos')
+ .returns(Promise.resolve([
+ {
+ id: 'plugins%2Favatars-external',
+ name: 'plugins/avatars-external',
+ }, {
+ id: 'plugins%2Favatars-gravatar',
+ name: 'plugins/avatars-gravatar',
+ }, {
+ id: 'plugins%2Favatars%2Fexternal',
+ name: 'plugins/avatars/external',
+ }, {
+ id: 'plugins%2Favatars%2Fgravatar',
+ name: 'plugins/avatars/gravatar',
+ },
+ ]));
});
- teardown(() => { sandbox.restore(); });
-
- suite('_getRepoSuggestions', () => {
- setup(() => {
- sandbox.stub(element.$.restAPI, 'getRepos')
- .returns(Promise.resolve([
- {
- id: 'plugins%2Favatars-external',
- name: 'plugins/avatars-external',
- }, {
- id: 'plugins%2Favatars-gravatar',
- name: 'plugins/avatars-gravatar',
- }, {
- id: 'plugins%2Favatars%2Fexternal',
- name: 'plugins/avatars/external',
- }, {
- id: 'plugins%2Favatars%2Fgravatar',
- name: 'plugins/avatars/gravatar',
- },
- ]));
- });
-
- test('converts to suggestion objects', () => {
- const input = 'plugins/avatars';
- return element._getRepoSuggestions(input).then(suggestions => {
- assert.isTrue(element.$.restAPI.getRepos.calledWith(input));
- const unencodedNames = [
- 'plugins/avatars-external',
- 'plugins/avatars-gravatar',
- 'plugins/avatars/external',
- 'plugins/avatars/gravatar',
- ];
- assert.deepEqual(suggestions.map(s => s.name), unencodedNames);
- assert.deepEqual(suggestions.map(s => s.value), unencodedNames);
- });
- });
- });
-
- suite('_getRepoBranchesSuggestions', () => {
- setup(() => {
- sandbox.stub(element.$.restAPI, 'getRepoBranches')
- .returns(Promise.resolve([
- {ref: 'refs/heads/stable-2.10'},
- {ref: 'refs/heads/stable-2.11'},
- {ref: 'refs/heads/stable-2.12'},
- {ref: 'refs/heads/stable-2.13'},
- {ref: 'refs/heads/stable-2.14'},
- {ref: 'refs/heads/stable-2.15'},
- ]));
- });
-
- test('converts to suggestion objects', () => {
- const repo = 'gerrit';
- const branchInput = 'stable-2.1';
- element.repo = repo;
- return element._getRepoBranchesSuggestions(branchInput)
- .then(suggestions => {
- assert.isTrue(element.$.restAPI.getRepoBranches.calledWith(
- branchInput, repo, 15));
- const refNames = [
- 'stable-2.10',
- 'stable-2.11',
- 'stable-2.12',
- 'stable-2.13',
- 'stable-2.14',
- 'stable-2.15',
- ];
- assert.deepEqual(suggestions.map(s => s.name), refNames);
- assert.deepEqual(suggestions.map(s => s.value), refNames);
- });
- });
-
- test('filters out ref prefix', () => {
- const repo = 'gerrit';
- const branchInput = 'refs/heads/stable-2.1';
- element.repo = repo;
- return element._getRepoBranchesSuggestions(branchInput)
- .then(suggestions => {
- assert.isTrue(element.$.restAPI.getRepoBranches.calledWith(
- 'stable-2.1', repo, 15));
- });
- });
-
- test('does not query when repo is unset', done => {
- element
- ._getRepoBranchesSuggestions('')
- .then(() => {
- assert.isFalse(element.$.restAPI.getRepoBranches.called);
- element.repo = 'gerrit';
- return element._getRepoBranchesSuggestions('');
- })
- .then(() => {
- assert.isTrue(element.$.restAPI.getRepoBranches.called);
- done();
- });
+ test('converts to suggestion objects', () => {
+ const input = 'plugins/avatars';
+ return element._getRepoSuggestions(input).then(suggestions => {
+ assert.isTrue(element.$.restAPI.getRepos.calledWith(input));
+ const unencodedNames = [
+ 'plugins/avatars-external',
+ 'plugins/avatars-gravatar',
+ 'plugins/avatars/external',
+ 'plugins/avatars/gravatar',
+ ];
+ assert.deepEqual(suggestions.map(s => s.name), unencodedNames);
+ assert.deepEqual(suggestions.map(s => s.value), unencodedNames);
});
});
});
+
+ suite('_getRepoBranchesSuggestions', () => {
+ setup(() => {
+ sandbox.stub(element.$.restAPI, 'getRepoBranches')
+ .returns(Promise.resolve([
+ {ref: 'refs/heads/stable-2.10'},
+ {ref: 'refs/heads/stable-2.11'},
+ {ref: 'refs/heads/stable-2.12'},
+ {ref: 'refs/heads/stable-2.13'},
+ {ref: 'refs/heads/stable-2.14'},
+ {ref: 'refs/heads/stable-2.15'},
+ ]));
+ });
+
+ test('converts to suggestion objects', () => {
+ const repo = 'gerrit';
+ const branchInput = 'stable-2.1';
+ element.repo = repo;
+ return element._getRepoBranchesSuggestions(branchInput)
+ .then(suggestions => {
+ assert.isTrue(element.$.restAPI.getRepoBranches.calledWith(
+ branchInput, repo, 15));
+ const refNames = [
+ 'stable-2.10',
+ 'stable-2.11',
+ 'stable-2.12',
+ 'stable-2.13',
+ 'stable-2.14',
+ 'stable-2.15',
+ ];
+ assert.deepEqual(suggestions.map(s => s.name), refNames);
+ assert.deepEqual(suggestions.map(s => s.value), refNames);
+ });
+ });
+
+ test('filters out ref prefix', () => {
+ const repo = 'gerrit';
+ const branchInput = 'refs/heads/stable-2.1';
+ element.repo = repo;
+ return element._getRepoBranchesSuggestions(branchInput)
+ .then(suggestions => {
+ assert.isTrue(element.$.restAPI.getRepoBranches.calledWith(
+ 'stable-2.1', repo, 15));
+ });
+ });
+
+ test('does not query when repo is unset', done => {
+ element
+ ._getRepoBranchesSuggestions('')
+ .then(() => {
+ assert.isFalse(element.$.restAPI.getRepoBranches.called);
+ element.repo = 'gerrit';
+ return element._getRepoBranchesSuggestions('');
+ })
+ .then(() => {
+ assert.isTrue(element.$.restAPI.getRepoBranches.called);
+ done();
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
index 091c88e..ab79f64 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
@@ -19,377 +19,373 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-auth</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import './gr-auth.js';
+suite('gr-auth', () => {
+ let auth;
+ let sandbox;
-<script src="gr-auth.js"></script>
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ auth = Gerrit.Auth;
+ });
-<script>
- suite('gr-auth', async () => {
- await readyToTest();
- let auth;
- let sandbox;
+ teardown(() => {
+ sandbox.restore();
+ });
+ suite('Auth class methods', () => {
+ let fakeFetch;
setup(() => {
- sandbox = sinon.sandbox.create();
- auth = Gerrit.Auth;
+ auth = new Auth();
+ fakeFetch = sandbox.stub(window, 'fetch');
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('Auth class methods', () => {
- let fakeFetch;
- setup(() => {
- auth = new Auth();
- fakeFetch = sandbox.stub(window, 'fetch');
+ test('auth-check returns 403', done => {
+ fakeFetch.returns(Promise.resolve({status: 403}));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ done();
});
+ });
- test('auth-check returns 403', done => {
- fakeFetch.returns(Promise.resolve({status: 403}));
- auth.authCheck().then(authed => {
+ test('auth-check returns 204', done => {
+ fakeFetch.returns(Promise.resolve({status: 204}));
+ auth.authCheck().then(authed => {
+ assert.isTrue(authed);
+ assert.equal(auth.status, Auth.STATUS.AUTHED);
+ done();
+ });
+ });
+
+ test('auth-check returns 502', done => {
+ fakeFetch.returns(Promise.resolve({status: 502}));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ done();
+ });
+ });
+
+ test('auth-check failed', done => {
+ fakeFetch.returns(Promise.reject(new Error('random error')));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.ERROR);
+ done();
+ });
+ });
+ });
+
+ suite('cache and events behaivor', () => {
+ let fakeFetch;
+ let clock;
+ setup(() => {
+ auth = new Auth();
+ clock = sinon.useFakeTimers();
+ fakeFetch = sandbox.stub(window, 'fetch');
+ });
+
+ test('cache auth-check result', done => {
+ fakeFetch.returns(Promise.resolve({status: 403}));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ fakeFetch.returns(Promise.resolve({status: 204}));
+ auth.authCheck().then(authed2 => {
assert.isFalse(authed);
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
done();
});
});
+ });
- test('auth-check returns 204', done => {
+ test('clearCache should refetch auth-check result', done => {
+ fakeFetch.returns(Promise.resolve({status: 403}));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
fakeFetch.returns(Promise.resolve({status: 204}));
- auth.authCheck().then(authed => {
- assert.isTrue(authed);
+ auth.clearCache();
+ auth.authCheck().then(authed2 => {
+ assert.isTrue(authed2);
assert.equal(auth.status, Auth.STATUS.AUTHED);
done();
});
});
+ });
- test('auth-check returns 502', done => {
- fakeFetch.returns(Promise.resolve({status: 502}));
- auth.authCheck().then(authed => {
- assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ test('cache expired on auth-check after certain time', done => {
+ fakeFetch.returns(Promise.resolve({status: 403}));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ clock.tick(1000 * 10000);
+ fakeFetch.returns(Promise.resolve({status: 204}));
+ auth.authCheck().then(authed2 => {
+ assert.isTrue(authed2);
+ assert.equal(auth.status, Auth.STATUS.AUTHED);
done();
});
});
+ });
- test('auth-check failed', done => {
+ test('no cache if auth-check failed', done => {
+ fakeFetch.returns(Promise.reject(new Error('random error')));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.ERROR);
+ assert.equal(fakeFetch.callCount, 1);
+ auth.authCheck().then(() => {
+ assert.equal(fakeFetch.callCount, 2);
+ done();
+ });
+ });
+ });
+
+ test('fire event when switch from authed to unauthed', done => {
+ fakeFetch.returns(Promise.resolve({status: 204}));
+ auth.authCheck().then(authed => {
+ assert.isTrue(authed);
+ assert.equal(auth.status, Auth.STATUS.AUTHED);
+ clock.tick(1000 * 10000);
+ fakeFetch.returns(Promise.resolve({status: 403}));
+ const emitStub = sinon.stub();
+ Gerrit.emit = emitStub;
+ auth.authCheck().then(authed2 => {
+ assert.isFalse(authed2);
+ assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ assert.isTrue(emitStub.called);
+ done();
+ });
+ });
+ });
+
+ test('fire event when switch from authed to error', done => {
+ fakeFetch.returns(Promise.resolve({status: 204}));
+ auth.authCheck().then(authed => {
+ assert.isTrue(authed);
+ assert.equal(auth.status, Auth.STATUS.AUTHED);
+ clock.tick(1000 * 10000);
fakeFetch.returns(Promise.reject(new Error('random error')));
- auth.authCheck().then(authed => {
- assert.isFalse(authed);
+ const emitStub = sinon.stub();
+ Gerrit.emit = emitStub;
+ auth.authCheck().then(authed2 => {
+ assert.isFalse(authed2);
+ assert.isTrue(emitStub.called);
assert.equal(auth.status, Auth.STATUS.ERROR);
done();
});
});
});
- suite('cache and events behaivor', () => {
- let fakeFetch;
- let clock;
- setup(() => {
- auth = new Auth();
- clock = sinon.useFakeTimers();
- fakeFetch = sandbox.stub(window, 'fetch');
- });
-
- test('cache auth-check result', done => {
- fakeFetch.returns(Promise.resolve({status: 403}));
- auth.authCheck().then(authed => {
- assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
- fakeFetch.returns(Promise.resolve({status: 204}));
- auth.authCheck().then(authed2 => {
- assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
- done();
- });
+ test('no event from non-authed to other status', done => {
+ fakeFetch.returns(Promise.resolve({status: 403}));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ clock.tick(1000 * 10000);
+ fakeFetch.returns(Promise.resolve({status: 204}));
+ const emitStub = sinon.stub();
+ Gerrit.emit = emitStub;
+ auth.authCheck().then(authed2 => {
+ assert.isTrue(authed2);
+ assert.isFalse(emitStub.called);
+ assert.equal(auth.status, Auth.STATUS.AUTHED);
+ done();
});
});
+ });
- test('clearCache should refetch auth-check result', done => {
- fakeFetch.returns(Promise.resolve({status: 403}));
- auth.authCheck().then(authed => {
- assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
- fakeFetch.returns(Promise.resolve({status: 204}));
- auth.clearCache();
- auth.authCheck().then(authed2 => {
- assert.isTrue(authed2);
- assert.equal(auth.status, Auth.STATUS.AUTHED);
- done();
- });
- });
- });
-
- test('cache expired on auth-check after certain time', done => {
- fakeFetch.returns(Promise.resolve({status: 403}));
- auth.authCheck().then(authed => {
- assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
- clock.tick(1000 * 10000);
- fakeFetch.returns(Promise.resolve({status: 204}));
- auth.authCheck().then(authed2 => {
- assert.isTrue(authed2);
- assert.equal(auth.status, Auth.STATUS.AUTHED);
- done();
- });
- });
- });
-
- test('no cache if auth-check failed', done => {
+ test('no event from non-authed to other status', done => {
+ fakeFetch.returns(Promise.resolve({status: 403}));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ clock.tick(1000 * 10000);
fakeFetch.returns(Promise.reject(new Error('random error')));
- auth.authCheck().then(authed => {
- assert.isFalse(authed);
+ const emitStub = sinon.stub();
+ Gerrit.emit = emitStub;
+ auth.authCheck().then(authed2 => {
+ assert.isFalse(authed2);
+ assert.isFalse(emitStub.called);
assert.equal(auth.status, Auth.STATUS.ERROR);
- assert.equal(fakeFetch.callCount, 1);
- auth.authCheck().then(() => {
- assert.equal(fakeFetch.callCount, 2);
- done();
- });
- });
- });
-
- test('fire event when switch from authed to unauthed', done => {
- fakeFetch.returns(Promise.resolve({status: 204}));
- auth.authCheck().then(authed => {
- assert.isTrue(authed);
- assert.equal(auth.status, Auth.STATUS.AUTHED);
- clock.tick(1000 * 10000);
- fakeFetch.returns(Promise.resolve({status: 403}));
- const emitStub = sinon.stub();
- Gerrit.emit = emitStub;
- auth.authCheck().then(authed2 => {
- assert.isFalse(authed2);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
- assert.isTrue(emitStub.called);
- done();
- });
- });
- });
-
- test('fire event when switch from authed to error', done => {
- fakeFetch.returns(Promise.resolve({status: 204}));
- auth.authCheck().then(authed => {
- assert.isTrue(authed);
- assert.equal(auth.status, Auth.STATUS.AUTHED);
- clock.tick(1000 * 10000);
- fakeFetch.returns(Promise.reject(new Error('random error')));
- const emitStub = sinon.stub();
- Gerrit.emit = emitStub;
- auth.authCheck().then(authed2 => {
- assert.isFalse(authed2);
- assert.isTrue(emitStub.called);
- assert.equal(auth.status, Auth.STATUS.ERROR);
- done();
- });
- });
- });
-
- test('no event from non-authed to other status', done => {
- fakeFetch.returns(Promise.resolve({status: 403}));
- auth.authCheck().then(authed => {
- assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
- clock.tick(1000 * 10000);
- fakeFetch.returns(Promise.resolve({status: 204}));
- const emitStub = sinon.stub();
- Gerrit.emit = emitStub;
- auth.authCheck().then(authed2 => {
- assert.isTrue(authed2);
- assert.isFalse(emitStub.called);
- assert.equal(auth.status, Auth.STATUS.AUTHED);
- done();
- });
- });
- });
-
- test('no event from non-authed to other status', done => {
- fakeFetch.returns(Promise.resolve({status: 403}));
- auth.authCheck().then(authed => {
- assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
- clock.tick(1000 * 10000);
- fakeFetch.returns(Promise.reject(new Error('random error')));
- const emitStub = sinon.stub();
- Gerrit.emit = emitStub;
- auth.authCheck().then(authed2 => {
- assert.isFalse(authed2);
- assert.isFalse(emitStub.called);
- assert.equal(auth.status, Auth.STATUS.ERROR);
- done();
- });
- });
- });
- });
-
- suite('default (xsrf token header)', () => {
- setup(() => {
- sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
- });
-
- test('GET', done => {
- auth.fetch('/url', {bar: 'bar'}).then(() => {
- const [url, options] = fetch.lastCall.args;
- assert.equal(url, '/url');
- assert.equal(options.credentials, 'same-origin');
- done();
- });
- });
-
- test('POST', done => {
- sandbox.stub(auth, '_getCookie')
- .withArgs('XSRF_TOKEN')
- .returns('foobar');
- auth.fetch('/url', {method: 'POST'}).then(() => {
- const [url, options] = fetch.lastCall.args;
- assert.equal(url, '/url');
- assert.equal(options.credentials, 'same-origin');
- assert.equal(options.headers.get('X-Gerrit-Auth'), 'foobar');
- done();
- });
- });
- });
-
- suite('cors (access token)', () => {
- setup(() => {
- sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
- });
-
- let getToken;
-
- const makeToken = opt_accessToken => {
- return {
- access_token: opt_accessToken || 'zbaz',
- expires_at: new Date(Date.now() + 10e8).getTime(),
- };
- };
-
- setup(() => {
- getToken = sandbox.stub();
- getToken.returns(Promise.resolve(makeToken()));
- auth.setup(getToken);
- });
-
- test('base url support', done => {
- const baseUrl = 'http://foo';
- sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns(baseUrl);
- auth.fetch(baseUrl + '/url', {bar: 'bar'}).then(() => {
- const [url] = fetch.lastCall.args;
- assert.equal(url, 'http://foo/a/url?access_token=zbaz');
- done();
- });
- });
-
- test('fetch not signed in', done => {
- getToken.returns(Promise.resolve());
- auth.fetch('/url', {bar: 'bar'}).then(() => {
- const [url, options] = fetch.lastCall.args;
- assert.equal(url, '/url');
- assert.equal(options.bar, 'bar');
- assert.equal(Object.keys(options.headers).length, 0);
- done();
- });
- });
-
- test('fetch signed in', done => {
- auth.fetch('/url', {bar: 'bar'}).then(() => {
- const [url, options] = fetch.lastCall.args;
- assert.equal(url, '/a/url?access_token=zbaz');
- assert.equal(options.bar, 'bar');
- done();
- });
- });
-
- test('getToken calls are cached', done => {
- Promise.all([
- auth.fetch('/url-one'), auth.fetch('/url-two')]).then(() => {
- assert.equal(getToken.callCount, 1);
- done();
- });
- });
-
- test('getToken refreshes token', done => {
- sandbox.stub(auth, '_isTokenValid');
- auth._isTokenValid
- .onFirstCall().returns(true)
- .onSecondCall()
- .returns(false)
- .onThirdCall()
- .returns(true);
- auth.fetch('/url-one')
- .then(() => {
- getToken.returns(Promise.resolve(makeToken('bzzbb')));
- return auth.fetch('/url-two');
- })
- .then(() => {
- const [[firstUrl], [secondUrl]] = fetch.args;
- assert.equal(firstUrl, '/a/url-one?access_token=zbaz');
- assert.equal(secondUrl, '/a/url-two?access_token=bzzbb');
- done();
- });
- });
-
- test('signed in token error falls back to anonymous', done => {
- getToken.returns(Promise.resolve('rubbish'));
- auth.fetch('/url', {bar: 'bar'}).then(() => {
- const [url, options] = fetch.lastCall.args;
- assert.equal(url, '/url');
- assert.equal(options.bar, 'bar');
- done();
- });
- });
-
- test('_isTokenValid', () => {
- assert.isFalse(auth._isTokenValid());
- assert.isFalse(auth._isTokenValid({}));
- assert.isFalse(auth._isTokenValid({access_token: 'foo'}));
- assert.isFalse(auth._isTokenValid({
- access_token: 'foo',
- expires_at: Date.now()/1000 - 1,
- }));
- assert.isTrue(auth._isTokenValid({
- access_token: 'foo',
- expires_at: Date.now()/1000 + 1,
- }));
- });
-
- test('HTTP PUT with content type', done => {
- const originalOptions = {
- method: 'PUT',
- headers: new Headers({'Content-Type': 'mail/pigeon'}),
- };
- auth.fetch('/url', originalOptions).then(() => {
- assert.isTrue(getToken.called);
- const [url, options] = fetch.lastCall.args;
- assert.include(url, '$ct=mail%2Fpigeon');
- assert.include(url, '$m=PUT');
- assert.include(url, 'access_token=zbaz');
- assert.equal(options.method, 'POST');
- assert.equal(options.headers.get('Content-Type'), 'text/plain');
- done();
- });
- });
-
- test('HTTP PUT without content type', done => {
- const originalOptions = {
- method: 'PUT',
- };
- auth.fetch('/url', originalOptions).then(() => {
- assert.isTrue(getToken.called);
- const [url, options] = fetch.lastCall.args;
- assert.include(url, '$ct=text%2Fplain');
- assert.include(url, '$m=PUT');
- assert.include(url, 'access_token=zbaz');
- assert.equal(options.method, 'POST');
- assert.equal(options.headers.get('Content-Type'), 'text/plain');
done();
});
});
});
});
+
+ suite('default (xsrf token header)', () => {
+ setup(() => {
+ sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
+ });
+
+ test('GET', done => {
+ auth.fetch('/url', {bar: 'bar'}).then(() => {
+ const [url, options] = fetch.lastCall.args;
+ assert.equal(url, '/url');
+ assert.equal(options.credentials, 'same-origin');
+ done();
+ });
+ });
+
+ test('POST', done => {
+ sandbox.stub(auth, '_getCookie')
+ .withArgs('XSRF_TOKEN')
+ .returns('foobar');
+ auth.fetch('/url', {method: 'POST'}).then(() => {
+ const [url, options] = fetch.lastCall.args;
+ assert.equal(url, '/url');
+ assert.equal(options.credentials, 'same-origin');
+ assert.equal(options.headers.get('X-Gerrit-Auth'), 'foobar');
+ done();
+ });
+ });
+ });
+
+ suite('cors (access token)', () => {
+ setup(() => {
+ sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
+ });
+
+ let getToken;
+
+ const makeToken = opt_accessToken => {
+ return {
+ access_token: opt_accessToken || 'zbaz',
+ expires_at: new Date(Date.now() + 10e8).getTime(),
+ };
+ };
+
+ setup(() => {
+ getToken = sandbox.stub();
+ getToken.returns(Promise.resolve(makeToken()));
+ auth.setup(getToken);
+ });
+
+ test('base url support', done => {
+ const baseUrl = 'http://foo';
+ sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns(baseUrl);
+ auth.fetch(baseUrl + '/url', {bar: 'bar'}).then(() => {
+ const [url] = fetch.lastCall.args;
+ assert.equal(url, 'http://foo/a/url?access_token=zbaz');
+ done();
+ });
+ });
+
+ test('fetch not signed in', done => {
+ getToken.returns(Promise.resolve());
+ auth.fetch('/url', {bar: 'bar'}).then(() => {
+ const [url, options] = fetch.lastCall.args;
+ assert.equal(url, '/url');
+ assert.equal(options.bar, 'bar');
+ assert.equal(Object.keys(options.headers).length, 0);
+ done();
+ });
+ });
+
+ test('fetch signed in', done => {
+ auth.fetch('/url', {bar: 'bar'}).then(() => {
+ const [url, options] = fetch.lastCall.args;
+ assert.equal(url, '/a/url?access_token=zbaz');
+ assert.equal(options.bar, 'bar');
+ done();
+ });
+ });
+
+ test('getToken calls are cached', done => {
+ Promise.all([
+ auth.fetch('/url-one'), auth.fetch('/url-two')]).then(() => {
+ assert.equal(getToken.callCount, 1);
+ done();
+ });
+ });
+
+ test('getToken refreshes token', done => {
+ sandbox.stub(auth, '_isTokenValid');
+ auth._isTokenValid
+ .onFirstCall().returns(true)
+ .onSecondCall()
+ .returns(false)
+ .onThirdCall()
+ .returns(true);
+ auth.fetch('/url-one')
+ .then(() => {
+ getToken.returns(Promise.resolve(makeToken('bzzbb')));
+ return auth.fetch('/url-two');
+ })
+ .then(() => {
+ const [[firstUrl], [secondUrl]] = fetch.args;
+ assert.equal(firstUrl, '/a/url-one?access_token=zbaz');
+ assert.equal(secondUrl, '/a/url-two?access_token=bzzbb');
+ done();
+ });
+ });
+
+ test('signed in token error falls back to anonymous', done => {
+ getToken.returns(Promise.resolve('rubbish'));
+ auth.fetch('/url', {bar: 'bar'}).then(() => {
+ const [url, options] = fetch.lastCall.args;
+ assert.equal(url, '/url');
+ assert.equal(options.bar, 'bar');
+ done();
+ });
+ });
+
+ test('_isTokenValid', () => {
+ assert.isFalse(auth._isTokenValid());
+ assert.isFalse(auth._isTokenValid({}));
+ assert.isFalse(auth._isTokenValid({access_token: 'foo'}));
+ assert.isFalse(auth._isTokenValid({
+ access_token: 'foo',
+ expires_at: Date.now()/1000 - 1,
+ }));
+ assert.isTrue(auth._isTokenValid({
+ access_token: 'foo',
+ expires_at: Date.now()/1000 + 1,
+ }));
+ });
+
+ test('HTTP PUT with content type', done => {
+ const originalOptions = {
+ method: 'PUT',
+ headers: new Headers({'Content-Type': 'mail/pigeon'}),
+ };
+ auth.fetch('/url', originalOptions).then(() => {
+ assert.isTrue(getToken.called);
+ const [url, options] = fetch.lastCall.args;
+ assert.include(url, '$ct=mail%2Fpigeon');
+ assert.include(url, '$m=PUT');
+ assert.include(url, 'access_token=zbaz');
+ assert.equal(options.method, 'POST');
+ assert.equal(options.headers.get('Content-Type'), 'text/plain');
+ done();
+ });
+ });
+
+ test('HTTP PUT without content type', done => {
+ const originalOptions = {
+ method: 'PUT',
+ };
+ auth.fetch('/url', originalOptions).then(() => {
+ assert.isTrue(getToken.called);
+ const [url, options] = fetch.lastCall.args;
+ assert.include(url, '$ct=text%2Fplain');
+ assert.include(url, '$m=PUT');
+ assert.include(url, 'access_token=zbaz');
+ assert.equal(options.method, 'POST');
+ assert.equal(options.headers.get('Content-Type'), 'text/plain');
+ done();
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.html
deleted file mode 100644
index d3500d8..0000000
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<dom-module id="gr-etag-decorator">
- <script src="gr-etag-decorator.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
index 7022d23..33c8d8e 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
@@ -14,6 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-etag-decorator">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
index 93b6a21..482dc6a 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
@@ -19,83 +19,79 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-etag-decorator</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-etag-decorator.js';
+suite('gr-etag-decorator', () => {
+ let etag;
+ let sandbox;
-<script src="gr-etag-decorator.js"></script>
+ const fakeRequest = (opt_etag, opt_status) => {
+ const headers = new Headers();
+ if (opt_etag) {
+ headers.set('etag', opt_etag);
+ }
+ const status = opt_status || 200;
+ return {ok: true, status, headers};
+ };
-<script>
- suite('gr-etag-decorator', async () => {
- await readyToTest();
- let etag;
- let sandbox;
-
- const fakeRequest = (opt_etag, opt_status) => {
- const headers = new Headers();
- if (opt_etag) {
- headers.set('etag', opt_etag);
- }
- const status = opt_status || 200;
- return {ok: true, status, headers};
- };
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- etag = new GrEtagDecorator();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('exists', () => {
- assert.isOk(etag);
- });
-
- test('works', () => {
- etag.collect('/foo', fakeRequest('bar'));
- const options = etag.getOptions('/foo');
- assert.strictEqual(options.headers.get('If-None-Match'), 'bar');
- });
-
- test('updates etags', () => {
- etag.collect('/foo', fakeRequest('bar'));
- etag.collect('/foo', fakeRequest('baz'));
- const options = etag.getOptions('/foo');
- assert.strictEqual(options.headers.get('If-None-Match'), 'baz');
- });
-
- test('discards empty etags', () => {
- etag.collect('/foo', fakeRequest('bar'));
- etag.collect('/foo', fakeRequest());
- const options = etag.getOptions('/foo', {headers: new Headers()});
- assert.isNull(options.headers.get('If-None-Match'));
- });
-
- test('discards etags in order used', () => {
- etag.collect('/foo', fakeRequest('bar'));
- _.times(29, i => {
- etag.collect('/qaz/' + i, fakeRequest('qaz'));
- });
- let options = etag.getOptions('/foo');
- assert.strictEqual(options.headers.get('If-None-Match'), 'bar');
- etag.collect('/zaq', fakeRequest('zaq'));
- options = etag.getOptions('/foo', {headers: new Headers()});
- assert.isNull(options.headers.get('If-None-Match'));
- });
-
- test('getCachedPayload', () => {
- const payload = 'payload';
- etag.collect('/foo', fakeRequest('bar'), payload);
- assert.strictEqual(etag.getCachedPayload('/foo'), payload);
- etag.collect('/foo', fakeRequest('bar', 304), 'garbage');
- assert.strictEqual(etag.getCachedPayload('/foo'), payload);
- etag.collect('/foo', fakeRequest('bar', 200), 'new payload');
- assert.strictEqual(etag.getCachedPayload('/foo'), 'new payload');
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ etag = new GrEtagDecorator();
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('exists', () => {
+ assert.isOk(etag);
+ });
+
+ test('works', () => {
+ etag.collect('/foo', fakeRequest('bar'));
+ const options = etag.getOptions('/foo');
+ assert.strictEqual(options.headers.get('If-None-Match'), 'bar');
+ });
+
+ test('updates etags', () => {
+ etag.collect('/foo', fakeRequest('bar'));
+ etag.collect('/foo', fakeRequest('baz'));
+ const options = etag.getOptions('/foo');
+ assert.strictEqual(options.headers.get('If-None-Match'), 'baz');
+ });
+
+ test('discards empty etags', () => {
+ etag.collect('/foo', fakeRequest('bar'));
+ etag.collect('/foo', fakeRequest());
+ const options = etag.getOptions('/foo', {headers: new Headers()});
+ assert.isNull(options.headers.get('If-None-Match'));
+ });
+
+ test('discards etags in order used', () => {
+ etag.collect('/foo', fakeRequest('bar'));
+ _.times(29, i => {
+ etag.collect('/qaz/' + i, fakeRequest('qaz'));
+ });
+ let options = etag.getOptions('/foo');
+ assert.strictEqual(options.headers.get('If-None-Match'), 'bar');
+ etag.collect('/zaq', fakeRequest('zaq'));
+ options = etag.getOptions('/foo', {headers: new Headers()});
+ assert.isNull(options.headers.get('If-None-Match'));
+ });
+
+ test('getCachedPayload', () => {
+ const payload = 'payload';
+ etag.collect('/foo', fakeRequest('bar'), payload);
+ assert.strictEqual(etag.getCachedPayload('/foo'), payload);
+ etag.collect('/foo', fakeRequest('bar', 304), 'garbage');
+ assert.strictEqual(etag.getCachedPayload('/foo'), payload);
+ etag.collect('/foo', fakeRequest('bar', 200), 'new payload');
+ assert.strictEqual(etag.getCachedPayload('/foo'), 'new payload');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
deleted file mode 100644
index 7461ac4..0000000
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
+++ /dev/null
@@ -1,36 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="gr-etag-decorator.html">
-
-<!-- NB: es6-promise Needed for IE11 and fetch polyfill support, see Issue 4308 -->
-<script src="/bower_components/es6-promise/dist/es6-promise.min.js"></script>
-<script src="/bower_components/fetch/fetch.js"></script>
-
-<dom-module id="gr-rest-api-interface">
- <!-- NB: Order is important, because of namespaced classes. -->
- <script src="gr-rest-apis/gr-rest-api-helper.js"></script>
- <script src="gr-auth.js"></script>
- <script src="gr-reviewer-updates-parser.js"></script>
- <script src="gr-rest-api-interface.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 5b88d34..849be00 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -14,2756 +14,2786 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+/* NB: es6-promise Needed for IE11 and fetch polyfill support, see Issue 4308 */
+/* NB: Order is important, because of namespaced classes. */
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+import '../../../scripts/bundled-polymer.js';
- const DiffViewMode = {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- };
- const JSON_PREFIX = ')]}\'';
- const MAX_PROJECT_RESULTS = 25;
- // This value is somewhat arbitrary and not based on research or calculations.
- const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 850;
- const PARENT_PATCH_NUM = 'PARENT';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import './gr-etag-decorator.js';
+import './gr-rest-apis/gr-rest-api-helper.js';
+import './gr-auth.js';
+import './gr-reviewer-updates-parser.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import 'es6-promise/lib/es6-promise.js';
+import 'whatwg-fetch/fetch.js';
- const Requests = {
- SEND_DIFF_DRAFT: 'sendDiffDraft',
- };
+const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+};
+const JSON_PREFIX = ')]}\'';
+const MAX_PROJECT_RESULTS = 25;
+// This value is somewhat arbitrary and not based on research or calculations.
+const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 850;
+const PARENT_PATCH_NUM = 'PARENT';
- const CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE =
- 'Saving draft resulted in HTTP 200 (OK) but expected HTTP 201 (Created)';
- const HEADER_REPORTING_BLACKLIST = /^set-cookie$/i;
+const Requests = {
+ SEND_DIFF_DRAFT: 'sendDiffDraft',
+};
- const ANONYMIZED_CHANGE_BASE_URL = '/changes/*~*';
- const ANONYMIZED_REVISION_BASE_URL = ANONYMIZED_CHANGE_BASE_URL +
- '/revisions/*';
+const CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE =
+ 'Saving draft resulted in HTTP 200 (OK) but expected HTTP 201 (Created)';
+const HEADER_REPORTING_BLACKLIST = /^set-cookie$/i;
+
+const ANONYMIZED_CHANGE_BASE_URL = '/changes/*~*';
+const ANONYMIZED_REVISION_BASE_URL = ANONYMIZED_CHANGE_BASE_URL +
+ '/revisions/*';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.PathListMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrRestApiInterface extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.PathListBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get is() { return 'gr-rest-api-interface'; }
+ /**
+ * Fired when an server error occurs.
+ *
+ * @event server-error
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.PathListMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * Fired when a network error occurs.
+ *
+ * @event network-error
*/
- class GrRestApiInterface extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.PathListBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-rest-api-interface'; }
- /**
- * Fired when an server error occurs.
- *
- * @event server-error
- */
- /**
- * Fired when a network error occurs.
- *
- * @event network-error
- */
+ /**
+ * Fired after an RPC completes.
+ *
+ * @event rpc-log
+ */
- /**
- * Fired after an RPC completes.
- *
- * @event rpc-log
- */
+ constructor() {
+ super();
+ this.JSON_PREFIX = JSON_PREFIX;
+ }
- constructor() {
- super();
- this.JSON_PREFIX = JSON_PREFIX;
+ static get properties() {
+ return {
+ _cache: {
+ type: Object,
+ value: new SiteBasedCache(), // Shared across instances.
+ },
+ _sharedFetchPromises: {
+ type: Object,
+ value: new FetchPromisesCache(), // Shared across instances.
+ },
+ _pendingRequests: {
+ type: Object,
+ value: {}, // Intentional to share the object across instances.
+ },
+ _etags: {
+ type: Object,
+ value: new GrEtagDecorator(), // Share across instances.
+ },
+ /**
+ * Used to maintain a mapping of changeNums to project names.
+ */
+ _projectLookup: {
+ type: Object,
+ value: {}, // Intentional to share the object across instances.
+ },
+ };
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this._auth = Gerrit.Auth;
+ this._initRestApiHelper();
+ }
+
+ _initRestApiHelper() {
+ if (this._restApiHelper) {
+ return;
}
-
- static get properties() {
- return {
- _cache: {
- type: Object,
- value: new SiteBasedCache(), // Shared across instances.
- },
- _sharedFetchPromises: {
- type: Object,
- value: new FetchPromisesCache(), // Shared across instances.
- },
- _pendingRequests: {
- type: Object,
- value: {}, // Intentional to share the object across instances.
- },
- _etags: {
- type: Object,
- value: new GrEtagDecorator(), // Share across instances.
- },
- /**
- * Used to maintain a mapping of changeNums to project names.
- */
- _projectLookup: {
- type: Object,
- value: {}, // Intentional to share the object across instances.
- },
- };
+ if (this._cache && this._auth && this._sharedFetchPromises) {
+ this._restApiHelper = new GrRestApiHelper(this._cache, this._auth,
+ this._sharedFetchPromises, this);
}
+ }
- /** @override */
- created() {
- super.created();
- this._auth = Gerrit.Auth;
- this._initRestApiHelper();
- }
+ _fetchSharedCacheURL(req) {
+ // Cache is shared across instances
+ return this._restApiHelper.fetchCacheURL(req);
+ }
- _initRestApiHelper() {
- if (this._restApiHelper) {
- return;
- }
- if (this._cache && this._auth && this._sharedFetchPromises) {
- this._restApiHelper = new GrRestApiHelper(this._cache, this._auth,
- this._sharedFetchPromises, this);
- }
- }
+ /**
+ * @param {!Object} response
+ * @return {?}
+ */
+ getResponseObject(response) {
+ return this._restApiHelper.getResponseObject(response);
+ }
- _fetchSharedCacheURL(req) {
- // Cache is shared across instances
- return this._restApiHelper.fetchCacheURL(req);
- }
-
- /**
- * @param {!Object} response
- * @return {?}
- */
- getResponseObject(response) {
- return this._restApiHelper.getResponseObject(response);
- }
-
- getConfig(noCache) {
- if (!noCache) {
- return this._fetchSharedCacheURL({
- url: '/config/server/info',
- reportUrlAsIs: true,
- });
- }
-
- return this._restApiHelper.fetchJSON({
+ getConfig(noCache) {
+ if (!noCache) {
+ return this._fetchSharedCacheURL({
url: '/config/server/info',
reportUrlAsIs: true,
});
}
- getRepo(repo, opt_errFn) {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._fetchSharedCacheURL({
- url: '/projects/' + encodeURIComponent(repo),
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*',
- });
- }
+ return this._restApiHelper.fetchJSON({
+ url: '/config/server/info',
+ reportUrlAsIs: true,
+ });
+ }
- getProjectConfig(repo, opt_errFn) {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._fetchSharedCacheURL({
- url: '/projects/' + encodeURIComponent(repo) + '/config',
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/config',
- });
- }
+ getRepo(repo, opt_errFn) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url: '/projects/' + encodeURIComponent(repo),
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*',
+ });
+ }
- getRepoAccess(repo) {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._fetchSharedCacheURL({
- url: '/access/?project=' + encodeURIComponent(repo),
- anonymizedUrl: '/access/?project=*',
- });
- }
+ getProjectConfig(repo, opt_errFn) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url: '/projects/' + encodeURIComponent(repo) + '/config',
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/config',
+ });
+ }
- getRepoDashboards(repo, opt_errFn) {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._fetchSharedCacheURL({
- url: `/projects/${encodeURIComponent(repo)}/dashboards?inherited`,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/dashboards?inherited',
- });
- }
+ getRepoAccess(repo) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url: '/access/?project=' + encodeURIComponent(repo),
+ anonymizedUrl: '/access/?project=*',
+ });
+ }
- saveRepoConfig(repo, config, opt_errFn) {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- const url = `/projects/${encodeURIComponent(repo)}/config`;
- this._cache.delete(url);
- return this._restApiHelper.send({
- method: 'PUT',
- url,
- body: config,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/config',
- });
- }
+ getRepoDashboards(repo, opt_errFn) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url: `/projects/${encodeURIComponent(repo)}/dashboards?inherited`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/dashboards?inherited',
+ });
+ }
- runRepoGC(repo, opt_errFn) {
- if (!repo) { return ''; }
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- const encodeName = encodeURIComponent(repo);
- return this._restApiHelper.send({
- method: 'POST',
- url: `/projects/${encodeName}/gc`,
- body: '',
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/gc',
- });
- }
+ saveRepoConfig(repo, config, opt_errFn) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const url = `/projects/${encodeURIComponent(repo)}/config`;
+ this._cache.delete(url);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url,
+ body: config,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/config',
+ });
+ }
- /**
- * @param {?Object} config
- * @param {function(?Response, string=)=} opt_errFn
- */
- createRepo(config, opt_errFn) {
- if (!config.name) { return ''; }
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- const encodeName = encodeURIComponent(config.name);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/projects/${encodeName}`,
- body: config,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*',
- });
- }
+ runRepoGC(repo, opt_errFn) {
+ if (!repo) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const encodeName = encodeURIComponent(repo);
+ return this._restApiHelper.send({
+ method: 'POST',
+ url: `/projects/${encodeName}/gc`,
+ body: '',
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/gc',
+ });
+ }
- /**
- * @param {?Object} config
- * @param {function(?Response, string=)=} opt_errFn
- */
- createGroup(config, opt_errFn) {
- if (!config.name) { return ''; }
- const encodeName = encodeURIComponent(config.name);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/groups/${encodeName}`,
- body: config,
- errFn: opt_errFn,
- anonymizedUrl: '/groups/*',
- });
- }
+ /**
+ * @param {?Object} config
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ createRepo(config, opt_errFn) {
+ if (!config.name) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const encodeName = encodeURIComponent(config.name);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/projects/${encodeName}`,
+ body: config,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*',
+ });
+ }
- getGroupConfig(group, opt_errFn) {
- return this._restApiHelper.fetchJSON({
- url: `/groups/${encodeURIComponent(group)}/detail`,
- errFn: opt_errFn,
- anonymizedUrl: '/groups/*/detail',
- });
- }
+ /**
+ * @param {?Object} config
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ createGroup(config, opt_errFn) {
+ if (!config.name) { return ''; }
+ const encodeName = encodeURIComponent(config.name);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/groups/${encodeName}`,
+ body: config,
+ errFn: opt_errFn,
+ anonymizedUrl: '/groups/*',
+ });
+ }
- /**
- * @param {string} repo
- * @param {string} ref
- * @param {function(?Response, string=)=} opt_errFn
- */
- deleteRepoBranches(repo, ref, opt_errFn) {
- if (!repo || !ref) { return ''; }
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- const encodeName = encodeURIComponent(repo);
- const encodeRef = encodeURIComponent(ref);
- return this._restApiHelper.send({
- method: 'DELETE',
- url: `/projects/${encodeName}/branches/${encodeRef}`,
- body: '',
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/branches/*',
- });
- }
+ getGroupConfig(group, opt_errFn) {
+ return this._restApiHelper.fetchJSON({
+ url: `/groups/${encodeURIComponent(group)}/detail`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/groups/*/detail',
+ });
+ }
- /**
- * @param {string} repo
- * @param {string} ref
- * @param {function(?Response, string=)=} opt_errFn
- */
- deleteRepoTags(repo, ref, opt_errFn) {
- if (!repo || !ref) { return ''; }
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- const encodeName = encodeURIComponent(repo);
- const encodeRef = encodeURIComponent(ref);
- return this._restApiHelper.send({
- method: 'DELETE',
- url: `/projects/${encodeName}/tags/${encodeRef}`,
- body: '',
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/tags/*',
- });
- }
+ /**
+ * @param {string} repo
+ * @param {string} ref
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ deleteRepoBranches(repo, ref, opt_errFn) {
+ if (!repo || !ref) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const encodeName = encodeURIComponent(repo);
+ const encodeRef = encodeURIComponent(ref);
+ return this._restApiHelper.send({
+ method: 'DELETE',
+ url: `/projects/${encodeName}/branches/${encodeRef}`,
+ body: '',
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/branches/*',
+ });
+ }
- /**
- * @param {string} name
- * @param {string} branch
- * @param {string} revision
- * @param {function(?Response, string=)=} opt_errFn
- */
- createRepoBranch(name, branch, revision, opt_errFn) {
- if (!name || !branch || !revision) { return ''; }
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- const encodeName = encodeURIComponent(name);
- const encodeBranch = encodeURIComponent(branch);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/projects/${encodeName}/branches/${encodeBranch}`,
- body: revision,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/branches/*',
- });
- }
+ /**
+ * @param {string} repo
+ * @param {string} ref
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ deleteRepoTags(repo, ref, opt_errFn) {
+ if (!repo || !ref) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const encodeName = encodeURIComponent(repo);
+ const encodeRef = encodeURIComponent(ref);
+ return this._restApiHelper.send({
+ method: 'DELETE',
+ url: `/projects/${encodeName}/tags/${encodeRef}`,
+ body: '',
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/tags/*',
+ });
+ }
- /**
- * @param {string} name
- * @param {string} tag
- * @param {string} revision
- * @param {function(?Response, string=)=} opt_errFn
- */
- createRepoTag(name, tag, revision, opt_errFn) {
- if (!name || !tag || !revision) { return ''; }
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- const encodeName = encodeURIComponent(name);
- const encodeTag = encodeURIComponent(tag);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/projects/${encodeName}/tags/${encodeTag}`,
- body: revision,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/tags/*',
- });
- }
+ /**
+ * @param {string} name
+ * @param {string} branch
+ * @param {string} revision
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ createRepoBranch(name, branch, revision, opt_errFn) {
+ if (!name || !branch || !revision) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const encodeName = encodeURIComponent(name);
+ const encodeBranch = encodeURIComponent(branch);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/projects/${encodeName}/branches/${encodeBranch}`,
+ body: revision,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/branches/*',
+ });
+ }
- /**
- * @param {!string} groupName
- * @returns {!Promise<boolean>}
- */
- getIsGroupOwner(groupName) {
- const encodeName = encodeURIComponent(groupName);
- const req = {
- url: `/groups/?owned&g=${encodeName}`,
- anonymizedUrl: '/groups/owned&g=*',
- };
- return this._fetchSharedCacheURL(req)
- .then(configs => configs.hasOwnProperty(groupName));
- }
+ /**
+ * @param {string} name
+ * @param {string} tag
+ * @param {string} revision
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ createRepoTag(name, tag, revision, opt_errFn) {
+ if (!name || !tag || !revision) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const encodeName = encodeURIComponent(name);
+ const encodeTag = encodeURIComponent(tag);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/projects/${encodeName}/tags/${encodeTag}`,
+ body: revision,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/tags/*',
+ });
+ }
- getGroupMembers(groupName, opt_errFn) {
- const encodeName = encodeURIComponent(groupName);
- return this._restApiHelper.fetchJSON({
- url: `/groups/${encodeName}/members/`,
- errFn: opt_errFn,
- anonymizedUrl: '/groups/*/members',
- });
- }
+ /**
+ * @param {!string} groupName
+ * @returns {!Promise<boolean>}
+ */
+ getIsGroupOwner(groupName) {
+ const encodeName = encodeURIComponent(groupName);
+ const req = {
+ url: `/groups/?owned&g=${encodeName}`,
+ anonymizedUrl: '/groups/owned&g=*',
+ };
+ return this._fetchSharedCacheURL(req)
+ .then(configs => configs.hasOwnProperty(groupName));
+ }
- getIncludedGroup(groupName) {
- return this._restApiHelper.fetchJSON({
- url: `/groups/${encodeURIComponent(groupName)}/groups/`,
- anonymizedUrl: '/groups/*/groups',
- });
- }
+ getGroupMembers(groupName, opt_errFn) {
+ const encodeName = encodeURIComponent(groupName);
+ return this._restApiHelper.fetchJSON({
+ url: `/groups/${encodeName}/members/`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/groups/*/members',
+ });
+ }
- saveGroupName(groupId, name) {
- const encodeId = encodeURIComponent(groupId);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/groups/${encodeId}/name`,
- body: {name},
- anonymizedUrl: '/groups/*/name',
- });
- }
+ getIncludedGroup(groupName) {
+ return this._restApiHelper.fetchJSON({
+ url: `/groups/${encodeURIComponent(groupName)}/groups/`,
+ anonymizedUrl: '/groups/*/groups',
+ });
+ }
- saveGroupOwner(groupId, ownerId) {
- const encodeId = encodeURIComponent(groupId);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/groups/${encodeId}/owner`,
- body: {owner: ownerId},
- anonymizedUrl: '/groups/*/owner',
- });
- }
+ saveGroupName(groupId, name) {
+ const encodeId = encodeURIComponent(groupId);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/groups/${encodeId}/name`,
+ body: {name},
+ anonymizedUrl: '/groups/*/name',
+ });
+ }
- saveGroupDescription(groupId, description) {
- const encodeId = encodeURIComponent(groupId);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/groups/${encodeId}/description`,
- body: {description},
- anonymizedUrl: '/groups/*/description',
- });
- }
+ saveGroupOwner(groupId, ownerId) {
+ const encodeId = encodeURIComponent(groupId);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/groups/${encodeId}/owner`,
+ body: {owner: ownerId},
+ anonymizedUrl: '/groups/*/owner',
+ });
+ }
- saveGroupOptions(groupId, options) {
- const encodeId = encodeURIComponent(groupId);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/groups/${encodeId}/options`,
- body: options,
- anonymizedUrl: '/groups/*/options',
- });
- }
+ saveGroupDescription(groupId, description) {
+ const encodeId = encodeURIComponent(groupId);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/groups/${encodeId}/description`,
+ body: {description},
+ anonymizedUrl: '/groups/*/description',
+ });
+ }
- getGroupAuditLog(group, opt_errFn) {
- return this._fetchSharedCacheURL({
- url: '/groups/' + group + '/log.audit',
- errFn: opt_errFn,
- anonymizedUrl: '/groups/*/log.audit',
- });
- }
+ saveGroupOptions(groupId, options) {
+ const encodeId = encodeURIComponent(groupId);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/groups/${encodeId}/options`,
+ body: options,
+ anonymizedUrl: '/groups/*/options',
+ });
+ }
- saveGroupMembers(groupName, groupMembers) {
- const encodeName = encodeURIComponent(groupName);
- const encodeMember = encodeURIComponent(groupMembers);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/groups/${encodeName}/members/${encodeMember}`,
- parseResponse: true,
- anonymizedUrl: '/groups/*/members/*',
- });
- }
+ getGroupAuditLog(group, opt_errFn) {
+ return this._fetchSharedCacheURL({
+ url: '/groups/' + group + '/log.audit',
+ errFn: opt_errFn,
+ anonymizedUrl: '/groups/*/log.audit',
+ });
+ }
- saveIncludedGroup(groupName, includedGroup, opt_errFn) {
- const encodeName = encodeURIComponent(groupName);
- const encodeIncludedGroup = encodeURIComponent(includedGroup);
- const req = {
- method: 'PUT',
- url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
- errFn: opt_errFn,
- anonymizedUrl: '/groups/*/groups/*',
- };
- return this._restApiHelper.send(req).then(response => {
- if (response.ok) {
- return this.getResponseObject(response);
- }
- });
- }
+ saveGroupMembers(groupName, groupMembers) {
+ const encodeName = encodeURIComponent(groupName);
+ const encodeMember = encodeURIComponent(groupMembers);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/groups/${encodeName}/members/${encodeMember}`,
+ parseResponse: true,
+ anonymizedUrl: '/groups/*/members/*',
+ });
+ }
- deleteGroupMembers(groupName, groupMembers) {
- const encodeName = encodeURIComponent(groupName);
- const encodeMember = encodeURIComponent(groupMembers);
- return this._restApiHelper.send({
- method: 'DELETE',
- url: `/groups/${encodeName}/members/${encodeMember}`,
- anonymizedUrl: '/groups/*/members/*',
- });
- }
-
- deleteIncludedGroup(groupName, includedGroup) {
- const encodeName = encodeURIComponent(groupName);
- const encodeIncludedGroup = encodeURIComponent(includedGroup);
- return this._restApiHelper.send({
- method: 'DELETE',
- url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
- anonymizedUrl: '/groups/*/groups/*',
- });
- }
-
- getVersion() {
- return this._fetchSharedCacheURL({
- url: '/config/server/version',
- reportUrlAsIs: true,
- });
- }
-
- getDiffPreferences() {
- return this.getLoggedIn().then(loggedIn => {
- if (loggedIn) {
- return this._fetchSharedCacheURL({
- url: '/accounts/self/preferences.diff',
- reportUrlAsIs: true,
- });
- }
- // These defaults should match the defaults in
- // java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
- // NOTE: There are some settings that don't apply to PolyGerrit
- // (Render mode being at least one of them).
- return Promise.resolve({
- auto_hide_diff_table_header: true,
- context: 10,
- cursor_blink_rate: 0,
- font_size: 12,
- ignore_whitespace: 'IGNORE_NONE',
- intraline_difference: true,
- line_length: 100,
- line_wrapping: false,
- show_line_endings: true,
- show_tabs: true,
- show_whitespace_errors: true,
- syntax_highlighting: true,
- tab_size: 8,
- theme: 'DEFAULT',
- });
- });
- }
-
- getEditPreferences() {
- return this.getLoggedIn().then(loggedIn => {
- if (loggedIn) {
- return this._fetchSharedCacheURL({
- url: '/accounts/self/preferences.edit',
- reportUrlAsIs: true,
- });
- }
- // These defaults should match the defaults in
- // java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
- return Promise.resolve({
- auto_close_brackets: false,
- cursor_blink_rate: 0,
- hide_line_numbers: false,
- hide_top_menu: false,
- indent_unit: 2,
- indent_with_tabs: false,
- key_map_type: 'DEFAULT',
- line_length: 100,
- line_wrapping: false,
- match_brackets: true,
- show_base: false,
- show_tabs: true,
- show_whitespace_errors: true,
- syntax_highlighting: true,
- tab_size: 8,
- theme: 'DEFAULT',
- });
- });
- }
-
- /**
- * @param {?Object} prefs
- * @param {function(?Response, string=)=} opt_errFn
- */
- savePreferences(prefs, opt_errFn) {
- // Note (Issue 5142): normalize the download scheme with lower case before
- // saving.
- if (prefs.download_scheme) {
- prefs.download_scheme = prefs.download_scheme.toLowerCase();
+ saveIncludedGroup(groupName, includedGroup, opt_errFn) {
+ const encodeName = encodeURIComponent(groupName);
+ const encodeIncludedGroup = encodeURIComponent(includedGroup);
+ const req = {
+ method: 'PUT',
+ url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/groups/*/groups/*',
+ };
+ return this._restApiHelper.send(req).then(response => {
+ if (response.ok) {
+ return this.getResponseObject(response);
}
+ });
+ }
- return this._restApiHelper.send({
- method: 'PUT',
- url: '/accounts/self/preferences',
- body: prefs,
- errFn: opt_errFn,
- reportUrlAsIs: true,
+ deleteGroupMembers(groupName, groupMembers) {
+ const encodeName = encodeURIComponent(groupName);
+ const encodeMember = encodeURIComponent(groupMembers);
+ return this._restApiHelper.send({
+ method: 'DELETE',
+ url: `/groups/${encodeName}/members/${encodeMember}`,
+ anonymizedUrl: '/groups/*/members/*',
+ });
+ }
+
+ deleteIncludedGroup(groupName, includedGroup) {
+ const encodeName = encodeURIComponent(groupName);
+ const encodeIncludedGroup = encodeURIComponent(includedGroup);
+ return this._restApiHelper.send({
+ method: 'DELETE',
+ url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
+ anonymizedUrl: '/groups/*/groups/*',
+ });
+ }
+
+ getVersion() {
+ return this._fetchSharedCacheURL({
+ url: '/config/server/version',
+ reportUrlAsIs: true,
+ });
+ }
+
+ getDiffPreferences() {
+ return this.getLoggedIn().then(loggedIn => {
+ if (loggedIn) {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/preferences.diff',
+ reportUrlAsIs: true,
+ });
+ }
+ // These defaults should match the defaults in
+ // java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
+ // NOTE: There are some settings that don't apply to PolyGerrit
+ // (Render mode being at least one of them).
+ return Promise.resolve({
+ auto_hide_diff_table_header: true,
+ context: 10,
+ cursor_blink_rate: 0,
+ font_size: 12,
+ ignore_whitespace: 'IGNORE_NONE',
+ intraline_difference: true,
+ line_length: 100,
+ line_wrapping: false,
+ show_line_endings: true,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ tab_size: 8,
+ theme: 'DEFAULT',
});
+ });
+ }
+
+ getEditPreferences() {
+ return this.getLoggedIn().then(loggedIn => {
+ if (loggedIn) {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/preferences.edit',
+ reportUrlAsIs: true,
+ });
+ }
+ // These defaults should match the defaults in
+ // java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
+ return Promise.resolve({
+ auto_close_brackets: false,
+ cursor_blink_rate: 0,
+ hide_line_numbers: false,
+ hide_top_menu: false,
+ indent_unit: 2,
+ indent_with_tabs: false,
+ key_map_type: 'DEFAULT',
+ line_length: 100,
+ line_wrapping: false,
+ match_brackets: true,
+ show_base: false,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ tab_size: 8,
+ theme: 'DEFAULT',
+ });
+ });
+ }
+
+ /**
+ * @param {?Object} prefs
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ savePreferences(prefs, opt_errFn) {
+ // Note (Issue 5142): normalize the download scheme with lower case before
+ // saving.
+ if (prefs.download_scheme) {
+ prefs.download_scheme = prefs.download_scheme.toLowerCase();
}
- /**
- * @param {?Object} prefs
- * @param {function(?Response, string=)=} opt_errFn
- */
- saveDiffPreferences(prefs, opt_errFn) {
- // Invalidate the cache.
- this._cache.delete('/accounts/self/preferences.diff');
- return this._restApiHelper.send({
- method: 'PUT',
- url: '/accounts/self/preferences.diff',
- body: prefs,
- errFn: opt_errFn,
- reportUrlAsIs: true,
- });
- }
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: '/accounts/self/preferences',
+ body: prefs,
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
+ }
- /**
- * @param {?Object} prefs
- * @param {function(?Response, string=)=} opt_errFn
- */
- saveEditPreferences(prefs, opt_errFn) {
- // Invalidate the cache.
- this._cache.delete('/accounts/self/preferences.edit');
- return this._restApiHelper.send({
- method: 'PUT',
- url: '/accounts/self/preferences.edit',
- body: prefs,
- errFn: opt_errFn,
- reportUrlAsIs: true,
- });
- }
+ /**
+ * @param {?Object} prefs
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ saveDiffPreferences(prefs, opt_errFn) {
+ // Invalidate the cache.
+ this._cache.delete('/accounts/self/preferences.diff');
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: '/accounts/self/preferences.diff',
+ body: prefs,
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
+ }
- getAccount() {
- return this._fetchSharedCacheURL({
- url: '/accounts/self/detail',
- reportUrlAsIs: true,
- errFn: resp => {
- if (!resp || resp.status === 403) {
- this._cache.delete('/accounts/self/detail');
- }
- },
- });
- }
+ /**
+ * @param {?Object} prefs
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ saveEditPreferences(prefs, opt_errFn) {
+ // Invalidate the cache.
+ this._cache.delete('/accounts/self/preferences.edit');
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: '/accounts/self/preferences.edit',
+ body: prefs,
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
+ }
- getAvatarChangeUrl() {
- return this._fetchSharedCacheURL({
- url: '/accounts/self/avatar.change.url',
- reportUrlAsIs: true,
- errFn: resp => {
- if (!resp || resp.status === 403) {
- this._cache.delete('/accounts/self/avatar.change.url');
- }
- },
- });
- }
-
- getExternalIds() {
- return this._restApiHelper.fetchJSON({
- url: '/accounts/self/external.ids',
- reportUrlAsIs: true,
- });
- }
-
- deleteAccountIdentity(id) {
- return this._restApiHelper.send({
- method: 'POST',
- url: '/accounts/self/external.ids:delete',
- body: id,
- parseResponse: true,
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @param {string} userId the ID of the user usch as an email address.
- * @return {!Promise<!Object>}
- */
- getAccountDetails(userId) {
- return this._restApiHelper.fetchJSON({
- url: `/accounts/${encodeURIComponent(userId)}/detail`,
- anonymizedUrl: '/accounts/*/detail',
- });
- }
-
- getAccountEmails() {
- return this._fetchSharedCacheURL({
- url: '/accounts/self/emails',
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @param {string} email
- * @param {function(?Response, string=)=} opt_errFn
- */
- addAccountEmail(email, opt_errFn) {
- return this._restApiHelper.send({
- method: 'PUT',
- url: '/accounts/self/emails/' + encodeURIComponent(email),
- errFn: opt_errFn,
- anonymizedUrl: '/account/self/emails/*',
- });
- }
-
- /**
- * @param {string} email
- * @param {function(?Response, string=)=} opt_errFn
- */
- deleteAccountEmail(email, opt_errFn) {
- return this._restApiHelper.send({
- method: 'DELETE',
- url: '/accounts/self/emails/' + encodeURIComponent(email),
- errFn: opt_errFn,
- anonymizedUrl: '/accounts/self/email/*',
- });
- }
-
- /**
- * @param {string} email
- * @param {function(?Response, string=)=} opt_errFn
- */
- setPreferredAccountEmail(email, opt_errFn) {
- const encodedEmail = encodeURIComponent(email);
- const req = {
- method: 'PUT',
- url: `/accounts/self/emails/${encodedEmail}/preferred`,
- errFn: opt_errFn,
- anonymizedUrl: '/accounts/self/emails/*/preferred',
- };
- return this._restApiHelper.send(req).then(() => {
- // If result of getAccountEmails is in cache, update it in the cache
- // so we don't have to invalidate it.
- const cachedEmails = this._cache.get('/accounts/self/emails');
- if (cachedEmails) {
- const emails = cachedEmails.map(entry => {
- if (entry.email === email) {
- return {email, preferred: true};
- } else {
- return {email};
- }
- });
- this._cache.set('/accounts/self/emails', emails);
+ getAccount() {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/detail',
+ reportUrlAsIs: true,
+ errFn: resp => {
+ if (!resp || resp.status === 403) {
+ this._cache.delete('/accounts/self/detail');
}
- });
- }
+ },
+ });
+ }
- /**
- * @param {?Object} obj
- */
- _updateCachedAccount(obj) {
- // If result of getAccount is in cache, update it in the cache
+ getAvatarChangeUrl() {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/avatar.change.url',
+ reportUrlAsIs: true,
+ errFn: resp => {
+ if (!resp || resp.status === 403) {
+ this._cache.delete('/accounts/self/avatar.change.url');
+ }
+ },
+ });
+ }
+
+ getExternalIds() {
+ return this._restApiHelper.fetchJSON({
+ url: '/accounts/self/external.ids',
+ reportUrlAsIs: true,
+ });
+ }
+
+ deleteAccountIdentity(id) {
+ return this._restApiHelper.send({
+ method: 'POST',
+ url: '/accounts/self/external.ids:delete',
+ body: id,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @param {string} userId the ID of the user usch as an email address.
+ * @return {!Promise<!Object>}
+ */
+ getAccountDetails(userId) {
+ return this._restApiHelper.fetchJSON({
+ url: `/accounts/${encodeURIComponent(userId)}/detail`,
+ anonymizedUrl: '/accounts/*/detail',
+ });
+ }
+
+ getAccountEmails() {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/emails',
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @param {string} email
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ addAccountEmail(email, opt_errFn) {
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: '/accounts/self/emails/' + encodeURIComponent(email),
+ errFn: opt_errFn,
+ anonymizedUrl: '/account/self/emails/*',
+ });
+ }
+
+ /**
+ * @param {string} email
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ deleteAccountEmail(email, opt_errFn) {
+ return this._restApiHelper.send({
+ method: 'DELETE',
+ url: '/accounts/self/emails/' + encodeURIComponent(email),
+ errFn: opt_errFn,
+ anonymizedUrl: '/accounts/self/email/*',
+ });
+ }
+
+ /**
+ * @param {string} email
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ setPreferredAccountEmail(email, opt_errFn) {
+ const encodedEmail = encodeURIComponent(email);
+ const req = {
+ method: 'PUT',
+ url: `/accounts/self/emails/${encodedEmail}/preferred`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/accounts/self/emails/*/preferred',
+ };
+ return this._restApiHelper.send(req).then(() => {
+ // If result of getAccountEmails is in cache, update it in the cache
// so we don't have to invalidate it.
- const cachedAccount = this._cache.get('/accounts/self/detail');
- if (cachedAccount) {
- // Replace object in cache with new object to force UI updates.
- this._cache.set('/accounts/self/detail',
- Object.assign({}, cachedAccount, obj));
- }
- }
-
- /**
- * @param {string} name
- * @param {function(?Response, string=)=} opt_errFn
- */
- setAccountName(name, opt_errFn) {
- const req = {
- method: 'PUT',
- url: '/accounts/self/name',
- body: {name},
- errFn: opt_errFn,
- parseResponse: true,
- reportUrlAsIs: true,
- };
- return this._restApiHelper.send(req)
- .then(newName => this._updateCachedAccount({name: newName}));
- }
-
- /**
- * @param {string} username
- * @param {function(?Response, string=)=} opt_errFn
- */
- setAccountUsername(username, opt_errFn) {
- const req = {
- method: 'PUT',
- url: '/accounts/self/username',
- body: {username},
- errFn: opt_errFn,
- parseResponse: true,
- reportUrlAsIs: true,
- };
- return this._restApiHelper.send(req)
- .then(newName => this._updateCachedAccount({username: newName}));
- }
-
- /**
- * @param {string} status
- * @param {function(?Response, string=)=} opt_errFn
- */
- setAccountStatus(status, opt_errFn) {
- const req = {
- method: 'PUT',
- url: '/accounts/self/status',
- body: {status},
- errFn: opt_errFn,
- parseResponse: true,
- reportUrlAsIs: true,
- };
- return this._restApiHelper.send(req)
- .then(newStatus => this._updateCachedAccount({status: newStatus}));
- }
-
- getAccountStatus(userId) {
- return this._restApiHelper.fetchJSON({
- url: `/accounts/${encodeURIComponent(userId)}/status`,
- anonymizedUrl: '/accounts/*/status',
- });
- }
-
- getAccountGroups() {
- return this._restApiHelper.fetchJSON({
- url: '/accounts/self/groups',
- reportUrlAsIs: true,
- });
- }
-
- getAccountAgreements() {
- return this._restApiHelper.fetchJSON({
- url: '/accounts/self/agreements',
- reportUrlAsIs: true,
- });
- }
-
- saveAccountAgreement(name) {
- return this._restApiHelper.send({
- method: 'PUT',
- url: '/accounts/self/agreements',
- body: name,
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @param {string=} opt_params
- */
- getAccountCapabilities(opt_params) {
- let queryString = '';
- if (opt_params) {
- queryString = '?q=' + opt_params
- .map(param => encodeURIComponent(param))
- .join('&q=');
- }
- return this._fetchSharedCacheURL({
- url: '/accounts/self/capabilities' + queryString,
- anonymizedUrl: '/accounts/self/capabilities?q=*',
- });
- }
-
- getLoggedIn() {
- return this._auth.authCheck();
- }
-
- getIsAdmin() {
- return this.getLoggedIn()
- .then(isLoggedIn => {
- if (isLoggedIn) {
- return this.getAccountCapabilities();
- } else {
- return Promise.resolve();
- }
- })
- .then(
- capabilities => capabilities && capabilities.administrateServer
- );
- }
-
- getDefaultPreferences() {
- return this._fetchSharedCacheURL({
- url: '/config/server/preferences',
- reportUrlAsIs: true,
- });
- }
-
- getPreferences() {
- return this.getLoggedIn().then(loggedIn => {
- if (loggedIn) {
- const req = {url: '/accounts/self/preferences', reportUrlAsIs: true};
- return this._fetchSharedCacheURL(req).then(res => {
- if (this._isNarrowScreen()) {
- // Note that this can be problematic, because the diff will stay
- // unified even after increasing the window width.
- res.default_diff_view = DiffViewMode.UNIFIED;
- } else {
- res.default_diff_view = res.diff_view;
- }
- return Promise.resolve(res);
- });
- }
-
- return Promise.resolve({
- changes_per_page: 25,
- default_diff_view: this._isNarrowScreen() ?
- DiffViewMode.UNIFIED : DiffViewMode.SIDE_BY_SIDE,
- diff_view: 'SIDE_BY_SIDE',
- size_bar_in_change_table: true,
- });
- });
- }
-
- getWatchedProjects() {
- return this._fetchSharedCacheURL({
- url: '/accounts/self/watched.projects',
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @param {string} projects
- * @param {function(?Response, string=)=} opt_errFn
- */
- saveWatchedProjects(projects, opt_errFn) {
- return this._restApiHelper.send({
- method: 'POST',
- url: '/accounts/self/watched.projects',
- body: projects,
- errFn: opt_errFn,
- parseResponse: true,
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @param {string} projects
- * @param {function(?Response, string=)=} opt_errFn
- */
- deleteWatchedProjects(projects, opt_errFn) {
- return this._restApiHelper.send({
- method: 'POST',
- url: '/accounts/self/watched.projects:delete',
- body: projects,
- errFn: opt_errFn,
- reportUrlAsIs: true,
- });
- }
-
- _isNarrowScreen() {
- return window.innerWidth < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX;
- }
-
- /**
- * @param {number=} opt_changesPerPage
- * @param {string|!Array<string>=} opt_query A query or an array of queries.
- * @param {number|string=} opt_offset
- * @param {!Object=} opt_options
- * @return {?Array<!Object>|?Array<!Array<!Object>>} If opt_query is an
- * array, _fetchJSON will return an array of arrays of changeInfos. If it
- * is unspecified or a string, _fetchJSON will return an array of
- * changeInfos.
- */
- getChanges(opt_changesPerPage, opt_query, opt_offset, opt_options) {
- const options = opt_options || this.listChangesOptionsToHex(
- this.ListChangesOption.LABELS,
- this.ListChangesOption.DETAILED_ACCOUNTS
- );
- // Issue 4524: respect legacy token with max sortkey.
- if (opt_offset === 'n,z') {
- opt_offset = 0;
- }
- const params = {
- O: options,
- S: opt_offset || 0,
- };
- if (opt_changesPerPage) { params.n = opt_changesPerPage; }
- if (opt_query && opt_query.length > 0) {
- params.q = opt_query;
- }
- const iterateOverChanges = arr => {
- for (const change of (arr || [])) {
- this._maybeInsertInLookup(change);
- }
- };
- const req = {
- url: '/changes/',
- params,
- reportUrlAsIs: true,
- };
- return this._restApiHelper.fetchJSON(req).then(response => {
- // Response may be an array of changes OR an array of arrays of
- // changes.
- if (opt_query instanceof Array) {
- // Normalize the response to look like a multi-query response
- // when there is only one query.
- if (opt_query.length === 1) {
- response = [response];
- }
- for (const arr of response) {
- iterateOverChanges(arr);
- }
- } else {
- iterateOverChanges(response);
- }
- return response;
- });
- }
-
- /**
- * Inserts a change into _projectLookup iff it has a valid structure.
- *
- * @param {?{ _number: (number|string) }} change
- */
- _maybeInsertInLookup(change) {
- if (change && change.project && change._number) {
- this.setInProjectLookup(change._number, change.project);
- }
- }
-
- /**
- * TODO (beckysiegel) this needs to be rewritten with the optional param
- * at the end.
- *
- * @param {number|string} changeNum
- * @param {?number|string=} opt_patchNum passed as null sometimes.
- * @param {?=} endpoint
- * @return {!Promise<string>}
- */
- getChangeActionURL(changeNum, opt_patchNum, endpoint) {
- return this._changeBaseURL(changeNum, opt_patchNum)
- .then(url => url + endpoint);
- }
-
- /**
- * @param {number|string} changeNum
- * @param {function(?Response, string=)=} opt_errFn
- * @param {function()=} opt_cancelCondition
- */
- getChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
- return this.getConfig(false).then(config => {
- const optionsHex = this._getChangeOptionsHex(config);
- return this._getChangeDetail(
- changeNum, optionsHex, opt_errFn, opt_cancelCondition)
- .then(GrReviewerUpdatesParser.parse);
- });
- }
-
- _getChangeOptionsHex(config) {
- if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.changePage
- && !(config.receive && config.receive.enable_signed_push)) {
- return window.DEFAULT_DETAIL_HEXES.changePage;
- }
-
- // This list MUST be kept in sync with
- // ChangeIT#changeDetailsDoesNotRequireIndex
- const options = [
- this.ListChangesOption.ALL_COMMITS,
- this.ListChangesOption.ALL_REVISIONS,
- this.ListChangesOption.CHANGE_ACTIONS,
- this.ListChangesOption.DETAILED_LABELS,
- this.ListChangesOption.DOWNLOAD_COMMANDS,
- this.ListChangesOption.MESSAGES,
- this.ListChangesOption.SUBMITTABLE,
- this.ListChangesOption.WEB_LINKS,
- this.ListChangesOption.SKIP_DIFFSTAT,
- ];
- if (config.receive && config.receive.enable_signed_push) {
- options.push(this.ListChangesOption.PUSH_CERTIFICATES);
- }
- return this.listChangesOptionsToHex(...options);
- }
-
- /**
- * @param {number|string} changeNum
- * @param {function(?Response, string=)=} opt_errFn
- * @param {function()=} opt_cancelCondition
- */
- getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
- let optionsHex = '';
- if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.diffPage) {
- optionsHex = window.DEFAULT_DETAIL_HEXES.diffPage;
- } else {
- optionsHex = this.listChangesOptionsToHex(
- this.ListChangesOption.ALL_COMMITS,
- this.ListChangesOption.ALL_REVISIONS,
- this.ListChangesOption.SKIP_DIFFSTAT
- );
- }
- return this._getChangeDetail(changeNum, optionsHex, opt_errFn,
- opt_cancelCondition);
- }
-
- /**
- * @param {number|string} changeNum
- * @param {string|undefined} optionsHex list changes options in hex
- * @param {function(?Response, string=)=} opt_errFn
- * @param {function()=} opt_cancelCondition
- */
- _getChangeDetail(changeNum, optionsHex, opt_errFn, opt_cancelCondition) {
- return this.getChangeActionURL(changeNum, null, '/detail').then(url => {
- const urlWithParams = this._restApiHelper
- .urlWithParams(url, optionsHex);
- const params = {O: optionsHex};
- const req = {
- url,
- errFn: opt_errFn,
- cancelCondition: opt_cancelCondition,
- params,
- fetchOptions: this._etags.getOptions(urlWithParams),
- anonymizedUrl: '/changes/*~*/detail?O=' + optionsHex,
- };
- return this._restApiHelper.fetchRawJSON(req).then(response => {
- if (response && response.status === 304) {
- return Promise.resolve(this._restApiHelper.parsePrefixedJSON(
- this._etags.getCachedPayload(urlWithParams)));
- }
-
- if (response && !response.ok) {
- if (opt_errFn) {
- opt_errFn.call(null, response);
- } else {
- this.fire('server-error', {request: req, response});
- }
- return;
- }
-
- const payloadPromise = response ?
- this._restApiHelper.readResponsePayload(response) :
- Promise.resolve(null);
-
- return payloadPromise.then(payload => {
- if (!payload) { return null; }
- this._etags.collect(urlWithParams, response, payload.raw);
- this._maybeInsertInLookup(payload.parsed);
-
- return payload.parsed;
- });
- });
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string} patchNum
- */
- getChangeCommitInfo(changeNum, patchNum) {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/commit?links',
- patchNum,
- reportEndpointAsIs: true,
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {Gerrit.PatchRange} patchRange
- * @param {number=} opt_parentIndex
- */
- getChangeFiles(changeNum, patchRange, opt_parentIndex) {
- let params = undefined;
- if (this.isMergeParent(patchRange.basePatchNum)) {
- params = {parent: this.getParentIndex(patchRange.basePatchNum)};
- } else if (!this.patchNumEquals(patchRange.basePatchNum, 'PARENT')) {
- params = {base: patchRange.basePatchNum};
- }
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/files',
- patchNum: patchRange.patchNum,
- params,
- reportEndpointAsIs: true,
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {Gerrit.PatchRange} patchRange
- */
- getChangeEditFiles(changeNum, patchRange) {
- let endpoint = '/edit?list';
- let anonymizedEndpoint = endpoint;
- if (patchRange.basePatchNum !== 'PARENT') {
- endpoint += '&base=' + encodeURIComponent(patchRange.basePatchNum + '');
- anonymizedEndpoint += '&base=*';
- }
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint,
- anonymizedEndpoint,
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string} patchNum
- * @param {string} query
- * @return {!Promise<!Object>}
- */
- queryChangeFiles(changeNum, patchNum, query) {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: `/files?q=${encodeURIComponent(query)}`,
- patchNum,
- anonymizedEndpoint: '/files?q=*',
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {Gerrit.PatchRange} patchRange
- * @return {!Promise<!Array<!Object>>}
- */
- getChangeOrEditFiles(changeNum, patchRange) {
- if (this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME)) {
- return this.getChangeEditFiles(changeNum, patchRange).then(res =>
- res.files);
- }
- return this.getChangeFiles(changeNum, patchRange);
- }
-
- getChangeRevisionActions(changeNum, patchNum) {
- const req = {
- changeNum,
- endpoint: '/actions',
- patchNum,
- reportEndpointAsIs: true,
- };
- return this._getChangeURLAndFetch(req).then(revisionActions => {
- // The rebase button on change screen is always enabled.
- if (revisionActions.rebase) {
- revisionActions.rebase.rebaseOnCurrent =
- !!revisionActions.rebase.enabled;
- revisionActions.rebase.enabled = true;
- }
- return revisionActions;
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {string} inputVal
- * @param {function(?Response, string=)=} opt_errFn
- */
- getChangeSuggestedReviewers(changeNum, inputVal, opt_errFn) {
- return this._getChangeSuggestedGroup('REVIEWER', changeNum, inputVal,
- opt_errFn);
- }
-
- /**
- * @param {number|string} changeNum
- * @param {string} inputVal
- * @param {function(?Response, string=)=} opt_errFn
- */
- getChangeSuggestedCCs(changeNum, inputVal, opt_errFn) {
- return this._getChangeSuggestedGroup('CC', changeNum, inputVal,
- opt_errFn);
- }
-
- _getChangeSuggestedGroup(reviewerState, changeNum, inputVal, opt_errFn) {
- // More suggestions may obscure content underneath in the reply dialog,
- // see issue 10793.
- const params = {'n': 6, 'reviewer-state': reviewerState};
- if (inputVal) { params.q = inputVal; }
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/suggest_reviewers',
- errFn: opt_errFn,
- params,
- reportEndpointAsIs: true,
- });
- }
-
- /**
- * @param {number|string} changeNum
- */
- getChangeIncludedIn(changeNum) {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/in',
- reportEndpointAsIs: true,
- });
- }
-
- _computeFilter(filter) {
- if (filter && filter.startsWith('^')) {
- filter = '&r=' + encodeURIComponent(filter);
- } else if (filter) {
- filter = '&m=' + encodeURIComponent(filter);
- } else {
- filter = '';
- }
- return filter;
- }
-
- /**
- * @param {string} filter
- * @param {number} groupsPerPage
- * @param {number=} opt_offset
- */
- _getGroupsUrl(filter, groupsPerPage, opt_offset) {
- const offset = opt_offset || 0;
-
- return `/groups/?n=${groupsPerPage + 1}&S=${offset}` +
- this._computeFilter(filter);
- }
-
- /**
- * @param {string} filter
- * @param {number} reposPerPage
- * @param {number=} opt_offset
- */
- _getReposUrl(filter, reposPerPage, opt_offset) {
- const defaultFilter = 'state:active OR state:read-only';
- const namePartDelimiters = /[@.\-\s\/_]/g;
- const offset = opt_offset || 0;
-
- if (filter && !filter.includes(':') && filter.match(namePartDelimiters)) {
- // The query language specifies hyphens as operators. Split the string
- // by hyphens and 'AND' the parts together as 'inname:' queries.
- // If the filter includes a semicolon, the user is using a more complex
- // query so we trust them and don't do any magic under the hood.
- const originalFilter = filter;
- filter = '';
- originalFilter.split(namePartDelimiters).forEach(part => {
- if (part) {
- filter += (filter === '' ? 'inname:' : ' AND inname:') + part;
+ const cachedEmails = this._cache.get('/accounts/self/emails');
+ if (cachedEmails) {
+ const emails = cachedEmails.map(entry => {
+ if (entry.email === email) {
+ return {email, preferred: true};
+ } else {
+ return {email};
}
});
+ this._cache.set('/accounts/self/emails', emails);
}
- // Check if filter is now empty which could be either because the user did
- // not provide it or because the user provided only a split character.
- if (!filter) {
- filter = defaultFilter;
- }
+ });
+ }
- filter = filter.trim();
- const encodedFilter = encodeURIComponent(filter);
-
- return `/projects/?n=${reposPerPage + 1}&S=${offset}` +
- `&query=${encodedFilter}`;
- }
-
- invalidateGroupsCache() {
- this._restApiHelper.invalidateFetchPromisesPrefix('/groups/?');
- }
-
- invalidateReposCache() {
- this._restApiHelper.invalidateFetchPromisesPrefix('/projects/?');
- }
-
- invalidateAccountsCache() {
- this._restApiHelper.invalidateFetchPromisesPrefix('/accounts/');
- }
-
- /**
- * @param {string} filter
- * @param {number} groupsPerPage
- * @param {number=} opt_offset
- * @return {!Promise<?Object>}
- */
- getGroups(filter, groupsPerPage, opt_offset) {
- const url = this._getGroupsUrl(filter, groupsPerPage, opt_offset);
-
- return this._fetchSharedCacheURL({
- url,
- anonymizedUrl: '/groups/?*',
- });
- }
-
- /**
- * @param {string} filter
- * @param {number} reposPerPage
- * @param {number=} opt_offset
- * @return {!Promise<?Object>}
- */
- getRepos(filter, reposPerPage, opt_offset) {
- const url = this._getReposUrl(filter, reposPerPage, opt_offset);
-
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._fetchSharedCacheURL({
- url,
- anonymizedUrl: '/projects/?*',
- });
- }
-
- setRepoHead(repo, ref) {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/projects/${encodeURIComponent(repo)}/HEAD`,
- body: {ref},
- anonymizedUrl: '/projects/*/HEAD',
- });
- }
-
- /**
- * @param {string} filter
- * @param {string} repo
- * @param {number} reposBranchesPerPage
- * @param {number=} opt_offset
- * @param {?function(?Response, string=)=} opt_errFn
- * @return {!Promise<?Object>}
- */
- getRepoBranches(filter, repo, reposBranchesPerPage, opt_offset, opt_errFn) {
- const offset = opt_offset || 0;
- const count = reposBranchesPerPage + 1;
- filter = this._computeFilter(filter);
- repo = encodeURIComponent(repo);
- const url = `/projects/${repo}/branches?n=${count}&S=${offset}${filter}`;
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._restApiHelper.fetchJSON({
- url,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/branches?*',
- });
- }
-
- /**
- * @param {string} filter
- * @param {string} repo
- * @param {number} reposTagsPerPage
- * @param {number=} opt_offset
- * @param {?function(?Response, string=)=} opt_errFn
- * @return {!Promise<?Object>}
- */
- getRepoTags(filter, repo, reposTagsPerPage, opt_offset, opt_errFn) {
- const offset = opt_offset || 0;
- const encodedRepo = encodeURIComponent(repo);
- const n = reposTagsPerPage + 1;
- const encodedFilter = this._computeFilter(filter);
- const url = `/projects/${encodedRepo}/tags` + `?n=${n}&S=${offset}` +
- encodedFilter;
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._restApiHelper.fetchJSON({
- url,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/tags',
- });
- }
-
- /**
- * @param {string} filter
- * @param {number} pluginsPerPage
- * @param {number=} opt_offset
- * @param {?function(?Response, string=)=} opt_errFn
- * @return {!Promise<?Object>}
- */
- getPlugins(filter, pluginsPerPage, opt_offset, opt_errFn) {
- const offset = opt_offset || 0;
- const encodedFilter = this._computeFilter(filter);
- const n = pluginsPerPage + 1;
- const url = `/plugins/?all&n=${n}&S=${offset}${encodedFilter}`;
- return this._restApiHelper.fetchJSON({
- url,
- errFn: opt_errFn,
- anonymizedUrl: '/plugins/?all',
- });
- }
-
- getRepoAccessRights(repoName, opt_errFn) {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._restApiHelper.fetchJSON({
- url: `/projects/${encodeURIComponent(repoName)}/access`,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/access',
- });
- }
-
- setRepoAccessRights(repoName, repoInfo) {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._restApiHelper.send({
- method: 'POST',
- url: `/projects/${encodeURIComponent(repoName)}/access`,
- body: repoInfo,
- anonymizedUrl: '/projects/*/access',
- });
- }
-
- setRepoAccessRightsForReview(projectName, projectInfo) {
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/projects/${encodeURIComponent(projectName)}/access:review`,
- body: projectInfo,
- parseResponse: true,
- anonymizedUrl: '/projects/*/access:review',
- });
- }
-
- /**
- * @param {string} inputVal
- * @param {number} opt_n
- * @param {function(?Response, string=)=} opt_errFn
- */
- getSuggestedGroups(inputVal, opt_n, opt_errFn) {
- const params = {s: inputVal};
- if (opt_n) { params.n = opt_n; }
- return this._restApiHelper.fetchJSON({
- url: '/groups/',
- errFn: opt_errFn,
- params,
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @param {string} inputVal
- * @param {number} opt_n
- * @param {function(?Response, string=)=} opt_errFn
- */
- getSuggestedProjects(inputVal, opt_n, opt_errFn) {
- const params = {
- m: inputVal,
- n: MAX_PROJECT_RESULTS,
- type: 'ALL',
- };
- if (opt_n) { params.n = opt_n; }
- return this._restApiHelper.fetchJSON({
- url: '/projects/',
- errFn: opt_errFn,
- params,
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @param {string} inputVal
- * @param {number} opt_n
- * @param {function(?Response, string=)=} opt_errFn
- */
- getSuggestedAccounts(inputVal, opt_n, opt_errFn) {
- if (!inputVal) {
- return Promise.resolve([]);
- }
- const params = {suggest: null, q: inputVal};
- if (opt_n) { params.n = opt_n; }
- return this._restApiHelper.fetchJSON({
- url: '/accounts/',
- errFn: opt_errFn,
- params,
- anonymizedUrl: '/accounts/?n=*',
- });
- }
-
- addChangeReviewer(changeNum, reviewerID) {
- return this._sendChangeReviewerRequest('POST', changeNum, reviewerID);
- }
-
- removeChangeReviewer(changeNum, reviewerID) {
- return this._sendChangeReviewerRequest('DELETE', changeNum, reviewerID);
- }
-
- _sendChangeReviewerRequest(method, changeNum, reviewerID) {
- return this.getChangeActionURL(changeNum, null, '/reviewers')
- .then(url => {
- let body;
- switch (method) {
- case 'POST':
- body = {reviewer: reviewerID};
- break;
- case 'DELETE':
- url += '/' + encodeURIComponent(reviewerID);
- break;
- default:
- throw Error('Unsupported HTTP method: ' + method);
- }
-
- return this._restApiHelper.send({method, url, body});
- });
- }
-
- getRelatedChanges(changeNum, patchNum) {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/related',
- patchNum,
- reportEndpointAsIs: true,
- });
- }
-
- getChangesSubmittedTogether(changeNum) {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/submitted_together?o=NON_VISIBLE_CHANGES',
- reportEndpointAsIs: true,
- });
- }
-
- getChangeConflicts(changeNum) {
- const options = this.listChangesOptionsToHex(
- this.ListChangesOption.CURRENT_REVISION,
- this.ListChangesOption.CURRENT_COMMIT
- );
- const params = {
- O: options,
- q: 'status:open conflicts:' + changeNum,
- };
- return this._restApiHelper.fetchJSON({
- url: '/changes/',
- params,
- anonymizedUrl: '/changes/conflicts:*',
- });
- }
-
- getChangeCherryPicks(project, changeID, changeNum) {
- const options = this.listChangesOptionsToHex(
- this.ListChangesOption.CURRENT_REVISION,
- this.ListChangesOption.CURRENT_COMMIT
- );
- const query = [
- 'project:' + project,
- 'change:' + changeID,
- '-change:' + changeNum,
- '-is:abandoned',
- ].join(' ');
- const params = {
- O: options,
- q: query,
- };
- return this._restApiHelper.fetchJSON({
- url: '/changes/',
- params,
- anonymizedUrl: '/changes/change:*',
- });
- }
-
- getChangesWithSameTopic(topic, changeNum) {
- const options = this.listChangesOptionsToHex(
- this.ListChangesOption.LABELS,
- this.ListChangesOption.CURRENT_REVISION,
- this.ListChangesOption.CURRENT_COMMIT,
- this.ListChangesOption.DETAILED_LABELS
- );
- const query = [
- 'status:open',
- '-change:' + changeNum,
- `topic:"${topic}"`,
- ].join(' ');
- const params = {
- O: options,
- q: query,
- };
- return this._restApiHelper.fetchJSON({
- url: '/changes/',
- params,
- anonymizedUrl: '/changes/topic:*',
- });
- }
-
- getReviewedFiles(changeNum, patchNum) {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/files?reviewed',
- patchNum,
- reportEndpointAsIs: true,
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string} patchNum
- * @param {string} path
- * @param {boolean} reviewed
- * @param {function(?Response, string=)=} opt_errFn
- */
- saveFileReviewed(changeNum, patchNum, path, reviewed, opt_errFn) {
- return this._getChangeURLAndSend({
- changeNum,
- method: reviewed ? 'PUT' : 'DELETE',
- patchNum,
- endpoint: `/files/${encodeURIComponent(path)}/reviewed`,
- errFn: opt_errFn,
- anonymizedEndpoint: '/files/*/reviewed',
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string} patchNum
- * @param {!Object} review
- * @param {function(?Response, string=)=} opt_errFn
- */
- saveChangeReview(changeNum, patchNum, review, opt_errFn) {
- const promises = [
- this.awaitPendingDiffDrafts(),
- this.getChangeActionURL(changeNum, patchNum, '/review'),
- ];
- return Promise.all(promises).then(([, url]) => this._restApiHelper.send({
- method: 'POST',
- url,
- body: review,
- errFn: opt_errFn,
- }));
- }
-
- getChangeEdit(changeNum, opt_download_commands) {
- const params = opt_download_commands ? {'download-commands': true} : null;
- return this.getLoggedIn().then(loggedIn => {
- if (!loggedIn) { return false; }
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/edit/',
- params,
- reportEndpointAsIs: true,
- }, true);
- });
- }
-
- /**
- * @param {string} project
- * @param {string} branch
- * @param {string} subject
- * @param {string=} opt_topic
- * @param {boolean=} opt_isPrivate
- * @param {boolean=} opt_workInProgress
- * @param {string=} opt_baseChange
- * @param {string=} opt_baseCommit
- */
- createChange(project, branch, subject, opt_topic, opt_isPrivate,
- opt_workInProgress, opt_baseChange, opt_baseCommit) {
- return this._restApiHelper.send({
- method: 'POST',
- url: '/changes/',
- body: {
- project,
- branch,
- subject,
- topic: opt_topic,
- is_private: opt_isPrivate,
- work_in_progress: opt_workInProgress,
- base_change: opt_baseChange,
- base_commit: opt_baseCommit,
- },
- parseResponse: true,
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {string} path
- * @param {number|string} patchNum
- */
- getFileContent(changeNum, path, patchNum) {
- // 404s indicate the file does not exist yet in the revision, so suppress
- // them.
- const suppress404s = res => {
- if (res && res.status !== 404) { this.fire('server-error', {res}); }
- return res;
- };
- const promise = this.patchNumEquals(patchNum, this.EDIT_NAME) ?
- this._getFileInChangeEdit(changeNum, path) :
- this._getFileInRevision(changeNum, path, patchNum, suppress404s);
-
- return promise.then(res => {
- if (!res.ok) { return res; }
-
- // The file type (used for syntax highlighting) is identified in the
- // X-FYI-Content-Type header of the response.
- const type = res.headers.get('X-FYI-Content-Type');
- return this.getResponseObject(res).then(content => {
- return {content, type, ok: true};
- });
- });
- }
-
- /**
- * Gets a file in a specific change and revision.
- *
- * @param {number|string} changeNum
- * @param {string} path
- * @param {number|string} patchNum
- * @param {?function(?Response, string=)=} opt_errFn
- */
- _getFileInRevision(changeNum, path, patchNum, opt_errFn) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'GET',
- patchNum,
- endpoint: `/files/${encodeURIComponent(path)}/content`,
- errFn: opt_errFn,
- headers: {Accept: 'application/json'},
- anonymizedEndpoint: '/files/*/content',
- });
- }
-
- /**
- * Gets a file in a change edit.
- *
- * @param {number|string} changeNum
- * @param {string} path
- */
- _getFileInChangeEdit(changeNum, path) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'GET',
- endpoint: '/edit/' + encodeURIComponent(path),
- headers: {Accept: 'application/json'},
- anonymizedEndpoint: '/edit/*',
- });
- }
-
- rebaseChangeEdit(changeNum) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'POST',
- endpoint: '/edit:rebase',
- reportEndpointAsIs: true,
- });
- }
-
- deleteChangeEdit(changeNum) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'DELETE',
- endpoint: '/edit',
- reportEndpointAsIs: true,
- });
- }
-
- restoreFileInChangeEdit(changeNum, restore_path) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'POST',
- endpoint: '/edit',
- body: {restore_path},
- reportEndpointAsIs: true,
- });
- }
-
- renameFileInChangeEdit(changeNum, old_path, new_path) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'POST',
- endpoint: '/edit',
- body: {old_path, new_path},
- reportEndpointAsIs: true,
- });
- }
-
- deleteFileInChangeEdit(changeNum, path) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'DELETE',
- endpoint: '/edit/' + encodeURIComponent(path),
- anonymizedEndpoint: '/edit/*',
- });
- }
-
- saveChangeEdit(changeNum, path, contents) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'PUT',
- endpoint: '/edit/' + encodeURIComponent(path),
- body: contents,
- contentType: 'text/plain',
- anonymizedEndpoint: '/edit/*',
- });
- }
-
- getRobotCommentFixPreview(changeNum, patchNum, fixId) {
- return this._getChangeURLAndFetch({
- changeNum,
- patchNum,
- endpoint: `/fixes/${encodeURIComponent(fixId)}/preview`,
- reportEndpointAsId: true,
- });
- }
-
- applyFixSuggestion(changeNum, patchNum, fixId) {
- return this._getChangeURLAndSend({
- method: 'POST',
- changeNum,
- patchNum,
- endpoint: `/fixes/${encodeURIComponent(fixId)}/apply`,
- reportEndpointAsId: true,
- });
- }
-
- // Deprecated, prefer to use putChangeCommitMessage instead.
- saveChangeCommitMessageEdit(changeNum, message) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'PUT',
- endpoint: '/edit:message',
- body: {message},
- reportEndpointAsIs: true,
- });
- }
-
- publishChangeEdit(changeNum) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'POST',
- endpoint: '/edit:publish',
- reportEndpointAsIs: true,
- });
- }
-
- putChangeCommitMessage(changeNum, message) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'PUT',
- endpoint: '/message',
- body: {message},
- reportEndpointAsIs: true,
- });
- }
-
- deleteChangeCommitMessage(changeNum, messageId) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'DELETE',
- endpoint: '/messages/' + messageId,
- reportEndpointAsIs: true,
- });
- }
-
- saveChangeStarred(changeNum, starred) {
- // Some servers may require the project name to be provided
- // alongside the change number, so resolve the project name
- // first.
- return this.getFromProjectLookup(changeNum).then(project => {
- const url = '/accounts/self/starred.changes/' +
- (project ? encodeURIComponent(project) + '~' : '') + changeNum;
- return this._restApiHelper.send({
- method: starred ? 'PUT' : 'DELETE',
- url,
- anonymizedUrl: '/accounts/self/starred.changes/*',
- });
- });
- }
-
- saveChangeReviewed(changeNum, reviewed) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'PUT',
- endpoint: reviewed ? '/reviewed' : '/unreviewed',
- });
- }
-
- /**
- * Public version of the _restApiHelper.send method preserved for plugins.
- *
- * @param {string} method
- * @param {string} url
- * @param {?string|number|Object=} opt_body passed as null sometimes
- * and also apparently a number. TODO (beckysiegel) remove need for
- * number at least.
- * @param {?function(?Response, string=)=} opt_errFn
- * passed as null sometimes.
- * @param {?string=} opt_contentType
- * @param {Object=} opt_headers
- */
- send(method, url, opt_body, opt_errFn, opt_contentType,
- opt_headers) {
- return this._restApiHelper.send({
- method,
- url,
- body: opt_body,
- errFn: opt_errFn,
- contentType: opt_contentType,
- headers: opt_headers,
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string} basePatchNum Negative values specify merge parent
- * index.
- * @param {number|string} patchNum
- * @param {string} path
- * @param {string=} opt_whitespace the ignore-whitespace level for the diff
- * algorithm.
- * @param {function(?Response, string=)=} opt_errFn
- */
- getDiff(changeNum, basePatchNum, patchNum, path, opt_whitespace,
- opt_errFn) {
- const params = {
- context: 'ALL',
- intraline: null,
- whitespace: opt_whitespace || 'IGNORE_NONE',
- };
- if (this.isMergeParent(basePatchNum)) {
- params.parent = this.getParentIndex(basePatchNum);
- } else if (!this.patchNumEquals(basePatchNum, PARENT_PATCH_NUM)) {
- params.base = basePatchNum;
- }
- const endpoint = `/files/${encodeURIComponent(path)}/diff`;
- const req = {
- changeNum,
- endpoint,
- patchNum,
- errFn: opt_errFn,
- params,
- anonymizedEndpoint: '/files/*/diff',
- };
-
- // Invalidate the cache if its edit patch to make sure we always get latest.
- if (patchNum === this.EDIT_NAME) {
- if (!req.fetchOptions) req.fetchOptions = {};
- if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers();
- req.fetchOptions.headers.append('Cache-Control', 'no-cache');
- }
-
- return this._getChangeURLAndFetch(req);
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string=} opt_basePatchNum
- * @param {number|string=} opt_patchNum
- * @param {string=} opt_path
- * @return {!Promise<!Object>}
- */
- getDiffComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
- return this._getDiffComments(changeNum, '/comments', opt_basePatchNum,
- opt_patchNum, opt_path);
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string=} opt_basePatchNum
- * @param {number|string=} opt_patchNum
- * @param {string=} opt_path
- * @return {!Promise<!Object>}
- */
- getDiffRobotComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
- return this._getDiffComments(changeNum, '/robotcomments',
- opt_basePatchNum, opt_patchNum, opt_path);
- }
-
- /**
- * If the user is logged in, fetch the user's draft diff comments. If there
- * is no logged in user, the request is not made and the promise yields an
- * empty object.
- *
- * @param {number|string} changeNum
- * @param {number|string=} opt_basePatchNum
- * @param {number|string=} opt_patchNum
- * @param {string=} opt_path
- * @return {!Promise<!Object>}
- */
- getDiffDrafts(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
- return this.getLoggedIn().then(loggedIn => {
- if (!loggedIn) { return Promise.resolve({}); }
- return this._getDiffComments(changeNum, '/drafts', opt_basePatchNum,
- opt_patchNum, opt_path);
- });
- }
-
- _setRange(comments, comment) {
- if (comment.in_reply_to && !comment.range) {
- for (let i = 0; i < comments.length; i++) {
- if (comments[i].id === comment.in_reply_to) {
- comment.range = comments[i].range;
- break;
- }
- }
- }
- return comment;
- }
-
- _setRanges(comments) {
- comments = comments || [];
- comments.sort(
- (a, b) => util.parseDate(a.updated) - util.parseDate(b.updated)
- );
- for (const comment of comments) {
- this._setRange(comments, comment);
- }
- return comments;
- }
-
- /**
- * @param {number|string} changeNum
- * @param {string} endpoint
- * @param {number|string=} opt_basePatchNum
- * @param {number|string=} opt_patchNum
- * @param {string=} opt_path
- * @return {!Promise<!Object>}
- */
- _getDiffComments(changeNum, endpoint, opt_basePatchNum,
- opt_patchNum, opt_path) {
- /**
- * Fetches the comments for a given patchNum.
- * Helper function to make promises more legible.
- *
- * @param {string|number=} opt_patchNum
- * @return {!Promise<!Object>} Diff comments response.
- */
- // We don't want to add accept header, since preloading of comments is
- // working only without accept header.
- const noAcceptHeader = true;
- const fetchComments = opt_patchNum => this._getChangeURLAndFetch({
- changeNum,
- endpoint,
- patchNum: opt_patchNum,
- reportEndpointAsIs: true,
- }, noAcceptHeader);
-
- if (!opt_basePatchNum && !opt_patchNum && !opt_path) {
- return fetchComments();
- }
- function onlyParent(c) { return c.side == PARENT_PATCH_NUM; }
- function withoutParent(c) { return c.side != PARENT_PATCH_NUM; }
- function setPath(c) { c.path = opt_path; }
-
- const promises = [];
- let comments;
- let baseComments;
- let fetchPromise;
- fetchPromise = fetchComments(opt_patchNum).then(response => {
- comments = response[opt_path] || [];
- // TODO(kaspern): Implement this on in the backend so this can
- // be removed.
- // Sort comments by date so that parent ranges can be propagated
- // in a single pass.
- comments = this._setRanges(comments);
-
- if (opt_basePatchNum == PARENT_PATCH_NUM) {
- baseComments = comments.filter(onlyParent);
- baseComments.forEach(setPath);
- }
- comments = comments.filter(withoutParent);
-
- comments.forEach(setPath);
- });
- promises.push(fetchPromise);
-
- if (opt_basePatchNum != PARENT_PATCH_NUM) {
- fetchPromise = fetchComments(opt_basePatchNum).then(response => {
- baseComments = (response[opt_path] || [])
- .filter(withoutParent);
- baseComments = this._setRanges(baseComments);
- baseComments.forEach(setPath);
- });
- promises.push(fetchPromise);
- }
-
- return Promise.all(promises).then(() => Promise.resolve({
- baseComments,
- comments,
- }));
- }
-
- /**
- * @param {number|string} changeNum
- * @param {string} endpoint
- * @param {number|string=} opt_patchNum
- */
- _getDiffCommentsFetchURL(changeNum, endpoint, opt_patchNum) {
- return this._changeBaseURL(changeNum, opt_patchNum)
- .then(url => url + endpoint);
- }
-
- saveDiffDraft(changeNum, patchNum, draft) {
- return this._sendDiffDraftRequest('PUT', changeNum, patchNum, draft);
- }
-
- deleteDiffDraft(changeNum, patchNum, draft) {
- return this._sendDiffDraftRequest('DELETE', changeNum, patchNum, draft);
- }
-
- /**
- * @returns {boolean} Whether there are pending diff draft sends.
- */
- hasPendingDiffDrafts() {
- const promises = this._pendingRequests[Requests.SEND_DIFF_DRAFT];
- return promises && promises.length;
- }
-
- /**
- * @returns {!Promise<undefined>} A promise that resolves when all pending
- * diff draft sends have resolved.
- */
- awaitPendingDiffDrafts() {
- return Promise.all(this._pendingRequests[Requests.SEND_DIFF_DRAFT] || [])
- .then(() => {
- this._pendingRequests[Requests.SEND_DIFF_DRAFT] = [];
- });
- }
-
- _sendDiffDraftRequest(method, changeNum, patchNum, draft) {
- const isCreate = !draft.id && method === 'PUT';
- let endpoint = '/drafts';
- let anonymizedEndpoint = endpoint;
- if (draft.id) {
- endpoint += '/' + draft.id;
- anonymizedEndpoint += '/*';
- }
- let body;
- if (method === 'PUT') {
- body = draft;
- }
-
- if (!this._pendingRequests[Requests.SEND_DIFF_DRAFT]) {
- this._pendingRequests[Requests.SEND_DIFF_DRAFT] = [];
- }
-
- const req = {
- changeNum,
- method,
- patchNum,
- endpoint,
- body,
- anonymizedEndpoint,
- };
-
- const promise = this._getChangeURLAndSend(req);
- this._pendingRequests[Requests.SEND_DIFF_DRAFT].push(promise);
-
- if (isCreate) {
- return this._failForCreate200(promise);
- }
-
- return promise;
- }
-
- getCommitInfo(project, commit) {
- return this._restApiHelper.fetchJSON({
- url: '/projects/' + encodeURIComponent(project) +
- '/commits/' + encodeURIComponent(commit),
- anonymizedUrl: '/projects/*/comments/*',
- });
- }
-
- _fetchB64File(url) {
- return this._restApiHelper.fetch({url: this.getBaseUrl() + url})
- .then(response => {
- if (!response.ok) {
- return Promise.reject(new Error(response.statusText));
- }
- const type = response.headers.get('X-FYI-Content-Type');
- return response.text()
- .then(text => {
- return {body: text, type};
- });
- });
- }
-
- /**
- * @param {string} changeId
- * @param {string|number} patchNum
- * @param {string} path
- * @param {number=} opt_parentIndex
- */
- getB64FileContents(changeId, patchNum, path, opt_parentIndex) {
- const parent = typeof opt_parentIndex === 'number' ?
- '?parent=' + opt_parentIndex : '';
- return this._changeBaseURL(changeId, patchNum).then(url => {
- url = `${url}/files/${encodeURIComponent(path)}/content${parent}`;
- return this._fetchB64File(url);
- });
- }
-
- getImagesForDiff(changeNum, diff, patchRange) {
- let promiseA;
- let promiseB;
-
- if (diff.meta_a && diff.meta_a.content_type.startsWith('image/')) {
- if (patchRange.basePatchNum === 'PARENT') {
- // Note: we only attempt to get the image from the first parent.
- promiseA = this.getB64FileContents(changeNum, patchRange.patchNum,
- diff.meta_a.name, 1);
- } else {
- promiseA = this.getB64FileContents(changeNum,
- patchRange.basePatchNum, diff.meta_a.name);
- }
- } else {
- promiseA = Promise.resolve(null);
- }
-
- if (diff.meta_b && diff.meta_b.content_type.startsWith('image/')) {
- promiseB = this.getB64FileContents(changeNum, patchRange.patchNum,
- diff.meta_b.name);
- } else {
- promiseB = Promise.resolve(null);
- }
-
- return Promise.all([promiseA, promiseB]).then(results => {
- const baseImage = results[0];
- const revisionImage = results[1];
-
- // Sometimes the server doesn't send back the content type.
- if (baseImage) {
- baseImage._expectedType = diff.meta_a.content_type;
- baseImage._name = diff.meta_a.name;
- }
- if (revisionImage) {
- revisionImage._expectedType = diff.meta_b.content_type;
- revisionImage._name = diff.meta_b.name;
- }
-
- return {baseImage, revisionImage};
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {?number|string=} opt_patchNum passed as null sometimes.
- * @param {string=} opt_project
- * @return {!Promise<string>}
- */
- _changeBaseURL(changeNum, opt_patchNum, opt_project) {
- // TODO(kaspern): For full slicer migration, app should warn with a call
- // stack every time _changeBaseURL is called without a project.
- const projectPromise = opt_project ?
- Promise.resolve(opt_project) :
- this.getFromProjectLookup(changeNum);
- return projectPromise.then(project => {
- let url = `/changes/${encodeURIComponent(project)}~${changeNum}`;
- if (opt_patchNum) {
- url += `/revisions/${opt_patchNum}`;
- }
- return url;
- });
- }
-
- /**
- * @suppress {checkTypes}
- * Resulted in error: Promise.prototype.then does not match formal
- * parameter.
- */
- setChangeTopic(changeNum, topic) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'PUT',
- endpoint: '/topic',
- body: {topic},
- parseResponse: true,
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @suppress {checkTypes}
- * Resulted in error: Promise.prototype.then does not match formal
- * parameter.
- */
- setChangeHashtag(changeNum, hashtag) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'POST',
- endpoint: '/hashtags',
- body: hashtag,
- parseResponse: true,
- reportUrlAsIs: true,
- });
- }
-
- deleteAccountHttpPassword() {
- return this._restApiHelper.send({
- method: 'DELETE',
- url: '/accounts/self/password.http',
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @suppress {checkTypes}
- * Resulted in error: Promise.prototype.then does not match formal
- * parameter.
- */
- generateAccountHttpPassword() {
- return this._restApiHelper.send({
- method: 'PUT',
- url: '/accounts/self/password.http',
- body: {generate: true},
- parseResponse: true,
- reportUrlAsIs: true,
- });
- }
-
- getAccountSSHKeys() {
- return this._fetchSharedCacheURL({
- url: '/accounts/self/sshkeys',
- reportUrlAsIs: true,
- });
- }
-
- addAccountSSHKey(key) {
- const req = {
- method: 'POST',
- url: '/accounts/self/sshkeys',
- body: key,
- contentType: 'text/plain',
- reportUrlAsIs: true,
- };
- return this._restApiHelper.send(req)
- .then(response => {
- if (response.status < 200 && response.status >= 300) {
- return Promise.reject(new Error('error'));
- }
- return this.getResponseObject(response);
- })
- .then(obj => {
- if (!obj.valid) { return Promise.reject(new Error('error')); }
- return obj;
- });
- }
-
- deleteAccountSSHKey(id) {
- return this._restApiHelper.send({
- method: 'DELETE',
- url: '/accounts/self/sshkeys/' + id,
- anonymizedUrl: '/accounts/self/sshkeys/*',
- });
- }
-
- getAccountGPGKeys() {
- return this._restApiHelper.fetchJSON({
- url: '/accounts/self/gpgkeys',
- reportUrlAsIs: true,
- });
- }
-
- addAccountGPGKey(key) {
- const req = {
- method: 'POST',
- url: '/accounts/self/gpgkeys',
- body: key,
- reportUrlAsIs: true,
- };
- return this._restApiHelper.send(req)
- .then(response => {
- if (response.status < 200 && response.status >= 300) {
- return Promise.reject(new Error('error'));
- }
- return this.getResponseObject(response);
- })
- .then(obj => {
- if (!obj) { return Promise.reject(new Error('error')); }
- return obj;
- });
- }
-
- deleteAccountGPGKey(id) {
- return this._restApiHelper.send({
- method: 'DELETE',
- url: '/accounts/self/gpgkeys/' + id,
- anonymizedUrl: '/accounts/self/gpgkeys/*',
- });
- }
-
- deleteVote(changeNum, account, label) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'DELETE',
- endpoint: `/reviewers/${account}/votes/${encodeURIComponent(label)}`,
- anonymizedEndpoint: '/reviewers/*/votes/*',
- });
- }
-
- setDescription(changeNum, patchNum, desc) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'PUT', patchNum,
- endpoint: '/description',
- body: {description: desc},
- reportUrlAsIs: true,
- });
- }
-
- confirmEmail(token) {
- const req = {
- method: 'PUT',
- url: '/config/server/email.confirm',
- body: {token},
- reportUrlAsIs: true,
- };
- return this._restApiHelper.send(req).then(response => {
- if (response.status === 204) {
- return 'Email confirmed successfully.';
- }
- return null;
- });
- }
-
- getCapabilities(opt_errFn) {
- return this._restApiHelper.fetchJSON({
- url: '/config/server/capabilities',
- errFn: opt_errFn,
- reportUrlAsIs: true,
- });
- }
-
- getTopMenus(opt_errFn) {
- return this._fetchSharedCacheURL({
- url: '/config/server/top-menus',
- errFn: opt_errFn,
- reportUrlAsIs: true,
- });
- }
-
- setAssignee(changeNum, assignee) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'PUT',
- endpoint: '/assignee',
- body: {assignee},
- reportUrlAsIs: true,
- });
- }
-
- deleteAssignee(changeNum) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'DELETE',
- endpoint: '/assignee',
- reportUrlAsIs: true,
- });
- }
-
- probePath(path) {
- return fetch(new Request(path, {method: 'HEAD'}))
- .then(response => response.ok);
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string=} opt_message
- */
- startWorkInProgress(changeNum, opt_message) {
- const body = {};
- if (opt_message) {
- body.message = opt_message;
- }
- const req = {
- changeNum,
- method: 'POST',
- endpoint: '/wip',
- body,
- reportUrlAsIs: true,
- };
- return this._getChangeURLAndSend(req).then(response => {
- if (response.status === 204) {
- return 'Change marked as Work In Progress.';
- }
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string=} opt_body
- * @param {function(?Response, string=)=} opt_errFn
- */
- startReview(changeNum, opt_body, opt_errFn) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'POST',
- endpoint: '/ready',
- body: opt_body,
- errFn: opt_errFn,
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @suppress {checkTypes}
- * Resulted in error: Promise.prototype.then does not match formal
- * parameter.
- */
- deleteComment(changeNum, patchNum, commentID, reason) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'POST',
- patchNum,
- endpoint: `/comments/${commentID}/delete`,
- body: {reason},
- parseResponse: true,
- anonymizedEndpoint: '/comments/*/delete',
- });
- }
-
- /**
- * Given a changeNum, gets the change.
- *
- * @param {number|string} changeNum
- * @param {function(?Response, string=)=} opt_errFn
- * @return {!Promise<?Object>} The change
- */
- getChange(changeNum, opt_errFn) {
- // Cannot use _changeBaseURL, as this function is used by _projectLookup.
- return this._restApiHelper.fetchJSON({
- url: `/changes/?q=change:${changeNum}`,
- errFn: opt_errFn,
- anonymizedUrl: '/changes/?q=change:*',
- }).then(res => {
- if (!res || !res.length) { return null; }
- return res[0];
- });
- }
-
- /**
- * @param {string|number} changeNum
- * @param {string=} project
- */
- setInProjectLookup(changeNum, project) {
- if (this._projectLookup[changeNum] &&
- this._projectLookup[changeNum] !== project) {
- console.warn('Change set with multiple project nums.' +
- 'One of them must be invalid.');
- }
- this._projectLookup[changeNum] = project;
- }
-
- /**
- * Checks in _projectLookup for the changeNum. If it exists, returns the
- * project. If not, calls the restAPI to get the change, populates
- * _projectLookup with the project for that change, and returns the project.
- *
- * @param {string|number} changeNum
- * @return {!Promise<string|undefined>}
- */
- getFromProjectLookup(changeNum) {
- const project = this._projectLookup[changeNum];
- if (project) { return Promise.resolve(project); }
-
- const onError = response => {
- // Fire a page error so that the visual 404 is displayed.
- this.fire('page-error', {response});
- };
-
- return this.getChange(changeNum, onError).then(change => {
- if (!change || !change.project) { return; }
- this.setInProjectLookup(changeNum, change.project);
- return change.project;
- });
- }
-
- /**
- * Alias for _changeBaseURL.then(send).
- *
- * @todo(beckysiegel) clean up comments
- * @param {Gerrit.ChangeSendRequest} req
- * @return {!Promise<!Object>}
- */
- _getChangeURLAndSend(req) {
- const anonymizedBaseUrl = req.patchNum ?
- ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL;
- const anonymizedEndpoint = req.reportEndpointAsIs ?
- req.endpoint : req.anonymizedEndpoint;
-
- return this._changeBaseURL(req.changeNum, req.patchNum)
- .then(url => this._restApiHelper.send({
- method: req.method,
- url: url + req.endpoint,
- body: req.body,
- errFn: req.errFn,
- contentType: req.contentType,
- headers: req.headers,
- parseResponse: req.parseResponse,
- anonymizedUrl: anonymizedEndpoint ?
- (anonymizedBaseUrl + anonymizedEndpoint) : undefined,
- }));
- }
-
- /**
- * Alias for _changeBaseURL.then(_fetchJSON).
- *
- * @param {Gerrit.ChangeFetchRequest} req
- * @return {!Promise<!Object>}
- */
- _getChangeURLAndFetch(req, noAcceptHeader) {
- const anonymizedEndpoint = req.reportEndpointAsIs ?
- req.endpoint : req.anonymizedEndpoint;
- const anonymizedBaseUrl = req.patchNum ?
- ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL;
- return this._changeBaseURL(req.changeNum, req.patchNum)
- .then(url => this._restApiHelper.fetchJSON({
- url: url + req.endpoint,
- errFn: req.errFn,
- params: req.params,
- fetchOptions: req.fetchOptions,
- anonymizedUrl: anonymizedEndpoint ?
- (anonymizedBaseUrl + anonymizedEndpoint) : undefined,
- }, noAcceptHeader));
- }
-
- /**
- * Execute a change action or revision action on a change.
- *
- * @param {number} changeNum
- * @param {string} method
- * @param {string} endpoint
- * @param {string|number|undefined} opt_patchNum
- * @param {Object=} opt_payload
- * @param {?function(?Response, string=)=} opt_errFn
- * @return {Promise}
- */
- executeChangeAction(changeNum, method, endpoint, opt_patchNum, opt_payload,
- opt_errFn) {
- return this._getChangeURLAndSend({
- changeNum,
- method,
- patchNum: opt_patchNum,
- endpoint,
- body: opt_payload,
- errFn: opt_errFn,
- });
- }
-
- /**
- * Get blame information for the given diff.
- *
- * @param {string|number} changeNum
- * @param {string|number} patchNum
- * @param {string} path
- * @param {boolean=} opt_base If true, requests blame for the base of the
- * diff, rather than the revision.
- * @return {!Promise<!Object>}
- */
- getBlame(changeNum, patchNum, path, opt_base) {
- const encodedPath = encodeURIComponent(path);
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: `/files/${encodedPath}/blame`,
- patchNum,
- params: opt_base ? {base: 't'} : undefined,
- anonymizedEndpoint: '/files/*/blame',
- });
- }
-
- /**
- * Modify the given create draft request promise so that it fails and throws
- * an error if the response bears HTTP status 200 instead of HTTP 201.
- *
- * @see Issue 7763
- * @param {Promise} promise The original promise.
- * @return {Promise} The modified promise.
- */
- _failForCreate200(promise) {
- return promise.then(result => {
- if (result.status === 200) {
- // Read the response headers into an object representation.
- const headers = Array.from(result.headers.entries())
- .reduce((obj, [key, val]) => {
- if (!HEADER_REPORTING_BLACKLIST.test(key)) {
- obj[key] = val;
- }
- return obj;
- }, {});
- const err = new Error([
- CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE,
- JSON.stringify(headers),
- ].join('\n'));
- // Throw the error so that it is caught by gr-reporting.
- throw err;
- }
- return result;
- });
- }
-
- /**
- * Fetch a project dashboard definition.
- * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-dashboard
- *
- * @param {string} project
- * @param {string} dashboard
- * @param {function(?Response, string=)=} opt_errFn
- * passed as null sometimes.
- * @return {!Promise<!Object>}
- */
- getDashboard(project, dashboard, opt_errFn) {
- const url = '/projects/' + encodeURIComponent(project) + '/dashboards/' +
- encodeURIComponent(dashboard);
- return this._fetchSharedCacheURL({
- url,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/dashboards/*',
- });
- }
-
- /**
- * @param {string} filter
- * @return {!Promise<?Object>}
- */
- getDocumentationSearches(filter) {
- filter = filter.trim();
- const encodedFilter = encodeURIComponent(filter);
-
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._fetchSharedCacheURL({
- url: `/Documentation/?q=${encodedFilter}`,
- anonymizedUrl: '/Documentation/?*',
- });
- }
-
- getMergeable(changeNum) {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/revisions/current/mergeable',
- parseResponse: true,
- reportEndpointAsIs: true,
- });
- }
-
- deleteDraftComments(query) {
- return this._restApiHelper.send({
- method: 'POST',
- url: '/accounts/self/drafts:delete',
- body: {query},
- });
+ /**
+ * @param {?Object} obj
+ */
+ _updateCachedAccount(obj) {
+ // If result of getAccount is in cache, update it in the cache
+ // so we don't have to invalidate it.
+ const cachedAccount = this._cache.get('/accounts/self/detail');
+ if (cachedAccount) {
+ // Replace object in cache with new object to force UI updates.
+ this._cache.set('/accounts/self/detail',
+ Object.assign({}, cachedAccount, obj));
}
}
- customElements.define(GrRestApiInterface.is, GrRestApiInterface);
-})();
+ /**
+ * @param {string} name
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ setAccountName(name, opt_errFn) {
+ const req = {
+ method: 'PUT',
+ url: '/accounts/self/name',
+ body: {name},
+ errFn: opt_errFn,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ };
+ return this._restApiHelper.send(req)
+ .then(newName => this._updateCachedAccount({name: newName}));
+ }
+
+ /**
+ * @param {string} username
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ setAccountUsername(username, opt_errFn) {
+ const req = {
+ method: 'PUT',
+ url: '/accounts/self/username',
+ body: {username},
+ errFn: opt_errFn,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ };
+ return this._restApiHelper.send(req)
+ .then(newName => this._updateCachedAccount({username: newName}));
+ }
+
+ /**
+ * @param {string} displayName
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ setAccountDisplayName(displayName, opt_errFn) {
+ const req = {
+ method: 'PUT',
+ url: '/accounts/self/displayname',
+ body: {display_name: displayName},
+ errFn: opt_errFn,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ };
+ return this._restApiHelper.send(req)
+ .then(newName => this._updateCachedAccount({displayName: newName}));
+ }
+
+ /**
+ * @param {string} status
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ setAccountStatus(status, opt_errFn) {
+ const req = {
+ method: 'PUT',
+ url: '/accounts/self/status',
+ body: {status},
+ errFn: opt_errFn,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ };
+ return this._restApiHelper.send(req)
+ .then(newStatus => this._updateCachedAccount({status: newStatus}));
+ }
+
+ getAccountStatus(userId) {
+ return this._restApiHelper.fetchJSON({
+ url: `/accounts/${encodeURIComponent(userId)}/status`,
+ anonymizedUrl: '/accounts/*/status',
+ });
+ }
+
+ getAccountGroups() {
+ return this._restApiHelper.fetchJSON({
+ url: '/accounts/self/groups',
+ reportUrlAsIs: true,
+ });
+ }
+
+ getAccountAgreements() {
+ return this._restApiHelper.fetchJSON({
+ url: '/accounts/self/agreements',
+ reportUrlAsIs: true,
+ });
+ }
+
+ saveAccountAgreement(name) {
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: '/accounts/self/agreements',
+ body: name,
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @param {string=} opt_params
+ */
+ getAccountCapabilities(opt_params) {
+ let queryString = '';
+ if (opt_params) {
+ queryString = '?q=' + opt_params
+ .map(param => encodeURIComponent(param))
+ .join('&q=');
+ }
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/capabilities' + queryString,
+ anonymizedUrl: '/accounts/self/capabilities?q=*',
+ });
+ }
+
+ getLoggedIn() {
+ return this._auth.authCheck();
+ }
+
+ getIsAdmin() {
+ return this.getLoggedIn()
+ .then(isLoggedIn => {
+ if (isLoggedIn) {
+ return this.getAccountCapabilities();
+ } else {
+ return Promise.resolve();
+ }
+ })
+ .then(
+ capabilities => capabilities && capabilities.administrateServer
+ );
+ }
+
+ getDefaultPreferences() {
+ return this._fetchSharedCacheURL({
+ url: '/config/server/preferences',
+ reportUrlAsIs: true,
+ });
+ }
+
+ getPreferences() {
+ return this.getLoggedIn().then(loggedIn => {
+ if (loggedIn) {
+ const req = {url: '/accounts/self/preferences', reportUrlAsIs: true};
+ return this._fetchSharedCacheURL(req).then(res => {
+ if (this._isNarrowScreen()) {
+ // Note that this can be problematic, because the diff will stay
+ // unified even after increasing the window width.
+ res.default_diff_view = DiffViewMode.UNIFIED;
+ } else {
+ res.default_diff_view = res.diff_view;
+ }
+ return Promise.resolve(res);
+ });
+ }
+
+ return Promise.resolve({
+ changes_per_page: 25,
+ default_diff_view: this._isNarrowScreen() ?
+ DiffViewMode.UNIFIED : DiffViewMode.SIDE_BY_SIDE,
+ diff_view: 'SIDE_BY_SIDE',
+ size_bar_in_change_table: true,
+ });
+ });
+ }
+
+ getWatchedProjects() {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/watched.projects',
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @param {string} projects
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ saveWatchedProjects(projects, opt_errFn) {
+ return this._restApiHelper.send({
+ method: 'POST',
+ url: '/accounts/self/watched.projects',
+ body: projects,
+ errFn: opt_errFn,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @param {string} projects
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ deleteWatchedProjects(projects, opt_errFn) {
+ return this._restApiHelper.send({
+ method: 'POST',
+ url: '/accounts/self/watched.projects:delete',
+ body: projects,
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
+ }
+
+ _isNarrowScreen() {
+ return window.innerWidth < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX;
+ }
+
+ /**
+ * @param {number=} opt_changesPerPage
+ * @param {string|!Array<string>=} opt_query A query or an array of queries.
+ * @param {number|string=} opt_offset
+ * @param {!Object=} opt_options
+ * @return {?Array<!Object>|?Array<!Array<!Object>>} If opt_query is an
+ * array, _fetchJSON will return an array of arrays of changeInfos. If it
+ * is unspecified or a string, _fetchJSON will return an array of
+ * changeInfos.
+ */
+ getChanges(opt_changesPerPage, opt_query, opt_offset, opt_options) {
+ const options = opt_options || this.listChangesOptionsToHex(
+ this.ListChangesOption.LABELS,
+ this.ListChangesOption.DETAILED_ACCOUNTS
+ );
+ // Issue 4524: respect legacy token with max sortkey.
+ if (opt_offset === 'n,z') {
+ opt_offset = 0;
+ }
+ const params = {
+ O: options,
+ S: opt_offset || 0,
+ };
+ if (opt_changesPerPage) { params.n = opt_changesPerPage; }
+ if (opt_query && opt_query.length > 0) {
+ params.q = opt_query;
+ }
+ const iterateOverChanges = arr => {
+ for (const change of (arr || [])) {
+ this._maybeInsertInLookup(change);
+ }
+ };
+ const req = {
+ url: '/changes/',
+ params,
+ reportUrlAsIs: true,
+ };
+ return this._restApiHelper.fetchJSON(req).then(response => {
+ // Response may be an array of changes OR an array of arrays of
+ // changes.
+ if (opt_query instanceof Array) {
+ // Normalize the response to look like a multi-query response
+ // when there is only one query.
+ if (opt_query.length === 1) {
+ response = [response];
+ }
+ for (const arr of response) {
+ iterateOverChanges(arr);
+ }
+ } else {
+ iterateOverChanges(response);
+ }
+ return response;
+ });
+ }
+
+ /**
+ * Inserts a change into _projectLookup iff it has a valid structure.
+ *
+ * @param {?{ _number: (number|string) }} change
+ */
+ _maybeInsertInLookup(change) {
+ if (change && change.project && change._number) {
+ this.setInProjectLookup(change._number, change.project);
+ }
+ }
+
+ /**
+ * TODO (beckysiegel) this needs to be rewritten with the optional param
+ * at the end.
+ *
+ * @param {number|string} changeNum
+ * @param {?number|string=} opt_patchNum passed as null sometimes.
+ * @param {?=} endpoint
+ * @return {!Promise<string>}
+ */
+ getChangeActionURL(changeNum, opt_patchNum, endpoint) {
+ return this._changeBaseURL(changeNum, opt_patchNum)
+ .then(url => url + endpoint);
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {function(?Response, string=)=} opt_errFn
+ * @param {function()=} opt_cancelCondition
+ */
+ getChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
+ return this.getConfig(false).then(config => {
+ const optionsHex = this._getChangeOptionsHex(config);
+ return this._getChangeDetail(
+ changeNum, optionsHex, opt_errFn, opt_cancelCondition)
+ .then(GrReviewerUpdatesParser.parse);
+ });
+ }
+
+ _getChangeOptionsHex(config) {
+ if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.changePage
+ && !(config.receive && config.receive.enable_signed_push)) {
+ return window.DEFAULT_DETAIL_HEXES.changePage;
+ }
+
+ // This list MUST be kept in sync with
+ // ChangeIT#changeDetailsDoesNotRequireIndex
+ const options = [
+ this.ListChangesOption.ALL_COMMITS,
+ this.ListChangesOption.ALL_REVISIONS,
+ this.ListChangesOption.CHANGE_ACTIONS,
+ this.ListChangesOption.DETAILED_LABELS,
+ this.ListChangesOption.DOWNLOAD_COMMANDS,
+ this.ListChangesOption.MESSAGES,
+ this.ListChangesOption.SUBMITTABLE,
+ this.ListChangesOption.WEB_LINKS,
+ this.ListChangesOption.SKIP_DIFFSTAT,
+ ];
+ if (config.receive && config.receive.enable_signed_push) {
+ options.push(this.ListChangesOption.PUSH_CERTIFICATES);
+ }
+ return this.listChangesOptionsToHex(...options);
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {function(?Response, string=)=} opt_errFn
+ * @param {function()=} opt_cancelCondition
+ */
+ getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
+ let optionsHex = '';
+ if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.diffPage) {
+ optionsHex = window.DEFAULT_DETAIL_HEXES.diffPage;
+ } else {
+ optionsHex = this.listChangesOptionsToHex(
+ this.ListChangesOption.ALL_COMMITS,
+ this.ListChangesOption.ALL_REVISIONS,
+ this.ListChangesOption.SKIP_DIFFSTAT
+ );
+ }
+ return this._getChangeDetail(changeNum, optionsHex, opt_errFn,
+ opt_cancelCondition);
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {string|undefined} optionsHex list changes options in hex
+ * @param {function(?Response, string=)=} opt_errFn
+ * @param {function()=} opt_cancelCondition
+ */
+ _getChangeDetail(changeNum, optionsHex, opt_errFn, opt_cancelCondition) {
+ return this.getChangeActionURL(changeNum, null, '/detail').then(url => {
+ const urlWithParams = this._restApiHelper
+ .urlWithParams(url, optionsHex);
+ const params = {O: optionsHex};
+ const req = {
+ url,
+ errFn: opt_errFn,
+ cancelCondition: opt_cancelCondition,
+ params,
+ fetchOptions: this._etags.getOptions(urlWithParams),
+ anonymizedUrl: '/changes/*~*/detail?O=' + optionsHex,
+ };
+ return this._restApiHelper.fetchRawJSON(req).then(response => {
+ if (response && response.status === 304) {
+ return Promise.resolve(this._restApiHelper.parsePrefixedJSON(
+ this._etags.getCachedPayload(urlWithParams)));
+ }
+
+ if (response && !response.ok) {
+ if (opt_errFn) {
+ opt_errFn.call(null, response);
+ } else {
+ this.fire('server-error', {request: req, response});
+ }
+ return;
+ }
+
+ const payloadPromise = response ?
+ this._restApiHelper.readResponsePayload(response) :
+ Promise.resolve(null);
+
+ return payloadPromise.then(payload => {
+ if (!payload) { return null; }
+ this._etags.collect(urlWithParams, response, payload.raw);
+ this._maybeInsertInLookup(payload.parsed);
+
+ return payload.parsed;
+ });
+ });
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string} patchNum
+ */
+ getChangeCommitInfo(changeNum, patchNum) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/commit?links',
+ patchNum,
+ reportEndpointAsIs: true,
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {Gerrit.PatchRange} patchRange
+ * @param {number=} opt_parentIndex
+ */
+ getChangeFiles(changeNum, patchRange, opt_parentIndex) {
+ let params = undefined;
+ if (this.isMergeParent(patchRange.basePatchNum)) {
+ params = {parent: this.getParentIndex(patchRange.basePatchNum)};
+ } else if (!this.patchNumEquals(patchRange.basePatchNum, 'PARENT')) {
+ params = {base: patchRange.basePatchNum};
+ }
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/files',
+ patchNum: patchRange.patchNum,
+ params,
+ reportEndpointAsIs: true,
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {Gerrit.PatchRange} patchRange
+ */
+ getChangeEditFiles(changeNum, patchRange) {
+ let endpoint = '/edit?list';
+ let anonymizedEndpoint = endpoint;
+ if (patchRange.basePatchNum !== 'PARENT') {
+ endpoint += '&base=' + encodeURIComponent(patchRange.basePatchNum + '');
+ anonymizedEndpoint += '&base=*';
+ }
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint,
+ anonymizedEndpoint,
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string} patchNum
+ * @param {string} query
+ * @return {!Promise<!Object>}
+ */
+ queryChangeFiles(changeNum, patchNum, query) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: `/files?q=${encodeURIComponent(query)}`,
+ patchNum,
+ anonymizedEndpoint: '/files?q=*',
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {Gerrit.PatchRange} patchRange
+ * @return {!Promise<!Array<!Object>>}
+ */
+ getChangeOrEditFiles(changeNum, patchRange) {
+ if (this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME)) {
+ return this.getChangeEditFiles(changeNum, patchRange).then(res =>
+ res.files);
+ }
+ return this.getChangeFiles(changeNum, patchRange);
+ }
+
+ getChangeRevisionActions(changeNum, patchNum) {
+ const req = {
+ changeNum,
+ endpoint: '/actions',
+ patchNum,
+ reportEndpointAsIs: true,
+ };
+ return this._getChangeURLAndFetch(req);
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {string} inputVal
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ getChangeSuggestedReviewers(changeNum, inputVal, opt_errFn) {
+ return this._getChangeSuggestedGroup('REVIEWER', changeNum, inputVal,
+ opt_errFn);
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {string} inputVal
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ getChangeSuggestedCCs(changeNum, inputVal, opt_errFn) {
+ return this._getChangeSuggestedGroup('CC', changeNum, inputVal,
+ opt_errFn);
+ }
+
+ _getChangeSuggestedGroup(reviewerState, changeNum, inputVal, opt_errFn) {
+ // More suggestions may obscure content underneath in the reply dialog,
+ // see issue 10793.
+ const params = {'n': 6, 'reviewer-state': reviewerState};
+ if (inputVal) { params.q = inputVal; }
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/suggest_reviewers',
+ errFn: opt_errFn,
+ params,
+ reportEndpointAsIs: true,
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ */
+ getChangeIncludedIn(changeNum) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/in',
+ reportEndpointAsIs: true,
+ });
+ }
+
+ _computeFilter(filter) {
+ if (filter && filter.startsWith('^')) {
+ filter = '&r=' + encodeURIComponent(filter);
+ } else if (filter) {
+ filter = '&m=' + encodeURIComponent(filter);
+ } else {
+ filter = '';
+ }
+ return filter;
+ }
+
+ /**
+ * @param {string} filter
+ * @param {number} groupsPerPage
+ * @param {number=} opt_offset
+ */
+ _getGroupsUrl(filter, groupsPerPage, opt_offset) {
+ const offset = opt_offset || 0;
+
+ return `/groups/?n=${groupsPerPage + 1}&S=${offset}` +
+ this._computeFilter(filter);
+ }
+
+ /**
+ * @param {string} filter
+ * @param {number} reposPerPage
+ * @param {number=} opt_offset
+ */
+ _getReposUrl(filter, reposPerPage, opt_offset) {
+ const defaultFilter = 'state:active OR state:read-only';
+ const namePartDelimiters = /[@.\-\s\/_]/g;
+ const offset = opt_offset || 0;
+
+ if (filter && !filter.includes(':') && filter.match(namePartDelimiters)) {
+ // The query language specifies hyphens as operators. Split the string
+ // by hyphens and 'AND' the parts together as 'inname:' queries.
+ // If the filter includes a semicolon, the user is using a more complex
+ // query so we trust them and don't do any magic under the hood.
+ const originalFilter = filter;
+ filter = '';
+ originalFilter.split(namePartDelimiters).forEach(part => {
+ if (part) {
+ filter += (filter === '' ? 'inname:' : ' AND inname:') + part;
+ }
+ });
+ }
+ // Check if filter is now empty which could be either because the user did
+ // not provide it or because the user provided only a split character.
+ if (!filter) {
+ filter = defaultFilter;
+ }
+
+ filter = filter.trim();
+ const encodedFilter = encodeURIComponent(filter);
+
+ return `/projects/?n=${reposPerPage + 1}&S=${offset}` +
+ `&query=${encodedFilter}`;
+ }
+
+ invalidateGroupsCache() {
+ this._restApiHelper.invalidateFetchPromisesPrefix('/groups/?');
+ }
+
+ invalidateReposCache() {
+ this._restApiHelper.invalidateFetchPromisesPrefix('/projects/?');
+ }
+
+ invalidateAccountsCache() {
+ this._restApiHelper.invalidateFetchPromisesPrefix('/accounts/');
+ }
+
+ /**
+ * @param {string} filter
+ * @param {number} groupsPerPage
+ * @param {number=} opt_offset
+ * @return {!Promise<?Object>}
+ */
+ getGroups(filter, groupsPerPage, opt_offset) {
+ const url = this._getGroupsUrl(filter, groupsPerPage, opt_offset);
+
+ return this._fetchSharedCacheURL({
+ url,
+ anonymizedUrl: '/groups/?*',
+ });
+ }
+
+ /**
+ * @param {string} filter
+ * @param {number} reposPerPage
+ * @param {number=} opt_offset
+ * @return {!Promise<?Object>}
+ */
+ getRepos(filter, reposPerPage, opt_offset) {
+ const url = this._getReposUrl(filter, reposPerPage, opt_offset);
+
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url,
+ anonymizedUrl: '/projects/?*',
+ });
+ }
+
+ setRepoHead(repo, ref) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/projects/${encodeURIComponent(repo)}/HEAD`,
+ body: {ref},
+ anonymizedUrl: '/projects/*/HEAD',
+ });
+ }
+
+ /**
+ * @param {string} filter
+ * @param {string} repo
+ * @param {number} reposBranchesPerPage
+ * @param {number=} opt_offset
+ * @param {?function(?Response, string=)=} opt_errFn
+ * @return {!Promise<?Object>}
+ */
+ getRepoBranches(filter, repo, reposBranchesPerPage, opt_offset, opt_errFn) {
+ const offset = opt_offset || 0;
+ const count = reposBranchesPerPage + 1;
+ filter = this._computeFilter(filter);
+ repo = encodeURIComponent(repo);
+ const url = `/projects/${repo}/branches?n=${count}&S=${offset}${filter}`;
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._restApiHelper.fetchJSON({
+ url,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/branches?*',
+ });
+ }
+
+ /**
+ * @param {string} filter
+ * @param {string} repo
+ * @param {number} reposTagsPerPage
+ * @param {number=} opt_offset
+ * @param {?function(?Response, string=)=} opt_errFn
+ * @return {!Promise<?Object>}
+ */
+ getRepoTags(filter, repo, reposTagsPerPage, opt_offset, opt_errFn) {
+ const offset = opt_offset || 0;
+ const encodedRepo = encodeURIComponent(repo);
+ const n = reposTagsPerPage + 1;
+ const encodedFilter = this._computeFilter(filter);
+ const url = `/projects/${encodedRepo}/tags` + `?n=${n}&S=${offset}` +
+ encodedFilter;
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._restApiHelper.fetchJSON({
+ url,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/tags',
+ });
+ }
+
+ /**
+ * @param {string} filter
+ * @param {number} pluginsPerPage
+ * @param {number=} opt_offset
+ * @param {?function(?Response, string=)=} opt_errFn
+ * @return {!Promise<?Object>}
+ */
+ getPlugins(filter, pluginsPerPage, opt_offset, opt_errFn) {
+ const offset = opt_offset || 0;
+ const encodedFilter = this._computeFilter(filter);
+ const n = pluginsPerPage + 1;
+ const url = `/plugins/?all&n=${n}&S=${offset}${encodedFilter}`;
+ return this._restApiHelper.fetchJSON({
+ url,
+ errFn: opt_errFn,
+ anonymizedUrl: '/plugins/?all',
+ });
+ }
+
+ getRepoAccessRights(repoName, opt_errFn) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._restApiHelper.fetchJSON({
+ url: `/projects/${encodeURIComponent(repoName)}/access`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/access',
+ });
+ }
+
+ setRepoAccessRights(repoName, repoInfo) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._restApiHelper.send({
+ method: 'POST',
+ url: `/projects/${encodeURIComponent(repoName)}/access`,
+ body: repoInfo,
+ anonymizedUrl: '/projects/*/access',
+ });
+ }
+
+ setRepoAccessRightsForReview(projectName, projectInfo) {
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/projects/${encodeURIComponent(projectName)}/access:review`,
+ body: projectInfo,
+ parseResponse: true,
+ anonymizedUrl: '/projects/*/access:review',
+ });
+ }
+
+ /**
+ * @param {string} inputVal
+ * @param {number} opt_n
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ getSuggestedGroups(inputVal, opt_n, opt_errFn) {
+ const params = {s: inputVal};
+ if (opt_n) { params.n = opt_n; }
+ return this._restApiHelper.fetchJSON({
+ url: '/groups/',
+ errFn: opt_errFn,
+ params,
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @param {string} inputVal
+ * @param {number} opt_n
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ getSuggestedProjects(inputVal, opt_n, opt_errFn) {
+ const params = {
+ m: inputVal,
+ n: MAX_PROJECT_RESULTS,
+ type: 'ALL',
+ };
+ if (opt_n) { params.n = opt_n; }
+ return this._restApiHelper.fetchJSON({
+ url: '/projects/',
+ errFn: opt_errFn,
+ params,
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @param {string} inputVal
+ * @param {number} opt_n
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ getSuggestedAccounts(inputVal, opt_n, opt_errFn) {
+ if (!inputVal) {
+ return Promise.resolve([]);
+ }
+ const params = {suggest: null, q: inputVal};
+ if (opt_n) { params.n = opt_n; }
+ return this._restApiHelper.fetchJSON({
+ url: '/accounts/',
+ errFn: opt_errFn,
+ params,
+ anonymizedUrl: '/accounts/?n=*',
+ });
+ }
+
+ addChangeReviewer(changeNum, reviewerID) {
+ return this._sendChangeReviewerRequest('POST', changeNum, reviewerID);
+ }
+
+ removeChangeReviewer(changeNum, reviewerID) {
+ return this._sendChangeReviewerRequest('DELETE', changeNum, reviewerID);
+ }
+
+ _sendChangeReviewerRequest(method, changeNum, reviewerID) {
+ return this.getChangeActionURL(changeNum, null, '/reviewers')
+ .then(url => {
+ let body;
+ switch (method) {
+ case 'POST':
+ body = {reviewer: reviewerID};
+ break;
+ case 'DELETE':
+ url += '/' + encodeURIComponent(reviewerID);
+ break;
+ default:
+ throw Error('Unsupported HTTP method: ' + method);
+ }
+
+ return this._restApiHelper.send({method, url, body});
+ });
+ }
+
+ getRelatedChanges(changeNum, patchNum) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/related',
+ patchNum,
+ reportEndpointAsIs: true,
+ });
+ }
+
+ getChangesSubmittedTogether(changeNum) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/submitted_together?o=NON_VISIBLE_CHANGES',
+ reportEndpointAsIs: true,
+ });
+ }
+
+ getChangeConflicts(changeNum) {
+ const options = this.listChangesOptionsToHex(
+ this.ListChangesOption.CURRENT_REVISION,
+ this.ListChangesOption.CURRENT_COMMIT
+ );
+ const params = {
+ O: options,
+ q: 'status:open conflicts:' + changeNum,
+ };
+ return this._restApiHelper.fetchJSON({
+ url: '/changes/',
+ params,
+ anonymizedUrl: '/changes/conflicts:*',
+ });
+ }
+
+ getChangeCherryPicks(project, changeID, changeNum) {
+ const options = this.listChangesOptionsToHex(
+ this.ListChangesOption.CURRENT_REVISION,
+ this.ListChangesOption.CURRENT_COMMIT
+ );
+ const query = [
+ 'project:' + project,
+ 'change:' + changeID,
+ '-change:' + changeNum,
+ '-is:abandoned',
+ ].join(' ');
+ const params = {
+ O: options,
+ q: query,
+ };
+ return this._restApiHelper.fetchJSON({
+ url: '/changes/',
+ params,
+ anonymizedUrl: '/changes/change:*',
+ });
+ }
+
+ getChangesWithSameTopic(topic, changeNum) {
+ const options = this.listChangesOptionsToHex(
+ this.ListChangesOption.LABELS,
+ this.ListChangesOption.CURRENT_REVISION,
+ this.ListChangesOption.CURRENT_COMMIT,
+ this.ListChangesOption.DETAILED_LABELS
+ );
+ const query = [
+ 'status:open',
+ '-change:' + changeNum,
+ `topic:"${topic}"`,
+ ].join(' ');
+ const params = {
+ O: options,
+ q: query,
+ };
+ return this._restApiHelper.fetchJSON({
+ url: '/changes/',
+ params,
+ anonymizedUrl: '/changes/topic:*',
+ });
+ }
+
+ getReviewedFiles(changeNum, patchNum) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/files?reviewed',
+ patchNum,
+ reportEndpointAsIs: true,
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string} patchNum
+ * @param {string} path
+ * @param {boolean} reviewed
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ saveFileReviewed(changeNum, patchNum, path, reviewed, opt_errFn) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: reviewed ? 'PUT' : 'DELETE',
+ patchNum,
+ endpoint: `/files/${encodeURIComponent(path)}/reviewed`,
+ errFn: opt_errFn,
+ anonymizedEndpoint: '/files/*/reviewed',
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string} patchNum
+ * @param {!Object} review
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ saveChangeReview(changeNum, patchNum, review, opt_errFn) {
+ const promises = [
+ this.awaitPendingDiffDrafts(),
+ this.getChangeActionURL(changeNum, patchNum, '/review'),
+ ];
+ return Promise.all(promises).then(([, url]) => this._restApiHelper.send({
+ method: 'POST',
+ url,
+ body: review,
+ errFn: opt_errFn,
+ }));
+ }
+
+ getChangeEdit(changeNum, opt_download_commands) {
+ const params = opt_download_commands ? {'download-commands': true} : null;
+ return this.getLoggedIn().then(loggedIn => {
+ if (!loggedIn) { return false; }
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/edit/',
+ params,
+ reportEndpointAsIs: true,
+ }, true);
+ });
+ }
+
+ /**
+ * @param {string} project
+ * @param {string} branch
+ * @param {string} subject
+ * @param {string=} opt_topic
+ * @param {boolean=} opt_isPrivate
+ * @param {boolean=} opt_workInProgress
+ * @param {string=} opt_baseChange
+ * @param {string=} opt_baseCommit
+ */
+ createChange(project, branch, subject, opt_topic, opt_isPrivate,
+ opt_workInProgress, opt_baseChange, opt_baseCommit) {
+ return this._restApiHelper.send({
+ method: 'POST',
+ url: '/changes/',
+ body: {
+ project,
+ branch,
+ subject,
+ topic: opt_topic,
+ is_private: opt_isPrivate,
+ work_in_progress: opt_workInProgress,
+ base_change: opt_baseChange,
+ base_commit: opt_baseCommit,
+ },
+ parseResponse: true,
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {string} path
+ * @param {number|string} patchNum
+ */
+ getFileContent(changeNum, path, patchNum) {
+ // 404s indicate the file does not exist yet in the revision, so suppress
+ // them.
+ const suppress404s = res => {
+ if (res && res.status !== 404) { this.fire('server-error', {res}); }
+ return res;
+ };
+ const promise = this.patchNumEquals(patchNum, this.EDIT_NAME) ?
+ this._getFileInChangeEdit(changeNum, path) :
+ this._getFileInRevision(changeNum, path, patchNum, suppress404s);
+
+ return promise.then(res => {
+ if (!res.ok) { return res; }
+
+ // The file type (used for syntax highlighting) is identified in the
+ // X-FYI-Content-Type header of the response.
+ const type = res.headers.get('X-FYI-Content-Type');
+ return this.getResponseObject(res).then(content => {
+ return {content, type, ok: true};
+ });
+ });
+ }
+
+ /**
+ * Gets a file in a specific change and revision.
+ *
+ * @param {number|string} changeNum
+ * @param {string} path
+ * @param {number|string} patchNum
+ * @param {?function(?Response, string=)=} opt_errFn
+ */
+ _getFileInRevision(changeNum, path, patchNum, opt_errFn) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'GET',
+ patchNum,
+ endpoint: `/files/${encodeURIComponent(path)}/content`,
+ errFn: opt_errFn,
+ headers: {Accept: 'application/json'},
+ anonymizedEndpoint: '/files/*/content',
+ });
+ }
+
+ /**
+ * Gets a file in a change edit.
+ *
+ * @param {number|string} changeNum
+ * @param {string} path
+ */
+ _getFileInChangeEdit(changeNum, path) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'GET',
+ endpoint: '/edit/' + encodeURIComponent(path),
+ headers: {Accept: 'application/json'},
+ anonymizedEndpoint: '/edit/*',
+ });
+ }
+
+ rebaseChangeEdit(changeNum) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/edit:rebase',
+ reportEndpointAsIs: true,
+ });
+ }
+
+ deleteChangeEdit(changeNum) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'DELETE',
+ endpoint: '/edit',
+ reportEndpointAsIs: true,
+ });
+ }
+
+ restoreFileInChangeEdit(changeNum, restore_path) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/edit',
+ body: {restore_path},
+ reportEndpointAsIs: true,
+ });
+ }
+
+ renameFileInChangeEdit(changeNum, old_path, new_path) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/edit',
+ body: {old_path, new_path},
+ reportEndpointAsIs: true,
+ });
+ }
+
+ deleteFileInChangeEdit(changeNum, path) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'DELETE',
+ endpoint: '/edit/' + encodeURIComponent(path),
+ anonymizedEndpoint: '/edit/*',
+ });
+ }
+
+ saveChangeEdit(changeNum, path, contents) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: '/edit/' + encodeURIComponent(path),
+ body: contents,
+ contentType: 'text/plain',
+ anonymizedEndpoint: '/edit/*',
+ });
+ }
+
+ getRobotCommentFixPreview(changeNum, patchNum, fixId) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ patchNum,
+ endpoint: `/fixes/${encodeURIComponent(fixId)}/preview`,
+ reportEndpointAsId: true,
+ });
+ }
+
+ applyFixSuggestion(changeNum, patchNum, fixId) {
+ return this._getChangeURLAndSend({
+ method: 'POST',
+ changeNum,
+ patchNum,
+ endpoint: `/fixes/${encodeURIComponent(fixId)}/apply`,
+ reportEndpointAsId: true,
+ });
+ }
+
+ // Deprecated, prefer to use putChangeCommitMessage instead.
+ saveChangeCommitMessageEdit(changeNum, message) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: '/edit:message',
+ body: {message},
+ reportEndpointAsIs: true,
+ });
+ }
+
+ publishChangeEdit(changeNum) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/edit:publish',
+ reportEndpointAsIs: true,
+ });
+ }
+
+ putChangeCommitMessage(changeNum, message) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: '/message',
+ body: {message},
+ reportEndpointAsIs: true,
+ });
+ }
+
+ deleteChangeCommitMessage(changeNum, messageId) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'DELETE',
+ endpoint: '/messages/' + messageId,
+ reportEndpointAsIs: true,
+ });
+ }
+
+ saveChangeStarred(changeNum, starred) {
+ // Some servers may require the project name to be provided
+ // alongside the change number, so resolve the project name
+ // first.
+ return this.getFromProjectLookup(changeNum).then(project => {
+ const url = '/accounts/self/starred.changes/' +
+ (project ? encodeURIComponent(project) + '~' : '') + changeNum;
+ return this._restApiHelper.send({
+ method: starred ? 'PUT' : 'DELETE',
+ url,
+ anonymizedUrl: '/accounts/self/starred.changes/*',
+ });
+ });
+ }
+
+ saveChangeReviewed(changeNum, reviewed) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: reviewed ? '/reviewed' : '/unreviewed',
+ });
+ }
+
+ /**
+ * Public version of the _restApiHelper.send method preserved for plugins.
+ *
+ * @param {string} method
+ * @param {string} url
+ * @param {?string|number|Object=} opt_body passed as null sometimes
+ * and also apparently a number. TODO (beckysiegel) remove need for
+ * number at least.
+ * @param {?function(?Response, string=)=} opt_errFn
+ * passed as null sometimes.
+ * @param {?string=} opt_contentType
+ * @param {Object=} opt_headers
+ */
+ send(method, url, opt_body, opt_errFn, opt_contentType,
+ opt_headers) {
+ return this._restApiHelper.send({
+ method,
+ url,
+ body: opt_body,
+ errFn: opt_errFn,
+ contentType: opt_contentType,
+ headers: opt_headers,
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string} basePatchNum Negative values specify merge parent
+ * index.
+ * @param {number|string} patchNum
+ * @param {string} path
+ * @param {string=} opt_whitespace the ignore-whitespace level for the diff
+ * algorithm.
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ getDiff(changeNum, basePatchNum, patchNum, path, opt_whitespace,
+ opt_errFn) {
+ const params = {
+ context: 'ALL',
+ intraline: null,
+ whitespace: opt_whitespace || 'IGNORE_NONE',
+ };
+ if (this.isMergeParent(basePatchNum)) {
+ params.parent = this.getParentIndex(basePatchNum);
+ } else if (!this.patchNumEquals(basePatchNum, PARENT_PATCH_NUM)) {
+ params.base = basePatchNum;
+ }
+ const endpoint = `/files/${encodeURIComponent(path)}/diff`;
+ const req = {
+ changeNum,
+ endpoint,
+ patchNum,
+ errFn: opt_errFn,
+ params,
+ anonymizedEndpoint: '/files/*/diff',
+ };
+
+ // Invalidate the cache if its edit patch to make sure we always get latest.
+ if (patchNum === this.EDIT_NAME) {
+ if (!req.fetchOptions) req.fetchOptions = {};
+ if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers();
+ req.fetchOptions.headers.append('Cache-Control', 'no-cache');
+ }
+
+ return this._getChangeURLAndFetch(req);
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string=} opt_basePatchNum
+ * @param {number|string=} opt_patchNum
+ * @param {string=} opt_path
+ * @return {!Promise<!Object>}
+ */
+ getDiffComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
+ return this._getDiffComments(changeNum, '/comments', opt_basePatchNum,
+ opt_patchNum, opt_path);
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string=} opt_basePatchNum
+ * @param {number|string=} opt_patchNum
+ * @param {string=} opt_path
+ * @return {!Promise<!Object>}
+ */
+ getDiffRobotComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
+ return this._getDiffComments(changeNum, '/robotcomments',
+ opt_basePatchNum, opt_patchNum, opt_path);
+ }
+
+ /**
+ * If the user is logged in, fetch the user's draft diff comments. If there
+ * is no logged in user, the request is not made and the promise yields an
+ * empty object.
+ *
+ * @param {number|string} changeNum
+ * @param {number|string=} opt_basePatchNum
+ * @param {number|string=} opt_patchNum
+ * @param {string=} opt_path
+ * @return {!Promise<!Object>}
+ */
+ getDiffDrafts(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
+ return this.getLoggedIn().then(loggedIn => {
+ if (!loggedIn) { return Promise.resolve({}); }
+ return this._getDiffComments(changeNum, '/drafts', opt_basePatchNum,
+ opt_patchNum, opt_path);
+ });
+ }
+
+ _setRange(comments, comment) {
+ if (comment.in_reply_to && !comment.range) {
+ for (let i = 0; i < comments.length; i++) {
+ if (comments[i].id === comment.in_reply_to) {
+ comment.range = comments[i].range;
+ break;
+ }
+ }
+ }
+ return comment;
+ }
+
+ _setRanges(comments) {
+ comments = comments || [];
+ comments.sort(
+ (a, b) => util.parseDate(a.updated) - util.parseDate(b.updated)
+ );
+ for (const comment of comments) {
+ this._setRange(comments, comment);
+ }
+ return comments;
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {string} endpoint
+ * @param {number|string=} opt_basePatchNum
+ * @param {number|string=} opt_patchNum
+ * @param {string=} opt_path
+ * @return {!Promise<!Object>}
+ */
+ _getDiffComments(changeNum, endpoint, opt_basePatchNum,
+ opt_patchNum, opt_path) {
+ /**
+ * Fetches the comments for a given patchNum.
+ * Helper function to make promises more legible.
+ *
+ * @param {string|number=} opt_patchNum
+ * @return {!Promise<!Object>} Diff comments response.
+ */
+ // We don't want to add accept header, since preloading of comments is
+ // working only without accept header.
+ const noAcceptHeader = true;
+ const fetchComments = opt_patchNum => this._getChangeURLAndFetch({
+ changeNum,
+ endpoint,
+ patchNum: opt_patchNum,
+ reportEndpointAsIs: true,
+ }, noAcceptHeader);
+
+ if (!opt_basePatchNum && !opt_patchNum && !opt_path) {
+ return fetchComments();
+ }
+ function onlyParent(c) { return c.side == PARENT_PATCH_NUM; }
+ function withoutParent(c) { return c.side != PARENT_PATCH_NUM; }
+ function setPath(c) { c.path = opt_path; }
+
+ const promises = [];
+ let comments;
+ let baseComments;
+ let fetchPromise;
+ fetchPromise = fetchComments(opt_patchNum).then(response => {
+ comments = response[opt_path] || [];
+ // TODO(kaspern): Implement this on in the backend so this can
+ // be removed.
+ // Sort comments by date so that parent ranges can be propagated
+ // in a single pass.
+ comments = this._setRanges(comments);
+
+ if (opt_basePatchNum == PARENT_PATCH_NUM) {
+ baseComments = comments.filter(onlyParent);
+ baseComments.forEach(setPath);
+ }
+ comments = comments.filter(withoutParent);
+
+ comments.forEach(setPath);
+ });
+ promises.push(fetchPromise);
+
+ if (opt_basePatchNum != PARENT_PATCH_NUM) {
+ fetchPromise = fetchComments(opt_basePatchNum).then(response => {
+ baseComments = (response[opt_path] || [])
+ .filter(withoutParent);
+ baseComments = this._setRanges(baseComments);
+ baseComments.forEach(setPath);
+ });
+ promises.push(fetchPromise);
+ }
+
+ return Promise.all(promises).then(() => Promise.resolve({
+ baseComments,
+ comments,
+ }));
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {string} endpoint
+ * @param {number|string=} opt_patchNum
+ */
+ _getDiffCommentsFetchURL(changeNum, endpoint, opt_patchNum) {
+ return this._changeBaseURL(changeNum, opt_patchNum)
+ .then(url => url + endpoint);
+ }
+
+ saveDiffDraft(changeNum, patchNum, draft) {
+ return this._sendDiffDraftRequest('PUT', changeNum, patchNum, draft);
+ }
+
+ deleteDiffDraft(changeNum, patchNum, draft) {
+ return this._sendDiffDraftRequest('DELETE', changeNum, patchNum, draft);
+ }
+
+ /**
+ * @returns {boolean} Whether there are pending diff draft sends.
+ */
+ hasPendingDiffDrafts() {
+ const promises = this._pendingRequests[Requests.SEND_DIFF_DRAFT];
+ return promises && promises.length;
+ }
+
+ /**
+ * @returns {!Promise<undefined>} A promise that resolves when all pending
+ * diff draft sends have resolved.
+ */
+ awaitPendingDiffDrafts() {
+ return Promise.all(this._pendingRequests[Requests.SEND_DIFF_DRAFT] || [])
+ .then(() => {
+ this._pendingRequests[Requests.SEND_DIFF_DRAFT] = [];
+ });
+ }
+
+ _sendDiffDraftRequest(method, changeNum, patchNum, draft) {
+ const isCreate = !draft.id && method === 'PUT';
+ let endpoint = '/drafts';
+ let anonymizedEndpoint = endpoint;
+ if (draft.id) {
+ endpoint += '/' + draft.id;
+ anonymizedEndpoint += '/*';
+ }
+ let body;
+ if (method === 'PUT') {
+ body = draft;
+ }
+
+ if (!this._pendingRequests[Requests.SEND_DIFF_DRAFT]) {
+ this._pendingRequests[Requests.SEND_DIFF_DRAFT] = [];
+ }
+
+ const req = {
+ changeNum,
+ method,
+ patchNum,
+ endpoint,
+ body,
+ anonymizedEndpoint,
+ };
+
+ const promise = this._getChangeURLAndSend(req);
+ this._pendingRequests[Requests.SEND_DIFF_DRAFT].push(promise);
+
+ if (isCreate) {
+ return this._failForCreate200(promise);
+ }
+
+ return promise;
+ }
+
+ getCommitInfo(project, commit) {
+ return this._restApiHelper.fetchJSON({
+ url: '/projects/' + encodeURIComponent(project) +
+ '/commits/' + encodeURIComponent(commit),
+ anonymizedUrl: '/projects/*/comments/*',
+ });
+ }
+
+ _fetchB64File(url) {
+ return this._restApiHelper.fetch({url: this.getBaseUrl() + url})
+ .then(response => {
+ if (!response.ok) {
+ return Promise.reject(new Error(response.statusText));
+ }
+ const type = response.headers.get('X-FYI-Content-Type');
+ return response.text()
+ .then(text => {
+ return {body: text, type};
+ });
+ });
+ }
+
+ /**
+ * @param {string} changeId
+ * @param {string|number} patchNum
+ * @param {string} path
+ * @param {number=} opt_parentIndex
+ */
+ getB64FileContents(changeId, patchNum, path, opt_parentIndex) {
+ const parent = typeof opt_parentIndex === 'number' ?
+ '?parent=' + opt_parentIndex : '';
+ return this._changeBaseURL(changeId, patchNum).then(url => {
+ url = `${url}/files/${encodeURIComponent(path)}/content${parent}`;
+ return this._fetchB64File(url);
+ });
+ }
+
+ getImagesForDiff(changeNum, diff, patchRange) {
+ let promiseA;
+ let promiseB;
+
+ if (diff.meta_a && diff.meta_a.content_type.startsWith('image/')) {
+ if (patchRange.basePatchNum === 'PARENT') {
+ // Note: we only attempt to get the image from the first parent.
+ promiseA = this.getB64FileContents(changeNum, patchRange.patchNum,
+ diff.meta_a.name, 1);
+ } else {
+ promiseA = this.getB64FileContents(changeNum,
+ patchRange.basePatchNum, diff.meta_a.name);
+ }
+ } else {
+ promiseA = Promise.resolve(null);
+ }
+
+ if (diff.meta_b && diff.meta_b.content_type.startsWith('image/')) {
+ promiseB = this.getB64FileContents(changeNum, patchRange.patchNum,
+ diff.meta_b.name);
+ } else {
+ promiseB = Promise.resolve(null);
+ }
+
+ return Promise.all([promiseA, promiseB]).then(results => {
+ const baseImage = results[0];
+ const revisionImage = results[1];
+
+ // Sometimes the server doesn't send back the content type.
+ if (baseImage) {
+ baseImage._expectedType = diff.meta_a.content_type;
+ baseImage._name = diff.meta_a.name;
+ }
+ if (revisionImage) {
+ revisionImage._expectedType = diff.meta_b.content_type;
+ revisionImage._name = diff.meta_b.name;
+ }
+
+ return {baseImage, revisionImage};
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {?number|string=} opt_patchNum passed as null sometimes.
+ * @param {string=} opt_project
+ * @return {!Promise<string>}
+ */
+ _changeBaseURL(changeNum, opt_patchNum, opt_project) {
+ // TODO(kaspern): For full slicer migration, app should warn with a call
+ // stack every time _changeBaseURL is called without a project.
+ const projectPromise = opt_project ?
+ Promise.resolve(opt_project) :
+ this.getFromProjectLookup(changeNum);
+ return projectPromise.then(project => {
+ let url = `/changes/${encodeURIComponent(project)}~${changeNum}`;
+ if (opt_patchNum) {
+ url += `/revisions/${opt_patchNum}`;
+ }
+ return url;
+ });
+ }
+
+ /**
+ * @suppress {checkTypes}
+ * Resulted in error: Promise.prototype.then does not match formal
+ * parameter.
+ */
+ setChangeTopic(changeNum, topic) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: '/topic',
+ body: {topic},
+ parseResponse: true,
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @suppress {checkTypes}
+ * Resulted in error: Promise.prototype.then does not match formal
+ * parameter.
+ */
+ setChangeHashtag(changeNum, hashtag) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/hashtags',
+ body: hashtag,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ });
+ }
+
+ deleteAccountHttpPassword() {
+ return this._restApiHelper.send({
+ method: 'DELETE',
+ url: '/accounts/self/password.http',
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @suppress {checkTypes}
+ * Resulted in error: Promise.prototype.then does not match formal
+ * parameter.
+ */
+ generateAccountHttpPassword() {
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: '/accounts/self/password.http',
+ body: {generate: true},
+ parseResponse: true,
+ reportUrlAsIs: true,
+ });
+ }
+
+ getAccountSSHKeys() {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/sshkeys',
+ reportUrlAsIs: true,
+ });
+ }
+
+ addAccountSSHKey(key) {
+ const req = {
+ method: 'POST',
+ url: '/accounts/self/sshkeys',
+ body: key,
+ contentType: 'text/plain',
+ reportUrlAsIs: true,
+ };
+ return this._restApiHelper.send(req)
+ .then(response => {
+ if (response.status < 200 && response.status >= 300) {
+ return Promise.reject(new Error('error'));
+ }
+ return this.getResponseObject(response);
+ })
+ .then(obj => {
+ if (!obj.valid) { return Promise.reject(new Error('error')); }
+ return obj;
+ });
+ }
+
+ deleteAccountSSHKey(id) {
+ return this._restApiHelper.send({
+ method: 'DELETE',
+ url: '/accounts/self/sshkeys/' + id,
+ anonymizedUrl: '/accounts/self/sshkeys/*',
+ });
+ }
+
+ getAccountGPGKeys() {
+ return this._restApiHelper.fetchJSON({
+ url: '/accounts/self/gpgkeys',
+ reportUrlAsIs: true,
+ });
+ }
+
+ addAccountGPGKey(key) {
+ const req = {
+ method: 'POST',
+ url: '/accounts/self/gpgkeys',
+ body: key,
+ reportUrlAsIs: true,
+ };
+ return this._restApiHelper.send(req)
+ .then(response => {
+ if (response.status < 200 && response.status >= 300) {
+ return Promise.reject(new Error('error'));
+ }
+ return this.getResponseObject(response);
+ })
+ .then(obj => {
+ if (!obj) { return Promise.reject(new Error('error')); }
+ return obj;
+ });
+ }
+
+ deleteAccountGPGKey(id) {
+ return this._restApiHelper.send({
+ method: 'DELETE',
+ url: '/accounts/self/gpgkeys/' + id,
+ anonymizedUrl: '/accounts/self/gpgkeys/*',
+ });
+ }
+
+ deleteVote(changeNum, account, label) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'DELETE',
+ endpoint: `/reviewers/${account}/votes/${encodeURIComponent(label)}`,
+ anonymizedEndpoint: '/reviewers/*/votes/*',
+ });
+ }
+
+ setDescription(changeNum, patchNum, desc) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT', patchNum,
+ endpoint: '/description',
+ body: {description: desc},
+ reportUrlAsIs: true,
+ });
+ }
+
+ confirmEmail(token) {
+ const req = {
+ method: 'PUT',
+ url: '/config/server/email.confirm',
+ body: {token},
+ reportUrlAsIs: true,
+ };
+ return this._restApiHelper.send(req).then(response => {
+ if (response.status === 204) {
+ return 'Email confirmed successfully.';
+ }
+ return null;
+ });
+ }
+
+ getCapabilities(opt_errFn) {
+ return this._restApiHelper.fetchJSON({
+ url: '/config/server/capabilities',
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
+ }
+
+ getTopMenus(opt_errFn) {
+ return this._fetchSharedCacheURL({
+ url: '/config/server/top-menus',
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
+ }
+
+ setAssignee(changeNum, assignee) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: '/assignee',
+ body: {assignee},
+ reportUrlAsIs: true,
+ });
+ }
+
+ deleteAssignee(changeNum) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'DELETE',
+ endpoint: '/assignee',
+ reportUrlAsIs: true,
+ });
+ }
+
+ probePath(path) {
+ return fetch(new Request(path, {method: 'HEAD'}))
+ .then(response => response.ok);
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string=} opt_message
+ */
+ startWorkInProgress(changeNum, opt_message) {
+ const body = {};
+ if (opt_message) {
+ body.message = opt_message;
+ }
+ const req = {
+ changeNum,
+ method: 'POST',
+ endpoint: '/wip',
+ body,
+ reportUrlAsIs: true,
+ };
+ return this._getChangeURLAndSend(req).then(response => {
+ if (response.status === 204) {
+ return 'Change marked as Work In Progress.';
+ }
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string=} opt_body
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ startReview(changeNum, opt_body, opt_errFn) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/ready',
+ body: opt_body,
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @suppress {checkTypes}
+ * Resulted in error: Promise.prototype.then does not match formal
+ * parameter.
+ */
+ deleteComment(changeNum, patchNum, commentID, reason) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ patchNum,
+ endpoint: `/comments/${commentID}/delete`,
+ body: {reason},
+ parseResponse: true,
+ anonymizedEndpoint: '/comments/*/delete',
+ });
+ }
+
+ /**
+ * Given a changeNum, gets the change.
+ *
+ * @param {number|string} changeNum
+ * @param {function(?Response, string=)=} opt_errFn
+ * @return {!Promise<?Object>} The change
+ */
+ getChange(changeNum, opt_errFn) {
+ // Cannot use _changeBaseURL, as this function is used by _projectLookup.
+ return this._restApiHelper.fetchJSON({
+ url: `/changes/?q=change:${changeNum}`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/changes/?q=change:*',
+ }).then(res => {
+ if (!res || !res.length) { return null; }
+ return res[0];
+ });
+ }
+
+ /**
+ * @param {string|number} changeNum
+ * @param {string=} project
+ */
+ setInProjectLookup(changeNum, project) {
+ if (this._projectLookup[changeNum] &&
+ this._projectLookup[changeNum] !== project) {
+ console.warn('Change set with multiple project nums.' +
+ 'One of them must be invalid.');
+ }
+ this._projectLookup[changeNum] = project;
+ }
+
+ /**
+ * Checks in _projectLookup for the changeNum. If it exists, returns the
+ * project. If not, calls the restAPI to get the change, populates
+ * _projectLookup with the project for that change, and returns the project.
+ *
+ * @param {string|number} changeNum
+ * @return {!Promise<string|undefined>}
+ */
+ getFromProjectLookup(changeNum) {
+ const project = this._projectLookup[changeNum];
+ if (project) { return Promise.resolve(project); }
+
+ const onError = response => {
+ // Fire a page error so that the visual 404 is displayed.
+ this.fire('page-error', {response});
+ };
+
+ return this.getChange(changeNum, onError).then(change => {
+ if (!change || !change.project) { return; }
+ this.setInProjectLookup(changeNum, change.project);
+ return change.project;
+ });
+ }
+
+ /**
+ * Alias for _changeBaseURL.then(send).
+ *
+ * @todo(beckysiegel) clean up comments
+ * @param {Gerrit.ChangeSendRequest} req
+ * @return {!Promise<!Object>}
+ */
+ _getChangeURLAndSend(req) {
+ const anonymizedBaseUrl = req.patchNum ?
+ ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL;
+ const anonymizedEndpoint = req.reportEndpointAsIs ?
+ req.endpoint : req.anonymizedEndpoint;
+
+ return this._changeBaseURL(req.changeNum, req.patchNum)
+ .then(url => this._restApiHelper.send({
+ method: req.method,
+ url: url + req.endpoint,
+ body: req.body,
+ errFn: req.errFn,
+ contentType: req.contentType,
+ headers: req.headers,
+ parseResponse: req.parseResponse,
+ anonymizedUrl: anonymizedEndpoint ?
+ (anonymizedBaseUrl + anonymizedEndpoint) : undefined,
+ }));
+ }
+
+ /**
+ * Alias for _changeBaseURL.then(_fetchJSON).
+ *
+ * @param {Gerrit.ChangeFetchRequest} req
+ * @return {!Promise<!Object>}
+ */
+ _getChangeURLAndFetch(req, noAcceptHeader) {
+ const anonymizedEndpoint = req.reportEndpointAsIs ?
+ req.endpoint : req.anonymizedEndpoint;
+ const anonymizedBaseUrl = req.patchNum ?
+ ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL;
+ return this._changeBaseURL(req.changeNum, req.patchNum)
+ .then(url => this._restApiHelper.fetchJSON({
+ url: url + req.endpoint,
+ errFn: req.errFn,
+ params: req.params,
+ fetchOptions: req.fetchOptions,
+ anonymizedUrl: anonymizedEndpoint ?
+ (anonymizedBaseUrl + anonymizedEndpoint) : undefined,
+ }, noAcceptHeader));
+ }
+
+ /**
+ * Execute a change action or revision action on a change.
+ *
+ * @param {number} changeNum
+ * @param {string} method
+ * @param {string} endpoint
+ * @param {string|number|undefined} opt_patchNum
+ * @param {Object=} opt_payload
+ * @param {?function(?Response, string=)=} opt_errFn
+ * @return {Promise}
+ */
+ executeChangeAction(changeNum, method, endpoint, opt_patchNum, opt_payload,
+ opt_errFn) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method,
+ patchNum: opt_patchNum,
+ endpoint,
+ body: opt_payload,
+ errFn: opt_errFn,
+ });
+ }
+
+ /**
+ * Get blame information for the given diff.
+ *
+ * @param {string|number} changeNum
+ * @param {string|number} patchNum
+ * @param {string} path
+ * @param {boolean=} opt_base If true, requests blame for the base of the
+ * diff, rather than the revision.
+ * @return {!Promise<!Object>}
+ */
+ getBlame(changeNum, patchNum, path, opt_base) {
+ const encodedPath = encodeURIComponent(path);
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: `/files/${encodedPath}/blame`,
+ patchNum,
+ params: opt_base ? {base: 't'} : undefined,
+ anonymizedEndpoint: '/files/*/blame',
+ });
+ }
+
+ /**
+ * Modify the given create draft request promise so that it fails and throws
+ * an error if the response bears HTTP status 200 instead of HTTP 201.
+ *
+ * @see Issue 7763
+ * @param {Promise} promise The original promise.
+ * @return {Promise} The modified promise.
+ */
+ _failForCreate200(promise) {
+ return promise.then(result => {
+ if (result.status === 200) {
+ // Read the response headers into an object representation.
+ const headers = Array.from(result.headers.entries())
+ .reduce((obj, [key, val]) => {
+ if (!HEADER_REPORTING_BLACKLIST.test(key)) {
+ obj[key] = val;
+ }
+ return obj;
+ }, {});
+ const err = new Error([
+ CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE,
+ JSON.stringify(headers),
+ ].join('\n'));
+ // Throw the error so that it is caught by gr-reporting.
+ throw err;
+ }
+ return result;
+ });
+ }
+
+ /**
+ * Fetch a project dashboard definition.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-dashboard
+ *
+ * @param {string} project
+ * @param {string} dashboard
+ * @param {function(?Response, string=)=} opt_errFn
+ * passed as null sometimes.
+ * @return {!Promise<!Object>}
+ */
+ getDashboard(project, dashboard, opt_errFn) {
+ const url = '/projects/' + encodeURIComponent(project) + '/dashboards/' +
+ encodeURIComponent(dashboard);
+ return this._fetchSharedCacheURL({
+ url,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/dashboards/*',
+ });
+ }
+
+ /**
+ * @param {string} filter
+ * @return {!Promise<?Object>}
+ */
+ getDocumentationSearches(filter) {
+ filter = filter.trim();
+ const encodedFilter = encodeURIComponent(filter);
+
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url: `/Documentation/?q=${encodedFilter}`,
+ anonymizedUrl: '/Documentation/?*',
+ });
+ }
+
+ getMergeable(changeNum) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/revisions/current/mergeable',
+ parseResponse: true,
+ reportEndpointAsIs: true,
+ });
+ }
+
+ deleteDraftComments(query) {
+ return this._restApiHelper.send({
+ method: 'POST',
+ url: '/accounts/self/drafts:delete',
+ body: {query},
+ });
+ }
+}
+
+customElements.define(GrRestApiInterface.is, GrRestApiInterface);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 1088f7e..814a474 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -19,17 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-rest-api-interface</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="gr-rest-api-interface.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -37,92 +30,150 @@
</template>
</test-fixture>
-<script>
- suite('gr-rest-api-interface tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let ctr = 0;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-rest-api-interface.js';
+suite('gr-rest-api-interface tests', () => {
+ let element;
+ let sandbox;
+ let ctr = 0;
- setup(() => {
- // Modify CANONICAL_PATH to effectively reset cache.
- ctr += 1;
- window.CANONICAL_PATH = `test${ctr}`;
+ setup(() => {
+ // Modify CANONICAL_PATH to effectively reset cache.
+ ctr += 1;
+ window.CANONICAL_PATH = `test${ctr}`;
- sandbox = sinon.sandbox.create();
- const testJSON = ')]}\'\n{"hello": "bonjour"}';
- sandbox.stub(window, 'fetch').returns(Promise.resolve({
- ok: true,
- text() {
- return Promise.resolve(testJSON);
- },
- }));
- // fake auth
- sandbox.stub(Gerrit.Auth, 'authCheck').returns(Promise.resolve(true));
- element = fixture('basic');
- element._projectLookup = {};
- });
+ sandbox = sinon.sandbox.create();
+ const testJSON = ')]}\'\n{"hello": "bonjour"}';
+ sandbox.stub(window, 'fetch').returns(Promise.resolve({
+ ok: true,
+ text() {
+ return Promise.resolve(testJSON);
+ },
+ }));
+ // fake auth
+ sandbox.stub(Gerrit.Auth, 'authCheck').returns(Promise.resolve(true));
+ element = fixture('basic');
+ element._projectLookup = {};
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('parent diff comments are properly grouped', done => {
- sandbox.stub(element._restApiHelper, 'fetchJSON', () => Promise.resolve({
- '/COMMIT_MSG': [],
- 'sieve.go': [
- {
- updated: '2017-02-03 22:32:28.000000000',
- message: 'this isn’t quite right',
- },
- {
- side: 'PARENT',
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:33:28.000000000',
- },
- ],
- }));
- element._getDiffComments('42', '', 'PARENT', 1, 'sieve.go').then(
- obj => {
- assert.equal(obj.baseComments.length, 1);
- assert.deepEqual(obj.baseComments[0], {
- side: 'PARENT',
- message: 'how did this work in the first place?',
- path: 'sieve.go',
- updated: '2017-02-03 22:33:28.000000000',
- });
- assert.equal(obj.comments.length, 1);
- assert.deepEqual(obj.comments[0], {
- message: 'this isn’t quite right',
- path: 'sieve.go',
- updated: '2017-02-03 22:32:28.000000000',
- });
- done();
- });
- });
-
- test('_setRange', () => {
- const comments = [
+ test('parent diff comments are properly grouped', done => {
+ sandbox.stub(element._restApiHelper, 'fetchJSON', () => Promise.resolve({
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
{
- id: 1,
+ updated: '2017-02-03 22:32:28.000000000',
+ message: 'this isn’t quite right',
+ },
+ {
side: 'PARENT',
message: 'how did this work in the first place?',
- updated: '2017-02-03 22:32:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- {
- id: 2,
- in_reply_to: 1,
- message: 'this isn’t quite right',
updated: '2017-02-03 22:33:28.000000000',
},
- ];
- const expectedResult = {
+ ],
+ }));
+ element._getDiffComments('42', '', 'PARENT', 1, 'sieve.go').then(
+ obj => {
+ assert.equal(obj.baseComments.length, 1);
+ assert.deepEqual(obj.baseComments[0], {
+ side: 'PARENT',
+ message: 'how did this work in the first place?',
+ path: 'sieve.go',
+ updated: '2017-02-03 22:33:28.000000000',
+ });
+ assert.equal(obj.comments.length, 1);
+ assert.deepEqual(obj.comments[0], {
+ message: 'this isn’t quite right',
+ path: 'sieve.go',
+ updated: '2017-02-03 22:32:28.000000000',
+ });
+ done();
+ });
+ });
+
+ test('_setRange', () => {
+ const comments = [
+ {
+ id: 1,
+ side: 'PARENT',
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:32:28.000000000',
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ {
+ id: 2,
+ in_reply_to: 1,
+ message: 'this isn’t quite right',
+ updated: '2017-02-03 22:33:28.000000000',
+ },
+ ];
+ const expectedResult = {
+ id: 2,
+ in_reply_to: 1,
+ message: 'this isn’t quite right',
+ updated: '2017-02-03 22:33:28.000000000',
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ };
+ const comment = comments[1];
+ assert.deepEqual(element._setRange(comments, comment), expectedResult);
+ });
+
+ test('_setRanges', () => {
+ const comments = [
+ {
+ id: 3,
+ in_reply_to: 2,
+ message: 'this isn’t quite right either',
+ updated: '2017-02-03 22:34:28.000000000',
+ },
+ {
+ id: 2,
+ in_reply_to: 1,
+ message: 'this isn’t quite right',
+ updated: '2017-02-03 22:33:28.000000000',
+ },
+ {
+ id: 1,
+ side: 'PARENT',
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:32:28.000000000',
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ ];
+ const expectedResult = [
+ {
+ id: 1,
+ side: 'PARENT',
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:32:28.000000000',
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ {
id: 2,
in_reply_to: 1,
message: 'this isn’t quite right',
@@ -133,1347 +184,1261 @@
end_line: 2,
end_character: 1,
},
- };
- const comment = comments[1];
- assert.deepEqual(element._setRange(comments, comment), expectedResult);
- });
+ },
+ {
+ id: 3,
+ in_reply_to: 2,
+ message: 'this isn’t quite right either',
+ updated: '2017-02-03 22:34:28.000000000',
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ ];
+ assert.deepEqual(element._setRanges(comments), expectedResult);
+ });
- test('_setRanges', () => {
- const comments = [
- {
- id: 3,
- in_reply_to: 2,
- message: 'this isn’t quite right either',
- updated: '2017-02-03 22:34:28.000000000',
- },
- {
- id: 2,
- in_reply_to: 1,
- message: 'this isn’t quite right',
- updated: '2017-02-03 22:33:28.000000000',
- },
- {
- id: 1,
- side: 'PARENT',
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:32:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- ];
- const expectedResult = [
- {
- id: 1,
- side: 'PARENT',
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:32:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- {
- id: 2,
- in_reply_to: 1,
- message: 'this isn’t quite right',
- updated: '2017-02-03 22:33:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- {
- id: 3,
- in_reply_to: 2,
- message: 'this isn’t quite right either',
- updated: '2017-02-03 22:34:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- ];
- assert.deepEqual(element._setRanges(comments), expectedResult);
- });
-
- test('differing patch diff comments are properly grouped', done => {
- sandbox.stub(element, 'getFromProjectLookup')
- .returns(Promise.resolve('test'));
- sandbox.stub(element._restApiHelper, 'fetchJSON', request => {
- const url = request.url;
- if (url === '/changes/test~42/revisions/1') {
- return Promise.resolve({
- '/COMMIT_MSG': [],
- 'sieve.go': [
- {
- message: 'this isn’t quite right',
- updated: '2017-02-03 22:32:28.000000000',
- },
- {
- side: 'PARENT',
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:33:28.000000000',
- },
- ],
- });
- } else if (url === '/changes/test~42/revisions/2') {
- return Promise.resolve({
- '/COMMIT_MSG': [],
- 'sieve.go': [
- {
- message: 'What on earth are you thinking, here?',
- updated: '2017-02-03 22:32:28.000000000',
- },
- {
- side: 'PARENT',
- message: 'Yeah not sure how this worked either?',
- updated: '2017-02-03 22:33:28.000000000',
- },
- {
- message: '¯\\_(ツ)_/¯',
- updated: '2017-02-04 22:33:28.000000000',
- },
- ],
- });
- }
- });
- element._getDiffComments('42', '', 1, 2, 'sieve.go').then(
- obj => {
- assert.equal(obj.baseComments.length, 1);
- assert.deepEqual(obj.baseComments[0], {
+ test('differing patch diff comments are properly grouped', done => {
+ sandbox.stub(element, 'getFromProjectLookup')
+ .returns(Promise.resolve('test'));
+ sandbox.stub(element._restApiHelper, 'fetchJSON', request => {
+ const url = request.url;
+ if (url === '/changes/test~42/revisions/1') {
+ return Promise.resolve({
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
message: 'this isn’t quite right',
- path: 'sieve.go',
updated: '2017-02-03 22:32:28.000000000',
- });
- assert.equal(obj.comments.length, 2);
- assert.deepEqual(obj.comments[0], {
+ },
+ {
+ side: 'PARENT',
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:33:28.000000000',
+ },
+ ],
+ });
+ } else if (url === '/changes/test~42/revisions/2') {
+ return Promise.resolve({
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
message: 'What on earth are you thinking, here?',
- path: 'sieve.go',
updated: '2017-02-03 22:32:28.000000000',
- });
- assert.deepEqual(obj.comments[1], {
+ },
+ {
+ side: 'PARENT',
+ message: 'Yeah not sure how this worked either?',
+ updated: '2017-02-03 22:33:28.000000000',
+ },
+ {
message: '¯\\_(ツ)_/¯',
- path: 'sieve.go',
updated: '2017-02-04 22:33:28.000000000',
- });
- done();
+ },
+ ],
+ });
+ }
+ });
+ element._getDiffComments('42', '', 1, 2, 'sieve.go').then(
+ obj => {
+ assert.equal(obj.baseComments.length, 1);
+ assert.deepEqual(obj.baseComments[0], {
+ message: 'this isn’t quite right',
+ path: 'sieve.go',
+ updated: '2017-02-03 22:32:28.000000000',
});
- });
-
- test('special file path sorting', () => {
- assert.deepEqual(
- ['.b', '/COMMIT_MSG', '.a', 'file'].sort(
- element.specialFilePathCompare),
- ['/COMMIT_MSG', '.a', '.b', 'file']);
-
- assert.deepEqual(
- ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.h'].sort(
- element.specialFilePathCompare),
- ['/COMMIT_MSG', '.b', 'foo/bar/baz.h', 'foo/bar/baz.cc']);
-
- assert.deepEqual(
- ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hpp'].sort(
- element.specialFilePathCompare),
- ['/COMMIT_MSG', '.b', 'foo/bar/baz.hpp', 'foo/bar/baz.cc']);
-
- assert.deepEqual(
- ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hxx'].sort(
- element.specialFilePathCompare),
- ['/COMMIT_MSG', '.b', 'foo/bar/baz.hxx', 'foo/bar/baz.cc']);
-
- assert.deepEqual(
- ['foo/bar.h', 'foo/bar.hxx', 'foo/bar.hpp'].sort(
- element.specialFilePathCompare),
- ['foo/bar.h', 'foo/bar.hpp', 'foo/bar.hxx']);
-
- // Regression test for Issue 4448.
- assert.deepEqual(
- [
- 'minidump/minidump_memory_writer.cc',
- 'minidump/minidump_memory_writer.h',
- 'minidump/minidump_thread_writer.cc',
- 'minidump/minidump_thread_writer.h',
- ].sort(element.specialFilePathCompare),
- [
- 'minidump/minidump_memory_writer.h',
- 'minidump/minidump_memory_writer.cc',
- 'minidump/minidump_thread_writer.h',
- 'minidump/minidump_thread_writer.cc',
- ]);
-
- // Regression test for Issue 4545.
- assert.deepEqual(
- [
- 'task_test.go',
- 'task.go',
- ].sort(element.specialFilePathCompare),
- [
- 'task.go',
- 'task_test.go',
- ]);
- });
-
- suite('rebase action', () => {
- let resolve_fetchJSON;
- setup(() => {
- sandbox.stub(element._restApiHelper, 'fetchJSON').returns(
- new Promise(resolve => {
- resolve_fetchJSON = resolve;
- }));
- });
-
- test('no rebase on current', done => {
- element.getChangeRevisionActions('42', '1337').then(
- response => {
- assert.isTrue(response.rebase.enabled);
- assert.isFalse(response.rebase.rebaseOnCurrent);
- done();
- });
- resolve_fetchJSON({rebase: {}});
- });
-
- test('rebase on current', done => {
- element.getChangeRevisionActions('42', '1337').then(
- response => {
- assert.isTrue(response.rebase.enabled);
- assert.isTrue(response.rebase.rebaseOnCurrent);
- done();
- });
- resolve_fetchJSON({rebase: {enabled: true}});
- });
- });
-
- test('server error', done => {
- const getResponseObjectStub = sandbox.stub(element, 'getResponseObject');
- window.fetch.returns(Promise.resolve({ok: false}));
- const serverErrorEventPromise = new Promise(resolve => {
- element.addEventListener('server-error', resolve);
- });
-
- element._restApiHelper.fetchJSON({}).then(response => {
- assert.isUndefined(response);
- assert.isTrue(getResponseObjectStub.notCalled);
- serverErrorEventPromise.then(() => done());
- });
- });
-
- test('legacy n,z key in change url is replaced', () => {
- const stub = sandbox.stub(element._restApiHelper, 'fetchJSON')
- .returns(Promise.resolve([]));
- element.getChanges(1, null, 'n,z');
- assert.equal(stub.lastCall.args[0].params.S, 0);
- });
-
- test('saveDiffPreferences invalidates cache line', () => {
- const cacheKey = '/accounts/self/preferences.diff';
- const sendStub = sandbox.stub(element._restApiHelper, 'send');
- element._cache.set(cacheKey, {tab_size: 4});
- element.saveDiffPreferences({tab_size: 8});
- assert.isTrue(sendStub.called);
- assert.isFalse(element._restApiHelper._cache.has(cacheKey));
- });
-
- test('getAccount when resp is null does not add anything to the cache',
- done => {
- const cacheKey = '/accounts/self/detail';
- const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
- () => Promise.resolve());
-
- element.getAccount().then(() => {
- assert.isTrue(stub.called);
- assert.isFalse(element._restApiHelper._cache.has(cacheKey));
- done();
+ assert.equal(obj.comments.length, 2);
+ assert.deepEqual(obj.comments[0], {
+ message: 'What on earth are you thinking, here?',
+ path: 'sieve.go',
+ updated: '2017-02-03 22:32:28.000000000',
});
+ assert.deepEqual(obj.comments[1], {
+ message: '¯\\_(ツ)_/¯',
+ path: 'sieve.go',
+ updated: '2017-02-04 22:33:28.000000000',
+ });
+ done();
+ });
+ });
- element._restApiHelper._cache.set(cacheKey, 'fake cache');
- stub.lastCall.args[0].errFn();
+ test('special file path sorting', () => {
+ assert.deepEqual(
+ ['.b', '/COMMIT_MSG', '.a', 'file'].sort(
+ element.specialFilePathCompare),
+ ['/COMMIT_MSG', '.a', '.b', 'file']);
+
+ assert.deepEqual(
+ ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.h'].sort(
+ element.specialFilePathCompare),
+ ['/COMMIT_MSG', '.b', 'foo/bar/baz.h', 'foo/bar/baz.cc']);
+
+ assert.deepEqual(
+ ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hpp'].sort(
+ element.specialFilePathCompare),
+ ['/COMMIT_MSG', '.b', 'foo/bar/baz.hpp', 'foo/bar/baz.cc']);
+
+ assert.deepEqual(
+ ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hxx'].sort(
+ element.specialFilePathCompare),
+ ['/COMMIT_MSG', '.b', 'foo/bar/baz.hxx', 'foo/bar/baz.cc']);
+
+ assert.deepEqual(
+ ['foo/bar.h', 'foo/bar.hxx', 'foo/bar.hpp'].sort(
+ element.specialFilePathCompare),
+ ['foo/bar.h', 'foo/bar.hpp', 'foo/bar.hxx']);
+
+ // Regression test for Issue 4448.
+ assert.deepEqual(
+ [
+ 'minidump/minidump_memory_writer.cc',
+ 'minidump/minidump_memory_writer.h',
+ 'minidump/minidump_thread_writer.cc',
+ 'minidump/minidump_thread_writer.h',
+ ].sort(element.specialFilePathCompare),
+ [
+ 'minidump/minidump_memory_writer.h',
+ 'minidump/minidump_memory_writer.cc',
+ 'minidump/minidump_thread_writer.h',
+ 'minidump/minidump_thread_writer.cc',
+ ]);
+
+ // Regression test for Issue 4545.
+ assert.deepEqual(
+ [
+ 'task_test.go',
+ 'task.go',
+ ].sort(element.specialFilePathCompare),
+ [
+ 'task.go',
+ 'task_test.go',
+ ]);
+ });
+
+ test('server error', done => {
+ const getResponseObjectStub = sandbox.stub(element, 'getResponseObject');
+ window.fetch.returns(Promise.resolve({ok: false}));
+ const serverErrorEventPromise = new Promise(resolve => {
+ element.addEventListener('server-error', resolve);
+ });
+
+ element._restApiHelper.fetchJSON({}).then(response => {
+ assert.isUndefined(response);
+ assert.isTrue(getResponseObjectStub.notCalled);
+ serverErrorEventPromise.then(() => done());
+ });
+ });
+
+ test('legacy n,z key in change url is replaced', () => {
+ const stub = sandbox.stub(element._restApiHelper, 'fetchJSON')
+ .returns(Promise.resolve([]));
+ element.getChanges(1, null, 'n,z');
+ assert.equal(stub.lastCall.args[0].params.S, 0);
+ });
+
+ test('saveDiffPreferences invalidates cache line', () => {
+ const cacheKey = '/accounts/self/preferences.diff';
+ const sendStub = sandbox.stub(element._restApiHelper, 'send');
+ element._cache.set(cacheKey, {tab_size: 4});
+ element.saveDiffPreferences({tab_size: 8});
+ assert.isTrue(sendStub.called);
+ assert.isFalse(element._restApiHelper._cache.has(cacheKey));
+ });
+
+ test('getAccount when resp is null does not add anything to the cache',
+ done => {
+ const cacheKey = '/accounts/self/detail';
+ const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
+ () => Promise.resolve());
+
+ element.getAccount().then(() => {
+ assert.isTrue(stub.called);
+ assert.isFalse(element._restApiHelper._cache.has(cacheKey));
+ done();
});
- test('getAccount does not add to the cache when resp.status is 403',
- done => {
- const cacheKey = '/accounts/self/detail';
- const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
- () => Promise.resolve());
-
- element.getAccount().then(() => {
- assert.isTrue(stub.called);
- assert.isFalse(element._restApiHelper._cache.has(cacheKey));
- done();
- });
- element._cache.set(cacheKey, 'fake cache');
- stub.lastCall.args[0].errFn({status: 403});
- });
-
- test('getAccount when resp is successful', done => {
- const cacheKey = '/accounts/self/detail';
- const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
- () => Promise.resolve());
-
- element.getAccount().then(response => {
- assert.isTrue(stub.called);
- assert.equal(element._restApiHelper._cache.get(cacheKey), 'fake cache');
- done();
+ element._restApiHelper._cache.set(cacheKey, 'fake cache');
+ stub.lastCall.args[0].errFn();
});
- element._restApiHelper._cache.set(cacheKey, 'fake cache');
- stub.lastCall.args[0].errFn({});
- });
+ test('getAccount does not add to the cache when resp.status is 403',
+ done => {
+ const cacheKey = '/accounts/self/detail';
+ const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
+ () => Promise.resolve());
- const preferenceSetup = function(testJSON, loggedIn, smallScreen) {
- sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(loggedIn));
- sandbox.stub(element, '_isNarrowScreen', () => smallScreen);
- sandbox.stub(
- element._restApiHelper,
- 'fetchCacheURL',
- () => Promise.resolve(testJSON));
- };
-
- test('getPreferences returns correctly on small screens logged in',
- done => {
- const testJSON = {diff_view: 'SIDE_BY_SIDE'};
- const loggedIn = true;
- const smallScreen = true;
-
- preferenceSetup(testJSON, loggedIn, smallScreen);
-
- element.getPreferences().then(obj => {
- assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
- assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
- done();
- });
+ element.getAccount().then(() => {
+ assert.isTrue(stub.called);
+ assert.isFalse(element._restApiHelper._cache.has(cacheKey));
+ done();
});
-
- test('getPreferences returns correctly on small screens not logged in',
- done => {
- const testJSON = {diff_view: 'SIDE_BY_SIDE'};
- const loggedIn = false;
- const smallScreen = true;
-
- preferenceSetup(testJSON, loggedIn, smallScreen);
- element.getPreferences().then(obj => {
- assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
- assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
- done();
- });
- });
-
- test('getPreferences returns correctly on larger screens logged in',
- done => {
- const testJSON = {diff_view: 'UNIFIED_DIFF'};
- const loggedIn = true;
- const smallScreen = false;
-
- preferenceSetup(testJSON, loggedIn, smallScreen);
-
- element.getPreferences().then(obj => {
- assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
- assert.equal(obj.diff_view, 'UNIFIED_DIFF');
- done();
- });
- });
-
- test('getPreferences returns correctly on larger screens not logged in',
- done => {
- const testJSON = {diff_view: 'UNIFIED_DIFF'};
- const loggedIn = false;
- const smallScreen = false;
-
- preferenceSetup(testJSON, loggedIn, smallScreen);
-
- element.getPreferences().then(obj => {
- assert.equal(obj.default_diff_view, 'SIDE_BY_SIDE');
- assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
- done();
- });
- });
-
- test('savPreferences normalizes download scheme', () => {
- const sendStub = sandbox.stub(element._restApiHelper, 'send');
- element.savePreferences({download_scheme: 'HTTP'});
- assert.isTrue(sendStub.called);
- assert.equal(sendStub.lastCall.args[0].body.download_scheme, 'http');
- });
-
- test('getDiffPreferences returns correct defaults', done => {
- sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(false));
-
- element.getDiffPreferences().then(obj => {
- assert.equal(obj.auto_hide_diff_table_header, true);
- assert.equal(obj.context, 10);
- assert.equal(obj.cursor_blink_rate, 0);
- assert.equal(obj.font_size, 12);
- assert.equal(obj.ignore_whitespace, 'IGNORE_NONE');
- assert.equal(obj.intraline_difference, true);
- assert.equal(obj.line_length, 100);
- assert.equal(obj.line_wrapping, false);
- assert.equal(obj.show_line_endings, true);
- assert.equal(obj.show_tabs, true);
- assert.equal(obj.show_whitespace_errors, true);
- assert.equal(obj.syntax_highlighting, true);
- assert.equal(obj.tab_size, 8);
- assert.equal(obj.theme, 'DEFAULT');
- done();
+ element._cache.set(cacheKey, 'fake cache');
+ stub.lastCall.args[0].errFn({status: 403});
});
+
+ test('getAccount when resp is successful', done => {
+ const cacheKey = '/accounts/self/detail';
+ const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
+ () => Promise.resolve());
+
+ element.getAccount().then(response => {
+ assert.isTrue(stub.called);
+ assert.equal(element._restApiHelper._cache.get(cacheKey), 'fake cache');
+ done();
});
+ element._restApiHelper._cache.set(cacheKey, 'fake cache');
- test('saveDiffPreferences set show_tabs to false', () => {
- const sendStub = sandbox.stub(element._restApiHelper, 'send');
- element.saveDiffPreferences({show_tabs: false});
- assert.isTrue(sendStub.called);
- assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
- });
+ stub.lastCall.args[0].errFn({});
+ });
- test('getEditPreferences returns correct defaults', done => {
- sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(false));
+ const preferenceSetup = function(testJSON, loggedIn, smallScreen) {
+ sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(loggedIn));
+ sandbox.stub(element, '_isNarrowScreen', () => smallScreen);
+ sandbox.stub(
+ element._restApiHelper,
+ 'fetchCacheURL',
+ () => Promise.resolve(testJSON));
+ };
- element.getEditPreferences().then(obj => {
- assert.equal(obj.auto_close_brackets, false);
- assert.equal(obj.cursor_blink_rate, 0);
- assert.equal(obj.hide_line_numbers, false);
- assert.equal(obj.hide_top_menu, false);
- assert.equal(obj.indent_unit, 2);
- assert.equal(obj.indent_with_tabs, false);
- assert.equal(obj.key_map_type, 'DEFAULT');
- assert.equal(obj.line_length, 100);
- assert.equal(obj.line_wrapping, false);
- assert.equal(obj.match_brackets, true);
- assert.equal(obj.show_base, false);
- assert.equal(obj.show_tabs, true);
- assert.equal(obj.show_whitespace_errors, true);
- assert.equal(obj.syntax_highlighting, true);
- assert.equal(obj.tab_size, 8);
- assert.equal(obj.theme, 'DEFAULT');
- done();
+ test('getPreferences returns correctly on small screens logged in',
+ done => {
+ const testJSON = {diff_view: 'SIDE_BY_SIDE'};
+ const loggedIn = true;
+ const smallScreen = true;
+
+ preferenceSetup(testJSON, loggedIn, smallScreen);
+
+ element.getPreferences().then(obj => {
+ assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
+ assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
+ done();
+ });
});
- });
- test('saveEditPreferences set show_tabs to false', () => {
- const sendStub = sandbox.stub(element._restApiHelper, 'send');
- element.saveEditPreferences({show_tabs: false});
- assert.isTrue(sendStub.called);
- assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
- });
+ test('getPreferences returns correctly on small screens not logged in',
+ done => {
+ const testJSON = {diff_view: 'SIDE_BY_SIDE'};
+ const loggedIn = false;
+ const smallScreen = true;
- test('confirmEmail', () => {
- const sendStub = sandbox.spy(element._restApiHelper, 'send');
- element.confirmEmail('foo');
+ preferenceSetup(testJSON, loggedIn, smallScreen);
+ element.getPreferences().then(obj => {
+ assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
+ assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
+ done();
+ });
+ });
+
+ test('getPreferences returns correctly on larger screens logged in',
+ done => {
+ const testJSON = {diff_view: 'UNIFIED_DIFF'};
+ const loggedIn = true;
+ const smallScreen = false;
+
+ preferenceSetup(testJSON, loggedIn, smallScreen);
+
+ element.getPreferences().then(obj => {
+ assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
+ assert.equal(obj.diff_view, 'UNIFIED_DIFF');
+ done();
+ });
+ });
+
+ test('getPreferences returns correctly on larger screens not logged in',
+ done => {
+ const testJSON = {diff_view: 'UNIFIED_DIFF'};
+ const loggedIn = false;
+ const smallScreen = false;
+
+ preferenceSetup(testJSON, loggedIn, smallScreen);
+
+ element.getPreferences().then(obj => {
+ assert.equal(obj.default_diff_view, 'SIDE_BY_SIDE');
+ assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
+ done();
+ });
+ });
+
+ test('savPreferences normalizes download scheme', () => {
+ const sendStub = sandbox.stub(element._restApiHelper, 'send');
+ element.savePreferences({download_scheme: 'HTTP'});
+ assert.isTrue(sendStub.called);
+ assert.equal(sendStub.lastCall.args[0].body.download_scheme, 'http');
+ });
+
+ test('getDiffPreferences returns correct defaults', done => {
+ sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(false));
+
+ element.getDiffPreferences().then(obj => {
+ assert.equal(obj.auto_hide_diff_table_header, true);
+ assert.equal(obj.context, 10);
+ assert.equal(obj.cursor_blink_rate, 0);
+ assert.equal(obj.font_size, 12);
+ assert.equal(obj.ignore_whitespace, 'IGNORE_NONE');
+ assert.equal(obj.intraline_difference, true);
+ assert.equal(obj.line_length, 100);
+ assert.equal(obj.line_wrapping, false);
+ assert.equal(obj.show_line_endings, true);
+ assert.equal(obj.show_tabs, true);
+ assert.equal(obj.show_whitespace_errors, true);
+ assert.equal(obj.syntax_highlighting, true);
+ assert.equal(obj.tab_size, 8);
+ assert.equal(obj.theme, 'DEFAULT');
+ done();
+ });
+ });
+
+ test('saveDiffPreferences set show_tabs to false', () => {
+ const sendStub = sandbox.stub(element._restApiHelper, 'send');
+ element.saveDiffPreferences({show_tabs: false});
+ assert.isTrue(sendStub.called);
+ assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
+ });
+
+ test('getEditPreferences returns correct defaults', done => {
+ sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(false));
+
+ element.getEditPreferences().then(obj => {
+ assert.equal(obj.auto_close_brackets, false);
+ assert.equal(obj.cursor_blink_rate, 0);
+ assert.equal(obj.hide_line_numbers, false);
+ assert.equal(obj.hide_top_menu, false);
+ assert.equal(obj.indent_unit, 2);
+ assert.equal(obj.indent_with_tabs, false);
+ assert.equal(obj.key_map_type, 'DEFAULT');
+ assert.equal(obj.line_length, 100);
+ assert.equal(obj.line_wrapping, false);
+ assert.equal(obj.match_brackets, true);
+ assert.equal(obj.show_base, false);
+ assert.equal(obj.show_tabs, true);
+ assert.equal(obj.show_whitespace_errors, true);
+ assert.equal(obj.syntax_highlighting, true);
+ assert.equal(obj.tab_size, 8);
+ assert.equal(obj.theme, 'DEFAULT');
+ done();
+ });
+ });
+
+ test('saveEditPreferences set show_tabs to false', () => {
+ const sendStub = sandbox.stub(element._restApiHelper, 'send');
+ element.saveEditPreferences({show_tabs: false});
+ assert.isTrue(sendStub.called);
+ assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
+ });
+
+ test('confirmEmail', () => {
+ const sendStub = sandbox.spy(element._restApiHelper, 'send');
+ element.confirmEmail('foo');
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].method, 'PUT');
+ assert.equal(sendStub.lastCall.args[0].url,
+ '/config/server/email.confirm');
+ assert.deepEqual(sendStub.lastCall.args[0].body, {token: 'foo'});
+ });
+
+ test('setAccountStatus', () => {
+ const sendStub = sandbox.stub(element._restApiHelper, 'send')
+ .returns(Promise.resolve('OOO'));
+ element._cache.set('/accounts/self/detail', {});
+ return element.setAccountStatus('OOO').then(() => {
assert.isTrue(sendStub.calledOnce);
assert.equal(sendStub.lastCall.args[0].method, 'PUT');
assert.equal(sendStub.lastCall.args[0].url,
- '/config/server/email.confirm');
- assert.deepEqual(sendStub.lastCall.args[0].body, {token: 'foo'});
- });
-
- test('setAccountStatus', () => {
- const sendStub = sandbox.stub(element._restApiHelper, 'send')
- .returns(Promise.resolve('OOO'));
- element._cache.set('/accounts/self/detail', {});
- return element.setAccountStatus('OOO').then(() => {
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].method, 'PUT');
- assert.equal(sendStub.lastCall.args[0].url,
- '/accounts/self/status');
- assert.deepEqual(sendStub.lastCall.args[0].body,
- {status: 'OOO'});
- assert.deepEqual(element._restApiHelper
- ._cache.get('/accounts/self/detail'),
- {status: 'OOO'});
- });
- });
-
- suite('draft comments', () => {
- test('_sendDiffDraftRequest pending requests tracked', () => {
- const obj = element._pendingRequests;
- sandbox.stub(element, '_getChangeURLAndSend', () => mockPromise());
- assert.notOk(element.hasPendingDiffDrafts());
-
- element._sendDiffDraftRequest(null, null, null, {});
- assert.equal(obj.sendDiffDraft.length, 1);
- assert.isTrue(!!element.hasPendingDiffDrafts());
-
- element._sendDiffDraftRequest(null, null, null, {});
- assert.equal(obj.sendDiffDraft.length, 2);
- assert.isTrue(!!element.hasPendingDiffDrafts());
-
- for (const promise of obj.sendDiffDraft) { promise.resolve(); }
-
- return element.awaitPendingDiffDrafts().then(() => {
- assert.equal(obj.sendDiffDraft.length, 0);
- assert.isFalse(!!element.hasPendingDiffDrafts());
- });
- });
-
- suite('_failForCreate200', () => {
- test('_sendDiffDraftRequest checks for 200 on create', () => {
- const sendPromise = Promise.resolve();
- sandbox.stub(element, '_getChangeURLAndSend').returns(sendPromise);
- const failStub = sandbox.stub(element, '_failForCreate200')
- .returns(Promise.resolve());
- return element._sendDiffDraftRequest('PUT', 123, 4, {}).then(() => {
- assert.isTrue(failStub.calledOnce);
- assert.isTrue(failStub.calledWithExactly(sendPromise));
- });
- });
-
- test('_sendDiffDraftRequest no checks for 200 on non create', () => {
- sandbox.stub(element, '_getChangeURLAndSend')
- .returns(Promise.resolve());
- const failStub = sandbox.stub(element, '_failForCreate200')
- .returns(Promise.resolve());
- return element._sendDiffDraftRequest('PUT', 123, 4, {id: '123'})
- .then(() => {
- assert.isFalse(failStub.called);
- });
- });
-
- test('_failForCreate200 fails on 200', done => {
- const result = {
- ok: true,
- status: 200,
- headers: {entries: () => [
- ['Set-CoOkiE', 'secret'],
- ['Innocuous', 'hello'],
- ]},
- };
- element._failForCreate200(Promise.resolve(result))
- .then(() => {
- assert.isTrue(false, 'Promise should not resolve');
- })
- .catch(e => {
- assert.isOk(e);
- assert.include(e.message, 'Saving draft resulted in HTTP 200');
- assert.include(e.message, 'hello');
- assert.notInclude(e.message, 'secret');
- done();
- });
- });
-
- test('_failForCreate200 does not fail on 201', done => {
- const result = {
- ok: true,
- status: 201,
- headers: {entries: () => []},
- };
- element._failForCreate200(Promise.resolve(result))
- .then(() => {
- done();
- })
- .catch(e => {
- assert.isTrue(false, 'Promise should not fail');
- });
- });
- });
- });
-
- test('saveChangeEdit', () => {
- element._projectLookup = {1: 'test'};
- const change_num = '1';
- const file_name = 'index.php';
- const file_contents = '<?php';
- sandbox.stub(element._restApiHelper, 'send').returns(
- Promise.resolve([change_num, file_name, file_contents]));
- sandbox.stub(element, 'getResponseObject')
- .returns(Promise.resolve([change_num, file_name, file_contents]));
- element._cache.set('/changes/' + change_num + '/edit/' + file_name, {});
- return element.saveChangeEdit(change_num, file_name, file_contents)
- .then(() => {
- assert.isTrue(element._restApiHelper.send.calledOnce);
- assert.equal(element._restApiHelper.send.lastCall.args[0].method,
- 'PUT');
- assert.equal(element._restApiHelper.send.lastCall.args[0].url,
- '/changes/test~1/edit/' + file_name);
- assert.equal(element._restApiHelper.send.lastCall.args[0].body,
- file_contents);
- });
- });
-
- test('putChangeCommitMessage', () => {
- element._projectLookup = {1: 'test'};
- const change_num = '1';
- const message = 'this is a commit message';
- sandbox.stub(element._restApiHelper, 'send').returns(
- Promise.resolve([change_num, message]));
- sandbox.stub(element, 'getResponseObject')
- .returns(Promise.resolve([change_num, message]));
- element._cache.set('/changes/' + change_num + '/message', {});
- return element.putChangeCommitMessage(change_num, message).then(() => {
- assert.isTrue(element._restApiHelper.send.calledOnce);
- assert.equal(element._restApiHelper.send.lastCall.args[0].method, 'PUT');
- assert.equal(element._restApiHelper.send.lastCall.args[0].url,
- '/changes/test~1/message');
- assert.deepEqual(element._restApiHelper.send.lastCall.args[0].body,
- {message});
- });
- });
-
- test('deleteChangeCommitMessage', () => {
- element._projectLookup = {1: 'test'};
- const change_num = '1';
- const messageId = 'abc';
- sandbox.stub(element._restApiHelper, 'send').returns(
- Promise.resolve([change_num, messageId]));
- sandbox.stub(element, 'getResponseObject')
- .returns(Promise.resolve([change_num, messageId]));
- return element.deleteChangeCommitMessage(change_num, messageId).then(() => {
- assert.isTrue(element._restApiHelper.send.calledOnce);
- assert.equal(
- element._restApiHelper.send.lastCall.args[0].method,
- 'DELETE'
- );
- assert.equal(element._restApiHelper.send.lastCall.args[0].url,
- '/changes/test~1/messages/abc');
- });
- });
-
- test('startWorkInProgress', () => {
- const sendStub = sandbox.stub(element, '_getChangeURLAndSend')
- .returns(Promise.resolve('ok'));
- element.startWorkInProgress('42');
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].changeNum, '42');
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.isNotOk(sendStub.lastCall.args[0].patchNum);
- assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
- assert.deepEqual(sendStub.lastCall.args[0].body, {});
-
- element.startWorkInProgress('42', 'revising...');
- assert.isTrue(sendStub.calledTwice);
- assert.equal(sendStub.lastCall.args[0].changeNum, '42');
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.isNotOk(sendStub.lastCall.args[0].patchNum);
- assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
+ '/accounts/self/status');
assert.deepEqual(sendStub.lastCall.args[0].body,
- {message: 'revising...'});
+ {status: 'OOO'});
+ assert.deepEqual(element._restApiHelper
+ ._cache.get('/accounts/self/detail'),
+ {status: 'OOO'});
});
+ });
- test('startReview', () => {
- const sendStub = sandbox.stub(element, '_getChangeURLAndSend')
- .returns(Promise.resolve({}));
- element.startReview('42', {message: 'Please review.'});
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].changeNum, '42');
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.isNotOk(sendStub.lastCall.args[0].patchNum);
- assert.equal(sendStub.lastCall.args[0].endpoint, '/ready');
- assert.deepEqual(sendStub.lastCall.args[0].body,
- {message: 'Please review.'});
- });
+ suite('draft comments', () => {
+ test('_sendDiffDraftRequest pending requests tracked', () => {
+ const obj = element._pendingRequests;
+ sandbox.stub(element, '_getChangeURLAndSend', () => mockPromise());
+ assert.notOk(element.hasPendingDiffDrafts());
- test('deleteComment', () => {
- const sendStub = sandbox.stub(element, '_getChangeURLAndSend')
- .returns(Promise.resolve('some response'));
- return element.deleteComment('foo', 'bar', '01234', 'removal reason')
- .then(response => {
- assert.equal(response, 'some response');
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].changeNum, 'foo');
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.equal(sendStub.lastCall.args[0].patchNum, 'bar');
- assert.equal(sendStub.lastCall.args[0].endpoint,
- '/comments/01234/delete');
- assert.deepEqual(sendStub.lastCall.args[0].body,
- {reason: 'removal reason'});
- });
- });
+ element._sendDiffDraftRequest(null, null, null, {});
+ assert.equal(obj.sendDiffDraft.length, 1);
+ assert.isTrue(!!element.hasPendingDiffDrafts());
- test('createRepo encodes name', () => {
- const sendStub = sandbox.stub(element._restApiHelper, 'send')
- .returns(Promise.resolve());
- return element.createRepo({name: 'x/y'}).then(() => {
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].url, '/projects/x%2Fy');
+ element._sendDiffDraftRequest(null, null, null, {});
+ assert.equal(obj.sendDiffDraft.length, 2);
+ assert.isTrue(!!element.hasPendingDiffDrafts());
+
+ for (const promise of obj.sendDiffDraft) { promise.resolve(); }
+
+ return element.awaitPendingDiffDrafts().then(() => {
+ assert.equal(obj.sendDiffDraft.length, 0);
+ assert.isFalse(!!element.hasPendingDiffDrafts());
});
});
- test('queryChangeFiles', () => {
- const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- return element.queryChangeFiles('42', 'edit', 'test/path.js').then(() => {
- assert.equal(fetchStub.lastCall.args[0].changeNum, '42');
- assert.equal(fetchStub.lastCall.args[0].endpoint,
- '/files?q=test%2Fpath.js');
- assert.equal(fetchStub.lastCall.args[0].patchNum, 'edit');
- });
- });
-
- test('normal use', () => {
- const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only';
-
- assert.equal(element._getReposUrl('test', 25),
- '/projects/?n=26&S=0&query=test');
-
- assert.equal(element._getReposUrl(null, 25),
- `/projects/?n=26&S=0&query=${defaultQuery}`);
-
- assert.equal(element._getReposUrl('test', 25, 25),
- '/projects/?n=26&S=25&query=test');
- });
-
- test('invalidateReposCache', () => {
- const url = '/projects/?n=26&S=0&query=test';
-
- element._cache.set(url, {});
-
- element.invalidateReposCache();
-
- assert.isUndefined(element._sharedFetchPromises[url]);
-
- assert.isFalse(element._cache.has(url));
- });
-
- test('invalidateAccountsCache', () => {
- const url = '/accounts/self/detail';
-
- element._cache.set(url, {});
-
- element.invalidateAccountsCache();
-
- assert.isUndefined(element._sharedFetchPromises[url]);
-
- assert.isFalse(element._cache.has(url));
- });
-
- suite('getRepos', () => {
- const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only';
- let fetchCacheURLStub;
- setup(() => {
- fetchCacheURLStub =
- sandbox.stub(element._restApiHelper, 'fetchCacheURL');
- });
-
- test('normal use', () => {
- element.getRepos('test', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&query=test');
-
- element.getRepos(null, 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- `/projects/?n=26&S=0&query=${defaultQuery}`);
-
- element.getRepos('test', 25, 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=25&query=test');
- });
-
- test('with blank', () => {
- element.getRepos('test/test', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&query=inname%3Atest%20AND%20inname%3Atest');
- });
-
- test('with hyphen', () => {
- element.getRepos('foo-bar', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
- });
-
- test('with leading hyphen', () => {
- element.getRepos('-bar', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&query=inname%3Abar');
- });
-
- test('with trailing hyphen', () => {
- element.getRepos('foo-bar-', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
- });
-
- test('with underscore', () => {
- element.getRepos('foo_bar', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
- });
-
- test('with underscore', () => {
- element.getRepos('foo_bar', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
- });
-
- test('hyphen only', () => {
- element.getRepos('-', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- `/projects/?n=26&S=0&query=${defaultQuery}`);
- });
- });
-
- test('_getGroupsUrl normal use', () => {
- assert.equal(element._getGroupsUrl('test', 25),
- '/groups/?n=26&S=0&m=test');
-
- assert.equal(element._getGroupsUrl(null, 25),
- '/groups/?n=26&S=0');
-
- assert.equal(element._getGroupsUrl('test', 25, 25),
- '/groups/?n=26&S=25&m=test');
- });
-
- test('invalidateGroupsCache', () => {
- const url = '/groups/?n=26&S=0&m=test';
-
- element._cache.set(url, {});
-
- element.invalidateGroupsCache();
-
- assert.isUndefined(element._sharedFetchPromises[url]);
-
- assert.isFalse(element._cache.has(url));
- });
-
- suite('getGroups', () => {
- let fetchCacheURLStub;
- setup(() => {
- fetchCacheURLStub =
- sandbox.stub(element._restApiHelper, 'fetchCacheURL');
- });
-
- test('normal use', () => {
- element.getGroups('test', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=0&m=test');
-
- element.getGroups(null, 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=0');
-
- element.getGroups('test', 25, 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=25&m=test');
- });
-
- test('regex', () => {
- element.getGroups('^test.*', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=0&r=%5Etest.*');
-
- element.getGroups('^test.*', 25, 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=25&r=%5Etest.*');
- });
- });
-
- test('gerrit auth is used', () => {
- sandbox.stub(Gerrit.Auth, 'fetch').returns(Promise.resolve());
- element._restApiHelper.fetchJSON({url: 'foo'});
- assert(Gerrit.Auth.fetch.called);
- });
-
- test('getSuggestedAccounts does not return _fetchJSON', () => {
- const _fetchJSONSpy = sandbox.spy(element._restApiHelper, 'fetchJSON');
- return element.getSuggestedAccounts().then(accts => {
- assert.isFalse(_fetchJSONSpy.called);
- assert.equal(accts.length, 0);
- });
- });
-
- test('_fetchJSON gets called by getSuggestedAccounts', () => {
- const _fetchJSONStub = sandbox.stub(element._restApiHelper, 'fetchJSON',
- () => Promise.resolve());
- return element.getSuggestedAccounts('own').then(() => {
- assert.deepEqual(_fetchJSONStub.lastCall.args[0].params, {
- q: 'own',
- suggest: null,
- });
- });
- });
-
- suite('getChangeDetail', () => {
- suite('change detail options', () => {
- let toHexStub;
-
- setup(() => {
- toHexStub = sandbox.stub(element, 'listChangesOptionsToHex',
- options => 'deadbeef');
- sandbox.stub(element, '_getChangeDetail',
- async (changeNum, options) => { return {changeNum, options}; });
- });
-
- test('signed pushes disabled', async () => {
- const {PUSH_CERTIFICATES} = element.ListChangesOption;
- sandbox.stub(element, 'getConfig', async () => { return {}; });
- const {changeNum, options} = await element.getChangeDetail(123);
- assert.strictEqual(123, changeNum);
- assert.strictEqual('deadbeef', options);
- assert.isTrue(toHexStub.calledOnce);
- assert.isFalse(toHexStub.lastCall.args.includes(PUSH_CERTIFICATES));
- });
-
- test('signed pushes enabled', async () => {
- const {PUSH_CERTIFICATES} = element.ListChangesOption;
- sandbox.stub(element, 'getConfig', async () => {
- return {receive: {enable_signed_push: true}};
- });
- const {changeNum, options} = await element.getChangeDetail(123);
- assert.strictEqual(123, changeNum);
- assert.strictEqual('deadbeef', options);
- assert.isTrue(toHexStub.calledOnce);
- assert.isTrue(toHexStub.lastCall.args.includes(PUSH_CERTIFICATES));
+ suite('_failForCreate200', () => {
+ test('_sendDiffDraftRequest checks for 200 on create', () => {
+ const sendPromise = Promise.resolve();
+ sandbox.stub(element, '_getChangeURLAndSend').returns(sendPromise);
+ const failStub = sandbox.stub(element, '_failForCreate200')
+ .returns(Promise.resolve());
+ return element._sendDiffDraftRequest('PUT', 123, 4, {}).then(() => {
+ assert.isTrue(failStub.calledOnce);
+ assert.isTrue(failStub.calledWithExactly(sendPromise));
});
});
- test('GrReviewerUpdatesParser.parse is used', () => {
- sandbox.stub(GrReviewerUpdatesParser, 'parse').returns(
- Promise.resolve('foo'));
- return element.getChangeDetail(42).then(result => {
- assert.isTrue(GrReviewerUpdatesParser.parse.calledOnce);
- assert.equal(result, 'foo');
- });
- });
-
- test('_getChangeDetail passes params to ETags decorator', () => {
- const changeNum = 4321;
- element._projectLookup[changeNum] = 'test';
- const expectedUrl =
- window.CANONICAL_PATH + '/changes/test~4321/detail?'+
- '0=5&1=1&2=6&3=7&4=1&5=4';
- sandbox.stub(element._etags, 'getOptions');
- sandbox.stub(element._etags, 'collect');
- return element._getChangeDetail(changeNum, '516714').then(() => {
- assert.isTrue(element._etags.getOptions.calledWithExactly(
- expectedUrl));
- assert.equal(element._etags.collect.lastCall.args[0], expectedUrl);
- });
- });
-
- test('_getChangeDetail calls errFn on 500', () => {
- const errFn = sinon.stub();
- sandbox.stub(element, 'getChangeActionURL')
- .returns(Promise.resolve(''));
- sandbox.stub(element._restApiHelper, 'fetchRawJSON')
- .returns(Promise.resolve({ok: false, status: 500}));
- return element._getChangeDetail(123, '516714', errFn).then(() => {
- assert.isTrue(errFn.called);
- });
- });
-
- test('_getChangeDetail populates _projectLookup', () => {
- sandbox.stub(element, 'getChangeActionURL')
- .returns(Promise.resolve(''));
- sandbox.stub(element._restApiHelper, 'fetchRawJSON')
- .returns(Promise.resolve({ok: true}));
-
- const mockResponse = {_number: 1, project: 'test'};
- sandbox.stub(element._restApiHelper, 'readResponsePayload')
- .returns(Promise.resolve({
- parsed: mockResponse,
- raw: JSON.stringify(mockResponse),
- }));
- return element._getChangeDetail(1, '516714').then(() => {
- assert.equal(Object.keys(element._projectLookup).length, 1);
- assert.equal(element._projectLookup[1], 'test');
- });
- });
-
- suite('_getChangeDetail ETag cache', () => {
- let requestUrl;
- let mockResponseSerial;
- let collectSpy;
- let getPayloadSpy;
-
- setup(() => {
- requestUrl = '/foo/bar';
- const mockResponse = {foo: 'bar', baz: 42};
- mockResponseSerial = element.JSON_PREFIX +
- JSON.stringify(mockResponse);
- sandbox.stub(element._restApiHelper, 'urlWithParams')
- .returns(requestUrl);
- sandbox.stub(element, 'getChangeActionURL')
- .returns(Promise.resolve(requestUrl));
- collectSpy = sandbox.spy(element._etags, 'collect');
- getPayloadSpy = sandbox.spy(element._etags, 'getCachedPayload');
- });
-
- test('contributes to cache', () => {
- sandbox.stub(element._restApiHelper, 'fetchRawJSON')
- .returns(Promise.resolve({
- text: () => Promise.resolve(mockResponseSerial),
- status: 200,
- ok: true,
- }));
-
- return element._getChangeDetail(123, '516714').then(detail => {
- assert.isFalse(getPayloadSpy.called);
- assert.isTrue(collectSpy.calledOnce);
- const cachedResponse = element._etags.getCachedPayload(requestUrl);
- assert.equal(cachedResponse, mockResponseSerial);
- });
- });
-
- test('uses cache on HTTP 304', () => {
- sandbox.stub(element._restApiHelper, 'fetchRawJSON')
- .returns(Promise.resolve({
- text: () => Promise.resolve(mockResponseSerial),
- status: 304,
- ok: true,
- }));
-
- return element._getChangeDetail(123, {}).then(detail => {
- assert.isFalse(collectSpy.called);
- assert.isTrue(getPayloadSpy.calledOnce);
- });
- });
- });
- });
-
- test('setInProjectLookup', () => {
- element.setInProjectLookup('test', 'project');
- assert.deepEqual(element._projectLookup, {test: 'project'});
- });
-
- suite('getFromProjectLookup', () => {
- test('getChange fails', () => {
- sandbox.stub(element, 'getChange')
- .returns(Promise.resolve(null));
- return element.getFromProjectLookup().then(val => {
- assert.strictEqual(val, undefined);
- assert.deepEqual(element._projectLookup, {});
- });
- });
-
- test('getChange succeeds, no project', () => {
- sandbox.stub(element, 'getChange').returns(Promise.resolve(null));
- return element.getFromProjectLookup().then(val => {
- assert.strictEqual(val, undefined);
- assert.deepEqual(element._projectLookup, {});
- });
- });
-
- test('getChange succeeds with project', () => {
- sandbox.stub(element, 'getChange')
- .returns(Promise.resolve({project: 'project'}));
- return element.getFromProjectLookup('test').then(val => {
- assert.equal(val, 'project');
- assert.deepEqual(element._projectLookup, {test: 'project'});
- });
- });
- });
-
- suite('getChanges populates _projectLookup', () => {
- test('multiple queries', () => {
- sandbox.stub(element._restApiHelper, 'fetchJSON')
- .returns(Promise.resolve([
- [
- {_number: 1, project: 'test'},
- {_number: 2, project: 'test'},
- ], [
- {_number: 3, project: 'test/test'},
- ],
- ]));
- // When opt_query instanceof Array, _fetchJSON returns
- // Array<Array<Object>>.
- return element.getChanges(null, []).then(() => {
- assert.equal(Object.keys(element._projectLookup).length, 3);
- assert.equal(element._projectLookup[1], 'test');
- assert.equal(element._projectLookup[2], 'test');
- assert.equal(element._projectLookup[3], 'test/test');
- });
- });
-
- test('no query', () => {
- sandbox.stub(element._restApiHelper, 'fetchJSON')
- .returns(Promise.resolve([
- {_number: 1, project: 'test'},
- {_number: 2, project: 'test'},
- {_number: 3, project: 'test/test'},
- ]));
-
- // When opt_query !instanceof Array, _fetchJSON returns
- // Array<Object>.
- return element.getChanges().then(() => {
- assert.equal(Object.keys(element._projectLookup).length, 3);
- assert.equal(element._projectLookup[1], 'test');
- assert.equal(element._projectLookup[2], 'test');
- assert.equal(element._projectLookup[3], 'test/test');
- });
- });
- });
-
- test('_getChangeURLAndFetch', () => {
- element._projectLookup = {1: 'test'};
- const fetchStub = sandbox.stub(element._restApiHelper, 'fetchJSON')
- .returns(Promise.resolve());
- const req = {changeNum: 1, endpoint: '/test', patchNum: 1};
- return element._getChangeURLAndFetch(req).then(() => {
- assert.equal(fetchStub.lastCall.args[0].url,
- '/changes/test~1/revisions/1/test');
- });
- });
-
- test('_getChangeURLAndSend', () => {
- element._projectLookup = {1: 'test'};
- const sendStub = sandbox.stub(element._restApiHelper, 'send')
- .returns(Promise.resolve());
-
- const req = {
- changeNum: 1,
- method: 'POST',
- patchNum: 1,
- endpoint: '/test',
- };
- return element._getChangeURLAndSend(req).then(() => {
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.equal(sendStub.lastCall.args[0].url,
- '/changes/test~1/revisions/1/test');
- });
- });
-
- suite('reading responses', () => {
- test('_readResponsePayload', () => {
- const mockObject = {foo: 'bar', baz: 'foo'};
- const serial = element.JSON_PREFIX + JSON.stringify(mockObject);
- const mockResponse = {text: () => Promise.resolve(serial)};
- return element._restApiHelper.readResponsePayload(mockResponse)
- .then(payload => {
- assert.deepEqual(payload.parsed, mockObject);
- assert.equal(payload.raw, serial);
+ test('_sendDiffDraftRequest no checks for 200 on non create', () => {
+ sandbox.stub(element, '_getChangeURLAndSend')
+ .returns(Promise.resolve());
+ const failStub = sandbox.stub(element, '_failForCreate200')
+ .returns(Promise.resolve());
+ return element._sendDiffDraftRequest('PUT', 123, 4, {id: '123'})
+ .then(() => {
+ assert.isFalse(failStub.called);
});
});
- test('_parsePrefixedJSON', () => {
- const obj = {x: 3, y: {z: 4}, w: 23};
- const serial = element.JSON_PREFIX + JSON.stringify(obj);
- const result = element._restApiHelper.parsePrefixedJSON(serial);
- assert.deepEqual(result, obj);
- });
- });
-
- test('setChangeTopic', () => {
- const sendSpy = sandbox.spy(element, '_getChangeURLAndSend');
- return element.setChangeTopic(123, 'foo-bar').then(() => {
- assert.isTrue(sendSpy.calledOnce);
- assert.deepEqual(sendSpy.lastCall.args[0].body, {topic: 'foo-bar'});
- });
- });
-
- test('setChangeHashtag', () => {
- const sendSpy = sandbox.spy(element, '_getChangeURLAndSend');
- return element.setChangeHashtag(123, 'foo-bar').then(() => {
- assert.isTrue(sendSpy.calledOnce);
- assert.equal(sendSpy.lastCall.args[0].body, 'foo-bar');
- });
- });
-
- test('generateAccountHttpPassword', () => {
- const sendSpy = sandbox.spy(element._restApiHelper, 'send');
- return element.generateAccountHttpPassword().then(() => {
- assert.isTrue(sendSpy.calledOnce);
- assert.deepEqual(sendSpy.lastCall.args[0].body, {generate: true});
- });
- });
-
- suite('getChangeFiles', () => {
- test('patch only', () => {
- const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- const range = {basePatchNum: 'PARENT', patchNum: 2};
- return element.getChangeFiles(123, range).then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].patchNum, 2);
- assert.isNotOk(fetchStub.lastCall.args[0].params);
- });
+ test('_failForCreate200 fails on 200', done => {
+ const result = {
+ ok: true,
+ status: 200,
+ headers: {entries: () => [
+ ['Set-CoOkiE', 'secret'],
+ ['Innocuous', 'hello'],
+ ]},
+ };
+ element._failForCreate200(Promise.resolve(result))
+ .then(() => {
+ assert.isTrue(false, 'Promise should not resolve');
+ })
+ .catch(e => {
+ assert.isOk(e);
+ assert.include(e.message, 'Saving draft resulted in HTTP 200');
+ assert.include(e.message, 'hello');
+ assert.notInclude(e.message, 'secret');
+ done();
+ });
});
- test('simple range', () => {
- const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- const range = {basePatchNum: 4, patchNum: 5};
- return element.getChangeFiles(123, range).then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].patchNum, 5);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.equal(fetchStub.lastCall.args[0].params.base, 4);
- assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
- });
- });
-
- test('parent index', () => {
- const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- const range = {basePatchNum: -3, patchNum: 5};
- return element.getChangeFiles(123, range).then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].patchNum, 5);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.isNotOk(fetchStub.lastCall.args[0].params.base);
- assert.equal(fetchStub.lastCall.args[0].params.parent, 3);
- });
- });
- });
-
- suite('getDiff', () => {
- test('patchOnly', () => {
- const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- return element.getDiff(123, 'PARENT', 2, 'foo/bar.baz').then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].patchNum, 2);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
- assert.isNotOk(fetchStub.lastCall.args[0].params.base);
- });
- });
-
- test('simple range', () => {
- const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- return element.getDiff(123, 4, 5, 'foo/bar.baz').then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].patchNum, 5);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
- assert.equal(fetchStub.lastCall.args[0].params.base, 4);
- });
- });
-
- test('parent index', () => {
- const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- return element.getDiff(123, -3, 5, 'foo/bar.baz').then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].patchNum, 5);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.isNotOk(fetchStub.lastCall.args[0].params.base);
- assert.equal(fetchStub.lastCall.args[0].params.parent, 3);
- });
- });
- });
-
- test('getDashboard', () => {
- const fetchCacheURLStub = sandbox.stub(element._restApiHelper,
- 'fetchCacheURL');
- element.getDashboard('gerrit/project', 'default:main');
- assert.isTrue(fetchCacheURLStub.calledOnce);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/gerrit%2Fproject/dashboards/default%3Amain');
- });
-
- test('getFileContent', () => {
- sandbox.stub(element, '_getChangeURLAndSend')
- .returns(Promise.resolve({
- ok: 'true',
- headers: {
- get(header) {
- if (header === 'X-FYI-Content-Type') {
- return 'text/java';
- }
- },
- },
- }));
-
- sandbox.stub(element, 'getResponseObject')
- .returns(Promise.resolve('new content'));
-
- const edit = element.getFileContent('1', 'tst/path', 'EDIT').then(res => {
- assert.deepEqual(res,
- {content: 'new content', type: 'text/java', ok: true});
- });
-
- const normal = element.getFileContent('1', 'tst/path', '3').then(res => {
- assert.deepEqual(res,
- {content: 'new content', type: 'text/java', ok: true});
- });
-
- return Promise.all([edit, normal]);
- });
-
- test('getFileContent suppresses 404s', done => {
- const res = {status: 404};
- const handler = e => {
- assert.isFalse(e.detail.res.status === 404);
- done();
- };
- element.addEventListener('server-error', handler);
- sandbox.stub(Gerrit.Auth, 'fetch').returns(Promise.resolve(res));
- sandbox.stub(element, '_changeBaseURL').returns(Promise.resolve(''));
- element.getFileContent('1', 'tst/path', '1').then(() => {
- flushAsynchronousOperations();
-
- res.status = 500;
- element.getFileContent('1', 'tst/path', '1');
- });
- });
-
- test('getChangeFilesOrEditFiles is edit-sensitive', () => {
- const fn = element.getChangeOrEditFiles.bind(element);
- const getChangeFilesStub = sandbox.stub(element, 'getChangeFiles')
- .returns(Promise.resolve({}));
- const getChangeEditFilesStub = sandbox.stub(element, 'getChangeEditFiles')
- .returns(Promise.resolve({}));
-
- return fn('1', {patchNum: 'edit'}).then(() => {
- assert.isTrue(getChangeEditFilesStub.calledOnce);
- assert.isFalse(getChangeFilesStub.called);
- return fn('1', {patchNum: '1'}).then(() => {
- assert.isTrue(getChangeEditFilesStub.calledOnce);
- assert.isTrue(getChangeFilesStub.calledOnce);
- });
- });
- });
-
- test('_fetch forwards request and logs', () => {
- const logStub = sandbox.stub(element._restApiHelper, '_logCall');
- const response = {status: 404, text: sinon.stub()};
- const url = 'my url';
- const fetchOptions = {method: 'DELETE'};
- sandbox.stub(element._auth, 'fetch').returns(Promise.resolve(response));
- const startTime = 123;
- sandbox.stub(Date, 'now').returns(startTime);
- const req = {url, fetchOptions};
- return element._restApiHelper.fetch(req).then(() => {
- assert.isTrue(logStub.calledOnce);
- assert.isTrue(logStub.calledWith(req, startTime, response.status));
- assert.isFalse(response.text.called);
- });
- });
-
- test('_logCall only reports requests with anonymized URLss', () => {
- sandbox.stub(Date, 'now').returns(200);
- const handler = sinon.stub();
- element.addEventListener('rpc-log', handler);
-
- element._restApiHelper._logCall({url: 'url'}, 100, 200);
- assert.isFalse(handler.called);
-
- element._restApiHelper
- ._logCall({url: 'url', anonymizedUrl: 'not url'}, 100, 200);
- flushAsynchronousOperations();
- assert.isTrue(handler.calledOnce);
- });
-
- test('saveChangeStarred', async () => {
- sandbox.stub(element, 'getFromProjectLookup')
- .returns(Promise.resolve('test'));
- const sendStub =
- sandbox.stub(element._restApiHelper, 'send').returns(Promise.resolve());
-
- await element.saveChangeStarred(123, true);
- assert.isTrue(sendStub.calledOnce);
- assert.deepEqual(sendStub.lastCall.args[0], {
- method: 'PUT',
- url: '/accounts/self/starred.changes/test~123',
- anonymizedUrl: '/accounts/self/starred.changes/*',
- });
-
- await element.saveChangeStarred(456, false);
- assert.isTrue(sendStub.calledTwice);
- assert.deepEqual(sendStub.lastCall.args[0], {
- method: 'DELETE',
- url: '/accounts/self/starred.changes/test~456',
- anonymizedUrl: '/accounts/self/starred.changes/*',
+ test('_failForCreate200 does not fail on 201', done => {
+ const result = {
+ ok: true,
+ status: 201,
+ headers: {entries: () => []},
+ };
+ element._failForCreate200(Promise.resolve(result))
+ .then(() => {
+ done();
+ })
+ .catch(e => {
+ assert.isTrue(false, 'Promise should not fail');
+ });
});
});
});
+
+ test('saveChangeEdit', () => {
+ element._projectLookup = {1: 'test'};
+ const change_num = '1';
+ const file_name = 'index.php';
+ const file_contents = '<?php';
+ sandbox.stub(element._restApiHelper, 'send').returns(
+ Promise.resolve([change_num, file_name, file_contents]));
+ sandbox.stub(element, 'getResponseObject')
+ .returns(Promise.resolve([change_num, file_name, file_contents]));
+ element._cache.set('/changes/' + change_num + '/edit/' + file_name, {});
+ return element.saveChangeEdit(change_num, file_name, file_contents)
+ .then(() => {
+ assert.isTrue(element._restApiHelper.send.calledOnce);
+ assert.equal(element._restApiHelper.send.lastCall.args[0].method,
+ 'PUT');
+ assert.equal(element._restApiHelper.send.lastCall.args[0].url,
+ '/changes/test~1/edit/' + file_name);
+ assert.equal(element._restApiHelper.send.lastCall.args[0].body,
+ file_contents);
+ });
+ });
+
+ test('putChangeCommitMessage', () => {
+ element._projectLookup = {1: 'test'};
+ const change_num = '1';
+ const message = 'this is a commit message';
+ sandbox.stub(element._restApiHelper, 'send').returns(
+ Promise.resolve([change_num, message]));
+ sandbox.stub(element, 'getResponseObject')
+ .returns(Promise.resolve([change_num, message]));
+ element._cache.set('/changes/' + change_num + '/message', {});
+ return element.putChangeCommitMessage(change_num, message).then(() => {
+ assert.isTrue(element._restApiHelper.send.calledOnce);
+ assert.equal(element._restApiHelper.send.lastCall.args[0].method, 'PUT');
+ assert.equal(element._restApiHelper.send.lastCall.args[0].url,
+ '/changes/test~1/message');
+ assert.deepEqual(element._restApiHelper.send.lastCall.args[0].body,
+ {message});
+ });
+ });
+
+ test('deleteChangeCommitMessage', () => {
+ element._projectLookup = {1: 'test'};
+ const change_num = '1';
+ const messageId = 'abc';
+ sandbox.stub(element._restApiHelper, 'send').returns(
+ Promise.resolve([change_num, messageId]));
+ sandbox.stub(element, 'getResponseObject')
+ .returns(Promise.resolve([change_num, messageId]));
+ return element.deleteChangeCommitMessage(change_num, messageId).then(() => {
+ assert.isTrue(element._restApiHelper.send.calledOnce);
+ assert.equal(
+ element._restApiHelper.send.lastCall.args[0].method,
+ 'DELETE'
+ );
+ assert.equal(element._restApiHelper.send.lastCall.args[0].url,
+ '/changes/test~1/messages/abc');
+ });
+ });
+
+ test('startWorkInProgress', () => {
+ const sendStub = sandbox.stub(element, '_getChangeURLAndSend')
+ .returns(Promise.resolve('ok'));
+ element.startWorkInProgress('42');
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].changeNum, '42');
+ assert.equal(sendStub.lastCall.args[0].method, 'POST');
+ assert.isNotOk(sendStub.lastCall.args[0].patchNum);
+ assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
+ assert.deepEqual(sendStub.lastCall.args[0].body, {});
+
+ element.startWorkInProgress('42', 'revising...');
+ assert.isTrue(sendStub.calledTwice);
+ assert.equal(sendStub.lastCall.args[0].changeNum, '42');
+ assert.equal(sendStub.lastCall.args[0].method, 'POST');
+ assert.isNotOk(sendStub.lastCall.args[0].patchNum);
+ assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
+ assert.deepEqual(sendStub.lastCall.args[0].body,
+ {message: 'revising...'});
+ });
+
+ test('startReview', () => {
+ const sendStub = sandbox.stub(element, '_getChangeURLAndSend')
+ .returns(Promise.resolve({}));
+ element.startReview('42', {message: 'Please review.'});
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].changeNum, '42');
+ assert.equal(sendStub.lastCall.args[0].method, 'POST');
+ assert.isNotOk(sendStub.lastCall.args[0].patchNum);
+ assert.equal(sendStub.lastCall.args[0].endpoint, '/ready');
+ assert.deepEqual(sendStub.lastCall.args[0].body,
+ {message: 'Please review.'});
+ });
+
+ test('deleteComment', () => {
+ const sendStub = sandbox.stub(element, '_getChangeURLAndSend')
+ .returns(Promise.resolve('some response'));
+ return element.deleteComment('foo', 'bar', '01234', 'removal reason')
+ .then(response => {
+ assert.equal(response, 'some response');
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].changeNum, 'foo');
+ assert.equal(sendStub.lastCall.args[0].method, 'POST');
+ assert.equal(sendStub.lastCall.args[0].patchNum, 'bar');
+ assert.equal(sendStub.lastCall.args[0].endpoint,
+ '/comments/01234/delete');
+ assert.deepEqual(sendStub.lastCall.args[0].body,
+ {reason: 'removal reason'});
+ });
+ });
+
+ test('createRepo encodes name', () => {
+ const sendStub = sandbox.stub(element._restApiHelper, 'send')
+ .returns(Promise.resolve());
+ return element.createRepo({name: 'x/y'}).then(() => {
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].url, '/projects/x%2Fy');
+ });
+ });
+
+ test('queryChangeFiles', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ return element.queryChangeFiles('42', 'edit', 'test/path.js').then(() => {
+ assert.equal(fetchStub.lastCall.args[0].changeNum, '42');
+ assert.equal(fetchStub.lastCall.args[0].endpoint,
+ '/files?q=test%2Fpath.js');
+ assert.equal(fetchStub.lastCall.args[0].patchNum, 'edit');
+ });
+ });
+
+ test('normal use', () => {
+ const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only';
+
+ assert.equal(element._getReposUrl('test', 25),
+ '/projects/?n=26&S=0&query=test');
+
+ assert.equal(element._getReposUrl(null, 25),
+ `/projects/?n=26&S=0&query=${defaultQuery}`);
+
+ assert.equal(element._getReposUrl('test', 25, 25),
+ '/projects/?n=26&S=25&query=test');
+ });
+
+ test('invalidateReposCache', () => {
+ const url = '/projects/?n=26&S=0&query=test';
+
+ element._cache.set(url, {});
+
+ element.invalidateReposCache();
+
+ assert.isUndefined(element._sharedFetchPromises[url]);
+
+ assert.isFalse(element._cache.has(url));
+ });
+
+ test('invalidateAccountsCache', () => {
+ const url = '/accounts/self/detail';
+
+ element._cache.set(url, {});
+
+ element.invalidateAccountsCache();
+
+ assert.isUndefined(element._sharedFetchPromises[url]);
+
+ assert.isFalse(element._cache.has(url));
+ });
+
+ suite('getRepos', () => {
+ const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only';
+ let fetchCacheURLStub;
+ setup(() => {
+ fetchCacheURLStub =
+ sandbox.stub(element._restApiHelper, 'fetchCacheURL');
+ });
+
+ test('normal use', () => {
+ element.getRepos('test', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&query=test');
+
+ element.getRepos(null, 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ `/projects/?n=26&S=0&query=${defaultQuery}`);
+
+ element.getRepos('test', 25, 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=25&query=test');
+ });
+
+ test('with blank', () => {
+ element.getRepos('test/test', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&query=inname%3Atest%20AND%20inname%3Atest');
+ });
+
+ test('with hyphen', () => {
+ element.getRepos('foo-bar', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
+ });
+
+ test('with leading hyphen', () => {
+ element.getRepos('-bar', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&query=inname%3Abar');
+ });
+
+ test('with trailing hyphen', () => {
+ element.getRepos('foo-bar-', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
+ });
+
+ test('with underscore', () => {
+ element.getRepos('foo_bar', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
+ });
+
+ test('with underscore', () => {
+ element.getRepos('foo_bar', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
+ });
+
+ test('hyphen only', () => {
+ element.getRepos('-', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ `/projects/?n=26&S=0&query=${defaultQuery}`);
+ });
+ });
+
+ test('_getGroupsUrl normal use', () => {
+ assert.equal(element._getGroupsUrl('test', 25),
+ '/groups/?n=26&S=0&m=test');
+
+ assert.equal(element._getGroupsUrl(null, 25),
+ '/groups/?n=26&S=0');
+
+ assert.equal(element._getGroupsUrl('test', 25, 25),
+ '/groups/?n=26&S=25&m=test');
+ });
+
+ test('invalidateGroupsCache', () => {
+ const url = '/groups/?n=26&S=0&m=test';
+
+ element._cache.set(url, {});
+
+ element.invalidateGroupsCache();
+
+ assert.isUndefined(element._sharedFetchPromises[url]);
+
+ assert.isFalse(element._cache.has(url));
+ });
+
+ suite('getGroups', () => {
+ let fetchCacheURLStub;
+ setup(() => {
+ fetchCacheURLStub =
+ sandbox.stub(element._restApiHelper, 'fetchCacheURL');
+ });
+
+ test('normal use', () => {
+ element.getGroups('test', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=0&m=test');
+
+ element.getGroups(null, 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=0');
+
+ element.getGroups('test', 25, 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=25&m=test');
+ });
+
+ test('regex', () => {
+ element.getGroups('^test.*', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=0&r=%5Etest.*');
+
+ element.getGroups('^test.*', 25, 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=25&r=%5Etest.*');
+ });
+ });
+
+ test('gerrit auth is used', () => {
+ sandbox.stub(Gerrit.Auth, 'fetch').returns(Promise.resolve());
+ element._restApiHelper.fetchJSON({url: 'foo'});
+ assert(Gerrit.Auth.fetch.called);
+ });
+
+ test('getSuggestedAccounts does not return _fetchJSON', () => {
+ const _fetchJSONSpy = sandbox.spy(element._restApiHelper, 'fetchJSON');
+ return element.getSuggestedAccounts().then(accts => {
+ assert.isFalse(_fetchJSONSpy.called);
+ assert.equal(accts.length, 0);
+ });
+ });
+
+ test('_fetchJSON gets called by getSuggestedAccounts', () => {
+ const _fetchJSONStub = sandbox.stub(element._restApiHelper, 'fetchJSON',
+ () => Promise.resolve());
+ return element.getSuggestedAccounts('own').then(() => {
+ assert.deepEqual(_fetchJSONStub.lastCall.args[0].params, {
+ q: 'own',
+ suggest: null,
+ });
+ });
+ });
+
+ suite('getChangeDetail', () => {
+ suite('change detail options', () => {
+ let toHexStub;
+
+ setup(() => {
+ toHexStub = sandbox.stub(element, 'listChangesOptionsToHex',
+ options => 'deadbeef');
+ sandbox.stub(element, '_getChangeDetail',
+ async (changeNum, options) => { return {changeNum, options}; });
+ });
+
+ test('signed pushes disabled', async () => {
+ const {PUSH_CERTIFICATES} = element.ListChangesOption;
+ sandbox.stub(element, 'getConfig', async () => { return {}; });
+ const {changeNum, options} = await element.getChangeDetail(123);
+ assert.strictEqual(123, changeNum);
+ assert.strictEqual('deadbeef', options);
+ assert.isTrue(toHexStub.calledOnce);
+ assert.isFalse(toHexStub.lastCall.args.includes(PUSH_CERTIFICATES));
+ });
+
+ test('signed pushes enabled', async () => {
+ const {PUSH_CERTIFICATES} = element.ListChangesOption;
+ sandbox.stub(element, 'getConfig', async () => {
+ return {receive: {enable_signed_push: true}};
+ });
+ const {changeNum, options} = await element.getChangeDetail(123);
+ assert.strictEqual(123, changeNum);
+ assert.strictEqual('deadbeef', options);
+ assert.isTrue(toHexStub.calledOnce);
+ assert.isTrue(toHexStub.lastCall.args.includes(PUSH_CERTIFICATES));
+ });
+ });
+
+ test('GrReviewerUpdatesParser.parse is used', () => {
+ sandbox.stub(GrReviewerUpdatesParser, 'parse').returns(
+ Promise.resolve('foo'));
+ return element.getChangeDetail(42).then(result => {
+ assert.isTrue(GrReviewerUpdatesParser.parse.calledOnce);
+ assert.equal(result, 'foo');
+ });
+ });
+
+ test('_getChangeDetail passes params to ETags decorator', () => {
+ const changeNum = 4321;
+ element._projectLookup[changeNum] = 'test';
+ const expectedUrl =
+ window.CANONICAL_PATH + '/changes/test~4321/detail?'+
+ '0=5&1=1&2=6&3=7&4=1&5=4';
+ sandbox.stub(element._etags, 'getOptions');
+ sandbox.stub(element._etags, 'collect');
+ return element._getChangeDetail(changeNum, '516714').then(() => {
+ assert.isTrue(element._etags.getOptions.calledWithExactly(
+ expectedUrl));
+ assert.equal(element._etags.collect.lastCall.args[0], expectedUrl);
+ });
+ });
+
+ test('_getChangeDetail calls errFn on 500', () => {
+ const errFn = sinon.stub();
+ sandbox.stub(element, 'getChangeActionURL')
+ .returns(Promise.resolve(''));
+ sandbox.stub(element._restApiHelper, 'fetchRawJSON')
+ .returns(Promise.resolve({ok: false, status: 500}));
+ return element._getChangeDetail(123, '516714', errFn).then(() => {
+ assert.isTrue(errFn.called);
+ });
+ });
+
+ test('_getChangeDetail populates _projectLookup', () => {
+ sandbox.stub(element, 'getChangeActionURL')
+ .returns(Promise.resolve(''));
+ sandbox.stub(element._restApiHelper, 'fetchRawJSON')
+ .returns(Promise.resolve({ok: true}));
+
+ const mockResponse = {_number: 1, project: 'test'};
+ sandbox.stub(element._restApiHelper, 'readResponsePayload')
+ .returns(Promise.resolve({
+ parsed: mockResponse,
+ raw: JSON.stringify(mockResponse),
+ }));
+ return element._getChangeDetail(1, '516714').then(() => {
+ assert.equal(Object.keys(element._projectLookup).length, 1);
+ assert.equal(element._projectLookup[1], 'test');
+ });
+ });
+
+ suite('_getChangeDetail ETag cache', () => {
+ let requestUrl;
+ let mockResponseSerial;
+ let collectSpy;
+ let getPayloadSpy;
+
+ setup(() => {
+ requestUrl = '/foo/bar';
+ const mockResponse = {foo: 'bar', baz: 42};
+ mockResponseSerial = element.JSON_PREFIX +
+ JSON.stringify(mockResponse);
+ sandbox.stub(element._restApiHelper, 'urlWithParams')
+ .returns(requestUrl);
+ sandbox.stub(element, 'getChangeActionURL')
+ .returns(Promise.resolve(requestUrl));
+ collectSpy = sandbox.spy(element._etags, 'collect');
+ getPayloadSpy = sandbox.spy(element._etags, 'getCachedPayload');
+ });
+
+ test('contributes to cache', () => {
+ sandbox.stub(element._restApiHelper, 'fetchRawJSON')
+ .returns(Promise.resolve({
+ text: () => Promise.resolve(mockResponseSerial),
+ status: 200,
+ ok: true,
+ }));
+
+ return element._getChangeDetail(123, '516714').then(detail => {
+ assert.isFalse(getPayloadSpy.called);
+ assert.isTrue(collectSpy.calledOnce);
+ const cachedResponse = element._etags.getCachedPayload(requestUrl);
+ assert.equal(cachedResponse, mockResponseSerial);
+ });
+ });
+
+ test('uses cache on HTTP 304', () => {
+ sandbox.stub(element._restApiHelper, 'fetchRawJSON')
+ .returns(Promise.resolve({
+ text: () => Promise.resolve(mockResponseSerial),
+ status: 304,
+ ok: true,
+ }));
+
+ return element._getChangeDetail(123, {}).then(detail => {
+ assert.isFalse(collectSpy.called);
+ assert.isTrue(getPayloadSpy.calledOnce);
+ });
+ });
+ });
+ });
+
+ test('setInProjectLookup', () => {
+ element.setInProjectLookup('test', 'project');
+ assert.deepEqual(element._projectLookup, {test: 'project'});
+ });
+
+ suite('getFromProjectLookup', () => {
+ test('getChange fails', () => {
+ sandbox.stub(element, 'getChange')
+ .returns(Promise.resolve(null));
+ return element.getFromProjectLookup().then(val => {
+ assert.strictEqual(val, undefined);
+ assert.deepEqual(element._projectLookup, {});
+ });
+ });
+
+ test('getChange succeeds, no project', () => {
+ sandbox.stub(element, 'getChange').returns(Promise.resolve(null));
+ return element.getFromProjectLookup().then(val => {
+ assert.strictEqual(val, undefined);
+ assert.deepEqual(element._projectLookup, {});
+ });
+ });
+
+ test('getChange succeeds with project', () => {
+ sandbox.stub(element, 'getChange')
+ .returns(Promise.resolve({project: 'project'}));
+ return element.getFromProjectLookup('test').then(val => {
+ assert.equal(val, 'project');
+ assert.deepEqual(element._projectLookup, {test: 'project'});
+ });
+ });
+ });
+
+ suite('getChanges populates _projectLookup', () => {
+ test('multiple queries', () => {
+ sandbox.stub(element._restApiHelper, 'fetchJSON')
+ .returns(Promise.resolve([
+ [
+ {_number: 1, project: 'test'},
+ {_number: 2, project: 'test'},
+ ], [
+ {_number: 3, project: 'test/test'},
+ ],
+ ]));
+ // When opt_query instanceof Array, _fetchJSON returns
+ // Array<Array<Object>>.
+ return element.getChanges(null, []).then(() => {
+ assert.equal(Object.keys(element._projectLookup).length, 3);
+ assert.equal(element._projectLookup[1], 'test');
+ assert.equal(element._projectLookup[2], 'test');
+ assert.equal(element._projectLookup[3], 'test/test');
+ });
+ });
+
+ test('no query', () => {
+ sandbox.stub(element._restApiHelper, 'fetchJSON')
+ .returns(Promise.resolve([
+ {_number: 1, project: 'test'},
+ {_number: 2, project: 'test'},
+ {_number: 3, project: 'test/test'},
+ ]));
+
+ // When opt_query !instanceof Array, _fetchJSON returns
+ // Array<Object>.
+ return element.getChanges().then(() => {
+ assert.equal(Object.keys(element._projectLookup).length, 3);
+ assert.equal(element._projectLookup[1], 'test');
+ assert.equal(element._projectLookup[2], 'test');
+ assert.equal(element._projectLookup[3], 'test/test');
+ });
+ });
+ });
+
+ test('_getChangeURLAndFetch', () => {
+ element._projectLookup = {1: 'test'};
+ const fetchStub = sandbox.stub(element._restApiHelper, 'fetchJSON')
+ .returns(Promise.resolve());
+ const req = {changeNum: 1, endpoint: '/test', patchNum: 1};
+ return element._getChangeURLAndFetch(req).then(() => {
+ assert.equal(fetchStub.lastCall.args[0].url,
+ '/changes/test~1/revisions/1/test');
+ });
+ });
+
+ test('_getChangeURLAndSend', () => {
+ element._projectLookup = {1: 'test'};
+ const sendStub = sandbox.stub(element._restApiHelper, 'send')
+ .returns(Promise.resolve());
+
+ const req = {
+ changeNum: 1,
+ method: 'POST',
+ patchNum: 1,
+ endpoint: '/test',
+ };
+ return element._getChangeURLAndSend(req).then(() => {
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].method, 'POST');
+ assert.equal(sendStub.lastCall.args[0].url,
+ '/changes/test~1/revisions/1/test');
+ });
+ });
+
+ suite('reading responses', () => {
+ test('_readResponsePayload', () => {
+ const mockObject = {foo: 'bar', baz: 'foo'};
+ const serial = element.JSON_PREFIX + JSON.stringify(mockObject);
+ const mockResponse = {text: () => Promise.resolve(serial)};
+ return element._restApiHelper.readResponsePayload(mockResponse)
+ .then(payload => {
+ assert.deepEqual(payload.parsed, mockObject);
+ assert.equal(payload.raw, serial);
+ });
+ });
+
+ test('_parsePrefixedJSON', () => {
+ const obj = {x: 3, y: {z: 4}, w: 23};
+ const serial = element.JSON_PREFIX + JSON.stringify(obj);
+ const result = element._restApiHelper.parsePrefixedJSON(serial);
+ assert.deepEqual(result, obj);
+ });
+ });
+
+ test('setChangeTopic', () => {
+ const sendSpy = sandbox.spy(element, '_getChangeURLAndSend');
+ return element.setChangeTopic(123, 'foo-bar').then(() => {
+ assert.isTrue(sendSpy.calledOnce);
+ assert.deepEqual(sendSpy.lastCall.args[0].body, {topic: 'foo-bar'});
+ });
+ });
+
+ test('setChangeHashtag', () => {
+ const sendSpy = sandbox.spy(element, '_getChangeURLAndSend');
+ return element.setChangeHashtag(123, 'foo-bar').then(() => {
+ assert.isTrue(sendSpy.calledOnce);
+ assert.equal(sendSpy.lastCall.args[0].body, 'foo-bar');
+ });
+ });
+
+ test('generateAccountHttpPassword', () => {
+ const sendSpy = sandbox.spy(element._restApiHelper, 'send');
+ return element.generateAccountHttpPassword().then(() => {
+ assert.isTrue(sendSpy.calledOnce);
+ assert.deepEqual(sendSpy.lastCall.args[0].body, {generate: true});
+ });
+ });
+
+ suite('getChangeFiles', () => {
+ test('patch only', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ const range = {basePatchNum: 'PARENT', patchNum: 2};
+ return element.getChangeFiles(123, range).then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].patchNum, 2);
+ assert.isNotOk(fetchStub.lastCall.args[0].params);
+ });
+ });
+
+ test('simple range', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ const range = {basePatchNum: 4, patchNum: 5};
+ return element.getChangeFiles(123, range).then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].patchNum, 5);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.equal(fetchStub.lastCall.args[0].params.base, 4);
+ assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
+ });
+ });
+
+ test('parent index', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ const range = {basePatchNum: -3, patchNum: 5};
+ return element.getChangeFiles(123, range).then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].patchNum, 5);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.isNotOk(fetchStub.lastCall.args[0].params.base);
+ assert.equal(fetchStub.lastCall.args[0].params.parent, 3);
+ });
+ });
+ });
+
+ suite('getDiff', () => {
+ test('patchOnly', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ return element.getDiff(123, 'PARENT', 2, 'foo/bar.baz').then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].patchNum, 2);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
+ assert.isNotOk(fetchStub.lastCall.args[0].params.base);
+ });
+ });
+
+ test('simple range', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ return element.getDiff(123, 4, 5, 'foo/bar.baz').then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].patchNum, 5);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
+ assert.equal(fetchStub.lastCall.args[0].params.base, 4);
+ });
+ });
+
+ test('parent index', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ return element.getDiff(123, -3, 5, 'foo/bar.baz').then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].patchNum, 5);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.isNotOk(fetchStub.lastCall.args[0].params.base);
+ assert.equal(fetchStub.lastCall.args[0].params.parent, 3);
+ });
+ });
+ });
+
+ test('getDashboard', () => {
+ const fetchCacheURLStub = sandbox.stub(element._restApiHelper,
+ 'fetchCacheURL');
+ element.getDashboard('gerrit/project', 'default:main');
+ assert.isTrue(fetchCacheURLStub.calledOnce);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/gerrit%2Fproject/dashboards/default%3Amain');
+ });
+
+ test('getFileContent', () => {
+ sandbox.stub(element, '_getChangeURLAndSend')
+ .returns(Promise.resolve({
+ ok: 'true',
+ headers: {
+ get(header) {
+ if (header === 'X-FYI-Content-Type') {
+ return 'text/java';
+ }
+ },
+ },
+ }));
+
+ sandbox.stub(element, 'getResponseObject')
+ .returns(Promise.resolve('new content'));
+
+ const edit = element.getFileContent('1', 'tst/path', 'EDIT').then(res => {
+ assert.deepEqual(res,
+ {content: 'new content', type: 'text/java', ok: true});
+ });
+
+ const normal = element.getFileContent('1', 'tst/path', '3').then(res => {
+ assert.deepEqual(res,
+ {content: 'new content', type: 'text/java', ok: true});
+ });
+
+ return Promise.all([edit, normal]);
+ });
+
+ test('getFileContent suppresses 404s', done => {
+ const res = {status: 404};
+ const handler = e => {
+ assert.isFalse(e.detail.res.status === 404);
+ done();
+ };
+ element.addEventListener('server-error', handler);
+ sandbox.stub(Gerrit.Auth, 'fetch').returns(Promise.resolve(res));
+ sandbox.stub(element, '_changeBaseURL').returns(Promise.resolve(''));
+ element.getFileContent('1', 'tst/path', '1').then(() => {
+ flushAsynchronousOperations();
+
+ res.status = 500;
+ element.getFileContent('1', 'tst/path', '1');
+ });
+ });
+
+ test('getChangeFilesOrEditFiles is edit-sensitive', () => {
+ const fn = element.getChangeOrEditFiles.bind(element);
+ const getChangeFilesStub = sandbox.stub(element, 'getChangeFiles')
+ .returns(Promise.resolve({}));
+ const getChangeEditFilesStub = sandbox.stub(element, 'getChangeEditFiles')
+ .returns(Promise.resolve({}));
+
+ return fn('1', {patchNum: 'edit'}).then(() => {
+ assert.isTrue(getChangeEditFilesStub.calledOnce);
+ assert.isFalse(getChangeFilesStub.called);
+ return fn('1', {patchNum: '1'}).then(() => {
+ assert.isTrue(getChangeEditFilesStub.calledOnce);
+ assert.isTrue(getChangeFilesStub.calledOnce);
+ });
+ });
+ });
+
+ test('_fetch forwards request and logs', () => {
+ const logStub = sandbox.stub(element._restApiHelper, '_logCall');
+ const response = {status: 404, text: sinon.stub()};
+ const url = 'my url';
+ const fetchOptions = {method: 'DELETE'};
+ sandbox.stub(element._auth, 'fetch').returns(Promise.resolve(response));
+ const startTime = 123;
+ sandbox.stub(Date, 'now').returns(startTime);
+ const req = {url, fetchOptions};
+ return element._restApiHelper.fetch(req).then(() => {
+ assert.isTrue(logStub.calledOnce);
+ assert.isTrue(logStub.calledWith(req, startTime, response.status));
+ assert.isFalse(response.text.called);
+ });
+ });
+
+ test('_logCall only reports requests with anonymized URLss', () => {
+ sandbox.stub(Date, 'now').returns(200);
+ const handler = sinon.stub();
+ element.addEventListener('rpc-log', handler);
+
+ element._restApiHelper._logCall({url: 'url'}, 100, 200);
+ assert.isFalse(handler.called);
+
+ element._restApiHelper
+ ._logCall({url: 'url', anonymizedUrl: 'not url'}, 100, 200);
+ flushAsynchronousOperations();
+ assert.isTrue(handler.calledOnce);
+ });
+
+ test('saveChangeStarred', async () => {
+ sandbox.stub(element, 'getFromProjectLookup')
+ .returns(Promise.resolve('test'));
+ const sendStub =
+ sandbox.stub(element._restApiHelper, 'send').returns(Promise.resolve());
+
+ await element.saveChangeStarred(123, true);
+ assert.isTrue(sendStub.calledOnce);
+ assert.deepEqual(sendStub.lastCall.args[0], {
+ method: 'PUT',
+ url: '/accounts/self/starred.changes/test~123',
+ anonymizedUrl: '/accounts/self/starred.changes/*',
+ });
+
+ await element.saveChangeStarred(456, false);
+ assert.isTrue(sendStub.calledTwice);
+ assert.deepEqual(sendStub.lastCall.args[0], {
+ method: 'DELETE',
+ url: '/accounts/self/starred.changes/test~456',
+ anonymizedUrl: '/accounts/self/starred.changes/*',
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html
index 310063c..5ca70a3 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html
@@ -19,158 +19,154 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-rest-api-helper</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../../test/common-test-setup.html"/>
-<script src="../../../../scripts/util.js"></script>
-<script src="../gr-auth.js"></script>
-<script src="gr-rest-api-helper.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../../test/common-test-setup.js';
+import '../../../../scripts/util.js';
+import '../gr-auth.js';
+import './gr-rest-api-helper.js';
+suite('gr-rest-api-helper tests', () => {
+ let helper;
+ let sandbox;
+ let cache;
+ let fetchPromisesCache;
-<script>
- suite('gr-rest-api-helper tests', async () => {
- await readyToTest();
- let helper;
- let sandbox;
- let cache;
- let fetchPromisesCache;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ cache = new SiteBasedCache();
+ fetchPromisesCache = new FetchPromisesCache();
- setup(() => {
- sandbox = sinon.sandbox.create();
- cache = new SiteBasedCache();
- fetchPromisesCache = new FetchPromisesCache();
+ window.CANONICAL_PATH = 'testhelper';
- window.CANONICAL_PATH = 'testhelper';
+ const mockRestApiInterface = {
+ getBaseUrl: sinon.stub().returns(window.CANONICAL_PATH),
+ fire: sinon.stub(),
+ };
- const mockRestApiInterface = {
- getBaseUrl: sinon.stub().returns(window.CANONICAL_PATH),
- fire: sinon.stub(),
- };
+ const testJSON = ')]}\'\n{"hello": "bonjour"}';
+ sandbox.stub(window, 'fetch').returns(Promise.resolve({
+ ok: true,
+ text() {
+ return Promise.resolve(testJSON);
+ },
+ }));
- const testJSON = ')]}\'\n{"hello": "bonjour"}';
- sandbox.stub(window, 'fetch').returns(Promise.resolve({
- ok: true,
- text() {
- return Promise.resolve(testJSON);
- },
- }));
+ helper = new GrRestApiHelper(cache, Gerrit.Auth, fetchPromisesCache,
+ mockRestApiInterface);
+ });
- helper = new GrRestApiHelper(cache, Gerrit.Auth, fetchPromisesCache,
- mockRestApiInterface);
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('fetchJSON()', () => {
+ test('Sets header to accept application/json', () => {
+ const authFetchStub = sandbox.stub(helper._auth, 'fetch')
+ .returns(Promise.resolve());
+ helper.fetchJSON({url: '/dummy/url'});
+ assert.isTrue(authFetchStub.called);
+ assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
+ 'application/json');
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('fetchJSON()', () => {
- test('Sets header to accept application/json', () => {
- const authFetchStub = sandbox.stub(helper._auth, 'fetch')
- .returns(Promise.resolve());
- helper.fetchJSON({url: '/dummy/url'});
- assert.isTrue(authFetchStub.called);
- assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
- 'application/json');
- });
-
- test('Use header option accept when provided', () => {
- const authFetchStub = sandbox.stub(helper._auth, 'fetch')
- .returns(Promise.resolve());
- const headers = new Headers();
- headers.append('Accept', '*/*');
- const fetchOptions = {headers};
- helper.fetchJSON({url: '/dummy/url', fetchOptions});
- assert.isTrue(authFetchStub.called);
- assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
- '*/*');
- });
- });
-
- test('JSON prefix is properly removed', done => {
- helper.fetchJSON({url: '/dummy/url'}).then(obj => {
- assert.deepEqual(obj, {hello: 'bonjour'});
- done();
- });
- });
-
- test('cached results', done => {
- let n = 0;
- sandbox.stub(helper, 'fetchJSON', () => Promise.resolve(++n));
- const promises = [];
- promises.push(helper.fetchCacheURL('/foo'));
- promises.push(helper.fetchCacheURL('/foo'));
- promises.push(helper.fetchCacheURL('/foo'));
-
- Promise.all(promises).then(results => {
- assert.deepEqual(results, [1, 1, 1]);
- helper.fetchCacheURL('/foo').then(foo => {
- assert.equal(foo, 1);
- done();
- });
- });
- });
-
- test('cached promise', done => {
- const promise = Promise.reject(new Error('foo'));
- cache.set('/foo', promise);
- helper.fetchCacheURL({url: '/foo'}).catch(p => {
- assert.equal(p.message, 'foo');
- done();
- });
- });
-
- test('cache invalidation', () => {
- cache.set('/foo/bar', 1);
- cache.set('/bar', 2);
- fetchPromisesCache.set('/foo/bar', 3);
- fetchPromisesCache.set('/bar', 4);
- helper.invalidateFetchPromisesPrefix('/foo/');
- assert.isFalse(cache.has('/foo/bar'));
- assert.isTrue(cache.has('/bar'));
- assert.isUndefined(fetchPromisesCache.get('/foo/bar'));
- assert.strictEqual(4, fetchPromisesCache.get('/bar'));
- });
-
- test('params are properly encoded', () => {
- let url = helper.urlWithParams('/path/', {
- sp: 'hola',
- gr: 'guten tag',
- noval: null,
- });
- assert.equal(url,
- window.CANONICAL_PATH + '/path/?sp=hola&gr=guten%20tag&noval');
-
- url = helper.urlWithParams('/path/', {
- sp: 'hola',
- en: ['hey', 'hi'],
- });
- assert.equal(url, window.CANONICAL_PATH + '/path/?sp=hola&en=hey&en=hi');
-
- // Order must be maintained with array params.
- url = helper.urlWithParams('/path/', {
- l: ['c', 'b', 'a'],
- });
- assert.equal(url, window.CANONICAL_PATH + '/path/?l=c&l=b&l=a');
- });
-
- test('request callbacks can be canceled', done => {
- let cancelCalled = false;
- window.fetch.returns(Promise.resolve({
- body: {
- cancel() { cancelCalled = true; },
- },
- }));
- const cancelCondition = () => true;
- helper.fetchJSON({url: '/dummy/url', cancelCondition}).then(
- obj => {
- assert.isUndefined(obj);
- assert.isTrue(cancelCalled);
- done();
- });
+ test('Use header option accept when provided', () => {
+ const authFetchStub = sandbox.stub(helper._auth, 'fetch')
+ .returns(Promise.resolve());
+ const headers = new Headers();
+ headers.append('Accept', '*/*');
+ const fetchOptions = {headers};
+ helper.fetchJSON({url: '/dummy/url', fetchOptions});
+ assert.isTrue(authFetchStub.called);
+ assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
+ '*/*');
});
});
+
+ test('JSON prefix is properly removed', done => {
+ helper.fetchJSON({url: '/dummy/url'}).then(obj => {
+ assert.deepEqual(obj, {hello: 'bonjour'});
+ done();
+ });
+ });
+
+ test('cached results', done => {
+ let n = 0;
+ sandbox.stub(helper, 'fetchJSON', () => Promise.resolve(++n));
+ const promises = [];
+ promises.push(helper.fetchCacheURL('/foo'));
+ promises.push(helper.fetchCacheURL('/foo'));
+ promises.push(helper.fetchCacheURL('/foo'));
+
+ Promise.all(promises).then(results => {
+ assert.deepEqual(results, [1, 1, 1]);
+ helper.fetchCacheURL('/foo').then(foo => {
+ assert.equal(foo, 1);
+ done();
+ });
+ });
+ });
+
+ test('cached promise', done => {
+ const promise = Promise.reject(new Error('foo'));
+ cache.set('/foo', promise);
+ helper.fetchCacheURL({url: '/foo'}).catch(p => {
+ assert.equal(p.message, 'foo');
+ done();
+ });
+ });
+
+ test('cache invalidation', () => {
+ cache.set('/foo/bar', 1);
+ cache.set('/bar', 2);
+ fetchPromisesCache.set('/foo/bar', 3);
+ fetchPromisesCache.set('/bar', 4);
+ helper.invalidateFetchPromisesPrefix('/foo/');
+ assert.isFalse(cache.has('/foo/bar'));
+ assert.isTrue(cache.has('/bar'));
+ assert.isUndefined(fetchPromisesCache.get('/foo/bar'));
+ assert.strictEqual(4, fetchPromisesCache.get('/bar'));
+ });
+
+ test('params are properly encoded', () => {
+ let url = helper.urlWithParams('/path/', {
+ sp: 'hola',
+ gr: 'guten tag',
+ noval: null,
+ });
+ assert.equal(url,
+ window.CANONICAL_PATH + '/path/?sp=hola&gr=guten%20tag&noval');
+
+ url = helper.urlWithParams('/path/', {
+ sp: 'hola',
+ en: ['hey', 'hi'],
+ });
+ assert.equal(url, window.CANONICAL_PATH + '/path/?sp=hola&en=hey&en=hi');
+
+ // Order must be maintained with array params.
+ url = helper.urlWithParams('/path/', {
+ l: ['c', 'b', 'a'],
+ });
+ assert.equal(url, window.CANONICAL_PATH + '/path/?l=c&l=b&l=a');
+ });
+
+ test('request callbacks can be canceled', done => {
+ let cancelCalled = false;
+ window.fetch.returns(Promise.resolve({
+ body: {
+ cancel() { cancelCalled = true; },
+ },
+ }));
+ const cancelCondition = () => true;
+ helper.fetchJSON({url: '/dummy/url', cancelCondition}).then(
+ obj => {
+ assert.isUndefined(obj);
+ assert.isTrue(cancelCalled);
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
index 678e02a..4e17e13 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
@@ -19,289 +19,286 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reviewer-updates-parser</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
-<script src="gr-reviewer-updates-parser.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-reviewer-updates-parser.js';
+suite('gr-reviewer-updates-parser tests', () => {
+ let sandbox;
+ let instance;
-<script>
- suite('gr-reviewer-updates-parser tests', async () => {
- await readyToTest();
- let sandbox;
- let instance;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ });
- setup(() => {
- sandbox = sinon.sandbox.create();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- teardown(() => {
- sandbox.restore();
- });
+ test('ignores changes without messages', () => {
+ const change = {};
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_filterRemovedMessages');
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_groupUpdates');
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_formatUpdates');
+ assert.strictEqual(GrReviewerUpdatesParser.parse(change), change);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._filterRemovedMessages.called);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._groupUpdates.called);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._formatUpdates.called);
+ });
- test('ignores changes without messages', () => {
- const change = {};
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_filterRemovedMessages');
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_groupUpdates');
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_formatUpdates');
- assert.strictEqual(GrReviewerUpdatesParser.parse(change), change);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._filterRemovedMessages.called);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._groupUpdates.called);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._formatUpdates.called);
- });
+ test('ignores changes without reviewer updates', () => {
+ const change = {
+ messages: [],
+ };
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_filterRemovedMessages');
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_groupUpdates');
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_formatUpdates');
+ assert.strictEqual(GrReviewerUpdatesParser.parse(change), change);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._filterRemovedMessages.called);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._groupUpdates.called);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._formatUpdates.called);
+ });
- test('ignores changes without reviewer updates', () => {
- const change = {
- messages: [],
- };
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_filterRemovedMessages');
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_groupUpdates');
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_formatUpdates');
- assert.strictEqual(GrReviewerUpdatesParser.parse(change), change);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._filterRemovedMessages.called);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._groupUpdates.called);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._formatUpdates.called);
- });
+ test('ignores changes with empty reviewer updates', () => {
+ const change = {
+ messages: [],
+ reviewer_updates: [],
+ };
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_filterRemovedMessages');
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_groupUpdates');
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_formatUpdates');
+ assert.strictEqual(GrReviewerUpdatesParser.parse(change), change);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._filterRemovedMessages.called);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._groupUpdates.called);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._formatUpdates.called);
+ });
- test('ignores changes with empty reviewer updates', () => {
- const change = {
- messages: [],
- reviewer_updates: [],
- };
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_filterRemovedMessages');
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_groupUpdates');
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_formatUpdates');
- assert.strictEqual(GrReviewerUpdatesParser.parse(change), change);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._filterRemovedMessages.called);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._groupUpdates.called);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._formatUpdates.called);
- });
-
- test('filter removed messages', () => {
- const change = {
- messages: [
- {
- message: 'msg1',
- tag: 'autogenerated:gerrit:deleteReviewer',
- },
- {
- message: 'msg2',
- tag: 'foo',
- },
- ],
- };
- instance = new GrReviewerUpdatesParser(change);
- instance._filterRemovedMessages();
- assert.deepEqual(instance.result, {
- messages: [{
+ test('filter removed messages', () => {
+ const change = {
+ messages: [
+ {
+ message: 'msg1',
+ tag: 'autogenerated:gerrit:deleteReviewer',
+ },
+ {
message: 'msg2',
tag: 'foo',
- }],
- });
- });
-
- test('group reviewer updates', () => {
- const reviewer1 = {_account_id: 1};
- const reviewer2 = {_account_id: 2};
- const date1 = '2017-01-26 12:11:50.000000000';
- const date2 = '2017-01-26 12:11:55.000000000'; // Within threshold.
- const date3 = '2017-01-26 12:33:50.000000000';
- const date4 = '2017-01-26 12:44:50.000000000';
- const makeItem = function(state, reviewer, opt_date, opt_author) {
- return {
- reviewer,
- updated: opt_date || date1,
- updated_by: opt_author || reviewer1,
- state,
- };
- };
- let change = {
- reviewer_updates: [
- makeItem('REVIEWER', reviewer1), // New group.
- makeItem('CC', reviewer2), // Appended.
- makeItem('REVIEWER', reviewer2, date2), // Overrides previous one.
-
- makeItem('CC', reviewer1, date2, reviewer2), // New group.
-
- makeItem('REMOVED', reviewer2, date3), // Group has no state change.
- makeItem('REVIEWER', reviewer2, date3),
-
- makeItem('CC', reviewer1, date4), // No change, removed.
- makeItem('REVIEWER', reviewer1, date4), // Forms new group
- makeItem('REMOVED', reviewer2, date4), // Should be grouped.
- ],
- };
-
- instance = new GrReviewerUpdatesParser(change);
- instance._groupUpdates();
- change = instance.result;
-
- assert.equal(change.reviewer_updates.length, 3);
- assert.equal(change.reviewer_updates[0].updates.length, 2);
- assert.equal(change.reviewer_updates[1].updates.length, 1);
- assert.equal(change.reviewer_updates[2].updates.length, 2);
-
- assert.equal(change.reviewer_updates[0].date, date1);
- assert.deepEqual(change.reviewer_updates[0].author, reviewer1);
- assert.deepEqual(change.reviewer_updates[0].updates, [
- {
- reviewer: reviewer1,
- state: 'REVIEWER',
},
- {
- reviewer: reviewer2,
- state: 'REVIEWER',
- },
- ]);
-
- assert.equal(change.reviewer_updates[1].date, date2);
- assert.deepEqual(change.reviewer_updates[1].author, reviewer2);
- assert.deepEqual(change.reviewer_updates[1].updates, [
- {
- reviewer: reviewer1,
- state: 'CC',
- prev_state: 'REVIEWER',
- },
- ]);
-
- assert.equal(change.reviewer_updates[2].date, date4);
- assert.deepEqual(change.reviewer_updates[2].author, reviewer1);
- assert.deepEqual(change.reviewer_updates[2].updates, [
- {
- reviewer: reviewer1,
- prev_state: 'CC',
- state: 'REVIEWER',
- },
- {
- reviewer: reviewer2,
- prev_state: 'REVIEWER',
- state: 'REMOVED',
- },
- ]);
- });
-
- test('format reviewer updates', () => {
- const reviewer1 = {_account_id: 1};
- const reviewer2 = {_account_id: 2};
- const makeItem = function(prev, state, opt_reviewer) {
- return {
- reviewer: opt_reviewer || reviewer1,
- prev_state: prev,
- state,
- };
- };
- const makeUpdate = function(items) {
- return {
- author: reviewer1,
- updated: '',
- updates: items,
- };
- };
- const change = {
- reviewer_updates: [
- makeUpdate([
- makeItem(undefined, 'CC'),
- makeItem(undefined, 'CC', reviewer2),
- ]),
- makeUpdate([
- makeItem('CC', 'REVIEWER'),
- makeItem('REVIEWER', 'REMOVED'),
- makeItem('REMOVED', 'REVIEWER'),
- makeItem(undefined, 'REVIEWER', reviewer2),
- ]),
- ],
- };
-
- instance = new GrReviewerUpdatesParser(change);
- instance._formatUpdates();
-
- assert.equal(change.reviewer_updates.length, 2);
- assert.equal(change.reviewer_updates[0].updates.length, 1);
- assert.equal(change.reviewer_updates[1].updates.length, 3);
-
- let items = change.reviewer_updates[0].updates;
- assert.equal(items[0].message, 'Added to cc: ');
- assert.deepEqual(items[0].reviewers, [reviewer1, reviewer2]);
-
- items = change.reviewer_updates[1].updates;
- assert.equal(items[0].message, 'Moved from cc to reviewer: ');
- assert.deepEqual(items[0].reviewers, [reviewer1]);
- assert.equal(items[1].message, 'Removed from reviewer: ');
- assert.deepEqual(items[1].reviewers, [reviewer1]);
- assert.equal(items[2].message, 'Added to reviewer: ');
- assert.deepEqual(items[2].reviewers, [reviewer1, reviewer2]);
- });
-
- test('_advanceUpdates', () => {
- const T0 = util.parseDate('2017-02-17 19:04:18.000000000').getTime();
- const tplus = delta => new Date(T0 + delta)
- .toISOString()
- .replace('T', ' ')
- .replace('Z', '000000');
- const change = {
- reviewer_updates: [{
- date: tplus(0),
- type: 'REVIEWER_UPDATE',
- updates: [{
- message: 'same time update',
- }],
- }, {
- date: tplus(200),
- type: 'REVIEWER_UPDATE',
- updates: [{
- message: 'update within threshold',
- }],
- }, {
- date: tplus(600),
- type: 'REVIEWER_UPDATE',
- updates: [{
- message: 'update between messages',
- }],
- }, {
- date: tplus(1000),
- type: 'REVIEWER_UPDATE',
- updates: [{
- message: 'late update',
- }],
- }],
- messages: [{
- id: '6734489eb9d642de28dbf2bcf9bda875923800d8',
- date: tplus(0),
- message: 'Uploaded patch set 1.',
- }, {
- id: '6734489eb9d642de28dbf2bcf9bda875923800d8',
- date: tplus(800),
- message: 'Uploaded patch set 2.',
- }],
- };
- instance = new GrReviewerUpdatesParser(change);
- instance._advanceUpdates();
- const updates = instance.result.reviewer_updates;
- assert.isBelow(util.parseDate(updates[0].date).getTime(), T0);
- assert.isBelow(util.parseDate(updates[1].date).getTime(), T0);
- assert.equal(updates[2].date, tplus(100));
- assert.equal(updates[3].date, tplus(500));
+ ],
+ };
+ instance = new GrReviewerUpdatesParser(change);
+ instance._filterRemovedMessages();
+ assert.deepEqual(instance.result, {
+ messages: [{
+ message: 'msg2',
+ tag: 'foo',
+ }],
});
});
+
+ test('group reviewer updates', () => {
+ const reviewer1 = {_account_id: 1};
+ const reviewer2 = {_account_id: 2};
+ const date1 = '2017-01-26 12:11:50.000000000';
+ const date2 = '2017-01-26 12:11:55.000000000'; // Within threshold.
+ const date3 = '2017-01-26 12:33:50.000000000';
+ const date4 = '2017-01-26 12:44:50.000000000';
+ const makeItem = function(state, reviewer, opt_date, opt_author) {
+ return {
+ reviewer,
+ updated: opt_date || date1,
+ updated_by: opt_author || reviewer1,
+ state,
+ };
+ };
+ let change = {
+ reviewer_updates: [
+ makeItem('REVIEWER', reviewer1), // New group.
+ makeItem('CC', reviewer2), // Appended.
+ makeItem('REVIEWER', reviewer2, date2), // Overrides previous one.
+
+ makeItem('CC', reviewer1, date2, reviewer2), // New group.
+
+ makeItem('REMOVED', reviewer2, date3), // Group has no state change.
+ makeItem('REVIEWER', reviewer2, date3),
+
+ makeItem('CC', reviewer1, date4), // No change, removed.
+ makeItem('REVIEWER', reviewer1, date4), // Forms new group
+ makeItem('REMOVED', reviewer2, date4), // Should be grouped.
+ ],
+ };
+
+ instance = new GrReviewerUpdatesParser(change);
+ instance._groupUpdates();
+ change = instance.result;
+
+ assert.equal(change.reviewer_updates.length, 3);
+ assert.equal(change.reviewer_updates[0].updates.length, 2);
+ assert.equal(change.reviewer_updates[1].updates.length, 1);
+ assert.equal(change.reviewer_updates[2].updates.length, 2);
+
+ assert.equal(change.reviewer_updates[0].date, date1);
+ assert.deepEqual(change.reviewer_updates[0].author, reviewer1);
+ assert.deepEqual(change.reviewer_updates[0].updates, [
+ {
+ reviewer: reviewer1,
+ state: 'REVIEWER',
+ },
+ {
+ reviewer: reviewer2,
+ state: 'REVIEWER',
+ },
+ ]);
+
+ assert.equal(change.reviewer_updates[1].date, date2);
+ assert.deepEqual(change.reviewer_updates[1].author, reviewer2);
+ assert.deepEqual(change.reviewer_updates[1].updates, [
+ {
+ reviewer: reviewer1,
+ state: 'CC',
+ prev_state: 'REVIEWER',
+ },
+ ]);
+
+ assert.equal(change.reviewer_updates[2].date, date4);
+ assert.deepEqual(change.reviewer_updates[2].author, reviewer1);
+ assert.deepEqual(change.reviewer_updates[2].updates, [
+ {
+ reviewer: reviewer1,
+ prev_state: 'CC',
+ state: 'REVIEWER',
+ },
+ {
+ reviewer: reviewer2,
+ prev_state: 'REVIEWER',
+ state: 'REMOVED',
+ },
+ ]);
+ });
+
+ test('format reviewer updates', () => {
+ const reviewer1 = {_account_id: 1};
+ const reviewer2 = {_account_id: 2};
+ const makeItem = function(prev, state, opt_reviewer) {
+ return {
+ reviewer: opt_reviewer || reviewer1,
+ prev_state: prev,
+ state,
+ };
+ };
+ const makeUpdate = function(items) {
+ return {
+ author: reviewer1,
+ updated: '',
+ updates: items,
+ };
+ };
+ const change = {
+ reviewer_updates: [
+ makeUpdate([
+ makeItem(undefined, 'CC'),
+ makeItem(undefined, 'CC', reviewer2),
+ ]),
+ makeUpdate([
+ makeItem('CC', 'REVIEWER'),
+ makeItem('REVIEWER', 'REMOVED'),
+ makeItem('REMOVED', 'REVIEWER'),
+ makeItem(undefined, 'REVIEWER', reviewer2),
+ ]),
+ ],
+ };
+
+ instance = new GrReviewerUpdatesParser(change);
+ instance._formatUpdates();
+
+ assert.equal(change.reviewer_updates.length, 2);
+ assert.equal(change.reviewer_updates[0].updates.length, 1);
+ assert.equal(change.reviewer_updates[1].updates.length, 3);
+
+ let items = change.reviewer_updates[0].updates;
+ assert.equal(items[0].message, 'Added to cc: ');
+ assert.deepEqual(items[0].reviewers, [reviewer1, reviewer2]);
+
+ items = change.reviewer_updates[1].updates;
+ assert.equal(items[0].message, 'Moved from cc to reviewer: ');
+ assert.deepEqual(items[0].reviewers, [reviewer1]);
+ assert.equal(items[1].message, 'Removed from reviewer: ');
+ assert.deepEqual(items[1].reviewers, [reviewer1]);
+ assert.equal(items[2].message, 'Added to reviewer: ');
+ assert.deepEqual(items[2].reviewers, [reviewer1, reviewer2]);
+ });
+
+ test('_advanceUpdates', () => {
+ const T0 = util.parseDate('2017-02-17 19:04:18.000000000').getTime();
+ const tplus = delta => new Date(T0 + delta)
+ .toISOString()
+ .replace('T', ' ')
+ .replace('Z', '000000');
+ const change = {
+ reviewer_updates: [{
+ date: tplus(0),
+ type: 'REVIEWER_UPDATE',
+ updates: [{
+ message: 'same time update',
+ }],
+ }, {
+ date: tplus(200),
+ type: 'REVIEWER_UPDATE',
+ updates: [{
+ message: 'update within threshold',
+ }],
+ }, {
+ date: tplus(600),
+ type: 'REVIEWER_UPDATE',
+ updates: [{
+ message: 'update between messages',
+ }],
+ }, {
+ date: tplus(1000),
+ type: 'REVIEWER_UPDATE',
+ updates: [{
+ message: 'late update',
+ }],
+ }],
+ messages: [{
+ id: '6734489eb9d642de28dbf2bcf9bda875923800d8',
+ date: tplus(0),
+ message: 'Uploaded patch set 1.',
+ }, {
+ id: '6734489eb9d642de28dbf2bcf9bda875923800d8',
+ date: tplus(800),
+ message: 'Uploaded patch set 2.',
+ }],
+ };
+ instance = new GrReviewerUpdatesParser(change);
+ instance._advanceUpdates();
+ const updates = instance.result.reviewer_updates;
+ assert.isBelow(util.parseDate(updates[0].date).getTime(), T0);
+ assert.isBelow(util.parseDate(updates[1].date).getTime(), T0);
+ assert.equal(updates[2].date, tplus(100));
+ assert.equal(updates[3].date, tplus(500));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
index df3b9a5..015d71e 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
@@ -16,7 +16,6 @@
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
-<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<dom-module id="mock-diff-response">
<template></template>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.js
new file mode 100644
index 0000000..e7bc662
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.js
@@ -0,0 +1,166 @@
+/**
+ * @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 '../../../scripts/bundled-polymer.js';
+
+import '../../../test/common-test-setup.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const RESPONSE = {
+ meta_a: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 45,
+ },
+ meta_b: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 48,
+ },
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt',
+ 'index b2adcf4..554ae49 100644',
+ '--- a/lorem-ipsum.txt',
+ '+++ b/lorem-ipsum.txt',
+ ],
+ content: [
+ {
+ ab: [
+ 'Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, ' +
+ 'nulla phasellus.',
+ 'Mattis lectus.',
+ 'Sodales duis.',
+ 'Orci a faucibus.',
+ ],
+ },
+ {
+ b: [
+ 'Nullam neque, ligula ac, id blandit.',
+ 'Sagittis tincidunt torquent, tempor nunc amet.',
+ 'At rhoncus id.',
+ ],
+ },
+ {
+ ab: [
+ 'Sem nascetur, erat ut, non in.',
+ 'A donec, venenatis pellentesque dis.',
+ 'Mauris mauris.',
+ 'Quisque nisl duis, facilisis viverra.',
+ 'Justo purus, semper eget et.',
+ ],
+ },
+ {
+ a: [
+ 'Est amet, vestibulum pellentesque.',
+ 'Erat ligula.',
+ 'Justo eros.',
+ 'Fringilla quisque.',
+ ],
+ },
+ {
+ ab: [
+ 'Arcu eget, rhoncus amet cursus, ipsum elementum.',
+ 'Eros suspendisse.',
+ ],
+ },
+ {
+ a: [
+ 'Rhoncus tempor, ultricies aliquam ipsum.',
+ ],
+ b: [
+ 'Rhoncus tempor, ultricies praesent ipsum.',
+ ],
+ edit_a: [
+ [
+ 26,
+ 7,
+ ],
+ ],
+ edit_b: [
+ [
+ 26,
+ 8,
+ ],
+ ],
+ },
+ {
+ ab: [
+ 'Sollicitudin duis.',
+ 'Blandit blandit, ante nisl fusce.',
+ 'Felis ac at, tellus consectetuer.',
+ 'Sociis ligula sapien, egestas leo.',
+ 'Cum pulvinar, sed mauris, cursus neque velit.',
+ 'Augue porta lobortis.',
+ 'Nibh lorem, amet fermentum turpis, vel pulvinar diam.',
+ 'Id quam ipsum, id urna et, massa suspendisse.',
+ 'Ac nec, nibh praesent.',
+ 'Rutrum vestibulum.',
+ 'Est tellus, bibendum habitasse.',
+ 'Justo facilisis, vel nulla.',
+ 'Donec eu, vulputate neque aliquam, nulla dui.',
+ 'Risus adipiscing in.',
+ 'Lacus arcu arcu.',
+ 'Urna velit.',
+ 'Urna a dolor.',
+ 'Lectus magna augue, convallis mattis tortor, sed tellus ' +
+ 'consequat.',
+ 'Etiam dui, blandit wisi.',
+ 'Mi nec.',
+ 'Vitae eget vestibulum.',
+ 'Ullamcorper nunc ante, nec imperdiet felis, consectetur in.',
+ 'Ac eget.',
+ 'Vel fringilla, interdum pellentesque placerat, proin ante.',
+ ],
+ },
+ {
+ b: [
+ 'Eu congue risus.',
+ 'Enim ac, quis elementum.',
+ 'Non et elit.',
+ 'Etiam aliquam, diam vel nunc.',
+ ],
+ },
+ {
+ ab: [
+ 'Nec at.',
+ 'Arcu mauris, venenatis lacus fermentum, praesent duis.',
+ 'Pellentesque amet et, tellus duis.',
+ 'Ipsum arcu vitae, justo elit, sed libero tellus.',
+ 'Metus rutrum euismod, vivamus sodales, vel arcu nisl.',
+ ],
+ },
+ ],
+};
+
+Polymer({
+ _template: html`
+
+`,
+
+ is: 'mock-diff-response',
+
+ properties: {
+ diffResponse: {
+ type: Object,
+ value() {
+ return RESPONSE;
+ },
+ },
+ },
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.html b/polygerrit-ui/app/elements/shared/gr-select/gr-select.html
deleted file mode 100644
index f1ef86a..0000000
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.html
+++ /dev/null
@@ -1,24 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-
-<dom-module id="gr-select">
- <slot></slot>
- <script src="gr-select.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
index 3e59aee..18be73d 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -14,77 +14,89 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
- */
- class GrSelect extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-select'; }
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+const $_documentContainer = document.createElement('template');
- static get properties() {
- return {
- bindValue: {
- type: String,
- notify: true,
- observer: '_updateValue',
- },
- };
- }
+$_documentContainer.innerHTML = `<dom-module id="gr-select">
+ <slot></slot>
+
+</dom-module>`;
- get nativeSelect() {
- // gr-select is not a shadow component
- // TODO(taoalpha): maybe we should convert
- // it into a shadow dom component instead
- return this.querySelector('select');
- }
+document.head.appendChild($_documentContainer.content);
- _updateValue() {
- // It's possible to have a value of 0.
- if (this.bindValue !== undefined) {
- // Set for chrome/safari so it happens instantly
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrSelect extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get is() { return 'gr-select'; }
+
+ static get properties() {
+ return {
+ bindValue: {
+ type: String,
+ notify: true,
+ observer: '_updateValue',
+ },
+ };
+ }
+
+ get nativeSelect() {
+ // gr-select is not a shadow component
+ // TODO(taoalpha): maybe we should convert
+ // it into a shadow dom component instead
+ return this.querySelector('select');
+ }
+
+ _updateValue() {
+ // It's possible to have a value of 0.
+ if (this.bindValue !== undefined) {
+ // Set for chrome/safari so it happens instantly
+ this.nativeSelect.value = this.bindValue;
+ // Async needed for firefox to populate value. It was trying to do it
+ // before options from a dom-repeat were rendered previously.
+ // See https://bugs.chromium.org/p/gerrit/issues/detail?id=7735
+ this.async(() => {
this.nativeSelect.value = this.bindValue;
- // Async needed for firefox to populate value. It was trying to do it
- // before options from a dom-repeat were rendered previously.
- // See https://bugs.chromium.org/p/gerrit/issues/detail?id=7735
- this.async(() => {
- this.nativeSelect.value = this.bindValue;
- }, 1);
- }
- }
-
- _valueChanged() {
- this.bindValue = this.nativeSelect.value;
- }
-
- focus() {
- this.nativeSelect.focus();
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('change',
- () => this._valueChanged());
- this.addEventListener('dom-change',
- () => this._updateValue());
- }
-
- /** @override */
- ready() {
- super.ready();
- // If not set via the property, set bind-value to the element value.
- if (this.bindValue == undefined && this.nativeSelect.options.length > 0) {
- this.bindValue = this.nativeSelect.value;
- }
+ }, 1);
}
}
- customElements.define(GrSelect.is, GrSelect);
-})();
+ _valueChanged() {
+ this.bindValue = this.nativeSelect.value;
+ }
+
+ focus() {
+ this.nativeSelect.focus();
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('change',
+ () => this._valueChanged());
+ this.addEventListener('dom-change',
+ () => this._updateValue());
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ // If not set via the property, set bind-value to the element value.
+ if (this.bindValue == undefined && this.nativeSelect.options.length > 0) {
+ this.bindValue = this.nativeSelect.value;
+ }
+ }
+}
+
+customElements.define(GrSelect.is, GrSelect);
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
index 536f4f8..b18eb41 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-select</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-select.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -50,71 +45,72 @@
</template>
</test-fixture>
-<script>
- suite('gr-select tests', async () => {
- await readyToTest();
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-select.js';
+suite('gr-select tests', () => {
+ let element;
+
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ test('bindValue must be set to the first option value', () => {
+ assert.equal(element.bindValue, '1');
+ });
+
+ test('value of 0 should still trigger value updates', () => {
+ element.bindValue = 0;
+ assert.equal(element.nativeSelect.value, 0);
+ });
+
+ test('bidirectional binding property-to-attribute', () => {
+ const changeStub = sinon.stub();
+ element.addEventListener('bind-value-changed', changeStub);
+
+ // The selected element should be the first one by default.
+ assert.equal(element.nativeSelect.value, '1');
+ assert.equal(element.bindValue, '1');
+ assert.isFalse(changeStub.called);
+
+ // Now change the value.
+ element.bindValue = '2';
+
+ // It should be updated.
+ assert.equal(element.nativeSelect.value, '2');
+ assert.equal(element.bindValue, '2');
+ assert.isTrue(changeStub.called);
+ });
+
+ test('bidirectional binding attribute-to-property', () => {
+ const changeStub = sinon.stub();
+ element.addEventListener('bind-value-changed', changeStub);
+
+ // The selected element should be the first one by default.
+ assert.equal(element.nativeSelect.value, '1');
+ assert.equal(element.bindValue, '1');
+ assert.isFalse(changeStub.called);
+
+ // Now change the value.
+ element.nativeSelect.value = '3';
+ element.fire('change');
+
+ // It should be updated.
+ assert.equal(element.nativeSelect.value, '3');
+ assert.equal(element.bindValue, '3');
+ assert.isTrue(changeStub.called);
+ });
+
+ suite('gr-select no options tests', () => {
let element;
setup(() => {
- element = fixture('basic');
+ element = fixture('noOptions');
});
- test('bindValue must be set to the first option value', () => {
- assert.equal(element.bindValue, '1');
- });
-
- test('value of 0 should still trigger value updates', () => {
- element.bindValue = 0;
- assert.equal(element.nativeSelect.value, 0);
- });
-
- test('bidirectional binding property-to-attribute', () => {
- const changeStub = sinon.stub();
- element.addEventListener('bind-value-changed', changeStub);
-
- // The selected element should be the first one by default.
- assert.equal(element.nativeSelect.value, '1');
- assert.equal(element.bindValue, '1');
- assert.isFalse(changeStub.called);
-
- // Now change the value.
- element.bindValue = '2';
-
- // It should be updated.
- assert.equal(element.nativeSelect.value, '2');
- assert.equal(element.bindValue, '2');
- assert.isTrue(changeStub.called);
- });
-
- test('bidirectional binding attribute-to-property', () => {
- const changeStub = sinon.stub();
- element.addEventListener('bind-value-changed', changeStub);
-
- // The selected element should be the first one by default.
- assert.equal(element.nativeSelect.value, '1');
- assert.equal(element.bindValue, '1');
- assert.isFalse(changeStub.called);
-
- // Now change the value.
- element.nativeSelect.value = '3';
- element.fire('change');
-
- // It should be updated.
- assert.equal(element.nativeSelect.value, '3');
- assert.equal(element.bindValue, '3');
- assert.isTrue(changeStub.called);
- });
-
- suite('gr-select no options tests', () => {
- let element;
-
- setup(() => {
- element = fixture('noOptions');
- });
-
- test('bindValue must not be changed', () => {
- assert.isUndefined(element.bindValue);
- });
+ test('bindValue must not be changed', () => {
+ assert.isUndefined(element.bindValue);
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.html b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.html
deleted file mode 100644
index 15e282f..0000000
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.html
+++ /dev/null
@@ -1,63 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
-
-<dom-module id="gr-shell-command">
- <template>
- <style include="shared-styles">
- .commandContainer {
- margin-bottom: var(--spacing-m);
- }
- .commandContainer {
- background-color: var(--shell-command-background-color);
- /* Should be spacing-m larger than the :before width. */
- padding: var(--spacing-m) var(--spacing-m) var(--spacing-m) calc(3*var(--spacing-m) + 0.5em);
- position: relative;
- width: 100%;
- }
- .commandContainer:before {
- content: '$';
- position: absolute;
- display: block;
- box-sizing: border-box;
- background: var(--shell-command-decoration-background-color);
- top: 0;
- bottom: 0;
- left: 0;
- /* Should be spacing-m smaller than the .commandContainer padding-left. */
- width: calc(2*var(--spacing-m) + 0.5em);
- /* Should vertically match the padding of .commandContainer. */
- padding: var(--spacing-m);
- /* Should roughly match the height of .commandContainer without padding. */
- line-height: 26px;
- }
- .commandContainer gr-copy-clipboard {
- --text-container-style: {
- border: none;
- }
- }
- </style>
- <label>[[label]]</label>
- <div class="commandContainer">
- <gr-copy-clipboard text="[[command]]"></gr-copy-clipboard>
- </div>
- </template>
- <script src="gr-shell-command.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
index 63dbcbd..151498c 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
@@ -14,26 +14,33 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrShellCommand extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-shell-command'; }
+import '../../../styles/shared-styles.js';
+import '../gr-copy-clipboard/gr-copy-clipboard.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-shell-command_html.js';
- static get properties() {
- return {
- command: String,
- label: String,
- };
- }
+/** @extends Polymer.Element */
+class GrShellCommand extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- focusOnCopy() {
- this.shadowRoot.querySelector('gr-copy-clipboard').focusOnCopy();
- }
+ static get is() { return 'gr-shell-command'; }
+
+ static get properties() {
+ return {
+ command: String,
+ label: String,
+ };
}
- customElements.define(GrShellCommand.is, GrShellCommand);
-})();
+ focusOnCopy() {
+ this.shadowRoot.querySelector('gr-copy-clipboard').focusOnCopy();
+ }
+}
+
+customElements.define(GrShellCommand.is, GrShellCommand);
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_html.js b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_html.js
new file mode 100644
index 0000000..8fbf2b6
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_html.js
@@ -0,0 +1,57 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ .commandContainer {
+ margin-bottom: var(--spacing-m);
+ }
+ .commandContainer {
+ background-color: var(--shell-command-background-color);
+ /* Should be spacing-m larger than the :before width. */
+ padding: var(--spacing-m) var(--spacing-m) var(--spacing-m) calc(3*var(--spacing-m) + 0.5em);
+ position: relative;
+ width: 100%;
+ }
+ .commandContainer:before {
+ content: '\$';
+ position: absolute;
+ display: block;
+ box-sizing: border-box;
+ background: var(--shell-command-decoration-background-color);
+ top: 0;
+ bottom: 0;
+ left: 0;
+ /* Should be spacing-m smaller than the .commandContainer padding-left. */
+ width: calc(2*var(--spacing-m) + 0.5em);
+ /* Should vertically match the padding of .commandContainer. */
+ padding: var(--spacing-m);
+ /* Should roughly match the height of .commandContainer without padding. */
+ line-height: 26px;
+ }
+ .commandContainer gr-copy-clipboard {
+ --text-container-style: {
+ border: none;
+ }
+ }
+ </style>
+ <label>[[label]]</label>
+ <div class="commandContainer">
+ <gr-copy-clipboard text="[[command]]"></gr-copy-clipboard>
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html
index b596a4a..cc779c7 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html
@@ -19,15 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-shell-command</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-shell-command.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,30 +30,31 @@
</template>
</test-fixture>
-<script>
- suite('gr-shell-command tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-shell-command.js';
+suite('gr-shell-command tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.text = `git fetch http://gerrit@localhost:8080/a/test-project
- refs/changes/05/5/1 && git checkout FETCH_HEAD`;
- flushAsynchronousOperations();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('focusOnCopy', () => {
- const focusStub = sandbox.stub(element.shadowRoot
- .querySelector('gr-copy-clipboard'),
- 'focusOnCopy');
- element.focusOnCopy();
- assert.isTrue(focusStub.called);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.text = `git fetch http://gerrit@localhost:8080/a/test-project
+ refs/changes/05/5/1 && git checkout FETCH_HEAD`;
+ flushAsynchronousOperations();
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('focusOnCopy', () => {
+ const focusStub = sandbox.stub(element.shadowRoot
+ .querySelector('gr-copy-clipboard'),
+ 'focusOnCopy');
+ element.focusOnCopy();
+ assert.isTrue(focusStub.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html
deleted file mode 100644
index 7215b26..0000000
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html
+++ /dev/null
@@ -1,20 +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.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<dom-module id="gr-storage">
- <script src="gr-storage.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
index 8cc9de9..8f5c486 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -14,148 +14,150 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const DURATION_DAY = 24 * 60 * 60 * 1000;
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
- // Clean up old entries no more frequently than one day.
- const CLEANUP_THROTTLE_INTERVAL = DURATION_DAY;
+const DURATION_DAY = 24 * 60 * 60 * 1000;
- const CLEANUP_PREFIXES_MAX_AGE_MAP = {
- // respectfultip has a 3 day expiration
- 'respectfultip:': 3 * DURATION_DAY,
- 'draft:': DURATION_DAY,
- 'editablecontent:': DURATION_DAY,
- };
+// Clean up old entries no more frequently than one day.
+const CLEANUP_THROTTLE_INTERVAL = DURATION_DAY;
- /** @extends Polymer.Element */
- class GrStorage extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-storage'; }
+const CLEANUP_PREFIXES_MAX_AGE_MAP = {
+ // respectfultip has a 14-day expiration
+ 'respectfultip:': 14 * DURATION_DAY,
+ 'draft:': DURATION_DAY,
+ 'editablecontent:': DURATION_DAY,
+};
- static get properties() {
- return {
- _lastCleanup: Number,
- /** @type {?Storage} */
- _storage: {
- type: Object,
- value() {
- return window.localStorage;
- },
+/** @extends Polymer.Element */
+class GrStorage extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get is() { return 'gr-storage'; }
+
+ static get properties() {
+ return {
+ _lastCleanup: Number,
+ /** @type {?Storage} */
+ _storage: {
+ type: Object,
+ value() {
+ return window.localStorage;
},
- _exceededQuota: {
- type: Boolean,
- value: false,
- },
- };
+ },
+ _exceededQuota: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
+
+ getDraftComment(location) {
+ this._cleanupItems();
+ return this._getObject(this._getDraftKey(location));
+ }
+
+ setDraftComment(location, message) {
+ const key = this._getDraftKey(location);
+ this._setObject(key, {message, updated: Date.now()});
+ }
+
+ eraseDraftComment(location) {
+ const key = this._getDraftKey(location);
+ this._storage.removeItem(key);
+ }
+
+ getEditableContentItem(key) {
+ this._cleanupItems();
+ return this._getObject(this._getEditableContentKey(key));
+ }
+
+ setEditableContentItem(key, message) {
+ this._setObject(this._getEditableContentKey(key),
+ {message, updated: Date.now()});
+ }
+
+ getRespectfulTipVisibility() {
+ this._cleanupItems();
+ return this._getObject('respectfultip:visibility');
+ }
+
+ setRespectfulTipVisibility(delayDays = 0) {
+ this._cleanupItems();
+ this._setObject(
+ 'respectfultip:visibility',
+ {updated: Date.now() + delayDays * DURATION_DAY}
+ );
+ }
+
+ eraseEditableContentItem(key) {
+ this._storage.removeItem(this._getEditableContentKey(key));
+ }
+
+ _getDraftKey(location) {
+ const range = location.range ?
+ `${location.range.start_line}-${location.range.start_character}` +
+ `-${location.range.end_character}-${location.range.end_line}` :
+ null;
+ let key = ['draft', location.changeNum, location.patchNum, location.path,
+ location.line || ''].join(':');
+ if (range) {
+ key = key + ':' + range;
}
+ return key;
+ }
- getDraftComment(location) {
- this._cleanupItems();
- return this._getObject(this._getDraftKey(location));
+ _getEditableContentKey(key) {
+ return `editablecontent:${key}`;
+ }
+
+ _cleanupItems() {
+ // Throttle cleanup to the throttle interval.
+ if (this._lastCleanup &&
+ Date.now() - this._lastCleanup < CLEANUP_THROTTLE_INTERVAL) {
+ return;
}
+ this._lastCleanup = Date.now();
- setDraftComment(location, message) {
- const key = this._getDraftKey(location);
- this._setObject(key, {message, updated: Date.now()});
- }
-
- eraseDraftComment(location) {
- const key = this._getDraftKey(location);
- this._storage.removeItem(key);
- }
-
- getEditableContentItem(key) {
- this._cleanupItems();
- return this._getObject(this._getEditableContentKey(key));
- }
-
- setEditableContentItem(key, message) {
- this._setObject(this._getEditableContentKey(key),
- {message, updated: Date.now()});
- }
-
- getRespectfulTipVisibility() {
- this._cleanupItems();
- return this._getObject('respectfultip:visibility');
- }
-
- setRespectfulTipVisibility(delayDays = 0) {
- this._cleanupItems();
- this._setObject(
- 'respectfultip:visibility',
- {updated: Date.now() + delayDays * DURATION_DAY}
- );
- }
-
- eraseEditableContentItem(key) {
- this._storage.removeItem(this._getEditableContentKey(key));
- }
-
- _getDraftKey(location) {
- const range = location.range ?
- `${location.range.start_line}-${location.range.start_character}` +
- `-${location.range.end_character}-${location.range.end_line}` :
- null;
- let key = ['draft', location.changeNum, location.patchNum, location.path,
- location.line || ''].join(':');
- if (range) {
- key = key + ':' + range;
- }
- return key;
- }
-
- _getEditableContentKey(key) {
- return `editablecontent:${key}`;
- }
-
- _cleanupItems() {
- // Throttle cleanup to the throttle interval.
- if (this._lastCleanup &&
- Date.now() - this._lastCleanup < CLEANUP_THROTTLE_INTERVAL) {
- return;
- }
- this._lastCleanup = Date.now();
-
- let item;
- Object.keys(this._storage).forEach(key => {
- Object.keys(CLEANUP_PREFIXES_MAX_AGE_MAP).forEach(prefix => {
- if (key.startsWith(prefix)) {
- item = this._getObject(key);
- const expiration = CLEANUP_PREFIXES_MAX_AGE_MAP[prefix];
- if (Date.now() - item.updated > expiration) {
- this._storage.removeItem(key);
- }
+ let item;
+ Object.keys(this._storage).forEach(key => {
+ Object.keys(CLEANUP_PREFIXES_MAX_AGE_MAP).forEach(prefix => {
+ if (key.startsWith(prefix)) {
+ item = this._getObject(key);
+ const expiration = CLEANUP_PREFIXES_MAX_AGE_MAP[prefix];
+ if (Date.now() - item.updated > expiration) {
+ this._storage.removeItem(key);
}
- });
- });
- }
-
- _getObject(key) {
- const serial = this._storage.getItem(key);
- if (!serial) { return null; }
- return JSON.parse(serial);
- }
-
- _setObject(key, obj) {
- if (this._exceededQuota) { return; }
- try {
- this._storage.setItem(key, JSON.stringify(obj));
- } catch (exc) {
- // Catch for QuotaExceededError and disable writes on local storage the
- // first time that it occurs.
- if (exc.code === 22) {
- this._exceededQuota = true;
- console.warn('Local storage quota exceeded: disabling');
- return;
- } else {
- throw exc;
}
+ });
+ });
+ }
+
+ _getObject(key) {
+ const serial = this._storage.getItem(key);
+ if (!serial) { return null; }
+ return JSON.parse(serial);
+ }
+
+ _setObject(key, obj) {
+ if (this._exceededQuota) { return; }
+ try {
+ this._storage.setItem(key, JSON.stringify(obj));
+ } catch (exc) {
+ // Catch for QuotaExceededError and disable writes on local storage the
+ // first time that it occurs.
+ if (exc.code === 22) {
+ this._exceededQuota = true;
+ console.warn('Local storage quota exceeded: disabling');
+ return;
+ } else {
+ throw exc;
}
}
}
+}
- customElements.define(GrStorage.is, GrStorage);
-})();
+customElements.define(GrStorage.is, GrStorage);
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
index 66e7f98..5642e15 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
@@ -18,15 +18,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-storage</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-storage.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -34,165 +29,166 @@
</template>
</test-fixture>
-<script>
- suite('gr-storage tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-storage.js';
+suite('gr-storage tests', () => {
+ let element;
+ let sandbox;
- function mockStorage(opt_quotaExceeded) {
- return {
- getItem(key) { return this[key]; },
- removeItem(key) { delete this[key]; },
- setItem(key, value) {
- // eslint-disable-next-line no-throw-literal
- if (opt_quotaExceeded) { throw {code: 22}; /* Quota exceeded */ }
- this[key] = value;
- },
- };
- }
+ function mockStorage(opt_quotaExceeded) {
+ return {
+ getItem(key) { return this[key]; },
+ removeItem(key) { delete this[key]; },
+ setItem(key, value) {
+ // eslint-disable-next-line no-throw-literal
+ if (opt_quotaExceeded) { throw {code: 22}; /* Quota exceeded */ }
+ this[key] = value;
+ },
+ };
+ }
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- element._storage = mockStorage();
- });
-
- teardown(() => sandbox.restore());
-
- test('storing, retrieving and erasing drafts', () => {
- const changeNum = 1234;
- const patchNum = 5;
- const path = 'my_source_file.js';
- const line = 123;
- const location = {
- changeNum,
- patchNum,
- path,
- line,
- };
-
- // The key is in the expected format.
- const key = element._getDraftKey(location);
- assert.equal(key, ['draft', changeNum, patchNum, path, line].join(':'));
-
- // There should be no draft initially.
- const draft = element.getDraftComment(location);
- assert.isNotOk(draft);
-
- // Setting the draft stores it under the expected key.
- element.setDraftComment(location, 'my comment');
- assert.isOk(element._storage.getItem(key));
- assert.equal(JSON.parse(element._storage.getItem(key)).message,
- 'my comment');
- assert.isOk(JSON.parse(element._storage.getItem(key)).updated);
-
- // Erasing the draft removes the key.
- element.eraseDraftComment(location);
- assert.isNotOk(element._storage.getItem(key));
- });
-
- test('automatically removes old drafts', () => {
- const changeNum = 1234;
- const patchNum = 5;
- const path = 'my_source_file.js';
- const line = 123;
- const location = {
- changeNum,
- patchNum,
- path,
- line,
- };
-
- const key = element._getDraftKey(location);
-
- // Make sure that the call to cleanup doesn't get throttled.
- element._lastCleanup = 0;
-
- const cleanupSpy = sandbox.spy(element, '_cleanupItems');
-
- // Create a message with a timestamp that is a second behind the max age.
- element._storage.setItem(key, JSON.stringify({
- message: 'old message',
- updated: Date.now() - 24 * 60 * 60 * 1000 - 1000,
- }));
-
- // Getting the draft should cause it to be removed.
- const draft = element.getDraftComment(location);
-
- assert.isTrue(cleanupSpy.called);
- assert.isNotOk(draft);
- assert.isNotOk(element._storage.getItem(key));
- });
-
- test('_getDraftKey', () => {
- const changeNum = 1234;
- const patchNum = 5;
- const path = 'my_source_file.js';
- const line = 123;
- const location = {
- changeNum,
- patchNum,
- path,
- line,
- };
- let expectedResult = 'draft:1234:5:my_source_file.js:123';
- assert.equal(element._getDraftKey(location), expectedResult);
- location.range = {
- start_character: 1,
- start_line: 1,
- end_character: 1,
- end_line: 2,
- };
- expectedResult = 'draft:1234:5:my_source_file.js:123:1-1-1-2';
- assert.equal(element._getDraftKey(location), expectedResult);
- });
-
- test('exceeded quota disables storage', () => {
- element._storage = mockStorage(true);
- assert.isFalse(element._exceededQuota);
-
- const changeNum = 1234;
- const patchNum = 5;
- const path = 'my_source_file.js';
- const line = 123;
- const location = {
- changeNum,
- patchNum,
- path,
- line,
- };
- const key = element._getDraftKey(location);
- element.setDraftComment(location, 'my comment');
- assert.isTrue(element._exceededQuota);
- assert.isNotOk(element._storage.getItem(key));
- });
-
- test('editable content items', () => {
- const cleanupStub = sandbox.stub(element, '_cleanupItems');
- const key = 'testKey';
- const computedKey = element._getEditableContentKey(key);
- // Key correctly computed.
- assert.equal(computedKey, 'editablecontent:testKey');
-
- element.setEditableContentItem(key, 'my content');
-
- // Setting the draft stores it under the expected key.
- let item = element._storage.getItem(computedKey);
- assert.isOk(item);
- assert.equal(JSON.parse(item).message, 'my content');
- assert.isOk(JSON.parse(item).updated);
-
- // getEditableContentItem performs as expected.
- item = element.getEditableContentItem(key);
- assert.isOk(item);
- assert.equal(item.message, 'my content');
- assert.isOk(item.updated);
- assert.isTrue(cleanupStub.called);
-
- // eraseEditableContentItem performs as expected.
- element.eraseEditableContentItem(key);
- assert.isNotOk(element._storage.getItem(computedKey));
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ element._storage = mockStorage();
});
+
+ teardown(() => sandbox.restore());
+
+ test('storing, retrieving and erasing drafts', () => {
+ const changeNum = 1234;
+ const patchNum = 5;
+ const path = 'my_source_file.js';
+ const line = 123;
+ const location = {
+ changeNum,
+ patchNum,
+ path,
+ line,
+ };
+
+ // The key is in the expected format.
+ const key = element._getDraftKey(location);
+ assert.equal(key, ['draft', changeNum, patchNum, path, line].join(':'));
+
+ // There should be no draft initially.
+ const draft = element.getDraftComment(location);
+ assert.isNotOk(draft);
+
+ // Setting the draft stores it under the expected key.
+ element.setDraftComment(location, 'my comment');
+ assert.isOk(element._storage.getItem(key));
+ assert.equal(JSON.parse(element._storage.getItem(key)).message,
+ 'my comment');
+ assert.isOk(JSON.parse(element._storage.getItem(key)).updated);
+
+ // Erasing the draft removes the key.
+ element.eraseDraftComment(location);
+ assert.isNotOk(element._storage.getItem(key));
+ });
+
+ test('automatically removes old drafts', () => {
+ const changeNum = 1234;
+ const patchNum = 5;
+ const path = 'my_source_file.js';
+ const line = 123;
+ const location = {
+ changeNum,
+ patchNum,
+ path,
+ line,
+ };
+
+ const key = element._getDraftKey(location);
+
+ // Make sure that the call to cleanup doesn't get throttled.
+ element._lastCleanup = 0;
+
+ const cleanupSpy = sandbox.spy(element, '_cleanupItems');
+
+ // Create a message with a timestamp that is a second behind the max age.
+ element._storage.setItem(key, JSON.stringify({
+ message: 'old message',
+ updated: Date.now() - 24 * 60 * 60 * 1000 - 1000,
+ }));
+
+ // Getting the draft should cause it to be removed.
+ const draft = element.getDraftComment(location);
+
+ assert.isTrue(cleanupSpy.called);
+ assert.isNotOk(draft);
+ assert.isNotOk(element._storage.getItem(key));
+ });
+
+ test('_getDraftKey', () => {
+ const changeNum = 1234;
+ const patchNum = 5;
+ const path = 'my_source_file.js';
+ const line = 123;
+ const location = {
+ changeNum,
+ patchNum,
+ path,
+ line,
+ };
+ let expectedResult = 'draft:1234:5:my_source_file.js:123';
+ assert.equal(element._getDraftKey(location), expectedResult);
+ location.range = {
+ start_character: 1,
+ start_line: 1,
+ end_character: 1,
+ end_line: 2,
+ };
+ expectedResult = 'draft:1234:5:my_source_file.js:123:1-1-1-2';
+ assert.equal(element._getDraftKey(location), expectedResult);
+ });
+
+ test('exceeded quota disables storage', () => {
+ element._storage = mockStorage(true);
+ assert.isFalse(element._exceededQuota);
+
+ const changeNum = 1234;
+ const patchNum = 5;
+ const path = 'my_source_file.js';
+ const line = 123;
+ const location = {
+ changeNum,
+ patchNum,
+ path,
+ line,
+ };
+ const key = element._getDraftKey(location);
+ element.setDraftComment(location, 'my comment');
+ assert.isTrue(element._exceededQuota);
+ assert.isNotOk(element._storage.getItem(key));
+ });
+
+ test('editable content items', () => {
+ const cleanupStub = sandbox.stub(element, '_cleanupItems');
+ const key = 'testKey';
+ const computedKey = element._getEditableContentKey(key);
+ // Key correctly computed.
+ assert.equal(computedKey, 'editablecontent:testKey');
+
+ element.setEditableContentItem(key, 'my content');
+
+ // Setting the draft stores it under the expected key.
+ let item = element._storage.getItem(computedKey);
+ assert.isOk(item);
+ assert.equal(JSON.parse(item).message, 'my content');
+ assert.isOk(JSON.parse(item).updated);
+
+ // getEditableContentItem performs as expected.
+ item = element.getEditableContentItem(key);
+ assert.isOk(item);
+ assert.equal(item.message, 'my content');
+ assert.isOk(item.updated);
+ assert.isTrue(cleanupStub.called);
+
+ // eraseEditableContentItem performs as expected.
+ element.eraseEditableContentItem(key);
+ assert.isNotOk(element._storage.getItem(computedKey));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
deleted file mode 100644
index 42a4f3b..0000000
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
+++ /dev/null
@@ -1,108 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html">
-<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="/bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-
-<dom-module id="gr-textarea">
- <template>
- <style include="shared-styles">
- :host {
- display: flex;
- position: relative;
- }
- :host(.monospace) {
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-mono);
- line-height: var(--line-height-mono);
- font-weight: var(--font-weight-normal);
- }
- :host(.code) {
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-code);
- line-height: var(--line-height-code);
- font-weight: var(--font-weight-normal);
- }
- #emojiSuggestions {
- font-family: var(--font-family);
- }
- gr-autocomplete {
- display: inline-block
- }
- #textarea {
- background-color: var(--view-background-color);
- width: 100%;
- }
- #hiddenText #emojiSuggestions {
- visibility: visible;
- white-space: normal;
- }
- iron-autogrow-textarea {
- position: relative;
-
- /** This is needed for firefox */
- --iron-autogrow-textarea_-_white-space: pre-wrap;
- }
- #textarea.noBorder {
- border: none;
- }
- #hiddenText {
- display: block;
- float: left;
- position: absolute;
- visibility: hidden;
- width: 100%;
- white-space: pre-wrap;
- }
- </style>
- <div id="hiddenText"></div>
- <!-- When the autocomplete is open, the span is moved at the end of
- hiddenText in order to correctly position the dropdown. After being moved,
- it is set as the positionTarget for the emojiSuggestions dropdown. -->
- <span id="caratSpan"></span>
- <gr-autocomplete-dropdown
- vertical-align="top"
- horizontal-align="left"
- dynamic-align
- id="emojiSuggestions"
- suggestions="[[_suggestions]]"
- index="[[_index]]"
- vertical-offset="[[_verticalOffset]]"
- on-dropdown-closed="_resetEmojiDropdown"
- on-item-selected="_handleEmojiSelect">
- </gr-autocomplete-dropdown>
- <iron-autogrow-textarea
- id="textarea"
- autocomplete="[[autocomplete]]"
- placeholder=[[placeholder]]
- disabled="[[disabled]]"
- rows="[[rows]]"
- max-rows="[[maxRows]]"
- value="{{text}}"
- on-bind-value-changed="_onValueChanged"></iron-autogrow-textarea>
- <gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="gr-textarea.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
index 07b664b2..6f4c75d 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -14,319 +14,335 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const MAX_ITEMS_DROPDOWN = 10;
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../gr-autocomplete-dropdown/gr-autocomplete-dropdown.js';
+import '../gr-cursor-manager/gr-cursor-manager.js';
+import '../gr-overlay/gr-overlay.js';
+import '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-textarea_html.js';
- const ALL_SUGGESTIONS = [
- {value: '😊', match: 'smile :)'},
- {value: '👍', match: 'thumbs up'},
- {value: '😄', match: 'laugh :D'},
- {value: '🎉', match: 'party'},
- {value: '😞', match: 'sad :('},
- {value: '😂', match: 'tears :\')'},
- {value: '🙏', match: 'pray'},
- {value: '😐', match: 'neutral :|'},
- {value: '😮', match: 'shock :O'},
- {value: '👎', match: 'thumbs down'},
- {value: '😎', match: 'cool |;)'},
- {value: '😕', match: 'confused'},
- {value: '👌', match: 'ok'},
- {value: '🔥', match: 'fire'},
- {value: '👊', match: 'fistbump'},
- {value: '💯', match: '100'},
- {value: '💔', match: 'broken heart'},
- {value: '🍺', match: 'beer'},
- {value: '✔', match: 'check'},
- {value: '😋', match: 'tongue'},
- {value: '😭', match: 'crying :\'('},
- {value: '🐨', match: 'koala'},
- {value: '🤓', match: 'glasses'},
- {value: '😆', match: 'grin'},
- {value: '💩', match: 'poop'},
- {value: '😢', match: 'tear'},
- {value: '😒', match: 'unamused'},
- {value: '😉', match: 'wink ;)'},
- {value: '🍷', match: 'wine'},
- {value: '😜', match: 'winking tongue ;)'},
- ];
+const MAX_ITEMS_DROPDOWN = 10;
+const ALL_SUGGESTIONS = [
+ {value: '😊', match: 'smile :)'},
+ {value: '👍', match: 'thumbs up'},
+ {value: '😄', match: 'laugh :D'},
+ {value: '🎉', match: 'party'},
+ {value: '😞', match: 'sad :('},
+ {value: '😂', match: 'tears :\')'},
+ {value: '🙏', match: 'pray'},
+ {value: '😐', match: 'neutral :|'},
+ {value: '😮', match: 'shock :O'},
+ {value: '👎', match: 'thumbs down'},
+ {value: '😎', match: 'cool |;)'},
+ {value: '😕', match: 'confused'},
+ {value: '👌', match: 'ok'},
+ {value: '🔥', match: 'fire'},
+ {value: '👊', match: 'fistbump'},
+ {value: '💯', match: '100'},
+ {value: '💔', match: 'broken heart'},
+ {value: '🍺', match: 'beer'},
+ {value: '✔', match: 'check'},
+ {value: '😋', match: 'tongue'},
+ {value: '😭', match: 'crying :\'('},
+ {value: '🐨', match: 'koala'},
+ {value: '🤓', match: 'glasses'},
+ {value: '😆', match: 'grin'},
+ {value: '💩', match: 'poop'},
+ {value: '😢', match: 'tear'},
+ {value: '😒', match: 'unamused'},
+ {value: '😉', match: 'wink ;)'},
+ {value: '🍷', match: 'wine'},
+ {value: '😜', match: 'winking tongue ;)'},
+];
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrTextarea extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-textarea'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * @event bind-value-changed
*/
- class GrTextarea extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-textarea'; }
- /**
- * @event bind-value-changed
- */
- static get properties() {
- return {
- autocomplete: Boolean,
- disabled: Boolean,
- rows: Number,
- maxRows: Number,
- placeholder: String,
- text: {
- type: String,
- notify: true,
- observer: '_handleTextChanged',
- },
- hideBorder: {
- type: Boolean,
- value: false,
- },
- /** Text input should be rendered in monspace font. */
- monospace: {
- type: Boolean,
- value: false,
- },
- /** Text input should be rendered in code font, which is smaller than the
- standard monospace font. */
- code: {
- type: Boolean,
- value: false,
- },
- /** @type {?number} */
- _colonIndex: Number,
- _currentSearchString: {
- type: String,
- observer: '_determineSuggestions',
- },
- _hideAutocomplete: {
- type: Boolean,
- value: true,
- },
- _index: Number,
- _suggestions: Array,
- // Offset makes dropdown appear below text.
- _verticalOffset: {
- type: Number,
- value: 20,
- readOnly: true,
- },
- };
+ static get properties() {
+ return {
+ autocomplete: Boolean,
+ disabled: Boolean,
+ rows: Number,
+ maxRows: Number,
+ placeholder: String,
+ text: {
+ type: String,
+ notify: true,
+ observer: '_handleTextChanged',
+ },
+ hideBorder: {
+ type: Boolean,
+ value: false,
+ },
+ /** Text input should be rendered in monspace font. */
+ monospace: {
+ type: Boolean,
+ value: false,
+ },
+ /** Text input should be rendered in code font, which is smaller than the
+ standard monospace font. */
+ code: {
+ type: Boolean,
+ value: false,
+ },
+ /** @type {?number} */
+ _colonIndex: Number,
+ _currentSearchString: {
+ type: String,
+ observer: '_determineSuggestions',
+ },
+ _hideAutocomplete: {
+ type: Boolean,
+ value: true,
+ },
+ _index: Number,
+ _suggestions: Array,
+ // Offset makes dropdown appear below text.
+ _verticalOffset: {
+ type: Number,
+ value: 20,
+ readOnly: true,
+ },
+ };
+ }
+
+ get keyBindings() {
+ return {
+ esc: '_handleEscKey',
+ tab: '_handleEnterByKey',
+ enter: '_handleEnterByKey',
+ up: '_handleUpKey',
+ down: '_handleDownKey',
+ };
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ if (this.monospace) {
+ this.classList.add('monospace');
}
-
- get keyBindings() {
- return {
- esc: '_handleEscKey',
- tab: '_handleEnterByKey',
- enter: '_handleEnterByKey',
- up: '_handleUpKey',
- down: '_handleDownKey',
- };
+ if (this.code) {
+ this.classList.add('code');
}
-
- /** @override */
- ready() {
- super.ready();
- if (this.monospace) {
- this.classList.add('monospace');
- }
- if (this.code) {
- this.classList.add('code');
- }
- if (this.hideBorder) {
- this.$.textarea.classList.add('noBorder');
- }
- }
-
- closeDropdown() {
- return this.$.emojiSuggestions.close();
- }
-
- getNativeTextarea() {
- return this.$.textarea.textarea;
- }
-
- putCursorAtEnd() {
- const textarea = this.getNativeTextarea();
- // Put the cursor at the end always.
- textarea.selectionStart = textarea.value.length;
- textarea.selectionEnd = textarea.selectionStart;
- this.async(() => {
- textarea.focus();
- });
- }
-
- _handleEscKey(e) {
- if (this._hideAutocomplete) { return; }
- e.preventDefault();
- e.stopPropagation();
- this._resetEmojiDropdown();
- }
-
- _handleUpKey(e) {
- if (this._hideAutocomplete) { return; }
- e.preventDefault();
- e.stopPropagation();
- this.$.emojiSuggestions.cursorUp();
- this.$.textarea.textarea.focus();
- this.disableEnterKeyForSelectingEmoji = false;
- }
-
- _handleDownKey(e) {
- if (this._hideAutocomplete) { return; }
- e.preventDefault();
- e.stopPropagation();
- this.$.emojiSuggestions.cursorDown();
- this.$.textarea.textarea.focus();
- this.disableEnterKeyForSelectingEmoji = false;
- }
-
- _handleEnterByKey(e) {
- if (this._hideAutocomplete || this.disableEnterKeyForSelectingEmoji) {
- return;
- }
- e.preventDefault();
- e.stopPropagation();
- this._setEmoji(this.$.emojiSuggestions.getCurrentText());
- }
-
- _handleEmojiSelect(e) {
- this._setEmoji(e.detail.selected.dataset.value);
- }
-
- _setEmoji(text) {
- const colonIndex = this._colonIndex;
- this.text = this._getText(text);
- this.$.textarea.selectionStart = colonIndex + 1;
- this.$.textarea.selectionEnd = colonIndex + 1;
- this.$.reporting.reportInteraction('select-emoji', {type: text});
- this._resetEmojiDropdown();
- }
-
- _getText(value) {
- return this.text.substr(0, this._colonIndex || 0) +
- value + this.text.substr(this.$.textarea.selectionStart);
- }
-
- /**
- * Uses a hidden element with the same width and styling of the textarea and
- * the text up until the point of interest. Then caratSpan element is added
- * to the end and is set to be the positionTarget for the dropdown. Together
- * this allows the dropdown to appear near where the user is typing.
- */
- _updateCaratPosition() {
- this._hideAutocomplete = false;
- this.$.hiddenText.textContent = this.$.textarea.value.substr(0,
- this.$.textarea.selectionStart);
-
- const caratSpan = this.$.caratSpan;
- this.$.hiddenText.appendChild(caratSpan);
- this.$.emojiSuggestions.positionTarget = caratSpan;
- this._openEmojiDropdown();
- }
-
- _getFontSize() {
- const fontSizePx = getComputedStyle(this).fontSize || '12px';
- return parseInt(fontSizePx.substr(0, fontSizePx.length - 2),
- 10);
- }
-
- _getScrollTop() {
- return document.body.scrollTop;
- }
-
- /**
- * _handleKeydown used for key handling in the this.$.textarea AND all child
- * autocomplete options.
- */
- _onValueChanged(e) {
- // Relay the event.
- this.fire('bind-value-changed', e);
-
- // If cursor is not in textarea (just opened with colon as last char),
- // Don't do anything.
- if (!e.currentTarget.focused) { return; }
-
- const charAtCursor = e.detail && e.detail.value ?
- e.detail.value[this.$.textarea.selectionStart - 1] : '';
- if (charAtCursor !== ':' && this._colonIndex == null) { return; }
-
- // When a colon is detected, set a colon index. We are interested only on
- // colons after space or in beginning of textarea
- if (charAtCursor === ':') {
- if (this.$.textarea.selectionStart < 2 ||
- e.detail.value[this.$.textarea.selectionStart - 2] === ' ') {
- this._colonIndex = this.$.textarea.selectionStart - 1;
- }
- }
-
- this._currentSearchString = e.detail.value.substr(this._colonIndex + 1,
- this.$.textarea.selectionStart - this._colonIndex - 1);
- // Under the following conditions, close and reset the dropdown:
- // - The cursor is no longer at the end of the current search string
- // - The search string is an space or new line
- // - The colon has been removed
- // - There are no suggestions that match the search string
- if (this.$.textarea.selectionStart !==
- this._currentSearchString.length + this._colonIndex + 1 ||
- this._currentSearchString === ' ' ||
- this._currentSearchString === '\n' ||
- !(e.detail.value[this._colonIndex] === ':') ||
- !this._suggestions.length) {
- this._resetEmojiDropdown();
- // Otherwise open the dropdown and set the position to be just below the
- // cursor.
- } else if (this.$.emojiSuggestions.isHidden) {
- this._updateCaratPosition();
- }
- this.$.textarea.textarea.focus();
- }
-
- _openEmojiDropdown() {
- this.$.emojiSuggestions.open();
- this.$.reporting.reportInteraction('open-emoji-dropdown');
- }
-
- _formatSuggestions(matchedSuggestions) {
- const suggestions = [];
- for (const suggestion of matchedSuggestions) {
- suggestion.dataValue = suggestion.value;
- suggestion.text = suggestion.value + ' ' + suggestion.match;
- suggestions.push(suggestion);
- }
- this.set('_suggestions', suggestions);
- }
-
- _determineSuggestions(emojiText) {
- if (!emojiText.length) {
- this._formatSuggestions(ALL_SUGGESTIONS);
- this.disableEnterKeyForSelectingEmoji = true;
- } else {
- const matches = ALL_SUGGESTIONS
- .filter(suggestion => suggestion.match.includes(emojiText))
- .slice(0, MAX_ITEMS_DROPDOWN);
- this._formatSuggestions(matches);
- this.disableEnterKeyForSelectingEmoji = false;
- }
- }
-
- _resetEmojiDropdown() {
- // hide and reset the autocomplete dropdown.
- Polymer.dom.flush();
- this._currentSearchString = '';
- this._hideAutocomplete = true;
- this.closeDropdown();
- this._colonIndex = null;
- this.$.textarea.textarea.focus();
- }
-
- _handleTextChanged(text) {
- this.dispatchEvent(
- new CustomEvent('value-changed', {detail: {value: text}}));
+ if (this.hideBorder) {
+ this.$.textarea.classList.add('noBorder');
}
}
- customElements.define(GrTextarea.is, GrTextarea);
-})();
+ closeDropdown() {
+ return this.$.emojiSuggestions.close();
+ }
+
+ getNativeTextarea() {
+ return this.$.textarea.textarea;
+ }
+
+ putCursorAtEnd() {
+ const textarea = this.getNativeTextarea();
+ // Put the cursor at the end always.
+ textarea.selectionStart = textarea.value.length;
+ textarea.selectionEnd = textarea.selectionStart;
+ this.async(() => {
+ textarea.focus();
+ });
+ }
+
+ _handleEscKey(e) {
+ if (this._hideAutocomplete) { return; }
+ e.preventDefault();
+ e.stopPropagation();
+ this._resetEmojiDropdown();
+ }
+
+ _handleUpKey(e) {
+ if (this._hideAutocomplete) { return; }
+ e.preventDefault();
+ e.stopPropagation();
+ this.$.emojiSuggestions.cursorUp();
+ this.$.textarea.textarea.focus();
+ this.disableEnterKeyForSelectingEmoji = false;
+ }
+
+ _handleDownKey(e) {
+ if (this._hideAutocomplete) { return; }
+ e.preventDefault();
+ e.stopPropagation();
+ this.$.emojiSuggestions.cursorDown();
+ this.$.textarea.textarea.focus();
+ this.disableEnterKeyForSelectingEmoji = false;
+ }
+
+ _handleEnterByKey(e) {
+ if (this._hideAutocomplete || this.disableEnterKeyForSelectingEmoji) {
+ return;
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ this._setEmoji(this.$.emojiSuggestions.getCurrentText());
+ }
+
+ _handleEmojiSelect(e) {
+ this._setEmoji(e.detail.selected.dataset.value);
+ }
+
+ _setEmoji(text) {
+ const colonIndex = this._colonIndex;
+ this.text = this._getText(text);
+ this.$.textarea.selectionStart = colonIndex + 1;
+ this.$.textarea.selectionEnd = colonIndex + 1;
+ this.$.reporting.reportInteraction('select-emoji', {type: text});
+ this._resetEmojiDropdown();
+ }
+
+ _getText(value) {
+ return this.text.substr(0, this._colonIndex || 0) +
+ value + this.text.substr(this.$.textarea.selectionStart);
+ }
+
+ /**
+ * Uses a hidden element with the same width and styling of the textarea and
+ * the text up until the point of interest. Then caratSpan element is added
+ * to the end and is set to be the positionTarget for the dropdown. Together
+ * this allows the dropdown to appear near where the user is typing.
+ */
+ _updateCaratPosition() {
+ this._hideAutocomplete = false;
+ this.$.hiddenText.textContent = this.$.textarea.value.substr(0,
+ this.$.textarea.selectionStart);
+
+ const caratSpan = this.$.caratSpan;
+ this.$.hiddenText.appendChild(caratSpan);
+ this.$.emojiSuggestions.positionTarget = caratSpan;
+ this._openEmojiDropdown();
+ }
+
+ _getFontSize() {
+ const fontSizePx = getComputedStyle(this).fontSize || '12px';
+ return parseInt(fontSizePx.substr(0, fontSizePx.length - 2),
+ 10);
+ }
+
+ _getScrollTop() {
+ return document.body.scrollTop;
+ }
+
+ /**
+ * _handleKeydown used for key handling in the this.$.textarea AND all child
+ * autocomplete options.
+ */
+ _onValueChanged(e) {
+ // Relay the event.
+ this.fire('bind-value-changed', e);
+
+ // If cursor is not in textarea (just opened with colon as last char),
+ // Don't do anything.
+ if (!e.currentTarget.focused) { return; }
+
+ const charAtCursor = e.detail && e.detail.value ?
+ e.detail.value[this.$.textarea.selectionStart - 1] : '';
+ if (charAtCursor !== ':' && this._colonIndex == null) { return; }
+
+ // When a colon is detected, set a colon index. We are interested only on
+ // colons after space or in beginning of textarea
+ if (charAtCursor === ':') {
+ if (this.$.textarea.selectionStart < 2 ||
+ e.detail.value[this.$.textarea.selectionStart - 2] === ' ') {
+ this._colonIndex = this.$.textarea.selectionStart - 1;
+ }
+ }
+
+ this._currentSearchString = e.detail.value.substr(this._colonIndex + 1,
+ this.$.textarea.selectionStart - this._colonIndex - 1);
+ // Under the following conditions, close and reset the dropdown:
+ // - The cursor is no longer at the end of the current search string
+ // - The search string is an space or new line
+ // - The colon has been removed
+ // - There are no suggestions that match the search string
+ if (this.$.textarea.selectionStart !==
+ this._currentSearchString.length + this._colonIndex + 1 ||
+ this._currentSearchString === ' ' ||
+ this._currentSearchString === '\n' ||
+ !(e.detail.value[this._colonIndex] === ':') ||
+ !this._suggestions.length) {
+ this._resetEmojiDropdown();
+ // Otherwise open the dropdown and set the position to be just below the
+ // cursor.
+ } else if (this.$.emojiSuggestions.isHidden) {
+ this._updateCaratPosition();
+ }
+ this.$.textarea.textarea.focus();
+ }
+
+ _openEmojiDropdown() {
+ this.$.emojiSuggestions.open();
+ this.$.reporting.reportInteraction('open-emoji-dropdown');
+ }
+
+ _formatSuggestions(matchedSuggestions) {
+ const suggestions = [];
+ for (const suggestion of matchedSuggestions) {
+ suggestion.dataValue = suggestion.value;
+ suggestion.text = suggestion.value + ' ' + suggestion.match;
+ suggestions.push(suggestion);
+ }
+ this.set('_suggestions', suggestions);
+ }
+
+ _determineSuggestions(emojiText) {
+ if (!emojiText.length) {
+ this._formatSuggestions(ALL_SUGGESTIONS);
+ this.disableEnterKeyForSelectingEmoji = true;
+ } else {
+ const matches = ALL_SUGGESTIONS
+ .filter(suggestion => suggestion.match.includes(emojiText))
+ .slice(0, MAX_ITEMS_DROPDOWN);
+ this._formatSuggestions(matches);
+ this.disableEnterKeyForSelectingEmoji = false;
+ }
+ }
+
+ _resetEmojiDropdown() {
+ // hide and reset the autocomplete dropdown.
+ flush();
+ this._currentSearchString = '';
+ this._hideAutocomplete = true;
+ this.closeDropdown();
+ this._colonIndex = null;
+ this.$.textarea.textarea.focus();
+ }
+
+ _handleTextChanged(text) {
+ this.dispatchEvent(
+ new CustomEvent('value-changed', {detail: {value: text}}));
+ }
+}
+
+customElements.define(GrTextarea.is, GrTextarea);
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js
new file mode 100644
index 0000000..99dd52d
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js
@@ -0,0 +1,78 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style include="shared-styles">
+ :host {
+ display: flex;
+ position: relative;
+ }
+ :host(.monospace) {
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-mono);
+ line-height: var(--line-height-mono);
+ font-weight: var(--font-weight-normal);
+ }
+ :host(.code) {
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-code);
+ line-height: var(--line-height-code);
+ font-weight: var(--font-weight-normal);
+ }
+ #emojiSuggestions {
+ font-family: var(--font-family);
+ }
+ gr-autocomplete {
+ display: inline-block
+ }
+ #textarea {
+ background-color: var(--view-background-color);
+ width: 100%;
+ }
+ #hiddenText #emojiSuggestions {
+ visibility: visible;
+ white-space: normal;
+ }
+ iron-autogrow-textarea {
+ position: relative;
+
+ /** This is needed for firefox */
+ --iron-autogrow-textarea_-_white-space: pre-wrap;
+ }
+ #textarea.noBorder {
+ border: none;
+ }
+ #hiddenText {
+ display: block;
+ float: left;
+ position: absolute;
+ visibility: hidden;
+ width: 100%;
+ white-space: pre-wrap;
+ }
+ </style>
+ <div id="hiddenText"></div>
+ <!-- When the autocomplete is open, the span is moved at the end of
+ hiddenText in order to correctly position the dropdown. After being moved,
+ it is set as the positionTarget for the emojiSuggestions dropdown. -->
+ <span id="caratSpan"></span>
+ <gr-autocomplete-dropdown vertical-align="top" horizontal-align="left" dynamic-align="" id="emojiSuggestions" suggestions="[[_suggestions]]" index="[[_index]]" vertical-offset="[[_verticalOffset]]" on-dropdown-closed="_resetEmojiDropdown" on-item-selected="_handleEmojiSelect">
+ </gr-autocomplete-dropdown>
+ <iron-autogrow-textarea id="textarea" autocomplete="[[autocomplete]]" placeholder="[[placeholder]]" disabled="[[disabled]]" rows="[[rows]]" max-rows="[[maxRows]]" value="{{text}}" on-bind-value-changed="_onValueChanged"></iron-autogrow-textarea>
+ <gr-reporting id="reporting"></gr-reporting>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
index 674089d..b8828d7 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
@@ -19,15 +19,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-textarea</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-textarea.html">
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
-<script>void(0);</script>
<test-fixture id="basic">
<template>
<gr-textarea></gr-textarea>
@@ -46,16 +42,300 @@
</template>
</test-fixture>
-<script>
- suite('gr-textarea tests', async () => {
- await readyToTest();
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-textarea.js';
+suite('gr-textarea tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ sandbox.stub(element.$.reporting, 'reportInteraction');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('monospace is set properly', () => {
+ assert.isFalse(element.classList.contains('monospace'));
+ });
+
+ test('hideBorder is set properly', () => {
+ assert.isFalse(element.$.textarea.classList.contains('noBorder'));
+ });
+
+ test('emoji selector is not open with the textarea lacks focus', () => {
+ element.$.textarea.selectionStart = 1;
+ element.$.textarea.selectionEnd = 1;
+ element.text = ':';
+ assert.isFalse(!element.$.emojiSuggestions.isHidden);
+ });
+
+ test('emoji selector is not open when a general text is entered', () => {
+ MockInteractions.focus(element.$.textarea);
+ element.$.textarea.selectionStart = 9;
+ element.$.textarea.selectionEnd = 9;
+ element.text = 'some text';
+ assert.isFalse(!element.$.emojiSuggestions.isHidden);
+ });
+
+ test('emoji selector opens when a colon is typed & the textarea has focus',
+ () => {
+ MockInteractions.focus(element.$.textarea);
+ // Needed for Safari tests. selectionStart is not updated when text is
+ // updated.
+ element.$.textarea.selectionStart = 1;
+ element.$.textarea.selectionEnd = 1;
+ element.text = ':';
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.emojiSuggestions.isHidden);
+ assert.equal(element._colonIndex, 0);
+ assert.isFalse(element._hideAutocomplete);
+ assert.equal(element._currentSearchString, '');
+ });
+
+ test('emoji selector opens when a colon is typed after space',
+ () => {
+ MockInteractions.focus(element.$.textarea);
+ // Needed for Safari tests. selectionStart is not updated when text is
+ // updated.
+ element.$.textarea.selectionStart = 2;
+ element.$.textarea.selectionEnd = 2;
+ element.text = ' :';
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.emojiSuggestions.isHidden);
+ assert.equal(element._colonIndex, 1);
+ assert.isFalse(element._hideAutocomplete);
+ assert.equal(element._currentSearchString, '');
+ });
+
+ test('emoji selector doesn\`t open when a colon is typed after character',
+ () => {
+ MockInteractions.focus(element.$.textarea);
+ // Needed for Safari tests. selectionStart is not updated when text is
+ // updated.
+ element.$.textarea.selectionStart = 5;
+ element.$.textarea.selectionEnd = 5;
+ element.text = 'test:';
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.emojiSuggestions.isHidden);
+ assert.isTrue(element._hideAutocomplete);
+ });
+
+ test('emoji selector opens when a colon is typed and some substring',
+ () => {
+ MockInteractions.focus(element.$.textarea);
+ // Needed for Safari tests. selectionStart is not updated when text is
+ // updated.
+ element.$.textarea.selectionStart = 1;
+ element.$.textarea.selectionEnd = 1;
+ element.text = ':';
+ element.$.textarea.selectionStart = 2;
+ element.$.textarea.selectionEnd = 2;
+ element.text = ':t';
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.emojiSuggestions.isHidden);
+ assert.equal(element._colonIndex, 0);
+ assert.isFalse(element._hideAutocomplete);
+ assert.equal(element._currentSearchString, 't');
+ });
+
+ test('emoji selector opens when a colon is typed in middle of text',
+ () => {
+ MockInteractions.focus(element.$.textarea);
+ // Needed for Safari tests. selectionStart is not updated when text is
+ // updated.
+ element.$.textarea.selectionStart = 1;
+ element.$.textarea.selectionEnd = 1;
+ // Since selectionStart is on Chrome set always on end of text, we
+ // stub it to 1
+ const text = ': hello';
+ sandbox.stub(element.$, 'textarea', {
+ selectionStart: 1,
+ value: text,
+ textarea: {
+ focus: () => {},
+ },
+ });
+ element.text = text;
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.emojiSuggestions.isHidden);
+ assert.equal(element._colonIndex, 0);
+ assert.isFalse(element._hideAutocomplete);
+ assert.equal(element._currentSearchString, '');
+ });
+ test('emoji selector closes when text changes before the colon', () => {
+ const resetStub = sandbox.stub(element, '_resetEmojiDropdown');
+ MockInteractions.focus(element.$.textarea);
+ flushAsynchronousOperations();
+ element.$.textarea.selectionStart = 10;
+ element.$.textarea.selectionEnd = 10;
+ element.text = 'test test ';
+ element.$.textarea.selectionStart = 12;
+ element.$.textarea.selectionEnd = 12;
+ element.text = 'test test :';
+ element.$.textarea.selectionStart = 15;
+ element.$.textarea.selectionEnd = 15;
+ element.text = 'test test :smi';
+
+ assert.equal(element._currentSearchString, 'smi');
+ assert.isFalse(resetStub.called);
+ element.text = 'test test test :smi';
+ assert.isTrue(resetStub.called);
+ });
+
+ test('_resetEmojiDropdown', () => {
+ const closeSpy = sandbox.spy(element, 'closeDropdown');
+ element._resetEmojiDropdown();
+ assert.equal(element._currentSearchString, '');
+ assert.isTrue(element._hideAutocomplete);
+ assert.equal(element._colonIndex, null);
+
+ element.$.emojiSuggestions.open();
+ flushAsynchronousOperations();
+ element._resetEmojiDropdown();
+ assert.isTrue(closeSpy.called);
+ });
+
+ test('_determineSuggestions', () => {
+ const emojiText = 'tear';
+ const formatSpy = sandbox.spy(element, '_formatSuggestions');
+ element._determineSuggestions(emojiText);
+ assert.isTrue(formatSpy.called);
+ assert.isTrue(formatSpy.lastCall.calledWithExactly(
+ [{dataValue: '😂', value: '😂', match: 'tears :\')',
+ text: '😂 tears :\')'},
+ {dataValue: '😢', value: '😢', match: 'tear', text: '😢 tear'},
+ ]));
+ });
+
+ test('_formatSuggestions', () => {
+ const matchedSuggestions = [{value: '😢', match: 'tear'},
+ {value: '😂', match: 'tears'}];
+ element._formatSuggestions(matchedSuggestions);
+ assert.deepEqual(
+ [{value: '😢', dataValue: '😢', match: 'tear', text: '😢 tear'},
+ {value: '😂', dataValue: '😂', match: 'tears', text: '😂 tears'}],
+ element._suggestions);
+ });
+
+ test('_handleEmojiSelect', () => {
+ element.$.textarea.selectionStart = 16;
+ element.$.textarea.selectionEnd = 16;
+ element.text = 'test test :tears';
+ element._colonIndex = 10;
+ const selectedItem = {dataset: {value: '😂'}};
+ const event = {detail: {selected: selectedItem}};
+ element._handleEmojiSelect(event);
+ assert.equal(element.text, 'test test 😂');
+ });
+
+ test('_updateCaratPosition', () => {
+ element.$.textarea.selectionStart = 4;
+ element.$.textarea.selectionEnd = 4;
+ element.text = 'test';
+ element._updateCaratPosition();
+ assert.deepEqual(element.$.hiddenText.innerHTML, element.text +
+ element.$.caratSpan.outerHTML);
+ });
+
+ test('emoji dropdown is closed when iron-overlay-closed is fired', () => {
+ const resetSpy = sandbox.spy(element, '_resetEmojiDropdown');
+ element.$.emojiSuggestions.fire('dropdown-closed');
+ assert.isTrue(resetSpy.called);
+ });
+
+ test('_onValueChanged fires bind-value-changed', () => {
+ const listenerStub = sinon.stub();
+ const eventObject = {currentTarget: {focused: false}};
+ element.addEventListener('bind-value-changed', listenerStub);
+ element._onValueChanged(eventObject);
+ assert.isTrue(listenerStub.called);
+ });
+
+ suite('keyboard shortcuts', () => {
+ function setupDropdown(callback) {
+ MockInteractions.focus(element.$.textarea);
+ element.$.textarea.selectionStart = 1;
+ element.$.textarea.selectionEnd = 1;
+ element.text = ':';
+ element.$.textarea.selectionStart = 1;
+ element.$.textarea.selectionEnd = 2;
+ element.text = ':1';
+ flushAsynchronousOperations();
+ }
+
+ test('escape key', () => {
+ const resetSpy = sandbox.spy(element, '_resetEmojiDropdown');
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
+ assert.isFalse(resetSpy.called);
+ setupDropdown();
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
+ assert.isTrue(resetSpy.called);
+ assert.isFalse(!element.$.emojiSuggestions.isHidden);
+ });
+
+ test('up key', () => {
+ const upSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorUp');
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
+ assert.isFalse(upSpy.called);
+ setupDropdown();
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
+ assert.isTrue(upSpy.called);
+ });
+
+ test('down key', () => {
+ const downSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorDown');
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
+ assert.isFalse(downSpy.called);
+ setupDropdown();
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
+ assert.isTrue(downSpy.called);
+ });
+
+ test('enter key', () => {
+ const enterSpy = sandbox.spy(element.$.emojiSuggestions,
+ 'getCursorTarget');
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+ assert.isFalse(enterSpy.called);
+ setupDropdown();
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+ assert.isTrue(enterSpy.called);
+ flushAsynchronousOperations();
+ assert.equal(element.text, '💯');
+ });
+
+ test('enter key - ignored on just colon without more information', () => {
+ const enterSpy = sandbox.spy(element.$.emojiSuggestions,
+ 'getCursorTarget');
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+ assert.isFalse(enterSpy.called);
+ MockInteractions.focus(element.$.textarea);
+ element.$.textarea.selectionStart = 1;
+ element.$.textarea.selectionEnd = 1;
+ element.text = ':';
+ flushAsynchronousOperations();
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+ assert.isFalse(enterSpy.called);
+ });
+ });
+
+ suite('gr-textarea monospace', () => {
+ // gr-textarea set monospace class in the ready() method.
+ // In Polymer2, ready() is called from the fixture(...) method,
+ // If ready() is called again later, some nested elements doesn't
+ // handle it correctly. A separate test-fixture is used to set
+ // properties before ready() is called.
+
let element;
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
- element = fixture('basic');
- sandbox.stub(element.$.reporting, 'reportInteraction');
+ element = fixture('monospace');
});
teardown(() => {
@@ -63,315 +343,32 @@
});
test('monospace is set properly', () => {
- assert.isFalse(element.classList.contains('monospace'));
+ assert.isTrue(element.classList.contains('monospace'));
+ });
+ });
+
+ suite('gr-textarea hideBorder', () => {
+ // gr-textarea set noBorder class in the ready() method.
+ // In Polymer2, ready() is called from the fixture(...) method,
+ // If ready() is called again later, some nested elements doesn't
+ // handle it correctly. A separate test-fixture is used to set
+ // properties before ready() is called.
+
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('hideBorder');
+ });
+
+ teardown(() => {
+ sandbox.restore();
});
test('hideBorder is set properly', () => {
- assert.isFalse(element.$.textarea.classList.contains('noBorder'));
- });
-
- test('emoji selector is not open with the textarea lacks focus', () => {
- element.$.textarea.selectionStart = 1;
- element.$.textarea.selectionEnd = 1;
- element.text = ':';
- assert.isFalse(!element.$.emojiSuggestions.isHidden);
- });
-
- test('emoji selector is not open when a general text is entered', () => {
- MockInteractions.focus(element.$.textarea);
- element.$.textarea.selectionStart = 9;
- element.$.textarea.selectionEnd = 9;
- element.text = 'some text';
- assert.isFalse(!element.$.emojiSuggestions.isHidden);
- });
-
- test('emoji selector opens when a colon is typed & the textarea has focus',
- () => {
- MockInteractions.focus(element.$.textarea);
- // Needed for Safari tests. selectionStart is not updated when text is
- // updated.
- element.$.textarea.selectionStart = 1;
- element.$.textarea.selectionEnd = 1;
- element.text = ':';
- flushAsynchronousOperations();
- assert.isFalse(element.$.emojiSuggestions.isHidden);
- assert.equal(element._colonIndex, 0);
- assert.isFalse(element._hideAutocomplete);
- assert.equal(element._currentSearchString, '');
- });
-
- test('emoji selector opens when a colon is typed after space',
- () => {
- MockInteractions.focus(element.$.textarea);
- // Needed for Safari tests. selectionStart is not updated when text is
- // updated.
- element.$.textarea.selectionStart = 2;
- element.$.textarea.selectionEnd = 2;
- element.text = ' :';
- flushAsynchronousOperations();
- assert.isFalse(element.$.emojiSuggestions.isHidden);
- assert.equal(element._colonIndex, 1);
- assert.isFalse(element._hideAutocomplete);
- assert.equal(element._currentSearchString, '');
- });
-
- test('emoji selector doesn\`t open when a colon is typed after character',
- () => {
- MockInteractions.focus(element.$.textarea);
- // Needed for Safari tests. selectionStart is not updated when text is
- // updated.
- element.$.textarea.selectionStart = 5;
- element.$.textarea.selectionEnd = 5;
- element.text = 'test:';
- flushAsynchronousOperations();
- assert.isTrue(element.$.emojiSuggestions.isHidden);
- assert.isTrue(element._hideAutocomplete);
- });
-
- test('emoji selector opens when a colon is typed and some substring',
- () => {
- MockInteractions.focus(element.$.textarea);
- // Needed for Safari tests. selectionStart is not updated when text is
- // updated.
- element.$.textarea.selectionStart = 1;
- element.$.textarea.selectionEnd = 1;
- element.text = ':';
- element.$.textarea.selectionStart = 2;
- element.$.textarea.selectionEnd = 2;
- element.text = ':t';
- flushAsynchronousOperations();
- assert.isFalse(element.$.emojiSuggestions.isHidden);
- assert.equal(element._colonIndex, 0);
- assert.isFalse(element._hideAutocomplete);
- assert.equal(element._currentSearchString, 't');
- });
-
- test('emoji selector opens when a colon is typed in middle of text',
- () => {
- MockInteractions.focus(element.$.textarea);
- // Needed for Safari tests. selectionStart is not updated when text is
- // updated.
- element.$.textarea.selectionStart = 1;
- element.$.textarea.selectionEnd = 1;
- // Since selectionStart is on Chrome set always on end of text, we
- // stub it to 1
- const text = ': hello';
- sandbox.stub(element.$, 'textarea', {
- selectionStart: 1,
- value: text,
- textarea: {
- focus: () => {},
- },
- });
- element.text = text;
- flushAsynchronousOperations();
- assert.isFalse(element.$.emojiSuggestions.isHidden);
- assert.equal(element._colonIndex, 0);
- assert.isFalse(element._hideAutocomplete);
- assert.equal(element._currentSearchString, '');
- });
- test('emoji selector closes when text changes before the colon', () => {
- const resetStub = sandbox.stub(element, '_resetEmojiDropdown');
- MockInteractions.focus(element.$.textarea);
- flushAsynchronousOperations();
- element.$.textarea.selectionStart = 10;
- element.$.textarea.selectionEnd = 10;
- element.text = 'test test ';
- element.$.textarea.selectionStart = 12;
- element.$.textarea.selectionEnd = 12;
- element.text = 'test test :';
- element.$.textarea.selectionStart = 15;
- element.$.textarea.selectionEnd = 15;
- element.text = 'test test :smi';
-
- assert.equal(element._currentSearchString, 'smi');
- assert.isFalse(resetStub.called);
- element.text = 'test test test :smi';
- assert.isTrue(resetStub.called);
- });
-
- test('_resetEmojiDropdown', () => {
- const closeSpy = sandbox.spy(element, 'closeDropdown');
- element._resetEmojiDropdown();
- assert.equal(element._currentSearchString, '');
- assert.isTrue(element._hideAutocomplete);
- assert.equal(element._colonIndex, null);
-
- element.$.emojiSuggestions.open();
- flushAsynchronousOperations();
- element._resetEmojiDropdown();
- assert.isTrue(closeSpy.called);
- });
-
- test('_determineSuggestions', () => {
- const emojiText = 'tear';
- const formatSpy = sandbox.spy(element, '_formatSuggestions');
- element._determineSuggestions(emojiText);
- assert.isTrue(formatSpy.called);
- assert.isTrue(formatSpy.lastCall.calledWithExactly(
- [{dataValue: '😂', value: '😂', match: 'tears :\')',
- text: '😂 tears :\')'},
- {dataValue: '😢', value: '😢', match: 'tear', text: '😢 tear'},
- ]));
- });
-
- test('_formatSuggestions', () => {
- const matchedSuggestions = [{value: '😢', match: 'tear'},
- {value: '😂', match: 'tears'}];
- element._formatSuggestions(matchedSuggestions);
- assert.deepEqual(
- [{value: '😢', dataValue: '😢', match: 'tear', text: '😢 tear'},
- {value: '😂', dataValue: '😂', match: 'tears', text: '😂 tears'}],
- element._suggestions);
- });
-
- test('_handleEmojiSelect', () => {
- element.$.textarea.selectionStart = 16;
- element.$.textarea.selectionEnd = 16;
- element.text = 'test test :tears';
- element._colonIndex = 10;
- const selectedItem = {dataset: {value: '😂'}};
- const event = {detail: {selected: selectedItem}};
- element._handleEmojiSelect(event);
- assert.equal(element.text, 'test test 😂');
- });
-
- test('_updateCaratPosition', () => {
- element.$.textarea.selectionStart = 4;
- element.$.textarea.selectionEnd = 4;
- element.text = 'test';
- element._updateCaratPosition();
- assert.deepEqual(element.$.hiddenText.innerHTML, element.text +
- element.$.caratSpan.outerHTML);
- });
-
- test('emoji dropdown is closed when iron-overlay-closed is fired', () => {
- const resetSpy = sandbox.spy(element, '_resetEmojiDropdown');
- element.$.emojiSuggestions.fire('dropdown-closed');
- assert.isTrue(resetSpy.called);
- });
-
- test('_onValueChanged fires bind-value-changed', () => {
- const listenerStub = sinon.stub();
- const eventObject = {currentTarget: {focused: false}};
- element.addEventListener('bind-value-changed', listenerStub);
- element._onValueChanged(eventObject);
- assert.isTrue(listenerStub.called);
- });
-
- suite('keyboard shortcuts', () => {
- function setupDropdown(callback) {
- MockInteractions.focus(element.$.textarea);
- element.$.textarea.selectionStart = 1;
- element.$.textarea.selectionEnd = 1;
- element.text = ':';
- element.$.textarea.selectionStart = 1;
- element.$.textarea.selectionEnd = 2;
- element.text = ':1';
- flushAsynchronousOperations();
- }
-
- test('escape key', () => {
- const resetSpy = sandbox.spy(element, '_resetEmojiDropdown');
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
- assert.isFalse(resetSpy.called);
- setupDropdown();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
- assert.isTrue(resetSpy.called);
- assert.isFalse(!element.$.emojiSuggestions.isHidden);
- });
-
- test('up key', () => {
- const upSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorUp');
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
- assert.isFalse(upSpy.called);
- setupDropdown();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
- assert.isTrue(upSpy.called);
- });
-
- test('down key', () => {
- const downSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorDown');
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
- assert.isFalse(downSpy.called);
- setupDropdown();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
- assert.isTrue(downSpy.called);
- });
-
- test('enter key', () => {
- const enterSpy = sandbox.spy(element.$.emojiSuggestions,
- 'getCursorTarget');
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
- assert.isFalse(enterSpy.called);
- setupDropdown();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
- assert.isTrue(enterSpy.called);
- flushAsynchronousOperations();
- assert.equal(element.text, '💯');
- });
-
- test('enter key - ignored on just colon without more information', () => {
- const enterSpy = sandbox.spy(element.$.emojiSuggestions,
- 'getCursorTarget');
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
- assert.isFalse(enterSpy.called);
- MockInteractions.focus(element.$.textarea);
- element.$.textarea.selectionStart = 1;
- element.$.textarea.selectionEnd = 1;
- element.text = ':';
- flushAsynchronousOperations();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
- assert.isFalse(enterSpy.called);
- });
- });
-
- suite('gr-textarea monospace', () => {
- // gr-textarea set monospace class in the ready() method.
- // In Polymer2, ready() is called from the fixture(...) method,
- // If ready() is called again later, some nested elements doesn't
- // handle it correctly. A separate test-fixture is used to set
- // properties before ready() is called.
-
- let element;
- let sandbox;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('monospace');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('monospace is set properly', () => {
- assert.isTrue(element.classList.contains('monospace'));
- });
- });
-
- suite('gr-textarea hideBorder', () => {
- // gr-textarea set noBorder class in the ready() method.
- // In Polymer2, ready() is called from the fixture(...) method,
- // If ready() is called again later, some nested elements doesn't
- // handle it correctly. A separate test-fixture is used to set
- // properties before ready() is called.
-
- let element;
- let sandbox;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('hideBorder');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('hideBorder is set properly', () => {
- assert.isTrue(element.$.textarea.classList.contains('noBorder'));
- });
+ assert.isTrue(element.$.textarea.classList.contains('noBorder'));
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
deleted file mode 100644
index ec56912..0000000
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../gr-icons/gr-icons.html">
-<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
-
-<dom-module id="gr-tooltip-content">
- <template>
- <style>
- iron-icon {
- width: var(--line-height-normal);
- height: var(--line-height-normal);
- vertical-align: top;
- }
- </style>
- <slot></slot><!--
- --><iron-icon icon="gr-icons:info" hidden$="[[!showIcon]]"></iron-icon>
- </template>
- <script src="gr-tooltip-content.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
index baa0fc9..3c9181f 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
@@ -14,33 +14,41 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.TooltipMixin
- * @extends Polymer.Element
- */
- class GrTooltipContent extends Polymer.mixinBehaviors( [
- Gerrit.TooltipBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-tooltip-content'; }
+import '../gr-icons/gr-icons.js';
+import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-tooltip-content_html.js';
- static get properties() {
- return {
- maxWidth: {
- type: String,
- reflectToAttribute: true,
- },
- showIcon: {
- type: Boolean,
- value: false,
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.TooltipMixin
+ * @extends Polymer.Element
+ */
+class GrTooltipContent extends mixinBehaviors( [
+ Gerrit.TooltipBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-tooltip-content'; }
+
+ static get properties() {
+ return {
+ maxWidth: {
+ type: String,
+ reflectToAttribute: true,
+ },
+ showIcon: {
+ type: Boolean,
+ value: false,
+ },
+ };
}
+}
- customElements.define(GrTooltipContent.is, GrTooltipContent);
-})();
+customElements.define(GrTooltipContent.is, GrTooltipContent);
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.js
new file mode 100644
index 0000000..e4b5891
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.js
@@ -0,0 +1,29 @@
+/**
+ * @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.js';
+
+export const htmlTemplate = html`
+ <style>
+ iron-icon {
+ width: var(--line-height-normal);
+ height: var(--line-height-normal);
+ vertical-align: top;
+ }
+ </style>
+ <slot></slot><!--
+ --><iron-icon icon="gr-icons:info" hidden\$="[[!showIcon]]"></iron-icon>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
index 8237552..e2c2f60 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
@@ -18,15 +18,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-storage</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-tooltip-content.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,29 +30,31 @@
</template>
</test-fixture>
-<script>
- suite('gr-tooltip-content tests', async () => {
- await readyToTest();
- let element;
- setup(() => {
- element = fixture('basic');
- });
-
- test('icon is not visible by default', () => {
- assert.equal(Polymer.dom(element.root)
- .querySelector('iron-icon').hidden, true);
- });
-
- test('position-below attribute is reflected', () => {
- assert.isFalse(element.hasAttribute('position-below'));
- element.positionBelow = true;
- assert.isTrue(element.hasAttribute('position-below'));
- });
-
- test('icon is visible with showIcon property', () => {
- element.showIcon = true;
- assert.equal(Polymer.dom(element.root)
- .querySelector('iron-icon').hidden, false);
- });
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-tooltip-content.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-tooltip-content tests', () => {
+ let element;
+ setup(() => {
+ element = fixture('basic');
});
+
+ test('icon is not visible by default', () => {
+ assert.equal(dom(element.root)
+ .querySelector('iron-icon').hidden, true);
+ });
+
+ test('position-below attribute is reflected', () => {
+ assert.isFalse(element.hasAttribute('position-below'));
+ element.positionBelow = true;
+ assert.isTrue(element.hasAttribute('position-below'));
+ });
+
+ test('icon is visible with showIcon property', () => {
+ element.showIcon = true;
+ assert.equal(dom(element.root)
+ .querySelector('iron-icon').hidden, false);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
index 6f458d1..0cd2d7c 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
@@ -14,33 +14,39 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /** @extends Polymer.Element */
- class GrTooltip extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-tooltip'; }
+import '../../../styles/shared-styles.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-tooltip_html.js';
- static get properties() {
- return {
- text: String,
- maxWidth: {
- type: String,
- observer: '_updateWidth',
- },
- positionBelow: {
- type: Boolean,
- reflectToAttribute: true,
- },
- };
- }
+/** @extends Polymer.Element */
+class GrTooltip extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _updateWidth(maxWidth) {
- this.updateStyles({'--tooltip-max-width': maxWidth});
- }
+ static get is() { return 'gr-tooltip'; }
+
+ static get properties() {
+ return {
+ text: String,
+ maxWidth: {
+ type: String,
+ observer: '_updateWidth',
+ },
+ positionBelow: {
+ type: Boolean,
+ reflectToAttribute: true,
+ },
+ };
}
- customElements.define(GrTooltip.is, GrTooltip);
-})();
+ _updateWidth(maxWidth) {
+ this.updateStyles({'--tooltip-max-width': maxWidth});
+ }
+}
+
+customElements.define(GrTooltip.is, GrTooltip);
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.js
similarity index 65%
rename from polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
rename to polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.js
index d78d554..5f9ce51 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.js
@@ -1,25 +1,22 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.js';
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-tooltip">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
--gr-tooltip-arrow-size: .5em;
@@ -66,6 +63,4 @@
[[text]]
<i class="arrowPositionAbove arrow"></i>
</div>
- </template>
- <script src="gr-tooltip.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
index 4c9b954..7d599b23 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
@@ -18,15 +18,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-storage</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-tooltip.html">
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -35,35 +30,36 @@
</template>
</test-fixture>
-<script>
- suite('gr-tooltip tests', async () => {
- await readyToTest();
- let element;
- setup(() => {
- element = fixture('basic');
- });
-
- test('max-width is respected if set', () => {
- element.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit' +
- ', sed do eiusmod tempor incididunt ut labore et dolore magna aliqua';
- element.maxWidth = '50px';
- assert.equal(getComputedStyle(element).width, '50px');
- });
-
- test('the correct arrow is displayed', () => {
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('.arrowPositionBelow')).display,
- 'none');
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('.arrowPositionAbove'))
- .display, 'none');
- element.positionBelow = true;
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('.arrowPositionBelow'))
- .display, 'none');
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('.arrowPositionAbove'))
- .display, 'none');
- });
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './gr-tooltip.js';
+suite('gr-tooltip tests', () => {
+ let element;
+ setup(() => {
+ element = fixture('basic');
});
+
+ test('max-width is respected if set', () => {
+ element.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit' +
+ ', sed do eiusmod tempor incididunt ut labore et dolore magna aliqua';
+ element.maxWidth = '50px';
+ assert.equal(getComputedStyle(element).width, '50px');
+ });
+
+ test('the correct arrow is displayed', () => {
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('.arrowPositionBelow')).display,
+ 'none');
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('.arrowPositionAbove'))
+ .display, 'none');
+ element.positionBelow = true;
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('.arrowPositionBelow'))
+ .display, 'none');
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('.arrowPositionAbove'))
+ .display, 'none');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/revision-info/revision-info.html b/polygerrit-ui/app/elements/shared/revision-info/revision-info.html
deleted file mode 100644
index 239e0fa..0000000
--- a/polygerrit-ui/app/elements/shared/revision-info/revision-info.html
+++ /dev/null
@@ -1,88 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<script>
- (function() {
- 'use strict';
-
- /**
- * @constructor
- * @param {Object} change A change object resulting from a change detail
- * call that includes revision information.
- */
- function RevisionInfo(change) {
- this._change = change;
- }
-
- /**
- * Get the largest number of parents of the commit in any revision. For
- * example, with normal changes this will always return 1. For merge changes
- * wherein the revisions are merge commits this will return 2 or potentially
- * more.
- *
- * @return {number}
- */
- RevisionInfo.prototype.getMaxParents = function() {
- if (!this._change || !this._change.revisions) {
- return 0;
- }
- return Object.values(this._change.revisions)
- .reduce((acc, rev) => Math.max(rev.commit.parents.length, acc), 0);
- };
-
- /**
- * Get an object that maps revision numbers to the number of parents of the
- * commit of that revision.
- *
- * @return {!Object}
- */
- RevisionInfo.prototype.getParentCountMap = function() {
- const result = {};
- if (!this._change || !this._change.revisions) {
- return {};
- }
- Object.values(this._change.revisions)
- .forEach(rev => { result[rev._number] = rev.commit.parents.length; });
- return result;
- };
-
- /**
- * @param {number|string} patchNum
- * @return {number}
- */
- RevisionInfo.prototype.getParentCount = function(patchNum) {
- return this.getParentCountMap()[patchNum];
- };
-
- /**
- * Get the commit ID of the (0-offset) indexed parent in the given revision
- * number.
- *
- * @param {number|string} patchNum
- * @param {number} parentIndex (0-offset)
- * @return {string}
- */
- RevisionInfo.prototype.getParentId = function(patchNum, parentIndex) {
- const rev = Object.values(this._change.revisions).find(rev =>
- Gerrit.PatchSetBehavior.patchNumEquals(rev._number, patchNum));
- return rev.commit.parents[parentIndex].commit;
- };
-
- window.Gerrit = window.Gerrit || {};
- window.Gerrit.RevisionInfo = RevisionInfo;
- })();
-</script>
diff --git a/polygerrit-ui/app/elements/shared/revision-info/revision-info.js b/polygerrit-ui/app/elements/shared/revision-info/revision-info.js
new file mode 100644
index 0000000..f89234f
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/revision-info/revision-info.js
@@ -0,0 +1,83 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+
+/**
+ * @constructor
+ * @param {Object} change A change object resulting from a change detail
+ * call that includes revision information.
+ */
+function RevisionInfo(change) {
+ this._change = change;
+}
+
+/**
+ * Get the largest number of parents of the commit in any revision. For
+ * example, with normal changes this will always return 1. For merge changes
+ * wherein the revisions are merge commits this will return 2 or potentially
+ * more.
+ *
+ * @return {number}
+ */
+RevisionInfo.prototype.getMaxParents = function() {
+ if (!this._change || !this._change.revisions) {
+ return 0;
+ }
+ return Object.values(this._change.revisions)
+ .reduce((acc, rev) => Math.max(rev.commit.parents.length, acc), 0);
+};
+
+/**
+ * Get an object that maps revision numbers to the number of parents of the
+ * commit of that revision.
+ *
+ * @return {!Object}
+ */
+RevisionInfo.prototype.getParentCountMap = function() {
+ const result = {};
+ if (!this._change || !this._change.revisions) {
+ return {};
+ }
+ Object.values(this._change.revisions)
+ .forEach(rev => { result[rev._number] = rev.commit.parents.length; });
+ return result;
+};
+
+/**
+ * @param {number|string} patchNum
+ * @return {number}
+ */
+RevisionInfo.prototype.getParentCount = function(patchNum) {
+ return this.getParentCountMap()[patchNum];
+};
+
+/**
+ * Get the commit ID of the (0-offset) indexed parent in the given revision
+ * number.
+ *
+ * @param {number|string} patchNum
+ * @param {number} parentIndex (0-offset)
+ * @return {string}
+ */
+RevisionInfo.prototype.getParentId = function(patchNum, parentIndex) {
+ const rev = Object.values(this._change.revisions).find(rev =>
+ Gerrit.PatchSetBehavior.patchNumEquals(rev._number, patchNum));
+ return rev.commit.parents[parentIndex].commit;
+};
+
+window.Gerrit = window.Gerrit || {};
+window.Gerrit.RevisionInfo = RevisionInfo;
diff --git a/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html b/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html
index fb7a011..d6804c0 100644
--- a/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html
+++ b/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html
@@ -19,72 +19,70 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>revision-info</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="revision-info.html">
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
-<script>
- suite('revision-info tests', async () => {
- await readyToTest();
- let mockChange;
+<script type="module">
+import '../../../test/common-test-setup.js';
+import './revision-info.js';
+suite('revision-info tests', () => {
+ let mockChange;
- setup(() => {
- mockChange = {
- revisions: {
- r1: {_number: 1, commit: {parents: [
- {commit: 'p1'},
- {commit: 'p2'},
- {commit: 'p3'},
- ]}},
- r2: {_number: 2, commit: {parents: [
- {commit: 'p1'},
- {commit: 'p4'},
- ]}},
- r3: {_number: 3, commit: {parents: [{commit: 'p5'}]}},
- r4: {_number: 4, commit: {parents: [
- {commit: 'p2'},
- {commit: 'p3'},
- ]}},
- r5: {_number: 5, commit: {parents: [
- {commit: 'p5'},
- {commit: 'p2'},
- {commit: 'p3'},
- ]}},
- },
- };
- });
-
- test('getMaxParents', () => {
- const ri = new window.Gerrit.RevisionInfo(mockChange);
- assert.equal(ri.getMaxParents(), 3);
- });
-
- test('getParentCountMap', () => {
- const ri = new window.Gerrit.RevisionInfo(mockChange);
- assert.deepEqual(ri.getParentCountMap(), {1: 3, 2: 2, 3: 1, 4: 2, 5: 3});
- });
-
- test('getParentCount', () => {
- const ri = new window.Gerrit.RevisionInfo(mockChange);
- assert.deepEqual(ri.getParentCount(1), 3);
- assert.deepEqual(ri.getParentCount(3), 1);
- });
-
- test('getParentCount', () => {
- const ri = new window.Gerrit.RevisionInfo(mockChange);
- assert.deepEqual(ri.getParentCount(1), 3);
- assert.deepEqual(ri.getParentCount(3), 1);
- });
-
- test('getParentId', () => {
- const ri = new window.Gerrit.RevisionInfo(mockChange);
- assert.deepEqual(ri.getParentId(1, 2), 'p3');
- assert.deepEqual(ri.getParentId(2, 1), 'p4');
- assert.deepEqual(ri.getParentId(3, 0), 'p5');
- });
+ setup(() => {
+ mockChange = {
+ revisions: {
+ r1: {_number: 1, commit: {parents: [
+ {commit: 'p1'},
+ {commit: 'p2'},
+ {commit: 'p3'},
+ ]}},
+ r2: {_number: 2, commit: {parents: [
+ {commit: 'p1'},
+ {commit: 'p4'},
+ ]}},
+ r3: {_number: 3, commit: {parents: [{commit: 'p5'}]}},
+ r4: {_number: 4, commit: {parents: [
+ {commit: 'p2'},
+ {commit: 'p3'},
+ ]}},
+ r5: {_number: 5, commit: {parents: [
+ {commit: 'p5'},
+ {commit: 'p2'},
+ {commit: 'p3'},
+ ]}},
+ },
+ };
});
+
+ test('getMaxParents', () => {
+ const ri = new window.Gerrit.RevisionInfo(mockChange);
+ assert.equal(ri.getMaxParents(), 3);
+ });
+
+ test('getParentCountMap', () => {
+ const ri = new window.Gerrit.RevisionInfo(mockChange);
+ assert.deepEqual(ri.getParentCountMap(), {1: 3, 2: 2, 3: 1, 4: 2, 5: 3});
+ });
+
+ test('getParentCount', () => {
+ const ri = new window.Gerrit.RevisionInfo(mockChange);
+ assert.deepEqual(ri.getParentCount(1), 3);
+ assert.deepEqual(ri.getParentCount(3), 1);
+ });
+
+ test('getParentCount', () => {
+ const ri = new window.Gerrit.RevisionInfo(mockChange);
+ assert.deepEqual(ri.getParentCount(1), 3);
+ assert.deepEqual(ri.getParentCount(3), 1);
+ });
+
+ test('getParentId', () => {
+ const ri = new window.Gerrit.RevisionInfo(mockChange);
+ assert.deepEqual(ri.getParentId(1, 2), 'p3');
+ assert.deepEqual(ri.getParentId(2, 1), 'p4');
+ assert.deepEqual(ri.getParentId(3, 0), 'p5');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/embed/gr-diff.html b/polygerrit-ui/app/embed/gr-diff.html
index f5f74bd..c6b9f94 100644
--- a/polygerrit-ui/app/embed/gr-diff.html
+++ b/polygerrit-ui/app/embed/gr-diff.html
@@ -14,8 +14,5 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<script>
- window.Gerrit = window.Gerrit || {};
-</script>
-<link rel="import" href="../elements/diff/gr-diff/gr-diff.html">
-<link rel="import" href="../elements/diff/gr-diff-cursor/gr-diff-cursor.html">
+
+<script type="module" src="./gr-diff.js"> </script>
diff --git a/polygerrit-ui/app/embed/gr-diff.js b/polygerrit-ui/app/embed/gr-diff.js
new file mode 100644
index 0000000..76de583
--- /dev/null
+++ b/polygerrit-ui/app/embed/gr-diff.js
@@ -0,0 +1,20 @@
+/**
+ * @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.
+ */
+
+window.Gerrit = window.Gerrit || {};
+import '../elements/diff/gr-diff/gr-diff.js';
+import '../elements/diff/gr-diff-cursor/gr-diff-cursor.js';
diff --git a/polygerrit-ui/app/gr-diff/gr-diff-root.html b/polygerrit-ui/app/gr-diff/gr-diff-root.html
index b3f0d34..989da46 100644
--- a/polygerrit-ui/app/gr-diff/gr-diff-root.html
+++ b/polygerrit-ui/app/gr-diff/gr-diff-root.html
@@ -1,4 +1,18 @@
-<script>
- window.Gerrit = window.Gerrit || {};
-</script>
-<link rel="import" href="../elements/diff/gr-diff/gr-diff.html">
+<!--
+@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.
+-->
+
+<script type="module" src="./gr-diff-root.js"></script>
diff --git a/polygerrit-ui/app/gr-diff/gr-diff-root.js b/polygerrit-ui/app/gr-diff/gr-diff-root.js
new file mode 100644
index 0000000..bb5d602
--- /dev/null
+++ b/polygerrit-ui/app/gr-diff/gr-diff-root.js
@@ -0,0 +1,19 @@
+/**
+ * @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.
+ */
+
+window.Gerrit = window.Gerrit || {};
+import '../elements/diff/gr-diff/gr-diff.js';
diff --git a/polygerrit-ui/app/polylint_test.sh b/polygerrit-ui/app/polylint_test.sh
index 79c5972..bc06c1c 100755
--- a/polygerrit-ui/app/polylint_test.sh
+++ b/polygerrit-ui/app/polylint_test.sh
@@ -5,7 +5,7 @@
DIR=$(pwd)
ln -s $RUNFILES_DIR/ui_npm/node_modules $TEST_TMPDIR/node_modules
cp $2 $TEST_TMPDIR/polymer.json
-cp -R -L polygerrit-ui/app/polylint-updated-links/polygerrit-ui/app/* $TEST_TMPDIR
+cp -R -L polygerrit-ui/app/* $TEST_TMPDIR
#Can't use --root with polymer.json - see https://github.com/Polymer/tools/issues/2616
#Change current directory to the root folder
diff --git a/polygerrit-ui/app/redirects.json b/polygerrit-ui/app/redirects.json
deleted file mode 100644
index a51d361..0000000
--- a/polygerrit-ui/app/redirects.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "description": "See tools/node_tools/polygerrit_app_preprocessor/redirects.ts",
- "redirects": [
- {
- "from": "/bower_components/ba-linkify",
- "to": {
- "npm_module": "ba-linkify"
- }
- },
- {
- "from": "/bower_components/es6-promise",
- "to": {
- "npm_module": "es6-promise"
- }
- },
- {
- "from": "/bower_components/fetch",
- "to": {
- "npm_module": "whatwg-fetch",
- "files": {
- "fetch.js": "dist/fetch.umd.js"
- }
- }
- },
- {
- "from": "/bower_components/moment",
- "to": {
- "npm_module": "moment"
- }
- },
- {
- "from": "/bower_components/webcomponentsjs",
- "to": {
- "npm_module": "@webcomponents/webcomponentsjs"
- }
- },
- {
- "from": "/bower_components/page",
- "to": {
- "npm_module": "page"
- }
- },
- {
- "from": "/bower_components",
- "to": {
- "npm_module": "polymer-bridges"
- }
- }
- ]
-}
diff --git a/polygerrit-ui/app/rollup.config.js b/polygerrit-ui/app/rollup.config.js
index eb489ce..d83f24f 100644
--- a/polygerrit-ui/app/rollup.config.js
+++ b/polygerrit-ui/app/rollup.config.js
@@ -81,7 +81,7 @@
context: 'window',
plugins: [resolve({
customResolveOptions: {
- moduleDirectory: 'node_modules'
+ moduleDirectory: 'external/ui_npm/node_modules'
}
}), importLocalFontMetaUrlResolver()],
};
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index 12b6911..6e1f63a 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -1,9 +1,7 @@
load("//tools/bzl:genrule2.bzl", "genrule2")
-load("//tools/node_tools/polygerrit_app_preprocessor:index.bzl", "prepare_for_bundling", "update_links")
-load("//tools/node_tools/legacy:index.bzl", "polymer_bundler_tool")
load("@npm_bazel_rollup//:index.bzl", "rollup_bundle")
-def polygerrit_bundle(name, srcs, outs, entry_point, redirects):
+def polygerrit_bundle(name, srcs, outs, entry_point):
"""Build .zip bundle from source code
Args:
@@ -11,89 +9,22 @@
srcs: source files
outs: array with a single item - the output file name
entry_point: application entry-point
- redirects: .json file with redirects
"""
app_name = entry_point.split(".html")[0].split("/").pop() # eg: gr-app
- # Update links in all .html files according to rules in redirects.json file. All other files
- # remain unchanged. After the update, all references to bower_components have been replaced with
- # correct references to node_modules.
- # The output of this rule is a directory, which mirrors the directory layout of srcs files.
- update_links(
- name = app_name + "-updated-links",
- srcs = srcs,
- redirects = redirects,
- )
- # Note: prepare_for_bundling and polymer_bundler_tool will be removed after switch to
- # ES6 modules.
- # Polymer 3 uses ES modules; gerrit still use HTML imports and polymer-bridges. In such
- # conditions, polymer-bundler/crisper and polymer-cli tools crash without an error or complains
- # about non-existing syntax error in .js code. But even if they works with some config, the
- # output result is not correct. At the same time, polymer-bundler/crisper work well if input
- # files are HTML and js without javascript modules.
- #
- # Polygerrit's code follows simple rules, so it is quite easy to preprocess code in a way, that
- # it can be consumed by polymer-bundler/crisper tool. Rules do the following:
- # 1) prepare_for_bundling - update srcs by moving all scripts out of HTML files.
- # For each HTML file it creates file.html_gen.js file in the same directory and put all
- # scripts there in the same order, as script tags appear in HTML file.
- # - Inline javascript is copied as is;
- # - <script src = "path/to/file.js" > adds to .js file as
- # import 'path/to/file.js'
- # statement. Such import statement run all side-effects in file.js (i.e. it run all global
- # code).
- # - <link rel="import" href = "path/to/file.html"> adds to .js file as
- # import 'path/to/file.html.js' - i.e. instead of html, the .js script imports another
- # generated js file ('path/to/file.html_gen.js').
- # Because output JS keeps the order of imports, all global variables are initialized in a
- # correct order (this is important for gerrit; it is impossible to use AMD modules here).
- # Then, all scripts are removed from HTML file.
-
- # Output of this rule - directory with updated HTML and JS files; all other files are copied
- # to the output directory without changes.
- # 2) rollup_bundle - combines all .js files from the previous step into one bundle.
- # 3) polymer_bundler_tool -
- # a) run polymer-bundle tool on HTML files (i.e. on output from the first step). Because
- # these files don't contain scripts anymore, it just combine all HTML/CSS files in one file
- # (by following HTML imports).
- # b) run crisper to add script tag at the end of generated HTML
- #
- # Output of the rule is 2 file: HTML bundle and JS bundle and HTML file loads JS file with
- # <script src="..."> tag.
-
- prepare_for_bundling(
- name = app_name + "-prebundling-srcs",
- srcs = [
- app_name + "-updated-links",
- ],
- additional_node_modules_to_preprocess = [
- "@ui_npm//polymer-bridges",
- ],
- entry_point = entry_point,
- node_modules = [
+ native.filegroup(
+ name = app_name + "-full-src",
+ srcs = srcs + [
"@ui_npm//:node_modules",
],
- root_path = "polygerrit-ui/app/" + app_name + "-updated-links/polygerrit-ui/app",
- )
-
- native.filegroup(
- name = app_name + "-prebundling-srcs-js",
- srcs = [app_name + "-prebundling-srcs"],
- output_group = "js",
- )
-
- native.filegroup(
- name = app_name + "-prebundling-srcs-html",
- srcs = [app_name + "-prebundling-srcs"],
- output_group = "html",
)
rollup_bundle(
name = app_name + "-bundle-js",
- srcs = [app_name + "-prebundling-srcs-js"],
+ srcs = [app_name + "-full-src"],
config_file = ":rollup.config.js",
- entry_point = app_name + "-prebundling-srcs/entry.js",
+ entry_point = "elements/" + app_name + ".js",
rollup_bin = "//tools/node_tools:rollup-bin",
sourcemap = "hidden",
deps = [
@@ -101,18 +32,11 @@
],
)
- polymer_bundler_tool(
- name = app_name + "-bundle-html",
- srcs = [app_name + "-prebundling-srcs-html"],
- entry_point = app_name + "-prebundling-srcs/entry.html",
- script_src_value = app_name + ".js",
- )
-
native.filegroup(
name = name + "_app_sources",
srcs = [
app_name + "-bundle-js.js",
- app_name + "-bundle-html.html",
+ entry_point,
],
)
diff --git a/polygerrit-ui/app/samples/bind-parameters.html b/polygerrit-ui/app/samples/bind-parameters.html
deleted file mode 100644
index e6bf9d1..0000000
--- a/polygerrit-ui/app/samples/bind-parameters.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<dom-module id="bind-parameters">
- <script>
- Gerrit.install(plugin => {
- plugin.registerCustomComponent(
- 'change-view-integration', 'my-bind-sample');
- });
- </script>
-</dom-module>
-
-<dom-module id="my-bind-sample">
- <template>
- Template example: Patchset number [[revision._number]]. <br/>
- Computed example: [[computedExample]].
- </template>
- <script>
- Polymer({
- is: 'my-bind-sample',
-
- properties: {
- computedExample: {
- type: String,
- computed: '_computeExample(revision._number)',
- },
- },
- /** @override */
- attached() {
- this.plugin.attributeHelper(this).bind(
- 'revision', this._onRevisionChanged.bind(this));
- },
- _computeExample(value) {
- if (!value) { return '(empty)'; }
- return `(patchset ${value} selected)`;
- },
- _onRevisionChanged(value) {
- console.log(`(attributeHelper.bind) revision number: ${value._number}`);
- },
- });
- </script>
-</dom-module>
diff --git a/polygerrit-ui/app/samples/bind-parameters.js b/polygerrit-ui/app/samples/bind-parameters.js
new file mode 100644
index 0000000..8f08e27
--- /dev/null
+++ b/polygerrit-ui/app/samples/bind-parameters.js
@@ -0,0 +1,65 @@
+/**
+ * @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.
+ */
+const {Element, html} = Polymer;
+
+class MyBindSample extends Element {
+ static get is() { return 'my-bind-sample'; }
+
+ static get properties() {
+ return {
+ computedExample: {
+ type: String,
+ computed: '_computeExample(revision._number)',
+ },
+ revision: {
+ type: Object,
+ observer: '_onRevisionChanged',
+ },
+ };
+ }
+
+ static get template() {
+ return html`
+ Template example: Patchset number [[revision._number]]. <br/>
+ Computed example: [[computedExample]].
+ `;
+ }
+
+ _computeExample(value) {
+ if (!value) { return '(empty)'; }
+ return `(patchset ${value} selected)`;
+ }
+
+ _onRevisionChanged(value) {
+ console.log(`(attributeHelper.bind) revision number: ${value._number}`);
+ }
+}
+
+// register the custom component
+customElements.define(MyBindSample.is, MyBindSample);
+
+/**
+ * This plugin will add a new section
+ * between the file list and change log with the
+ * `my-bind-sample` component.
+ */
+Gerrit.install(plugin => {
+ // You should see the above text with the right revision number shown
+ // between the file list and the change log
+ plugin.registerCustomComponent(
+ 'change-view-integration', 'my-bind-sample');
+});
\ No newline at end of file
diff --git a/polygerrit-ui/app/samples/coverage-plugin.html b/polygerrit-ui/app/samples/coverage-plugin.html
deleted file mode 100644
index fa44a47..0000000
--- a/polygerrit-ui/app/samples/coverage-plugin.html
+++ /dev/null
@@ -1,80 +0,0 @@
-<dom-module id="coverage-plugin">
- <script>
- function populateWithDummyData(coverageData) {
- coverageData['NewFile'] = {
- linesMissingCoverage: [1, 2, 3],
- totalLines: 5,
- changeNum: 94,
- patchNum: 2,
- };
- coverageData['/COMMIT_MSG'] = {
- linesMissingCoverage: [3, 4, 7, 14],
- totalLines: 14,
- changeNum: 94,
- patchNum: 2,
- };
- coverageData['DEPS'] = {
- linesMissingCoverage: [3, 4, 7, 14],
- totalLines: 16,
- changeNum: 77001,
- patchNum: 1,
- };
- coverageData['go/sklog/sklog.go'] = {
- linesMissingCoverage: [3, 322, 323, 324],
- totalLines: 350,
- changeNum: 85963,
- patchNum: 13,
- };
- }
-
- Gerrit.install(plugin => {
- const coverageData = {};
- let displayCoverage = false;
- const annotationApi = plugin.annotationApi();
- const styleApi = plugin.styles();
-
- const coverageStyle = styleApi.css('background-color: #EF9B9B !important');
- const emptyStyle = styleApi.css('');
-
- annotationApi.addLayer(context => {
- if (Object.keys(coverageData).length === 0) {
- // Coverage data is not ready yet.
- return;
- }
- const path = context.path;
- const line = context.line;
- // Highlight lines missing coverage with this background color if
- // coverage should be displayed, else do nothing.
- const annotationStyle = displayCoverage
- ? coverageStyle
- : emptyStyle;
- if (coverageData[path] &&
- coverageData[path].changeNum === context.changeNum &&
- coverageData[path].patchNum === context.patchNum) {
- const linesMissingCoverage = coverageData[path].linesMissingCoverage;
- if (linesMissingCoverage.includes(line.afterNumber)) {
- context.annotateRange(0, line.text.length, annotationStyle, 'right');
- context.annotateLineNumber(annotationStyle, 'right');
- }
- }
- }).enableToggleCheckbox('Display Coverage', checkbox => {
- // Checkbox is attached so now add the notifier that will be controlled
- // by the checkbox.
- // Checkbox will only be added to the file diff page, in the top right
- // section near the "Diff view".
- annotationApi.addNotifier(notifyFunc => {
- new Promise(resolve => setTimeout(resolve, 3000)).then(() => {
- populateWithDummyData(coverageData);
- checkbox.disabled = false;
- checkbox.onclick = e => {
- displayCoverage = e.target.checked;
- Object.keys(coverageData).forEach(file => {
- notifyFunc(file, 0, coverageData[file].totalLines, 'right');
- });
- };
- });
- });
- });
- });
- </script>
-</dom-module>
diff --git a/polygerrit-ui/app/samples/coverage-plugin.js b/polygerrit-ui/app/samples/coverage-plugin.js
new file mode 100644
index 0000000..9b2b687
--- /dev/null
+++ b/polygerrit-ui/app/samples/coverage-plugin.js
@@ -0,0 +1,82 @@
+/**
+ * @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.
+ */
+function populateWithDummyData(coverageData) {
+ coverageData['/COMMIT_MSG'] = {
+ linesMissingCoverage: [3, 4, 7, 14],
+ totalLines: 14,
+ changeNum: 94,
+ patchNum: 2,
+ };
+
+ // more coverage info on other files
+}
+
+/**
+ * This plugin will add a toggler on file diff page to
+ * display fake coverage data.
+ *
+ * As the fake coverage data only provided for COMMIT_MSG file,
+ * so it will only work for COMMIT_MSG file diff.
+ */
+Gerrit.install(plugin => {
+ const coverageData = {};
+ let displayCoverage = false;
+ const annotationApi = plugin.annotationApi();
+ const styleApi = plugin.styles();
+
+ const coverageStyle = styleApi.css('background-color: #EF9B9B !important');
+ const emptyStyle = styleApi.css('');
+
+ annotationApi.addLayer(context => {
+ if (Object.keys(coverageData).length === 0) {
+ // Coverage data is not ready yet.
+ return;
+ }
+ const path = context.path;
+ const line = context.line;
+ // Highlight lines missing coverage with this background color if
+ // coverage should be displayed, else do nothing.
+ const annotationStyle = displayCoverage
+ ? coverageStyle
+ : emptyStyle;
+
+ // ideally should check to make sure its the same patch for same change
+ // for demo purpose, this is only checking to make sure we have fake data
+ if (coverageData[path]) {
+ const linesMissingCoverage = coverageData[path].linesMissingCoverage;
+ if (linesMissingCoverage.includes(line.afterNumber)) {
+ context.annotateRange(0, line.text.length, annotationStyle, 'right');
+ context.annotateLineNumber(annotationStyle, 'right');
+ }
+ }
+ }).enableToggleCheckbox('Display Coverage', checkbox => {
+ // Checkbox is attached so now add the notifier that will be controlled
+ // by the checkbox.
+ // Checkbox will only be added to the file diff page, in the top right
+ // section near the "Diff view".
+ annotationApi.addNotifier(notifyFunc => {
+ populateWithDummyData(coverageData);
+ checkbox.disabled = false;
+ checkbox.onclick = e => {
+ displayCoverage = e.target.checked;
+ Object.keys(coverageData).forEach(file => {
+ notifyFunc(file, 0, coverageData[file].totalLines, 'right');
+ });
+ };
+ });
+ });
+});
\ No newline at end of file
diff --git a/polygerrit-ui/app/samples/lgtm-plugin.html b/polygerrit-ui/app/samples/lgtm-plugin.html
deleted file mode 100644
index d58034d..0000000
--- a/polygerrit-ui/app/samples/lgtm-plugin.html
+++ /dev/null
@@ -1,16 +0,0 @@
-<dom-module id="lgtm-plugin">
- <script>
- Gerrit.install(plugin => {
- const replyApi = plugin.changeReply();
- replyApi.addReplyTextChangedCallback(text => {
- const label = 'Code-Review';
- const labelValue = replyApi.getLabelValue(label);
- if (labelValue &&
- labelValue === ' 0' &&
- text.indexOf('LGTM') === 0) {
- replyApi.setLabelValue(label, '+1');
- }
- });
- });
- </script>
-</dom-module>
diff --git a/polygerrit-ui/app/samples/lgtm-plugin.js b/polygerrit-ui/app/samples/lgtm-plugin.js
new file mode 100644
index 0000000..9de1496
--- /dev/null
+++ b/polygerrit-ui/app/samples/lgtm-plugin.js
@@ -0,0 +1,32 @@
+/**
+ * @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.
+ */
+/**
+ * This plugin will +1 on Code-Review label if detect that you have
+ * LGTM as start of your reply.
+ */
+Gerrit.install(plugin => {
+ const replyApi = plugin.changeReply();
+ replyApi.addReplyTextChangedCallback(text => {
+ const label = 'Code-Review';
+ const labelValue = replyApi.getLabelValue(label);
+ if (labelValue &&
+ labelValue === ' 0' &&
+ text.indexOf('LGTM') === 0) {
+ replyApi.setLabelValue(label, '+1');
+ }
+ });
+});
\ No newline at end of file
diff --git a/polygerrit-ui/app/samples/repo-command.html b/polygerrit-ui/app/samples/repo-command.html
deleted file mode 100644
index 5b3ee2c..0000000
--- a/polygerrit-ui/app/samples/repo-command.html
+++ /dev/null
@@ -1,45 +0,0 @@
-<dom-module id="sample-repo-command">
- <script>
- Gerrit.install(plugin => {
- // High-level API
- plugin.project()
- .createCommand('Bork', (repoName, projectConfig) => {
- if (repoName !== 'All-Projects') {
- return false;
- }
- })
- .onTap(() => {
- alert('Bork, bork!');
- });
-
- // Low-level API
- plugin.registerCustomComponent(
- 'repo-command', 'repo-command-low');
- });
- </script>
-</dom-module>
-
-<!-- Low-level custom component for repo command. -->
-<dom-module id="repo-command-low">
- <template>
- <gr-repo-command
- title="Low-level bork"
- on-command-tap="_handleCommandTap">
- </gr-repo-command>
- </template>
- <script>
- Polymer({
- is: 'repo-command-low',
-
- /** @override */
- attached() {
- console.log(this.repoName);
- console.log(this.config);
- this.hidden = this.repoName !== 'All-Projects';
- },
- _handleCommandTap() {
- alert('(softly) bork, bork.');
- },
- });
- </script>
-</dom-module>
diff --git a/polygerrit-ui/app/samples/repo-command.js b/polygerrit-ui/app/samples/repo-command.js
new file mode 100644
index 0000000..00f95f5
--- /dev/null
+++ b/polygerrit-ui/app/samples/repo-command.js
@@ -0,0 +1,73 @@
+/**
+ * @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.
+ */
+const {Element, html} = Polymer;
+
+class RepoCommandLow extends Element {
+ static get is() { return 'repo-command-low'; }
+
+ static get properties() {
+ return {
+ rootUrl: String,
+ };
+ }
+
+ static get template() {
+ return html`
+ <gr-repo-command
+ title="Low-level bork"
+ on-command-tap="_handleCommandTap">
+ </gr-repo-command>
+ `;
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ console.log(this.repoName);
+ console.log(this.config);
+ this.hidden = this.repoName !== 'All-Projects';
+ }
+
+ _handleCommandTap() {
+ alert('(softly) bork, bork.');
+ }
+}
+
+// register the custom component
+customElements.define(RepoCommandLow.is, RepoCommandLow);
+
+/**
+ * This plugin will add two new commands in command page for
+ * All-Projects.
+ *
+ * The added commands will simply alert you when click.
+ */
+Gerrit.install(plugin => {
+ // High-level API
+ plugin.project()
+ .createCommand('Bork', (repoName, projectConfig) => {
+ if (repoName !== 'All-Projects') {
+ return false;
+ }
+ })
+ .onTap(() => {
+ alert('Bork, bork!');
+ });
+
+ // Low-level API
+ plugin.registerCustomComponent(
+ 'repo-command', 'repo-command-low');
+});
\ No newline at end of file
diff --git a/polygerrit-ui/app/samples/some-screen.html b/polygerrit-ui/app/samples/some-screen.html
deleted file mode 100644
index 593b8ab..0000000
--- a/polygerrit-ui/app/samples/some-screen.html
+++ /dev/null
@@ -1,51 +0,0 @@
-<dom-module id="some-screen">
- <script>
- Gerrit.install(plugin => {
- // Recommended approach for screen() API.
- plugin.screen('main', 'some-screen-main');
-
- const mainUrl = plugin.screenUrl('main');
-
- // Support for deprecated screen API.
- plugin.deprecated.screen('foo', ({token, body, show}) => {
- body.innerHTML = `This is a plugin screen at ${token}<br/>` +
- `<a href="${mainUrl}">Go to main plugin screen</a>`;
- show();
- });
-
- // Quick and dirty way to get something on screen.
- plugin.screen('bar').onAttached(el => {
- el.innerHTML = `This is a plugin screen at ${el.token}<br/>` +
- `<a href="${mainUrl}">Go to main plugin screen</a>`;
- });
-
- // Add a "Plugin screen" link to the change view screen.
- plugin.hook('change-metadata-item').onAttached(el => {
- el.innerHTML = `<a href="${mainUrl}">Plugin screen</a>`;
- });
- });
- </script>
-</dom-module>
-
-<dom-module id="some-screen-main">
- <template>
- This is the <b>main</b> plugin screen at [[token]]
- <ul>
- <li><a href$="[[rootUrl]]/foo">via deprecated</a></li>
- <li><a href$="[[rootUrl]]/bar">without component</a></li>
- </ul>
- </template>
- <script>
- Polymer({
- is: 'some-screen-main',
-
- properties: {
- rootUrl: String,
- },
- /** @override */
- attached() {
- this.rootUrl = `${this.plugin.screenUrl()}`;
- },
- });
- </script>
-</dom-module>
diff --git a/polygerrit-ui/app/samples/some-screen.js b/polygerrit-ui/app/samples/some-screen.js
new file mode 100644
index 0000000..09acc81
--- /dev/null
+++ b/polygerrit-ui/app/samples/some-screen.js
@@ -0,0 +1,67 @@
+/**
+ * @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.
+ */
+const {Element, html} = Polymer;
+
+class SomeScreenMain extends Element {
+ static get is() { return 'some-screen-main'; }
+
+ static get properties() {
+ return {
+ rootUrl: String,
+ };
+ }
+
+ static get template() {
+ return html`
+ This is the <b>main</b> plugin screen at [[token]]
+ <ul>
+ <li><a href$="[[rootUrl]]/bar">without component</a></li>
+ </ul>
+ `;
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.rootUrl = `${this.plugin.screenUrl()}`;
+ }
+}
+
+// register the custom component
+customElements.define(SomeScreenMain.is, SomeScreenMain);
+
+/**
+ * This plugin will add several things to gerrit:
+ * 1. two screens added by this plugin in two different ways
+ * 2. a link in change page under meta info to the added main screen
+ */
+Gerrit.install(plugin => {
+ // Recommended approach for screen() API.
+ plugin.screen('main', 'some-screen-main');
+
+ const mainUrl = plugin.screenUrl('main');
+
+ // Quick and dirty way to get something on screen.
+ plugin.screen('bar').onAttached(el => {
+ el.innerHTML = `This is a plugin screen at ${el.token}<br/>` +
+ `<a href="${mainUrl}">Go to main plugin screen</a>`;
+ });
+
+ // Add a "Plugin screen" link to the change view screen.
+ plugin.hook('change-metadata-item').onAttached(el => {
+ el.innerHTML = `<a href="${mainUrl}">Plugin screen</a>`;
+ });
+});
\ No newline at end of file
diff --git a/polygerrit-ui/app/samples/suggest-vote.html b/polygerrit-ui/app/samples/suggest-vote.html
deleted file mode 100644
index 657fa73..0000000
--- a/polygerrit-ui/app/samples/suggest-vote.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<dom-module id="suggested-vote">
- <script>
- Gerrit.install(plugin => {
- const replyApi = plugin.changeReply();
- let wasSuggested = false;
- plugin.on('showchange', () => {
- wasSuggested = false;
- });
- const CODE_REVIEW = 'Code-Review';
- replyApi.addLabelValuesChangedCallback(({name, value}) => {
- if (wasSuggested && name === CODE_REVIEW) {
- replyApi.showMessage('');
- wasSuggested = false;
- } else if (replyApi.getLabelValue(CODE_REVIEW) === '+1' &&
- !wasSuggested) {
- replyApi.setLabelValue(CODE_REVIEW, '+2');
- replyApi.showMessage(`Suggested ${CODE_REVIEW} upgrade: +2`);
- wasSuggested = true;
- }
- });
- });
- </script>
-</dom-module>
diff --git a/polygerrit-ui/app/samples/suggest-vote.js b/polygerrit-ui/app/samples/suggest-vote.js
new file mode 100644
index 0000000..b3c3046
--- /dev/null
+++ b/polygerrit-ui/app/samples/suggest-vote.js
@@ -0,0 +1,39 @@
+/**
+ * @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.
+ */
+/**
+ * This plugin will upgrade your +1 on Code-Review label
+ * to +2 and show a message below the voting labels.
+ */
+Gerrit.install(plugin => {
+ const replyApi = plugin.changeReply();
+ let wasSuggested = false;
+ plugin.on('showchange', () => {
+ wasSuggested = false;
+ });
+ const CODE_REVIEW = 'Code-Review';
+ replyApi.addLabelValuesChangedCallback(({name, value}) => {
+ if (wasSuggested && name === CODE_REVIEW) {
+ replyApi.showMessage('');
+ wasSuggested = false;
+ } else if (replyApi.getLabelValue(CODE_REVIEW) === '+1' &&
+ !wasSuggested) {
+ replyApi.setLabelValue(CODE_REVIEW, '+2');
+ replyApi.showMessage(`Suggested ${CODE_REVIEW} upgrade: +2`);
+ wasSuggested = true;
+ }
+ });
+});
diff --git a/polygerrit-ui/app/samples/theme-plugin.js b/polygerrit-ui/app/samples/theme-plugin.js
new file mode 100644
index 0000000..f3a8931
--- /dev/null
+++ b/polygerrit-ui/app/samples/theme-plugin.js
@@ -0,0 +1,49 @@
+/**
+ * @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.
+ */
+const customTheme = document.createElement('dom-module');
+customTheme.id = 'theme-plugin';
+customTheme.innerHTML = `
+ <template>
+ <style>
+ html {
+ --primary-text-color: red;
+ }
+ </style>
+ </template>
+`;
+
+const darkCustomTheme = document.createElement('dom-module');
+darkCustomTheme.id = 'dark-theme-plugin';
+darkCustomTheme.innerHTML = `
+ <template>
+ <style>
+ html {
+ --background-color-primary: yellow;
+ }
+ </style>
+ </template>
+`;
+
+/**
+ * This plugin will change the primary text color to red.
+ *
+ * Also change the primary background color to yellow for dark theme.
+ */
+Gerrit.install(plugin => {
+ plugin.registerStyleModule('app-theme', 'theme-plugin');
+ plugin.registerStyleModule('app-theme-dark', 'dark-theme-plugin');
+});
\ No newline at end of file
diff --git a/polygerrit-ui/app/scripts/bundled-polymer.js b/polygerrit-ui/app/scripts/bundled-polymer.js
new file mode 100644
index 0000000..711d587
--- /dev/null
+++ b/polygerrit-ui/app/scripts/bundled-polymer.js
@@ -0,0 +1,71 @@
+/**
+ * @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.
+ */
+
+// This file is a replacement for the
+// polymer-bridges/polymer/polymer.html file. The polymer.html file loads
+// other scripts to setup different global variables. Because polygerrit
+// code still uses global variables (like Polymer.importHref and other),
+// we must setup this global variables after conversion to es6 modules.
+//
+// The bundled-polymer.js imports all scripts in the same order as the
+// polymer.html does and must be imported in all es6-modules instead
+// of the polymer.html file.
+
+import 'polymer-bridges/polymer/lib/utils/boot_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/resolve-url_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/settings_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/mixin_bridge.js';
+import 'polymer-bridges/polymer/lib/elements/dom-module_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/style-gather_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/path_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/case-map_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/async_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/wrap_bridge.js';
+import 'polymer-bridges/polymer/lib/mixins/properties-changed_bridge.js';
+import 'polymer-bridges/polymer/lib/mixins/property-accessors_bridge.js';
+import 'polymer-bridges/polymer/lib/mixins/template-stamp_bridge.js';
+import 'polymer-bridges/polymer/lib/mixins/property-effects_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/telemetry_bridge.js';
+import 'polymer-bridges/polymer/lib/mixins/properties-mixin_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/debounce_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/gestures_bridge.js';
+import 'polymer-bridges/polymer/lib/mixins/gesture-event-listeners_bridge.js';
+import 'polymer-bridges/polymer/lib/mixins/dir-mixin_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/render-status_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/unresolved_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/array-splice_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/flattened-nodes-observer_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/flush_bridge.js';
+import 'polymer-bridges/polymer/lib/legacy/polymer.dom_bridge.js';
+import 'polymer-bridges/polymer/lib/legacy/legacy-element-mixin_bridge.js';
+import 'polymer-bridges/polymer/lib/legacy/class_bridge.js';
+import 'polymer-bridges/polymer/lib/legacy/polymer-fn_bridge.js';
+import 'polymer-bridges/polymer/lib/mixins/mutable-data_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/templatize_bridge.js';
+import 'polymer-bridges/polymer/lib/legacy/templatizer-behavior_bridge.js';
+import 'polymer-bridges/polymer/lib/elements/dom-bind_bridge.js';
+import 'polymer-bridges/polymer/lib/utils/html-tag_bridge.js';
+import 'polymer-bridges/polymer/polymer-element_bridge.js';
+import 'polymer-bridges/polymer/lib/elements/dom-repeat_bridge.js';
+import 'polymer-bridges/polymer/lib/elements/dom-if_bridge.js';
+import 'polymer-bridges/polymer/lib/elements/array-selector_bridge.js';
+import 'polymer-bridges/polymer/lib/elements/custom-style_bridge.js';
+import 'polymer-bridges/polymer/lib/legacy/mutable-data-behavior_bridge.js';
+import 'polymer-bridges/polymer/polymer-legacy_bridge.js';
+import {importHref} from './import-href.js';
+
+Polymer.importHref = importHref;
diff --git a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.js b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.js
index f0d0e7f..cefd254 100644
--- a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.js
+++ b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.js
@@ -24,16 +24,12 @@
const ANONYMOUS_NAME = 'Anonymous';
class GrDisplayNameUtils {
- /**
- * enableEmail when true enables to fallback to using email if
- * the account name is not avilable.
- */
- static getUserName(config, account, enableEmail) {
+ static getUserName(config, account) {
if (account && account.name) {
return account.name;
} else if (account && account.username) {
return account.username;
- } else if (enableEmail && account && account.email) {
+ } else if (account && account.email) {
return account.email;
} else if (config && config.user &&
config.user.anonymous_coward_name !== 'Anonymous Coward') {
@@ -43,8 +39,8 @@
return ANONYMOUS_NAME;
}
- static getAccountDisplayName(config, account, enableEmail) {
- const reviewerName = this.getUserName(config, account, !!enableEmail);
+ static getAccountDisplayName(config, account) {
+ const reviewerName = this.getUserName(config, account);
const reviewerEmail = this._accountEmail(account.email);
const reviewerStatus = account.status ? '(' + account.status + ')' : '';
return [reviewerName, reviewerEmail, reviewerStatus]
diff --git a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
index 0ab41de..262d53c 100644
--- a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
+++ b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
@@ -19,123 +19,113 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-display-name-utils</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
-<script src="gr-display-name-utils.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module">
+import '../../test/common-test-setup.js';
+import './gr-display-name-utils.js';
+suite('gr-display-name-utils tests', () => {
+ // eslint-disable-next-line no-unused-vars
+ const config = {
+ user: {
+ anonymous_coward_name: 'Anonymous Coward',
+ },
+ };
-<script>
- suite('gr-display-name-utils tests', async () => {
- await readyToTest();
- // eslint-disable-next-line no-unused-vars
+ test('getUserName name only', () => {
+ const account = {
+ name: 'test-name',
+ };
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, account),
+ 'test-name');
+ });
+
+ test('getUserName username only', () => {
+ const account = {
+ username: 'test-user',
+ };
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, account),
+ 'test-user');
+ });
+
+ test('getUserName email only', () => {
+ const account = {
+ email: 'test-user@test-url.com',
+ };
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, account),
+ 'test-user@test-url.com');
+ });
+
+ test('getUserName returns not Anonymous Coward as the anon name', () => {
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, null),
+ 'Anonymous');
+ });
+
+ test('getUserName for the config returning the anon name', () => {
const config = {
user: {
- anonymous_coward_name: 'Anonymous Coward',
+ anonymous_coward_name: 'Test Anon',
},
};
-
- test('getUserName name only', () => {
- const account = {
- name: 'test-name',
- };
- assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
- 'test-name');
- });
-
- test('getUserName username only', () => {
- const account = {
- username: 'test-user',
- };
- assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
- 'test-user');
- });
-
- test('getUserName email only', () => {
- const account = {
- email: 'test-user@test-url.com',
- };
- assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
- 'test-user@test-url.com');
- });
-
- test('getUserName returns not Anonymous Coward as the anon name', () => {
- assert.deepEqual(GrDisplayNameUtils.getUserName(config, null, true),
- 'Anonymous');
- });
-
- test('getUserName for the config returning the anon name', () => {
- const config = {
- user: {
- anonymous_coward_name: 'Test Anon',
- },
- };
- assert.deepEqual(GrDisplayNameUtils.getUserName(config, null, true),
- 'Test Anon');
- });
-
- test('getAccountDisplayName - account with name only', () => {
- assert.equal(
- GrDisplayNameUtils.getAccountDisplayName(config,
- {name: 'Some user name'}),
- 'Some user name');
- });
-
- test('getAccountDisplayName - account with email only', () => {
- assert.equal(
- GrDisplayNameUtils.getAccountDisplayName(config,
- {email: 'my@example.com'}),
- 'Anonymous <my@example.com>');
- });
-
- test('getAccountDisplayName - account with email only - allowEmail', () => {
- assert.equal(
- GrDisplayNameUtils.getAccountDisplayName(config,
- {email: 'my@example.com'}, true),
- 'my@example.com <my@example.com>');
- });
-
- test('getAccountDisplayName - account with name and status', () => {
- assert.equal(
- GrDisplayNameUtils.getAccountDisplayName(config, {
- name: 'Some name',
- status: 'OOO',
- }),
- 'Some name (OOO)');
- });
-
- test('getAccountDisplayName - account with name and email', () => {
- assert.equal(
- GrDisplayNameUtils.getAccountDisplayName(config, {
- name: 'Some name',
- email: 'my@example.com',
- }),
- 'Some name <my@example.com>');
- });
-
- test('getAccountDisplayName - account with name, email and status', () => {
- assert.equal(
- GrDisplayNameUtils.getAccountDisplayName(config, {
- name: 'Some name',
- email: 'my@example.com',
- status: 'OOO',
- }),
- 'Some name <my@example.com> (OOO)');
- });
-
- test('getGroupDisplayName', () => {
- assert.equal(
- GrDisplayNameUtils.getGroupDisplayName({name: 'Some user name'}),
- 'Some user name (group)');
- });
-
- test('_accountEmail', () => {
- assert.equal(
- GrDisplayNameUtils._accountEmail('email@gerritreview.com'),
- '<email@gerritreview.com>');
- assert.equal(GrDisplayNameUtils._accountEmail(undefined), '');
- });
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, null),
+ 'Test Anon');
});
+
+ test('getAccountDisplayName - account with name only', () => {
+ assert.equal(
+ GrDisplayNameUtils.getAccountDisplayName(config,
+ {name: 'Some user name'}),
+ 'Some user name');
+ });
+
+ test('getAccountDisplayName - account with email only', () => {
+ assert.equal(
+ GrDisplayNameUtils.getAccountDisplayName(config,
+ {email: 'my@example.com'}),
+ 'my@example.com <my@example.com>');
+ });
+
+ test('getAccountDisplayName - account with name and status', () => {
+ assert.equal(
+ GrDisplayNameUtils.getAccountDisplayName(config, {
+ name: 'Some name',
+ status: 'OOO',
+ }),
+ 'Some name (OOO)');
+ });
+
+ test('getAccountDisplayName - account with name and email', () => {
+ assert.equal(
+ GrDisplayNameUtils.getAccountDisplayName(config, {
+ name: 'Some name',
+ email: 'my@example.com',
+ }),
+ 'Some name <my@example.com>');
+ });
+
+ test('getAccountDisplayName - account with name, email and status', () => {
+ assert.equal(
+ GrDisplayNameUtils.getAccountDisplayName(config, {
+ name: 'Some name',
+ email: 'my@example.com',
+ status: 'OOO',
+ }),
+ 'Some name <my@example.com> (OOO)');
+ });
+
+ test('getGroupDisplayName', () => {
+ assert.equal(
+ GrDisplayNameUtils.getGroupDisplayName({name: 'Some user name'}),
+ 'Some user name (group)');
+ });
+
+ test('_accountEmail', () => {
+ assert.equal(
+ GrDisplayNameUtils._accountEmail('email@gerritreview.com'),
+ '<email@gerritreview.com>');
+ assert.equal(GrDisplayNameUtils._accountEmail(undefined), '');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js
index 67001d2..a1fd94a 100644
--- a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js
+++ b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js
@@ -36,7 +36,7 @@
makeSuggestionItem(account) {
return {
- name: GrDisplayNameUtils.getAccountDisplayName(null, account, true),
+ name: GrDisplayNameUtils.getAccountDisplayName(null, account),
value: {account, count: 1},
};
}
diff --git a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html
index f64d9ef..2f2cf0b 100644
--- a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html
+++ b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html
@@ -19,18 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-email-suggestions-provider</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
-<link rel="import" href="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
-<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
-<script src="gr-email-suggestions-provider.js"></script>
-
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -38,64 +30,67 @@
</template>
</test-fixture>
-<script>
- suite('GrEmailSuggestionsProvider tests', async () => {
- await readyToTest();
- let sandbox;
- let restAPI;
- let provider;
- const account1 = {
- name: 'Some name',
- email: 'some@example.com',
- };
- const account2 = {
- email: 'other@example.com',
- _account_id: 3,
- };
+<script type="module">
+import '../../test/common-test-setup.js';
+import '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-display-name-utils/gr-display-name-utils.js';
+import './gr-email-suggestions-provider.js';
+suite('GrEmailSuggestionsProvider tests', () => {
+ let sandbox;
+ let restAPI;
+ let provider;
+ const account1 = {
+ name: 'Some name',
+ email: 'some@example.com',
+ };
+ const account2 = {
+ email: 'other@example.com',
+ _account_id: 3,
+ };
- setup(() => {
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- });
- restAPI = fixture('basic');
- provider = new GrEmailSuggestionsProvider(restAPI);
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
});
+ restAPI = fixture('basic');
+ provider = new GrEmailSuggestionsProvider(restAPI);
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('getSuggestions', done => {
- const getSuggestedAccountsStub =
- sandbox.stub(restAPI, 'getSuggestedAccounts')
- .returns(Promise.resolve([account1, account2]));
+ test('getSuggestions', done => {
+ const getSuggestedAccountsStub =
+ sandbox.stub(restAPI, 'getSuggestedAccounts')
+ .returns(Promise.resolve([account1, account2]));
- provider.getSuggestions('Some input').then(res => {
- assert.deepEqual(res, [account1, account2]);
- assert.isTrue(getSuggestedAccountsStub.calledOnce);
- assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
- done();
- });
- });
-
- test('makeSuggestionItem', () => {
- assert.deepEqual(provider.makeSuggestionItem(account1), {
- name: 'Some name <some@example.com>',
- value: {
- account: account1,
- count: 1,
- },
- });
-
- assert.deepEqual(provider.makeSuggestionItem(account2), {
- name: 'other@example.com <other@example.com>',
- value: {
- account: account2,
- count: 1,
- },
- });
+ provider.getSuggestions('Some input').then(res => {
+ assert.deepEqual(res, [account1, account2]);
+ assert.isTrue(getSuggestedAccountsStub.calledOnce);
+ assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
+ done();
});
});
+
+ test('makeSuggestionItem', () => {
+ assert.deepEqual(provider.makeSuggestionItem(account1), {
+ name: 'Some name <some@example.com>',
+ value: {
+ account: account1,
+ count: 1,
+ },
+ });
+
+ assert.deepEqual(provider.makeSuggestionItem(account2), {
+ name: 'other@example.com <other@example.com>',
+ value: {
+ account: account2,
+ count: 1,
+ },
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html
index c46b443..dc772db 100644
--- a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html
+++ b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html
@@ -19,17 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-group-suggestions-provider</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
-<link rel="import" href="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
-<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
-<script src="gr-group-suggestions-provider.js"></script>
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -37,72 +30,75 @@
</template>
</test-fixture>
-<script>
- suite('GrGroupSuggestionsProvider tests', async () => {
- await readyToTest();
- let sandbox;
- let restAPI;
- let provider;
- const group1 = {
- name: 'Some name',
- id: 1,
- };
- const group2 = {
- name: 'Other name',
- id: 3,
- url: 'abcd',
- };
+<script type="module">
+import '../../test/common-test-setup.js';
+import '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-display-name-utils/gr-display-name-utils.js';
+import './gr-group-suggestions-provider.js';
+suite('GrGroupSuggestionsProvider tests', () => {
+ let sandbox;
+ let restAPI;
+ let provider;
+ const group1 = {
+ name: 'Some name',
+ id: 1,
+ };
+ const group2 = {
+ name: 'Other name',
+ id: 3,
+ url: 'abcd',
+ };
- setup(() => {
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- });
- restAPI = fixture('basic');
- provider = new GrGroupSuggestionsProvider(restAPI);
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
});
+ restAPI = fixture('basic');
+ provider = new GrGroupSuggestionsProvider(restAPI);
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('getSuggestions', done => {
- const getSuggestedAccountsStub =
- sandbox.stub(restAPI, 'getSuggestedGroups')
- .returns(Promise.resolve({
- 'Some name': {id: 1},
- 'Other name': {id: 3, url: 'abcd'},
- }));
+ test('getSuggestions', done => {
+ const getSuggestedAccountsStub =
+ sandbox.stub(restAPI, 'getSuggestedGroups')
+ .returns(Promise.resolve({
+ 'Some name': {id: 1},
+ 'Other name': {id: 3, url: 'abcd'},
+ }));
- provider.getSuggestions('Some input').then(res => {
- assert.deepEqual(res, [group1, group2]);
- assert.isTrue(getSuggestedAccountsStub.calledOnce);
- assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
- done();
- });
- });
-
- test('makeSuggestionItem', () => {
- assert.deepEqual(provider.makeSuggestionItem(group1), {
- name: 'Some name',
- value: {
- group: {
- name: 'Some name',
- id: 1,
- },
- },
- });
-
- assert.deepEqual(provider.makeSuggestionItem(group2), {
- name: 'Other name',
- value: {
- group: {
- name: 'Other name',
- id: 3,
- },
- },
- });
+ provider.getSuggestions('Some input').then(res => {
+ assert.deepEqual(res, [group1, group2]);
+ assert.isTrue(getSuggestedAccountsStub.calledOnce);
+ assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
+ done();
});
});
+
+ test('makeSuggestionItem', () => {
+ assert.deepEqual(provider.makeSuggestionItem(group1), {
+ name: 'Some name',
+ value: {
+ group: {
+ name: 'Some name',
+ id: 1,
+ },
+ },
+ });
+
+ assert.deepEqual(provider.makeSuggestionItem(group2), {
+ name: 'Other name',
+ value: {
+ group: {
+ name: 'Other name',
+ id: 3,
+ },
+ },
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
index fecf75aa..a47eb72 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
@@ -87,7 +87,7 @@
// Reviewer is an account suggestion from getChangeSuggestedReviewers.
return {
name: GrDisplayNameUtils.getAccountDisplayName(this._config,
- suggestion.account, false),
+ suggestion.account),
value: suggestion,
};
}
@@ -104,7 +104,7 @@
// Reviewer is an account suggestion from getSuggestedAccounts.
return {
name: GrDisplayNameUtils.getAccountDisplayName(this._config,
- suggestion, false),
+ suggestion),
value: {account: suggestion, count: 1},
};
}
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
index 9696c2e..da2eef4 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
@@ -19,17 +19,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reviewer-suggestions-provider</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../test/common-test-setup.html"/>
-<link rel="import" href="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
-<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
-<script src="gr-reviewer-suggestions-provider.js"></script>
-
-<script>void(0);</script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
<test-fixture id="basic">
<template>
@@ -37,227 +30,230 @@
</template>
</test-fixture>
-<script>
- suite('GrReviewerSuggestionsProvider tests', async () => {
- await readyToTest();
- let sandbox;
- let _nextAccountId = 0;
- const makeAccount = function(opt_status) {
- const accountId = ++_nextAccountId;
- return {
- _account_id: accountId,
- name: 'name ' + accountId,
- email: 'email ' + accountId,
- status: opt_status,
- };
+<script type="module">
+import '../../test/common-test-setup.js';
+import '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-display-name-utils/gr-display-name-utils.js';
+import './gr-reviewer-suggestions-provider.js';
+suite('GrReviewerSuggestionsProvider tests', () => {
+ let sandbox;
+ let _nextAccountId = 0;
+ const makeAccount = function(opt_status) {
+ const accountId = ++_nextAccountId;
+ return {
+ _account_id: accountId,
+ name: 'name ' + accountId,
+ email: 'email ' + accountId,
+ status: opt_status,
};
- let _nextAccountId2 = 0;
- const makeAccount2 = function(opt_status) {
- const accountId2 = ++_nextAccountId2;
- return {
- _account_id: accountId2,
- name: 'name ' + accountId2,
- status: opt_status,
- };
+ };
+ let _nextAccountId2 = 0;
+ const makeAccount2 = function(opt_status) {
+ const accountId2 = ++_nextAccountId2;
+ return {
+ _account_id: accountId2,
+ name: 'name ' + accountId2,
+ status: opt_status,
+ };
+ };
+
+ let owner;
+ let existingReviewer1;
+ let existingReviewer2;
+ let suggestion1;
+ let suggestion2;
+ let suggestion3;
+ let restAPI;
+ let provider;
+
+ let redundantSuggestion1;
+ let redundantSuggestion2;
+ let redundantSuggestion3;
+ let change;
+
+ setup(done => {
+ owner = makeAccount();
+ existingReviewer1 = makeAccount();
+ existingReviewer2 = makeAccount();
+ suggestion1 = {account: makeAccount()};
+ suggestion2 = {account: makeAccount()};
+ suggestion3 = {
+ group: {
+ id: 'suggested group id',
+ name: 'suggested group',
+ },
};
- let owner;
- let existingReviewer1;
- let existingReviewer2;
- let suggestion1;
- let suggestion2;
- let suggestion3;
- let restAPI;
- let provider;
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
+ getConfig() { return Promise.resolve({}); },
+ });
- let redundantSuggestion1;
- let redundantSuggestion2;
- let redundantSuggestion3;
- let change;
+ restAPI = fixture('basic');
+ change = {
+ _number: 42,
+ owner,
+ reviewers: {
+ CC: [existingReviewer1],
+ REVIEWER: [existingReviewer2],
+ },
+ };
+ sandbox = sinon.sandbox.create();
+ return flush(done);
+ });
+ teardown(() => {
+ sandbox.restore();
+ });
+ suite('allowAnyUser set to false', () => {
setup(done => {
- owner = makeAccount();
- existingReviewer1 = makeAccount();
- existingReviewer2 = makeAccount();
- suggestion1 = {account: makeAccount()};
- suggestion2 = {account: makeAccount()};
- suggestion3 = {
- group: {
- id: 'suggested group id',
- name: 'suggested group',
- },
- };
-
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- getConfig() { return Promise.resolve({}); },
+ provider = GrReviewerSuggestionsProvider.create(restAPI, change._number,
+ Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER);
+ provider.init().then(done);
+ });
+ suite('stubbed values for _getReviewerSuggestions', () => {
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getChangeSuggestedReviewers() {
+ redundantSuggestion1 = {account: existingReviewer1};
+ redundantSuggestion2 = {account: existingReviewer2};
+ redundantSuggestion3 = {account: owner};
+ return Promise.resolve([redundantSuggestion1, redundantSuggestion2,
+ redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
+ },
+ });
});
- restAPI = fixture('basic');
- change = {
- _number: 42,
- owner,
- reviewers: {
- CC: [existingReviewer1],
- REVIEWER: [existingReviewer2],
- },
- };
- sandbox = sinon.sandbox.create();
- return flush(done);
- });
+ test('makeSuggestionItem formats account or group accordingly', () => {
+ let account = makeAccount();
+ const account3 = makeAccount2();
+ let suggestion = provider.makeSuggestionItem({account});
+ assert.deepEqual(suggestion, {
+ name: account.name + ' <' + account.email + '>',
+ value: {account},
+ });
- teardown(() => {
- sandbox.restore();
- });
- suite('allowAnyUser set to false', () => {
- setup(done => {
- provider = GrReviewerSuggestionsProvider.create(restAPI, change._number,
- Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER);
- provider.init().then(done);
+ const group = {name: 'test'};
+ suggestion = provider.makeSuggestionItem({group});
+ assert.deepEqual(suggestion, {
+ name: group.name + ' (group)',
+ value: {group},
+ });
+
+ suggestion = provider.makeSuggestionItem(account);
+ assert.deepEqual(suggestion, {
+ name: account.name + ' <' + account.email + '>',
+ value: {account, count: 1},
+ });
+
+ suggestion = provider.makeSuggestionItem({account: {}});
+ assert.deepEqual(suggestion, {
+ name: 'Anonymous',
+ value: {account: {}},
+ });
+
+ provider._config = {
+ user: {
+ anonymous_coward_name: 'Anonymous Coward Name',
+ },
+ };
+
+ suggestion = provider.makeSuggestionItem({account: {}});
+ assert.deepEqual(suggestion, {
+ name: 'Anonymous Coward Name',
+ value: {account: {}},
+ });
+
+ account = makeAccount('OOO');
+
+ suggestion = provider.makeSuggestionItem({account});
+ assert.deepEqual(suggestion, {
+ name: account.name + ' <' + account.email + '> (OOO)',
+ value: {account},
+ });
+
+ suggestion = provider.makeSuggestionItem(account);
+ assert.deepEqual(suggestion, {
+ name: account.name + ' <' + account.email + '> (OOO)',
+ value: {account, count: 1},
+ });
+
+ sandbox.stub(GrDisplayNameUtils, '_accountEmail',
+ () => '');
+
+ suggestion = provider.makeSuggestionItem(account3);
+ assert.deepEqual(suggestion, {
+ name: account3.name,
+ value: {account: account3, count: 1},
+ });
});
- suite('stubbed values for _getReviewerSuggestions', () => {
- setup(() => {
- stub('gr-rest-api-interface', {
- getChangeSuggestedReviewers() {
- redundantSuggestion1 = {account: existingReviewer1};
- redundantSuggestion2 = {account: existingReviewer2};
- redundantSuggestion3 = {account: owner};
- return Promise.resolve([redundantSuggestion1, redundantSuggestion2,
- redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
- },
- });
- });
- test('makeSuggestionItem formats account or group accordingly', () => {
- let account = makeAccount();
- const account3 = makeAccount2();
- let suggestion = provider.makeSuggestionItem({account});
- assert.deepEqual(suggestion, {
- name: account.name + ' <' + account.email + '>',
- value: {account},
- });
+ test('getSuggestions', done => {
+ provider.getSuggestions()
+ .then(reviewers => {
+ // Default is no filtering.
+ assert.equal(reviewers.length, 6);
+ assert.deepEqual(reviewers,
+ [redundantSuggestion1, redundantSuggestion2,
+ redundantSuggestion3, suggestion1,
+ suggestion2, suggestion3]);
+ })
+ .then(done);
+ });
- const group = {name: 'test'};
- suggestion = provider.makeSuggestionItem({group});
- assert.deepEqual(suggestion, {
- name: group.name + ' (group)',
- value: {group},
- });
-
- suggestion = provider.makeSuggestionItem(account);
- assert.deepEqual(suggestion, {
- name: account.name + ' <' + account.email + '>',
- value: {account, count: 1},
- });
-
- suggestion = provider.makeSuggestionItem({account: {}});
- assert.deepEqual(suggestion, {
- name: 'Anonymous',
- value: {account: {}},
- });
-
- provider._config = {
- user: {
- anonymous_coward_name: 'Anonymous Coward Name',
- },
- };
-
- suggestion = provider.makeSuggestionItem({account: {}});
- assert.deepEqual(suggestion, {
- name: 'Anonymous Coward Name',
- value: {account: {}},
- });
-
- account = makeAccount('OOO');
-
- suggestion = provider.makeSuggestionItem({account});
- assert.deepEqual(suggestion, {
- name: account.name + ' <' + account.email + '> (OOO)',
- value: {account},
- });
-
- suggestion = provider.makeSuggestionItem(account);
- assert.deepEqual(suggestion, {
- name: account.name + ' <' + account.email + '> (OOO)',
- value: {account, count: 1},
- });
-
- sandbox.stub(GrDisplayNameUtils, '_accountEmail',
- () => '');
-
- suggestion = provider.makeSuggestionItem(account3);
- assert.deepEqual(suggestion, {
- name: account3.name,
- value: {account: account3, count: 1},
- });
- });
-
- test('getSuggestions', done => {
- provider.getSuggestions()
- .then(reviewers => {
- // Default is no filtering.
- assert.equal(reviewers.length, 6);
- assert.deepEqual(reviewers,
- [redundantSuggestion1, redundantSuggestion2,
- redundantSuggestion3, suggestion1,
- suggestion2, suggestion3]);
- })
- .then(done);
- });
-
- test('getSuggestions short circuits when logged out', () => {
- // API call is already stubbed.
- const xhrSpy = restAPI.getChangeSuggestedReviewers;
- provider._loggedIn = false;
+ test('getSuggestions short circuits when logged out', () => {
+ // API call is already stubbed.
+ const xhrSpy = restAPI.getChangeSuggestedReviewers;
+ provider._loggedIn = false;
+ return provider.getSuggestions('').then(() => {
+ assert.isFalse(xhrSpy.called);
+ provider._loggedIn = true;
return provider.getSuggestions('').then(() => {
- assert.isFalse(xhrSpy.called);
- provider._loggedIn = true;
- return provider.getSuggestions('').then(() => {
- assert.isTrue(xhrSpy.called);
- });
+ assert.isTrue(xhrSpy.called);
});
});
});
-
- test('getChangeSuggestedReviewers is used', done => {
- const suggestReviewerStub =
- sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
- .returns(Promise.resolve([]));
- const suggestAccountStub =
- sandbox.stub(restAPI, 'getSuggestedAccounts')
- .returns(Promise.resolve([]));
-
- provider.getSuggestions('').then(() => {
- assert.isTrue(suggestReviewerStub.calledOnce);
- assert.isTrue(suggestReviewerStub.calledWith(42, ''));
- assert.isFalse(suggestAccountStub.called);
- done();
- });
- });
});
- suite('allowAnyUser set to true', () => {
- setup(done => {
- provider = GrReviewerSuggestionsProvider.create(restAPI, change._number,
- Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY);
- provider.init().then(done);
- });
+ test('getChangeSuggestedReviewers is used', done => {
+ const suggestReviewerStub =
+ sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
+ .returns(Promise.resolve([]));
+ const suggestAccountStub =
+ sandbox.stub(restAPI, 'getSuggestedAccounts')
+ .returns(Promise.resolve([]));
- test('getSuggestedAccounts is used', done => {
- const suggestReviewerStub =
- sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
- .returns(Promise.resolve([]));
- const suggestAccountStub =
- sandbox.stub(restAPI, 'getSuggestedAccounts')
- .returns(Promise.resolve([]));
-
- provider.getSuggestions('').then(() => {
- assert.isFalse(suggestReviewerStub.called);
- assert.isTrue(suggestAccountStub.calledOnce);
- assert.isTrue(suggestAccountStub.calledWith('cansee:42 '));
- done();
- });
+ provider.getSuggestions('').then(() => {
+ assert.isTrue(suggestReviewerStub.calledOnce);
+ assert.isTrue(suggestReviewerStub.calledWith(42, ''));
+ assert.isFalse(suggestAccountStub.called);
+ done();
});
});
});
+
+ suite('allowAnyUser set to true', () => {
+ setup(done => {
+ provider = GrReviewerSuggestionsProvider.create(restAPI, change._number,
+ Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY);
+ provider.init().then(done);
+ });
+
+ test('getSuggestedAccounts is used', done => {
+ const suggestReviewerStub =
+ sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
+ .returns(Promise.resolve([]));
+ const suggestAccountStub =
+ sandbox.stub(restAPI, 'getSuggestedAccounts')
+ .returns(Promise.resolve([]));
+
+ provider.getSuggestions('').then(() => {
+ assert.isFalse(suggestReviewerStub.called);
+ assert.isTrue(suggestAccountStub.calledOnce);
+ assert.isTrue(suggestAccountStub.calledWith('cansee:42 '));
+ done();
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/scripts/import-href.js b/polygerrit-ui/app/scripts/import-href.js
new file mode 100644
index 0000000..6ff40a5
--- /dev/null
+++ b/polygerrit-ui/app/scripts/import-href.js
@@ -0,0 +1,108 @@
+/**
+ * @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.
+ */
+
+// This file is a replacement for the
+// polymer-bridges/polymer/lib/utils/import-href.html file. The html
+// file contains code inside <script>...</script> and can't be imported
+// in es6 modules.
+
+// run a callback when HTMLImports are ready or immediately if
+// this api is not available.
+function whenImportsReady(cb) {
+ if (window.HTMLImports) {
+ HTMLImports.whenReady(cb);
+ } else {
+ cb();
+ }
+}
+
+/**
+ * Convenience method for importing an HTML document imperatively.
+ *
+ * This method creates a new `<link rel="import">` element with
+ * the provided URL and appends it to the document to start loading.
+ * In the `onload` callback, the `import` property of the `link`
+ * element will contain the imported document contents.
+ *
+ * @memberof Polymer
+ * @param {string} href URL to document to load.
+ * @param {?function(!Event):void=} onload Callback to notify when an import successfully
+ * loaded.
+ * @param {?function(!ErrorEvent):void=} onerror Callback to notify when an import
+ * unsuccessfully loaded.
+ * @param {boolean=} optAsync True if the import should be loaded `async`.
+ * Defaults to `false`.
+ * @return {!HTMLLinkElement} The link element for the URL to be loaded.
+ */
+export function importHref(href, onload, onerror, optAsync) {
+ let link = /** @type {HTMLLinkElement} */
+ (document.head.querySelector('link[href="' + href + '"][import-href]'));
+ if (!link) {
+ link = /** @type {HTMLLinkElement} */ (document.createElement('link'));
+ link.rel = 'import';
+ link.href = href;
+ link.setAttribute('import-href', '');
+ }
+ // always ensure link has `async` attribute if user specified one,
+ // even if it was previously not async. This is considered less confusing.
+ if (optAsync) {
+ link.setAttribute('async', '');
+ }
+ // NOTE: the link may now be in 3 states: (1) pending insertion,
+ // (2) inflight, (3) already loaded. In each case, we need to add
+ // event listeners to process callbacks.
+ const cleanup = function() {
+ link.removeEventListener('load', loadListener);
+ link.removeEventListener('error', errorListener);
+ };
+ const loadListener = function(event) {
+ cleanup();
+ // In case of a successful load, cache the load event on the link so
+ // that it can be used to short-circuit this method in the future when
+ // it is called with the same href param.
+ link.__dynamicImportLoaded = true;
+ if (onload) {
+ whenImportsReady(() => {
+ onload(event);
+ });
+ }
+ };
+ const errorListener = function(event) {
+ cleanup();
+ // In case of an error, remove the link from the document so that it
+ // will be automatically created again the next time `importHref` is
+ // called.
+ if (link.parentNode) {
+ link.parentNode.removeChild(link);
+ }
+ if (onerror) {
+ whenImportsReady(() => {
+ onerror(event);
+ });
+ }
+ };
+ link.addEventListener('load', loadListener);
+ link.addEventListener('error', errorListener);
+ if (link.parentNode == null) {
+ document.head.appendChild(link);
+ // if the link already loaded, dispatch a fake load event
+ // so that listeners are called and get a proper event argument.
+ } else if (link.__dynamicImportLoaded) {
+ link.dispatchEvent(new Event('load'));
+ }
+ return link;
+}
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
index 6a8a116..e8a1d21 100644
--- a/polygerrit-ui/app/scripts/util.js
+++ b/polygerrit-ui/app/scripts/util.js
@@ -170,5 +170,51 @@
return [...results];
};
+ function getPathFromNode(el) {
+ if (!el.tagName || el.tagName === 'GR-APP'
+ || el instanceof DocumentFragment
+ || el instanceof HTMLSlotElement) {
+ return '';
+ }
+ let path = el.tagName.toLowerCase();
+ if (el.id) path += `#${el.id}`;
+ if (el.className) path += `.${el.className.replace(/ /g, '.')}`;
+ return path;
+ }
+
+ /**
+ * Retrieves the dom path of the current event.
+ *
+ * If the event object contains a `path` property, then use it,
+ * otherwise, construct the dom path based on the event target.
+ *
+ * @param {!Event} e
+ * @return {string}
+ * @example
+ *
+ * domNode.onclick = e => {
+ * getEventPath(e); // eg: div.class1>p#pid.class2
+ * }
+ */
+ util.getEventPath = e => {
+ if (!e) return '';
+
+ let path = e.path;
+ if (!path || !path.length) {
+ path = [];
+ let el = e.target;
+ while (el) {
+ path.push(el);
+ el = el.parentNode || el.host;
+ }
+ }
+
+ return path.reduce((domPath, curEl) => {
+ const pathForEl = getPathFromNode(curEl);
+ if (!pathForEl) return domPath;
+ return domPath ? `${pathForEl}>${domPath}` : pathForEl;
+ }, '');
+ };
+
window.util = util;
})(window);
diff --git a/polygerrit-ui/app/scripts/util_test.html b/polygerrit-ui/app/scripts/util_test.html
new file mode 100644
index 0000000..332707e
--- /dev/null
+++ b/polygerrit-ui/app/scripts/util_test.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<!--
+@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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+
+<test-fixture id="basic">
+ <template>
+ <div id="test" class="a b c">
+ <a class="testBtn"></a>
+ </div>
+ </template>
+</test-fixture>
+
+<script type="module">
+ import '../test/common-test-setup.js';
+ import './util.js';
+ suite('util tests', () => {
+ suite('getEventPath', () => {
+ test('empty event', () => {
+ assert.equal(util.getEventPath(), '');
+ assert.equal(util.getEventPath(null), '');
+ assert.equal(util.getEventPath(undefined), '');
+ assert.equal(util.getEventPath({}), '');
+ });
+
+ test('event with fake path', () => {
+ assert.equal(util.getEventPath({path: []}), '');
+ assert.equal(util.getEventPath({path: [
+ {tagName: 'dd'},
+ ]}), 'dd');
+ });
+
+ test('event with fake complicated path', () => {
+ assert.equal(util.getEventPath({path: [
+ {tagName: 'dd', id: 'test', className: 'a b'},
+ {tagName: 'DIV', id: 'test2', className: 'a b c'},
+ ]}), 'div#test2.a.b.c>dd#test.a.b');
+ });
+
+ test('event with fake target', () => {
+ const fakeTargetParent2 = {
+ tagName: 'DIV', id: 'test2', className: 'a b c',
+ };
+ const fakeTargetParent1 = {
+ parentNode: fakeTargetParent2,
+ tagName: 'dd',
+ id: 'test',
+ className: 'a b',
+ };
+ const fakeTarget = {tagName: 'SPAN', parentNode: fakeTargetParent1};
+ assert.equal(
+ util.getEventPath({target: fakeTarget}),
+ 'div#test2.a.b.c>dd#test.a.b>span'
+ );
+ });
+
+ test('event with real click', () => {
+ const element = fixture('basic');
+ const aLink = element.querySelector('a');
+ let path;
+ aLink.onclick = e => path = util.getEventPath(e);
+ MockInteractions.click(aLink);
+ assert.equal(
+ path,
+ 'html>body>test-fixture#basic>div#test.a.b.c>a.testBtn'
+ );
+ });
+ });
+ });
+</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/styles/dashboard-header-styles.html b/polygerrit-ui/app/styles/dashboard-header-styles.html
deleted file mode 100644
index 88d50c0..0000000
--- a/polygerrit-ui/app/styles/dashboard-header-styles.html
+++ /dev/null
@@ -1,48 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 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.
--->
-
-<dom-module id="dashboard-header-styles">
- <template>
- <style>
- :host {
- background-color: var(--view-background-color);
- display: block;
- min-height: 9em;
- width: 100%;
- }
- gr-avatar {
- display: inline-block;
- height: 7em;
- left: 1em;
- margin: 1em;
- top: 1em;
- width: 7em;
- }
- .info {
- display: inline-block;
- padding: var(--spacing-l);
- vertical-align: top;
- }
- .info > div > span {
- display: inline-block;
- font-weight: var(--font-weight-bold);
- text-align: right;
- width: 4em;
- }
- </style>
- </template>
-</dom-module>
diff --git a/polygerrit-ui/app/styles/dashboard-header-styles.js b/polygerrit-ui/app/styles/dashboard-header-styles.js
new file mode 100644
index 0000000..683202e
--- /dev/null
+++ b/polygerrit-ui/app/styles/dashboard-header-styles.js
@@ -0,0 +1,58 @@
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="dashboard-header-styles">
+ <template>
+ <style>
+ :host {
+ background-color: var(--view-background-color);
+ display: block;
+ min-height: 9em;
+ width: 100%;
+ }
+ gr-avatar {
+ display: inline-block;
+ height: 7em;
+ left: 1em;
+ margin: 1em;
+ top: 1em;
+ width: 7em;
+ }
+ .info {
+ display: inline-block;
+ padding: var(--spacing-l);
+ vertical-align: top;
+ }
+ .info > div > span {
+ display: inline-block;
+ font-weight: var(--font-weight-bold);
+ text-align: right;
+ width: 4em;
+ }
+ </style>
+ </template>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.js
similarity index 79%
rename from polygerrit-ui/app/styles/gr-change-list-styles.html
rename to polygerrit-ui/app/styles/gr-change-list-styles.js
index a8f754d..148943b 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.html
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.js
@@ -1,20 +1,22 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-<dom-module id="gr-change-list-styles">
+$_documentContainer.innerHTML = `<dom-module id="gr-change-list-styles">
<template>
<style>
gr-change-list-item {
@@ -176,4 +178,13 @@
}
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.html b/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.html
deleted file mode 100644
index 84692ba..0000000
--- a/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<dom-module id="gr-change-metadata-shared-styles">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style>
- section {
- display: table-row;
- }
-
- section:not(:first-of-type) .title,
- section:not(:first-of-type) .value {
- padding-top: var(--spacing-s);
- }
-
- .title,
- .value {
- display: table-cell;
- }
-
- .title {
- color: var(--deemphasized-text-color);
- max-width: 20em;
- padding-left: var(--metadata-horizontal-padding);
- padding-right: var(--metadata-horizontal-padding);
- word-break: break-word;
- }
-
- .value {
- padding-right: var(--metadata-horizontal-padding);
- }
- </style>
- </template>
-</dom-module>
diff --git a/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.js b/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.js
new file mode 100644
index 0000000..51cf6d3
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.js
@@ -0,0 +1,61 @@
+/**
+ * @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.
+ */
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-change-metadata-shared-styles">
+ <template>
+ <style include="shared-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style>
+ section {
+ display: table-row;
+ }
+
+ section:not(:first-of-type) .title,
+ section:not(:first-of-type) .value {
+ padding-top: var(--spacing-s);
+ }
+
+ .title,
+ .value {
+ display: table-cell;
+ }
+
+ .title {
+ color: var(--deemphasized-text-color);
+ max-width: 20em;
+ padding-left: var(--metadata-horizontal-padding);
+ padding-right: var(--metadata-horizontal-padding);
+ word-break: break-word;
+ }
+
+ .value {
+ padding-right: var(--metadata-horizontal-padding);
+ }
+ </style>
+ </template>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.html b/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.html
deleted file mode 100644
index 8b26c95..0000000
--- a/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.html
+++ /dev/null
@@ -1,62 +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.
--->
-<!--
- This is shared styles for change-view-integration endpoints.
- All plugins that registered that endpoint should include this in
- the component to have a consistent UX:
-
- <style include="gr-change-view-integration-shared-styles"></style>
-
- And use those defined class to apply these styles.
--->
-<dom-module id="gr-change-view-integration-shared-styles">
- <template>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style>
- :host {
- border-top: 1px solid var(--border-color);
- display: block;
- }
- .header {
- color: var(--primary-text-color);
- background-color: var(--table-header-background-color);
- justify-content: space-between;
- padding: var(--spacing-m) var(--spacing-l);
- border-bottom: 1px solid var(--border-color);
- }
- .header .label {
- font-family: var(--header-font-family);
- font-size: var(--font-size-h3);
- font-weight: var(--font-weight-h3);
- line-height: var(--line-height-h3);
- margin: 0 var(--spacing-l) 0 0;
- }
- .header .note {
- color: var(--deemphasized-text-color);
- }
- .content {
- background-color: var(--view-background-color);
- }
- .header a,
- .content a {
- color: var(--link-color);
- }
- </style>
- </template>
-</dom-module>
diff --git a/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.js b/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.js
new file mode 100644
index 0000000..4bfb742
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.js
@@ -0,0 +1,73 @@
+/**
+ * @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.
+ */
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-change-view-integration-shared-styles">
+ <template>
+ <style include="shared-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style>
+ :host {
+ border-top: 1px solid var(--border-color);
+ display: block;
+ }
+ .header {
+ color: var(--primary-text-color);
+ background-color: var(--table-header-background-color);
+ justify-content: space-between;
+ padding: var(--spacing-m) var(--spacing-l);
+ border-bottom: 1px solid var(--border-color);
+ }
+ .header .label {
+ font-family: var(--header-font-family);
+ font-size: var(--font-size-h3);
+ font-weight: var(--font-weight-h3);
+ line-height: var(--line-height-h3);
+ margin: 0 var(--spacing-l) 0 0;
+ }
+ .header .note {
+ color: var(--deemphasized-text-color);
+ }
+ .content {
+ background-color: var(--view-background-color);
+ }
+ .header a,
+ .content a {
+ color: var(--link-color);
+ }
+ </style>
+ </template>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ This is shared styles for change-view-integration endpoints.
+ All plugins that registered that endpoint should include this in
+ the component to have a consistent UX:
+
+ <style include="gr-change-view-integration-shared-styles"></style>
+
+ And use those defined class to apply these styles.
+*/
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-form-styles.html b/polygerrit-ui/app/styles/gr-form-styles.js
similarity index 72%
rename from polygerrit-ui/app/styles/gr-form-styles.html
rename to polygerrit-ui/app/styles/gr-form-styles.js
index 5133051..91763c5 100644
--- a/polygerrit-ui/app/styles/gr-form-styles.html
+++ b/polygerrit-ui/app/styles/gr-form-styles.js
@@ -1,20 +1,22 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-<dom-module id="gr-form-styles">
+$_documentContainer.innerHTML = `<dom-module id="gr-form-styles">
<template>
<style>
.gr-form-styles input {
@@ -113,4 +115,13 @@
}
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-menu-page-styles.html b/polygerrit-ui/app/styles/gr-menu-page-styles.html
deleted file mode 100644
index 47c874b..0000000
--- a/polygerrit-ui/app/styles/gr-menu-page-styles.html
+++ /dev/null
@@ -1,71 +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.
--->
-<dom-module id="gr-menu-page-styles">
- <template>
- <style>
- :host {
- display: block;
- }
- main {
- margin: var(--spacing-xxl) auto;
- max-width: 50em;
- }
- .mainHeader {
- margin-left: 14em;
- padding: var(--spacing-l) 0 var(--spacing-l) var(--spacing-xxl);
- }
- main.table,
- .mainHeader {
- margin-top: 0;
- margin-right: 0;
- margin-left: 14em;
- max-width: none;
- }
- h2.edited:after {
- color: var(--deemphasized-text-color);
- content: ' *';
- }
- .loading {
- color: var(--deemphasized-text-color);
- padding: var(--spacing-l);
- }
- @media only screen and (max-width: 67em) {
- main {
- margin: var(--spacing-xxl) 0 var(--spacing-xxl) 15em;
- }
- main.table {
- margin-left: 14em;
- }
- }
- @media only screen and (max-width: 53em) {
- .loading {
- padding: 0 var(--spacing-l);
- }
- main {
- margin: var(--spacing-xxl) var(--spacing-l);
- }
- main.table {
- margin: 0;
- }
- .mainHeader {
- margin-left: 0;
- padding: var(--spacing-m) 0 var(--spacing-m) var(--spacing-l);
- }
- }
- </style>
- </template>
-</dom-module>
diff --git a/polygerrit-ui/app/styles/gr-menu-page-styles.js b/polygerrit-ui/app/styles/gr-menu-page-styles.js
new file mode 100644
index 0000000..e52a895
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-menu-page-styles.js
@@ -0,0 +1,82 @@
+/**
+ * @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.
+ */
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-menu-page-styles">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ main {
+ margin: var(--spacing-xxl) auto;
+ max-width: 50em;
+ }
+ .mainHeader {
+ margin-left: 14em;
+ padding: var(--spacing-l) 0 var(--spacing-l) var(--spacing-xxl);
+ }
+ main.table,
+ .mainHeader {
+ margin-top: 0;
+ margin-right: 0;
+ margin-left: 14em;
+ max-width: none;
+ }
+ h2.edited:after {
+ color: var(--deemphasized-text-color);
+ content: ' *';
+ }
+ .loading {
+ color: var(--deemphasized-text-color);
+ padding: var(--spacing-l);
+ }
+ @media only screen and (max-width: 67em) {
+ main {
+ margin: var(--spacing-xxl) 0 var(--spacing-xxl) 15em;
+ }
+ main.table {
+ margin-left: 14em;
+ }
+ }
+ @media only screen and (max-width: 53em) {
+ .loading {
+ padding: 0 var(--spacing-l);
+ }
+ main {
+ margin: var(--spacing-xxl) var(--spacing-l);
+ }
+ main.table {
+ margin: 0;
+ }
+ .mainHeader {
+ margin-left: 0;
+ padding: var(--spacing-m) 0 var(--spacing-m) var(--spacing-l);
+ }
+ }
+ </style>
+ </template>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-page-nav-styles.html b/polygerrit-ui/app/styles/gr-page-nav-styles.html
deleted file mode 100644
index ced6ecb..0000000
--- a/polygerrit-ui/app/styles/gr-page-nav-styles.html
+++ /dev/null
@@ -1,64 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<dom-module id="gr-page-nav-styles">
- <template>
- <style>
- .navStyles ul {
- padding: var(--spacing-l) 0;
- }
- .navStyles li {
- border-bottom: 1px solid transparent;
- border-top: 1px solid transparent;
- display: block;
- padding: 0 var(--spacing-xl);
- }
- .navStyles li a {
- display: block;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .navStyles .subsectionItem {
- padding-left: var(--spacing-xxl);
- }
- .navStyles .hideSubsection {
- display: none;
- }
- .navStyles li.sectionTitle {
- padding: 0 var(--spacing-xxl) 0 var(--spacing-l);
- }
- .navStyles li.sectionTitle:not(:first-child) {
- margin-top: var(--spacing-l);
- }
- .navStyles .title {
- font-weight: var(--font-weight-bold);
- margin: var(--spacing-s) 0;
- }
- .navStyles .selected {
- background-color: var(--view-background-color);
- border-bottom: 1px solid var(--border-color);
- border-top: 1px solid var(--border-color);
- font-weight: var(--font-weight-bold);
- }
- .navStyles a {
- color: var(--primary-text-color);
- display: inline-block;
- margin: var(--spacing-s) 0;
- }
- </style>
- </template>
-</dom-module>
diff --git a/polygerrit-ui/app/styles/gr-page-nav-styles.js b/polygerrit-ui/app/styles/gr-page-nav-styles.js
new file mode 100644
index 0000000..97f1a03
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-page-nav-styles.js
@@ -0,0 +1,75 @@
+/**
+ * @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.
+ */
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-page-nav-styles">
+ <template>
+ <style>
+ .navStyles ul {
+ padding: var(--spacing-l) 0;
+ }
+ .navStyles li {
+ border-bottom: 1px solid transparent;
+ border-top: 1px solid transparent;
+ display: block;
+ padding: 0 var(--spacing-xl);
+ }
+ .navStyles li a {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .navStyles .subsectionItem {
+ padding-left: var(--spacing-xxl);
+ }
+ .navStyles .hideSubsection {
+ display: none;
+ }
+ .navStyles li.sectionTitle {
+ padding: 0 var(--spacing-xxl) 0 var(--spacing-l);
+ }
+ .navStyles li.sectionTitle:not(:first-child) {
+ margin-top: var(--spacing-l);
+ }
+ .navStyles .title {
+ font-weight: var(--font-weight-bold);
+ margin: var(--spacing-s) 0;
+ }
+ .navStyles .selected {
+ background-color: var(--view-background-color);
+ border-bottom: 1px solid var(--border-color);
+ border-top: 1px solid var(--border-color);
+ font-weight: var(--font-weight-bold);
+ }
+ .navStyles a {
+ color: var(--primary-text-color);
+ display: inline-block;
+ margin: var(--spacing-s) 0;
+ }
+ </style>
+ </template>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-subpage-styles.html b/polygerrit-ui/app/styles/gr-subpage-styles.html
deleted file mode 100644
index 222c38b..0000000
--- a/polygerrit-ui/app/styles/gr-subpage-styles.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 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.
--->
-<dom-module id="gr-subpage-styles">
- <template>
- <style>
- main {
- margin: var(--spacing-l);
- }
- .loading {
- display: none;
- }
- #loading.loading {
- display: block;
- }
- #loading:not(.loading) {
- display: none;
- }
- </style>
- </template>
-</dom-module>
diff --git a/polygerrit-ui/app/styles/gr-subpage-styles.js b/polygerrit-ui/app/styles/gr-subpage-styles.js
new file mode 100644
index 0000000..f94cc9c
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-subpage-styles.js
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-subpage-styles">
+ <template>
+ <style>
+ main {
+ margin: var(--spacing-l);
+ }
+ .loading {
+ display: none;
+ }
+ #loading.loading {
+ display: block;
+ }
+ #loading:not(.loading) {
+ display: none;
+ }
+ </style>
+ </template>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-table-styles.html b/polygerrit-ui/app/styles/gr-table-styles.js
similarity index 71%
rename from polygerrit-ui/app/styles/gr-table-styles.html
rename to polygerrit-ui/app/styles/gr-table-styles.js
index 26b6db0..ceac675 100644
--- a/polygerrit-ui/app/styles/gr-table-styles.html
+++ b/polygerrit-ui/app/styles/gr-table-styles.js
@@ -1,21 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @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.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-
-<dom-module id="gr-table-styles">
+$_documentContainer.innerHTML = `<dom-module id="gr-table-styles">
<template>
<style>
.genericList {
@@ -106,4 +107,13 @@
}
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-voting-styles.html b/polygerrit-ui/app/styles/gr-voting-styles.html
deleted file mode 100644
index eec79be..0000000
--- a/polygerrit-ui/app/styles/gr-voting-styles.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<dom-module id="gr-voting-styles">
- <template>
- <style>
- :host {
- --vote-chip-styles: {
- border: 1px solid rgba(0,0,0,.12);
- border-radius: 1em;
- box-shadow: none;
- box-sizing: border-box;
- min-width: 3em;
- }
- }
- </style>
- </template>
-</dom-module>
diff --git a/polygerrit-ui/app/styles/gr-voting-styles.js b/polygerrit-ui/app/styles/gr-voting-styles.js
new file mode 100644
index 0000000..4860428
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-voting-styles.js
@@ -0,0 +1,42 @@
+/**
+ * @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.
+ */
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-voting-styles">
+ <template>
+ <style>
+ :host {
+ --vote-chip-styles: {
+ border: 1px solid rgba(0,0,0,.12);
+ border-radius: 1em;
+ box-shadow: none;
+ box-sizing: border-box;
+ min-width: 3em;
+ }
+ }
+ </style>
+ </template>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/shared-styles.html b/polygerrit-ui/app/styles/shared-styles.js
similarity index 82%
rename from polygerrit-ui/app/styles/shared-styles.html
rename to polygerrit-ui/app/styles/shared-styles.js
index 3a0de59..dc4735e 100644
--- a/polygerrit-ui/app/styles/shared-styles.html
+++ b/polygerrit-ui/app/styles/shared-styles.js
@@ -1,20 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @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.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-<dom-module id="shared-styles">
+$_documentContainer.innerHTML = `<dom-module id="shared-styles">
<template>
<style>
@@ -125,7 +127,7 @@
--iron-icon-width: 20px;
}
- /* Stopgap solution until we remove hidden$ attributes. */
+ /* Stopgap solution until we remove hidden\$ attributes. */
[hidden] {
display: none !important;
@@ -180,4 +182,13 @@
/** END: loading spiner */
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/themes/app-theme.html b/polygerrit-ui/app/styles/themes/app-theme.js
similarity index 87%
rename from polygerrit-ui/app/styles/themes/app-theme.html
rename to polygerrit-ui/app/styles/themes/app-theme.js
index 8aaaa01..295cb017 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.html
+++ b/polygerrit-ui/app/styles/themes/app-theme.js
@@ -1,20 +1,22 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-<custom-style><style is="custom-style">
+$_documentContainer.innerHTML = `<custom-style><style is="custom-style">
html {
/**
* When adding a new color variable make sure to also add it to the other
@@ -205,4 +207,13 @@
--spacing-xxl: 16px;
}
}
-</style></custom-style>
+</style></custom-style>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/test/common-test-setup.html b/polygerrit-ui/app/test/common-test-setup.html
deleted file mode 100644
index 7a391b7..0000000
--- a/polygerrit-ui/app/test/common-test-setup.html
+++ /dev/null
@@ -1,63 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<link rel="import"
- href="/bower_components/polymer-resin/standalone/polymer-resin.html" />
-<link rel="import" href="../behaviors/safe-types-behavior/safe-types-behavior.html">
-<script>
- security.polymer_resin.install({
- allowedIdentifierPrefixes: [''],
- reportHandler(isViolation, fmt, ...args) {
- const log = security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER;
- log(isViolation, fmt, ...args);
- if (isViolation) {
- // This will cause the test to fail if there is a data binding
- // violation.
- throw new Error(
- 'polymer-resin violation: ' + fmt +
- JSON.stringify(args));
- }
- },
- safeTypesBridge: Gerrit.SafeTypes.safeTypesBridge,
- });
-</script>
-<script>
- self.mockPromise = () => {
- let res;
- const promise = new Promise(resolve => {
- res = resolve;
- });
- promise.resolve = res;
- return promise;
- };
- self.isHidden = el => getComputedStyle(el).display === 'none';
-</script>
-<script>
- (function() {
- setup(() => {
- if (!window.Gerrit) { return; }
- if (Gerrit._testOnly_resetPlugins) {
- Gerrit._testOnly_resetPlugins();
- }
- });
- })();
-</script>
-<link rel="import"
- href="/bower_components/iron-test-helpers/iron-test-helpers.html" />
-<link rel="import" href="test-router.html" />
-<script src="/bower_components/moment/moment.js"></script>
diff --git a/polygerrit-ui/app/test/common-test-setup.js b/polygerrit-ui/app/test/common-test-setup.js
new file mode 100644
index 0000000..ab9cc39
--- /dev/null
+++ b/polygerrit-ui/app/test/common-test-setup.js
@@ -0,0 +1,54 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../scripts/bundled-polymer.js';
+
+import 'polymer-resin/standalone/polymer-resin.js';
+import '../behaviors/safe-types-behavior/safe-types-behavior.js';
+import '@polymer/iron-test-helpers/iron-test-helpers.js';
+import './test-router.js';
+import moment from 'moment/src/moment.js';
+self.moment = moment;
+security.polymer_resin.install({
+ allowedIdentifierPrefixes: [''],
+ reportHandler(isViolation, fmt, ...args) {
+ const log = security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER;
+ log(isViolation, fmt, ...args);
+ if (isViolation) {
+ // This will cause the test to fail if there is a data binding
+ // violation.
+ throw new Error(
+ 'polymer-resin violation: ' + fmt +
+ JSON.stringify(args));
+ }
+ },
+ safeTypesBridge: Gerrit.SafeTypes.safeTypesBridge,
+});
+self.mockPromise = () => {
+ let res;
+ const promise = new Promise(resolve => {
+ res = resolve;
+ });
+ promise.resolve = res;
+ return promise;
+};
+self.isHidden = el => getComputedStyle(el).display === 'none';
+setup(() => {
+ if (!window.Gerrit) { return; }
+ if (Gerrit._testOnly_resetPlugins) {
+ Gerrit._testOnly_resetPlugins();
+ }
+});
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 38dde97..5b19340 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -19,8 +19,8 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>Elements Test Runner</title>
<meta charset="utf-8">
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/node_modules/web-component-tester/browser.js"></script>
<style>
/* Prevent horizontal scrolling on page.
New version of web-component-tester creates very narrow iframe */
@@ -189,6 +189,7 @@
'shared/gr-editable-label/gr-editable-label_test.html',
'shared/gr-formatted-text/gr-formatted-text_test.html',
'shared/gr-hovercard/gr-hovercard_test.html',
+ 'shared/gr-hovercard-account/gr-hovercard-account_test.html',
'shared/gr-js-api-interface/gr-annotation-actions-context_test.html',
'shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html',
'shared/gr-js-api-interface/gr-change-actions-js-api_test.html',
@@ -262,6 +263,7 @@
'gr-group-suggestions-provider/gr-group-suggestions-provider_test.html',
'gr-display-name-utils/gr-display-name-utils_test.html',
'gr-email-suggestions-provider/gr-email-suggestions-provider_test.html',
+ 'util_test.html',
];
/* eslint-enable max-len */
for (let file of scripts) {
diff --git a/polygerrit-ui/app/test/test-pre-setup.js b/polygerrit-ui/app/test/test-pre-setup.js
deleted file mode 100644
index dd317cf..0000000
--- a/polygerrit-ui/app/test/test-pre-setup.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * After M80, htmlImports has been removed and the polyfill from
- * webcomponents can only support async html imports unlike
- * native htmlImports which loads htmls synchronously.
- */
-window.readyToTest = () => Promise.race([
- new Promise(resolve =>
- window.addEventListener('HTMLImportsLoaded', resolve)),
- // timeout after 5s, the test timeout is 10s
- new Promise(resolve => setTimeout(resolve, 5000)),
-]);
diff --git a/polygerrit-ui/app/test/test-router.html b/polygerrit-ui/app/test/test-router.html
deleted file mode 100644
index 34ff374..0000000
--- a/polygerrit-ui/app/test/test-router.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<link rel="import" href="../elements/core/gr-navigation/gr-navigation.html">
-<script>
- Gerrit.Nav.setup(url => { /* noop */ }, params => '', () => []);
-</script>
diff --git a/polygerrit-ui/app/test/test-router.js b/polygerrit-ui/app/test/test-router.js
new file mode 100644
index 0000000..914537c
--- /dev/null
+++ b/polygerrit-ui/app/test/test-router.js
@@ -0,0 +1,19 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../elements/core/gr-navigation/gr-navigation.js';
+
+Gerrit.Nav.setup(url => { /* noop */ }, params => '', () => []);
diff --git a/polygerrit-ui/app/wct.conf.js b/polygerrit-ui/app/wct.conf.js
index 0e53adb..1a9300e 100644
--- a/polygerrit-ui/app/wct.conf.js
+++ b/polygerrit-ui/app/wct.conf.js
@@ -27,7 +27,7 @@
*/
const headless = 'WCT_HEADLESS_MODE' in process.env ?
- process.env['WCT_HEADLESS_MODE'] !== '0' : false;
+ process.env['WCT_HEADLESS_MODE'] === '1' : false;
const headlessBrowserOptions = {
chrome: ['start-maximized', 'headless', 'disable-gpu', 'no-sandbox'],
diff --git a/polygerrit-ui/app/wct_test.sh b/polygerrit-ui/app/wct_test.sh
index 0970098..45149f8 100755
--- a/polygerrit-ui/app/wct_test.sh
+++ b/polygerrit-ui/app/wct_test.sh
@@ -16,7 +16,7 @@
# it always receives file from ui_npm. It can broke WCT itself but luckely it works.
cp -R -L ./external/ui_npm/node_modules/* $t/node_modules
-cp -R -L ./polygerrit-ui/app/test-srcs-updated-links/polygerrit-ui/app/* $t/
+cp -R -L ./polygerrit-ui/app/* $t/
export PATH="$(dirname $NPM):$PATH"
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index ad03e51..339812b 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -46,36 +46,6 @@
bundledPluginsPattern = regexp.MustCompile("https://cdn.googlesource.com/polygerrit_assets/[0-9.]*")
)
-type redirectTarget struct {
- NpmModule string `json:"npm_module"`
- Dir string `json:"dir"`
- Files map[string]string `json:"files"`
-}
-
-type redirects struct {
- From string `json:"from"`
- To redirectTarget `json:"to"`
-}
-
-type redirectsJson struct {
- Redirects []redirects `json:"redirects"`
-}
-
-func readRedirects() []redirects {
- redirectsFile, err := os.Open("app/redirects.json")
- if err != nil {
- log.Fatal(err)
- }
- defer redirectsFile.Close()
- redirectsFileContent, err := ioutil.ReadAll(redirectsFile)
- if err != nil {
- log.Fatal(err)
- }
- var result redirectsJson
- json.Unmarshal([]byte(redirectsFileContent), &result)
- return result.Redirects
-}
-
func main() {
flag.Parse()
@@ -89,14 +59,12 @@
log.Fatal(err)
}
- redirects := readRedirects()
-
dirListingMux := http.NewServeMux()
dirListingMux.Handle("/styles/", http.StripPrefix("/styles/", http.FileServer(http.Dir("app/styles"))))
dirListingMux.Handle("/elements/", http.StripPrefix("/elements/", http.FileServer(http.Dir("app/elements"))))
dirListingMux.Handle("/behaviors/", http.StripPrefix("/behaviors/", http.FileServer(http.Dir("app/behaviors"))))
- http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { handleSrcRequest(redirects, dirListingMux, w, req) })
+ http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { handleSrcRequest(dirListingMux, w, req) })
http.Handle("/fonts/",
addDevHeadersMiddleware(http.FileServer(httpfs.New(zipfs.New(fontsArchive, "fonts")))))
@@ -136,44 +104,7 @@
}
-func getFinalPath(redirects []redirects, originalPath string) string {
- testComponentsPrefix := "/components/"
- if strings.HasPrefix(originalPath, testComponentsPrefix) {
- return "/../node_modules/" + originalPath[len(testComponentsPrefix):]
- }
-
- for _, redirect := range redirects {
- fromDir := redirect.From
- if !strings.HasSuffix(fromDir, "/") {
- fromDir = fromDir + "/"
- }
- if strings.HasPrefix(originalPath, fromDir) {
- targetDir := ""
- if redirect.To.NpmModule != "" {
- targetDir = "node_modules/" + redirect.To.NpmModule
- } else {
- targetDir = redirect.To.Dir
- }
- if !strings.HasSuffix(targetDir, "/") {
- targetDir = targetDir + "/"
- }
- if !strings.HasPrefix(targetDir, "/") {
- targetDir = "/" + targetDir
- }
- filename := originalPath[len(fromDir):]
- if redirect.To.Files != nil {
- newfilename, found := redirect.To.Files[filename]
- if found {
- filename = newfilename
- }
- }
- return targetDir + filename
- }
- }
- return originalPath
-}
-
-func handleSrcRequest(redirects []redirects, dirListingMux *http.ServeMux, writer http.ResponseWriter, originalRequest *http.Request) {
+func handleSrcRequest(dirListingMux *http.ServeMux, writer http.ResponseWriter, originalRequest *http.Request) {
parsedUrl, err := url.Parse(originalRequest.RequestURI)
if err != nil {
writer.WriteHeader(500)
@@ -183,29 +114,34 @@
dirListingMux.ServeHTTP(writer, originalRequest)
return
}
- if parsedUrl.Path == "/bower_components/web-component-tester/browser.js" {
- http.Redirect(writer, originalRequest, "/bower_components/wct-browser-legacy/browser.js", 301)
- return
+
+ normalizedContentPath := parsedUrl.Path
+
+ if !strings.HasPrefix(normalizedContentPath, "/") {
+ normalizedContentPath = "/" + normalizedContentPath
}
- requestPath := getFinalPath(redirects, parsedUrl.Path)
-
- if !strings.HasPrefix(requestPath, "/") {
- requestPath = "/" + requestPath
- }
-
- data, err := readFile(parsedUrl.Path, requestPath)
+ isJsFile := strings.HasSuffix(normalizedContentPath, ".js") || strings.HasSuffix(normalizedContentPath, ".mjs")
+ data, err := getContent(normalizedContentPath)
if err != nil {
- writer.WriteHeader(404)
- return
+ data, err = getContent(normalizedContentPath + ".js")
+ if err != nil {
+ writer.WriteHeader(404)
+ return
+ }
+ isJsFile = true
}
- if strings.HasSuffix(requestPath, ".js") {
- r := regexp.MustCompile("(?m)^(import.*)'([^/.].*)';$")
- data = r.ReplaceAll(data, []byte("$1 '/node_modules/$2'"))
+ if isJsFile {
+ moduleImportRegexp := regexp.MustCompile("(?m)^(import.*)'([^/.].*)';$")
+ data = moduleImportRegexp.ReplaceAll(data, []byte("$1 '/node_modules/$2';"))
writer.Header().Set("Content-Type", "application/javascript")
- } else if strings.HasSuffix(requestPath, ".css") {
+ } else if strings.HasSuffix(normalizedContentPath, ".css") {
writer.Header().Set("Content-Type", "text/css")
- } else if strings.HasSuffix(requestPath, ".html") {
+ } else if strings.HasSuffix(normalizedContentPath, "_test.html") {
+ moduleImportRegexp := regexp.MustCompile("(?m)^(import.*)'([^/.].*)';$")
+ data = moduleImportRegexp.ReplaceAll(data, []byte("$1 '/node_modules/$2';"))
+ writer.Header().Set("Content-Type", "text/html")
+ } else if strings.HasSuffix(normalizedContentPath, ".html") {
writer.Header().Set("Content-Type", "text/html")
}
writer.WriteHeader(200)
@@ -213,18 +149,24 @@
writer.Write(data)
}
-func readFile(originalPath string, redirectedPath string) ([]byte, error) {
- pathsToTry := []string{"app" + redirectedPath}
+func getContent(normalizedContentPath string) ([]byte, error) {
+ //normalizedContentPath must always starts with '/'
+ pathsToTry := []string{"app" + normalizedContentPath}
bowerComponentsSuffix := "/bower_components/"
nodeModulesPrefix := "/node_modules/"
+ testComponentsPrefix := "/components/"
- if strings.HasPrefix(originalPath, bowerComponentsSuffix) {
- pathsToTry = append(pathsToTry, "node_modules/wct-browser-legacy/node_modules/"+originalPath[len(bowerComponentsSuffix):])
- pathsToTry = append(pathsToTry, "node_modules/"+originalPath[len(bowerComponentsSuffix):])
+ if strings.HasPrefix(normalizedContentPath, testComponentsPrefix) {
+ pathsToTry = append(pathsToTry, "node_modules/wct-browser-legacy/node_modules/"+normalizedContentPath[len(testComponentsPrefix):])
+ pathsToTry = append(pathsToTry, "node_modules/"+normalizedContentPath[len(testComponentsPrefix):])
}
- if strings.HasPrefix(originalPath, nodeModulesPrefix) {
- pathsToTry = append(pathsToTry, "node_modules/"+originalPath[len(nodeModulesPrefix):])
+ if strings.HasPrefix(normalizedContentPath, bowerComponentsSuffix) {
+ pathsToTry = append(pathsToTry, "node_modules/@webcomponents/"+normalizedContentPath[len(bowerComponentsSuffix):])
+ }
+
+ if strings.HasPrefix(normalizedContentPath, nodeModulesPrefix) {
+ pathsToTry = append(pathsToTry, "node_modules/"+normalizedContentPath[len(nodeModulesPrefix):])
}
for _, path := range pathsToTry {
diff --git a/proto/cache.proto b/proto/cache.proto
index 5fc5e68..c80d51b 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -208,17 +208,17 @@
}
repeated AssigneeStatusUpdateProto assignee_update = 22;
- // An update to the attention set of the change. See class AttentionStatus for
- // context.
- message AttentionStatusProto {
+ // An update to the attention set of the change. See class AttentionSetUpdate
+ // for context.
+ message AttentionSetUpdateProto {
// Epoch millis.
int64 timestamp_millis = 1;
int32 account = 2;
- // Maps to enum AttentionStatus.Operation
+ // Maps to enum AttentionSetUpdate.Operation
string operation = 3;
string reason = 4;
}
- repeated AttentionStatusProto attention_status = 23;
+ repeated AttentionSetUpdateProto attention_set_update = 23;
}
// Serialized form of com.google.gerrit.server.query.change.ConflictKey
diff --git a/tools/bzl/javadoc.bzl b/tools/bzl/javadoc.bzl
index eecafb44..62b4010 100644
--- a/tools/bzl/javadoc.bzl
+++ b/tools/bzl/javadoc.bzl
@@ -32,7 +32,7 @@
"export TZ",
"rm -rf %s" % source,
"mkdir %s" % source,
- " && ".join(["unzip -qud %s %s" % (source, j.path) for j in source_jars.to_list()]),
+ " && ".join(["unzip -qoud %s %s" % (source, j.path) for j in source_jars.to_list()]),
"rm -rf %s" % dir,
"mkdir %s" % dir,
" ".join([
diff --git a/tools/node_tools/polygerrit_app_preprocessor/index.bzl b/tools/node_tools/polygerrit_app_preprocessor/index.bzl
deleted file mode 100644
index ba53815..0000000
--- a/tools/node_tools/polygerrit_app_preprocessor/index.bzl
+++ /dev/null
@@ -1,184 +0,0 @@
-"""This file contains rules to preprocess files before bundling"""
-
-def _update_links_impl(ctx):
- """Wrapper for the links-update command-line tool"""
-
- dir_name = ctx.label.name
- output_files = []
- input_js_files = []
- output_js_files = []
- js_files_args = ctx.actions.args()
- js_files_args.set_param_file_format("multiline")
- js_files_args.use_param_file("%s", use_always = True)
-
- for f in ctx.files.srcs:
- output_file = ctx.actions.declare_file(dir_name + "/" + f.path)
- output_files.append(output_file)
- if f.extension == "html":
- input_js_files.append(f)
- output_js_files.append(output_file)
- js_files_args.add(f)
- js_files_args.add(output_file)
- else:
- ctx.actions.expand_template(
- output = output_file,
- template = f,
- substitutions = {},
- )
-
- ctx.actions.run(
- executable = ctx.executable._updater,
- outputs = output_js_files,
- inputs = input_js_files + [ctx.file.redirects],
- arguments = [js_files_args, ctx.file.redirects.path],
- )
- return [DefaultInfo(files = depset(output_files))]
-
-update_links = rule(
- implementation = _update_links_impl,
- attrs = {
- "srcs": attr.label_list(allow_files = True),
- "redirects": attr.label(allow_single_file = True, mandatory = True),
- "_updater": attr.label(
- default = ":links-updater-bin",
- executable = True,
- cfg = "host",
- ),
- },
-)
-
-def _get_node_modules_root(node_modules):
- if node_modules == None or len(node_modules) == 0:
- return None
-
- node_module_root = node_modules[0].label.workspace_root
- for target in node_modules:
- if target.label.workspace_root != node_module_root:
- fail("Only one node_modules workspace can be used")
- return node_module_root + "/"
-
-def _get_relative_path(file, root):
- root_len = len(root)
- if file.path.startswith(root):
- return file.path[root_len - 1:]
- else:
- fail("The file '%s' is not under the root '%s'." % (file.path, root))
-
-def _copy_file(ctx, src, target_name):
- output_file = ctx.actions.declare_file(target_name)
- ctx.actions.expand_template(
- output = output_file,
- template = src,
- substitutions = {},
- )
- return output_file
-
-def _get_generated_files(ctx, files, files_root_path, target_dir):
- gen_files_for_html = dict()
- gen_files_for_js = dict()
- copied_files = []
- for f in files:
- target_name = target_dir + _get_relative_path(f, files_root_path)
- if f.extension == "html":
- html_output_file = ctx.actions.declare_file(target_name)
- js_output_file = ctx.actions.declare_file(target_name + "_gen.js")
- gen_files_for_html.update([[f, {"html": html_output_file, "js": js_output_file}]])
- elif f.extension == "js":
- js_output_file = ctx.actions.declare_file(target_name)
- gen_files_for_js.update([[f, {"js": js_output_file}]])
- else:
- copied_files.append(_copy_file(ctx, f, target_name))
- return (gen_files_for_html, gen_files_for_js, copied_files)
-
-def _prepare_for_bundling_impl(ctx):
- dir_name = ctx.label.name
- all_output_files = []
-
- node_modules_root = _get_node_modules_root(ctx.attr.node_modules)
-
- html_files_dict = dict()
- js_files_dict = dict()
-
- root_path = ctx.bin_dir.path + "/" + ctx.attr.root_path
- if not root_path.endswith("/"):
- root_path = root_path + "/"
-
- gen_files_for_html, gen_files_for_js, copied_files = _get_generated_files(ctx, ctx.files.srcs, root_path, dir_name)
- html_files_dict.update(gen_files_for_html)
- js_files_dict.update(gen_files_for_js)
- all_output_files.extend(copied_files)
-
- gen_files_for_html, gen_files_for_js, copied_files = _get_generated_files(ctx, ctx.files.additional_node_modules_to_preprocess, node_modules_root, dir_name)
- html_files_dict.update(gen_files_for_html)
- js_files_dict.update(gen_files_for_js)
- all_output_files.extend(copied_files)
-
- for f in ctx.files.node_modules:
- target_name = dir_name + _get_relative_path(f, node_modules_root)
- if html_files_dict.get(f) == None and js_files_dict.get(f) == None:
- all_output_files.append(_copy_file(ctx, f, target_name))
-
- preprocessed_output_files = []
- html_files_args = ctx.actions.args()
- html_files_args.set_param_file_format("multiline")
- html_files_args.use_param_file("%s", use_always = True)
-
- for src_path, output_files in html_files_dict.items():
- html_files_args.add(src_path)
- html_files_args.add(output_files["html"])
- html_files_args.add(output_files["js"])
- preprocessed_output_files.append(output_files["html"])
- preprocessed_output_files.append(output_files["js"])
-
- js_files_args = ctx.actions.args()
- js_files_args.set_param_file_format("multiline")
- js_files_args.use_param_file("%s", use_always = True)
- for src_path, output_files in js_files_dict.items():
- js_files_args.add(src_path)
- js_files_args.add(output_files["js"])
- preprocessed_output_files.append(output_files["js"])
-
- all_output_files.extend(preprocessed_output_files)
-
- ctx.actions.run(
- executable = ctx.executable._preprocessor,
- outputs = preprocessed_output_files,
- inputs = ctx.files.srcs + ctx.files.additional_node_modules_to_preprocess,
- arguments = [root_path, html_files_args, js_files_args],
- )
-
- entry_point_html = ctx.attr.entry_point
- entry_point_js = ctx.attr.entry_point + "_gen.js"
- ctx.actions.write(ctx.outputs.html, "<link rel=\"import\" href=\"./%s\" >" % entry_point_html)
- ctx.actions.write(ctx.outputs.js, "import \"./%s\";" % entry_point_js)
-
- return [
- DefaultInfo(files = depset([ctx.outputs.html, ctx.outputs.js], transitive = [depset(all_output_files)])),
- OutputGroupInfo(
- js = depset([ctx.outputs.js] + [f for f in all_output_files if f.extension == "js"]),
- html = depset([ctx.outputs.html] + [f for f in all_output_files if f.extension == "html"]),
- ),
- ]
-
-prepare_for_bundling = rule(
- implementation = _prepare_for_bundling_impl,
- attrs = {
- "srcs": attr.label_list(allow_files = True),
- "node_modules": attr.label_list(allow_files = True),
- "_preprocessor": attr.label(
- default = ":preprocessor-bin",
- executable = True,
- cfg = "host",
- ),
- "additional_node_modules_to_preprocess": attr.label_list(allow_files = True),
- "root_path": attr.string(),
- "entry_point": attr.string(
- mandatory = True,
- doc = "Path relative to root_path",
- ),
- },
- outputs = {
- "html": "%{name}/entry.html",
- "js": "%{name}/entry.js",
- },
-)