aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2016-09-01 17:30:34 +0200
committerGitHub <noreply@github.com>2016-09-01 17:30:34 +0200
commitd2da7f30d512284c943b82abc135483b59b85536 (patch)
tree71a76b276c94a8025ae8b9e8d8be5387b356a1eb /server/sonar-web/src/main/js
parent11de4cfe580382a23ac664c5b4464d426b742a3e (diff)
downloadsonarqube-d2da7f30d512284c943b82abc135483b59b85536.tar.gz
sonarqube-d2da7f30d512284c943b82abc135483b59b85536.zip
SONAR-5856 Rewrite Settings page (#1163)
Diffstat (limited to 'server/sonar-web/src/main/js')
-rw-r--r--server/sonar-web/src/main/js/api/settings.js69
-rw-r--r--server/sonar-web/src/main/js/api/users.js6
-rw-r--r--server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.js46
-rw-r--r--server/sonar-web/src/main/js/apps/settings/app.js50
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.js37
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/App.js101
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js71
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.js37
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/Definition.js195
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/DefinitionChanges.js59
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/DefinitionDefaults.js60
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js45
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/GlobalMessagesContainer.js28
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/PageHeader.js44
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js62
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/Input.js51
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.js48
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.js29
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.js89
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.js50
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.js29
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/InputForText.js40
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.js90
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.js75
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.js110
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/SimpleInput.js45
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.js60
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.js77
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForNumber-test.js43
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.js97
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.js66
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForString-test.js43
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.js60
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.js103
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.js66
-rw-r--r--server/sonar-web/src/main/js/apps/settings/constants.js28
-rw-r--r--server/sonar-web/src/main/js/apps/settings/propTypes.js27
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/actions.js85
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/definitions/actions.js30
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/definitions/reducer.js56
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/rootReducer.js55
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/actions.js33
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/reducer.js37
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/actions.js31
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/reducer.js36
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/settingsPage/reducer.js38
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/actions.js33
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/reducer.js36
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/values/actions.js30
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/values/reducer.js34
-rw-r--r--server/sonar-web/src/main/js/apps/settings/styles.css157
-rw-r--r--server/sonar-web/src/main/js/apps/settings/utils.js140
-rw-r--r--server/sonar-web/src/main/js/components/controls/Toggle.js51
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/Toggle-test.js53
-rw-r--r--server/sonar-web/src/main/js/components/controls/styles.css51
-rw-r--r--server/sonar-web/src/main/js/helpers/l10n.js5
-rw-r--r--server/sonar-web/src/main/js/helpers/request.js2
57 files changed, 3228 insertions, 1 deletions
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>&nbsp;</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));
}