diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-09-01 17:30:34 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-09-01 17:30:34 +0200 |
commit | d2da7f30d512284c943b82abc135483b59b85536 (patch) | |
tree | 71a76b276c94a8025ae8b9e8d8be5387b356a1eb /server | |
parent | 11de4cfe580382a23ac664c5b4464d426b742a3e (diff) | |
download | sonarqube-d2da7f30d512284c943b82abc135483b59b85536.tar.gz sonarqube-d2da7f30d512284c943b82abc135483b59b85536.zip |
SONAR-5856 Rewrite Settings page (#1163)
Diffstat (limited to 'server')
87 files changed, 3321 insertions, 602 deletions
diff --git a/server/sonar-web/config/webpack/webpack.config.base.js b/server/sonar-web/config/webpack/webpack.config.base.js index 0533884f245..2e0dcc48236 100644 --- a/server/sonar-web/config/webpack/webpack.config.base.js +++ b/server/sonar-web/config/webpack/webpack.config.base.js @@ -45,6 +45,7 @@ module.exports = { 'projects': './src/main/js/apps/projects/app.js', 'quality-gates': './src/main/js/apps/quality-gates/app.js', 'quality-profiles': './src/main/js/apps/quality-profiles/app.js', + 'settings': './src/main/js/apps/settings/app.js', 'source-viewer': './src/main/js/apps/source-viewer/app.js', 'system': './src/main/js/apps/system/app.js', 'update-center': './src/main/js/apps/update-center/app.js', diff --git a/server/sonar-web/scripts/start.js b/server/sonar-web/scripts/start.js index ceb9d61928a..905793318e2 100644 --- a/server/sonar-web/scripts/start.js +++ b/server/sonar-web/scripts/start.js @@ -121,14 +121,10 @@ function setupCompiler () { function runDevServer (port) { app.use(require('webpack-dev-middleware')(compiler, { noInfo: true, - quiet: true, publicPath: config.output.publicPath })); - app.use(require('webpack-hot-middleware')(compiler, { - noInfo: true, - quiet: true - })); + app.use(require('webpack-hot-middleware')(compiler)); app.all('*', proxy(API_HOST, { forwardPath: function (req) { diff --git a/server/sonar-web/src/main/js/api/settings.js b/server/sonar-web/src/main/js/api/settings.js new file mode 100644 index 00000000000..14396243a2f --- /dev/null +++ b/server/sonar-web/src/main/js/api/settings.js @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import omitBy from 'lodash/omitBy'; +import { getJSON, post } from '../helpers/request'; +import { TYPE_PROPERTY_SET } from '../apps/settings/constants'; + +export function getDefinitions (componentKey) { + const url = '/api/settings/list_definitions'; + const data = componentKey ? { componentKey } : {}; + return getJSON(url, data).then(r => r.definitions); +} + +export function getValues (keys, componentKey) { + const url = '/api/settings/values'; + const data = { keys }; + if (componentKey) { + data.componentKey = componentKey; + } + return getJSON(url, data).then(r => r.settings); +} + +export function setSettingValue (definition, value, componentKey) { + const url = '/api/settings/set'; + + const { key } = definition; + const data = { key }; + + if (definition.multiValues) { + data.values = value; + } else if (definition.type === TYPE_PROPERTY_SET) { + data.fieldValues = value + .map(fields => omitBy(fields, value => value == null)) + .map(JSON.stringify); + } else { + data.value = value; + } + + if (componentKey) { + data.componentKey = componentKey; + } + + return post(url, data); +} + +export function resetSettingValue (key, componentKey) { + const url = '/api/settings/reset'; + const data = { key }; + if (componentKey) { + data.componentKey = componentKey; + } + return post(url, data); +} diff --git a/server/sonar-web/src/main/js/api/users.js b/server/sonar-web/src/main/js/api/users.js index 80865674d80..e5d5d15f7bc 100644 --- a/server/sonar-web/src/main/js/api/users.js +++ b/server/sonar-web/src/main/js/api/users.js @@ -39,3 +39,9 @@ export function getIdentityProviders () { const url = '/api/users/identity_providers'; return getJSON(url); } + +export function searchUsers (query) { + const url = '/api/users/search'; + const data = { q: query }; + return getJSON(url, data); +} diff --git a/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.js b/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.js new file mode 100644 index 00000000000..16b709d99ab --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.js @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { expect } from 'chai'; +import { getEmptyValue } from '../utils'; +import { TYPE_PROPERTY_SET, TYPE_STRING, TYPE_SINGLE_SELECT_LIST, TYPE_BOOLEAN } from '../constants'; + +const fields = [ + { key: 'foo', type: TYPE_STRING }, + { key: 'bar', type: TYPE_SINGLE_SELECT_LIST } +]; + +describe('Settings :: Utils', () => { + describe('#getEmptyValue()', () => { + it('should work for property sets', () => { + const setting = { type: TYPE_PROPERTY_SET, fields }; + expect(getEmptyValue(setting)).to.deep.equal([{ foo: '', bar: null }]); + }); + + it('should work for multi values string', () => { + const setting = { type: TYPE_STRING, multiValues: true }; + expect(getEmptyValue(setting)).to.deep.equal(['']); + }); + + it('should work for multi values boolean', () => { + const setting = { type: TYPE_BOOLEAN, multiValues: true }; + expect(getEmptyValue(setting)).to.deep.equal([null]); + }); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/app.js b/server/sonar-web/src/main/js/apps/settings/app.js new file mode 100644 index 00000000000..cf9dfcf07ad --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/app.js @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { render } from 'react-dom'; +import { Provider } from 'react-redux'; +import { Router, Route, Redirect, useRouterHistory } from 'react-router'; +import { createHistory } from 'history'; +import App from './components/App'; +import rootReducer from './store/rootReducer'; +import configureStore from '../../components/store/configureStore'; + +window.sonarqube.appStarted.then(options => { + const el = document.querySelector(options.el); + + const controller = options.component ? '/project/settings' : '/settings'; + const history = useRouterHistory(createHistory)({ + basename: window.baseUrl + controller + }); + + const store = configureStore(rootReducer); + + const withComponent = ComposedComponent => props => + <ComposedComponent {...props} component={options.component}/>; + + render(( + <Provider store={store}> + <Router history={history}> + <Redirect from="/index" to="/"/> + <Route path="/" component={withComponent(App)}/> + </Router> + </Provider> + ), el); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.js b/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.js new file mode 100644 index 00000000000..44d16ad6bdc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.js @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { connect } from 'react-redux'; +import CategoriesList from './CategoriesList'; +import { getAllCategories } from '../store/rootReducer'; + +class AllCategoriesList extends React.Component { + render () { + return <CategoriesList {...this.props}/>; + } +} + +const mapStateToProps = state => ({ + categories: getAllCategories(state) +}); + +export default connect( + mapStateToProps +)(AllCategoriesList); diff --git a/server/sonar-web/src/main/js/apps/settings/components/App.js b/server/sonar-web/src/main/js/apps/settings/components/App.js new file mode 100644 index 00000000000..424675ba502 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/App.js @@ -0,0 +1,101 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import { connect } from 'react-redux'; +import PageHeader from './PageHeader'; +import CategoryDefinitionsList from './CategoryDefinitionsList'; +import AllCategoriesList from './AllCategoriesList'; +import GlobalMessagesContainer from './GlobalMessagesContainer'; +import { fetchSettings } from '../store/actions'; +import { getDefaultCategory } from '../store/rootReducer'; +import '../styles.css'; + +class App extends React.Component { + static propTypes = { + component: React.PropTypes.object, + fetchSettings: React.PropTypes.func.isRequired, + defaultCategory: React.PropTypes.string + }; + + state = { loaded: false }; + + componentDidMount () { + document.querySelector('html').classList.add('dashboard-page'); + const componentKey = this.props.component ? this.props.component.key : null; + this.props.fetchSettings(componentKey).then(() => { + this.setState({ loaded: true }); + }); + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + componentDidUpdate (prevProps) { + if (prevProps.component !== this.props.component) { + const componentKey = this.props.component ? this.props.component.key : null; + this.props.fetchSettings(componentKey); + } + } + + componentWillUnmount () { + document.querySelector('html').classList.remove('dashboard-page'); + } + + render () { + if (!this.state.loaded) { + return null; + } + + const { query } = this.props.location; + const selectedCategory = query.category || this.props.defaultCategory; + + return ( + <div id="settings-page" className="page page-limited"> + <PageHeader component={this.props.component}/> + <GlobalMessagesContainer/> + <div className="settings-layout"> + <div className="settings-side"> + <AllCategoriesList + component={this.props.component} + selectedCategory={selectedCategory} + defaultCategory={this.props.defaultCategory}/> + </div> + <div className="settings-main"> + <CategoryDefinitionsList + component={this.props.component} + category={selectedCategory}/> + </div> + </div> + </div> + ); + } +} + +const mapStateToProps = state => ({ + defaultCategory: getDefaultCategory(state) +}); + +export default connect( + mapStateToProps, + { fetchSettings } +)(App); + diff --git a/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js b/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js new file mode 100644 index 00000000000..aa833d5ce90 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import sortBy from 'lodash/sortBy'; +import { IndexLink } from 'react-router'; +import { getCategoryName } from '../utils'; + +export default class CategoriesList extends React.Component { + static propTypes = { + categories: React.PropTypes.array.isRequired, + selectedCategory: React.PropTypes.string.isRequired, + defaultCategory: React.PropTypes.string.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + renderLink (category) { + const query = {}; + + if (category.key !== this.props.defaultCategory) { + query.category = category.key.toLowerCase(); + } + + if (this.props.component) { + query.id = this.props.component.key; + } + + const className = category.key.toLowerCase() === this.props.selectedCategory.toLowerCase() ? 'active' : ''; + + return ( + <IndexLink to={{ pathname: '/', query }} className={className} title={category.name}> + {category.name} + </IndexLink> + ); + } + + render () { + const categoriesWithName = this.props.categories.map(key => ({ key, name: getCategoryName(key) })); + const sortedCategories = sortBy(categoriesWithName, category => category.name.toLowerCase()); + + return ( + <ul className="settings-menu"> + {sortedCategories.map(category => ( + <li key={category.key}> + {this.renderLink(category)} + </li> + ))} + </ul> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.js b/server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.js new file mode 100644 index 00000000000..cc2e6d8c24e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.js @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { connect } from 'react-redux'; +import SubCategoryDefinitionsList from './SubCategoryDefinitionsList'; +import { getSettingsForCategory } from '../store/rootReducer'; + +class CategoryDefinitionsList extends React.Component { + render () { + return <SubCategoryDefinitionsList {...this.props}/>; + } +} + +const mapStateToProps = (state, ownProps) => ({ + settings: getSettingsForCategory(state, ownProps.category) +}); + +export default connect( + mapStateToProps +)(CategoryDefinitionsList); diff --git a/server/sonar-web/src/main/js/apps/settings/components/Definition.js b/server/sonar-web/src/main/js/apps/settings/components/Definition.js new file mode 100644 index 00000000000..66b19fd023a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/Definition.js @@ -0,0 +1,195 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { connect } from 'react-redux'; +import shallowCompare from 'react-addons-shallow-compare'; +import classNames from 'classnames'; +import Input from './inputs/Input'; +import DefinitionDefaults from './DefinitionDefaults'; +import DefinitionChanges from './DefinitionChanges'; +import { getPropertyName, getPropertyDescription, isEmptyValue, getSettingValue, isDefaultOrInherited } from '../utils'; +import { translateWithParameters, translate } from '../../../helpers/l10n'; +import { resetValue, saveValue } from '../store/actions'; +import { isLoading, getValidationMessage, getChangedValue } from '../store/rootReducer'; +import { failValidation, passValidation } from '../store/settingsPage/validationMessages/actions'; +import { cancelChange, changeValue } from '../store/settingsPage/changedValues/actions'; + +class Definition extends React.Component { + static propTypes = { + component: React.PropTypes.object, + setting: React.PropTypes.object.isRequired, + changedValue: React.PropTypes.any, + loading: React.PropTypes.bool.isRequired, + validationMessage: React.PropTypes.string, + + changeValue: React.PropTypes.func.isRequired, + cancelChange: React.PropTypes.func.isRequired, + saveValue: React.PropTypes.func.isRequired, + resetValue: React.PropTypes.func.isRequired, + failValidation: React.PropTypes.func.isRequired, + passValidation: React.PropTypes.func.isRequired + }; + + state = { + success: false + }; + + componentDidMount () { + this.mounted = true; + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + componentWillUnmount () { + this.mounted = false; + } + + safeSetState (changes) { + if (this.mounted) { + this.setState(changes); + } + } + + handleChange (value) { + clearTimeout(this.timeout); + return this.props.changeValue(this.props.setting.definition.key, value); + } + + handleReset () { + const componentKey = this.props.component ? this.props.component.key : null; + const { definition } = this.props.setting; + return this.props.resetValue(definition.key, componentKey).then(() => { + this.safeSetState({ success: true }); + this.timeout = setTimeout(() => this.safeSetState({ success: false }), 3000); + }).catch(() => { /* do nothing */ }); + } + + handleCancel () { + this.props.cancelChange(this.props.setting.definition.key); + this.props.passValidation(this.props.setting.definition.key); + } + + handleSave () { + this.safeSetState({ success: false }); + const { definition } = this.props.setting; + if (isEmptyValue(definition, this.props.changedValue)) { + this.props.failValidation(definition.key, translate('settings.state.value_cant_be_empty')); + return; + } + + const componentKey = this.props.component ? this.props.component.key : null; + this.props.saveValue(this.props.setting.definition.key, componentKey).then(() => { + this.safeSetState({ success: true }); + this.timeout = setTimeout(() => this.safeSetState({ success: false }), 3000); + }).catch(() => { /* do nothing */ }); + } + + render () { + const { setting, changedValue, loading } = this.props; + const { definition } = setting; + const propertyName = getPropertyName(definition); + + const hasValueChanged = changedValue != null; + + const className = classNames('settings-definition', { + 'settings-definition-changed': hasValueChanged + }); + + const effectiveValue = hasValueChanged ? changedValue : getSettingValue(setting); + + const isDefault = isDefaultOrInherited(setting) && !hasValueChanged; + + return ( + <div className={className} data-key={definition.key}> + <div className="settings-definition-left"> + <h3 className="settings-definition-name" title={propertyName}> + {propertyName} + </h3> + + <div className="settings-definition-description markdown small spacer-top" + dangerouslySetInnerHTML={{ __html: getPropertyDescription(definition) }}/> + + <div className="settings-definition-key note little-spacer-top"> + {translateWithParameters('settings.key_x', definition.key)} + </div> + </div> + + <div className="settings-definition-right"> + <Input setting={setting} value={effectiveValue} onChange={this.handleChange.bind(this)}/> + + {!hasValueChanged && ( + <DefinitionDefaults + setting={setting} + isDefault={isDefault} + onReset={() => this.handleReset()}/> + )} + + {hasValueChanged && ( + <DefinitionChanges + onSave={this.handleSave.bind(this)} + onCancel={this.handleCancel.bind(this)}/> + )} + + <div className="settings-definition-state"> + {loading && ( + <span className="text-info"> + <span className="settings-definition-state-icon"> + <i className="spinner"/> + </span> + {translate('settings.state.saving')} + </span> + )} + + {!loading && (this.props.validationMessage != null) && ( + <span className="text-danger"> + <span className="settings-definition-state-icon"> + <i className="icon-alert-error"/> + </span> + {translateWithParameters('settings.state.validation_failed', this.props.validationMessage)} + </span> + )} + + {!loading && this.state.success && ( + <span className="text-success"> + <span className="settings-definition-state-icon"> + <i className="icon-check"/> + </span> + {translate('settings.state.saved')} + </span> + )} + </div> + </div> + </div> + ); + } +} + +const mapStateToProps = (state, ownProps) => ({ + changedValue: getChangedValue(state, ownProps.setting.definition.key), + loading: isLoading(state, ownProps.setting.definition.key), + validationMessage: getValidationMessage(state, ownProps.setting.definition.key) +}); + +export default connect( + mapStateToProps, + { changeValue, saveValue, resetValue, failValidation, passValidation, cancelChange } +)(Definition); diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionChanges.js b/server/sonar-web/src/main/js/apps/settings/components/DefinitionChanges.js new file mode 100644 index 00000000000..edbeafecf9e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionChanges.js @@ -0,0 +1,59 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import { translate } from '../../../helpers/l10n'; + +export default class DefinitionChanges extends React.Component { + static propTypes = { + onSave: React.PropTypes.func.isRequired, + onCancel: React.PropTypes.func.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + handleSaveClick (e) { + e.preventDefault(); + e.target.blur(); + this.props.onSave(); + } + + handleCancelChange (e) { + e.preventDefault(); + e.target.blur(); + this.props.onCancel(); + } + + render () { + return ( + <div className="settings-definition-changes"> + <button className="js-save-changes button-success" onClick={e => this.handleSaveClick(e)}> + {translate('save')} + </button> + + <button className="js-cancel-changes big-spacer-left button-link" onClick={e => this.handleCancelChange(e)}> + {translate('cancel')} + </button> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionDefaults.js b/server/sonar-web/src/main/js/apps/settings/components/DefinitionDefaults.js new file mode 100644 index 00000000000..452b24c5ea5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionDefaults.js @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { getSettingValue, isEmptyValue, getDefaultValue } from '../utils'; +import { translate } from '../../../helpers/l10n'; + +export default class DefinitionDefaults extends React.Component { + static propTypes = { + setting: React.PropTypes.object.isRequired, + isDefault: React.PropTypes.bool.isRequired, + onReset: React.PropTypes.func.isRequired + }; + + handleReset (e) { + e.preventDefault(); + e.target.blur(); + this.props.onReset(); + } + + render () { + const { setting, isDefault } = this.props; + const { definition } = setting; + + const isExplicitlySet = !isDefault && !isEmptyValue(definition, getSettingValue(setting)); + + return ( + <div> + {isDefault && ( + <div className="spacer-top note" style={{ lineHeight: '24px' }}> + {translate('settings._default')} + </div> + )} + + {isExplicitlySet && ( + <div className="spacer-top nowrap"> + <button onClick={e => this.handleReset(e)}>{translate('reset_verb')}</button> + <span className="spacer-left note">{translate('default')}{': '}{getDefaultValue(setting)}</span> + </div> + )} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js b/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js new file mode 100644 index 00000000000..77d0d7fa95b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import Definition from './Definition'; + +export default class DefinitionsList extends React.Component { + static propTypes = { + component: React.PropTypes.object, + settings: React.PropTypes.array.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + render () { + return ( + <ul className="settings-definitions-list"> + {this.props.settings.map(setting => ( + <li key={setting.definition.key}> + <Definition component={this.props.component} setting={setting}/> + </li> + ))} + </ul> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/GlobalMessagesContainer.js b/server/sonar-web/src/main/js/apps/settings/components/GlobalMessagesContainer.js new file mode 100644 index 00000000000..f64ef91664b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/GlobalMessagesContainer.js @@ -0,0 +1,28 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { connect } from 'react-redux'; +import GlobalMessages from '../../../components/controls/GlobalMessages'; +import { getGlobalMessages } from '../store/rootReducer'; + +const mapStateToProps = state => ({ + messages: getGlobalMessages(state) +}); + +export default connect(mapStateToProps)(GlobalMessages); diff --git a/server/sonar-web/src/main/js/apps/settings/components/PageHeader.js b/server/sonar-web/src/main/js/apps/settings/components/PageHeader.js new file mode 100644 index 00000000000..34e402122cb --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/PageHeader.js @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { translate } from '../../../helpers/l10n'; + +export default class PageHeader extends React.Component { + static propTypes = { + component: React.PropTypes.object + }; + + render () { + const title = this.props.component != null ? + translate('project_settings.page') : + translate('settings.page'); + + const description = this.props.component != null ? + translate('project_settings.page.description') : + translate('settings.page.description'); + + return ( + <header className="page-header"> + <h1 className="page-title">{title}</h1> + <div className="page-description">{description}</div> + </header> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js new file mode 100644 index 00000000000..f61d6b2aa7a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import groupBy from 'lodash/groupBy'; +import sortBy from 'lodash/sortBy'; +import DefinitionsList from './DefinitionsList'; +import { getSubCategoryName, getSubCategoryDescription } from '../utils'; + +export default class SubCategoryDefinitionsList extends React.Component { + static propTypes = { + component: React.PropTypes.object, + settings: React.PropTypes.array.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + render () { + const bySubCategory = groupBy(this.props.settings, setting => setting.definition.subCategory); + const subCategories = Object.keys(bySubCategory).map(key => ({ + key, + name: getSubCategoryName(bySubCategory[key][0].definition.category, key), + description: getSubCategoryDescription(bySubCategory[key][0].definition.category, key) + })); + const sortedSubCategories = sortBy(subCategories, subCategory => subCategory.name.toLowerCase()); + + return ( + <ul className="settings-sub-categories-list"> + {sortedSubCategories.map(subCategory => ( + <li key={subCategory.key}> + <h2 className="settings-sub-category-name">{subCategory.name}</h2> + {subCategory.description != null && ( + <div className="settings-sub-category-description markdown"> + {subCategory.description} + </div> + )} + <DefinitionsList settings={bySubCategory[subCategory.key]} component={this.props.component}/> + </li> + ))} + </ul> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/Input.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/Input.js new file mode 100644 index 00000000000..02fcbfacef2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/Input.js @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import PropertySetInput from './PropertySetInput'; +import MultiValueInput from './MultiValueInput'; +import PrimitiveInput from './PrimitiveInput'; +import { TYPE_PROPERTY_SET } from '../../constants'; + +export default class Input extends React.Component { + static propTypes = { + setting: React.PropTypes.object.isRequired, + value: React.PropTypes.any, + onChange: React.PropTypes.func.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + render () { + const { definition } = this.props.setting; + + if (definition.multiValues) { + return <MultiValueInput {...this.props}/>; + } + + if (definition.type === TYPE_PROPERTY_SET) { + return <PropertySetInput {...this.props}/>; + } + + return <PrimitiveInput {...this.props}/>; + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.js new file mode 100644 index 00000000000..9f640591835 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.js @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import Toggle from '../../../../components/controls/Toggle'; +import { defaultInputPropTypes } from '../../propTypes'; +import { translate } from '../../../../helpers/l10n'; + +export default class InputForBoolean extends React.Component { + static propTypes = { + ...defaultInputPropTypes, + value: React.PropTypes.oneOfType([React.PropTypes.bool, React.PropTypes.string]) + }; + + render () { + const hasValue = this.props.value != null; + const displayedValue = hasValue ? this.props.value : false; + + return ( + <div className="display-inline-block text-top"> + <Toggle + name={this.props.name} + value={displayedValue} + onChange={this.props.onChange}/> + + {!hasValue && ( + <span className="spacer-left note">{translate('settings.not_set')}</span> + )} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.js new file mode 100644 index 00000000000..6f741da3730 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.js @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import SimpleInput from './SimpleInput'; + +export default class InputForNumber extends React.Component { + render () { + return ( + <SimpleInput {...this.props} className="input-small" type="text"/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.js new file mode 100644 index 00000000000..4e2a783ce11 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.js @@ -0,0 +1,89 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { translate } from '../../../../helpers/l10n'; +import { defaultInputPropTypes } from '../../propTypes'; + +export default class InputForPassword extends React.Component { + static propTypes = defaultInputPropTypes; + + state = { + changing: false + }; + + handleChangeClick (e) { + e.preventDefault(); + e.target.blur(); + this.setState({ changing: true }); + } + + handleCancelChangeClick (e) { + e.preventDefault(); + e.target.blur(); + this.setState({ changing: false }); + } + + handleFormSubmit (e) { + e.preventDefault(); + this.props.onChange(this.refs.input.value); + this.setState({ changing: false }); + } + + renderInput () { + return ( + <div> + <form onSubmit={e => this.handleFormSubmit(e)}> + <input className="hidden" type="password"/> + <input + ref="input" + name={this.props.name} + className="input-large text-top" + type="password" + autoFocus={true} + autoComplete={false}/> + <button className="spacer-left">{translate('set')}</button> + <a className="spacer-left" href="#" onClick={e => this.handleCancelChangeClick(e)}> + {translate('cancel')} + </a> + </form> + </div> + ); + } + + render () { + if (this.state.changing) { + return this.renderInput(); + } + + const hasValue = !!this.props.value; + + return ( + <div> + {hasValue && ( + <i className="big-spacer-right icon-lock icon-gray"/> + )} + + <button onClick={e => this.handleChangeClick(e)}> + {hasValue ? translate('change_verb') : translate('set')} + </button> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.js new file mode 100644 index 00000000000..a0c704623d9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.js @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import Select from 'react-select'; +import { defaultInputPropTypes } from '../../propTypes'; + +export default class InputForSingleSelectList extends React.Component { + static propTypes = { + ...defaultInputPropTypes, + options: React.PropTypes.arrayOf(React.PropTypes.string).isRequired + }; + + handleInputChange (option) { + this.props.onChange(option.value); + } + + render () { + const options = this.props.options.map(option => ({ + label: option, + value: option + })); + + return ( + <Select + name={this.props.name} + className="input-large" + options={options} + clearable={false} + value={this.props.value} + onChange={option => this.handleInputChange(option)}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.js new file mode 100644 index 00000000000..7eaef55399a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.js @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import SimpleInput from './SimpleInput'; + +export default class InputForString extends React.Component { + render () { + return ( + <SimpleInput {...this.props} className="input-large" type="text"/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForText.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForText.js new file mode 100644 index 00000000000..13cbbfabc85 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForText.js @@ -0,0 +1,40 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { defaultInputPropTypes } from '../../propTypes'; + +export default class InputForText extends React.Component { + static propTypes = defaultInputPropTypes; + + handleInputChange (e) { + this.props.onChange(e.target.value); + } + + render () { + return ( + <textarea + name={this.props.name} + className="input-super-large text-top" + rows="5" + value={this.props.value} + onChange={e => this.handleInputChange(e)}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.js new file mode 100644 index 00000000000..fa2facf6ee4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.js @@ -0,0 +1,90 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import PrimitiveInput from './PrimitiveInput'; +import { getEmptyValue } from '../../utils'; + +export default class MultiValueInput extends React.Component { + static propTypes = { + setting: React.PropTypes.object.isRequired, + value: React.PropTypes.array, + onChange: React.PropTypes.func.isRequired + }; + + ensureValue () { + return this.props.value || []; + } + + handleSingleInputChange (index, value) { + const newValue = [...this.ensureValue()]; + newValue.splice(index, 1, value); + this.props.onChange(newValue); + } + + handleDeleteValue (e, index) { + e.preventDefault(); + e.target.blur(); + + const newValue = [...this.ensureValue()]; + newValue.splice(index, 1); + this.props.onChange(newValue); + } + + prepareSetting () { + const { setting } = this.props; + const newDefinition = { ...setting.definition, multiValues: false }; + return { + ...setting, + definition: newDefinition, + values: undefined + }; + } + + renderInput (value, index, isLast) { + return ( + <li key={index} className="spacer-bottom"> + <PrimitiveInput + setting={this.prepareSetting()} + value={value} + onChange={this.handleSingleInputChange.bind(this, index)}/> + + {!isLast && ( + <div className="display-inline-block spacer-left"> + <button className="js-remove-value button-clean" onClick={e => this.handleDeleteValue(e, index)}> + <i className="icon-delete"/> + </button> + </div> + )} + </li> + ); + } + + render () { + const displayedValue = [...this.ensureValue(), ...getEmptyValue(this.props.setting.definition)]; + + return ( + <div> + <ul> + {displayedValue.map((value, index) => this.renderInput(value, index, index === displayedValue.length - 1))} + </ul> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.js new file mode 100644 index 00000000000..61e9db6052d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.js @@ -0,0 +1,75 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import InputForString from './InputForString'; +import InputForText from './InputForText'; +import InputForPassword from './InputForPassword'; +import InputForBoolean from './InputForBoolean'; +import InputForNumber from './InputForNumber'; +import InputForSingleSelectList from './InputForSingleSelectList'; +import { getUniqueName, isDefaultOrInherited } from '../../utils'; +import * as types from '../../constants'; + +const typeMapping = { + [types.TYPE_STRING]: InputForString, + [types.TYPE_TEXT]: InputForText, + [types.TYPE_PASSWORD]: InputForPassword, + [types.TYPE_BOOLEAN]: InputForBoolean, + [types.TYPE_INTEGER]: InputForNumber, + [types.TYPE_LONG]: InputForNumber, + [types.TYPE_FLOAT]: InputForNumber +}; + +export default class PrimitiveInput extends React.Component { + static propTypes = { + setting: React.PropTypes.object.isRequired, + value: React.PropTypes.any, + onChange: React.PropTypes.func.isRequired + }; + + render () { + const { setting, value, onChange, ...other } = this.props; + const { definition } = setting; + + const name = getUniqueName(definition); + + if (definition.type === types.TYPE_SINGLE_SELECT_LIST) { + return ( + <InputForSingleSelectList + name={name} + value={value} + isDefault={isDefaultOrInherited(setting)} + options={definition.options} + onChange={onChange} + {...other}/> + ); + } + + const InputComponent = typeMapping[definition.type] || InputForString; + return ( + <InputComponent + name={name} + value={value} + isDefault={isDefaultOrInherited(setting)} + onChange={onChange} + {...other}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.js new file mode 100644 index 00000000000..636a83896be --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.js @@ -0,0 +1,110 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import PrimitiveInput from './PrimitiveInput'; +import { getEmptyValue, getUniqueName } from '../../utils'; + +export default class PropertySetInput extends React.Component { + static propTypes = { + setting: React.PropTypes.object.isRequired, + value: React.PropTypes.array, + onChange: React.PropTypes.func.isRequired + }; + + ensureValue () { + return this.props.value || []; + } + + getFieldName (field) { + return getUniqueName(this.props.setting.definition, field.key); + } + + handleDeleteValue (e, index) { + e.preventDefault(); + e.target.blur(); + + const newValue = [...this.ensureValue()]; + newValue.splice(index, 1); + this.props.onChange(newValue); + } + + handleInputChange (index, fieldKey, value) { + const emptyValue = getEmptyValue(this.props.setting.definition)[0]; + const newValue = [...this.ensureValue()]; + const newFields = { ...emptyValue, ...newValue[index], [fieldKey]: value }; + newValue.splice(index, 1, newFields); + return this.props.onChange(newValue); + } + + renderFields (fieldValues, index, isLast) { + const { setting } = this.props; + + return ( + <tr key={index}> + {setting.definition.fields.map(field => ( + <td key={field.key}> + <PrimitiveInput + name={this.getFieldName(field)} + setting={{ definition: field, value: fieldValues[field.key] }} + value={fieldValues[field.key]} + onChange={this.handleInputChange.bind(this, index, field.key)}/> + </td> + ))} + <td className="thin nowrap"> + {!isLast && ( + <button className="js-remove-value button-link" onClick={e => this.handleDeleteValue(e, index)}> + <i className="icon-delete"/> + </button> + )} + </td> + </tr> + ); + } + + render () { + const { setting } = this.props; + + const displayedValue = [...this.ensureValue(), ...getEmptyValue(this.props.setting.definition)]; + + return ( + <div> + <table className="data zebra-hover no-outer-padding" style={{ width: 'auto', minWidth: 480, marginTop: -12 }}> + <thead> + <tr> + {setting.definition.fields.map(field => ( + <th key={field.key}> + {field.name} + {field.description != null && ( + <span className="spacer-top small">{field.description}</span> + )} + </th> + ))} + <th> </th> + </tr> + </thead> + <tbody> + {displayedValue.map((fieldValues, index) => + this.renderFields(fieldValues, index, index === displayedValue.length - 1))} + </tbody> + </table> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/SimpleInput.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/SimpleInput.js new file mode 100644 index 00000000000..6a7e24a6f8e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/SimpleInput.js @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { defaultInputPropTypes } from '../../propTypes'; + +export default class SimpleInput extends React.Component { + static propTypes = { + ...defaultInputPropTypes, + value: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]), + type: React.PropTypes.string.isRequired, + className: React.PropTypes.string.isRequired + }; + + handleInputChange (e) { + this.props.onChange(e.target.value); + } + + render () { + return ( + <input + name={this.props.name} + className={this.props.className + ' text-top'} + type={this.props.type} + value={this.props.value} + onChange={e => this.handleInputChange(e)}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.js new file mode 100644 index 00000000000..dbe570ec878 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.js @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import Input from '../Input'; +import PrimitiveInput from '../PrimitiveInput'; +import MultiValueInput from '../MultiValueInput'; +import PropertySetInput from '../PropertySetInput'; +import { TYPE_STRING, TYPE_PROPERTY_SET } from '../../../constants'; + +describe('Settings :: Inputs :: Input', () => { + it('should render PrimitiveInput', () => { + const setting = { definition: { key: 'example', type: TYPE_STRING } }; + const onChange = sinon.spy(); + const input = shallow(<Input setting={setting} value="foo" onChange={onChange}/>).find(PrimitiveInput); + expect(input).to.have.length(1); + expect(input.prop('setting')).to.equal(setting); + expect(input.prop('value')).to.equal('foo'); + expect(input.prop('onChange')).to.equal(onChange); + }); + + it('should render MultiValueInput', () => { + const setting = { definition: { key: 'example', type: TYPE_STRING, multiValues: true } }; + const onChange = sinon.spy(); + const input = shallow(<Input setting={setting} value="foo" onChange={onChange}/>).find(MultiValueInput); + expect(input).to.have.length(1); + expect(input.prop('setting')).to.equal(setting); + expect(input.prop('value')).to.equal('foo'); + expect(input.prop('onChange')).to.equal(onChange); + }); + + it('should render PropertySetInput', () => { + const setting = { definition: { key: 'example', type: TYPE_PROPERTY_SET, fields: [] } }; + const onChange = sinon.spy(); + const input = shallow(<Input setting={setting} value="foo" onChange={onChange}/>).find(PropertySetInput); + expect(input).to.have.length(1); + expect(input.prop('setting')).to.equal(setting); + expect(input.prop('value')).to.equal('foo'); + expect(input.prop('onChange')).to.equal(onChange); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.js new file mode 100644 index 00000000000..2fc8b9bb9c5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.js @@ -0,0 +1,77 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import InputForBoolean from '../InputForBoolean'; +import Toggle from '../../../../../components/controls/Toggle'; + +describe('Settings :: Inputs :: InputForBoolean', () => { + it('should render Toggle', () => { + const onChange = sinon.spy(); + const toggle = shallow( + <InputForBoolean + name="foo" + value={true} + isDefault={false} + onChange={onChange}/> + ).find(Toggle); + expect(toggle).to.have.length(1); + expect(toggle.prop('name')).to.equal('foo'); + expect(toggle.prop('value')).to.equal(true); + expect(toggle.prop('onChange')).to.be.a('function'); + }); + + it('should render Toggle without value', () => { + const onChange = sinon.spy(); + const input = shallow( + <InputForBoolean + name="foo" + isDefault={false} + onChange={onChange}/> + ); + const toggle = input.find(Toggle); + expect(toggle).to.have.length(1); + expect(toggle.prop('name')).to.equal('foo'); + expect(toggle.prop('value')).to.equal(false); + expect(toggle.prop('onChange')).to.be.a('function'); + expect(input.find('.note')).to.have.length(1); + }); + + it('should call onChange', () => { + const onChange = sinon.spy(); + const input = shallow( + <InputForBoolean + name="foo" + value={true} + isDefault={false} + onChange={onChange}/> + ); + const toggle = input.find(Toggle); + expect(toggle).to.have.length(1); + expect(toggle.prop('onChange')).to.be.a('function'); + + toggle.prop('onChange')(false); + + expect(onChange.called).to.equal(true); + expect(onChange.lastCall.args).to.deep.equal([false]); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForNumber-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForNumber-test.js new file mode 100644 index 00000000000..3544898fa2e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForNumber-test.js @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import InputForNumber from '../InputForNumber'; +import SimpleInput from '../SimpleInput'; + +describe('Settings :: Inputs :: InputForNumber', () => { + it('should render SimpleInput', () => { + const onChange = sinon.spy(); + const simpleInput = shallow( + <InputForNumber + name="foo" + value={17} + isDefault={false} + onChange={onChange}/> + ).find(SimpleInput); + expect(simpleInput).to.have.length(1); + expect(simpleInput.prop('name')).to.equal('foo'); + expect(simpleInput.prop('value')).to.equal(17); + expect(simpleInput.prop('type')).to.equal('text'); + expect(simpleInput.prop('onChange')).to.be.a('function'); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.js new file mode 100644 index 00000000000..6e463644879 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.js @@ -0,0 +1,97 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow, mount } from 'enzyme'; +import sinon from 'sinon'; +import InputForPassword from '../InputForPassword'; +import { click, submit } from '../../../../../../../../tests/utils'; + +describe('Settings :: Inputs :: InputForPassword', () => { + it('should render lock icon, but no form', () => { + const onChange = sinon.spy(); + const input = shallow( + <InputForPassword + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ); + expect(input.find('.icon-lock')).to.have.length(1); + expect(input.find('form')).to.have.length(0); + }); + + it('should open form', () => { + const onChange = sinon.spy(); + const input = shallow( + <InputForPassword + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ); + const button = input.find('button'); + expect(button).to.have.length(1); + + click(button); + expect(input.find('form')).to.have.length(1); + }); + + it('should close form', () => { + const onChange = sinon.spy(); + const input = shallow( + <InputForPassword + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ); + const button = input.find('button'); + expect(button).to.have.length(1); + + click(button); + expect(input.find('form')).to.have.length(1); + + click(input.find('form').find('a')); + expect(input.find('form')).to.have.length(0); + }); + + it('should set value', () => { + const onChange = sinon.stub().returns(Promise.resolve()); + const input = mount( + <InputForPassword + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ); + const button = input.find('button'); + expect(button).to.have.length(1); + + click(button); + const form = input.find('form'); + expect(form).to.have.length(1); + + input.ref('input').value = 'secret'; + submit(form); + + expect(onChange.called).to.equal(true); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.js new file mode 100644 index 00000000000..2be66e0ad80 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.js @@ -0,0 +1,66 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import Select from 'react-select'; +import InputForSingleSelectList from '../InputForSingleSelectList'; + +describe('Settings :: Inputs :: InputForSingleSelectList', () => { + it('should render Select', () => { + const onChange = sinon.spy(); + const select = shallow( + <InputForSingleSelectList + name="foo" + value="bar" + options={['foo', 'bar', 'baz']} + isDefault={false} + onChange={onChange}/> + ).find(Select); + expect(select).to.have.length(1); + expect(select.prop('name')).to.equal('foo'); + expect(select.prop('value')).to.equal('bar'); + expect(select.prop('options')).to.deep.equal([ + { value: 'foo', label: 'foo' }, + { value: 'bar', label: 'bar' }, + { value: 'baz', label: 'baz' } + ]); + expect(select.prop('onChange')).to.be.a('function'); + }); + + it('should call onChange', () => { + const onChange = sinon.spy(); + const select = shallow( + <InputForSingleSelectList + name="foo" + value="bar" + options={['foo', 'bar', 'baz']} + isDefault={false} + onChange={onChange}/> + ).find(Select); + expect(select).to.have.length(1); + expect(select.prop('onChange')).to.be.a('function'); + + select.prop('onChange')({ value: 'baz', label: 'baz' }); + expect(onChange.called).to.equal(true); + expect(onChange.lastCall.args).to.deep.equal(['baz']); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForString-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForString-test.js new file mode 100644 index 00000000000..4fd3c7a4b39 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForString-test.js @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import InputForString from '../InputForString'; +import SimpleInput from '../SimpleInput'; + +describe('Settings :: Inputs :: InputForString', () => { + it('should render SimpleInput', () => { + const onChange = sinon.spy(); + const simpleInput = shallow( + <InputForString + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ).find(SimpleInput); + expect(simpleInput).to.have.length(1); + expect(simpleInput.prop('name')).to.equal('foo'); + expect(simpleInput.prop('value')).to.equal('bar'); + expect(simpleInput.prop('type')).to.equal('text'); + expect(simpleInput.prop('onChange')).to.be.a('function'); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.js new file mode 100644 index 00000000000..97ef6ec5fab --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.js @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import InputForText from '../InputForText'; +import { change } from '../../../../../../../../tests/utils'; + +describe('Settings :: Inputs :: InputForText', () => { + it('should render textarea', () => { + const onChange = sinon.spy(); + const textarea = shallow( + <InputForText + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ).find('textarea'); + expect(textarea).to.have.length(1); + expect(textarea.prop('name')).to.equal('foo'); + expect(textarea.prop('value')).to.equal('bar'); + expect(textarea.prop('onChange')).to.be.a('function'); + }); + + it('should call onChange', () => { + const onChange = sinon.spy(); + const textarea = shallow( + <InputForText + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ).find('textarea'); + expect(textarea).to.have.length(1); + expect(textarea.prop('onChange')).to.be.a('function'); + + change(textarea, 'qux'); + + expect(onChange.called).to.equal(true); + expect(onChange.lastCall.args).to.deep.equal(['qux']); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.js new file mode 100644 index 00000000000..126a7a5a0ed --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.js @@ -0,0 +1,103 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow, mount } from 'enzyme'; +import sinon from 'sinon'; +import MultiValueInput from '../MultiValueInput'; +import InputForString from '../InputForString'; +import { click, change } from '../../../../../../../../tests/utils'; + +const definition = { multiValues: true }; + +const assertValues = (inputs, values) => { + values.forEach((value, index) => { + const input = inputs.at(index); + expect(input.prop('value')).to.equal(value); + }); +}; + +describe('Settings :: Inputs :: MultiValueInput', () => { + it('should render one value', () => { + const multiValueInput = mount( + <MultiValueInput + setting={{ definition }} + value={['foo']} + onChange={sinon.stub()}/> + ); + const stringInputs = multiValueInput.find(InputForString); + expect(stringInputs).to.have.length(1 + 1); + assertValues(stringInputs, ['foo', '']); + }); + + it('should render several values', () => { + const multiValueInput = mount( + <MultiValueInput + setting={{ definition }} + value={['foo', 'bar', 'baz']} + onChange={sinon.stub()}/> + ); + const stringInputs = multiValueInput.find(InputForString); + expect(stringInputs).to.have.length(3 + 1); + assertValues(stringInputs, ['foo', 'bar', 'baz', '']); + }); + + it('should remove value', () => { + const onChange = sinon.spy(); + const multiValueInput = mount( + <MultiValueInput + setting={{ definition }} + value={['foo', 'bar', 'baz']} + onChange={onChange}/> + ); + + click(multiValueInput.find('.js-remove-value').at(1)); + expect(onChange.called).to.equal(true); + expect(onChange.lastCall.args).to.deep.equal([['foo', 'baz']]); + }); + + it('should change existing value', () => { + const onChange = sinon.spy(); + const multiValueInput = mount( + <MultiValueInput + setting={{ definition }} + value={['foo', 'bar', 'baz']} + onChange={onChange}/> + ); + + change(multiValueInput.find(InputForString).at(1).find('input'), 'qux'); + expect(onChange.called).to.equal(true); + expect(onChange.lastCall.args).to.deep.equal([['foo', 'qux', 'baz']]); + }); + + it('should add new value', () => { + const onChange = sinon.spy(); + const multiValueInput = mount( + <MultiValueInput + setting={{ definition }} + value={['foo']} + onChange={onChange}/> + ); + + change(multiValueInput.find(InputForString).at(1).find('input'), 'bar'); + expect(onChange.called).to.equal(true); + expect(onChange.lastCall.args).to.deep.equal([['foo', 'bar']]); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.js new file mode 100644 index 00000000000..679d5302eb9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.js @@ -0,0 +1,66 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import SimpleInput from '../SimpleInput'; +import { change } from '../../../../../../../../tests/utils'; + +describe('Settings :: Inputs :: SimpleInput', () => { + it('should render input', () => { + const onChange = sinon.spy(); + const input = shallow( + <SimpleInput + type="text" + className="input-large" + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ).find('input'); + expect(input).to.have.length(1); + expect(input.prop('type')).to.equal('text'); + expect(input.prop('className')).to.include('input-large'); + expect(input.prop('name')).to.equal('foo'); + expect(input.prop('value')).to.equal('bar'); + expect(input.prop('onChange')).to.be.a('function'); + }); + + it('should call onChange', () => { + const onChange = sinon.spy(); + const input = shallow( + <SimpleInput + type="text" + className="input-large" + name="foo" + value="bar" + isDefault={false} + onChange={onChange}/> + ).find('input'); + expect(input).to.have.length(1); + expect(input.prop('onChange')).to.be.a('function'); + + change(input, 'qux'); + + expect(onChange.called).to.equal(true); + expect(onChange.lastCall.args).to.deep.equal(['qux']); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/constants.js b/server/sonar-web/src/main/js/apps/settings/constants.js new file mode 100644 index 00000000000..249c996b2ac --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/constants.js @@ -0,0 +1,28 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const TYPE_STRING = 'STRING'; +export const TYPE_TEXT = 'TEXT'; +export const TYPE_PASSWORD = 'PASSWORD'; +export const TYPE_BOOLEAN = 'BOOLEAN'; +export const TYPE_FLOAT = 'FLOAT'; +export const TYPE_INTEGER = 'INTEGER'; +export const TYPE_LONG = 'LONG'; +export const TYPE_SINGLE_SELECT_LIST = 'SINGLE_SELECT_LIST'; +export const TYPE_PROPERTY_SET = 'PROPERTY_SET'; diff --git a/server/sonar-web/src/main/js/apps/settings/propTypes.js b/server/sonar-web/src/main/js/apps/settings/propTypes.js new file mode 100644 index 00000000000..c7e64bfa695 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/propTypes.js @@ -0,0 +1,27 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { PropTypes } from 'react'; + +export const defaultInputPropTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.any, + isDefault: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired +}; diff --git a/server/sonar-web/src/main/js/apps/settings/store/actions.js b/server/sonar-web/src/main/js/apps/settings/store/actions.js new file mode 100644 index 00000000000..f2ba7d709e6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/actions.js @@ -0,0 +1,85 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { getDefinitions, getValues, setSettingValue, resetSettingValue } from '../../../api/settings'; +import { receiveValues } from './values/actions'; +import { receiveDefinitions } from './definitions/actions'; +import { startLoading, stopLoading } from './settingsPage/loading/actions'; +import { parseError } from '../../code/utils'; +import { addGlobalErrorMessage, closeAllGlobalMessages } from '../../../components/store/globalMessages'; +import { passValidation, failValidation } from './settingsPage/validationMessages/actions'; +import { cancelChange } from './settingsPage/changedValues/actions'; +import { getDefinition, getChangedValue } from './rootReducer'; + +export const fetchSettings = componentKey => dispatch => { + return getDefinitions(componentKey) + .then(definitions => { + dispatch(receiveDefinitions(definitions)); + const keys = definitions.map(definition => definition.key).join(); + return getValues(keys, componentKey); + }) + .then(settings => { + dispatch(receiveValues(settings)); + dispatch(closeAllGlobalMessages()); + }) + .catch(e => parseError(e).then(message => dispatch(addGlobalErrorMessage(message)))); +}; + +export const saveValue = (key, componentKey) => (dispatch, getState) => { + dispatch(startLoading(key)); + + const state = getState(); + const definition = getDefinition(state, key); + const value = getChangedValue(state, key); + + return setSettingValue(definition, value, componentKey) + .then(() => getValues(key, componentKey)) + .then(values => { + dispatch(receiveValues(values)); + dispatch(cancelChange(key)); + dispatch(passValidation(key)); + dispatch(stopLoading(key)); + }) + .catch(e => { + dispatch(stopLoading(key)); + parseError(e).then(message => dispatch(failValidation(key, message))); + return Promise.reject(); + }); +}; + +export const resetValue = (key, componentKey) => dispatch => { + dispatch(startLoading(key)); + + return resetSettingValue(key, componentKey) + .then(() => getValues(key, componentKey)) + .then(values => { + if (values.length > 0) { + dispatch(receiveValues(values)); + } else { + dispatch(receiveValues([{ key }])); + } + dispatch(passValidation(key)); + dispatch(stopLoading(key)); + }) + .catch(e => { + dispatch(stopLoading(key)); + parseError(e).then(message => dispatch(failValidation(key, message))); + return Promise.reject(); + }); +}; diff --git a/server/sonar-web/src/main/js/apps/settings/store/definitions/actions.js b/server/sonar-web/src/main/js/apps/settings/store/definitions/actions.js new file mode 100644 index 00000000000..589a05df223 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/definitions/actions.js @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const RECEIVE_DEFINITIONS = 'RECEIVE_DEFINITIONS'; + +/** + * Receive definitions action creator + * @param {Array} definitions + * @returns {Object} + */ +export const receiveDefinitions = definitions => ({ + type: RECEIVE_DEFINITIONS, + definitions +}); diff --git a/server/sonar-web/src/main/js/apps/settings/store/definitions/reducer.js b/server/sonar-web/src/main/js/apps/settings/store/definitions/reducer.js new file mode 100644 index 00000000000..d80ffc47c09 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/definitions/reducer.js @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import keyBy from 'lodash/keyBy'; +import sortBy from 'lodash/sortBy'; +import uniqBy from 'lodash/uniqBy'; +import { RECEIVE_DEFINITIONS } from './actions'; +import { DEFAULT_CATEGORY, getCategoryName } from '../../utils'; + +const reducer = (state = {}, action = {}) => { + if (action.type === RECEIVE_DEFINITIONS) { + const definitionsByKey = keyBy(action.definitions, 'key'); + return { ...state, ...definitionsByKey }; + } + + return state; +}; + +export default reducer; + +export const getDefinition = (state, key) => state[key]; + +export const getAllDefinitions = state => Object.values(state); + +export const getDefinitionsForCategory = (state, category) => + getAllDefinitions(state).filter(definition => definition.category.toLowerCase() === category.toLowerCase()); + +export const getAllCategories = state => uniqBy( + getAllDefinitions(state).map(definition => definition.category), + category => category.toLowerCase()); + +export const getDefaultCategory = state => { + const categories = getAllCategories(state); + if (categories.includes(DEFAULT_CATEGORY)) { + return DEFAULT_CATEGORY; + } else { + const sortedCategories = sortBy(categories, category => getCategoryName(category).toLowerCase()); + return sortedCategories[0]; + } +}; diff --git a/server/sonar-web/src/main/js/apps/settings/store/rootReducer.js b/server/sonar-web/src/main/js/apps/settings/store/rootReducer.js new file mode 100644 index 00000000000..603cb3b753a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/rootReducer.js @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { combineReducers } from 'redux'; +import definitions, * as fromDefinitions from './definitions/reducer'; +import values, * as fromValues from './values/reducer'; +import settingsPage, * as fromSettingsPage from './settingsPage/reducer'; +import globalMessages, * as fromGlobalMessages from '../../../components/store/globalMessages'; + +const rootReducer = combineReducers({ + definitions, + values, + settingsPage, + globalMessages +}); + +export default rootReducer; + +export const getDefinition = (state, key) => fromDefinitions.getDefinition(state.definitions, key); + +export const getAllCategories = state => fromDefinitions.getAllCategories(state.definitions); + +export const getDefaultCategory = state => fromDefinitions.getDefaultCategory(state.definitions); + +export const getValue = (state, key) => fromValues.getValue(state.values, key); + +export const getSettingsForCategory = (state, category) => + fromDefinitions.getDefinitionsForCategory(state.definitions, category).map(definition => ({ + ...getValue(state, definition.key), + definition + })); + +export const getChangedValue = (state, key) => fromSettingsPage.getChangedValue(state.settingsPage, key); + +export const isLoading = (state, key) => fromSettingsPage.isLoading(state.settingsPage, key); + +export const getValidationMessage = (state, key) => fromSettingsPage.getValidationMessage(state.settingsPage, key); + +export const getGlobalMessages = state => fromGlobalMessages.getGlobalMessages(state.globalMessages); diff --git a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/actions.js b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/actions.js new file mode 100644 index 00000000000..46a0beecd4d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/actions.js @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const CHANGE_VALUE = 'settingsPage/CHANGE_VALUE'; + +export const changeValue = (key, value) => ({ + type: CHANGE_VALUE, + key, + value +}); + +export const CANCEL_CHANGE = 'settingsPage/CANCEL_CHANGE'; + +export const cancelChange = key => ({ + type: CANCEL_CHANGE, + key +}); diff --git a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/reducer.js b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/reducer.js new file mode 100644 index 00000000000..3783b92ac4b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/reducer.js @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import omit from 'lodash/omit'; +import { CHANGE_VALUE, CANCEL_CHANGE } from './actions'; + +const reducer = (state = {}, action = {}) => { + if (action.type === CHANGE_VALUE) { + return { ...state, [action.key]: action.value }; + } + + if (action.type === CANCEL_CHANGE) { + return omit(state, action.key); + } + + return state; +}; + +export default reducer; + +export const getChangedValue = (state, key) => state[key]; diff --git a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/actions.js b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/actions.js new file mode 100644 index 00000000000..0a4363b41df --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/actions.js @@ -0,0 +1,31 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const START_LOADING = 'settingsPage/START_LOADING'; + +export const startLoading = key => ({ + type: START_LOADING, + key +}); +export const STOP_LOADING = 'settingsPage/STOP_LOADING'; + +export const stopLoading = key => ({ + type: STOP_LOADING, + key +}); diff --git a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/reducer.js b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/reducer.js new file mode 100644 index 00000000000..d0a6677029b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/reducer.js @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { START_LOADING, STOP_LOADING } from './actions'; + +const reducer = (state = {}, action = {}) => { + if (action.type === START_LOADING) { + return { ...state, [action.key]: true }; + } + + if (action.type === STOP_LOADING) { + return { ...state, [action.key]: false }; + } + + return state; +}; + +export default reducer; + +export const isLoading = (state, key) => !!state[key]; diff --git a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/reducer.js b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/reducer.js new file mode 100644 index 00000000000..a3dc0c7c37a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/reducer.js @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { combineReducers } from 'redux'; +import changedValues, * as fromChangedValues from './changedValues/reducer'; +import validationMessages, * as fromValidationMessages from './validationMessages/reducer'; +import loading, * as fromLoading from './loading/reducer'; + +export default combineReducers({ + changedValues, + validationMessages, + loading +}); + +export const getChangedValue = (state, key) => + fromChangedValues.getChangedValue(state.changedValues, key); + +export const getValidationMessage = (state, key) => + fromValidationMessages.getValidationMessage(state.validationMessages, key); + +export const isLoading = (state, key) => + fromLoading.isLoading(state.loading, key); diff --git a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/actions.js b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/actions.js new file mode 100644 index 00000000000..eb7b33d72b9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/actions.js @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const FAIL_VALIDATION = 'settingsPage/FAIL_VALIDATION'; + +export const failValidation = (key, message) => ({ + type: FAIL_VALIDATION, + key, + message +}); + +export const PASS_VALIDATION = 'settingsPage/PASS_VALIDATION'; + +export const passValidation = key => ({ + type: PASS_VALIDATION, + key +}); diff --git a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/reducer.js b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/reducer.js new file mode 100644 index 00000000000..b332de3a98f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/reducer.js @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { FAIL_VALIDATION, PASS_VALIDATION } from './actions'; + +const reducer = (state = {}, action = {}) => { + if (action.type === FAIL_VALIDATION) { + return { ...state, [action.key]: action.message }; + } + + if (action.type === PASS_VALIDATION) { + return { ...state, [action.key]: null }; + } + + return state; +}; + +export default reducer; + +export const getValidationMessage = (state, key) => state[key]; diff --git a/server/sonar-web/src/main/js/apps/settings/store/values/actions.js b/server/sonar-web/src/main/js/apps/settings/store/values/actions.js new file mode 100644 index 00000000000..582b6078233 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/values/actions.js @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const RECEIVE_VALUES = 'RECEIVE_VALUES'; + +/** + * Receive settings action creator + * @param {Array} settings + * @returns {Object} + */ +export const receiveValues = settings => ({ + type: RECEIVE_VALUES, + settings +}); diff --git a/server/sonar-web/src/main/js/apps/settings/store/values/reducer.js b/server/sonar-web/src/main/js/apps/settings/store/values/reducer.js new file mode 100644 index 00000000000..5ed6709bc5d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/values/reducer.js @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import keyBy from 'lodash/keyBy'; +import { RECEIVE_VALUES } from './actions'; + +const reducer = (state = {}, action = {}) => { + if (action.type === RECEIVE_VALUES) { + const settingsByKey = keyBy(action.settings, 'key'); + return { ...state, ...settingsByKey }; + } + + return state; +}; + +export default reducer; + +export const getValue = (state, key) => state[key]; diff --git a/server/sonar-web/src/main/js/apps/settings/styles.css b/server/sonar-web/src/main/js/apps/settings/styles.css new file mode 100644 index 00000000000..a5c4c61604a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/styles.css @@ -0,0 +1,157 @@ +.settings-layout { + display: flex; + justify-content: space-between; + align-items: stretch; + margin-bottom: 60px; +} + +.settings-main { + position: relative; + z-index: 2; + flex-grow: 1; + padding: 15px 20px; + border: 1px solid #e6e6e6; + box-sizing: border-box; + background-color: #fff; +} + +.settings-side { + position: relative; + z-index: 3; + width: 160px; + flex-shrink: 0; + padding: 10px 0; + box-sizing: border-box; + transform: translateX(1px); +} + +.settings-menu { +} + +.settings-menu > li { + margin-bottom: 4px; +} + +.settings-menu > li > a { + display: block; + padding: 10px 10px; + line-height: 1.5; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + border: 1px solid #e6e6e6; + border-right: none; + overflow: hidden; + text-overflow: ellipsis; + transition: color 0.3s ease, background-color 0.3s ease; +} + +.settings-menu > li > a:hover, +.settings-menu > li > a:focus, +.settings-menu > li > a.active { + background-color: #fff; +} + +.settings-menu > li > a.active { + color: #444; + cursor: default; +} + +.settings-definitions-list > li + li { + margin-top: 30px; +} + +.settings-definition { + display: flex; + align-items: stretch; +} + +.settings-definition-changed { + margin: -10px -20px; + padding: 9px 20px; + border-top: 1px solid #faebcc; + border-bottom: 1px solid #faebcc; + background-color: #fcf8e3; +} + +.settings-definition-left { + width: 330px; + padding-right: 30px; + box-sizing: border-box; +} + +.settings-definition-right { + position: relative; + width: calc(100% - 330px); + padding-top: 35px; + box-sizing: border-box; +} + +.settings-definition-name { + text-overflow: ellipsis; +} + +.settings-definition-key { + line-height: 1.5; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.settings-definition-key:hover { + overflow: visible; +} + +.settings-definition-state { + position: absolute; + top: 0; + left: 0; + right: 0; + line-height: 24px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.settings-definition-state-icon { + display: inline-block; + vertical-align: middle; + width: 24px; + height: 24px; + margin-right: 8px; + text-align: center; +} + +.settings-definition-state-icon > .icon-alert-error, +.settings-definition-state-icon > .spinner { + position: relative; + top: -2px; +} + +.settings-definition-changes { + margin-top: 20px; + padding-top: 20px; + border-top: 1px dotted #e6e6e6; +} + +.settings-sub-categories-list { +} + +.settings-sub-categories-list > li { +} + +.settings-sub-categories-list > li + li { + margin: 30px -20px 0; + padding: 30px 20px; + border-top: 1px solid #e6e6e6; +} + +.settings-sub-category-name { + margin-bottom: 20px; + font-size: 16px; +} + +.settings-sub-category-description { + margin-top: -15px; + margin-bottom: 20px; + color: #777; +} diff --git a/server/sonar-web/src/main/js/apps/settings/utils.js b/server/sonar-web/src/main/js/apps/settings/utils.js new file mode 100644 index 00000000000..5b40e2ed999 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/utils.js @@ -0,0 +1,140 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { translate, hasMessage } from '../../helpers/l10n'; +import { TYPE_PROPERTY_SET, TYPE_BOOLEAN, TYPE_SINGLE_SELECT_LIST, TYPE_PASSWORD } from './constants'; + +export const DEFAULT_CATEGORY = 'general'; + +export function getPropertyName (definition) { + const key = `property.${definition.key}.name`; + return hasMessage(key) ? translate(key) : definition.name; +} + +export function getPropertyDescription (definition) { + const key = `property.${definition.key}.description`; + return hasMessage(key) ? translate(key) : definition.description; +} + +export function getCategoryName (category) { + const key = `property.category.${category}`; + return hasMessage(key) ? translate(key) : category; +} + +export function getSubCategoryName (category, subCategory) { + const key = `property.category.${category}.${subCategory}`; + return hasMessage(key) ? translate(key) : getCategoryName(subCategory); +} + +export function getSubCategoryDescription (category, subCategory) { + const key = `property.category.${category}.${subCategory}.description`; + return hasMessage(key) ? translate(key) : null; +} + +export function getUniqueName (definition, index = null) { + const indexSuffix = index != null ? `[${index}]` : ''; + return `settings[${definition.key}]${indexSuffix}`; +} + +export function getSettingValue (setting) { + if (setting.definition.multiValues) { + return setting.values; + } else if (setting.definition.type === TYPE_PROPERTY_SET) { + return setting.fieldValues; + } else { + return setting.value; + } +} + +export function isEmptyValue (definition, value) { + if (value == null) { + return true; + } else if (definition.type === TYPE_BOOLEAN) { + return false; + } else { + return value.length === 0; + } +} + +export function getEmptyValue (definition) { + if (definition.multiValues) { + return [getEmptyValue({ ...definition, multiValues: false })]; + } + + if (definition.type === TYPE_PROPERTY_SET) { + const value = {}; + definition.fields.forEach(field => value[field.key] = getEmptyValue(field)); + return [value]; + } + + if (definition.type === TYPE_BOOLEAN || definition.type === TYPE_SINGLE_SELECT_LIST) { + return null; + } + + return ''; +} + +export function isDefaultOrInherited (setting) { + return !!setting.default || !!setting.inherited; +} + +function getParentValue (setting) { + if (setting.definition.multiValues) { + return setting.parentValues; + } else if (setting.definition.type === TYPE_PROPERTY_SET) { + return setting.parentFieldValues; + } else { + return setting.parentValue; + } +} + +/** + * Get and format the default value + * @param setting + * @returns {string} + */ +export function getDefaultValue (setting) { + const parentValue = getParentValue(setting); + + if (parentValue == null) { + return translate('settings.default.no_value'); + } + + if (setting.definition.multiValues) { + return parentValue.length > 0 ? + parentValue.join(', ') : + translate('settings.default.no_value'); + } + + if (setting.definition.type === TYPE_PROPERTY_SET) { + return parentValue.length > 0 ? + translate('settings.default.complex_value') : + translate('settings.default.no_value'); + } + + if (setting.definition.type === TYPE_PASSWORD) { + return translate('settings.default.password'); + } + + if (setting.definition.type === TYPE_BOOLEAN) { + return parentValue ? translate('settings.boolean.true') : translate('settings.boolean.false'); + } + + return parentValue; +} diff --git a/server/sonar-web/src/main/js/components/controls/Toggle.js b/server/sonar-web/src/main/js/components/controls/Toggle.js new file mode 100644 index 00000000000..c843657dae4 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/Toggle.js @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import classNames from 'classnames'; +import './styles.css'; + +export default class Toggle extends React.Component { + static propTypes = { + value: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.bool]).isRequired, + name: React.PropTypes.string, + onChange: React.PropTypes.func + }; + + handleClick (e, value) { + e.preventDefault(); + e.currentTarget.blur(); + if (this.props.onChange) { + this.props.onChange(!value); + } + } + + render () { + const { value } = this.props; + const booleanValue = typeof value === 'string' ? value === 'true' : value; + + const className = classNames('boolean-toggle', { 'boolean-toggle-on': booleanValue }); + + return ( + <button className={className} name={this.props.name} onClick={e => this.handleClick(e, booleanValue)}> + <div className="boolean-toggle-handle"/> + </button> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/Toggle-test.js b/server/sonar-web/src/main/js/components/controls/__tests__/Toggle-test.js new file mode 100644 index 00000000000..f194063188b --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/Toggle-test.js @@ -0,0 +1,53 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import chai, { expect } from 'chai'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import React from 'react'; +import Toggle from '../Toggle'; + +chai.use(sinonChai); + +function getSample (props) { + return ( + <Toggle value={true} onChange={() => true} {...props}/>); +} + +function click (element) { + return element.simulate('click', { + currentTarget: { blur () {} }, + preventDefault () {} + }); +} + +describe('Components :: Controls :: Toggle', () => { + it('should render', () => { + const Toggle = shallow(getSample()); + expect(Toggle.is('button')).to.equal(true); + }); + + it('should call onChange', () => { + const onChange = sinon.spy(); + const Toggle = shallow(getSample({ onChange })); + click(Toggle); + expect(onChange).to.have.been.calledWith(false); + }); +}); diff --git a/server/sonar-web/src/main/js/components/controls/styles.css b/server/sonar-web/src/main/js/components/controls/styles.css index 1ed5c1f4e52..b67f7f5eb71 100644 --- a/server/sonar-web/src/main/js/components/controls/styles.css +++ b/server/sonar-web/src/main/js/components/controls/styles.css @@ -24,3 +24,54 @@ .date-input-control-input:focus + .date-input-control-icon path { fill: #4b9fd5; } + +.boolean-toggle { + display: inline-block; + vertical-align: middle; + width: 48px; + height: 24px; + padding: 1px; + border: 1px solid #cdcdcd; + border-radius: 24px; + box-sizing: border-box; + background-color: #fff; + cursor: pointer; + transition: all 0.3s ease; +} + +.boolean-toggle:hover { + background-color: #fff; +} + +.boolean-toggle:focus { + border-color: #4b9fd5; + background-color: #f6f6f6; +} + +.boolean-toggle-handle { + width: 20px; + height: 20px; + border: 1px solid #cdcdcd; + border-radius: 22px; + box-sizing: border-box; + background-color: #f6f6f6; + transition: transform 0.3s cubic-bezier(.87,-.41,.19,1.44), border 0.3s ease; +} + +.boolean-toggle-on { + border-color: #236a97; + background-color: #236a97; +} + +.boolean-toggle-on:hover { + background-color: #236a97; +} + +.boolean-toggle-on:focus { + background-color: #236a97; +} + +.boolean-toggle-on .boolean-toggle-handle { + border-color: #f6f6f6; + transform: translateX(24px); +} diff --git a/server/sonar-web/src/main/js/helpers/l10n.js b/server/sonar-web/src/main/js/helpers/l10n.js index c3eadcab7bb..f2ea4d2b336 100644 --- a/server/sonar-web/src/main/js/helpers/l10n.js +++ b/server/sonar-web/src/main/js/helpers/l10n.js @@ -36,6 +36,11 @@ export function translateWithParameters (messageKey, ...parameters) { } } +export function hasMessage (...keys) { + const messageKey = keys.join('.'); + return messages[messageKey] != null; +} + function getCurrentLocale () { return window.navigator.languages ? window.navigator.languages[0] : window.navigator.language; } diff --git a/server/sonar-web/src/main/js/helpers/request.js b/server/sonar-web/src/main/js/helpers/request.js index bf88a0658be..ab92b6aaec3 100644 --- a/server/sonar-web/src/main/js/helpers/request.js +++ b/server/sonar-web/src/main/js/helpers/request.js @@ -201,5 +201,5 @@ export function requestDelete (url, data) { * @returns {Promise} */ export function delay (response) { - return new Promise(resolve => setTimeout(() => resolve(response), 3000)); + return new Promise(resolve => setTimeout(() => resolve(response), 500)); } diff --git a/server/sonar-web/src/main/less/components/react-select.less b/server/sonar-web/src/main/less/components/react-select.less index 05475d007e8..3ba2a2edaa2 100644 --- a/server/sonar-web/src/main/less/components/react-select.less +++ b/server/sonar-web/src/main/less/components/react-select.less @@ -92,7 +92,7 @@ left: 0; line-height: @formControlHeight; padding-left: 8px; - padding-right: 8px; + padding-right: 24px; position: absolute; right: 0; top: 0; diff --git a/server/sonar-web/src/main/less/init/forms.less b/server/sonar-web/src/main/less/init/forms.less index ffb3a31395a..bdc1c17ced3 100644 --- a/server/sonar-web/src/main/less/init/forms.less +++ b/server/sonar-web/src/main/less/init/forms.less @@ -30,6 +30,7 @@ input[type=password], input[type=email], input[type=search], input[type=date], +input[type=number], textarea, select { border: 1px solid @darkGrey; @@ -53,7 +54,8 @@ input[type=text], input[type=password], input[type=email], input[type=search], -input[type=date] { +input[type=date], +input[type=number] { height: @formControlHeight; padding: 0 6px; } @@ -132,6 +134,17 @@ input[type="submit"].button-red { } } +.button-success, +input[type="submit"].button-success { + border-color: @green; + color: @green; + + &:hover, &:focus, &.active { + background: @green; + color: #fff; + } +} + .button-clean, .button-clean:hover, .button-clean:focus { @@ -229,6 +242,12 @@ input[type="submit"].button-red { .input-super-large { width: 100%; max-width: 300px; + min-width: 200px; +} + +textarea.input-super-large { + max-width: 600px; + min-width: 300px; } em.mandatory { @@ -244,6 +263,7 @@ label[for] { display: inline-block; vertical-align: middle; font-size: 0; + white-space: nowrap; & > li { display: inline-block; diff --git a/server/sonar-web/src/main/less/init/tables.less b/server/sonar-web/src/main/less/init/tables.less index e157aebfbd4..142ed35ea8e 100644 --- a/server/sonar-web/src/main/less/init/tables.less +++ b/server/sonar-web/src/main/less/init/tables.less @@ -97,6 +97,26 @@ table.data.condensed > tbody > tr > td { padding-bottom: 5px; } +table.data.no-outer-padding > thead > tr { + > th:first-child { + padding-left: 0 + } + + > th:last-child { + padding-right: 0 + } +} + +table.data.no-outer-padding > tbody > tr { + > td:first-child { + padding-left: 0 + } + + > td:last-child { + padding-right: 0 + } +} + .data thead tr.total { background-color: #EFEFEF; font-weight: normal; diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb index a3dad4b490a..40393a473a0 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb @@ -28,6 +28,10 @@ class ProjectController < ApplicationController redirect_to :overwrite_params => {:controller => :dashboard, :action => 'index'} end + def settings + @project = get_current_project(params[:id]) + end + def deletion @project = get_current_project(params[:id]) end @@ -78,18 +82,6 @@ class ProjectController < ApplicationController :include => 'events', :order => 'snapshots.created_at DESC') end - def settings - @resource = get_current_project(params[:id]) - - if !java_facade.getResourceTypeBooleanProperty(@resource.qualifier, 'configurable') - redirect_to :action => 'index', :id => params[:id] - end - - @snapshot = @resource.last_snapshot - definitions_per_category = java_facade.propertyDefinitions.propertiesByCategory(@resource.qualifier) - processProperties(definitions_per_category) - end - def update_version snapshot=Snapshot.find(params[:sid], :include => 'project') not_found("Snapshot not found") unless snapshot diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb index e5bdc3f0c4f..ee4e72f070e 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb @@ -18,96 +18,10 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # class SettingsController < ApplicationController - SECTION=Navigation::SECTION_CONFIGURATION - - verify :method => :post, :only => %w(update), :redirect_to => {:action => :index} - before_filter :admin_required, :only => %w(index) + before_filter :admin_required def index - load_properties() - end - - def update - resource_id = params[:resource_id] - @resource = Project.by_key(resource_id) if resource_id - - access_denied if (@resource && !is_admin?(@resource)) - access_denied if (@resource.nil? && !is_admin?) - - load_properties() - - @updated_properties = {} - update_properties(resource_id) - update_property_sets(resource_id) - - render :partial => 'settings/properties' - end - - private - - def update_properties(resource_id) - (params[:settings] || []).each do |key, value| - update_property(key, value, resource_id) - end - end - def update_property_sets(resource_id) - (params[:property_sets] || []).each do |key, set_keys| - update_property_set(key, set_keys, params[key], resource_id, params[:auto_generate] && params[:auto_generate][key]) - end end - - def update_property_set(key, set_keys, fields_hash, resource_id, auto_generate) - if auto_generate - max = (Time.now.to_f * 100000).to_i - set_keys.each_with_index do |v, index| - if v.blank? - max += 1; - set_keys[index] = max.to_s - end - end - end - - set_key_values = {} - fields_hash.each do |field_key, field_values| - field_values.zip(set_keys).each do |field_value, set_key| - set_key_values[set_key] ||= {} - set_key_values[set_key][field_key] = field_value - end - end - - set_keys.reject! { |set_key| set_key.blank? || (auto_generate && set_key_values[set_key].values.all?(&:blank?)) } - - Property.transaction do - # Delete only property sets that are no more existing - condition = "prop_key LIKE '" + key + ".%' AND " - set_keys.each {|set_key| condition += "prop_key NOT LIKE ('#{key + '.' + set_key + '.%'}') AND "} - if resource_id - condition += 'resource_id=' + resource_id - else - condition += 'resource_id IS NULL' - end - Property.delete_all(condition) - - update_property(key, set_keys, resource_id) - set_keys.each do |set_key| - update_property("#{key}.#{set_key}.key", set_key, resource_id) unless auto_generate - - set_key_values[set_key].each do |field, value| - update_property("#{key}.#{set_key}.#{field}", value, resource_id) - end - end - end - end - - def update_property(key, value, resource_id) - @updated_properties[key] = Property.set(key, value, resource_id) - end - - def load_properties - definitions_per_category = java_facade.propertyDefinitions.propertiesByCategory(@resource.nil? ? nil : @resource.qualifier) - processProperties(definitions_per_category) - end - end diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/settings.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/settings.html.erb index ce82fb311bc..c0f9f94d9f0 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/settings.html.erb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/settings.html.erb @@ -1,5 +1,3 @@ -<div class="page" name="settings"> - <div class="yui-g widget" id="widget_plugins"> - <%= render :partial => 'settings/settings' %> - </div> -</div> +<% content_for :extra_script do %> +<script src="<%= ApplicationController.root_context -%>/js/bundles/settings.js?v=<%= sonar_version -%>"></script> +<% end %> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_error.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_error.html.erb deleted file mode 100644 index 6dff575b2b0..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_error.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<% updated_property = @updated_properties[key] if @updated_properties -%> - -<% if updated_property && !updated_property.valid? -%> - <div class="error"><%= updated_property.validation_error_message -%></div> -<% end -%> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_multi_value.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_multi_value.html.erb deleted file mode 100644 index 06897360059..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_multi_value.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<div class="multi_value marginbottom5"> - <%= render "settings/single_value", :property => property, :value => value -%> - <a href="#" class="delete link-action" style="<%= 'display:none;' if hide_delete -%>"><%= message('delete') -%></a> - <br/> -</div> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_properties.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_properties.html.erb deleted file mode 100644 index b19a33736ed..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_properties.html.erb +++ /dev/null @@ -1,216 +0,0 @@ -<% if @category.isSpecial -%> - <%= render 'special', :url => url_for(:controller => "#{@category.key}_configuration") -%> -<% else -%> - <form onsubmit="$j('#submit_settings').hide(); - $j('#loading_settings').show(); - $j.ajax({ url:'<%= url_for :controller => 'settings', :action => 'update', :category => @category.key, :subcategory => @subcategory.key, :resource_id => (@resource && @resource.id) -%>', - type:'post', - success:function(responseHTML){$j('#properties').html($j(responseHTML));$j('#loading_settings').hide();$j('#submit_settings').show()}, - data:$j(this).serialize()}); - return false;" - method='post' - action='#' - autocomplete="off" - > - <% subcategories = @subcategories_per_categories[@category] || [] -%> - <ul class="tabs"> - <% subcategories.each do |subcategory| -%> - <li> - <% if @resource %> - <a <% if @subcategory==subcategory %>class="selected"<% end -%> href="<%= url_for(:controller => 'project', :action => 'settings', :id => nil) -%>?id=<%= url_encode(@resource.key) -%>&category=<%= url_encode(@category.key) -%>&subcategory=<%= url_encode(subcategory.key) -%>"><%= h subcategory_name(@category, subcategory) -%></a> - <% else %> - <%= link_to subcategory_name(@category, subcategory), {:controller => 'settings', :action =>'index', :category => @category.key, :subcategory => subcategory.key}, :class => @subcategory==subcategory ? 'selected' : nil -%> - <% end %> - </li> - <% end -%> - </ul> - <% if @subcategory.isSpecial %> - <%= render 'special', :url => url_for(:controller => "#{@subcategory.key}_configuration") -%> - <% else %> - <% if @subcategory.key == @category.key && !category_desc(@category).blank? -%> - <p class="categoryDescription"><%= category_desc(@category) -%> </p> - <% end -%> - <% if @subcategory.key != @category.key && !subcategory_desc(@category, @subcategory).blank? -%> - <p class="categoryDescription" colspan="2"><%= subcategory_desc(@category, @subcategory) -%> </p> - <% end -%> - - <table class="marginbottom10"> - <tbody> - <% by_property_index_or_name(@definitions).each do |property| -%> - <tr class="property" id="block_<%= property.key -%>"> - <th> - <h3><%= property_name(property) -%></h3> - </th> - <td> - <% value = property_value(property) -%> - <% if property.multi_values -%> - <% value.each_with_index do |sub_value, index| -%> - <%= render "settings/multi_value", :property => property, :value => sub_value, :hide_delete => index == 0 -%> - <% end -%> - <div class="template" style="display:none;"> - <%= render "settings/multi_value", :property => property, :value => nil, :hide_delete => false -%> - </div> - <button class="add_value"><%= message('settings.add') -%></button> - <br/> - <% else -%> - <%= render "settings/single_value", :property => property, :value => value -%> - <% end -%> - - <%= render "settings/error", :key => property.key -%> - - <!-- SONAR-4707 Don't display default value for property sets --> - <% if property.fields.blank? %> - <% default_prop_value = (@resource ? Property.value(property.key, nil, property.defaultValue) : property.defaultValue) -%> - <% unless default_prop_value.blank? -%> - <div class="note"><%= message('default') %>: <%= property.type.to_s=='PASSWORD' ? '********' : h(default_prop_value) -%></div> - <% else -%> - <!-- SONAR-5162 When no default value, leave a space to add a separation with the description or the key --> - <p class="marginbottom10"></p> - <% end -%> - <% end -%> - - <% desc=property_description(property) -%> - <% unless desc.blank? %> - <p class="marginbottom10"><%= desc -%></p> - <% end -%> - <div class="note"><%= message('key') -%>: <%= property.key -%></div> - </td> - </tr> - <% end -%> - </tbody> - - </table> - - <% unless @definitions.empty? %> - <div class="marginbottom10" style="padding-left: 5px;"> - <%= hidden_field_tag('page_version', (params[:page_version] || 0).to_i + 1) -%> - <%= submit_tag(message('settings.save_category', :params => [subcategory_name(@category, @subcategory)]), :id => 'submit_settings') -%> - <img src="<%= ApplicationController.root_context -%>/images/loading.gif" id="loading_settings" style="display:none;"> - </div> - <% end %> - - <% if @category.key() == 'exclusions' -%> - <div class="help marginbottom10" style="margin-left: -1px"> - <h2>Wildcards</h2> - <p>Following rules are applied:</p> - <table class="data"> - <thead><tr><th colspan="2"></th></tr></thead> - <tr> - <td>*</td> - <td>Match zero or more characters</td> - </tr> - <tr> - <td>**</td> - <td>Match zero or more directories</td> - </tr> - <tr> - <td>?</td> - <td>Match a single character</td> - </tr> - </table> - <br> - <table class="data"> - <thead><tr><th>Example</th><th>Matches</th><th>Does not match</th></tr></thead> - <tbody> - <tr> - <td>**/foo/*.js</td> - <td> - <ul> - <li>src/foo/bar.js</li> - <li>lib/ui/foo/bar.js</li> - </ul> - </td> - <td> - <ul> - <li>src/bar.js</li> - <li>src/foo2/bar.js</li> - </ul> - </td> - </tr> - <tr> - <td>src/foo/*bar*.js</td> - <td> - <ul> - <li>src/foo/bar.js</li> - <li>src/foo/bar1.js</li> - <li>src/foo/bar123.js</li> - <li>src/foo/123bar123.js</li> - </ul> - </td> - <td> - <ul> - <li>src/foo/ui/bar.js</li> - <li>src/bar.js</li> - </ul> - </td> - </tr> - <tr> - <td>src/foo/**</td> - <td> - <ul> - <li>src/foo/bar.js</li> - <li>src/foo/ui/bar.js</li> - </ul> - </td> - <td> - <ul> - <li>src/bar/foo/bar.js</li> - <li>src/bar.js</li> - </ul> - </td> - </tr> - <tr> - <td>**/foo?.js</td> - <td> - <ul> - <li>src/foo1.js</li> - <li>src/bar/foo1.js</li> - </ul> - </td> - <td> - <ul> - <li>src/foo.js</li> - <li>src/foo12.js</li> - <li>src/12foo3.js</li> - </ul> - </td> - </tr> - </tbody> - </table> - </div> - <% else - help = category_help(@category) - unless help.blank? - -%> - <div class="help marginbottom10" style="margin-left: -1px"> - <%= help -%> - </div> - <% end - end - -%> - - <% end %> - </form> - <% end -%> - - - -<script> - $j('#properties > form') - .on('click', '.delete', function () { - $j(this).parents('.multi_value').remove(); - return false; - }) - .on('click', '.add_value', function () { - var template = $j(this).parents('.property').find('.template').last(); - template.clone().insertBefore(template).show(); - return false; - }) - .on('keypress', 'form', function (e) { - if (e.which == 13 && e.target.nodeName != "TEXTAREA") { - /* See https://jira.sonarsource.com/browse/SONAR-4363 */ - submit_settings.click(); - return false; - } - }); -</script> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_set_instance.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_set_instance.html.erb deleted file mode 100644 index 10d29f440af..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_set_instance.html.erb +++ /dev/null @@ -1,42 +0,0 @@ -<% errors = [] -%> -<% key_field = key_field(property) -%> - -<tr class="text-top multi_value <%= 'template' unless set_key -%> odd" style="<%= 'display:none' unless set_key -%>"> - <% unless key_field -%> - <%= hidden_field_tag "property_sets[#{property.key}][]", set_key -%> - <% end -%> - - <% property.fields.each do |field| -%> - <% if set_key -%> - <% key = "#{property.key}.#{set_key}.#{field.key}" -%> - <% value = Property.value(key, resource_id) -%> - <% errors << (render "settings/error", :key => key) -%> - <% end -%> - - <% if field == key_field -%> - <td><%= render "settings/type_#{field.type}", :property => field, :field => field, :value => value, :name => "property_sets[#{property.key}][]", :id => "input_#{h field.key}", :size => field.indicativeSize -%></td> - <% else -%> - <td><%= render "settings/type_#{field.type}", :property => field, :field => field, :value => value, :name => "#{property.key}[#{field.key}][]", :id => "input_#{h field.key}", :size => field.indicativeSize -%></td> - <% end -%> - <% end -%> - - <td style="width: 60px;"> - <% unless hide_delete -%> - <a href="#" class="delete link-action"><%= message('delete') -%></a> - <% end -%> - </td> -</tr> - -<% unless errors.all?(&:blank?) -%> - <tr> - <% if key_field -%> - <td></td> - <% end -%> - - <% errors.each do |error| -%> - <td><%= error -%></td> - <% end -%> - - <td></td> - </tr> -<% end -%> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_settings.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_settings.html.erb deleted file mode 100644 index 8ae1f0c2c7a..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_settings.html.erb +++ /dev/null @@ -1,42 +0,0 @@ -<div id="plugins"> - <header class="page-header"> - <h1 class="page-title"><%= message(@resource ? 'project_settings.page' : 'settings.page') -%></h1> - <p class="page-description"><%= message(@resource ? 'project_settings.page.description' : 'settings.page.description') -%> </p> - </header> - - <table width="100%"> - <tr> - <td width="1%" nowrap class="column first"> - <table class="data"> - <thead> - <tr> - <th><%= message('category') -%></th> - </tr> - </thead> - <tbody> - <% @categories.each do |category| -%> - <% if !category.key.blank?-%> - <tr id="select_<%= category.key -%>" class="select <%= cycle('even', 'odd', :name => 'category') -%> <%= 'selected' if @category.key==category.key -%>"> - <td class="category"> - <% if @resource %> - <a href="<%= url_for(:controller => 'project', :action => 'settings') -%>?id=<%= url_encode(@resource.key) -%>&category=<%= url_encode(category.key) -%>"><%= h category_name(category) -%></a> - <% else %> - <%= link_to category_name(category), :category => category.key -%> - <% end %> - </td> - </tr> - <% end -%> - <% end -%> - </tbody> - </table> - <br/> - </td> - - <td class="column"> - <div id="properties"> - <%= render 'settings/properties' -%> - </div> - </td> - </tr> - </table> -</div> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_single_value.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_single_value.html.erb deleted file mode 100644 index f9a0bcaa091..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_single_value.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render "settings/type_#{property_type(property, value)}", :property => property, :field => nil, :value => value, :name => input_name(property), :id => "input_#{h property.key}" -%> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_special.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_special.html.erb deleted file mode 100644 index afa5e1cffde..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_special.html.erb +++ /dev/null @@ -1 +0,0 @@ -<iframe src="<%= url -%>" width="100%" height="900" frameborder="0" style="overflow-y: auto;" name="settings_iframe"></iframe> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_BOOLEAN.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_BOOLEAN.html.erb deleted file mode 100644 index 74e6c8c8635..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_BOOLEAN.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= property_input_field(name, PropertyType::TYPE_BOOLEAN, value, PropertiesHelper::SCREEN_SETTINGS, {:id => id, :default => (defined? property.defaultValue) ? property.defaultValue : nil }) %>
\ No newline at end of file diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_FLOAT.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_FLOAT.html.erb deleted file mode 100644 index 721480a62aa..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_FLOAT.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<% - options = {:id => id} - options[:size] = (defined? size) ? size : nil -%> -<%= property_input_field(name, PropertyType::TYPE_FLOAT, value, PropertiesHelper::SCREEN_SETTINGS, options) %>
\ No newline at end of file diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_INTEGER.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_INTEGER.html.erb deleted file mode 100644 index 5f7c39e8c7b..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_INTEGER.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<% - options = {:id => id} - options[:size] = (defined? size) ? size : nil -%> -<%= property_input_field(name, PropertyType::TYPE_INTEGER, value, PropertiesHelper::SCREEN_SETTINGS, options) %>
\ No newline at end of file diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_LICENSE.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_LICENSE.html.erb deleted file mode 100644 index aeaa68efed4..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_LICENSE.html.erb +++ /dev/null @@ -1,62 +0,0 @@ -<% if !value || value.blank? %> - <textarea rows="5" cols="80" class="width100" name="<%= name -%>" id="<%= id -%>"></textarea> -<% - else - license = controller.java_facade.parseLicense(value) - product = license.getProduct() || '-' - # super-hack here - # should be avoided in the future - does_product_match = property.key.include? product -%> - <div class="width100"> - <textarea rows="6" name="<%= name -%>" id="<%= id -%>" style="float: left;width: 390px"><%= h value -%></textarea> - - <div style="margin-left: 400px"> - <table> - <tr> - <td class="form-key-cell <% if !does_product_match -%>bg-danger<% end -%>">Product:</td> - <td class="form-val-cell <% if !does_product_match -%>bg-danger<% end -%>"><%= product -%></td> - </tr> - <tr> - <td class="form-key-cell">Organization:</td> - <td><%= license.getOrganization() || '-' -%></td> - </tr> - <tr> - <td class="form-key-cell">Expiration:</td> - <td> - <% if license.getExpirationDate() - formatted_date = l(Date.parse(license.getExpirationDateAsString())) - %> - <%= license.isExpired() ? "<span class='error'>#{formatted_date}</span>" : formatted_date -%> - <% else %> - - - <% end %> - - </td> - </tr> - <tr> - <td class="form-key-cell">Type:</td> - <td><%= license.getType() || '-' -%></td> - </tr> - <tr> - <td class="form-key-cell">Server:</td> - <td> - <% if license.getServer() && - license.getServer() != "*" && - controller.java_facade.getConfigurationValue("sonar.server_id") != license.getServer() %> - <span class='error'><%= license.getServer() -%></span> - <% else %> - <%= license.getServer() || '-' -%> - <% end %> - </td> - </tr> - <% license.additionalProperties().each do |k,v| -%> - <tr> - <td class="form-key-cell"><%= k -%>:</td> - <td><%= v || '-' -%></td> - </tr> - <% end %> - </table> - </div> - </div> -<% end %> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_METRIC.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_METRIC.html.erb deleted file mode 100644 index 18e706b7f26..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_METRIC.html.erb +++ /dev/null @@ -1,24 +0,0 @@ -<% - defaultValue = (defined? property.defaultValue) ? property.defaultValue : nil -%> -<select name="<%= name -%>" id="<%= id -%>"> - <option value=""><%= !defaultValue.blank? ? message('default') : nil -%></option> - <% - metrics_per_domain={} - metrics_filtered_by(property.options).each do |metric| - domain=metric.domain || '' - metrics_per_domain[domain]||=[] - metrics_per_domain[domain]<<metric - end - - metrics_per_domain.keys.sort.each do |domain| - %> - <optgroup label="<%= h domain -%>"> - <% metrics_per_domain[domain].each do |m| - selected_attr = (m.key==value || m.id==value) ? " selected='selected'" : '' - %> - <option value="<%= m.key -%>" <%= selected_attr -%>><%= m.short_name -%></option> - <% end -%> - </optgroup> - <% end -%> -</select>
\ No newline at end of file diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PASSWORD.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PASSWORD.html.erb deleted file mode 100644 index d8c1556936c..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PASSWORD.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -<% - options = {:id => id} - options[:size] = (defined? size) ? size : nil -%> - -<% value = Property::EXISTING_PASSWORD unless value.blank? %> -<%= property_input_field(name, PropertyType::TYPE_PASSWORD, value, PropertiesHelper::SCREEN_SETTINGS, options) %> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PROPERTY_SET.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PROPERTY_SET.html.erb deleted file mode 100644 index ae8159a9df4..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PROPERTY_SET.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<% choices = Property.values(property.propertySetKey).reject(&:blank?) -%> -<% prompt = [[message('default'), '']] -%> - -<% if !value.blank? && (choices.exclude? value) -%> - <%= image_tag 'exclamation.png' -%> - <% missing = [[h(value + ' <' + message('deleted') + '>'), value]] -%> -<% else -%> - <% missing = [] -%> -<% end -%> - -<%= select_tag name, options_for_select(prompt + choices + missing, value), :id => id -%>
\ No newline at end of file diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PROPERTY_SET_DEFINITION.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PROPERTY_SET_DEFINITION.html.erb deleted file mode 100644 index beec593636c..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_PROPERTY_SET_DEFINITION.html.erb +++ /dev/null @@ -1,38 +0,0 @@ -<% resource_id = @resource.id if @resource -%> - -<table class="data"> - <thead> - <tr> - <% unless key_field(property) -%> - <%= hidden_field_tag "auto_generate[#{property.key}]", true -%> - <% end -%> - <% property.fields.each do |field| -%> - <th> - <%= field_name(property, field) -%> - <% desc = field_description(property, field) -%> - <% unless desc.blank? %> - <p class="note"><%= desc -%></p> - <% end -%> - </th> - <% end -%> - <th></th> - </tr> - </thead> - - <tbody> - <% set_keys = Property.values(property.key, resource_id) -%> - <% set_keys = [''] if set_keys.all?(&:blank?) -%> - <% set_keys.each_with_index do |set_key, index| -%> - <%= render 'settings/set_instance', :property => property, :set_key => set_key, :resource_id => resource_id, :hide_delete => (index == 0) %> - <% end -%> - <%= render 'settings/set_instance', :property => property, :set_key => nil, :resource_id => resource_id, :hide_delete => false %> - </tbody> - - <tfoot> - <tr> - <td colspan="<%= property.fields.size + 1 -%>"> - <button class="add_value"><%= message('settings.add') -%></button> - </td> - </tr> - </tfoot> -</table> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_REGULAR_EXPRESSION.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_REGULAR_EXPRESSION.html.erb deleted file mode 100644 index 8274fd8ef8f..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_REGULAR_EXPRESSION.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<% - options = {:id => id} - options[:size] = (defined? size) ? size : nil -%> -<%= property_input_field(name, PropertyType::TYPE_REGULAR_EXPRESSION, value, PropertiesHelper::SCREEN_SETTINGS, options) %>
\ No newline at end of file diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_SINGLE_SELECT_LIST.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_SINGLE_SELECT_LIST.html.erb deleted file mode 100644 index 4153c332d13..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_SINGLE_SELECT_LIST.html.erb +++ /dev/null @@ -1,2 +0,0 @@ -<%= property_input_field(name, PropertyType::TYPE_SINGLE_SELECT_LIST, value, PropertiesHelper::SCREEN_SETTINGS, - {:id => id, :default => (defined? property.defaultValue) ? property.defaultValue : nil, :values => property.options, :extra_values => {:property => property, :field => field}}) %>
\ No newline at end of file diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_STRING.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_STRING.html.erb deleted file mode 100644 index b28c75604a7..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_STRING.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<% - options = {:id => id} - options[:size] = (defined? size) ? size : nil -%> -<%= property_input_field(name, PropertyType::TYPE_STRING, value, PropertiesHelper::SCREEN_SETTINGS, options) %>
\ No newline at end of file diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_TEXT.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_TEXT.html.erb deleted file mode 100644 index af46cf40082..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_TEXT.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<% - options = {:id => id} - options[:size] = (defined? size) ? size : nil -%> -<%= property_input_field(name, PropertyType::TYPE_TEXT, value, PropertiesHelper::SCREEN_SETTINGS, options) %> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_USER_LOGIN.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_USER_LOGIN.html.erb deleted file mode 100644 index 6f94fa6a651..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/_type_USER_LOGIN.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<% - options = {:id => id} - options[:size] = (defined? size) ? size : nil -%> -<%= property_input_field(name, PropertyType::TYPE_USER_LOGIN, value, PropertiesHelper::SCREEN_SETTINGS, options) %> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/index.html.erb index 55ae2ad65ec..6c42ad1d4b7 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/index.html.erb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/settings/index.html.erb @@ -1,3 +1,3 @@ -<div class="page"> - <%= render 'settings', :project => nil %> -</div> +<% content_for :extra_script do %> + <script src="<%= ApplicationController.root_context -%>/js/bundles/settings.js?v=<%= sonar_version -%>"></script> +<% end %> diff --git a/server/sonar-web/tests/utils.js b/server/sonar-web/tests/utils.js new file mode 100644 index 00000000000..afa4f53d0d4 --- /dev/null +++ b/server/sonar-web/tests/utils.js @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const click = element => { + return element.simulate('click', { + target: { blur () {} }, + preventDefault () {} + }); +}; + +export const submit = element => { + return element.simulate('submit', { + preventDefault () {} + }); +}; + +export const change = (element, value) => { + return element.simulate('change', { + target: { value }, + currentTarget: { value } + }); +}; |