diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-09-01 17:30:34 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-09-01 17:30:34 +0200 |
commit | d2da7f30d512284c943b82abc135483b59b85536 (patch) | |
tree | 71a76b276c94a8025ae8b9e8d8be5387b356a1eb | |
parent | 11de4cfe580382a23ac664c5b4464d426b742a3e (diff) | |
download | sonarqube-d2da7f30d512284c943b82abc135483b59b85536.tar.gz sonarqube-d2da7f30d512284c943b82abc135483b59b85536.zip |
SONAR-5856 Rewrite Settings page (#1163)
128 files changed, 3591 insertions, 2545 deletions
diff --git a/it/it-tests/src/test/java/it/Category1Suite.java b/it/it-tests/src/test/java/it/Category1Suite.java index 18f2c01a352..8f1a4cbe3a0 100644 --- a/it/it-tests/src/test/java/it/Category1Suite.java +++ b/it/it-tests/src/test/java/it/Category1Suite.java @@ -49,7 +49,6 @@ import it.qualityGate.QualityGateTest; import it.qualityGate.QualityGateUiTest; import it.settings.PropertySetsTest; import it.settings.SettingsTest; -import it.settings.SubCategoriesTest; import it.sourceCode.EncodingTest; import it.sourceCode.HighlightingTest; import it.sourceCode.ProjectCodeTest; @@ -71,7 +70,6 @@ import static util.ItUtils.xooPlugin; BackgroundTasksTest.class, // settings PropertySetsTest.class, - SubCategoriesTest.class, SettingsTest.class, // i18n I18nTest.class, diff --git a/it/it-tests/src/test/java/it/componentDashboard/DashboardTest.java b/it/it-tests/src/test/java/it/componentDashboard/DashboardTest.java index 4ef6c3000e9..2f903b05fb6 100644 --- a/it/it-tests/src/test/java/it/componentDashboard/DashboardTest.java +++ b/it/it-tests/src/test/java/it/componentDashboard/DashboardTest.java @@ -70,9 +70,6 @@ public class DashboardTest { // SONAR-2073 "/componentDashboard/DashboardTest/global_dashboard/filter-widget-anonymous.html", - // SONAR-3460 - "/componentDashboard/DashboardTest/global_dashboard/global-admin-dashboards.html", - // SONAR-3461 "/componentDashboard/DashboardTest/global_dashboard/default-dashboards.html", diff --git a/it/it-tests/src/test/java/it/measureHistory/DifferentialPeriodsTest.java b/it/it-tests/src/test/java/it/measureHistory/DifferentialPeriodsTest.java index 0cd55995418..00a02366502 100644 --- a/it/it-tests/src/test/java/it/measureHistory/DifferentialPeriodsTest.java +++ b/it/it-tests/src/test/java/it/measureHistory/DifferentialPeriodsTest.java @@ -21,7 +21,6 @@ package it.measureHistory; import com.sonar.orchestrator.Orchestrator; import com.sonar.orchestrator.locator.FileLocation; -import com.sonar.orchestrator.selenium.Selenese; import it.Category1Suite; import java.util.Date; import java.util.List; @@ -32,8 +31,8 @@ import org.junit.Test; import org.sonar.wsclient.services.Measure; import org.sonar.wsclient.services.Resource; import org.sonar.wsclient.services.ResourceQuery; +import pageobjects.Navigation; import util.ItUtils; -import util.selenium.SeleneseTest; import static org.apache.commons.lang.time.DateUtils.addDays; import static org.assertj.core.api.Assertions.assertThat; @@ -133,9 +132,8 @@ public class DifferentialPeriodsTest { assertThat(measures.get(0).getVariation1()).isEqualTo(17); // Check on ui that it's possible to define leak period on project - new SeleneseTest(Selenese.builder().setHtmlTestsInClasspath("define-leak-period-on-project", - "/measureHistory/DifferentialPeriodsTest/define-leak-period-on-project.html" - ).build()).runOn(orchestrator); + Navigation.get(orchestrator).openHomepage().logIn().asAdmin().openSettings("sample") + .assertSettingDisplayed("sonar.timemachine.period1"); } /** diff --git a/it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java b/it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java index 50a3f12b3b4..5026725cbc8 100644 --- a/it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java +++ b/it/it-tests/src/test/java/it/projectAdministration/ProjectAdministrationTest.java @@ -23,6 +23,7 @@ import com.sonar.orchestrator.Orchestrator; import com.sonar.orchestrator.build.SonarScanner; import com.sonar.orchestrator.selenium.Selenese; import it.Category1Suite; +import java.io.UnsupportedEncodingException; import java.sql.SQLException; import java.util.Calendar; import java.util.GregorianCalendar; @@ -34,22 +35,27 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.sonar.wsclient.SonarClient; import org.sonar.wsclient.base.HttpException; -import org.sonar.wsclient.services.PropertyQuery; import org.sonar.wsclient.services.ResourceQuery; import org.sonar.wsclient.user.UserParameters; +import pageobjects.Navigation; +import pageobjects.settings.SettingsPage; import util.selenium.SeleneseTest; import static org.assertj.core.api.Assertions.assertThat; import static util.ItUtils.projectDir; public class ProjectAdministrationTest { - private static final String DELETE_WS_ENDPOINT = "api/projects/bulk_delete"; + @ClassRule public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR; + @Rule public ExpectedException expectedException = ExpectedException.none(); + @Rule + public Navigation nav = Navigation.get(orchestrator); + private static final String PROJECT_KEY = "sample"; private static final String FILE_KEY = "sample:src/main/xoo/sample/Sample.xoo"; @@ -116,7 +122,7 @@ public class ProjectAdministrationTest { new SeleneseTest( Selenese.builder().setHtmlTestsInClasspath("project-deletion", "/projectAdministration/ProjectAdministrationTest/project-deletion/project-deletion.html").build()) - .runOn(orchestrator); + .runOn(orchestrator); } finally { wsClient.userClient().deactivate(projectAdminUser); } @@ -156,40 +162,37 @@ public class ProjectAdministrationTest { assertThat(count("events where category='Version'")).as("Different number of events").isEqualTo(1); } - /** - * SONAR-3425 - */ @Test - public void project_settings() { - scanSampleWithDate("2012-01-01"); - - Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("project-settings", - // SONAR-3425 - "/projectAdministration/ProjectAdministrationTest/project-settings/override-global-settings.html", - - "/projectAdministration/ProjectAdministrationTest/project-settings/only-on-project-settings.html").build(); - new SeleneseTest(selenese).runOn(orchestrator); - - // GET /api/properties/sonar.exclusions?resource=sample - assertThat(orchestrator.getServer().getAdminWsClient().find(PropertyQuery.createForResource("sonar.exclusions", "sample")).getValue()) - .isEqualTo("my-exclusions"); - - // GET /api/properties?resource=sample - // Note that this WS is used by SonarLint - assertThat(orchestrator.getServer().getAdminWsClient().findAll(PropertyQuery.createForResource(null, "sample"))).isNotEmpty(); + public void display_project_settings() throws UnsupportedEncodingException { + scanSample(null, null); + + SettingsPage page = nav.logIn().asAdmin().openSettings("sample") + .assertMenuContains("Analysis Scope") + .assertMenuContains("Category 1") + .assertMenuContains("DEV") + .assertMenuContains("project-only") + .assertMenuContains("Xoo") + .assertSettingDisplayed("sonar.dbcleaner.daysBeforeDeletingClosedIssues"); + + page.openCategory("project-only") + .assertSettingDisplayed("prop_only_on_project"); + + page.openCategory("General") + .assertStringSettingValue("sonar.dbcleaner.daysBeforeDeletingClosedIssues", "30") + .assertStringSettingValue("sonar.timemachine.period1", "previous_version") + .assertBooleanSettingValue("sonar.dbcleaner.cleanDirectory", true) + .setStringValue("sonar.dbcleaner.daysBeforeDeletingClosedIssues", "1") + .assertStringSettingValue("sonar.dbcleaner.daysBeforeDeletingClosedIssues", "1"); } - /** - * SONAR-4060 - */ @Test - public void display_module_settings() { + public void display_module_settings() throws UnsupportedEncodingException { orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample"))); - Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("module-settings", - // SONAR-3425 - "/projectAdministration/ProjectAdministrationTest/module-settings/display-module-settings.html").build(); - new SeleneseTest(selenese).runOn(orchestrator); + nav.logIn().asAdmin() + .openSettings("com.sonarsource.it.samples:multi-modules-sample:module_a") + .assertMenuContains("Analysis Scope") + .assertSettingDisplayed("sonar.coverage.exclusions"); } private void scanSampleWithDate(String date) { diff --git a/it/it-tests/src/test/java/it/settings/PropertySetsTest.java b/it/it-tests/src/test/java/it/settings/PropertySetsTest.java index fb4858b6ff4..0dca1cf94b5 100644 --- a/it/it-tests/src/test/java/it/settings/PropertySetsTest.java +++ b/it/it-tests/src/test/java/it/settings/PropertySetsTest.java @@ -20,20 +20,22 @@ package it.settings; import com.sonar.orchestrator.Orchestrator; -import com.sonar.orchestrator.selenium.Selenese; import it.Category1Suite; +import java.io.UnsupportedEncodingException; import java.util.List; import java.util.Map; import org.junit.After; import org.junit.BeforeClass; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.sonarqube.ws.Settings; import org.sonarqube.ws.client.setting.ResetRequest; import org.sonarqube.ws.client.setting.SetRequest; import org.sonarqube.ws.client.setting.SettingsService; import org.sonarqube.ws.client.setting.ValuesRequest; -import util.selenium.SeleneseTest; +import pageobjects.Navigation; +import pageobjects.settings.SettingsPage; import static com.google.common.collect.Lists.newArrayList; import static java.util.Arrays.asList; @@ -47,6 +49,9 @@ public class PropertySetsTest { @ClassRule public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR; + @Rule + public Navigation nav = Navigation.get(orchestrator); + static SettingsService SETTINGS; @BeforeClass @@ -60,37 +65,37 @@ public class PropertySetsTest { } @Test - public void support_property_sets() { - Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("property-sets", - "/settings/PropertySetsTest/property-sets/create.html", - "/settings/PropertySetsTest/property-sets/delete.html", - "/settings/PropertySetsTest/property-sets/reference.html", - "/settings/PropertySetsTest/property-sets/all_types.html").build(); - // Use the old runner because it fails with the new Selenium runner - orchestrator.executeSelenese(selenese); - - // SSF-25 Check that the password has well be setted as now it does not appears in the html source code - assertPropertySet("sonar.demo", - asList(entry("password", "abcde"), - entry("boolean", "true"), - entry("float", "42.0"), - entry("license", "abc"), - entry("list", "AAA"), - entry("metric", "overall_branch_coverage"), - entry("regexp", ".*"), - entry("text", "text"))); + public void support_property_sets() throws UnsupportedEncodingException { + SettingsPage page = nav.logIn().asAdmin().openSettings(null).openCategory("DEV") + .assertSettingDisplayed("sonar.test.jira.servers"); + + page.getPropertySetInput("sonar.test.jira.servers") + .setFieldValue("key", "jira1") + .setFieldValue("url", "http://jira") + .setFieldValue("port", "12345") + .save(); + + assertPropertySet("sonar.test.jira.servers", asList( + entry("key", "jira1"), + entry("url", "http://jira"), + entry("port", "12345"))); } @Test - public void support_property_sets_with_auto_generated_keys() { - new SeleneseTest( - Selenese.builder().setHtmlTestsInClasspath("create-auto-generated", - "/settings/PropertySetsTest/auto-generated/create.html").build()).runOn(orchestrator); - assertPropertySet("sonar.autogenerated", asList(entry("value", "FIRST")), asList(entry("value", "SECOND")), asList(entry("value", "THIRD"))); - - new SeleneseTest(Selenese.builder().setHtmlTestsInClasspath("update-auto-generated", - "/settings/PropertySetsTest/auto-generated/update.html").build()).runOn(orchestrator); - assertPropertySet("sonar.autogenerated", asList(entry("value", "FIRST")), asList(entry("value", "THIRD"))); + public void support_property_sets_with_auto_generated_keys() throws UnsupportedEncodingException { + SettingsPage page = nav.logIn().asAdmin().openSettings(null).openCategory("DEV") + .assertSettingDisplayed("sonar.autogenerated"); + + page.getPropertySetInput("sonar.autogenerated") + .setFieldValue(0, "value", "FIRST") + .setFieldValue(1, "value", "SECOND") + .setFieldValue(2, "value", "THIRD") + .save(); + + assertPropertySet("sonar.autogenerated", + asList(entry("value", "FIRST")), + asList(entry("value", "SECOND")), + asList(entry("value", "THIRD"))); } @Test @@ -124,7 +129,7 @@ public class PropertySetsTest { assertThat(setting.getFieldValues().getFieldValuesList()).hasSize(fieldsValues.length); int index = 0; for (Settings.FieldValues.Value fieldsValue : setting.getFieldValues().getFieldValuesList()) { - assertThat(fieldsValue.getValue()).containsOnly(fieldsValues[index].toArray(new Map.Entry[] {})); + assertThat(fieldsValue.getValue()).containsOnly(fieldsValues[index].toArray(new Map.Entry[]{})); index++; } } diff --git a/it/it-tests/src/test/java/it/settings/SettingsTestRestartingOrchestrator.java b/it/it-tests/src/test/java/it/settings/SettingsTestRestartingOrchestrator.java index 055c7991026..67b2eefa62f 100644 --- a/it/it-tests/src/test/java/it/settings/SettingsTestRestartingOrchestrator.java +++ b/it/it-tests/src/test/java/it/settings/SettingsTestRestartingOrchestrator.java @@ -22,11 +22,13 @@ package it.settings; import com.sonar.orchestrator.Orchestrator; import com.sonar.orchestrator.build.SonarScanner; import com.sonar.orchestrator.selenium.Selenese; +import java.io.UnsupportedEncodingException; import java.net.URL; import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import pageobjects.Navigation; import util.selenium.SeleneseTest; import static util.ItUtils.pluginArtifact; @@ -51,7 +53,7 @@ public class SettingsTestRestartingOrchestrator { } @Test - public void test_settings() { + public void test_settings() throws UnsupportedEncodingException { URL secretKeyUrl = getClass().getResource("/settings/SettingsTest/sonar-secret.txt"); orchestrator = Orchestrator.builderEnv() .addPlugin(pluginArtifact("settings-plugin")) @@ -60,33 +62,28 @@ public class SettingsTestRestartingOrchestrator { .build(); orchestrator.start(); - Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_settings", - "/settings/SettingsTest/general-settings.html", - - // SONAR-2869 the annotation @Properties can be used on extensions and not only on plugin entry points - "/settings/SettingsTest/hidden-extension-property.html", - "/settings/SettingsTest/global-extension-property.html", + Navigation.get(orchestrator).openHomepage().logIn().asAdmin().openSettings(null) + .assertMenuContains("General") + .assertSettingDisplayed("sonar.dbcleaner.cleanDirectory") + .assertSettingNotDisplayed("settings.extension.hidden") + .assertSettingNotDisplayed("settings.extension.global"); - // SONAR-3344 - licenses - "/settings/SettingsTest/ignore-corrupted-license.html", - "/settings/SettingsTest/display-license.html", - "/settings/SettingsTest/display-untyped-license.html", - - // SONAR-2084 - encryption + Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_settings", + // test encryption "/settings/SettingsTest/generate-secret-key.html", - "/settings/SettingsTest/encrypt-text.html", - - // SONAR-1378 - property types - "/settings/SettingsTest/validate-property-type.html", - - // SONAR-3127 - hide passwords - "/settings/SettingsTest/hide-passwords.html" - ).build(); + "/settings/SettingsTest/encrypt-text.html" + + // test licenses + // TODO enable when license page will be rewritten + // "/settings/SettingsTest/ignore-corrupted-license.html", + // "/settings/SettingsTest/display-license.html", + // "/settings/SettingsTest/display-untyped-license.html" + ).build(); new SeleneseTest(selenese).runOn(orchestrator); } @Test - public void property_relocation() { + public void property_relocation() throws UnsupportedEncodingException { orchestrator = Orchestrator.builderEnv() .addPlugin(pluginArtifact("property-relocation-plugin")) .addPlugin(xooPlugin()) @@ -101,10 +98,10 @@ public class SettingsTestRestartingOrchestrator { // should not fail orchestrator.executeBuilds(withDeprecatedKey, withNewKey); - Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("property_relocation", - "/settings/SettingsTest/property_relocation.html" - ).build(); - new SeleneseTest(selenese).runOn(orchestrator); + Navigation.get(orchestrator).openHomepage().logIn().asAdmin().openSettings(null) + .assertMenuContains("General") + .assertSettingDisplayed("sonar.newKey") + .assertSettingNotDisplayed("sonar.deprecatedKey"); } } diff --git a/it/it-tests/src/test/java/it/settings/SubCategoriesTest.java b/it/it-tests/src/test/java/it/settings/SubCategoriesTest.java deleted file mode 100644 index 5bca8b88d5d..00000000000 --- a/it/it-tests/src/test/java/it/settings/SubCategoriesTest.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package it.settings; - -import com.sonar.orchestrator.Orchestrator; -import com.sonar.orchestrator.build.SonarScanner; -import com.sonar.orchestrator.selenium.Selenese; -import it.Category1Suite; -import org.junit.ClassRule; -import org.junit.Test; -import org.sonar.wsclient.services.PropertyQuery; -import util.selenium.SeleneseTest; - -import static org.assertj.core.api.Assertions.assertThat; -import static util.ItUtils.projectDir; - -public class SubCategoriesTest { - - @ClassRule - public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR; - - /** - * SONAR-3159 - */ - @Test - public void should_support_global_subcategories() { - Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("subcategories", - "/settings/subcategories/global-subcategories.html", - // SONAR-4495 - "/settings/subcategories/global-subcategories-no-default.html" - ).build(); - new SeleneseTest(selenese).runOn(orchestrator); - assertThat(getProperty("prop3", null)).isEqualTo("myValue"); - } - - /** - * SONAR-3159 - */ - @Test - public void should_support_project_subcategories() { - orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample"))); - - Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("subcategories", - "/settings/subcategories/project-subcategories.html", - // SONAR-4495 - "/settings/subcategories/project-subcategories-no-default.html" - ).build(); - new SeleneseTest(selenese).runOn(orchestrator); - assertThat(getProperty("prop3", "sample")).isEqualTo("myValue2"); - } - - static String getProperty(String key, String resourceKeyOrId) { - return orchestrator.getServer().getAdminWsClient().find(new PropertyQuery().setKey(key).setResourceKeyOrId(resourceKeyOrId)).getValue(); - } -} diff --git a/it/it-tests/src/test/java/pageobjects/LoginPage.java b/it/it-tests/src/test/java/pageobjects/LoginPage.java index 2aea164f0cb..98e95b01df8 100644 --- a/it/it-tests/src/test/java/pageobjects/LoginPage.java +++ b/it/it-tests/src/test/java/pageobjects/LoginPage.java @@ -36,6 +36,10 @@ public class LoginPage { return submitCredentials(login, password, Navigation.class); } + public Navigation asAdmin() { + return submitCredentials("admin", "admin"); + } + public LoginPage submitWrongCredentials(String login, String password) { $("#login").val(login); $("#password").val(password); diff --git a/it/it-tests/src/test/java/pageobjects/Navigation.java b/it/it-tests/src/test/java/pageobjects/Navigation.java index ecd9cc6bc67..28a99e4b3f8 100644 --- a/it/it-tests/src/test/java/pageobjects/Navigation.java +++ b/it/it-tests/src/test/java/pageobjects/Navigation.java @@ -24,8 +24,12 @@ import com.codeborne.selenide.Selenide; import com.codeborne.selenide.SelenideElement; import com.codeborne.selenide.WebDriverRunner; import com.sonar.orchestrator.Orchestrator; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import javax.annotation.Nullable; import org.junit.rules.ExternalResource; import org.openqa.selenium.By; +import pageobjects.settings.SettingsPage; import static com.codeborne.selenide.Selenide.$; import static com.codeborne.selenide.Selenide.page; @@ -84,6 +88,13 @@ public class Navigation extends ExternalResource { return open("/background_tasks", BackgroundTasksPage.class); } + public SettingsPage openSettings(@Nullable String projectKey) throws UnsupportedEncodingException { + String url = projectKey != null ? + "/project/settings?id=" + URLEncoder.encode(projectKey, "UTF-8") : + "/settings"; + return open(url, SettingsPage.class); + } + public void open(String relativeUrl) { Selenide.open(relativeUrl); } diff --git a/it/it-tests/src/test/java/pageobjects/settings/PropertySetInput.java b/it/it-tests/src/test/java/pageobjects/settings/PropertySetInput.java new file mode 100644 index 00000000000..9da8501fc02 --- /dev/null +++ b/it/it-tests/src/test/java/pageobjects/settings/PropertySetInput.java @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package pageobjects.settings; + +import com.codeborne.selenide.SelenideElement; + +import static com.codeborne.selenide.Condition.exist; + +public class PropertySetInput { + + private final SelenideElement elt; + + public PropertySetInput(SelenideElement elt) { + this.elt = elt; + } + + public PropertySetInput setFieldValue(int index, String fieldKey, String value) { + elt.findAll("input[name$=\"[" + fieldKey + "]\"]").get(index).val(value); + return this; + } + + public PropertySetInput setFieldValue(String fieldKey, String value) { + return setFieldValue(0, fieldKey, value); + } + + public PropertySetInput save() { + elt.find(".js-save-changes").click(); + elt.find(".js-save-changes").shouldNot(exist); + return this; + } +} diff --git a/it/it-tests/src/test/java/pageobjects/settings/SettingsPage.java b/it/it-tests/src/test/java/pageobjects/settings/SettingsPage.java new file mode 100644 index 00000000000..24676e896e5 --- /dev/null +++ b/it/it-tests/src/test/java/pageobjects/settings/SettingsPage.java @@ -0,0 +1,84 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package pageobjects.settings; + +import com.codeborne.selenide.SelenideElement; +import org.openqa.selenium.By; + +import static com.codeborne.selenide.Condition.cssClass; +import static com.codeborne.selenide.Condition.exactValue; +import static com.codeborne.selenide.Condition.exist; +import static com.codeborne.selenide.Condition.visible; +import static com.codeborne.selenide.Selenide.$; + +public class SettingsPage { + + public SettingsPage() { + $("#settings-page").shouldBe(visible); + } + + public SettingsPage assertMenuContains(String categoryName) { + $(".settings-menu").$(By.linkText(categoryName)).shouldBe(visible); + return this; + } + + public SettingsPage assertSettingDisplayed(String settingKey) { + $(".settings-definition[data-key='" + settingKey + "']").shouldBe(visible); + return this; + } + + public SettingsPage assertSettingNotDisplayed(String settingKey) { + $(".settings-definition[data-key='" + settingKey + "']").shouldNotBe(visible); + return this; + } + + public SettingsPage openCategory(String categoryName) { + $(".settings-menu").$(By.linkText(categoryName)).click(); + return this; + } + + public SettingsPage assertStringSettingValue(String settingKey, String value) { + $("input[name=\"settings[" + settingKey + "]\"]").shouldHave(exactValue(value)); + return this; + } + + public SettingsPage assertBooleanSettingValue(String settingKey, boolean value) { + SelenideElement toggle = $("button[name=\"settings[" + settingKey + "]\"]"); + if (value) { + toggle.shouldHave(cssClass("boolean-toggle-on")); + } else { + toggle.shouldNotHave(cssClass("boolean-toggle-on")); + } + return this; + } + + public SettingsPage setStringValue(String settingKey, String value) { + SelenideElement setting = $(".settings-definition[data-key=\"" + settingKey + "\"]"); + setting.find("input").val(value); + setting.find(".js-save-changes").click(); + setting.find(".js-save-changes").shouldNot(exist); + return this; + } + + public PropertySetInput getPropertySetInput(String settingKey) { + SelenideElement setting = $(".settings-definition[data-key=\"" + settingKey + "\"]"); + return new PropertySetInput(setting); + } +} diff --git a/it/it-tests/src/test/resources/componentDashboard/DashboardTest/global_dashboard/global-admin-dashboards.html b/it/it-tests/src/test/resources/componentDashboard/DashboardTest/global_dashboard/global-admin-dashboards.html deleted file mode 100644 index 67911a11d8c..00000000000 --- a/it/it-tests/src/test/resources/componentDashboard/DashboardTest/global_dashboard/global-admin-dashboards.html +++ /dev/null @@ -1,89 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>global-admin-dashboards</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <thead> - <tr> - <td rowspan="1" colspan="3">global-admin-dashboards</td> - </tr> - </thead> - <tbody> - <tr> - <td>open</td> - <td>/sessions/login</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>login</td> - <td>admin</td> - </tr> - <tr> - <td>type</td> - <td>password</td> - <td>admin</td> - </tr> - <tr> - <td>clickAndWait</td> - <td>commit</td> - <td></td> - </tr> - <tr> - <td>open</td> - <td>/</td> - <td></td> - </tr> - <tr> - <td>waitForElementPresent</td> - <td>link=Home</td> - <td></td> - </tr> - <tr> - <td>clickAndWait</td> - <td>link=Home</td> - <td></td> - </tr> - <tr> - <td>clickAndWait</td> - <td>link=Configure widgets</td> - <td></td> - </tr> - <tr> - <td>clickAndWait</td> - <td>link=Back to dashboard</td> - <td></td> - </tr> - <tr> - <td>open</td> - <td>/dashboards</td> - <td></td> - </tr> - <tr> - <td>open</td> - <td>/settings/index</td> - <td></td> - </tr> - <tr> - <td>waitForElementPresent</td> - <td>link=Configuration</td> - <td></td> - </tr> - <tr> - <td>click</td> - <td>link=Configuration</td> - <td></td> - </tr> - <tr> - <td>clickAndWait</td> - <td>link=Default Dashboards</td> - <td></td> - </tr> - </tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/measureHistory/DifferentialPeriodsTest/define-leak-period-on-project.html b/it/it-tests/src/test/resources/measureHistory/DifferentialPeriodsTest/define-leak-period-on-project.html deleted file mode 100644 index 0bba5f6e3b3..00000000000 --- a/it/it-tests/src/test/resources/measureHistory/DifferentialPeriodsTest/define-leak-period-on-project.html +++ /dev/null @@ -1,49 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>display-added-files</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <thead> - <tr> - <td rowspan="1" colspan="3">should_display_added_files_in_differential_drilldown</td> - </tr> - </thead> - <tbody> - <tr> - <td>open</td> - <td>/sessions/logout</td> - <td></td> -</tr> -<tr> - <td>open</td> - <td>/project/settings?id=sample&category=general&subcategory=differentialviews</td> - <td></td> -</tr> -<tr> - <td>type</td> - <td>id=login</td> - <td>admin</td> -</tr> -<tr> - <td>type</td> - <td>id=password</td> - <td>admin</td> -</tr> -<tr> - <td>clickAndWait</td> - <td>name=commit</td> - <td></td> -</tr> -<tr> - <td>waitForText</td> - <td>properties</td> - <td>*Leak Period*</td> -</tr> -</tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/module-settings/display-module-settings.html b/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/module-settings/display-module-settings.html deleted file mode 100644 index 6d301d77dbd..00000000000 --- a/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/module-settings/display-module-settings.html +++ /dev/null @@ -1,49 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>override-global-settings</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <thead> - <tr> - <td rowspan="1" colspan="3">override-global-settings</td> - </tr> - </thead> - <tbody> - <tr> - <td>open</td> - <td>/sessions/logout</td> - <td></td> -</tr> -<tr> - <td>open</td> - <td>/project/settings/com.sonarsource.it.samples%3Amulti-modules-sample%3Amodule_a</td> - <td></td> -</tr> -<tr> - <td>type</td> - <td>id=login</td> - <td>admin</td> -</tr> -<tr> - <td>type</td> - <td>id=password</td> - <td>admin</td> -</tr> -<tr> - <td>clickAndWait</td> - <td>name=commit</td> - <td></td> -</tr> -<tr> - <td>waitForText</td> - <td>plugins</td> - <td>*Settings*</td> -</tr> -</tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-settings/only-on-project-settings.html b/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-settings/only-on-project-settings.html deleted file mode 100644 index 9197fa9c348..00000000000 --- a/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-settings/only-on-project-settings.html +++ /dev/null @@ -1,64 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>override-global-settings</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <thead> - <tr> - <td rowspan="1" colspan="3">override-global-settings</td> - </tr> - </thead> - <tbody> - <tr> - <td>open</td> - <td>/sessions/logout</td> - <td></td> - </tr> - <tr> - <td>open</td> - <td>/project/settings/sample?category=project-only</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>id=login</td> - <td>admin</td> - </tr> - <tr> - <td>type</td> - <td>id=password</td> - <td>admin</td> - </tr> - <tr> - <td>clickAndWait</td> - <td>name=commit</td> - <td></td> - </tr> - <tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>id=input_prop_only_on_project</td> - <td>foo</td> - </tr> - <tr> - <td>click</td> - <td>id=submit_settings</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>id=input_prop_only_on_project</td> - <td>foo</td> - </tr> - </tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-settings/override-global-settings.html b/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-settings/override-global-settings.html deleted file mode 100644 index a73b2ca1475..00000000000 --- a/it/it-tests/src/test/resources/projectAdministration/ProjectAdministrationTest/project-settings/override-global-settings.html +++ /dev/null @@ -1,64 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>override-global-settings</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <thead> - <tr> - <td rowspan="1" colspan="3">override-global-settings</td> - </tr> - </thead> - <tbody> - <tr> - <td>open</td> - <td>/sessions/logout</td> - <td></td> -</tr> -<tr> - <td>open</td> - <td>/project/settings/sample?category=exclusions&subcategory=files</td> - <td></td> -</tr> -<tr> - <td>type</td> - <td>id=login</td> - <td>admin</td> -</tr> -<tr> - <td>type</td> - <td>id=password</td> - <td>admin</td> -</tr> -<tr> - <td>clickAndWait</td> - <td>name=commit</td> - <td></td> -</tr> -<tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> -</tr> -<tr> - <td>type</td> - <td>id=input_sonar.exclusions</td> - <td>my-exclusions</td> -</tr> -<tr> - <td>click</td> - <td>id=submit_settings</td> - <td></td> -</tr> -<tr> - <td>waitForValue</td> - <td>id=input_sonar.exclusions</td> - <td>my-exclusions</td> -</tr> -</tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/serverSystem/ServerSystemTest/missing_ip.html b/it/it-tests/src/test/resources/serverSystem/ServerSystemTest/missing_ip.html index 1a9725f07a8..82e481ae7b9 100644 --- a/it/it-tests/src/test/resources/serverSystem/ServerSystemTest/missing_ip.html +++ b/it/it-tests/src/test/resources/serverSystem/ServerSystemTest/missing_ip.html @@ -60,22 +60,7 @@ </tr> <tr> <td>open</td> - <td>/settings/index</td> - <td></td> - </tr> - <tr> - <td>clickAndWait</td> - <td>link=Licenses</td> - <td></td> - </tr> - <tr> - <td>clickAndWait</td> - <td>link=Server ID</td> - <td></td> - </tr> - <tr> - <td>selectFrame</td> - <td>settings_iframe</td> + <td>/server_id_configuration</td> <td></td> </tr> <tr> diff --git a/it/it-tests/src/test/resources/serverSystem/ServerSystemTest/organisation_must_not_accept_special_chars.html b/it/it-tests/src/test/resources/serverSystem/ServerSystemTest/organisation_must_not_accept_special_chars.html index 29823121244..d9b98cf4ed7 100644 --- a/it/it-tests/src/test/resources/serverSystem/ServerSystemTest/organisation_must_not_accept_special_chars.html +++ b/it/it-tests/src/test/resources/serverSystem/ServerSystemTest/organisation_must_not_accept_special_chars.html @@ -35,22 +35,7 @@ </tr> <tr> <td>open</td> - <td>/settings/index</td> - <td></td> - </tr> - <tr> - <td>clickAndWait</td> - <td>link=Licenses</td> - <td></td> - </tr> - <tr> - <td>clickAndWait</td> - <td>link=Server ID</td> - <td></td> - </tr> - <tr> - <td>selectFrame</td> - <td>settings_iframe</td> + <td>/server_id_configuration</td> <td></td> </tr> <tr> diff --git a/it/it-tests/src/test/resources/serverSystem/ServerSystemTest/valid_id.html b/it/it-tests/src/test/resources/serverSystem/ServerSystemTest/valid_id.html index 4f86397d642..dabdb002dff 100644 --- a/it/it-tests/src/test/resources/serverSystem/ServerSystemTest/valid_id.html +++ b/it/it-tests/src/test/resources/serverSystem/ServerSystemTest/valid_id.html @@ -40,27 +40,7 @@ </tr> <tr> <td>open</td> - <td>/settings/index</td> - <td></td> - </tr> - <tr> - <td>clickAndWait</td> - <td>link=Licenses</td> - <td></td> - </tr> - <tr> - <td>clickAndWait</td> - <td>link=Server ID</td> - <td></td> - </tr> - <tr> - <td>waitForElementPresent</td> - <td>settings_iframe</td> - <td></td> - </tr> - <tr> - <td>selectFrame</td> - <td>settings_iframe</td> + <td>/server_id_configuration</td> <td></td> </tr> <tr> diff --git a/it/it-tests/src/test/resources/settings/PropertySetsTest/auto-generated/create.html b/it/it-tests/src/test/resources/settings/PropertySetsTest/auto-generated/create.html deleted file mode 100644 index a28e6426fdf..00000000000 --- a/it/it-tests/src/test/resources/settings/PropertySetsTest/auto-generated/create.html +++ /dev/null @@ -1,84 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>create</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <tbody> - <tr> - <td>open</td> - <td>/sessions/new</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>login</td> - <td>admin</td> - </tr> - <tr> - <td>type</td> - <td>password</td> - <td>admin</td> - </tr> - <tr> - <td>clickAndWait</td> - <td>commit</td> - <td></td> - </tr> - <tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> - </tr> - <tr> - <td>open</td> - <td>/settings?category=DEV</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>1</td> - </tr> - <tr> - <td>type</td> - <td>id=input_value</td> - <td>FIRST</td> - </tr> - <tr> - <td>click</td> - <td>css=button.add_value</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>xpath=(//input[@id='input_value'])[2]</td> - <td>SECOND</td> - </tr> - <tr> - <td>click</td> - <td>css=button.add_value</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>xpath=(//input[@id='input_value'])[3]</td> - <td>THIRD</td> - </tr> - <tr> - <td>click</td> - <td>id=submit_settings</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>2</td> - </tr> - </tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/settings/PropertySetsTest/auto-generated/update.html b/it/it-tests/src/test/resources/settings/PropertySetsTest/auto-generated/update.html deleted file mode 100644 index 2db17093913..00000000000 --- a/it/it-tests/src/test/resources/settings/PropertySetsTest/auto-generated/update.html +++ /dev/null @@ -1,69 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>update</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <tbody> - <tr> - <td>open</td> - <td>/sessions/new</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>login</td> - <td>admin</td> - </tr> - <tr> - <td>type</td> - <td>password</td> - <td>admin</td> - </tr> - <tr> - <td>clickAndWait</td> - <td>commit</td> - <td></td> - </tr> - <tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> - </tr> - <tr> - <td>open</td> - <td>/settings?category=DEV</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>1</td> - </tr> - <tr> - <td>click</td> - <td>link=Delete</td> - <td></td> - </tr> - <tr> - <td>waitForVisible</td> - <td>id=submit_settings</td> - <td></td> - </tr> - <tr> - <td>click</td> - <td>id=submit_settings</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>2</td> - </tr> - </tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/all_types.html b/it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/all_types.html deleted file mode 100644 index 4dfffa1d384..00000000000 --- a/it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/all_types.html +++ /dev/null @@ -1,139 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>all_types</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <tbody> - <tr> - <td>open</td> - <td>/sessions/new</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>login</td> - <td>admin</td> - </tr> - <tr> - <td>type</td> - <td>password</td> - <td>admin</td> - </tr> - <tr> - <td>clickAndWait</td> - <td>commit</td> - <td></td> - </tr> - <tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> - </tr> - <tr> - <td>open</td> - <td>/settings?category=DEV</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>1</td> - </tr> - <tr> - <td>type</td> - <td>id=input_text</td> - <td>text</td> - </tr> - <tr> - <td>select</td> - <td>id=input_boolean</td> - <td>label=True</td> - </tr> - <tr> - <td>type</td> - <td>id=input_float</td> - <td>42.0</td> - </tr> - <tr> - <td>type</td> - <td>id=input_license</td> - <td>abc</td> - </tr> - <tr> - <td>select</td> - <td>id=input_metric</td> - <td>label=Overall Condition Coverage</td> - </tr> - <tr> - <td>type</td> - <td>id=input_password</td> - <td>abcde</td> - </tr> - <tr> - <td>type</td> - <td>id=input_regexp</td> - <td>.*</td> - </tr> - <tr> - <td>select</td> - <td>id=input_list</td> - <td>label=AAA</td> - </tr> - <tr> - <td>click</td> - <td>id=submit_settings</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>2</td> - </tr> - <tr> - <td>assertValue</td> - <td>id=input_text</td> - <td>text</td> - </tr> - <tr> - <td>assertValue</td> - <td>id=input_boolean</td> - <td>true</td> - </tr> - <tr> - <td>assertValue</td> - <td>id=input_float</td> - <td>42.0</td> - </tr> - <tr> - <td>assertValue</td> - <td>id=input_license</td> - <td>abc</td> - </tr> - <tr> - <td>assertValue</td> - <td>id=input_metric</td> - <td>overall_branch_coverage</td> - </tr> - <tr> - <td>assertValue</td> - <td>id=input_password</td> - <td>{{*******************}}</td> - </tr> - <tr> - <td>assertValue</td> - <td>id=input_regexp</td> - <td>exact:.*</td> - </tr> - <tr> - <td>assertValue</td> - <td>id=input_list</td> - <td>AAA</td> - </tr> - </tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/create.html b/it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/create.html deleted file mode 100644 index d85e447c5c4..00000000000 --- a/it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/create.html +++ /dev/null @@ -1,89 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>create</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <tbody> - <tr> - <td>open</td> - <td>/sessions/new</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>login</td> - <td>admin</td> - </tr> - <tr> - <td>type</td> - <td>password</td> - <td>admin</td> - </tr> - <tr> - <td>clickAndWait</td> - <td>commit</td> - <td></td> - </tr> - <tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> - </tr> - <tr> - <td>open</td> - <td>/settings?category=DEV</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>1</td> - </tr> - <tr> - <td>type</td> - <td>id=input_key</td> - <td>jira1</td> - </tr> - <tr> - <td>type</td> - <td>id=input_url</td> - <td>http://jira</td> - </tr> - <tr> - <td>type</td> - <td>id=input_port</td> - <td>12345</td> - </tr> - <tr> - <td>click</td> - <td>id=submit_settings</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>2</td> - </tr> - <tr> - <td>assertValue</td> - <td>id=input_key</td> - <td>jira1</td> - </tr> - <tr> - <td>assertValue</td> - <td>id=input_url</td> - <td>exact:http://jira</td> - </tr> - <tr> - <td>assertValue</td> - <td>id=input_port</td> - <td>12345</td> - </tr> - </tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/delete.html b/it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/delete.html deleted file mode 100644 index aa0e9cacc6d..00000000000 --- a/it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/delete.html +++ /dev/null @@ -1,119 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>delete</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <tbody> - <tr> - <td>open</td> - <td>/sessions/new</td> - <td></td> -</tr> -<tr> - <td>type</td> - <td>login</td> - <td>admin</td> -</tr> -<tr> - <td>type</td> - <td>password</td> - <td>admin</td> -</tr> -<tr> - <td>clickAndWait</td> - <td>commit</td> - <td></td> -</tr> -<tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> -</tr> -<tr> - <td>open</td> - <td>/settings?category=DEV</td> - <td></td> -</tr> -<tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>1</td> -</tr> -<tr> - <td>type</td> - <td>id=input_key</td> - <td>jira1</td> -</tr> -<tr> - <td>type</td> - <td>id=input_url</td> - <td>http://jira1</td> -</tr> -<tr> - <td>click</td> - <td>css=#block_sonar\.test\.jira\.servers .add_value</td> - <td></td> -</tr> -<tr> - <td>type</td> - <td>xpath=(//input[@id='input_key'])[2]</td> - <td>jira2</td> -</tr> -<tr> - <td>type</td> - <td>xpath=(//input[@id='input_url'])[2]</td> - <td>http://jira2</td> -</tr> -<tr> - <td>click</td> - <td>id=submit_settings</td> - <td></td> -</tr> -<tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>2</td> -</tr> -<tr> - <td>assertValue</td> - <td>xpath=(//input[@id='input_key'])[1]</td> - <td>jira1</td> -</tr> -<tr> - <td>assertValue</td> - <td>xpath=(//input[@id='input_key'])[2]</td> - <td>jira2</td> -</tr> -<tr> - <td>click</td> - <td>xpath=(//a[contains(text(),'Delete')])[3]</td> - <td></td> -</tr> -<tr> - <td>click</td> - <td>id=submit_settings</td> - <td></td> -</tr> -<tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>3</td> -</tr> -<tr> - <td>assertValue</td> - <td>xpath=(//input[@id='input_key'])[1]</td> - <td>jira1</td> -</tr> -<tr> - <td>waitForNotText</td> - <td>xpath=(//input[@id='input_key'])</td> - <td>*jira2*</td> -</tr> -</tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/reference.html b/it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/reference.html deleted file mode 100644 index c212340711f..00000000000 --- a/it/it-tests/src/test/resources/settings/PropertySetsTest/property-sets/reference.html +++ /dev/null @@ -1,94 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>reference</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <tbody> - <tr> - <td>open</td> - <td>/sessions/new</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>login</td> - <td>admin</td> - </tr> - <tr> - <td>type</td> - <td>password</td> - <td>admin</td> - </tr> - <tr> - <td>clickAndWait</td> - <td>commit</td> - <td></td> - </tr> - <tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> - </tr> - <tr> - <td>open</td> - <td>/settings?category=DEV</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>1</td> - </tr> - <tr> - <td>type</td> - <td>xpath=(//input[@id='input_key'])[1]</td> - <td>jira1</td> - </tr> - <tr> - <td>click</td> - <td>css=#block_sonar\.test\.jira\.servers .add_value</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>xpath=(//input[@id='input_key'])[2]</td> - <td>jira2</td> - </tr> - <tr> - <td>click</td> - <td>id=submit_settings</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>2</td> - </tr> - <tr> - <td>assertSelectOptions</td> - <td>id=input_sonar.test.jira</td> - <td>Default,jira1,jira2</td> - </tr> - <tr> - <td>click</td> - <td>xpath=(//a[contains(text(),'Delete')])[2]</td> - <td></td> - </tr> - <tr> - <td>click</td> - <td>id=submit_settings</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>3</td> - </tr> - </tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/settings/SettingsTest/encrypt-text.html b/it/it-tests/src/test/resources/settings/SettingsTest/encrypt-text.html index fe174478d54..d1258d789ac 100644 --- a/it/it-tests/src/test/resources/settings/SettingsTest/encrypt-text.html +++ b/it/it-tests/src/test/resources/settings/SettingsTest/encrypt-text.html @@ -15,7 +15,7 @@ </tr> <tr> <td>open</td> - <td>/settings?category=security&subcategory=encryption</td> + <td>/encryption_configuration</td> <td></td> </tr> <tr> @@ -35,12 +35,7 @@ </tr> <tr> <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> - </tr> - <tr> - <td>selectFrame</td> - <td>settings_iframe</td> + <td>css=#submit_encrypt</td> <td></td> </tr> <tr> diff --git a/it/it-tests/src/test/resources/settings/SettingsTest/general-settings.html b/it/it-tests/src/test/resources/settings/SettingsTest/general-settings.html deleted file mode 100644 index 89061e138ba..00000000000 --- a/it/it-tests/src/test/resources/settings/SettingsTest/general-settings.html +++ /dev/null @@ -1,59 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>settings_on_core_plugins</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <thead> - <tr> - <td rowspan="1" colspan="3">settings_on_core_plugins</td> - </tr> - </thead> - <tbody> - <tr> - <td>open</td> - <td>/sessions/logout</td> - <td></td> -</tr> -<tr> - <td>open</td> - <td>/sessions/new</td> - <td></td> -</tr> -<tr> - <td>type</td> - <td>login</td> - <td>admin</td> -</tr> -<tr> - <td>type</td> - <td>password</td> - <td>admin</td> -</tr> -<tr> - <td>clickAndWait</td> - <td>commit</td> - <td></td> -</tr> -<tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> -</tr> -<tr> - <td>open</td> - <td>/settings/index</td> - <td></td> -</tr> -<tr> - <td>waitForText</td> - <td>plugins</td> - <td>*General*</td> -</tr> -</tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/settings/SettingsTest/generate-secret-key.html b/it/it-tests/src/test/resources/settings/SettingsTest/generate-secret-key.html index c667bf3ca0b..dafe8ccfe82 100644 --- a/it/it-tests/src/test/resources/settings/SettingsTest/generate-secret-key.html +++ b/it/it-tests/src/test/resources/settings/SettingsTest/generate-secret-key.html @@ -15,7 +15,7 @@ </tr> <tr> <td>open</td> - <td>/settings?category=security&subcategory=encryption</td> + <td>/encryption_configuration</td> <td></td> </tr> <tr> @@ -35,12 +35,7 @@ </tr> <tr> <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> - </tr> - <tr> - <td>selectFrame</td> - <td>settings_iframe</td> + <td>css=#submit_encrypt</td> <td></td> </tr> <tr> diff --git a/it/it-tests/src/test/resources/settings/SettingsTest/global-extension-property.html b/it/it-tests/src/test/resources/settings/SettingsTest/global-extension-property.html deleted file mode 100644 index ad3d97682b3..00000000000 --- a/it/it-tests/src/test/resources/settings/SettingsTest/global-extension-property.html +++ /dev/null @@ -1,55 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>global-extension-property</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <thead> - <tr> - <td rowspan="1" colspan="3">global-extension-property</td> - </tr> - </thead> - <tbody> - <tr> - <td>open</td> - <td>/sessions/new</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>login</td> - <td>admin</td> - </tr> - <tr> - <td>type</td> - <td>password</td> - <td>admin</td> - </tr> - <tr> - <td>clickAndWait</td> - <td>commit</td> - <td></td> - </tr> - <tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> - </tr> - <tr> - <td>open</td> - <td>/settings?category=Settings</td> - <td></td> - </tr> - <tr> - <td>assertNotText</td> - <td>plugins</td> - <td>glob:*Hidden*</td> - </tr> - - </tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/settings/SettingsTest/hidden-extension-property.html b/it/it-tests/src/test/resources/settings/SettingsTest/hidden-extension-property.html deleted file mode 100644 index 7aa9bd9cba1..00000000000 --- a/it/it-tests/src/test/resources/settings/SettingsTest/hidden-extension-property.html +++ /dev/null @@ -1,55 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>hidden-extension-property</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <thead> - <tr> - <td rowspan="1" colspan="3">hidden-extension-property</td> - </tr> - </thead> - <tbody> - <tr> - <td>open</td> - <td>/sessions/new</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>login</td> - <td>admin</td> - </tr> - <tr> - <td>type</td> - <td>password</td> - <td>admin</td> - </tr> - <tr> - <td>clickAndWait</td> - <td>commit</td> - <td></td> - </tr> - <tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> - </tr> - <tr> - <td>open</td> - <td>/settings?category=Settings</td> - <td></td> - </tr> - <tr> - <td>assertNotText</td> - <td>plugins</td> - <td>glob:*Hidden*</td> - </tr> - - </tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/settings/SettingsTest/hide-passwords.html b/it/it-tests/src/test/resources/settings/SettingsTest/hide-passwords.html deleted file mode 100644 index 531e11e8bd4..00000000000 --- a/it/it-tests/src/test/resources/settings/SettingsTest/hide-passwords.html +++ /dev/null @@ -1,54 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>hide-passwords</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <tbody> - <tr> - <td>open</td> - <td>/sessions/logout</td> - <td></td> - </tr> - <tr> - <td>open</td> - <td>/settings?category=Settings</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>id=login</td> - <td>admin</td> - </tr> - <tr> - <td>type</td> - <td>id=password</td> - <td>admin</td> - </tr> - <tr> - <td>clickAndWait</td> - <td>name=commit</td> - <td></td> - </tr> - <tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> - </tr> - <tr> - <td>waitForText</td> - <td>block_password</td> - <td>*Default*</td> - </tr> - <tr> - <td>assertNotText</td> - <td>block_password</td> - <td>*Default*sonar*</td> - </tr> - </tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/settings/SettingsTest/property_relocation.html b/it/it-tests/src/test/resources/settings/SettingsTest/property_relocation.html deleted file mode 100644 index 37986cd560d..00000000000 --- a/it/it-tests/src/test/resources/settings/SettingsTest/property_relocation.html +++ /dev/null @@ -1,54 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>property-relocation</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <tbody> - <tr> - <td>open</td> - <td>/sessions/logout</td> - <td></td> -</tr> -<tr> - <td>open</td> - <td>/settings/index?category=general</td> - <td></td> -</tr> -<tr> - <td>type</td> - <td>id=login</td> - <td>admin</td> -</tr> -<tr> - <td>type</td> - <td>id=password</td> - <td>admin</td> -</tr> -<tr> - <td>clickAndWait</td> - <td>name=commit</td> - <td></td> -</tr> -<tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> -</tr> -<tr> - <td>waitForText</td> - <td>properties</td> - <td>*sonar.newKey*</td> -</tr> -<tr> - <td>assertNotText</td> - <td>properties</td> - <td>*sonar.deprecatedKey*</td> -</tr> -</tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/settings/SettingsTest/validate-property-type.html b/it/it-tests/src/test/resources/settings/SettingsTest/validate-property-type.html deleted file mode 100644 index 96024f79b91..00000000000 --- a/it/it-tests/src/test/resources/settings/SettingsTest/validate-property-type.html +++ /dev/null @@ -1,79 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>validate-property-type</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <tbody> - <tr> - <td>open</td> - <td>/sessions/logout</td> - <td></td> - </tr> - <tr> - <td>open</td> - <td>/settings?category=Settings</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>id=login</td> - <td>admin</td> - </tr> - <tr> - <td>type</td> - <td>id=password</td> - <td>admin</td> - </tr> - <tr> - <td>clickAndWait</td> - <td>name=commit</td> - <td></td> - </tr> - <tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>id=input_float</td> - <td>abc</td> - </tr> - <tr> - <td>type</td> - <td>id=input_integer</td> - <td>123</td> - </tr> - <tr> - <td>click</td> - <td>id=submit_settings</td> - <td></td> - </tr> - <tr> - <td>waitForText</td> - <td>properties</td> - <td>*Not a floating point number*</td> - </tr> - <tr> - <td>assertValue</td> - <td>input_integer</td> - <td>123</td> - </tr> - <tr> - <td>open</td> - <td>/settings?category=Settings</td> - <td></td> - </tr> - <tr> - <td>assertValue</td> - <td>input_float</td> - <td></td> - </tr> - </tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/settings/subcategories/global-subcategories-no-default.html b/it/it-tests/src/test/resources/settings/subcategories/global-subcategories-no-default.html deleted file mode 100644 index a4b69712397..00000000000 --- a/it/it-tests/src/test/resources/settings/subcategories/global-subcategories-no-default.html +++ /dev/null @@ -1,60 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>global-subcategories</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <tbody> - <tr> - <td>open</td> - <td>/sessions/new</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>login</td> - <td>admin</td> - </tr> - <tr> - <td>type</td> - <td>password</td> - <td>admin</td> - </tr> - <tr> - <td>clickAndWait</td> - <td>commit</td> - <td></td> - </tr> - <tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> - </tr> - <tr> - <td>open</td> - <td>/settings?category=Category 2</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>1</td> - </tr> - <!-- First subcategory should be selected by default --> - <tr> - <td>assertElementPresent</td> - <td>id=input_prop2_1</td> - <td></td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop2_2</td> - <td></td> - </tr> - </tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/settings/subcategories/global-subcategories.html b/it/it-tests/src/test/resources/settings/subcategories/global-subcategories.html deleted file mode 100644 index 3f578222762..00000000000 --- a/it/it-tests/src/test/resources/settings/subcategories/global-subcategories.html +++ /dev/null @@ -1,156 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>global-subcategories</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <tbody> - <tr> - <td>open</td> - <td>/sessions/new</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>login</td> - <td>admin</td> - </tr> - <tr> - <td>type</td> - <td>password</td> - <td>admin</td> - </tr> - <tr> - <td>clickAndWait</td> - <td>commit</td> - <td></td> - </tr> - <tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> - </tr> - <tr> - <td>open</td> - <td>/settings?category=Category 1</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>1</td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop1</td> - <td></td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop2</td> - <td></td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop3</td> - <td></td> - </tr> - <tr> - <td>assertElementPresent</td> - <td>id=input_prop4</td> - <td></td> - </tr> - <tr> - <td>clickAndWait</td> - <td>link=Sub category 1</td> - <td></td> - </tr> - <tr> - <td>assertElementPresent</td> - <td>id=input_prop1</td> - <td></td> - </tr> - <tr> - <td>assertElementPresent</td> - <td>id=input_prop2</td> - <td></td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop3</td> - <td></td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop4</td> - <td></td> - </tr> - <!-- Verify index attribute is taken into account --> - <tr> - <td>assertElementPresent</td> - <td>xpath=//.[@id='input_prop2']/following::input[@id='input_prop1']</td> - <td></td> - </tr> - <tr> - <td>clickAndWait</td> - <td>link=Sub category 2</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>id=input_prop3</td> - <td>myValue</td> - </tr> - <tr> - <td>click</td> - <td>id=submit_settings</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>2</td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop1</td> - <td></td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop2</td> - <td></td> - </tr> - <tr> - <td>assertElementPresent</td> - <td>id=input_prop3</td> - <td></td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop4</td> - <td></td> - </tr> - <tr> - <td>assertValue</td> - <td>id=input_prop3</td> - <td>myValue</td> - </tr> - <!-- SONAR-4473 --> - <tr> - <td>clickAndWait</td> - <td>link=Sub category 1</td> - <td></td> - </tr> - <tr> - <td>assertElementPresent</td> - <td>id=input_prop1</td> - <td></td> - </tr> - </tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/settings/subcategories/project-subcategories-no-default.html b/it/it-tests/src/test/resources/settings/subcategories/project-subcategories-no-default.html deleted file mode 100644 index f0efe3fcf35..00000000000 --- a/it/it-tests/src/test/resources/settings/subcategories/project-subcategories-no-default.html +++ /dev/null @@ -1,60 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>create</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <tbody> - <tr> - <td>open</td> - <td>/sessions/new</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>login</td> - <td>admin</td> - </tr> - <tr> - <td>type</td> - <td>password</td> - <td>admin</td> - </tr> - <tr> - <td>clickAndWait</td> - <td>commit</td> - <td></td> - </tr> - <tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> - </tr> - <tr> - <td>open</td> - <td>/project/settings/sample?category=Category 2</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>1</td> - </tr> - <!-- First subcategory should be selected by default --> - <tr> - <td>assertElementPresent</td> - <td>id=input_prop2_1</td> - <td></td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop2_2</td> - <td></td> - </tr> - </tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/settings/subcategories/project-subcategories.html b/it/it-tests/src/test/resources/settings/subcategories/project-subcategories.html deleted file mode 100644 index 1d6b7efc0c7..00000000000 --- a/it/it-tests/src/test/resources/settings/subcategories/project-subcategories.html +++ /dev/null @@ -1,150 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head profile="http://selenium-ide.openqa.org/profiles/test-case"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <title>create</title> -</head> -<body> -<table cellpadding="1" cellspacing="1" border="1"> - <tbody> - <tr> - <td>open</td> - <td>/sessions/new</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>login</td> - <td>admin</td> - </tr> - <tr> - <td>type</td> - <td>password</td> - <td>admin</td> - </tr> - <tr> - <td>clickAndWait</td> - <td>commit</td> - <td></td> - </tr> - <tr> - <td>waitForElementPresent</td> - <td>css=.js-user-authenticated</td> - <td></td> - </tr> - <tr> - <td>open</td> - <td>/project/settings/sample?category=Category 1</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>1</td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop1</td> - <td></td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop2</td> - <td></td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop3</td> - <td></td> - </tr> - <tr> - <td>assertElementPresent</td> - <td>id=input_prop4</td> - <td></td> - </tr> - <tr> - <td>clickAndWait</td> - <td>link=Sub category 1</td> - <td></td> - </tr> - <tr> - <td>assertElementPresent</td> - <td>id=input_prop1</td> - <td></td> - </tr> - <tr> - <td>assertElementPresent</td> - <td>id=input_prop2</td> - <td></td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop3</td> - <td></td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop4</td> - <td></td> - </tr> - <tr> - <td>clickAndWait</td> - <td>link=Sub category 2</td> - <td></td> - </tr> - <tr> - <td>type</td> - <td>id=input_prop3</td> - <td>myValue2</td> - </tr> - <tr> - <td>click</td> - <td>id=submit_settings</td> - <td></td> - </tr> - <tr> - <td>waitForValue</td> - <td>name=page_version</td> - <td>2</td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop1</td> - <td></td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop2</td> - <td></td> - </tr> - <tr> - <td>assertElementPresent</td> - <td>id=input_prop3</td> - <td></td> - </tr> - <tr> - <td>assertElementNotPresent</td> - <td>id=input_prop4</td> - <td></td> - </tr> - <tr> - <td>assertValue</td> - <td>id=input_prop3</td> - <td>myValue2</td> - </tr> - <!-- SONAR-4473 --> - <tr> - <td>clickAndWait</td> - <td>link=Sub category 1</td> - <td></td> - </tr> - <tr> - <td>assertElementPresent</td> - <td>id=input_prop1</td> - <td></td> - </tr> - </tbody> -</table> -</body> -</html> diff --git a/it/it-tests/src/test/resources/updateCenter/installed-plugins.html b/it/it-tests/src/test/resources/updateCenter/installed-plugins.html index 05c21066451..01c518c4f9f 100644 --- a/it/it-tests/src/test/resources/updateCenter/installed-plugins.html +++ b/it/it-tests/src/test/resources/updateCenter/installed-plugins.html @@ -15,7 +15,7 @@ </tr> <tr> <td>open</td> - <td>/settings</td> + <td>/updatecenter</td> <td></td> </tr> <tr> @@ -39,11 +39,6 @@ <td></td> </tr> <tr> - <td>open</td> - <td>/updatecenter</td> - <td></td> - </tr> - <tr> <td>waitForText</td> <td>content</td> <td>*Fake*</td> diff --git a/it/it-tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html b/it/it-tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html index ef3618524db..9683c351923 100644 --- a/it/it-tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html +++ b/it/it-tests/src/test/resources/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html @@ -14,7 +14,7 @@ </tr> <tr> <td>open</td> - <td>/settings/index</td> + <td>/settings</td> <td></td> </tr> <tr> @@ -54,12 +54,7 @@ </tr> <tr> <td>assertLocation</td> - <td>*/settings/index</td> - <td></td> - </tr> - <tr> - <td>assertElementPresent</td> - <td>plugins</td> + <td>*/settings</td> <td></td> </tr> </tbody> diff --git a/server/sonar-web/config/webpack/webpack.config.base.js b/server/sonar-web/config/webpack/webpack.config.base.js index 0533884f245..2e0dcc48236 100644 --- a/server/sonar-web/config/webpack/webpack.config.base.js +++ b/server/sonar-web/config/webpack/webpack.config.base.js @@ -45,6 +45,7 @@ module.exports = { 'projects': './src/main/js/apps/projects/app.js', 'quality-gates': './src/main/js/apps/quality-gates/app.js', 'quality-profiles': './src/main/js/apps/quality-profiles/app.js', + 'settings': './src/main/js/apps/settings/app.js', 'source-viewer': './src/main/js/apps/source-viewer/app.js', 'system': './src/main/js/apps/system/app.js', 'update-center': './src/main/js/apps/update-center/app.js', diff --git a/server/sonar-web/scripts/start.js b/server/sonar-web/scripts/start.js index ceb9d61928a..905793318e2 100644 --- a/server/sonar-web/scripts/start.js +++ b/server/sonar-web/scripts/start.js @@ -121,14 +121,10 @@ function setupCompiler () { function runDevServer (port) { app.use(require('webpack-dev-middleware')(compiler, { noInfo: true, - quiet: true, publicPath: config.output.publicPath })); - app.use(require('webpack-hot-middleware')(compiler, { - noInfo: true, - quiet: true - })); + app.use(require('webpack-hot-middleware')(compiler)); app.all('*', proxy(API_HOST, { forwardPath: function (req) { diff --git a/server/sonar-web/src/main/js/api/settings.js b/server/sonar-web/src/main/js/api/settings.js new file mode 100644 index 00000000000..14396243a2f --- /dev/null +++ b/server/sonar-web/src/main/js/api/settings.js @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import omitBy from 'lodash/omitBy'; +import { getJSON, post } from '../helpers/request'; +import { TYPE_PROPERTY_SET } from '../apps/settings/constants'; + +export function getDefinitions (componentKey) { + const url = '/api/settings/list_definitions'; + const data = componentKey ? { componentKey } : {}; + return getJSON(url, data).then(r => r.definitions); +} + +export function getValues (keys, componentKey) { + const url = '/api/settings/values'; + const data = { keys }; + if (componentKey) { + data.componentKey = componentKey; + } + return getJSON(url, data).then(r => r.settings); +} + +export function setSettingValue (definition, value, componentKey) { + const url = '/api/settings/set'; + + const { key } = definition; + const data = { key }; + + if (definition.multiValues) { + data.values = value; + } else if (definition.type === TYPE_PROPERTY_SET) { + data.fieldValues = value + .map(fields => omitBy(fields, value => value == null)) + .map(JSON.stringify); + } else { + data.value = value; + } + + if (componentKey) { + data.componentKey = componentKey; + } + + return post(url, data); +} + +export function resetSettingValue (key, componentKey) { + const url = '/api/settings/reset'; + const data = { key }; + if (componentKey) { + data.componentKey = componentKey; + } + return post(url, data); +} diff --git a/server/sonar-web/src/main/js/api/users.js b/server/sonar-web/src/main/js/api/users.js index 80865674d80..e5d5d15f7bc 100644 --- a/server/sonar-web/src/main/js/api/users.js +++ b/server/sonar-web/src/main/js/api/users.js @@ -39,3 +39,9 @@ export function getIdentityProviders () { const url = '/api/users/identity_providers'; return getJSON(url); } + +export function searchUsers (query) { + const url = '/api/users/search'; + const data = { q: query }; + return getJSON(url, data); +} diff --git a/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.js b/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.js new file mode 100644 index 00000000000..16b709d99ab --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.js @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { expect } from 'chai'; +import { getEmptyValue } from '../utils'; +import { TYPE_PROPERTY_SET, TYPE_STRING, TYPE_SINGLE_SELECT_LIST, TYPE_BOOLEAN } from '../constants'; + +const fields = [ + { key: 'foo', type: TYPE_STRING }, + { key: 'bar', type: TYPE_SINGLE_SELECT_LIST } +]; + +describe('Settings :: Utils', () => { + describe('#getEmptyValue()', () => { + it('should work for property sets', () => { + const setting = { type: TYPE_PROPERTY_SET, fields }; + expect(getEmptyValue(setting)).to.deep.equal([{ foo: '', bar: null }]); + }); + + it('should work for multi values string', () => { + const setting = { type: TYPE_STRING, multiValues: true }; + expect(getEmptyValue(setting)).to.deep.equal(['']); + }); + + it('should work for multi values boolean', () => { + const setting = { type: TYPE_BOOLEAN, multiValues: true }; + expect(getEmptyValue(setting)).to.deep.equal([null]); + }); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/app.js b/server/sonar-web/src/main/js/apps/settings/app.js new file mode 100644 index 00000000000..cf9dfcf07ad --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/app.js @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { render } from 'react-dom'; +import { Provider } from 'react-redux'; +import { Router, Route, Redirect, useRouterHistory } from 'react-router'; +import { createHistory } from 'history'; +import App from './components/App'; +import rootReducer from './store/rootReducer'; +import configureStore from '../../components/store/configureStore'; + +window.sonarqube.appStarted.then(options => { + const el = document.querySelector(options.el); + + const controller = options.component ? '/project/settings' : '/settings'; + const history = useRouterHistory(createHistory)({ + basename: window.baseUrl + controller + }); + + const store = configureStore(rootReducer); + + const withComponent = ComposedComponent => props => + <ComposedComponent {...props} component={options.component}/>; + + render(( + <Provider store={store}> + <Router history={history}> + <Redirect from="/index" to="/"/> + <Route path="/" component={withComponent(App)}/> + </Router> + </Provider> + ), el); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.js b/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.js new file mode 100644 index 00000000000..44d16ad6bdc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.js @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { connect } from 'react-redux'; +import CategoriesList from './CategoriesList'; +import { getAllCategories } from '../store/rootReducer'; + +class AllCategoriesList extends React.Component { + render () { + return <CategoriesList {...this.props}/>; + } +} + +const mapStateToProps = state => ({ + categories: getAllCategories(state) +}); + +export default connect( + mapStateToProps +)(AllCategoriesList); diff --git a/server/sonar-web/src/main/js/apps/settings/components/App.js b/server/sonar-web/src/main/js/apps/settings/components/App.js new file mode 100644 index 00000000000..424675ba502 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/App.js @@ -0,0 +1,101 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import { connect } from 'react-redux'; +import PageHeader from './PageHeader'; +import CategoryDefinitionsList from './CategoryDefinitionsList'; +import AllCategoriesList from './AllCategoriesList'; +import GlobalMessagesContainer from './GlobalMessagesContainer'; +import { fetchSettings } from '../store/actions'; +import { getDefaultCategory } from '../store/rootReducer'; +import '../styles.css'; + +class App extends React.Component { + static propTypes = { + component: React.PropTypes.object, + fetchSettings: React.PropTypes.func.isRequired, + defaultCategory: React.PropTypes.string + }; + + state = { loaded: false }; + + componentDidMount () { + document.querySelector('html').classList.add('dashboard-page'); + const componentKey = this.props.component ? this.props.component.key : null; + this.props.fetchSettings(componentKey).then(() => { + this.setState({ loaded: true }); + }); + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + componentDidUpdate (prevProps) { + if (prevProps.component !== this.props.component) { + const componentKey = this.props.component ? this.props.component.key : null; + this.props.fetchSettings(componentKey); + } + } + + componentWillUnmount () { + document.querySelector('html').classList.remove('dashboard-page'); + } + + render () { + if (!this.state.loaded) { + return null; + } + + const { query } = this.props.location; + const selectedCategory = query.category || this.props.defaultCategory; + + return ( + <div id="settings-page" className="page page-limited"> + <PageHeader component={this.props.component}/> + <GlobalMessagesContainer/> + <div className="settings-layout"> + <div className="settings-side"> + <AllCategoriesList + component={this.props.component} + selectedCategory={selectedCategory} + defaultCategory={this.props.defaultCategory}/> + </div> + <div className="settings-main"> + <CategoryDefinitionsList + component={this.props.component} + category={selectedCategory}/> + </div> + </div> + </div> + ); + } +} + +const mapStateToProps = state => ({ + defaultCategory: getDefaultCategory(state) +}); + +export default connect( + mapStateToProps, + { fetchSettings } +)(App); + diff --git a/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js b/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js new file mode 100644 index 00000000000..aa833d5ce90 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import sortBy from 'lodash/sortBy'; +import { IndexLink } from 'react-router'; +import { getCategoryName } from '../utils'; + +export default class CategoriesList extends React.Component { + static propTypes = { + categories: React.PropTypes.array.isRequired, + selectedCategory: React.PropTypes.string.isRequired, + defaultCategory: React.PropTypes.string.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + renderLink (category) { + const query = {}; + + if (category.key !== this.props.defaultCategory) { + query.category = category.key.toLowerCase(); + } + + if (this.props.component) { + query.id = this.props.component.key; + } + + const className = category.key.toLowerCase() === this.props.selectedCategory.toLowerCase() ? 'active' : ''; + + return ( + <IndexLink to={{ pathname: '/', query }} className={className} title={category.name}> + {category.name} + </IndexLink> + ); + } + + render () { + const categoriesWithName = this.props.categories.map(key => ({ key, name: getCategoryName(key) })); + const sortedCategories = sortBy(categoriesWithName, category => category.name.toLowerCase()); + + return ( + <ul className="settings-menu"> + {sortedCategories.map(category => ( + <li key={category.key}> + {this.renderLink(category)} + </li> + ))} + </ul> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.js b/server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.js new file mode 100644 index 00000000000..cc2e6d8c24e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.js @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { connect } from 'react-redux'; +import SubCategoryDefinitionsList from './SubCategoryDefinitionsList'; +import { getSettingsForCategory } from '../store/rootReducer'; + +class CategoryDefinitionsList extends React.Component { + render () { + return <SubCategoryDefinitionsList {...this.props}/>; + } +} + +const mapStateToProps = (state, ownProps) => ({ + settings: getSettingsForCategory(state, ownProps.category) +}); + +export default connect( + mapStateToProps +)(CategoryDefinitionsList); diff --git a/server/sonar-web/src/main/js/apps/settings/components/Definition.js b/server/sonar-web/src/main/js/apps/settings/components/Definition.js new file mode 100644 index 00000000000..66b19fd023a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/Definition.js @@ -0,0 +1,195 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { connect } from 'react-redux'; +import shallowCompare from 'react-addons-shallow-compare'; +import classNames from 'classnames'; +import Input from './inputs/Input'; +import DefinitionDefaults from './DefinitionDefaults'; +import DefinitionChanges from './DefinitionChanges'; +import { getPropertyName, getPropertyDescription, isEmptyValue, getSettingValue, isDefaultOrInherited } from '../utils'; +import { translateWithParameters, translate } from '../../../helpers/l10n'; +import { resetValue, saveValue } from '../store/actions'; +import { isLoading, getValidationMessage, getChangedValue } from '../store/rootReducer'; +import { failValidation, passValidation } from '../store/settingsPage/validationMessages/actions'; +import { cancelChange, changeValue } from '../store/settingsPage/changedValues/actions'; + +class Definition extends React.Component { + static propTypes = { + component: React.PropTypes.object, + setting: React.PropTypes.object.isRequired, + changedValue: React.PropTypes.any, + loading: React.PropTypes.bool.isRequired, + validationMessage: React.PropTypes.string, + + changeValue: React.PropTypes.func.isRequired, + cancelChange: React.PropTypes.func.isRequired, + saveValue: React.PropTypes.func.isRequired, + resetValue: React.PropTypes.func.isRequired, + failValidation: React.PropTypes.func.isRequired, + passValidation: React.PropTypes.func.isRequired + }; + + state = { + success: false + }; + + componentDidMount () { + this.mounted = true; + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + componentWillUnmount () { + this.mounted = false; + } + + safeSetState (changes) { + if (this.mounted) { + this.setState(changes); + } + } + + handleChange (value) { + clearTimeout(this.timeout); + return this.props.changeValue(this.props.setting.definition.key, value); + } + + handleReset () { + const componentKey = this.props.component ? this.props.component.key : null; + const { definition } = this.props.setting; + return this.props.resetValue(definition.key, componentKey).then(() => { + this.safeSetState({ success: true }); + this.timeout = setTimeout(() => this.safeSetState({ success: false }), 3000); + }).catch(() => { /* do nothing */ }); + } + + handleCancel () { + this.props.cancelChange(this.props.setting.definition.key); + this.props.passValidation(this.props.setting.definition.key); + } + + handleSave () { + this.safeSetState({ success: false }); + const { definition } = this.props.setting; + if (isEmptyValue(definition, this.props.changedValue)) { + this.props.failValidation(definition.key, translate('settings.state.value_cant_be_empty')); + return; + } + + const componentKey = this.props.component ? this.props.component.key : null; + this.props.saveValue(this.props.setting.definition.key, componentKey).then(() => { + this.safeSetState({ success: true }); + this.timeout = setTimeout(() => this.safeSetState({ success: false }), 3000); + }).catch(() => { /* do nothing */ }); + } + + render () { + const { setting, changedValue, loading } = this.props; + const { definition } = setting; + const propertyName = getPropertyName(definition); + + const hasValueChanged = changedValue != null; + + const className = classNames('settings-definition', { + 'settings-definition-changed': hasValueChanged + }); + + const effectiveValue = hasValueChanged ? changedValue : getSettingValue(setting); + + const isDefault = isDefaultOrInherited(setting) && !hasValueChanged; + + return ( + <div className={className} data-key={definition.key}> + <div className="settings-definition-left"> + <h3 className="settings-definition-name" title={propertyName}> + {propertyName} + </h3> + + <div className="settings-definition-description markdown small spacer-top" + dangerouslySetInnerHTML={{ __html: getPropertyDescription(definition) }}/> + + <div className="settings-definition-key note little-spacer-top"> + {translateWithParameters('settings.key_x', definition.key)} + </div> + </div> + + <div className="settings-definition-right"> + <Input setting={setting} value={effectiveValue} onChange={this.handleChange.bind(this)}/> + + {!hasValueChanged && ( + <DefinitionDefaults + setting={setting} + isDefault={isDefault} + onReset={() => this.handleReset()}/> + )} + + {hasValueChanged && ( + <DefinitionChanges + onSave={this.handleSave.bind(this)} + onCancel={this.handleCancel.bind(this)}/> + )} + + <div className="settings-definition-state"> + {loading && ( + <span className="text-info"> + <span className="settings-definition-state-icon"> + <i className="spinner"/> + </span> + {translate('settings.state.saving')} + </span> + )} + + {!loading && (this.props.validationMessage != null) && ( + <span className="text-danger"> + <span className="settings-definition-state-icon"> + <i className="icon-alert-error"/> + </span> + {translateWithParameters('settings.state.validation_failed', this.props.validationMessage)} + </span> + )} + + {!loading && this.state.success && ( + <span className="text-success"> + <span className="settings-definition-state-icon"> + <i className="icon-check"/> + </span> + {translate('settings.state.saved')} + </span> + )} + </div> + </div> + </div> + ); + } +} + +const mapStateToProps = (state, ownProps) => ({ + changedValue: getChangedValue(state, ownProps.setting.definition.key), + loading: isLoading(state, ownProps.setting.definition.key), + validationMessage: getValidationMessage(state, ownProps.setting.definition.key) +}); + +export default connect( + mapStateToProps, + { changeValue, saveValue, resetValue, failValidation, passValidation, cancelChange } +)(Definition); diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionChanges.js b/server/sonar-web/src/main/js/apps/settings/components/DefinitionChanges.js new file mode 100644 index 00000000000..edbeafecf9e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionChanges.js @@ -0,0 +1,59 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import { translate } from '../../../helpers/l10n'; + +export default class DefinitionChanges extends React.Component { + static propTypes = { + onSave: React.PropTypes.func.isRequired, + onCancel: React.PropTypes.func.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + handleSaveClick (e) { + e.preventDefault(); + e.target.blur(); + this.props.onSave(); + } + + handleCancelChange (e) { + e.preventDefault(); + e.target.blur(); + this.props.onCancel(); + } + + render () { + return ( + <div className="settings-definition-changes"> + <button className="js-save-changes button-success" onClick={e => this.handleSaveClick(e)}> + {translate('save')} + </button> + + <button className="js-cancel-changes big-spacer-left button-link" onClick={e => this.handleCancelChange(e)}> + {translate('cancel')} + </button> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionDefaults.js b/server/sonar-web/src/main/js/apps/settings/components/DefinitionDefaults.js new file mode 100644 index 00000000000..452b24c5ea5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionDefaults.js @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { getSettingValue, isEmptyValue, getDefaultValue } from '../utils'; +import { translate } from '../../../helpers/l10n'; + +export default class DefinitionDefaults extends React.Component { + static propTypes = { + setting: React.PropTypes.object.isRequired, + isDefault: React.PropTypes.bool.isRequired, + onReset: React.PropTypes.func.isRequired + }; + + handleReset (e) { + e.preventDefault(); + e.target.blur(); + this.props.onReset(); + } + + render () { + const { setting, isDefault } = this.props; + const { definition } = setting; + + const isExplicitlySet = !isDefault && !isEmptyValue(definition, getSettingValue(setting)); + + return ( + <div> + {isDefault && ( + <div className="spacer-top note" style={{ lineHeight: '24px' }}> + {translate('settings._default')} + </div> + )} + + {isExplicitlySet && ( + <div className="spacer-top nowrap"> + <button onClick={e => this.handleReset(e)}>{translate('reset_verb')}</button> + <span className="spacer-left note">{translate('default')}{': '}{getDefaultValue(setting)}</span> + </div> + )} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js b/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js new file mode 100644 index 00000000000..77d0d7fa95b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import Definition from './Definition'; + +export default class DefinitionsList extends React.Component { + static propTypes = { + component: React.PropTypes.object, + settings: React.PropTypes.array.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + render () { + return ( + <ul className="settings-definitions-list"> + {this.props.settings.map(setting => ( + <li key={setting.definition.key}> + <Definition component={this.props.component} setting={setting}/> + </li> + ))} + </ul> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/GlobalMessagesContainer.js b/server/sonar-web/src/main/js/apps/settings/components/GlobalMessagesContainer.js new file mode 100644 index 00000000000..f64ef91664b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/GlobalMessagesContainer.js @@ -0,0 +1,28 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { connect } from 'react-redux'; +import GlobalMessages from '../../../components/controls/GlobalMessages'; +import { getGlobalMessages } from '../store/rootReducer'; + +const mapStateToProps = state => ({ + messages: getGlobalMessages(state) +}); + +export default connect(mapStateToProps)(GlobalMessages); diff --git a/server/sonar-web/src/main/js/apps/settings/components/PageHeader.js b/server/sonar-web/src/main/js/apps/settings/components/PageHeader.js new file mode 100644 index 00000000000..34e402122cb --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/PageHeader.js @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { translate } from '../../../helpers/l10n'; + +export default class PageHeader extends React.Component { + static propTypes = { + component: React.PropTypes.object + }; + + render () { + const title = this.props.component != null ? + translate('project_settings.page') : + translate('settings.page'); + + const description = this.props.component != null ? + translate('project_settings.page.description') : + translate('settings.page.description'); + + return ( + <header className="page-header"> + <h1 className="page-title">{title}</h1> + <div className="page-description">{description}</div> + </header> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js new file mode 100644 index 00000000000..f61d6b2aa7a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import groupBy from 'lodash/groupBy'; +import sortBy from 'lodash/sortBy'; +import DefinitionsList from './DefinitionsList'; +import { getSubCategoryName, getSubCategoryDescription } from '../utils'; + +export default class SubCategoryDefinitionsList extends React.Component { + static propTypes = { + component: React.PropTypes.object, + settings: React.PropTypes.array.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + render () { + const bySubCategory = groupBy(this.props.settings, setting => setting.definition.subCategory); + const subCategories = Object.keys(bySubCategory).map(key => ({ + key, + name: getSubCategoryName(bySubCategory[key][0].definition.category, key), + description: getSubCategoryDescription(bySubCategory[key][0].definition.category, key) + })); + const sortedSubCategories = sortBy(subCategories, subCategory => subCategory.name.toLowerCase()); + + return ( + <ul className="settings-sub-categories-list"> + {sortedSubCategories.map(subCategory => ( + <li key={subCategory.key}> + <h2 className="settings-sub-category-name">{subCategory.name}</h2> + {subCategory.description != null && ( + <div className="settings-sub-category-description markdown"> + {subCategory.description} + </div> + )} + <DefinitionsList settings={bySubCategory[subCategory.key]} component={this.props.component}/> + </li> + ))} + </ul> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/Input.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/Input.js new file mode 100644 index 00000000000..02fcbfacef2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/Input.js @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import PropertySetInput from './PropertySetInput'; +import MultiValueInput from './MultiValueInput'; +import PrimitiveInput from './PrimitiveInput'; +import { TYPE_PROPERTY_SET } from '../../constants'; + +export default class Input extends React.Component { + static propTypes = { + setting: React.PropTypes.object.isRequired, + value: React.PropTypes.any, + onChange: React.PropTypes.func.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + render () { + const { definition } = this.props.setting; + + if (definition.multiValues) { + return <MultiValueInput {...this.props}/>; + } + + if (definition.type === TYPE_PROPERTY_SET) { + return <PropertySetInput {...this.props}/>; + } + + return <PrimitiveInput {...this.props}/>; + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.js new file mode 100644 index 00000000000..9f640591835 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.js @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import Toggle from '../../../../components/controls/Toggle'; +import { defaultInputPropTypes } from '../../propTypes'; +import { translate } from '../../../../helpers/l10n'; + +export default class InputForBoolean extends React.Component { + static propTypes = { + ...defaultInputPropTypes, + value: React.PropTypes.oneOfType([React.PropTypes.bool, React.PropTypes.string]) + }; + + render () { + const hasValue = this.props.value != null; + const displayedValue = hasValue ? this.props.value : false; + + return ( + <div className="display-inline-block text-top"> + <Toggle + name={this.props.name} + value={displayedValue} + onChange={this.props.onChange}/> + + {!hasValue && ( + <span className="spacer-left note">{translate('settings.not_set')}</span> + )} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.js new file mode 100644 index 00000000000..6f741da3730 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.js @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import SimpleInput from './SimpleInput'; + +export default class InputForNumber extends React.Component { + render () { + return ( + <SimpleInput {...this.props} className="input-small" type="text"/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.js new file mode 100644 index 00000000000..4e2a783ce11 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.js @@ -0,0 +1,89 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { translate } from '../../../../helpers/l10n'; +import { defaultInputPropTypes } from '../../propTypes'; + +export default class InputForPassword extends React.Component { + static propTypes = defaultInputPropTypes; + + state = { + changing: false + }; + + handleChangeClick (e) { + e.preventDefault(); + e.target.blur(); + this.setState({ changing: true }); + } + + handleCancelChangeClick (e) { + e.preventDefault(); + e.target.blur(); + this.setState({ changing: false }); + } + + handleFormSubmit (e) { + e.preventDefault(); + this.props.onChange(this.refs.input.value); + this.setState({ changing: false }); + } + + renderInput () { + return ( + <div> + <form onSubmit={e => this.handleFormSubmit(e)}> + <input className="hidden" type="password"/> + <input + ref="input" + name={this.props.name} + className="input-large text-top" + type="password" + autoFocus={true} + autoComplete={false}/> + <button className="spacer-left">{translate('set')}</button> + <a className="spacer-left" href="#" onClick={e => this.handleCancelChangeClick(e)}> + {translate('cancel')} + </a> + </form> + </div> + ); + } + + render () { + if (this.state.changing) { + return this.renderInput(); + } + + const hasValue = !!this.props.value; + + return ( + <div> + {hasValue && ( + <i className="big-spacer-right icon-lock icon-gray"/> + )} + + <button onClick={e => this.handleChangeClick(e)}> + {hasValue ? translate('change_verb') : translate('set')} + </button> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.js new file mode 100644 index 00000000000..a0c704623d9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.js @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import Select from 'react-select'; +import { defaultInputPropTypes } from '../../propTypes'; + +export default class InputForSingleSelectList extends React.Component { + static propTypes = { + ...defaultInputPropTypes, + options: React.PropTypes.arrayOf(React.PropTypes.string).isRequired + }; + + handleInputChange (option) { + this.props.onChange(option.value); + } + + render () { + const options = this.props.options.map(option => ({ + label: option, + value: option + })); + + return ( + <Select + name={this.props.name} + className="input-large" + options={options} + clearable={false} + value={this.props.value} + onChange={option => this.handleInputChange(option)}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.js new file mode 100644 index 00000000000..7eaef55399a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.js @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import SimpleInput from './SimpleInput'; + +export default class InputForString extends React.Component { + render () { + return ( + <SimpleInput {...this.props} className="input-large" type="text"/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForText.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForText.js new file mode 100644 index 00000000000..13cbbfabc85 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForText.js @@ -0,0 +1,40 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { defaultInputPropTypes } from '../../propTypes'; + +export default class InputForText extends React.Component { + static propTypes = defaultInputPropTypes; + + handleInputChange (e) { + this.props.onChange(e.target.value); + } + + render () { + return ( + <textarea + name={this.props.name} + className="input-super-large text-top" + rows="5" + value={this.props.value} + onChange={e => this.handleInputChange(e)}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.js new file mode 100644 index 00000000000..fa2facf6ee4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.js @@ -0,0 +1,90 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import PrimitiveInput from './PrimitiveInput'; +import { getEmptyValue } from '../../utils'; + +export default class MultiValueInput extends React.Component { + static propTypes = { + setting: React.PropTypes.object.isRequired, + value: React.PropTypes.array, + onChange: React.PropTypes.func.isRequired + }; + + ensureValue () { + return this.props.value || []; + } + + handleSingleInputChange (index, value) { + const newValue = [...this.ensureValue()]; + newValue.splice(index, 1, value); + this.props.onChange(newValue); + } + + handleDeleteValue (e, index) { + e.preventDefault(); + e.target.blur(); + + const newValue = [...this.ensureValue()]; + newValue.splice(index, 1); + this.props.onChange(newValue); + } + + prepareSetting () { + const { setting } = this.props; + const newDefinition = { ...setting.definition, multiValues: false }; + return { + ...setting, + definition: newDefinition, + values: undefined + }; + } + + renderInput (value, index, isLast) { + return ( + <li key={index} className="spacer-bottom"> + <PrimitiveInput + setting={this.prepareSetting()} + value={value} + onChange={this.handleSingleInputChange.bind(this, index)}/> + + {!isLast && ( + <div className="display-inline-block spacer-left"> + <button className="js-remove-value button-clean" onClick={e => this.handleDeleteValue(e, index)}> + <i className="icon-delete"/> + </button> + </div> + )} + </li> + ); + } + + render () { + const displayedValue = [...this.ensureValue(), ...getEmptyValue(this.props.setting.definition)]; + + return ( + <div> + <ul> + {displayedValue.map((value, index) => this.renderInput(value, index, index === displayedValue.length - 1))} + </ul> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.js new file mode 100644 index 00000000000..61e9db6052d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.js @@ -0,0 +1,75 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import InputForString from './InputForString'; +import InputForText from './InputForText'; +import InputForPassword from './InputForPassword'; +import InputForBoolean from './InputForBoolean'; +import InputForNumber from './InputForNumber'; +import InputForSingleSelectList from './InputForSingleSelectList'; +import { getUniqueName, isDefaultOrInherited } from '../../utils'; +import * as types from '../../constants'; + +const typeMapping = { + [types.TYPE_STRING]: InputForString, + [types.TYPE_TEXT]: InputForText, + [types.TYPE_PASSWORD]: InputForPassword, + [types.TYPE_BOOLEAN]: InputForBoolean, + [types.TYPE_INTEGER]: InputForNumber, + [types.TYPE_LONG]: InputForNumber, + [types.TYPE_FLOAT]: InputForNumber +}; + +export default class PrimitiveInput extends React.Component { + static propTypes = { + setting: React.PropTypes.object.isRequired, + value: React.PropTypes.any, + onChange: React.PropTypes.func.isRequired + }; + + render () { + const { setting, value, onChange, ...other } = this.props; + const { definition } = setting; + + const name = getUniqueName(definition); + + if (definition.type === types.TYPE_SINGLE_SELECT_LIST) { + return ( + <InputForSingleSelectList + name={name} + value={value} + isDefault={isDefaultOrInherited(setting)} + options={definition.options} + onChange={onChange} + {...other}/> + ); + } + + const InputComponent = typeMapping[definition.type] || InputForString; + return ( + <InputComponent + name={name} + value={value} + isDefault={isDefaultOrInherited(setting)} + onChange={onChange} + {...other}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.js new file mode 100644 index 00000000000..636a83896be --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.js @@ -0,0 +1,110 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import PrimitiveInput from './PrimitiveInput'; +import { getEmptyValue, getUniqueName } from '../../utils'; + +export default class PropertySetInput extends React.Component { + static propTypes = { + setting: React.PropTypes.object.isRequired, + value: React.PropTypes.array, + onChange: React.PropTypes.func.isRequired + }; + + ensureValue () { + return this.props.value || []; + } + + getFieldName (field) { + return getUniqueName(this.props.setting.definition, field.key); + } + + handleDeleteValue (e, index) { + e.preventDefault(); + e.target.blur(); + + const newValue = [...this.ensureValue()]; + newValue.splice(index, 1); + this.props.onChange(newValue); + } + + handleInputChange (index, fieldKey, value) { + const emptyValue = getEmptyValue(this.props.setting.definition)[0]; + const newValue = [...this.ensureValue()]; + const newFields = { ...emptyValue, ...newValue[index], [fieldKey]: value }; + newValue.splice(index, 1, newFields); + return this.props.onChange(newValue); + } + + renderFields (fieldValues, index, isLast) { + const { setting } = this.props; + + return ( + <tr key={index}> + {setting.definition.fields.map(field => ( + <td key={field.key}> + <PrimitiveInput + name={this.getFieldName(field)} + setting={{ definition: field, value: fieldValues[field.key] }} + value={fieldValues[field.key]} + onChange={this.handleInputChange.bind(this, index, field.key)}/> + </td> + ))} + <td className="thin nowrap"> + {!isLast && ( + <button className="js-remove-value button-link" onClick={e => this.handleDeleteValue(e, index)}> + <i className="icon-delete"/> + </button> + )} + </td> + </tr> + ); + } + + render () { + const { setting } = this.props; + + const displayedValue = [...this.ensureValue(), ...getEmptyValue(this.props.setting.definition)]; + + return ( + <div> + <table className="data zebra-hover no-outer-padding" style={{ width: 'auto', minWidth: 480, marginTop: -12 }}> + <thead> + <tr> + {setting.definition.fields.map(field => ( + <th key={field.key}> + {field.name} + {field.description != null && ( + <span className="spacer-top small">{field.description}</span> + )} + </th> + ))} + <th> </th> + </tr> + </thead> + <tbody> + {displayedValue.map((fieldValues, index) => + this.renderFields(fieldValues, index, index === displayedValue.length - 1))} + </tbody> + </table> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/SimpleInput.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/SimpleInput.js new file mode 100644 index 00000000000..6a7e24a6f8e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/SimpleInput.js @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { defaultInputPropTypes } from '../../propTypes'; + +export default class SimpleInput extends React.Component { + static propTypes = { + ...defaultInputPropTypes, + value: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]), + type: React.PropTypes.string.isRequired, + className: React.PropTypes.string.isRequired + }; + + handleInputChange (e) { + this.props.onChange(e.target.value); + } + + render () { + return ( + <input + name={this.props.name} + className={this.props.className + ' text-top'} + type={this.props.type} + value={this.props.value} + onChange={e => this.handleInputChange(e)}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.js new file mode 100644 index 00000000000..dbe570ec878 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.js @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import Input from '../Input'; +import PrimitiveInput from '../PrimitiveInput'; +import MultiValueInput from '../MultiValueInput'; +import PropertySetInput from '../PropertySetInput'; +import { TYPE_STRING, TYPE_PROPERTY_SET } from '../../../constants'; + +describe('Settings :: Inputs :: Input', () => { + it('should render PrimitiveInput', () => { + const setting = { definition: { key: 'example', type: TYPE_STRING } }; + const onChange = sinon.spy(); + const input = shallow(<Input setting={setting} value="foo" onChange={onChange}/>).find(PrimitiveInput); + expect(input).to.have.length(1); + expect(input.prop('setting')).to.equal(setting); + expect(input.prop('value')).to.equal('foo'); + expect(input.prop('onChange')).to.equal(onChange); + }); + + it('should render MultiValueInput', () => { + const setting = { definition: { key: 'example', type: TYPE_STRING, multiValues: true } }; + const onChange = sinon.spy(); + const input = shallow(<Input setting={setting} value="foo" onChange={onChange}/>).find(MultiValueInput); + expect(input).to.have.length(1); + expect(input.prop('setting')).to.equal(setting); + expect(input.prop('value')).to.equal('foo'); + expect(input.prop('onChange')).to.equal(onChange); + }); + + it('should render PropertySetInput', () => { + const setting = { definition: { key: 'example', type: TYPE_PROPERTY_SET, fields: [] } }; + const onChange = sinon.spy(); + const input = shallow(<Input setting={setting} value="foo" onChange={onChange}/>).find(PropertySetInput); + expect(input).to.have.length(1); + expect(input.prop('setting')).to.equal(setting); + expect(input.prop('value')).to.equal('foo'); + expect(input.prop('onChange')).to.equal(onChange); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.js new file mode 100644 index 00000000000..2fc8b9bb9c5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.js @@ -0,0 +1,77 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import InputForBoolean from '../InputForBoolean'; +import Toggle from '../../../../../components/controls/Toggle'; + +describe('Settings :: Inputs :: InputForBoolean', () => { + it('should render Toggle', () => { + const onChange = sinon.spy(); + const toggle = shallow( + <InputForBoolean + name="foo" + value={true} + isDefault={false} + onChange={onChange}/> + ).find(Toggle); + expect(toggle).to.have.length(1); + expect(toggle.prop('name')).to.equal('foo'); + expect(toggle.prop('value')).to.equal(true); + expect(toggle.prop('onChange')).to.be.a('function'); + }); + + it('should render Toggle without value', () => { + const onChange = sinon.spy(); + const input = shallow( + <InputForBoolean + name="foo" + isDefault={false} + onChange={onChange}/> + ); + const toggle = input.find(Toggle); + expect(toggle).to.have.length(1); + expect(toggle.prop('name')).to.equal('foo'); + expect(toggle.prop('value')).to.equal(false); + expect(toggle.prop('onChange')).to.be.a('function'); + expect(input.find('.note')).to.have.length(1); + }); + + it('should call onChange', () => { + const onChange = sinon.spy(); + const input = shallow( + <InputForBoolean + name="foo" + value={true} + isDefault={false} + onChange={onChange}/> + ); + const toggle = input.find(Toggle); + expect(toggle).to.have.length(1); + expect(toggle.prop('onChange')).to.be.a('function'); + + toggle.prop('onChange')(false); + + expect(onChange.called).to.equal(true); + expect(onChange.lastCall.args).to.deep.equal([false]); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForNumber-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForNumber-test.js new file mode 100644 index 00000000000..3544898fa2e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForNumber-test.js @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import InputForNumber from '../InputForNumber'; +import SimpleInput from '../SimpleInput'; + +describe('Settings :: Inputs :: InputForNumber', () => { + it('should render SimpleInput', () => { + const onChange = sinon.spy(); + const simpleInput = shallow( + <InputForNumber + name="foo" + value={17} + isDefault={false} + onChange={onChange}/> + ).find(SimpleInput); + expect(simpleInput).to.have.length(1); + expect(simpleInput.prop('name')).to.equal('foo'); + expect(simpleInput.prop('value')).to.equal(17); + expect(simpleInput.prop('type')).to.equal('text'); + expect(simpleInput.prop('onChange')).to.be.a('function'); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.js new file mode 100644 index 00000000000..6e463644879 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.js @@ -0,0 +1,97 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow, mount } from 'enzyme'; +import sinon from 'sinon'; +import InputForPassword from '../InputForPassword'; +import { click, submit } from '../../../../../../../../tests/utils'; + +describe('Settings :: Inputs :: InputForPassword', () => { + it('should render lock icon, but no form', () => { + const onChange = sinon.spy(); + const input = shallow( + <InputForPassword + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ); + expect(input.find('.icon-lock')).to.have.length(1); + expect(input.find('form')).to.have.length(0); + }); + + it('should open form', () => { + const onChange = sinon.spy(); + const input = shallow( + <InputForPassword + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ); + const button = input.find('button'); + expect(button).to.have.length(1); + + click(button); + expect(input.find('form')).to.have.length(1); + }); + + it('should close form', () => { + const onChange = sinon.spy(); + const input = shallow( + <InputForPassword + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ); + const button = input.find('button'); + expect(button).to.have.length(1); + + click(button); + expect(input.find('form')).to.have.length(1); + + click(input.find('form').find('a')); + expect(input.find('form')).to.have.length(0); + }); + + it('should set value', () => { + const onChange = sinon.stub().returns(Promise.resolve()); + const input = mount( + <InputForPassword + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ); + const button = input.find('button'); + expect(button).to.have.length(1); + + click(button); + const form = input.find('form'); + expect(form).to.have.length(1); + + input.ref('input').value = 'secret'; + submit(form); + + expect(onChange.called).to.equal(true); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.js new file mode 100644 index 00000000000..2be66e0ad80 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.js @@ -0,0 +1,66 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import Select from 'react-select'; +import InputForSingleSelectList from '../InputForSingleSelectList'; + +describe('Settings :: Inputs :: InputForSingleSelectList', () => { + it('should render Select', () => { + const onChange = sinon.spy(); + const select = shallow( + <InputForSingleSelectList + name="foo" + value="bar" + options={['foo', 'bar', 'baz']} + isDefault={false} + onChange={onChange}/> + ).find(Select); + expect(select).to.have.length(1); + expect(select.prop('name')).to.equal('foo'); + expect(select.prop('value')).to.equal('bar'); + expect(select.prop('options')).to.deep.equal([ + { value: 'foo', label: 'foo' }, + { value: 'bar', label: 'bar' }, + { value: 'baz', label: 'baz' } + ]); + expect(select.prop('onChange')).to.be.a('function'); + }); + + it('should call onChange', () => { + const onChange = sinon.spy(); + const select = shallow( + <InputForSingleSelectList + name="foo" + value="bar" + options={['foo', 'bar', 'baz']} + isDefault={false} + onChange={onChange}/> + ).find(Select); + expect(select).to.have.length(1); + expect(select.prop('onChange')).to.be.a('function'); + + select.prop('onChange')({ value: 'baz', label: 'baz' }); + expect(onChange.called).to.equal(true); + expect(onChange.lastCall.args).to.deep.equal(['baz']); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForString-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForString-test.js new file mode 100644 index 00000000000..4fd3c7a4b39 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForString-test.js @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import InputForString from '../InputForString'; +import SimpleInput from '../SimpleInput'; + +describe('Settings :: Inputs :: InputForString', () => { + it('should render SimpleInput', () => { + const onChange = sinon.spy(); + const simpleInput = shallow( + <InputForString + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ).find(SimpleInput); + expect(simpleInput).to.have.length(1); + expect(simpleInput.prop('name')).to.equal('foo'); + expect(simpleInput.prop('value')).to.equal('bar'); + expect(simpleInput.prop('type')).to.equal('text'); + expect(simpleInput.prop('onChange')).to.be.a('function'); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.js new file mode 100644 index 00000000000..97ef6ec5fab --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.js @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import InputForText from '../InputForText'; +import { change } from '../../../../../../../../tests/utils'; + +describe('Settings :: Inputs :: InputForText', () => { + it('should render textarea', () => { + const onChange = sinon.spy(); + const textarea = shallow( + <InputForText + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ).find('textarea'); + expect(textarea).to.have.length(1); + expect(textarea.prop('name')).to.equal('foo'); + expect(textarea.prop('value')).to.equal('bar'); + expect(textarea.prop('onChange')).to.be.a('function'); + }); + + it('should call onChange', () => { + const onChange = sinon.spy(); + const textarea = shallow( + <InputForText + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ).find('textarea'); + expect(textarea).to.have.length(1); + expect(textarea.prop('onChange')).to.be.a('function'); + + change(textarea, 'qux'); + + expect(onChange.called).to.equal(true); + expect(onChange.lastCall.args).to.deep.equal(['qux']); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.js new file mode 100644 index 00000000000..126a7a5a0ed --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.js @@ -0,0 +1,103 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow, mount } from 'enzyme'; +import sinon from 'sinon'; +import MultiValueInput from '../MultiValueInput'; +import InputForString from '../InputForString'; +import { click, change } from '../../../../../../../../tests/utils'; + +const definition = { multiValues: true }; + +const assertValues = (inputs, values) => { + values.forEach((value, index) => { + const input = inputs.at(index); + expect(input.prop('value')).to.equal(value); + }); +}; + +describe('Settings :: Inputs :: MultiValueInput', () => { + it('should render one value', () => { + const multiValueInput = mount( + <MultiValueInput + setting={{ definition }} + value={['foo']} + onChange={sinon.stub()}/> + ); + const stringInputs = multiValueInput.find(InputForString); + expect(stringInputs).to.have.length(1 + 1); + assertValues(stringInputs, ['foo', '']); + }); + + it('should render several values', () => { + const multiValueInput = mount( + <MultiValueInput + setting={{ definition }} + value={['foo', 'bar', 'baz']} + onChange={sinon.stub()}/> + ); + const stringInputs = multiValueInput.find(InputForString); + expect(stringInputs).to.have.length(3 + 1); + assertValues(stringInputs, ['foo', 'bar', 'baz', '']); + }); + + it('should remove value', () => { + const onChange = sinon.spy(); + const multiValueInput = mount( + <MultiValueInput + setting={{ definition }} + value={['foo', 'bar', 'baz']} + onChange={onChange}/> + ); + + click(multiValueInput.find('.js-remove-value').at(1)); + expect(onChange.called).to.equal(true); + expect(onChange.lastCall.args).to.deep.equal([['foo', 'baz']]); + }); + + it('should change existing value', () => { + const onChange = sinon.spy(); + const multiValueInput = mount( + <MultiValueInput + setting={{ definition }} + value={['foo', 'bar', 'baz']} + onChange={onChange}/> + ); + + change(multiValueInput.find(InputForString).at(1).find('input'), 'qux'); + expect(onChange.called).to.equal(true); + expect(onChange.lastCall.args).to.deep.equal([['foo', 'qux', 'baz']]); + }); + + it('should add new value', () => { + const onChange = sinon.spy(); + const multiValueInput = mount( + <MultiValueInput + setting={{ definition }} + value={['foo']} + onChange={onChange}/> + ); + + change(multiValueInput.find(InputForString).at(1).find('input'), 'bar'); + expect(onChange.called).to.equal(true); + expect(onChange.lastCall.args).to.deep.equal([['foo', 'bar']]); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.js new file mode 100644 index 00000000000..679d5302eb9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.js @@ -0,0 +1,66 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import SimpleInput from '../SimpleInput'; +import { change } from '../../../../../../../../tests/utils'; + +describe('Settings :: Inputs :: SimpleInput', () => { + it('should render input', () => { + const onChange = sinon.spy(); + const input = shallow( + <SimpleInput + type="text" + className="input-large" + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ).find('input'); + expect(input).to.have.length(1); + expect(input.prop('type')).to.equal('text'); + expect(input.prop('className')).to.include('input-large'); + expect(input.prop('name')).to.equal('foo'); + expect(input.prop('value')).to.equal('bar'); + expect(input.prop('onChange')).to.be.a('function'); + }); + + it('should call onChange', () => { + const onChange = sinon.spy(); + const input = shallow( + <SimpleInput + type="text" + className="input-large" + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ).find('input'); + expect(input).to.have.length(1); + expect(input.prop('onChange')).to.be.a('function'); + + change(input, 'qux'); + + expect(onChange.called).to.equal(true); + expect(onChange.lastCall.args).to.deep.equal(['qux']); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/constants.js b/server/sonar-web/src/main/js/apps/settings/constants.js new file mode 100644 index 00000000000..249c996b2ac --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/constants.js @@ -0,0 +1,28 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const TYPE_STRING = 'STRING'; +export const TYPE_TEXT = 'TEXT'; +export const TYPE_PASSWORD = 'PASSWORD'; +export const TYPE_BOOLEAN = 'BOOLEAN'; +export const TYPE_FLOAT = 'FLOAT'; +export const TYPE_INTEGER = 'INTEGER'; +export const TYPE_LONG = 'LONG'; +export const TYPE_SINGLE_SELECT_LIST = 'SINGLE_SELECT_LIST'; +export const TYPE_PROPERTY_SET = 'PROPERTY_SET'; diff --git a/server/sonar-web/src/main/js/apps/settings/propTypes.js b/server/sonar-web/src/main/js/apps/settings/propTypes.js new file mode 100644 index 00000000000..c7e64bfa695 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/propTypes.js @@ -0,0 +1,27 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { PropTypes } from 'react'; + +export const defaultInputPropTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.any, + isDefault: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired +}; diff --git a/server/sonar-web/src/main/js/apps/settings/store/actions.js b/server/sonar-web/src/main/js/apps/settings/store/actions.js new file mode 100644 index 00000000000..f2ba7d709e6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/actions.js @@ -0,0 +1,85 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { getDefinitions, getValues, setSettingValue, resetSettingValue } from '../../../api/settings'; +import { receiveValues } from './values/actions'; +import { receiveDefinitions } from './definitions/actions'; +import { startLoading, stopLoading } from './settingsPage/loading/actions'; +import { parseError } from '../../code/utils'; +import { addGlobalErrorMessage, closeAllGlobalMessages } from '../../../components/store/globalMessages'; +import { passValidation, failValidation } from './settingsPage/validationMessages/actions'; +import { cancelChange } from './settingsPage/changedValues/actions'; +import { getDefinition, getChangedValue } from './rootReducer'; + +export const fetchSettings = componentKey => dispatch => { + return getDefinitions(componentKey) + .then(definitions => { + dispatch(receiveDefinitions(definitions)); + const keys = definitions.map(definition => definition.key).join(); + return getValues(keys, componentKey); + }) + .then(settings => { + dispatch(receiveValues(settings)); + dispatch(closeAllGlobalMessages()); + }) + .catch(e => parseError(e).then(message => dispatch(addGlobalErrorMessage(message)))); +}; + +export const saveValue = (key, componentKey) => (dispatch, getState) => { + dispatch(startLoading(key)); + + const state = getState(); + const definition = getDefinition(state, key); + const value = getChangedValue(state, key); + + return setSettingValue(definition, value, componentKey) + .then(() => getValues(key, componentKey)) + .then(values => { + dispatch(receiveValues(values)); + dispatch(cancelChange(key)); + dispatch(passValidation(key)); + dispatch(stopLoading(key)); + }) + .catch(e => { + dispatch(stopLoading(key)); + parseError(e).then(message => dispatch(failValidation(key, message))); + return Promise.reject(); + }); +}; + +export const resetValue = (key, componentKey) => dispatch => { + dispatch(startLoading(key)); + + return resetSettingValue(key, componentKey) + .then(() => getValues(key, componentKey)) + .then(values => { + if (values.length > 0) { + dispatch(receiveValues(values)); + } else { + dispatch(receiveValues([{ key }])); + } + dispatch(passValidation(key)); + dispatch(stopLoading(key)); + }) + .catch(e => { + dispatch(stopLoading(key)); + parseError(e).then(message => dispatch(failValidation(key, message))); + return Promise.reject(); + }); +}; diff --git a/server/sonar-web/src/main/js/apps/settings/store/definitions/actions.js b/server/sonar-web/src/main/js/apps/settings/store/definitions/actions.js new file mode 100644 index 00000000000..589a05df223 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/definitions/actions.js @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const RECEIVE_DEFINITIONS = 'RECEIVE_DEFINITIONS'; + +/** + * Receive definitions action creator + * @param {Array} definitions + * @returns {Object} + */ +export const receiveDefinitions = definitions => ({ + type: RECEIVE_DEFINITIONS, + definitions +}); diff --git a/server/sonar-web/src/main/js/apps/settings/store/definitions/reducer.js b/server/sonar-web/src/main/js/apps/settings/store/definitions/reducer.js new file mode 100644 index 00000000000..d80ffc47c09 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/definitions/reducer.js @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import keyBy from 'lodash/keyBy'; +import sortBy from 'lodash/sortBy'; +import uniqBy from 'lodash/uniqBy'; +import { RECEIVE_DEFINITIONS } from './actions'; +import { DEFAULT_CATEGORY, getCategoryName } from '../../utils'; + +const reducer = (state = {}, action = {}) => { + if (action.type === RECEIVE_DEFINITIONS) { + const definitionsByKey = keyBy(action.definitions, 'key'); + return { ...state, ...definitionsByKey }; + } + + return state; +}; + +export default reducer; + +export const getDefinition = (state, key) => state[key]; + +export const getAllDefinitions = state => Object.values(state); + +export const getDefinitionsForCategory = (state, category) => + getAllDefinitions(state).filter(definition => definition.category.toLowerCase() === category.toLowerCase()); + +export const getAllCategories = state => uniqBy( + getAllDefinitions(state).map(definition => definition.category), + category => category.toLowerCase()); + +export const getDefaultCategory = state => { + const categories = getAllCategories(state); + if (categories.includes(DEFAULT_CATEGORY)) { + return DEFAULT_CATEGORY; + } else { + const sortedCategories = sortBy(categories, category => getCategoryName(category).toLowerCase()); + return sortedCategories[0]; + } +}; diff --git a/server/sonar-web/src/main/js/apps/settings/store/rootReducer.js b/server/sonar-web/src/main/js/apps/settings/store/rootReducer.js new file mode 100644 index 00000000000..603cb3b753a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/rootReducer.js @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { combineReducers } from 'redux'; +import definitions, * as fromDefinitions from './definitions/reducer'; +import values, * as fromValues from './values/reducer'; +import settingsPage, * as fromSettingsPage from './settingsPage/reducer'; +import globalMessages, * as fromGlobalMessages from '../../../components/store/globalMessages'; + +const rootReducer = combineReducers({ + definitions, + values, + settingsPage, + globalMessages +}); + +export default rootReducer; + +export const getDefinition = (state, key) => fromDefinitions.getDefinition(state.definitions, key); + +export const getAllCategories = state => fromDefinitions.getAllCategories(state.definitions); + +export const getDefaultCategory = state => fromDefinitions.getDefaultCategory(state.definitions); + +export const getValue = (state, key) => fromValues.getValue(state.values, key); + +export const getSettingsForCategory = (state, category) => + fromDefinitions.getDefinitionsForCategory(state.definitions, category).map(definition => ({ + ...getValue(state, definition.key), + definition + })); + +export const getChangedValue = (state, key) => fromSettingsPage.getChangedValue(state.settingsPage, key); + +export const isLoading = (state, key) => fromSettingsPage.isLoading(state.settingsPage, key); + +export const getValidationMessage = (state, key) => fromSettingsPage.getValidationMessage(state.settingsPage, key); + +export const getGlobalMessages = state => fromGlobalMessages.getGlobalMessages(state.globalMessages); diff --git a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/actions.js b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/actions.js new file mode 100644 index 00000000000..46a0beecd4d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/actions.js @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const CHANGE_VALUE = 'settingsPage/CHANGE_VALUE'; + +export const changeValue = (key, value) => ({ + type: CHANGE_VALUE, + key, + value +}); + +export const CANCEL_CHANGE = 'settingsPage/CANCEL_CHANGE'; + +export const cancelChange = key => ({ + type: CANCEL_CHANGE, + key +}); diff --git a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/reducer.js b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/reducer.js new file mode 100644 index 00000000000..3783b92ac4b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/reducer.js @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import omit from 'lodash/omit'; +import { CHANGE_VALUE, CANCEL_CHANGE } from './actions'; + +const reducer = (state = {}, action = {}) => { + if (action.type === CHANGE_VALUE) { + return { ...state, [action.key]: action.value }; + } + + if (action.type === CANCEL_CHANGE) { + return omit(state, action.key); + } + + return state; +}; + +export default reducer; + +export const getChangedValue = (state, key) => state[key]; diff --git a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/actions.js b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/actions.js new file mode 100644 index 00000000000..0a4363b41df --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/actions.js @@ -0,0 +1,31 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const START_LOADING = 'settingsPage/START_LOADING'; + +export const startLoading = key => ({ + type: START_LOADING, + key +}); +export const STOP_LOADING = 'settingsPage/STOP_LOADING'; + +export const stopLoading = key => ({ + type: STOP_LOADING, + key +}); diff --git a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/reducer.js b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/reducer.js new file mode 100644 index 00000000000..d0a6677029b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/reducer.js @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { START_LOADING, STOP_LOADING } from './actions'; + +const reducer = (state = {}, action = {}) => { + if (action.type === START_LOADING) { + return { ...state, [action.key]: true }; + } + + if (action.type === STOP_LOADING) { + return { ...state, [action.key]: false }; + } + + return state; +}; + +export default reducer; + +export const isLoading = (state, key) => !!state[key]; diff --git a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/reducer.js b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/reducer.js new file mode 100644 index 00000000000..a3dc0c7c37a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/reducer.js @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { combineReducers } from 'redux'; +import changedValues, * as fromChangedValues from './changedValues/reducer'; +import validationMessages, * as fromValidationMessages from './validationMessages/reducer'; +import loading, * as fromLoading from './loading/reducer'; + +export default combineReducers({ + changedValues, + validationMessages, + loading +}); + +export const getChangedValue = (state, key) => + fromChangedValues.getChangedValue(state.changedValues, key); + +export const getValidationMessage = (state, key) => + fromValidationMessages.getValidationMessage(state.validationMessages, key); + +export const isLoading = (state, key) => + fromLoading.isLoading(state.loading, key); diff --git a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/actions.js b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/actions.js new file mode 100644 index 00000000000..eb7b33d72b9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/actions.js @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const FAIL_VALIDATION = 'settingsPage/FAIL_VALIDATION'; + +export const failValidation = (key, message) => ({ + type: FAIL_VALIDATION, + key, + message +}); + +export const PASS_VALIDATION = 'settingsPage/PASS_VALIDATION'; + +export const passValidation = key => ({ + type: PASS_VALIDATION, + key +}); diff --git a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/reducer.js b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/reducer.js new file mode 100644 index 00000000000..b332de3a98f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/reducer.js @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { FAIL_VALIDATION, PASS_VALIDATION } from './actions'; + +const reducer = (state = {}, action = {}) => { + if (action.type === FAIL_VALIDATION) { + return { ...state, [action.key]: action.message }; + } + + if (action.type === PASS_VALIDATION) { + return { ...state, [action.key]: null }; + } + + return state; +}; + +export default reducer; + +export const getValidationMessage = (state, key) => state[key]; diff --git a/server/sonar-web/src/main/js/apps/settings/store/values/actions.js b/server/sonar-web/src/main/js/apps/settings/store/values/actions.js new file mode 100644 index 00000000000..582b6078233 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/values/actions.js @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const RECEIVE_VALUES = 'RECEIVE_VALUES'; + +/** + * Receive settings action creator + * @param {Array} settings + * @returns {Object} + */ +export const receiveValues = settings => ({ + type: RECEIVE_VALUES, + settings +}); diff --git a/server/sonar-web/src/main/js/apps/settings/store/values/reducer.js b/server/sonar-web/src/main/js/apps/settings/store/values/reducer.js new file mode 100644 index 00000000000..5ed6709bc5d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/values/reducer.js @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import keyBy from 'lodash/keyBy'; +import { RECEIVE_VALUES } from './actions'; + +const reducer = (state = {}, action = {}) => { + if (action.type === RECEIVE_VALUES) { + const settingsByKey = keyBy(action.settings, 'key'); + return { ...state, ...settingsByKey }; + } + + return state; +}; + +export default reducer; + +export const getValue = (state, key) => state[key]; diff --git a/server/sonar-web/src/main/js/apps/settings/styles.css b/server/sonar-web/src/main/js/apps/settings/styles.css new file mode 100644 index 00000000000..a5c4c61604a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/styles.css @@ -0,0 +1,157 @@ +.settings-layout { + display: flex; + justify-content: space-between; + align-items: stretch; + margin-bottom: 60px; +} + +.settings-main { + position: relative; + z-index: 2; + flex-grow: 1; + padding: 15px 20px; + border: 1px solid #e6e6e6; + box-sizing: border-box; + background-color: #fff; +} + +.settings-side { + position: relative; + z-index: 3; + width: 160px; + flex-shrink: 0; + padding: 10px 0; + box-sizing: border-box; + transform: translateX(1px); +} + +.settings-menu { +} + +.settings-menu > li { + margin-bottom: 4px; +} + +.settings-menu > li > a { + display: block; + padding: 10px 10px; + line-height: 1.5; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + border: 1px solid #e6e6e6; + border-right: none; + overflow: hidden; + text-overflow: ellipsis; + transition: color 0.3s ease, background-color 0.3s ease; +} + +.settings-menu > li > a:hover, +.settings-menu > li > a:focus, +.settings-menu > li > a.active { + background-color: #fff; +} + +.settings-menu > li > a.active { + color: #444; + cursor: default; +} + +.settings-definitions-list > li + li { + margin-top: 30px; +} + +.settings-definition { + display: flex; + align-items: stretch; +} + +.settings-definition-changed { + margin: -10px -20px; + padding: 9px 20px; + border-top: 1px solid #faebcc; + border-bottom: 1px solid #faebcc; + background-color: #fcf8e3; +} + +.settings-definition-left { + width: 330px; + padding-right: 30px; + box-sizing: border-box; +} + +.settings-definition-right { + position: relative; + width: calc(100% - 330px); + padding-top: 35px; + box-sizing: border-box; +} + +.settings-definition-name { + text-overflow: ellipsis; +} + +.settings-definition-key { + line-height: 1.5; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.settings-definition-key:hover { + overflow: visible; +} + +.settings-definition-state { + position: absolute; + top: 0; + left: 0; + right: 0; + line-height: 24px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.settings-definition-state-icon { + display: inline-block; + vertical-align: middle; + width: 24px; + height: 24px; + margin-right: 8px; + text-align: center; +} + +.settings-definition-state-icon > .icon-alert-error, +.settings-definition-state-icon > .spinner { + position: relative; + top: -2px; +} + +.settings-definition-changes { + margin-top: 20px; + padding-top: 20px; + border-top: 1px dotted #e6e6e6; +} + +.settings-sub-categories-list { +} + +.settings-sub-categories-list > li { +} + +.settings-sub-categories-list > li + li { + margin: 30px -20px 0; + padding: 30px 20px; + border-top: 1px solid #e6e6e6; +} + +.settings-sub-category-name { + margin-bottom: 20px; + font-size: 16px; +} + +.settings-sub-category-description { + margin-top: -15px; + margin-bottom: 20px; + color: #777; +} diff --git a/server/sonar-web/src/main/js/apps/settings/utils.js b/server/sonar-web/src/main/js/apps/settings/utils.js new file mode 100644 index 00000000000..5b40e2ed999 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/utils.js @@ -0,0 +1,140 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { translate, hasMessage } from '../../helpers/l10n'; +import { TYPE_PROPERTY_SET, TYPE_BOOLEAN, TYPE_SINGLE_SELECT_LIST, TYPE_PASSWORD } from './constants'; + +export const DEFAULT_CATEGORY = 'general'; + +export function getPropertyName (definition) { + const key = `property.${definition.key}.name`; + return hasMessage(key) ? translate(key) : definition.name; +} + +export function getPropertyDescription (definition) { + const key = `property.${definition.key}.description`; + return hasMessage(key) ? translate(key) : definition.description; +} + +export function getCategoryName (category) { + const key = `property.category.${category}`; + return hasMessage(key) ? translate(key) : category; +} + +export function getSubCategoryName (category, subCategory) { + const key = `property.category.${category}.${subCategory}`; + return hasMessage(key) ? translate(key) : getCategoryName(subCategory); +} + +export function getSubCategoryDescription (category, subCategory) { + const key = `property.category.${category}.${subCategory}.description`; + return hasMessage(key) ? translate(key) : null; +} + +export function getUniqueName (definition, index = null) { + const indexSuffix = index != null ? `[${index}]` : ''; + return `settings[${definition.key}]${indexSuffix}`; +} + +export function getSettingValue (setting) { + if (setting.definition.multiValues) { + return setting.values; + } else if (setting.definition.type === TYPE_PROPERTY_SET) { + return setting.fieldValues; + } else { + return setting.value; + } +} + +export function isEmptyValue (definition, value) { + if (value == null) { + return true; + } else if (definition.type === TYPE_BOOLEAN) { + return false; + } else { + return value.length === 0; + } +} + +export function getEmptyValue (definition) { + if (definition.multiValues) { + return [getEmptyValue({ ...definition, multiValues: false })]; + } + + if (definition.type === TYPE_PROPERTY_SET) { + const value = {}; + definition.fields.forEach(field => value[field.key] = getEmptyValue(field)); + return [value]; + } + + if (definition.type === TYPE_BOOLEAN || definition.type === TYPE_SINGLE_SELECT_LIST) { + return null; + } + + return ''; +} + +export function isDefaultOrInherited (setting) { + return !!setting.default || !!setting.inherited; +} + +function getParentValue (setting) { + if (setting.definition.multiValues) { + return setting.parentValues; + } else if (setting.definition.type === TYPE_PROPERTY_SET) { + return setting.parentFieldValues; + } else { + return setting.parentValue; + } +} + +/** + * Get and format the default value + * @param setting + * @returns {string} + */ +export function getDefaultValue (setting) { + const parentValue = getParentValue(setting); + + if (parentValue == null) { + return translate('settings.default.no_value'); + } + + if (setting.definition.multiValues) { + return parentValue.length > 0 ? + parentValue.join(', ') : + translate('settings.default.no_value'); + } + + if (setting.definition.type === TYPE_PROPERTY_SET) { + return parentValue.length > 0 ? + translate('settings.default.complex_value') : + translate('settings.default.no_value'); + } + + if (setting.definition.type === TYPE_PASSWORD) { + return translate('settings.default.password'); + } + + if (setting.definition.type === TYPE_BOOLEAN) { + return parentValue ? translate('settings.boolean.true') : translate('settings.boolean.false'); + } + + return parentValue; +} diff --git a/server/sonar-web/src/main/js/components/controls/Toggle.js b/server/sonar-web/src/main/js/components/controls/Toggle.js new file mode 100644 index 00000000000..c843657dae4 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/Toggle.js @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import classNames from 'classnames'; +import './styles.css'; + +export default class Toggle extends React.Component { + static propTypes = { + value: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.bool]).isRequired, + name: React.PropTypes.string, + onChange: React.PropTypes.func + }; + + handleClick (e, value) { + e.preventDefault(); + e.currentTarget.blur(); + if (this.props.onChange) { + this.props.onChange(!value); + } + } + + render () { + const { value } = this.props; + const booleanValue = typeof value === 'string' ? value === 'true' : value; + + const className = classNames('boolean-toggle', { 'boolean-toggle-on': booleanValue }); + + return ( + <button className={className} name={this.props.name} onClick={e => this.handleClick(e, booleanValue)}> + <div className="boolean-toggle-handle"/> + </button> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/Toggle-test.js b/server/sonar-web/src/main/js/components/controls/__tests__/Toggle-test.js new file mode 100644 index 00000000000..f194063188b --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/Toggle-test.js @@ -0,0 +1,53 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import chai, { expect } from 'chai'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import React from 'react'; +import Toggle from '../Toggle'; + +chai.use(sinonChai); + +function getSample (props) { + return ( + <Toggle value={true} onChange={() => true} {...props}/>); +} + +function click (element) { + return element.simulate('click', { + currentTarget: { blur () {} }, + preventDefault () {} + }); +} + +describe('Components :: Controls :: Toggle', () => { + it('should render', () => { + const Toggle = shallow(getSample()); + expect(Toggle.is('button')).to.equal(true); + }); + + it('should call onChange', () => { + const onChange = sinon.spy(); + const Toggle = shallow(getSample({ onChange })); + click(Toggle); + expect(onChange).to.have.been.calledWith(false); + }); +}); diff --git a/server/sonar-web/src/main/js/components/controls/styles.css b/server/sonar-web/src/main/js/components/controls/styles.css index 1ed5c1f4e52..b67f7f5eb71 100644 --- a/server/sonar-web/src/main/js/components/controls/styles.css +++ b/server/sonar-web/src/main/js/components/controls/styles.css @@ -24,3 +24,54 @@ .date-input-control-input:focus + .date-input-control-icon path { fill: #4b9fd5; } + +.boolean-toggle { + display: inline-block; + vertical-align: middle; + width: 48px; + height: 24px; + padding: 1px; + border: 1px solid #cdcdcd; + border-radius: 24px; + box-sizing: border-box; + background-color: #fff; + cursor: pointer; + transition: all 0.3s ease; +} + +.boolean-toggle:hover { + background-color: #fff; +} + +.boolean-toggle:focus { + border-color: #4b9fd5; + background-color: #f6f6f6; +} + +.boolean-toggle-handle { + width: 20px; + height: 20px; + border: 1px solid #cdcdcd; + border-radius: 22px; + box-sizing: border-box; + background-color: #f6f6f6; + transition: transform 0.3s cubic-bezier(.87,-.41,.19,1.44), border 0.3s ease; +} + +.boolean-toggle-on { + border-color: #236a97; + background-color: #236a97; +} + +.boolean-toggle-on:hover { + background-color: #236a97; +} + +.boolean-toggle-on:focus { + background-color: #236a97; +} + +.boolean-toggle-on .boolean-toggle-handle { + border-color: #f6f6f6; + transform: translateX(24px); +} diff --git a/server/sonar-web/src/main/js/helpers/l10n.js b/server/sonar-web/src/main/js/helpers/l10n.js index c3eadcab7bb..f2ea4d2b336 100644 --- a/server/sonar-web/src/main/js/helpers/l10n.js +++ b/server/sonar-web/src/main/js/helpers/l10n.js @@ -36,6 +36,11 @@ export function translateWithParameters (messageKey, ...parameters) { } } +export function hasMessage (...keys) { + const messageKey = keys.join('.'); + return messages[messageKey] != null; +} + function getCurrentLocale () { return window.navigator.languages ? window.navigator.languages[0] : window.navigator.language; } diff --git a/server/sonar-web/src/main/js/helpers/request.js b/server/sonar-web/src/main/js/helpers/request.js index bf88a0658be..ab92b6aaec3 100644 --- a/server/sonar-web/src/main/js/helpers/request.js +++ b/server/sonar-web/src/main/js/helpers/request.js @@ -201,5 +201,5 @@ export function requestDelete (url, data) { * @returns {Promise} */ export function delay (response) { - return new Promise(resolve => setTimeout(() => resolve(response), 3000)); + return new Promise(resolve => setTimeout(() => resolve(response), 500)); } diff --git a/server/sonar-web/src/main/less/components/react-select.less b/server/sonar-web/src/main/less/components/react-select.less index 05475d007e8..3ba2a2edaa2 100644 --- a/server/sonar-web/src/main/less/components/react-select.less +++ b/server/sonar-web/src/main/less/components/react-select.less @@ -92,7 +92,7 @@ left: 0; line-height: @formControlHeight; padding-left: 8px; - padding-right: 8px; + padding-right: 24px; position: absolute; right: 0; top: 0; diff --git a/server/sonar-web/src/main/less/init/forms.less b/server/sonar-web/src/main/less/init/forms.less index ffb3a31395a..bdc1c17ced3 100644 --- a/server/sonar-web/src/main/less/init/forms.less +++ b/server/sonar-web/src/main/less/init/forms.less @@ -30,6 +30,7 @@ input[type=password], input[type=email], input[type=search], input[type=date], +input[type=number], textarea, select { border: 1px solid @darkGrey; @@ -53,7 +54,8 @@ input[type=text], input[type=password], input[type=email], input[type=search], -input[type=date] { +input[type=date], +input[type=number] { height: @formControlHeight; padding: 0 6px; } @@ -132,6 +134,17 @@ input[type="submit"].button-red { } } +.button-success, +input[type="submit"].button-success { + border-color: @green; + color: @green; + + &:hover, &:focus, &.active { + background: @green; + color: #fff; + } +} + .button-clean, .button-clean:hover, .button-clean:focus { @@ -229,6 +242,12 @@ input[type="submit"].button-red { .input-super-large { width: 100%; max-width: 300px; + min-width: 200px; +} + +textarea.input-super-large { + max-width: 600px; + min-width: 300px; } em.mandatory { @@ -244,6 +263,7 @@ label[for] { display: inline-block; vertical-align: middle; font-size: 0; + white-space: nowrap; & > li { display: inline-block; diff --git a/server/sonar-web/src/main/less/init/tables.less b/server/sonar-web/src/main/less/init/tables.less index e157aebfbd4..142ed35ea8e 100644 --- a/server/sonar-web/src/main/less/init/tables.less +++ b/server/sonar-web/src/main/less/init/tables.less @@ -97,6 +97,26 @@ table.data.condensed > tbody > tr > td { padding-bottom: 5px; } +table.data.no-outer-padding > thead > tr { + > th:first-child { + padding-left: 0 + } + + > th:last-child { + padding-right: 0 + } +} + +table.data.no-outer-padding > tbody > tr { + > td:first-child { + padding-left: 0 + } + + > td:last-child { + padding-right: 0 + } +} + .data thead tr.total { background-color: #EFEFEF; font-weight: normal; diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb index a3dad4b490a..40393a473a0 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb @@ -28,6 +28,10 @@ class ProjectController < ApplicationController redirect_to :overwrite_params => {:controller => :dashboard, :action => 'index'} end + def settings + @project = get_current_project(params[:id]) + end + def deletion @project = get_current_project(params[:id]) end @@ -78,18 +82,6 @@ class ProjectController < ApplicationController :include => 'events', :order => 'snapshots.created_at DESC') end - def settings - @resource = get_current_project(params[:id]) - - if !java_facade.getResourceTypeBooleanProperty(@resource.qualifier, 'configurable') - redirect_to :action => 'index', :id => params[:id] - end - - @snapshot = @resource.last_snapshot - definitions_per_category = java_facade.propertyDefinitions.propertiesByCategory(@resource.qualifier) - processProperties(definitions_per_category) - end - def update_version snapshot=Snapshot.find(params[:sid], :include => 'project') not_found("Snapshot not found") unless snapshot diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb index e5bdc3f0c4f..ee4e72f070e 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb @@ -18,96 +18,10 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # class SettingsController < ApplicationController - SECTION=Navigation::SECTION_CONFIGURATION - - verify :method => :post, :only => %w(update), :redirect_to => {:action => :index} - before_filter :admin_required, :only => %w(index) + before_filter :admin_required def index - load_properties() - end - - def update - resource_id = params[:resource_id] - @resource = Project.by_key(resource_id) if resource_id - - access_denied if (@resource && !is_admin?(@resource)) - access_denied if (@resource.nil? && !is_admin?) - - load_properties() - - @updated_properties = {} - update_properties(resource_id) - update_property_sets(resource_id) - - render :partial => 'settings/properties' - end - - private - - def update_properties(resource_id) - (params[:settings] || []).each do |key, value| - update_property(key, value, resource_id) - end - end - def update_property_sets(resource_id) - (params[:property_sets] || []).each do |key, set_keys| - update_property_set(key, set_keys, params[key], resource_id, params[:auto_generate] && params[:auto_generate][key]) - end end - - def update_property_set(key, set_keys, fields_hash, resource_id, auto_generate) - if auto_generate - max = (Time.now.to_f * 100000).to_i - set_keys.each_with_index do |v, index| - if v.blank? - max += 1; - set_keys[index] = max.to_s - end - end - end - - set_key_values = {} - fields_hash.each do |field_key, field_values| - field_values.zip(set_keys).each do |field_value, set_key| - set_key_values[set_key] ||= {} - set_key_values[set_key][field_key] = field_value - end - end - - set_keys.reject! { |set_key| set_key.blank? || (auto_generate && set_key_values[set_key].values.all?(&:blank?)) } - - Property.transaction do - # Delete only property sets that are no more existing - condition = "prop_key LIKE '" + key + ".%' AND " - set_keys.each {|set_key| condition += "prop_key NOT LIKE ('#{key + '.' + set_key + '.%'}') AND "} - if resource_id - condition += 'resource_id=' + resource_id - else - condition += 'resource_id IS NULL' - end - Property.delete_all(condition) - - update_property(key, set_keys, resource_id) - set_keys.each do |set_key| - update_property("#{key}.#{set_key}.key", set_key, resource_id) unless auto_generate - - set_key_values[set_key].each do |field, value| - update_property("#{key}.#{set_key}.#{field}", value, resource_id) - end - end - end - end - - def update_property(key, value, resource_id) - @updated_properties[key] = Property.set(key, value, resource_id) - end - - def load_properties - definitions_per_category = java_facade.propertyDefinitions.propertiesByCategory(@resource.nil? ? nil : @resource.qualifier) - processProperties(definitions_per_category) - end - end diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/settings.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/settings.html.erb index ce82fb311bc..c0f9f94d9f0 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/settings.html.erb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/settings.html.erb @@ -1,5 +1,3 @@ -<div class="page" name="settings"> - <div class="yui-g widget" id="widget_plugins"> - <%= render :partial => 'settings/settings' %> - </div> -</div> +<% content_for :extra_script do %> +<script src="<%= ApplicationController.root_context -%>/js/bundles/settings.js?v=<%= sonar_version -%>"></script> +<% end %> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_error.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_error.html.erb deleted file mode 100644 index 6dff575b2b0..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_error.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<% updated_property = @updated_properties[key] if @updated_properties -%> - -<% if updated_property && !updated_property.valid? -%> - <div class="error"><%= updated_property.validation_error_message -%></div> -<% end -%> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_multi_value.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_multi_value.html.erb deleted file mode 100644 index 06897360059..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_multi_value.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<div class="multi_value marginbottom5"> - <%= render "settings/single_value", :property => property, :value => value -%> - <a href="#" class="delete link-action" style="<%= 'display:none;' if hide_delete -%>"><%= message('delete') -%></a> - <br/> -</div> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_properties.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_properties.html.erb deleted file mode 100644 index b19a33736ed..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_properties.html.erb +++ /dev/null @@ -1,216 +0,0 @@ -<% if @category.isSpecial -%> - <%= render 'special', :url => url_for(:controller => "#{@category.key}_configuration") -%> -<% else -%> - <form onsubmit="$j('#submit_settings').hide(); - $j('#loading_settings').show(); - $j.ajax({ url:'<%= url_for :controller => 'settings', :action => 'update', :category => @category.key, :subcategory => @subcategory.key, :resource_id => (@resource && @resource.id) -%>', - type:'post', - success:function(responseHTML){$j('#properties').html($j(responseHTML));$j('#loading_settings').hide();$j('#submit_settings').show()}, - data:$j(this).serialize()}); - return false;" - method='post' - action='#' - autocomplete="off" - > - <% subcategories = @subcategories_per_categories[@category] || [] -%> - <ul class="tabs"> - <% subcategories.each do |subcategory| -%> - <li> - <% if @resource %> - <a <% if @subcategory==subcategory %>class="selected"<% end -%> href="<%= url_for(:controller => 'project', :action => 'settings', :id => nil) -%>?id=<%= url_encode(@resource.key) -%>&category=<%= url_encode(@category.key) -%>&subcategory=<%= url_encode(subcategory.key) -%>"><%= h subcategory_name(@category, subcategory) -%></a> - <% else %> - <%= link_to subcategory_name(@category, subcategory), {:controller => 'settings', :action =>'index', :category => @category.key, :subcategory => subcategory.key}, :class => @subcategory==subcategory ? 'selected' : nil -%> - <% end %> - </li> - <% end -%> - </ul> - <% if @subcategory.isSpecial %> - <%= render 'special', :url => url_for(:controller => "#{@subcategory.key}_configuration") -%> - <% else %> - <% if @subcategory.key == @category.key && !category_desc(@category).blank? -%> - <p class="categoryDescription"><%= category_desc(@category) -%> </p> - <% end -%> - <% if @subcategory.key != @category.key && !subcategory_desc(@category, @subcategory).blank? -%> - <p class="categoryDescription" colspan="2"><%= subcategory_desc(@category, @subcategory) -%> </p> - <% end -%> - - <table class="marginbottom10"> - <tbody> - <% by_property_index_or_name(@definitions).each do |property| -%> - <tr class="property" id="block_<%= property.key -%>"> - <th> - <h3><%= property_name(property) -%></h3> - </th> - <td> - <% value = property_value(property) -%> - <% if property.multi_values -%> - <% value.each_with_index do |sub_value, index| -%> - <%= render "settings/multi_value", :property => property, :value => sub_value, :hide_delete => index == 0 -%> - <% end -%> - <div class="template" style="display:none;"> - <%= render "settings/multi_value", :property => property, :value => nil, :hide_delete => false -%> - </div> - <button class="add_value"><%= message('settings.add') -%></button> - <br/> - <% else -%> - <%= render "settings/single_value", :property => property, :value => value -%> - <% end -%> - - <%= render "settings/error", :key => property.key -%> - - <!-- SONAR-4707 Don't display default value for property sets --> - <% if property.fields.blank? %> - <% default_prop_value = (@resource ? Property.value(property.key, nil, property.defaultValue) : property.defaultValue) -%> - <% unless default_prop_value.blank? -%> - <div class="note"><%= message('default') %>: <%= property.type.to_s=='PASSWORD' ? '********' : h(default_prop_value) -%></div> - <% else -%> - <!-- SONAR-5162 When no default value, leave a space to add a separation with the description or the key --> - <p class="marginbottom10"></p> - <% end -%> - <% end -%> - - <% desc=property_description(property) -%> - <% unless desc.blank? %> - <p class="marginbottom10"><%= desc -%></p> - <% end -%> - <div class="note"><%= message('key') -%>: <%= property.key -%></div> - </td> - </tr> - <% end -%> - </tbody> - - </table> - - <% unless @definitions.empty? %> - <div class="marginbottom10" style="padding-left: 5px;"> - <%= hidden_field_tag('page_version', (params[:page_version] || 0).to_i + 1) -%> - <%= submit_tag(message('settings.save_category', :params => [subcategory_name(@category, @subcategory)]), :id => 'submit_settings') -%> - <img src="<%= ApplicationController.root_context -%>/images/loading.gif" id="loading_settings" style="display:none;"> - </div> - <% end %> - - <% if @category.key() == 'exclusions' -%> - <div class="help marginbottom10" style="margin-left: -1px"> - <h2>Wildcards</h2> - <p>Following rules are applied:</p> - <table class="data"> - <thead><tr><th colspan="2"></th></tr></thead> - <tr> - <td>*</td> - <td>Match zero or more characters</td> - </tr> - <tr> - <td>**</td> - <td>Match zero or more directories</td> - </tr> - <tr> - <td>?</td> - <td>Match a single character</td> - </tr> - </table> - <br> - <table class="data"> - <thead><tr><th>Example</th><th>Matches</th><th>Does not match</th></tr></thead> - <tbody> - <tr> - <td>**/foo/*.js</td> - <td> - <ul> - <li>src/foo/bar.js</li> - <li>lib/ui/foo/bar.js</li> - </ul> - </td> - <td> - <ul> - <li>src/bar.js</li> - <li>src/foo2/bar.js</li> - </ul> - </td> - </tr> - <tr> - <td>src/foo/*bar*.js</td> - <td> - <ul> - <li>src/foo/bar.js</li> - <li>src/foo/bar1.js</li> - <li>src/foo/bar123.js</li> - <li>src/foo/123bar123.js</li> - </ul> - </td> - <td> - <ul> - <li>src/foo/ui/bar.js</li> - <li>src/bar.js</li> - </ul> - </td> - </tr> - <tr> - <td>src/foo/**</td> - <td> - <ul> - <li>src/foo/bar.js</li> - <li>src/foo/ui/bar.js</li> - </ul> - </td> - <td> - <ul> - <li>src/bar/foo/bar.js</li> - <li>src/bar.js</li> - </ul> - </td> - </tr> - <tr> - <td>**/foo?.js</td> - <td> - <ul> - <li>src/foo1.js</li> - <li>src/bar/foo1.js</li> - </ul> - </td> - <td> - <ul> - <li>src/foo.js</li> - <li>src/foo12.js</li> - <li>src/12foo3.js</li> - </ul> - </td> - </tr> - </tbody> - </table> - </div> - <% else - help = category_help(@category) - unless help.blank? - -%> - <div class="help marginbottom10" style="margin-left: -1px"> - <%= help -%> - </div> - <% end - end - -%> - - <% end %> - </form> - <% end -%> - - - -<script> - $j('#properties > form') - .on('click', '.delete', function () { - $j(this).parents('.multi_value').remove(); - return false; - }) - .on('click', '.add_value', function () { - var template = $j(this).parents('.property').find('.template').last(); - template.clone().insertBefore(template).show(); - return false; - }) - .on('keypress', 'form', function (e) { - if (e.which == 13 && e.target.nodeName != "TEXTAREA") { - /* See https://jira.sonarsource.com/browse/SONAR-4363 */ - submit_settings.click(); - return false; - } - }); -</script> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_set_instance.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_set_instance.html.erb deleted file mode 100644 index 10d29f440af..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_set_instance.html.erb +++ /dev/null @@ -1,42 +0,0 @@ -<% errors = [] -%> -<% key_field = key_field(property) -%> - -<tr class="text-top multi_value <%= 'template' unless set_key -%> odd" style="<%= 'display:none' unless set_key -%>"> - <% unless key_field -%> - <%= hidden_field_tag "property_sets[#{property.key}][]", set_key -%> - <% end -%> - - <% property.fields.each do |field| -%> - <% if set_key -%> - <% key = "#{property.key}.#{set_key}.#{field.key}" -%> - <% value = Property.value(key, resource_id) -%> - <% errors << (render "settings/error", :key => key) -%> - <% end -%> - - <% if field == key_field -%> - <td><%= render "settings/type_#{field.type}", :property => field, :field => field, :value => value, :name => "property_sets[#{property.key}][]", :id => "input_#{h field.key}", :size => field.indicativeSize -%></td> - <% else -%> - <td><%= render "settings/type_#{field.type}", :property => field, :field => field, :value => value, :name => "#{property.key}[#{field.key}][]", :id => "input_#{h field.key}", :size => field.indicativeSize -%></td> - <% end -%> - <% end -%> - - <td style="width: 60px;"> - <% unless hide_delete -%> - <a href="#" class="delete link-action"><%= message('delete') -%></a> - <% end -%> - </td> -</tr> - -<% unless errors.all?(&:blank?) -%> - <tr> - <% if key_field -%> - <td></td> - <% end -%> - - <% errors.each do |error| -%> - <td><%= error -%></td> - <% end -%> - - <td></td> - </tr> -<% end -%> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_settings.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_settings.html.erb deleted file mode 100644 index 8ae1f0c2c7a..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_settings.html.erb +++ /dev/null @@ -1,42 +0,0 @@ -<div id="plugins"> - <header class="page-header"> - <h1 class="page-title"><%= message(@resource ? 'project_settings.page' : 'settings.page') -%></h1> - <p class="page-description"><%= message(@resource ? 'project_settings.page.description' : 'settings.page.description') -%> </p> - </header> - - <table width="100%"> - <tr> - <td width="1%" nowrap class="column first"> - <table class="data"> - <thead> - <tr> - <th><%= message('category') -%></th> - </tr> - </thead> - <tbody> - <% @categories.each do |category| -%> - <% if !category.key.blank?-%> - <tr id="select_<%= category.key -%>" class="select <%= cycle('even', 'odd', :name => 'category') -%> <%= 'selected' if @category.key==category.key -%>"> - <td class="category"> - <% if @resource %> - <a href="<%= url_for(:controller => 'project', :action => 'settings') -%>?id=<%= url_encode(@resource.key) -%>&category=<%= url_encode(category.key) -%>"><%= h category_name(category) -%></a> - <% else %> - <%= link_to category_name(category), :category => category.key -%> - <% end %> - </td> - </tr> - <% end -%> - <% end -%> - </tbody> - </table> - <br/> - </td> - - <td class="column"> - <div id="properties"> - <%= render 'settings/properties' -%> - </div> - </td> - </tr> - </table> -</div> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_single_value.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_single_value.html.erb deleted file mode 100644 index f9a0bcaa091..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_single_value.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render "settings/type_#{property_type(property, value)}", :property => property, :field => nil, :value => value, :name => input_name(property), :id => "input_#{h property.key}" -%> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_special.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_special.html.erb deleted file mode 100644 index afa5e1cffde..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_special.html.erb +++ /dev/null @@ -1 +0,0 @@ -<iframe src="<%= url -%>" width="100%" height="900" frameborder="0" style="overflow-y: auto;" name="settings_iframe"></iframe> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_BOOLEAN.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_BOOLEAN.html.erb deleted file mode 100644 index 74e6c8c8635..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_BOOLEAN.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= property_input_field(name, PropertyType::TYPE_BOOLEAN, value, PropertiesHelper::SCREEN_SETTINGS, {:id => id, :default => (defined? property.defaultValue) ? property.defaultValue : nil }) %>
\ No newline at end of file diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_FLOAT.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_FLOAT.html.erb deleted file mode 100644 index 721480a62aa..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_FLOAT.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<% - options = {:id => id} - options[:size] = (defined? size) ? size : nil -%> -<%= property_input_field(name, PropertyType::TYPE_FLOAT, value, PropertiesHelper::SCREEN_SETTINGS, options) %>
\ No newline at end of file diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_INTEGER.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_INTEGER.html.erb deleted file mode 100644 index 5f7c39e8c7b..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_INTEGER.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<% - options = {:id => id} - options[:size] = (defined? size) ? size : nil -%> -<%= property_input_field(name, PropertyType::TYPE_INTEGER, value, PropertiesHelper::SCREEN_SETTINGS, options) %>
\ No newline at end of file diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_LICENSE.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_LICENSE.html.erb deleted file mode 100644 index aeaa68efed4..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_LICENSE.html.erb +++ /dev/null @@ -1,62 +0,0 @@ -<% if !value || value.blank? %> - <textarea rows="5" cols="80" class="width100" name="<%= name -%>" id="<%= id -%>"></textarea> -<% - else - license = controller.java_facade.parseLicense(value) - product = license.getProduct() || '-' - # super-hack here - # should be avoided in the future - does_product_match = property.key.include? product -%> - <div class="width100"> - <textarea rows="6" name="<%= name -%>" id="<%= id -%>" style="float: left;width: 390px"><%= h value -%></textarea> - - <div style="margin-left: 400px"> - <table> - <tr> - <td class="form-key-cell <% if !does_product_match -%>bg-danger<% end -%>">Product:</td> - <td class="form-val-cell <% if !does_product_match -%>bg-danger<% end -%>"><%= product -%></td> - </tr> - <tr> - <td class="form-key-cell">Organization:</td> - <td><%= license.getOrganization() || '-' -%></td> - </tr> - <tr> - <td class="form-key-cell">Expiration:</td> - <td> - <% if license.getExpirationDate() - formatted_date = l(Date.parse(license.getExpirationDateAsString())) - %> - <%= license.isExpired() ? "<span class='error'>#{formatted_date}</span>" : formatted_date -%> - <% else %> - - - <% end %> - - </td> - </tr> - <tr> - <td class="form-key-cell">Type:</td> - <td><%= license.getType() || '-' -%></td> - </tr> - <tr> - <td class="form-key-cell">Server:</td> - <td> - <% if license.getServer() && - license.getServer() != "*" && - controller.java_facade.getConfigurationValue("sonar.server_id") != license.getServer() %> - <span class='error'><%= license.getServer() -%></span> - <% else %> - <%= license.getServer() || '-' -%> - <% end %> - </td> - </tr> - <% license.additionalProperties().each do |k,v| -%> - <tr> - <td class="form-key-cell"><%= k -%>:</td> - <td><%= v || '-' -%></td> - </tr> - <% end %> - </table> - </div> - </div> -<% end %> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_METRIC.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_METRIC.html.erb deleted file mode 100644 index 18e706b7f26..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_METRIC.html.erb +++ /dev/null @@ -1,24 +0,0 @@ -<% - defaultValue = (defined? property.defaultValue) ? property.defaultValue : nil -%> -<select name="<%= name -%>" id="<%= id -%>"> - <option value=""><%= !defaultValue.blank? ? message('default') : nil -%></option> - <% - metrics_per_domain={} - metrics_filtered_by(property.options).each do |metric| - domain=metric.domain || '' - metrics_per_domain[domain]||=[] - metrics_per_domain[domain]<<metric - end - - metrics_per_domain.keys.sort.each do |domain| - %> - <optgroup label="<%= h domain -%>"> - <% metrics_per_domain[domain].each do |m| - selected_attr = (m.key==value || m.id==value) ? " selected='selected'" : '' - %> - <option value="<%= m.key -%>" <%= selected_attr -%>><%= m.short_name -%></option> - <% end -%> - </optgroup> - <% end -%> -</select>
\ No newline at end of file diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PASSWORD.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PASSWORD.html.erb deleted file mode 100644 index d8c1556936c..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PASSWORD.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -<% - options = {:id => id} - options[:size] = (defined? size) ? size : nil -%> - -<% value = Property::EXISTING_PASSWORD unless value.blank? %> -<%= property_input_field(name, PropertyType::TYPE_PASSWORD, value, PropertiesHelper::SCREEN_SETTINGS, options) %> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PROPERTY_SET.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PROPERTY_SET.html.erb deleted file mode 100644 index ae8159a9df4..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PROPERTY_SET.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<% choices = Property.values(property.propertySetKey).reject(&:blank?) -%> -<% prompt = [[message('default'), '']] -%> - -<% if !value.blank? && (choices.exclude? value) -%> - <%= image_tag 'exclamation.png' -%> - <% missing = [[h(value + ' <' + message('deleted') + '>'), value]] -%> -<% else -%> - <% missing = [] -%> -<% end -%> - -<%= select_tag name, options_for_select(prompt + choices + missing, value), :id => id -%>
\ No newline at end of file diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PROPERTY_SET_DEFINITION.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PROPERTY_SET_DEFINITION.html.erb deleted file mode 100644 index beec593636c..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PROPERTY_SET_DEFINITION.html.erb +++ /dev/null @@ -1,38 +0,0 @@ -<% resource_id = @resource.id if @resource -%> - -<table class="data"> - <thead> - <tr> - <% unless key_field(property) -%> - <%= hidden_field_tag "auto_generate[#{property.key}]", true -%> - <% end -%> - <% property.fields.each do |field| -%> - <th> - <%= field_name(property, field) -%> - <% desc = field_description(property, field) -%> - <% unless desc.blank? %> - <p class="note"><%= desc -%></p> - <% end -%> - </th> - <% end -%> - <th></th> - </tr> - </thead> - - <tbody> - <% set_keys = Property.values(property.key, resource_id) -%> - <% set_keys = [''] if set_keys.all?(&:blank?) -%> - <% set_keys.each_with_index do |set_key, index| -%> - <%= render 'settings/set_instance', :property => property, :set_key => set_key, :resource_id => resource_id, :hide_delete => (index == 0) %> - <% end -%> - <%= render 'settings/set_instance', :property => property, :set_key => nil, :resource_id => resource_id, :hide_delete => false %> - </tbody> - - <tfoot> - <tr> - <td colspan="<%= property.fields.size + 1 -%>"> - <button class="add_value"><%= message('settings.add') -%></button> - </td> - </tr> - </tfoot> -</table> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_REGULAR_EXPRESSION.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_REGULAR_EXPRESSION.html.erb deleted file mode 100644 index 8274fd8ef8f..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_REGULAR_EXPRESSION.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<% - options = {:id => id} - options[:size] = (defined? size) ? size : nil -%> -<%= property_input_field(name, PropertyType::TYPE_REGULAR_EXPRESSION, value, PropertiesHelper::SCREEN_SETTINGS, options) %>
\ No newline at end of file diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_SINGLE_SELECT_LIST.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_SINGLE_SELECT_LIST.html.erb deleted file mode 100644 index 4153c332d13..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_SINGLE_SELECT_LIST.html.erb +++ /dev/null @@ -1,2 +0,0 @@ -<%= property_input_field(name, PropertyType::TYPE_SINGLE_SELECT_LIST, value, PropertiesHelper::SCREEN_SETTINGS, - {:id => id, :default => (defined? property.defaultValue) ? property.defaultValue : nil, :values => property.options, :extra_values => {:property => property, :field => field}}) %>
\ No newline at end of file diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_STRING.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_STRING.html.erb deleted file mode 100644 index b28c75604a7..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_STRING.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<% - options = {:id => id} - options[:size] = (defined? size) ? size : nil -%> -<%= property_input_field(name, PropertyType::TYPE_STRING, value, PropertiesHelper::SCREEN_SETTINGS, options) %>
\ No newline at end of file diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_TEXT.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_TEXT.html.erb deleted file mode 100644 index af46cf40082..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_TEXT.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<% - options = {:id => id} - options[:size] = (defined? size) ? size : nil -%> -<%= property_input_field(name, PropertyType::TYPE_TEXT, value, PropertiesHelper::SCREEN_SETTINGS, options) %> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_USER_LOGIN.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_USER_LOGIN.html.erb deleted file mode 100644 index 6f94fa6a651..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_USER_LOGIN.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<% - options = {:id => id} - options[:size] = (defined? size) ? size : nil -%> -<%= property_input_field(name, PropertyType::TYPE_USER_LOGIN, value, PropertiesHelper::SCREEN_SETTINGS, options) %> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/index.html.erb index 55ae2ad65ec..6c42ad1d4b7 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/index.html.erb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/index.html.erb @@ -1,3 +1,3 @@ -<div class="page"> - <%= render 'settings', :project => nil %> -</div> +<% content_for :extra_script do %> + <script src="<%= ApplicationController.root_context -%>/js/bundles/settings.js?v=<%= sonar_version -%>"></script> +<% end %> diff --git a/server/sonar-web/tests/utils.js b/server/sonar-web/tests/utils.js new file mode 100644 index 00000000000..afa4f53d0d4 --- /dev/null +++ b/server/sonar-web/tests/utils.js @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const click = element => { + return element.simulate('click', { + target: { blur () {} }, + preventDefault () {} + }); +}; + +export const submit = element => { + return element.simulate('submit', { + preventDefault () {} + }); +}; + +export const change = (element, value) => { + return element.simulate('change', { + target: { value }, + currentTarget: { value } + }); +}; diff --git a/sonar-application/pom.xml b/sonar-application/pom.xml index 8656a2f2718..3397de70d90 100644 --- a/sonar-application/pom.xml +++ b/sonar-application/pom.xml @@ -238,8 +238,8 @@ <configuration> <rules> <requireFilesSize> - <minsize>120000000</minsize> - <maxsize>128000000</maxsize> + <minsize>125000000</minsize> + <maxsize>132000000</maxsize> <files> <file>${project.build.directory}/sonarqube-${project.version}.zip</file> </files> diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index a21b3887b78..8b42dba8ba5 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -147,6 +147,7 @@ search_verb=Search see_all=See All select_all=Select all select_verb=Select +set=Set severity=Severity severity_abbreviated=Se. shared=Shared @@ -967,6 +968,20 @@ dashboard.default_dashboard=This dashboard is the default one and is displayed w #------------------------------------------------------------------------------ settings.add=Add value settings.save_category=Save {0} Settings +settings.key_x=Key: {0} +settings.default_x=Default: {0} +settings.not_set=(not set) +settings.state.saving=Saving... +settings.state.saved=Saved! +settings.state.validation_failed=Validation failed. {0} +settings.state.value_cant_be_empty=Value can't be empty. Use "Reset" to set value to the default one. +settings._default=(default) +settings.boolean.true=True +settings.boolean.false=False +settings.default.no_value=<no value> +settings.default.complex_value=<complex value> +settings.default.password=<password> + property.category.general=General property.category.general.email=Email property.category.general.duplications=Duplications |