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/sonar-web/src/main/js | |
parent | 11de4cfe580382a23ac664c5b4464d426b742a3e (diff) | |
download | sonarqube-d2da7f30d512284c943b82abc135483b59b85536.tar.gz sonarqube-d2da7f30d512284c943b82abc135483b59b85536.zip |
SONAR-5856 Rewrite Settings page (#1163)
Diffstat (limited to 'server/sonar-web/src/main/js')
57 files changed, 3228 insertions, 1 deletions
diff --git a/server/sonar-web/src/main/js/api/settings.js b/server/sonar-web/src/main/js/api/settings.js new file mode 100644 index 00000000000..14396243a2f --- /dev/null +++ b/server/sonar-web/src/main/js/api/settings.js @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import omitBy from 'lodash/omitBy'; +import { getJSON, post } from '../helpers/request'; +import { TYPE_PROPERTY_SET } from '../apps/settings/constants'; + +export function getDefinitions (componentKey) { + const url = '/api/settings/list_definitions'; + const data = componentKey ? { componentKey } : {}; + return getJSON(url, data).then(r => r.definitions); +} + +export function getValues (keys, componentKey) { + const url = '/api/settings/values'; + const data = { keys }; + if (componentKey) { + data.componentKey = componentKey; + } + return getJSON(url, data).then(r => r.settings); +} + +export function setSettingValue (definition, value, componentKey) { + const url = '/api/settings/set'; + + const { key } = definition; + const data = { key }; + + if (definition.multiValues) { + data.values = value; + } else if (definition.type === TYPE_PROPERTY_SET) { + data.fieldValues = value + .map(fields => omitBy(fields, value => value == null)) + .map(JSON.stringify); + } else { + data.value = value; + } + + if (componentKey) { + data.componentKey = componentKey; + } + + return post(url, data); +} + +export function resetSettingValue (key, componentKey) { + const url = '/api/settings/reset'; + const data = { key }; + if (componentKey) { + data.componentKey = componentKey; + } + return post(url, data); +} diff --git a/server/sonar-web/src/main/js/api/users.js b/server/sonar-web/src/main/js/api/users.js index 80865674d80..e5d5d15f7bc 100644 --- a/server/sonar-web/src/main/js/api/users.js +++ b/server/sonar-web/src/main/js/api/users.js @@ -39,3 +39,9 @@ export function getIdentityProviders () { const url = '/api/users/identity_providers'; return getJSON(url); } + +export function searchUsers (query) { + const url = '/api/users/search'; + const data = { q: query }; + return getJSON(url, data); +} diff --git a/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.js b/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.js new file mode 100644 index 00000000000..16b709d99ab --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.js @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { expect } from 'chai'; +import { getEmptyValue } from '../utils'; +import { TYPE_PROPERTY_SET, TYPE_STRING, TYPE_SINGLE_SELECT_LIST, TYPE_BOOLEAN } from '../constants'; + +const fields = [ + { key: 'foo', type: TYPE_STRING }, + { key: 'bar', type: TYPE_SINGLE_SELECT_LIST } +]; + +describe('Settings :: Utils', () => { + describe('#getEmptyValue()', () => { + it('should work for property sets', () => { + const setting = { type: TYPE_PROPERTY_SET, fields }; + expect(getEmptyValue(setting)).to.deep.equal([{ foo: '', bar: null }]); + }); + + it('should work for multi values string', () => { + const setting = { type: TYPE_STRING, multiValues: true }; + expect(getEmptyValue(setting)).to.deep.equal(['']); + }); + + it('should work for multi values boolean', () => { + const setting = { type: TYPE_BOOLEAN, multiValues: true }; + expect(getEmptyValue(setting)).to.deep.equal([null]); + }); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/app.js b/server/sonar-web/src/main/js/apps/settings/app.js new file mode 100644 index 00000000000..cf9dfcf07ad --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/app.js @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { render } from 'react-dom'; +import { Provider } from 'react-redux'; +import { Router, Route, Redirect, useRouterHistory } from 'react-router'; +import { createHistory } from 'history'; +import App from './components/App'; +import rootReducer from './store/rootReducer'; +import configureStore from '../../components/store/configureStore'; + +window.sonarqube.appStarted.then(options => { + const el = document.querySelector(options.el); + + const controller = options.component ? '/project/settings' : '/settings'; + const history = useRouterHistory(createHistory)({ + basename: window.baseUrl + controller + }); + + const store = configureStore(rootReducer); + + const withComponent = ComposedComponent => props => + <ComposedComponent {...props} component={options.component}/>; + + render(( + <Provider store={store}> + <Router history={history}> + <Redirect from="/index" to="/"/> + <Route path="/" component={withComponent(App)}/> + </Router> + </Provider> + ), el); +}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.js b/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.js new file mode 100644 index 00000000000..44d16ad6bdc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.js @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { connect } from 'react-redux'; +import CategoriesList from './CategoriesList'; +import { getAllCategories } from '../store/rootReducer'; + +class AllCategoriesList extends React.Component { + render () { + return <CategoriesList {...this.props}/>; + } +} + +const mapStateToProps = state => ({ + categories: getAllCategories(state) +}); + +export default connect( + mapStateToProps +)(AllCategoriesList); diff --git a/server/sonar-web/src/main/js/apps/settings/components/App.js b/server/sonar-web/src/main/js/apps/settings/components/App.js new file mode 100644 index 00000000000..424675ba502 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/App.js @@ -0,0 +1,101 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import { connect } from 'react-redux'; +import PageHeader from './PageHeader'; +import CategoryDefinitionsList from './CategoryDefinitionsList'; +import AllCategoriesList from './AllCategoriesList'; +import GlobalMessagesContainer from './GlobalMessagesContainer'; +import { fetchSettings } from '../store/actions'; +import { getDefaultCategory } from '../store/rootReducer'; +import '../styles.css'; + +class App extends React.Component { + static propTypes = { + component: React.PropTypes.object, + fetchSettings: React.PropTypes.func.isRequired, + defaultCategory: React.PropTypes.string + }; + + state = { loaded: false }; + + componentDidMount () { + document.querySelector('html').classList.add('dashboard-page'); + const componentKey = this.props.component ? this.props.component.key : null; + this.props.fetchSettings(componentKey).then(() => { + this.setState({ loaded: true }); + }); + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + componentDidUpdate (prevProps) { + if (prevProps.component !== this.props.component) { + const componentKey = this.props.component ? this.props.component.key : null; + this.props.fetchSettings(componentKey); + } + } + + componentWillUnmount () { + document.querySelector('html').classList.remove('dashboard-page'); + } + + render () { + if (!this.state.loaded) { + return null; + } + + const { query } = this.props.location; + const selectedCategory = query.category || this.props.defaultCategory; + + return ( + <div id="settings-page" className="page page-limited"> + <PageHeader component={this.props.component}/> + <GlobalMessagesContainer/> + <div className="settings-layout"> + <div className="settings-side"> + <AllCategoriesList + component={this.props.component} + selectedCategory={selectedCategory} + defaultCategory={this.props.defaultCategory}/> + </div> + <div className="settings-main"> + <CategoryDefinitionsList + component={this.props.component} + category={selectedCategory}/> + </div> + </div> + </div> + ); + } +} + +const mapStateToProps = state => ({ + defaultCategory: getDefaultCategory(state) +}); + +export default connect( + mapStateToProps, + { fetchSettings } +)(App); + diff --git a/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js b/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js new file mode 100644 index 00000000000..aa833d5ce90 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import sortBy from 'lodash/sortBy'; +import { IndexLink } from 'react-router'; +import { getCategoryName } from '../utils'; + +export default class CategoriesList extends React.Component { + static propTypes = { + categories: React.PropTypes.array.isRequired, + selectedCategory: React.PropTypes.string.isRequired, + defaultCategory: React.PropTypes.string.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + renderLink (category) { + const query = {}; + + if (category.key !== this.props.defaultCategory) { + query.category = category.key.toLowerCase(); + } + + if (this.props.component) { + query.id = this.props.component.key; + } + + const className = category.key.toLowerCase() === this.props.selectedCategory.toLowerCase() ? 'active' : ''; + + return ( + <IndexLink to={{ pathname: '/', query }} className={className} title={category.name}> + {category.name} + </IndexLink> + ); + } + + render () { + const categoriesWithName = this.props.categories.map(key => ({ key, name: getCategoryName(key) })); + const sortedCategories = sortBy(categoriesWithName, category => category.name.toLowerCase()); + + return ( + <ul className="settings-menu"> + {sortedCategories.map(category => ( + <li key={category.key}> + {this.renderLink(category)} + </li> + ))} + </ul> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.js b/server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.js new file mode 100644 index 00000000000..cc2e6d8c24e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.js @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { connect } from 'react-redux'; +import SubCategoryDefinitionsList from './SubCategoryDefinitionsList'; +import { getSettingsForCategory } from '../store/rootReducer'; + +class CategoryDefinitionsList extends React.Component { + render () { + return <SubCategoryDefinitionsList {...this.props}/>; + } +} + +const mapStateToProps = (state, ownProps) => ({ + settings: getSettingsForCategory(state, ownProps.category) +}); + +export default connect( + mapStateToProps +)(CategoryDefinitionsList); diff --git a/server/sonar-web/src/main/js/apps/settings/components/Definition.js b/server/sonar-web/src/main/js/apps/settings/components/Definition.js new file mode 100644 index 00000000000..66b19fd023a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/Definition.js @@ -0,0 +1,195 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { connect } from 'react-redux'; +import shallowCompare from 'react-addons-shallow-compare'; +import classNames from 'classnames'; +import Input from './inputs/Input'; +import DefinitionDefaults from './DefinitionDefaults'; +import DefinitionChanges from './DefinitionChanges'; +import { getPropertyName, getPropertyDescription, isEmptyValue, getSettingValue, isDefaultOrInherited } from '../utils'; +import { translateWithParameters, translate } from '../../../helpers/l10n'; +import { resetValue, saveValue } from '../store/actions'; +import { isLoading, getValidationMessage, getChangedValue } from '../store/rootReducer'; +import { failValidation, passValidation } from '../store/settingsPage/validationMessages/actions'; +import { cancelChange, changeValue } from '../store/settingsPage/changedValues/actions'; + +class Definition extends React.Component { + static propTypes = { + component: React.PropTypes.object, + setting: React.PropTypes.object.isRequired, + changedValue: React.PropTypes.any, + loading: React.PropTypes.bool.isRequired, + validationMessage: React.PropTypes.string, + + changeValue: React.PropTypes.func.isRequired, + cancelChange: React.PropTypes.func.isRequired, + saveValue: React.PropTypes.func.isRequired, + resetValue: React.PropTypes.func.isRequired, + failValidation: React.PropTypes.func.isRequired, + passValidation: React.PropTypes.func.isRequired + }; + + state = { + success: false + }; + + componentDidMount () { + this.mounted = true; + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + componentWillUnmount () { + this.mounted = false; + } + + safeSetState (changes) { + if (this.mounted) { + this.setState(changes); + } + } + + handleChange (value) { + clearTimeout(this.timeout); + return this.props.changeValue(this.props.setting.definition.key, value); + } + + handleReset () { + const componentKey = this.props.component ? this.props.component.key : null; + const { definition } = this.props.setting; + return this.props.resetValue(definition.key, componentKey).then(() => { + this.safeSetState({ success: true }); + this.timeout = setTimeout(() => this.safeSetState({ success: false }), 3000); + }).catch(() => { /* do nothing */ }); + } + + handleCancel () { + this.props.cancelChange(this.props.setting.definition.key); + this.props.passValidation(this.props.setting.definition.key); + } + + handleSave () { + this.safeSetState({ success: false }); + const { definition } = this.props.setting; + if (isEmptyValue(definition, this.props.changedValue)) { + this.props.failValidation(definition.key, translate('settings.state.value_cant_be_empty')); + return; + } + + const componentKey = this.props.component ? this.props.component.key : null; + this.props.saveValue(this.props.setting.definition.key, componentKey).then(() => { + this.safeSetState({ success: true }); + this.timeout = setTimeout(() => this.safeSetState({ success: false }), 3000); + }).catch(() => { /* do nothing */ }); + } + + render () { + const { setting, changedValue, loading } = this.props; + const { definition } = setting; + const propertyName = getPropertyName(definition); + + const hasValueChanged = changedValue != null; + + const className = classNames('settings-definition', { + 'settings-definition-changed': hasValueChanged + }); + + const effectiveValue = hasValueChanged ? changedValue : getSettingValue(setting); + + const isDefault = isDefaultOrInherited(setting) && !hasValueChanged; + + return ( + <div className={className} data-key={definition.key}> + <div className="settings-definition-left"> + <h3 className="settings-definition-name" title={propertyName}> + {propertyName} + </h3> + + <div className="settings-definition-description markdown small spacer-top" + dangerouslySetInnerHTML={{ __html: getPropertyDescription(definition) }}/> + + <div className="settings-definition-key note little-spacer-top"> + {translateWithParameters('settings.key_x', definition.key)} + </div> + </div> + + <div className="settings-definition-right"> + <Input setting={setting} value={effectiveValue} onChange={this.handleChange.bind(this)}/> + + {!hasValueChanged && ( + <DefinitionDefaults + setting={setting} + isDefault={isDefault} + onReset={() => this.handleReset()}/> + )} + + {hasValueChanged && ( + <DefinitionChanges + onSave={this.handleSave.bind(this)} + onCancel={this.handleCancel.bind(this)}/> + )} + + <div className="settings-definition-state"> + {loading && ( + <span className="text-info"> + <span className="settings-definition-state-icon"> + <i className="spinner"/> + </span> + {translate('settings.state.saving')} + </span> + )} + + {!loading && (this.props.validationMessage != null) && ( + <span className="text-danger"> + <span className="settings-definition-state-icon"> + <i className="icon-alert-error"/> + </span> + {translateWithParameters('settings.state.validation_failed', this.props.validationMessage)} + </span> + )} + + {!loading && this.state.success && ( + <span className="text-success"> + <span className="settings-definition-state-icon"> + <i className="icon-check"/> + </span> + {translate('settings.state.saved')} + </span> + )} + </div> + </div> + </div> + ); + } +} + +const mapStateToProps = (state, ownProps) => ({ + changedValue: getChangedValue(state, ownProps.setting.definition.key), + loading: isLoading(state, ownProps.setting.definition.key), + validationMessage: getValidationMessage(state, ownProps.setting.definition.key) +}); + +export default connect( + mapStateToProps, + { changeValue, saveValue, resetValue, failValidation, passValidation, cancelChange } +)(Definition); diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionChanges.js b/server/sonar-web/src/main/js/apps/settings/components/DefinitionChanges.js new file mode 100644 index 00000000000..edbeafecf9e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionChanges.js @@ -0,0 +1,59 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import { translate } from '../../../helpers/l10n'; + +export default class DefinitionChanges extends React.Component { + static propTypes = { + onSave: React.PropTypes.func.isRequired, + onCancel: React.PropTypes.func.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + handleSaveClick (e) { + e.preventDefault(); + e.target.blur(); + this.props.onSave(); + } + + handleCancelChange (e) { + e.preventDefault(); + e.target.blur(); + this.props.onCancel(); + } + + render () { + return ( + <div className="settings-definition-changes"> + <button className="js-save-changes button-success" onClick={e => this.handleSaveClick(e)}> + {translate('save')} + </button> + + <button className="js-cancel-changes big-spacer-left button-link" onClick={e => this.handleCancelChange(e)}> + {translate('cancel')} + </button> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionDefaults.js b/server/sonar-web/src/main/js/apps/settings/components/DefinitionDefaults.js new file mode 100644 index 00000000000..452b24c5ea5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionDefaults.js @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { getSettingValue, isEmptyValue, getDefaultValue } from '../utils'; +import { translate } from '../../../helpers/l10n'; + +export default class DefinitionDefaults extends React.Component { + static propTypes = { + setting: React.PropTypes.object.isRequired, + isDefault: React.PropTypes.bool.isRequired, + onReset: React.PropTypes.func.isRequired + }; + + handleReset (e) { + e.preventDefault(); + e.target.blur(); + this.props.onReset(); + } + + render () { + const { setting, isDefault } = this.props; + const { definition } = setting; + + const isExplicitlySet = !isDefault && !isEmptyValue(definition, getSettingValue(setting)); + + return ( + <div> + {isDefault && ( + <div className="spacer-top note" style={{ lineHeight: '24px' }}> + {translate('settings._default')} + </div> + )} + + {isExplicitlySet && ( + <div className="spacer-top nowrap"> + <button onClick={e => this.handleReset(e)}>{translate('reset_verb')}</button> + <span className="spacer-left note">{translate('default')}{': '}{getDefaultValue(setting)}</span> + </div> + )} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js b/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js new file mode 100644 index 00000000000..77d0d7fa95b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import Definition from './Definition'; + +export default class DefinitionsList extends React.Component { + static propTypes = { + component: React.PropTypes.object, + settings: React.PropTypes.array.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + render () { + return ( + <ul className="settings-definitions-list"> + {this.props.settings.map(setting => ( + <li key={setting.definition.key}> + <Definition component={this.props.component} setting={setting}/> + </li> + ))} + </ul> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/GlobalMessagesContainer.js b/server/sonar-web/src/main/js/apps/settings/components/GlobalMessagesContainer.js new file mode 100644 index 00000000000..f64ef91664b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/GlobalMessagesContainer.js @@ -0,0 +1,28 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { connect } from 'react-redux'; +import GlobalMessages from '../../../components/controls/GlobalMessages'; +import { getGlobalMessages } from '../store/rootReducer'; + +const mapStateToProps = state => ({ + messages: getGlobalMessages(state) +}); + +export default connect(mapStateToProps)(GlobalMessages); diff --git a/server/sonar-web/src/main/js/apps/settings/components/PageHeader.js b/server/sonar-web/src/main/js/apps/settings/components/PageHeader.js new file mode 100644 index 00000000000..34e402122cb --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/PageHeader.js @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { translate } from '../../../helpers/l10n'; + +export default class PageHeader extends React.Component { + static propTypes = { + component: React.PropTypes.object + }; + + render () { + const title = this.props.component != null ? + translate('project_settings.page') : + translate('settings.page'); + + const description = this.props.component != null ? + translate('project_settings.page.description') : + translate('settings.page.description'); + + return ( + <header className="page-header"> + <h1 className="page-title">{title}</h1> + <div className="page-description">{description}</div> + </header> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js new file mode 100644 index 00000000000..f61d6b2aa7a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import groupBy from 'lodash/groupBy'; +import sortBy from 'lodash/sortBy'; +import DefinitionsList from './DefinitionsList'; +import { getSubCategoryName, getSubCategoryDescription } from '../utils'; + +export default class SubCategoryDefinitionsList extends React.Component { + static propTypes = { + component: React.PropTypes.object, + settings: React.PropTypes.array.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + render () { + const bySubCategory = groupBy(this.props.settings, setting => setting.definition.subCategory); + const subCategories = Object.keys(bySubCategory).map(key => ({ + key, + name: getSubCategoryName(bySubCategory[key][0].definition.category, key), + description: getSubCategoryDescription(bySubCategory[key][0].definition.category, key) + })); + const sortedSubCategories = sortBy(subCategories, subCategory => subCategory.name.toLowerCase()); + + return ( + <ul className="settings-sub-categories-list"> + {sortedSubCategories.map(subCategory => ( + <li key={subCategory.key}> + <h2 className="settings-sub-category-name">{subCategory.name}</h2> + {subCategory.description != null && ( + <div className="settings-sub-category-description markdown"> + {subCategory.description} + </div> + )} + <DefinitionsList settings={bySubCategory[subCategory.key]} component={this.props.component}/> + </li> + ))} + </ul> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/Input.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/Input.js new file mode 100644 index 00000000000..02fcbfacef2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/Input.js @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import PropertySetInput from './PropertySetInput'; +import MultiValueInput from './MultiValueInput'; +import PrimitiveInput from './PrimitiveInput'; +import { TYPE_PROPERTY_SET } from '../../constants'; + +export default class Input extends React.Component { + static propTypes = { + setting: React.PropTypes.object.isRequired, + value: React.PropTypes.any, + onChange: React.PropTypes.func.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + render () { + const { definition } = this.props.setting; + + if (definition.multiValues) { + return <MultiValueInput {...this.props}/>; + } + + if (definition.type === TYPE_PROPERTY_SET) { + return <PropertySetInput {...this.props}/>; + } + + return <PrimitiveInput {...this.props}/>; + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.js new file mode 100644 index 00000000000..9f640591835 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.js @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import Toggle from '../../../../components/controls/Toggle'; +import { defaultInputPropTypes } from '../../propTypes'; +import { translate } from '../../../../helpers/l10n'; + +export default class InputForBoolean extends React.Component { + static propTypes = { + ...defaultInputPropTypes, + value: React.PropTypes.oneOfType([React.PropTypes.bool, React.PropTypes.string]) + }; + + render () { + const hasValue = this.props.value != null; + const displayedValue = hasValue ? this.props.value : false; + + return ( + <div className="display-inline-block text-top"> + <Toggle + name={this.props.name} + value={displayedValue} + onChange={this.props.onChange}/> + + {!hasValue && ( + <span className="spacer-left note">{translate('settings.not_set')}</span> + )} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.js new file mode 100644 index 00000000000..6f741da3730 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.js @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import SimpleInput from './SimpleInput'; + +export default class InputForNumber extends React.Component { + render () { + return ( + <SimpleInput {...this.props} className="input-small" type="text"/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.js new file mode 100644 index 00000000000..4e2a783ce11 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.js @@ -0,0 +1,89 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { translate } from '../../../../helpers/l10n'; +import { defaultInputPropTypes } from '../../propTypes'; + +export default class InputForPassword extends React.Component { + static propTypes = defaultInputPropTypes; + + state = { + changing: false + }; + + handleChangeClick (e) { + e.preventDefault(); + e.target.blur(); + this.setState({ changing: true }); + } + + handleCancelChangeClick (e) { + e.preventDefault(); + e.target.blur(); + this.setState({ changing: false }); + } + + handleFormSubmit (e) { + e.preventDefault(); + this.props.onChange(this.refs.input.value); + this.setState({ changing: false }); + } + + renderInput () { + return ( + <div> + <form onSubmit={e => this.handleFormSubmit(e)}> + <input className="hidden" type="password"/> + <input + ref="input" + name={this.props.name} + className="input-large text-top" + type="password" + autoFocus={true} + autoComplete={false}/> + <button className="spacer-left">{translate('set')}</button> + <a className="spacer-left" href="#" onClick={e => this.handleCancelChangeClick(e)}> + {translate('cancel')} + </a> + </form> + </div> + ); + } + + render () { + if (this.state.changing) { + return this.renderInput(); + } + + const hasValue = !!this.props.value; + + return ( + <div> + {hasValue && ( + <i className="big-spacer-right icon-lock icon-gray"/> + )} + + <button onClick={e => this.handleChangeClick(e)}> + {hasValue ? translate('change_verb') : translate('set')} + </button> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.js new file mode 100644 index 00000000000..a0c704623d9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.js @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import Select from 'react-select'; +import { defaultInputPropTypes } from '../../propTypes'; + +export default class InputForSingleSelectList extends React.Component { + static propTypes = { + ...defaultInputPropTypes, + options: React.PropTypes.arrayOf(React.PropTypes.string).isRequired + }; + + handleInputChange (option) { + this.props.onChange(option.value); + } + + render () { + const options = this.props.options.map(option => ({ + label: option, + value: option + })); + + return ( + <Select + name={this.props.name} + className="input-large" + options={options} + clearable={false} + value={this.props.value} + onChange={option => this.handleInputChange(option)}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.js new file mode 100644 index 00000000000..7eaef55399a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.js @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import SimpleInput from './SimpleInput'; + +export default class InputForString extends React.Component { + render () { + return ( + <SimpleInput {...this.props} className="input-large" type="text"/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForText.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForText.js new file mode 100644 index 00000000000..13cbbfabc85 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForText.js @@ -0,0 +1,40 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { defaultInputPropTypes } from '../../propTypes'; + +export default class InputForText extends React.Component { + static propTypes = defaultInputPropTypes; + + handleInputChange (e) { + this.props.onChange(e.target.value); + } + + render () { + return ( + <textarea + name={this.props.name} + className="input-super-large text-top" + rows="5" + value={this.props.value} + onChange={e => this.handleInputChange(e)}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.js new file mode 100644 index 00000000000..fa2facf6ee4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.js @@ -0,0 +1,90 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import PrimitiveInput from './PrimitiveInput'; +import { getEmptyValue } from '../../utils'; + +export default class MultiValueInput extends React.Component { + static propTypes = { + setting: React.PropTypes.object.isRequired, + value: React.PropTypes.array, + onChange: React.PropTypes.func.isRequired + }; + + ensureValue () { + return this.props.value || []; + } + + handleSingleInputChange (index, value) { + const newValue = [...this.ensureValue()]; + newValue.splice(index, 1, value); + this.props.onChange(newValue); + } + + handleDeleteValue (e, index) { + e.preventDefault(); + e.target.blur(); + + const newValue = [...this.ensureValue()]; + newValue.splice(index, 1); + this.props.onChange(newValue); + } + + prepareSetting () { + const { setting } = this.props; + const newDefinition = { ...setting.definition, multiValues: false }; + return { + ...setting, + definition: newDefinition, + values: undefined + }; + } + + renderInput (value, index, isLast) { + return ( + <li key={index} className="spacer-bottom"> + <PrimitiveInput + setting={this.prepareSetting()} + value={value} + onChange={this.handleSingleInputChange.bind(this, index)}/> + + {!isLast && ( + <div className="display-inline-block spacer-left"> + <button className="js-remove-value button-clean" onClick={e => this.handleDeleteValue(e, index)}> + <i className="icon-delete"/> + </button> + </div> + )} + </li> + ); + } + + render () { + const displayedValue = [...this.ensureValue(), ...getEmptyValue(this.props.setting.definition)]; + + return ( + <div> + <ul> + {displayedValue.map((value, index) => this.renderInput(value, index, index === displayedValue.length - 1))} + </ul> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.js new file mode 100644 index 00000000000..61e9db6052d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.js @@ -0,0 +1,75 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import InputForString from './InputForString'; +import InputForText from './InputForText'; +import InputForPassword from './InputForPassword'; +import InputForBoolean from './InputForBoolean'; +import InputForNumber from './InputForNumber'; +import InputForSingleSelectList from './InputForSingleSelectList'; +import { getUniqueName, isDefaultOrInherited } from '../../utils'; +import * as types from '../../constants'; + +const typeMapping = { + [types.TYPE_STRING]: InputForString, + [types.TYPE_TEXT]: InputForText, + [types.TYPE_PASSWORD]: InputForPassword, + [types.TYPE_BOOLEAN]: InputForBoolean, + [types.TYPE_INTEGER]: InputForNumber, + [types.TYPE_LONG]: InputForNumber, + [types.TYPE_FLOAT]: InputForNumber +}; + +export default class PrimitiveInput extends React.Component { + static propTypes = { + setting: React.PropTypes.object.isRequired, + value: React.PropTypes.any, + onChange: React.PropTypes.func.isRequired + }; + + render () { + const { setting, value, onChange, ...other } = this.props; + const { definition } = setting; + + const name = getUniqueName(definition); + + if (definition.type === types.TYPE_SINGLE_SELECT_LIST) { + return ( + <InputForSingleSelectList + name={name} + value={value} + isDefault={isDefaultOrInherited(setting)} + options={definition.options} + onChange={onChange} + {...other}/> + ); + } + + const InputComponent = typeMapping[definition.type] || InputForString; + return ( + <InputComponent + name={name} + value={value} + isDefault={isDefaultOrInherited(setting)} + onChange={onChange} + {...other}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.js new file mode 100644 index 00000000000..636a83896be --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.js @@ -0,0 +1,110 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import PrimitiveInput from './PrimitiveInput'; +import { getEmptyValue, getUniqueName } from '../../utils'; + +export default class PropertySetInput extends React.Component { + static propTypes = { + setting: React.PropTypes.object.isRequired, + value: React.PropTypes.array, + onChange: React.PropTypes.func.isRequired + }; + + ensureValue () { + return this.props.value || []; + } + + getFieldName (field) { + return getUniqueName(this.props.setting.definition, field.key); + } + + handleDeleteValue (e, index) { + e.preventDefault(); + e.target.blur(); + + const newValue = [...this.ensureValue()]; + newValue.splice(index, 1); + this.props.onChange(newValue); + } + + handleInputChange (index, fieldKey, value) { + const emptyValue = getEmptyValue(this.props.setting.definition)[0]; + const newValue = [...this.ensureValue()]; + const newFields = { ...emptyValue, ...newValue[index], [fieldKey]: value }; + newValue.splice(index, 1, newFields); + return this.props.onChange(newValue); + } + + renderFields (fieldValues, index, isLast) { + const { setting } = this.props; + + return ( + <tr key={index}> + {setting.definition.fields.map(field => ( + <td key={field.key}> + <PrimitiveInput + name={this.getFieldName(field)} + setting={{ definition: field, value: fieldValues[field.key] }} + value={fieldValues[field.key]} + onChange={this.handleInputChange.bind(this, index, field.key)}/> + </td> + ))} + <td className="thin nowrap"> + {!isLast && ( + <button className="js-remove-value button-link" onClick={e => this.handleDeleteValue(e, index)}> + <i className="icon-delete"/> + </button> + )} + </td> + </tr> + ); + } + + render () { + const { setting } = this.props; + + const displayedValue = [...this.ensureValue(), ...getEmptyValue(this.props.setting.definition)]; + + return ( + <div> + <table className="data zebra-hover no-outer-padding" style={{ width: 'auto', minWidth: 480, marginTop: -12 }}> + <thead> + <tr> + {setting.definition.fields.map(field => ( + <th key={field.key}> + {field.name} + {field.description != null && ( + <span className="spacer-top small">{field.description}</span> + )} + </th> + ))} + <th> </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)); } |