diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2018-11-19 11:41:06 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2018-11-30 11:20:35 +0100 |
commit | 50c708c1b62796cba971ed6579cb72373261e857 (patch) | |
tree | 0462dcd0c342845ac4a7a1923c0da0c1097a9588 /server/sonar-web | |
parent | 0a52c5067d46ce684231b8f61ea9f72d4d89fc22 (diff) | |
download | sonarqube-50c708c1b62796cba971ed6579cb72373261e857.tar.gz sonarqube-50c708c1b62796cba971ed6579cb72373261e857.zip |
Rewrite remaining of the settings page to TS
Diffstat (limited to 'server/sonar-web')
46 files changed, 858 insertions, 1042 deletions
diff --git a/server/sonar-web/src/main/js/api/settings.ts b/server/sonar-web/src/main/js/api/settings.ts index fbea55db86b..11b37f1d02f 100644 --- a/server/sonar-web/src/main/js/api/settings.ts +++ b/server/sonar-web/src/main/js/api/settings.ts @@ -23,12 +23,17 @@ import { BranchParameters, SettingCategoryDefinition, SettingValue, - SettingType + SettingType, + SettingDefinition } from '../app/types'; import throwGlobalError from '../app/utils/throwGlobalError'; +import { isCategoryDefinition } from '../apps/settings/utils'; export function getDefinitions(component?: string): Promise<SettingCategoryDefinition[]> { - return getJSON('/api/settings/list_definitions', { component }).then(r => r.definitions); + return getJSON('/api/settings/list_definitions', { component }).then( + r => r.definitions, + throwGlobalError + ); } export function getValues( @@ -37,11 +42,15 @@ export function getValues( return getJSON('/api/settings/values', data).then(r => r.settings); } -export function setSettingValue(definition: any, value: any, component?: string): Promise<void> { +export function setSettingValue( + definition: SettingDefinition, + value: any, + component?: string +): Promise<void> { const { key } = definition; const data: RequestData = { key, component }; - if (definition.multiValues) { + if (isCategoryDefinition(definition) && definition.multiValues) { data.values = value; } else if (definition.type === SettingType.PropertySet) { data.fieldValues = value diff --git a/server/sonar-web/src/main/js/app/components/App.tsx b/server/sonar-web/src/main/js/app/components/App.tsx index e5257a4a171..df1277c2222 100644 --- a/server/sonar-web/src/main/js/app/components/App.tsx +++ b/server/sonar-web/src/main/js/app/components/App.tsx @@ -27,6 +27,7 @@ import { fetchMyOrganizations } from '../../apps/account/organizations/actions'; import { getInstance, isSonarCloud } from '../../helpers/system'; import { lazyLoad } from '../../components/lazyLoad'; import { getCurrentUser, getAppState, getGlobalSettingValue, Store } from '../../store/rootReducer'; +import { isLoggedIn } from '../../helpers/users'; const PageTracker = lazyLoad(() => import('./PageTracker')); @@ -70,10 +71,8 @@ class App extends React.PureComponent<Props> { this.mounted = true; this.props.fetchLanguages(); const { appState, currentUser } = this.props; - if (appState && currentUser) { - if (appState.organizationsEnabled && currentUser.isLoggedIn) { - this.props.fetchMyOrganizations(); - } + if (appState && isSonarCloud() && currentUser && isLoggedIn(currentUser)) { + this.props.fetchMyOrganizations(); } } @@ -104,12 +103,16 @@ class App extends React.PureComponent<Props> { } } -const mapStateToProps = (state: Store): StateProps => ({ - appState: getAppState(state), - currentUser: getCurrentUser(state), - enableGravatar: (getGlobalSettingValue(state, 'sonar.lf.enableGravatar') || {}).value === 'true', - gravatarServerUrl: (getGlobalSettingValue(state, 'sonar.lf.gravatarServerUrl') || {}).value || '' -}); +const mapStateToProps = (state: Store): StateProps => { + const enableGravatar = getGlobalSettingValue(state, 'sonar.lf.enableGravatar'); + const gravatarServerUrl = getGlobalSettingValue(state, 'sonar.lf.gravatarServerUrl'); + return { + appState: getAppState(state), + currentUser: getCurrentUser(state), + enableGravatar: Boolean(enableGravatar && enableGravatar.value === 'true'), + gravatarServerUrl: (gravatarServerUrl && gravatarServerUrl.value) || '' + }; +}; const mapDispatchToProps = ({ fetchLanguages, diff --git a/server/sonar-web/src/main/js/app/components/PageTracker.tsx b/server/sonar-web/src/main/js/app/components/PageTracker.tsx index 2ae8c85026b..e1620791c9d 100644 --- a/server/sonar-web/src/main/js/app/components/PageTracker.tsx +++ b/server/sonar-web/src/main/js/app/components/PageTracker.tsx @@ -59,8 +59,11 @@ export class PageTracker extends React.PureComponent<Props> { } } -const mapStateToProps = (state: Store): StateProps => ({ - trackingId: (getGlobalSettingValue(state, 'sonar.analytics.trackingId') || {}).value -}); +const mapStateToProps = (state: Store): StateProps => { + const trackingId = getGlobalSettingValue(state, 'sonar.analytics.trackingId'); + return { + trackingId: trackingId && trackingId.value + }; +}; export default withRouter(connect(mapStateToProps)(PageTracker)); diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/ProductNewsMenuItem.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/ProductNewsMenuItem.tsx index 3037367c20e..a2565b563f7 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/ProductNewsMenuItem.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/ProductNewsMenuItem.tsx @@ -124,8 +124,11 @@ export class ProductNewsMenuItem extends React.PureComponent<Props, State> { } } -const mapStateToProps = (state: Store): StateProps => ({ - accessToken: (getGlobalSettingValue(state, 'sonar.prismic.accessToken') || {}).value -}); +const mapStateToProps = (state: Store): StateProps => { + const accessToken = getGlobalSettingValue(state, 'sonar.prismic.accessToken'); + return { + accessToken: accessToken && accessToken.value + }; +}; export default connect(mapStateToProps)(ProductNewsMenuItem); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.tsx index 876a1ccc62f..7cc8a5e14a4 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.tsx @@ -50,9 +50,13 @@ export function SonarCloudNavBranding() { ); } -const mapStateToProps = (state: Store): StateProps => ({ - customLogoUrl: (getGlobalSettingValue(state, 'sonar.lf.logoUrl') || {}).value, - customLogoWidth: (getGlobalSettingValue(state, 'sonar.lf.logoWidthPx') || {}).value -}); +const mapStateToProps = (state: Store): StateProps => { + const customLogoUrl = getGlobalSettingValue(state, 'sonar.lf.logoUrl'); + const customLogoWidth = getGlobalSettingValue(state, 'sonar.lf.logoWidthPx'); + return { + customLogoUrl: customLogoUrl && customLogoUrl.value, + customLogoWidth: customLogoWidth && customLogoWidth.value + }; +}; export default connect(mapStateToProps)(GlobalNavBranding); diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index 542324c1cd6..427cfe2a463 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -86,6 +86,7 @@ export interface AppState { organizationsEnabled?: boolean; productionDatabase: boolean; qualifiers: string[]; + settings: { [key: string]: string }; standalone?: boolean; version: string; } @@ -739,6 +740,7 @@ export enum SettingType { Boolean = 'BOOLEAN', Float = 'FLOAT', Integer = 'INTEGER', + License = 'LICENSE', Long = 'LONG', SingleSelectList = 'SINGLE_SELECT_LIST', PropertySet = 'PROPERTY_SET' diff --git a/server/sonar-web/src/main/js/apps/about/actions.ts b/server/sonar-web/src/main/js/apps/about/actions.ts index c9f54bb37e1..e99ab66bfb1 100644 --- a/server/sonar-web/src/main/js/apps/about/actions.ts +++ b/server/sonar-web/src/main/js/apps/about/actions.ts @@ -19,12 +19,11 @@ */ import { Dispatch } from 'redux'; import { getValues } from '../../api/settings'; -import { receiveValues } from '../settings/store/values/actions'; +import { receiveValues } from '../settings/store/values'; export function fetchAboutPageSettings() { return (dispatch: Dispatch) => { const keys = ['sonar.lf.aboutText']; - return getValues({ keys: keys.join() }).then(values => { dispatch(receiveValues(values)); }); diff --git a/server/sonar-web/src/main/js/apps/about/components/AboutApp.tsx b/server/sonar-web/src/main/js/apps/about/components/AboutApp.tsx index 6771f875ffe..752000d6e13 100644 --- a/server/sonar-web/src/main/js/apps/about/components/AboutApp.tsx +++ b/server/sonar-web/src/main/js/apps/about/components/AboutApp.tsx @@ -48,7 +48,7 @@ import '../styles.css'; interface Props { appState: Pick<AppState, 'defaultOrganization' | 'organizationsEnabled'>; currentUser: CurrentUser; - customText?: { value: string }; + customText?: string; fetchAboutPageSettings: () => Promise<void>; location: Location; } @@ -158,13 +158,9 @@ class AboutApp extends React.PureComponent<Props, State> { </div> </div> - {customText != null && - customText.value && ( - <div - className="about-page-section" - dangerouslySetInnerHTML={{ __html: customText.value }} - /> - )} + {customText && ( + <div className="about-page-section" dangerouslySetInnerHTML={{ __html: customText }} /> + )} <AboutLanguages /> @@ -195,11 +191,14 @@ class AboutApp extends React.PureComponent<Props, State> { } } -const mapStateToProps = (state: Store) => ({ - appState: getAppState(state), - currentUser: getCurrentUser(state), - customText: getGlobalSettingValue(state, 'sonar.lf.aboutText') -}); +const mapStateToProps = (state: Store) => { + const customText = getGlobalSettingValue(state, 'sonar.lf.aboutText'); + return { + appState: getAppState(state), + currentUser: getCurrentUser(state), + customText: customText && customText.value + }; +}; const mapDispatchToProps = { fetchAboutPageSettings } as any; diff --git a/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx b/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx index 94d4f9a7952..3ec744841b8 100644 --- a/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx +++ b/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx @@ -33,7 +33,7 @@ import { import { Organization } from '../../../app/types'; interface StateProps { - anyoneCanCreate?: { value: string }; + anyoneCanCreate: boolean; canAdmin?: boolean; organizations: Organization[]; } @@ -68,9 +68,7 @@ class UserOrganizations extends React.PureComponent<Props, State> { }; render() { - const anyoneCanCreate = - this.props.anyoneCanCreate != null && this.props.anyoneCanCreate.value === 'true'; - + const { anyoneCanCreate } = this.props; const canCreateOrganizations = !this.state.loading && (anyoneCanCreate || this.props.canAdmin); return ( @@ -100,11 +98,14 @@ class UserOrganizations extends React.PureComponent<Props, State> { } } -const mapStateToProps = (state: Store): StateProps => ({ - anyoneCanCreate: getGlobalSettingValue(state, 'sonar.organizations.anyoneCanCreate'), - canAdmin: getAppState(state).canAdmin, - organizations: getMyOrganizations(state) -}); +const mapStateToProps = (state: Store): StateProps => { + const anyoneCanCreate = getGlobalSettingValue(state, 'sonar.organizations.anyoneCanCreate'); + return { + anyoneCanCreate: Boolean(anyoneCanCreate && anyoneCanCreate.value === 'true'), + canAdmin: getAppState(state).canAdmin, + organizations: getMyOrganizations(state) + }; +}; const mapDispatchToProps = { fetchIfAnyoneCanCreateOrganizations: fetchIfAnyoneCanCreateOrganizations as any diff --git a/server/sonar-web/src/main/js/apps/account/organizations/actions.ts b/server/sonar-web/src/main/js/apps/account/organizations/actions.ts index 73fa8566dcd..329d831e107 100644 --- a/server/sonar-web/src/main/js/apps/account/organizations/actions.ts +++ b/server/sonar-web/src/main/js/apps/account/organizations/actions.ts @@ -21,7 +21,7 @@ import { Dispatch } from 'redux'; import { getOrganizations } from '../../../api/organizations'; import { receiveMyOrganizations } from '../../../store/organizations'; import { getValues } from '../../../api/settings'; -import { receiveValues } from '../../settings/store/values/actions'; +import { receiveValues } from '../../settings/store/values'; export const fetchMyOrganizations = () => (dispatch: Dispatch) => { return getOrganizations({ member: true }).then(({ organizations }) => { @@ -31,6 +31,6 @@ export const fetchMyOrganizations = () => (dispatch: Dispatch) => { export const fetchIfAnyoneCanCreateOrganizations = () => (dispatch: Dispatch) => { return getValues({ keys: 'sonar.organizations.anyoneCanCreate' }).then(values => { - dispatch(receiveValues(values, undefined)); + dispatch(receiveValues(values)); }); }; diff --git a/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx b/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx index 76627ae77bd..463d28dacbe 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx @@ -36,11 +36,11 @@ interface StateToProps { } const mapStateToProps = (state: Store) => { + const updateCenterActive = getGlobalSettingValue(state, 'sonar.updatecenter.activate'); return { currentEdition: getAppState(state).edition, standaloneMode: getAppState(state).standalone, - updateCenterActive: - (getGlobalSettingValue(state, 'sonar.updatecenter.activate') || {}).value === 'true' + updateCenterActive: Boolean(updateCenterActive && updateCenterActive.value === 'true') }; }; 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 deleted file mode 100644 index e59bff69f20..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info 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. - */ -// @flow -import React from 'react'; -import { connect } from 'react-redux'; -import CategoriesList from './CategoriesList'; -import { getSettingsAppAllCategories } from '../../../store/rootReducer'; - -function AllCategoriesList(props) { - return <CategoriesList {...props} />; -} - -const mapStateToProps = state => ({ - categories: getSettingsAppAllCategories(state) -}); - -export default connect(mapStateToProps)(AllCategoriesList); diff --git a/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js b/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.tsx index 8b61e566c2e..f01a2ed2b25 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js +++ b/server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.tsx @@ -17,49 +17,42 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; +import * as React from 'react'; +import * as classNames from 'classnames'; +import { connect } from 'react-redux'; import { sortBy } from 'lodash'; import { IndexLink } from 'react-router'; import { getCategoryName } from '../utils'; +import { Component } from '../../../app/types'; +import { getSettingsAppAllCategories, Store } from '../../../store/rootReducer'; -/*:: -type Category = { - key: string, - name: string -}; -*/ - -/*:: -type Props = { - categories: Category[], - component?: { key: string }, - defaultCategory: string, - selectedCategory: string -}; -*/ - -export default class CategoriesList extends React.PureComponent { - /*:: rops: Props; */ - - renderLink(category /*: Category */) { - const query /*: Object */ = {}; - - if (category.key !== this.props.defaultCategory) { - query.category = category.key.toLowerCase(); - } - - if (this.props.component) { - query.id = this.props.component.key; - } +interface Category { + key: string; + name: string; +} - const className = - category.key.toLowerCase() === this.props.selectedCategory.toLowerCase() ? 'active' : ''; +interface Props { + categories: string[]; + component?: Component; + defaultCategory: string; + selectedCategory: string; +} +export class CategoriesList extends React.PureComponent<Props> { + renderLink(category: Category) { + const { component, defaultCategory, selectedCategory } = this.props; const pathname = this.props.component ? '/project/settings' : '/settings'; - + const query = { + category: category.key !== defaultCategory ? category.key.toLowerCase() : undefined, + id: component && component.key + }; return ( - <IndexLink className={className} title={category.name} to={{ pathname, query }}> + <IndexLink + className={classNames({ + active: category.key.toLowerCase() === selectedCategory.toLowerCase() + })} + title={category.name} + to={{ pathname, query }}> {category.name} </IndexLink> ); @@ -71,7 +64,6 @@ export default class CategoriesList extends React.PureComponent { name: getCategoryName(key) })); const sortedCategories = sortBy(categoriesWithName, category => category.name.toLowerCase()); - return ( <ul className="side-tabs-menu"> {sortedCategories.map(category => ( @@ -81,3 +73,9 @@ export default class CategoriesList extends React.PureComponent { ); } } + +const mapStateToProps = (state: Store) => ({ + categories: getSettingsAppAllCategories(state) +}); + +export default connect(mapStateToProps)(CategoriesList); diff --git a/server/sonar-web/src/main/js/apps/settings/components/AppContainer.js b/server/sonar-web/src/main/js/apps/settings/components/AppContainer.js deleted file mode 100644 index d2d4f003e7a..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/components/AppContainer.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info 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 App from './App'; -import { fetchSettings } from '../store/actions'; -import { getSettingsAppDefaultCategory } from '../../../store/rootReducer'; - -const mapStateToProps = state => ({ - defaultCategory: getSettingsAppDefaultCategory(state) -}); - -const mapdispatchToProps = { fetchSettings }; - -export default connect( - mapStateToProps, - mapdispatchToProps -)(App); diff --git a/server/sonar-web/src/main/js/apps/settings/components/App.js b/server/sonar-web/src/main/js/apps/settings/components/AppContainer.tsx index 378494100f6..7a65fab906e 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/App.js +++ b/server/sonar-web/src/main/js/apps/settings/components/AppContainer.tsx @@ -17,51 +17,64 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; +import * as React from 'react'; import Helmet from 'react-helmet'; -import PageHeader from './PageHeader'; -import CategoryDefinitionsList from './CategoryDefinitionsList'; +import { connect } from 'react-redux'; +import { WithRouterProps } from 'react-router'; import AllCategoriesList from './AllCategoriesList'; +import CategoryDefinitionsList from './CategoryDefinitionsList'; +import PageHeader from './PageHeader'; import WildcardsHelp from './WildcardsHelp'; import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; +import { fetchSettings } from '../store/actions'; +import { getSettingsAppDefaultCategory, Store } from '../../../store/rootReducer'; import { translate } from '../../../helpers/l10n'; +import { Component } from '../../../app/types'; import '../styles.css'; import '../side-tabs.css'; -/*:: -type Props = { - component?: { key: string }, - defaultCategory: ?string, - fetchSettings(componentKey: ?string): Promise<*>, - location: { query: {} } -}; -*/ - -/*:: -type State = { - loaded: boolean -}; -*/ - -export default class App extends React.PureComponent { - /*:: props: Props; */ - state /*: State */ = { loaded: false }; +interface Props { + component?: Component; + defaultCategory: string; + fetchSettings(component?: string): Promise<void>; +} + +interface State { + loading: boolean; +} + +export class App extends React.PureComponent<Props & WithRouterProps, State> { + mounted = false; + state: State = { loading: true }; componentDidMount() { - const componentKey = this.props.component ? this.props.component.key : null; - this.props.fetchSettings(componentKey).then(() => this.setState({ loaded: true })); + this.mounted = true; + this.fetchSettings(); } - componentDidUpdate(prevProps /*: Props*/) { + componentDidUpdate(prevProps: Props) { if (prevProps.component !== this.props.component) { - const componentKey = this.props.component ? this.props.component.key : null; - this.props.fetchSettings(componentKey); + this.fetchSettings(); } } + componentWillUnmount() { + this.mounted = false; + } + + fetchSettings = () => { + const { component } = this.props; + this.props.fetchSettings(component && component.key).then(this.stopLoading, this.stopLoading); + }; + + stopLoading = () => { + if (this.mounted) { + this.setState({ loading: false }); + } + }; + render() { - if (!this.state.loaded) { + if (this.state.loading) { return null; } @@ -92,3 +105,14 @@ export default class App extends React.PureComponent { ); } } + +const mapStateToProps = (state: Store) => ({ + defaultCategory: getSettingsAppDefaultCategory(state) +}); + +const mapDispatchToProps = { fetchSettings: fetchSettings as any }; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(App); 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.tsx index 630ce8bbc98..87f503f9ee8 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.js +++ b/server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.tsx @@ -17,17 +17,22 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow import { connect } from 'react-redux'; import SubCategoryDefinitionsList from './SubCategoryDefinitionsList'; import { fetchValues } from '../store/actions'; -import { getSettingsAppSettingsForCategory } from '../../../store/rootReducer'; +import { getSettingsAppSettingsForCategory, Store } from '../../../store/rootReducer'; +import { Component } from '../../../app/types'; -const mapStateToProps = (state, ownProps) => ({ +interface Props { + category: string; + component?: Component; +} + +const mapStateToProps = (state: Store, ownProps: Props) => ({ settings: getSettingsAppSettingsForCategory( state, ownProps.category, - ownProps.component ? ownProps.component.key : null + ownProps.component && ownProps.component.key ) }); 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.tsx index 1ce6ac3ba10..d9b9b34d837 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/Definition.js +++ b/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx @@ -17,9 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; -import PropTypes from 'prop-types'; +import * as React from 'react'; import { connect } from 'react-redux'; import classNames from 'classnames'; import Input from './inputs/Input'; @@ -34,35 +32,37 @@ import AlertErrorIcon from '../../../components/icons-components/AlertErrorIcon' import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessIcon'; import { translateWithParameters, translate } from '../../../helpers/l10n'; import { resetValue, saveValue, checkValue } from '../store/actions'; -import { passValidation } from '../store/settingsPage/validationMessages/actions'; -import { cancelChange, changeValue } from '../store/settingsPage/changedValues/actions'; +import { cancelChange, changeValue, passValidation } from '../store/settingsPage'; import { getSettingsAppChangedValue, + getSettingsAppValidationMessage, isSettingsAppLoading, - getSettingsAppValidationMessage + Store } from '../../../store/rootReducer'; +import { Component, Setting } from '../../../app/types'; + +interface Props { + cancelChange: (key: string) => void; + changedValue: any; + changeValue: (key: string, value: any) => void; + checkValue: (key: string) => boolean; + component?: Component; + loading: boolean; + passValidation: (key: string) => void; + resetValue: (key: string, component?: string) => Promise<void>; + saveValue: (key: string, component?: string) => Promise<void>; + setting: Setting; + validationMessage?: string; +} -class Definition extends React.PureComponent { - /*:: mounted: boolean; */ - /*:: timeout: number; */ - - static propTypes = { - component: PropTypes.object, - setting: PropTypes.object.isRequired, - changedValue: PropTypes.any, - loading: PropTypes.bool.isRequired, - validationMessage: PropTypes.string, - - changeValue: PropTypes.func.isRequired, - cancelChange: PropTypes.func.isRequired, - saveValue: PropTypes.func.isRequired, - resetValue: PropTypes.func.isRequired, - passValidation: PropTypes.func.isRequired - }; +interface State { + success: boolean; +} - state = { - success: false - }; +export class Definition extends React.PureComponent<Props, State> { + timeout?: number; + mounted = false; + state = { success: false }; componentDidMount() { this.mounted = true; @@ -72,87 +72,80 @@ class Definition extends React.PureComponent { this.mounted = false; } - safeSetState(changes) { + safeSetState(changes: State) { if (this.mounted) { this.setState(changes); } } - handleChange = value => { + handleChange = (value: any) => { clearTimeout(this.timeout); this.props.changeValue(this.props.setting.definition.key, value); this.handleCheck(); }; 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.props.cancelChange(definition.key, componentKey); - this.safeSetState({ success: true }); - this.timeout = setTimeout(() => this.safeSetState({ success: false }), 3000); - }) - .catch(() => { - /* do nothing */ - }); + const { component, setting } = this.props; + const { definition } = setting; + const componentKey = component && component.key; + return this.props.resetValue(definition.key, componentKey).then(() => { + this.props.cancelChange(definition.key); + this.safeSetState({ success: true }); + this.timeout = window.setTimeout(() => this.safeSetState({ success: false }), 3000); + }); }; handleCancel = () => { - const componentKey = this.props.component ? this.props.component.key : null; - this.props.cancelChange(this.props.setting.definition.key, componentKey); - this.props.passValidation(this.props.setting.definition.key); + const { setting } = this.props; + this.props.cancelChange(setting.definition.key); + this.props.passValidation(setting.definition.key); }; handleCheck = () => { - const componentKey = this.props.component ? this.props.component.key : null; - this.props.checkValue(this.props.setting.definition.key, componentKey); + const { setting } = this.props; + this.props.checkValue(setting.definition.key); }; handleSave = () => { if (this.props.changedValue != null) { this.safeSetState({ success: false }); - const componentKey = this.props.component ? this.props.component.key : null; - this.props - .saveValue(this.props.setting.definition.key, componentKey) - .then(() => { + const { component, setting } = this.props; + this.props.saveValue(setting.definition.key, component && component.key).then( + () => { this.safeSetState({ success: true }); - this.timeout = setTimeout(() => this.safeSetState({ success: false }), 3000); - }) - .catch(() => { - /* do nothing */ - }); + this.timeout = window.setTimeout(() => this.safeSetState({ success: false }), 3000); + }, + () => {} + ); } }; render() { - const { setting, changedValue, loading } = this.props; + const { changedValue, loading, setting, validationMessage } = this.props; const { definition } = setting; const propertyName = getPropertyName(definition); - const hasError = this.props.validationMessage != null; - + const hasError = validationMessage != null; const hasValueChanged = changedValue != null; - - const className = classNames('settings-definition', { - 'settings-definition-changed': hasValueChanged - }); - const effectiveValue = hasValueChanged ? changedValue : getSettingValue(setting); - const isDefault = isDefaultOrInherited(setting); - + const description = getPropertyDescription(definition); return ( - <div className={className} data-key={definition.key}> + <div + className={classNames('settings-definition', { + 'settings-definition-changed': hasValueChanged + })} + data-key={definition.key}> <div className="settings-definition-left"> <h3 className="settings-definition-name" title={propertyName}> {propertyName} </h3> - <div - className="markdown small spacer-top" - dangerouslySetInnerHTML={{ __html: getPropertyDescription(definition) }} - /> + {description && ( + <div + className="markdown small spacer-top" + dangerouslySetInnerHTML={{ __html: description }} + /> + )} <div className="settings-definition-key note little-spacer-top"> {translateWithParameters('settings.key_x', definition.key)} @@ -169,14 +162,11 @@ class Definition extends React.PureComponent { )} {!loading && - hasError && ( + validationMessage && ( <span className="text-danger"> <AlertErrorIcon className="spacer-right" /> <span> - {translateWithParameters( - 'settings.state.validation_failed', - this.props.validationMessage - )} + {translateWithParameters('settings.state.validation_failed', validationMessage)} </span> </span> )} @@ -216,20 +206,22 @@ class Definition extends React.PureComponent { } } -const mapStateToProps = (state, ownProps) => ({ +const mapStateToProps = (state: Store, ownProps: Pick<Props, 'setting'>) => ({ changedValue: getSettingsAppChangedValue(state, ownProps.setting.definition.key), loading: isSettingsAppLoading(state, ownProps.setting.definition.key), validationMessage: getSettingsAppValidationMessage(state, ownProps.setting.definition.key) }); +const mapDispatchToProps = { + cancelChange: cancelChange as any, + changeValue: changeValue as any, + checkValue: checkValue as any, + passValidation: passValidation as any, + resetValue: resetValue as any, + saveValue: saveValue as any +}; + export default connect( mapStateToProps, - { - changeValue, - saveValue, - resetValue, - passValidation, - cancelChange, - checkValue - } + mapDispatchToProps )(Definition); 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 deleted file mode 100644 index d62e687ce8b..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info 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. - */ -// @flow -import React from 'react'; -import PropTypes from 'prop-types'; -import Definition from './Definition'; - -export default class DefinitionsList extends React.PureComponent { - static propTypes = { - component: PropTypes.object, - settings: PropTypes.array.isRequired - }; - - 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/store/values/actions.ts b/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.tsx index a7d6f8f30a8..2f597afc239 100644 --- a/server/sonar-web/src/main/js/apps/settings/store/values/actions.ts +++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.tsx @@ -17,13 +17,23 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -interface SettingValue { - key: string; - value?: string; -} +import * as React from 'react'; +import Definition from './Definition'; +import { Component, Setting } from '../../../app/types'; -export const RECEIVE_VALUES = 'RECEIVE_VALUES'; +interface Props { + component?: Component; + settings: Setting[]; +} -export function receiveValues(settings: SettingValue[], componentKey?: string) { - return { type: RECEIVE_VALUES, settings, componentKey }; +export default function DefinitionsList({ component, settings }: Props) { + return ( + <ul className="settings-definitions-list"> + {settings.map(setting => ( + <li key={setting.definition.key}> + <Definition component={component} setting={setting} /> + </li> + ))} + </ul> + ); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/EmailForm.js b/server/sonar-web/src/main/js/apps/settings/components/EmailForm.tsx index e3777e25633..f1516115bba 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/EmailForm.js +++ b/server/sonar-web/src/main/js/apps/settings/components/EmailForm.tsx @@ -17,36 +17,79 @@ * 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 * as React from 'react'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { sendTestEmail } from '../../../api/settings'; import { parseError } from '../../../helpers/request'; -import { getCurrentUser } from '../../../store/rootReducer'; import { SubmitButton } from '../../../components/ui/buttons'; import { Alert } from '../../../components/ui/Alert'; +import { LoggedInUser } from '../../../app/types'; +import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; -class EmailForm extends React.PureComponent { - constructor(props) { +interface Props { + currentUser: LoggedInUser; +} + +interface State { + recipient: string; + subject: string; + message: string; + loading: boolean; + success: boolean; + error?: string; +} + +class EmailForm extends React.PureComponent<Props, State> { + mounted = false; + + constructor(props: Props) { super(props); this.state = { - recipient: this.props.currentUser.email, + recipient: this.props.currentUser.email || '', subject: translate('email_configuration.test.subject'), message: translate('email_configuration.test.message_text'), loading: false, - success: false, - error: null + success: false }; } - handleFormSubmit = event => { + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleError = (error: { response: Response }) => { + return parseError(error).then(message => { + if (this.mounted) { + this.setState({ error: message, loading: false }); + } + }); + }; + + handleFormSubmit = (event: React.FormEvent) => { event.preventDefault(); - this.setState({ success: false, error: null, loading: true }); + this.setState({ success: false, error: undefined, loading: true }); const { recipient, subject, message } = this.state; - sendTestEmail(recipient, subject, message).then( - () => this.setState({ success: true, loading: false }), - error => parseError(error).then(message => this.setState({ error: message, loading: false })) - ); + sendTestEmail(recipient, subject, message).then(() => { + if (this.mounted) { + this.setState({ success: true, loading: false }); + } + }, this.handleError); + }; + + onRecipientChange = (event: React.ChangeEvent<HTMLInputElement>) => { + this.setState({ recipient: event.target.value }); + }; + + onSubjectChange = (event: React.ChangeEvent<HTMLInputElement>) => { + this.setState({ subject: event.target.value }); + }; + + onMessageChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => { + this.setState({ message: event.target.value }); }; render() { @@ -81,7 +124,7 @@ class EmailForm extends React.PureComponent { className="settings-large-input" disabled={this.state.loading} id="test-email-to" - onChange={e => this.setState({ recipient: e.target.value })} + onChange={this.onRecipientChange} required={true} type="email" value={this.state.recipient} @@ -95,7 +138,7 @@ class EmailForm extends React.PureComponent { className="settings-large-input" disabled={this.state.loading} id="test-email-subject" - onChange={e => this.setState({ subject: e.target.value })} + onChange={this.onSubjectChange} type="text" value={this.state.subject} /> @@ -109,9 +152,9 @@ class EmailForm extends React.PureComponent { className="settings-large-input" disabled={this.state.loading} id="test-email-title" - onChange={e => this.setState({ message: e.target.value })} + onChange={this.onMessageChange} required={true} - rows="5" + rows={5} value={this.state.message} /> </div> @@ -128,8 +171,4 @@ class EmailForm extends React.PureComponent { } } -const mapStateToProps = state => ({ - currentUser: getCurrentUser(state) -}); - -export default connect(mapStateToProps)(EmailForm); +export default withCurrentUser(EmailForm); 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.tsx index 77e21d06141..18b2f644585 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/PageHeader.js +++ b/server/sonar-web/src/main/js/apps/settings/components/PageHeader.tsx @@ -17,35 +17,28 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; -import PropTypes from 'prop-types'; +import * as React from 'react'; import InstanceMessage from '../../../components/common/InstanceMessage'; import { translate } from '../../../helpers/l10n'; +import { Component } from '../../../app/types'; -export default class PageHeader extends React.PureComponent { - static propTypes = { - component: PropTypes.object - }; +interface Props { + component?: Component; +} - render() { - const title = - this.props.component != null - ? translate('project_settings.page') - : translate('settings.page'); +export default function PageHeader({ component }: Props) { + const title = component ? translate('project_settings.page') : translate('settings.page'); - const description = - this.props.component != null ? ( - translate('project_settings.page.description') - ) : ( - <InstanceMessage message={translate('settings.page.description')} /> - ); + const description = component ? ( + translate('project_settings.page.description') + ) : ( + <InstanceMessage message={translate('settings.page.description')} /> + ); - return ( - <header className="page-header"> - <h1 className="page-title">{title}</h1> - <div className="page-description">{description}</div> - </header> - ); - } + 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.tsx index 075d96c4734..34fa14caaa0 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.js +++ b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx @@ -17,26 +17,26 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; -import PropTypes from 'prop-types'; +import * as React from 'react'; import { groupBy, isEqual, sortBy } from 'lodash'; import DefinitionsList from './DefinitionsList'; import EmailForm from './EmailForm'; import { getSubCategoryName, getSubCategoryDescription } from '../utils'; +import { Component, SettingCategoryDefinition, Setting } from '../../../app/types'; -export default class SubCategoryDefinitionsList extends React.PureComponent { - static propTypes = { - component: PropTypes.object, - fetchValues: PropTypes.func, - settings: PropTypes.array.isRequired - }; +interface Props { + category: string; + component?: Component; + fetchValues: Function; + settings: Array<Setting & { definition: SettingCategoryDefinition }>; +} +export default class SubCategoryDefinitionsList extends React.PureComponent<Props> { componentDidMount() { this.fetchValues(); } - componentDidUpdate(prevProps /*: Object */) { + componentDidUpdate(prevProps: Props) { const prevKeys = prevProps.settings.map(setting => setting.definition.key); const keys = this.props.settings.map(setting => setting.definition.key); if (prevProps.component !== this.props.component || !isEqual(prevKeys, keys)) { @@ -49,13 +49,13 @@ export default class SubCategoryDefinitionsList extends React.PureComponent { this.props.fetchValues(keys, this.props.component && this.props.component.key); } - renderEmailForm(subCategoryKey /*: string */) { + renderEmailForm = (subCategoryKey: string) => { const isEmailSettings = this.props.category === 'general' && subCategoryKey === 'email'; if (!isEmailSettings) { return null; } return <EmailForm />; - } + }; render() { const bySubCategory = groupBy(this.props.settings, setting => setting.definition.subCategory); @@ -67,7 +67,6 @@ export default class SubCategoryDefinitionsList extends React.PureComponent { const sortedSubCategories = sortBy(subCategories, subCategory => subCategory.name.toLowerCase() ); - return ( <ul className="settings-sub-categories-list"> {sortedSubCategories.map(subCategory => ( diff --git a/server/sonar-web/src/main/js/apps/settings/components/WildcardsHelp.js b/server/sonar-web/src/main/js/apps/settings/components/WildcardsHelp.tsx index 42c2047aecf..bcebe583031 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/WildcardsHelp.js +++ b/server/sonar-web/src/main/js/apps/settings/components/WildcardsHelp.tsx @@ -17,27 +17,28 @@ * 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 * as React from 'react'; +import { translate } from '../../../helpers/l10n'; export default function WildcardsHelp() { return ( <div className="huge-spacer-top"> - <h2 className="spacer-bottom">Wildcards</h2> - <p className="spacer-bottom">Following rules are applied:</p> + <h2 className="spacer-bottom">{translate('settings.wildcards')}</h2> + <p className="spacer-bottom">{translate('settings.wildcards.following_rules_are_applied')}</p> <table className="data spacer-bottom"> <tbody> <tr> <td>*</td> - <td>Match zero or more characters</td> + <td>{translate('settings.wildcards.zero_more_char')}</td> </tr> <tr> <td>**</td> - <td>Match zero or more directories</td> + <td>{translate('settings.wildcards.zero_more_dir')}</td> </tr> <tr> <td>?</td> - <td>Match a single character</td> + <td>{translate('settings.wildcards.single_char')}</td> </tr> </tbody> </table> @@ -45,72 +46,72 @@ export default function WildcardsHelp() { <table className="data zebra"> <thead> <tr> - <th>Example</th> - <th>Matches</th> - <th>Does not match</th> + <th>{translate('example')}</th> + <th>{translate('settings.wildcards.matches')}</th> + <th>{translate('settings.wildcards.does_no_match')}</th> </tr> </thead> <tbody> <tr> - <td>**/foo/*.js</td> + <td>{'**/foo/*.js'}</td> <td> <ul> - <li>src/foo/bar.js</li> - <li>lib/ui/foo/bar.js</li> + <li>{'src/foo/bar.js'}</li> + <li>{'lib/ui/foo/bar.js'}</li> </ul> </td> <td> <ul> - <li>src/bar.js</li> - <li>src/foo2/bar.js</li> + <li>{'src/bar.js'}</li> + <li>{'src/foo2/bar.js'}</li> </ul> </td> </tr> <tr> - <td>src/foo/*bar*.js</td> + <td>{'src/foo/*bar*.js'}</td> <td> <ul> - <li>src/foo/bar.js</li> - <li>src/foo/bar1.js</li> - <li>src/foo/bar123.js</li> - <li>src/foo/123bar123.js</li> + <li>{'src/foo/bar.js'}</li> + <li>{'src/foo/bar1.js'}</li> + <li>{'src/foo/bar123.js'}</li> + <li>{'src/foo/123bar123.js'}</li> </ul> </td> <td> <ul> - <li>src/foo/ui/bar.js</li> - <li>src/bar.js</li> + <li>{'src/foo/ui/bar.js'}</li> + <li>{'src/bar.js'}</li> </ul> </td> </tr> <tr> - <td>src/foo/**</td> + <td>{'src/foo/**'}</td> <td> <ul> - <li>src/foo/bar.js</li> - <li>src/foo/ui/bar.js</li> + <li>{'src/foo/bar.js'}</li> + <li>{'src/foo/ui/bar.js'}</li> </ul> </td> <td> <ul> - <li>src/bar/foo/bar.js</li> - <li>src/bar.js</li> + <li>{'src/bar/foo/bar.js'}</li> + <li>{'src/bar.js'}</li> </ul> </td> </tr> <tr> - <td>**/foo?.js</td> + <td>{'**/foo?.js'}</td> <td> <ul> - <li>src/foo1.js</li> - <li>src/bar/foo1.js</li> + <li>{'src/foo1.js'}</li> + <li>{'src/bar/foo1.js'}</li> </ul> </td> <td> <ul> - <li>src/foo.js</li> - <li>src/foo12.js</li> - <li>src/12foo3.js</li> + <li>{'src/foo.js'}</li> + <li>{'src/foo12.js'}</li> + <li>{'src/12foo3.js'}</li> </ul> </td> </tr> diff --git a/server/sonar-web/src/main/js/apps/settings/__tests__/DefinitionActions-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/DefinitionActions-test.tsx index 76b1d29491c..432be0e9f3b 100644 --- a/server/sonar-web/src/main/js/apps/settings/__tests__/DefinitionActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/DefinitionActions-test.tsx @@ -20,8 +20,8 @@ /* eslint-disable import/order */ import * as React from 'react'; import { shallow } from 'enzyme'; -import DefinitionActions from '../components/DefinitionActions'; -import { SettingType } from '../../../app/types'; +import DefinitionActions from '../DefinitionActions'; +import { SettingType } from '../../../../app/types'; const definition = { category: 'baz', diff --git a/server/sonar-web/src/main/js/apps/settings/__tests__/__snapshots__/DefinitionActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/DefinitionActions-test.tsx.snap index 046e36dbc27..046e36dbc27 100644 --- a/server/sonar-web/src/main/js/apps/settings/__tests__/__snapshots__/DefinitionActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/DefinitionActions-test.tsx.snap 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 deleted file mode 100644 index 34bdf549004..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/store/actions.js +++ /dev/null @@ -1,127 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info 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 { receiveValues } from './values/actions'; -import { receiveDefinitions } from './definitions/actions'; -import { startLoading, stopLoading } from './settingsPage/loading/actions'; -import { passValidation, failValidation } from './settingsPage/validationMessages/actions'; -import { cancelChange } from './settingsPage/changedValues/actions'; -import { - getDefinitions, - getValues, - setSettingValue, - resetSettingValue -} from '../../../api/settings'; -import { parseError } from '../../../helpers/request'; -import { addGlobalErrorMessage, closeAllGlobalMessages } from '../../../store/globalMessages'; -import { isEmptyValue } from '../utils'; -import { translate } from '../../../helpers/l10n'; -import { getSettingsAppDefinition, getSettingsAppChangedValue } from '../../../store/rootReducer'; - -export const fetchSettings = componentKey => dispatch => { - return getDefinitions(componentKey).then( - definitions => { - const filtered = definitions - .filter(definition => definition.type !== 'LICENSE') - // do not display this setting on project level - .filter( - definition => - componentKey == null || definition.key !== 'sonar.branch.longLivedBranches.regex' - ); - dispatch(receiveDefinitions(filtered)); - }, - e => parseError(e).then(message => dispatch(addGlobalErrorMessage(message))) - ); -}; - -export const fetchValues = (keys, component) => dispatch => - getValues({ keys, component }).then( - settings => { - dispatch(receiveValues(settings, component)); - dispatch(closeAllGlobalMessages()); - }, - () => {} - ); - -export const checkValue = (key, componentKey) => (dispatch, getState) => { - const state = getState(); - const definition = getSettingsAppDefinition(state, key); - const value = getSettingsAppChangedValue(state, key); - - if (isEmptyValue(definition, value)) { - if (definition.defaultValue === undefined) { - dispatch(failValidation(key, translate('settings.state.value_cant_be_empty_no_default'))); - } else { - dispatch(failValidation(key, translate('settings.state.value_cant_be_empty'))); - } - return false; - } - - dispatch(passValidation(key)); - return true; -}; - -export const saveValue = (key, componentKey) => (dispatch, getState) => { - dispatch(startLoading(key)); - - const state = getState(); - const definition = getSettingsAppDefinition(state, key); - const value = getSettingsAppChangedValue(state, key); - - if (isEmptyValue(definition, value)) { - dispatch(failValidation(key, translate('settings.state.value_cant_be_empty'))); - dispatch(stopLoading(key)); - return Promise.reject(); - } - - return setSettingValue(definition, value, componentKey) - .then(() => getValues({ keys: key, component: componentKey })) - .then(values => { - dispatch(receiveValues(values, componentKey)); - 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({ keys: key, component: componentKey }) - .then(() => getValues({ keys: key, component: componentKey })) - .then(values => { - if (values.length > 0) { - dispatch(receiveValues(values, componentKey)); - } else { - dispatch(receiveValues([{ key }], componentKey)); - } - 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/actions.ts b/server/sonar-web/src/main/js/apps/settings/store/actions.ts new file mode 100644 index 00000000000..d3b3526c06f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/actions.ts @@ -0,0 +1,141 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info 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 { Dispatch } from 'redux'; +import { receiveValues } from './values'; +import { receiveDefinitions } from './definitions'; +import { + cancelChange, + failValidation, + passValidation, + startLoading, + stopLoading +} from './settingsPage'; +import { + getDefinitions, + getValues, + setSettingValue, + resetSettingValue +} from '../../../api/settings'; +import { parseError } from '../../../helpers/request'; +import { closeAllGlobalMessages } from '../../../store/globalMessages'; +import { isEmptyValue } from '../utils'; +import { translate } from '../../../helpers/l10n'; +import { + getSettingsAppDefinition, + getSettingsAppChangedValue, + Store +} from '../../../store/rootReducer'; +import { SettingType } from '../../../app/types'; + +export function fetchSettings(component?: string) { + return (dispatch: Dispatch) => { + return getDefinitions(component).then(definitions => { + const filtered = definitions + .filter(definition => definition.type !== SettingType.License) + // do not display this setting on project level + .filter( + definition => !component || definition.key !== 'sonar.branch.longLivedBranches.regex' + ); + dispatch(receiveDefinitions(filtered)); + }); + }; +} + +export function fetchValues(keys: string, component?: string) { + return (dispatch: Dispatch) => + getValues({ keys, component }).then(settings => { + dispatch(receiveValues(settings, component)); + dispatch(closeAllGlobalMessages()); + }); +} + +export function checkValue(key: string) { + return (dispatch: Dispatch, getState: () => Store) => { + const state = getState(); + const definition = getSettingsAppDefinition(state, key); + const value = getSettingsAppChangedValue(state, key); + + if (isEmptyValue(definition, value)) { + if (definition.defaultValue === undefined) { + dispatch(failValidation(key, translate('settings.state.value_cant_be_empty_no_default'))); + } else { + dispatch(failValidation(key, translate('settings.state.value_cant_be_empty'))); + } + return false; + } + + dispatch(passValidation(key)); + return true; + }; +} + +export function saveValue(key: string, component?: string) { + return (dispatch: Dispatch, getState: () => Store) => { + dispatch(startLoading(key)); + const state = getState(); + const definition = getSettingsAppDefinition(state, key); + const value = getSettingsAppChangedValue(state, key); + + if (isEmptyValue(definition, value)) { + dispatch(failValidation(key, translate('settings.state.value_cant_be_empty'))); + dispatch(stopLoading(key)); + return Promise.reject(); + } + + return setSettingValue(definition, value, component) + .then(() => getValues({ keys: key, component })) + .then(values => { + dispatch(receiveValues(values, component)); + dispatch(cancelChange(key)); + dispatch(passValidation(key)); + dispatch(stopLoading(key)); + }) + .catch(handleError(key, dispatch)); + }; +} + +export function resetValue(key: string, component?: string) { + return (dispatch: Dispatch) => { + dispatch(startLoading(key)); + + return resetSettingValue({ keys: key, component }) + .then(() => getValues({ keys: key, component })) + .then(values => { + if (values.length > 0) { + dispatch(receiveValues(values, component)); + } else { + dispatch(receiveValues([{ key }], component)); + } + dispatch(passValidation(key)); + dispatch(stopLoading(key)); + }) + .catch(handleError(key, dispatch)); + }; +} + +function handleError(key: string, dispatch: Dispatch) { + return (error: { response: Response }) => { + dispatch(stopLoading(key)); + return parseError(error).then(message => { + dispatch(failValidation(key, message)); + return Promise.reject(); + }); + }; +} 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.ts index 8d5e17530ae..fef242043bf 100644 --- a/server/sonar-web/src/main/js/apps/settings/store/definitions/reducer.js +++ b/server/sonar-web/src/main/js/apps/settings/store/definitions.ts @@ -17,49 +17,53 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow import { keyBy, sortBy, uniqBy } from 'lodash'; -import { RECEIVE_DEFINITIONS } from './actions'; -import { DEFAULT_CATEGORY, getCategoryName } from '../../utils'; -/*:: import type { Definition } from '../../types'; */ +import { ActionType } from '../../../store/utils/actions'; +import { SettingCategoryDefinition } from '../../../app/types'; +import { DEFAULT_CATEGORY, getCategoryName } from '../utils'; -/*:: -type State = { [key: string]: Definition }; -*/ +const enum Actions { + ReceiveDefinitions = 'RECEIVE_DEFINITIONS' +} + +type Action = ActionType<typeof receiveDefinitions, Actions.ReceiveDefinitions>; + +export interface State { + [key: string]: SettingCategoryDefinition; +} -/*:: -type Action = { type: string, definitions: Definition[] }; -*/ +export function receiveDefinitions(definitions: SettingCategoryDefinition[]) { + return { type: Actions.ReceiveDefinitions, definitions }; +} -const reducer = (state /*: State */ = {}, action /*: Action */) => { - if (action.type === RECEIVE_DEFINITIONS) { +export default function components(state: State = {}, action: Action) { + if (action.type === Actions.ReceiveDefinitions) { return keyBy(action.definitions, 'key'); } - return state; -}; - -export default reducer; +} -export function getDefinition(state /*: State */, key /*: string */) /*: Definition */ { +export function getDefinition(state: State, key: string) { return state[key]; } -export function getAllDefinitions(state /*: State */) /*: Definition[] */ { +export function getAllDefinitions(state: State) { return Object.keys(state).map(key => state[key]); } -export const getDefinitionsForCategory = (state /*: State */, category /*: string */) => - getAllDefinitions(state).filter( +export function getDefinitionsForCategory(state: State, category: string) { + return getAllDefinitions(state).filter( definition => definition.category.toLowerCase() === category.toLowerCase() ); +} -export const getAllCategories = (state /*: State */) => - uniqBy(getAllDefinitions(state).map(definition => definition.category), category => +export function getAllCategories(state: State) { + return uniqBy(getAllDefinitions(state).map(definition => definition.category), category => category.toLowerCase() ); +} -export const getDefaultCategory = (state /*: State */) => { +export function getDefaultCategory(state: State) { const categories = getAllCategories(state); if (categories.includes(DEFAULT_CATEGORY)) { return DEFAULT_CATEGORY; @@ -69,4 +73,4 @@ export const getDefaultCategory = (state /*: State */) => { ); return sortedCategories[0]; } -}; +} 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 deleted file mode 100644 index e80a3ea20f7..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/store/definitions/actions.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info 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. - */ -// @flow -/*:: import type { Definition } from '../../types'; */ - -export const RECEIVE_DEFINITIONS /*: string */ = 'RECEIVE_DEFINITIONS'; - -/** - * Receive definitions action creator - * @param {Array} definitions - * @returns {Object} - */ -export const receiveDefinitions = (definitions /*: Definition[] */) => ({ - type: RECEIVE_DEFINITIONS, - definitions -}); 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 deleted file mode 100644 index 24b85559003..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/store/rootReducer.js +++ /dev/null @@ -1,79 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info 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. - */ -// @flow -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 '../../../store/globalMessages'; -/*:: import type { State as GlobalMessagesState } from '../../../store/globalMessages'; */ -/*:: import type { State as ValuesState } from './values/reducer'; */ - -/*:: -type State = { - definitions: {}, - globalMessages: GlobalMessagesState, - settingsPage: {}, - values: ValuesState -}; -*/ - -const rootReducer = combineReducers({ - definitions, - values, - settingsPage, - globalMessages -}); - -export default rootReducer; - -export const getDefinition = (state /*: State */, key /*: string */) => - fromDefinitions.getDefinition(state.definitions, key); - -export const getAllCategories = (state /*: State */) => - fromDefinitions.getAllCategories(state.definitions); - -export const getDefaultCategory = (state /*: State */) => - fromDefinitions.getDefaultCategory(state.definitions); - -export const getValue = (state /*: State */, key /*: string */, componentKey /*: ?string */) => - fromValues.getValue(state.values, key, componentKey); - -export const getSettingsForCategory = ( - state /*: State */, - category /*: string */, - componentKey /*: ?string */ -) => - fromDefinitions.getDefinitionsForCategory(state.definitions, category).map(definition => ({ - ...getValue(state, definition.key, componentKey), - definition - })); - -export const getChangedValue = (state /*: State */, key /*: string */) => - fromSettingsPage.getChangedValue(state.settingsPage, key); - -export const isLoading = (state /*: State */, key /*: string */) => - fromSettingsPage.isLoading(state.settingsPage, key); - -export const getValidationMessage = (state /*: State */, key /*: string */) => - fromSettingsPage.getValidationMessage(state.settingsPage, key); - -export const getGlobalMessages = (state /*: State */) => - fromGlobalMessages.getGlobalMessages(state.globalMessages); diff --git a/server/sonar-web/src/main/js/apps/settings/store/rootReducer.ts b/server/sonar-web/src/main/js/apps/settings/store/rootReducer.ts new file mode 100644 index 00000000000..625d6490155 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/rootReducer.ts @@ -0,0 +1,73 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info 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'; +import values, * as fromValues from './values'; +import settingsPage, * as fromSettingsPage from './settingsPage'; +import globalMessages, * as fromGlobalMessages from '../../../store/globalMessages'; + +interface State { + definitions: fromDefinitions.State; + globalMessages: fromGlobalMessages.State; + settingsPage: fromSettingsPage.State; + values: fromValues.State; +} + +export default combineReducers({ definitions, values, settingsPage, globalMessages }); + +export function getDefinition(state: State, key: string) { + return fromDefinitions.getDefinition(state.definitions, key); +} + +export function getAllCategories(state: State) { + return fromDefinitions.getAllCategories(state.definitions); +} + +export function getDefaultCategory(state: State) { + return fromDefinitions.getDefaultCategory(state.definitions); +} + +export function getValue(state: State, key: string, component?: string) { + return fromValues.getValue(state.values, key, component); +} + +export function getSettingsForCategory(state: State, category: string, component?: string) { + return fromDefinitions.getDefinitionsForCategory(state.definitions, category).map(definition => ({ + key: definition.key, + ...getValue(state, definition.key, component), + definition + })); +} + +export function getChangedValue(state: State, key: string) { + return fromSettingsPage.getChangedValue(state.settingsPage, key); +} + +export function isLoading(state: State, key: string) { + return fromSettingsPage.isLoading(state.settingsPage, key); +} + +export function getValidationMessage(state: State, key: string) { + return fromSettingsPage.getValidationMessage(state.settingsPage, key); +} + +export function getGlobalMessages(state: State) { + return fromGlobalMessages.getGlobalMessages(state.globalMessages); +} diff --git a/server/sonar-web/src/main/js/apps/settings/store/settingsPage.ts b/server/sonar-web/src/main/js/apps/settings/store/settingsPage.ts new file mode 100644 index 00000000000..25365a8d232 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/settingsPage.ts @@ -0,0 +1,113 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info 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'; +import { combineReducers } from 'redux'; +import { ActionType } from '../../../store/utils/actions'; + +const enum Actions { + CancelChange = 'settingsPage/CANCEL_CHANGE', + ChangeValue = 'settingsPage/CHANGE_VALUE', + FailValidation = 'settingsPage/FAIL_VALIDATION', + PassValidation = 'settingsPage/PASS_VALIDATION', + StartLoading = 'settingsPage/START_LOADING', + StopLoading = 'settingsPage/STOP_LOADING' +} + +type Action = + | ActionType<typeof cancelChange, Actions.CancelChange> + | ActionType<typeof changeValue, Actions.ChangeValue> + | ActionType<typeof failValidation, Actions.FailValidation> + | ActionType<typeof passValidation, Actions.PassValidation> + | ActionType<typeof startLoading, Actions.StartLoading> + | ActionType<typeof stopLoading, Actions.StopLoading>; + +export interface State { + changedValues: { [key: string]: any }; + loading: { [key: string]: boolean }; + validationMessages: { [key: string]: string }; +} + +export function cancelChange(key: string) { + return { type: Actions.CancelChange, key }; +} + +export function changeValue(key: string, value: any) { + return { type: Actions.ChangeValue, key, value }; +} + +function changedValues(state: State['changedValues'] = {}, action: Action) { + if (action.type === Actions.ChangeValue) { + return { ...state, [action.key]: action.value }; + } + if (action.type === Actions.CancelChange) { + return omit(state, action.key); + } + return state; +} + +export function failValidation(key: string, message: string) { + return { type: Actions.FailValidation, key, message }; +} + +export function passValidation(key: string) { + return { type: Actions.PassValidation, key }; +} + +function validationMessages(state: State['validationMessages'] = {}, action: Action) { + if (action.type === Actions.FailValidation) { + return { ...state, [action.key]: action.message }; + } + if (action.type === Actions.PassValidation) { + return omit(state, action.key); + } + return state; +} + +export function startLoading(key: string) { + return { type: Actions.StartLoading, key }; +} + +export function stopLoading(key: string) { + return { type: Actions.StopLoading, key }; +} + +function loading(state: State['loading'] = {}, action: Action) { + if (action.type === Actions.StartLoading) { + return { ...state, [action.key]: true }; + } + if (action.type === Actions.StopLoading) { + return { ...state, [action.key]: false }; + } + return state; +} + +export default combineReducers({ changedValues, loading, validationMessages }); + +export function getChangedValue(state: State, key: string) { + return state.changedValues[key]; +} + +export function getValidationMessage(state: State, key: string): string | undefined { + return state.validationMessages[key]; +} + +export function isLoading(state: State, key: string) { + return Boolean(state.loading[key]); +} 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 deleted file mode 100644 index 0364e252770..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/actions.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info 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 deleted file mode 100644 index 1c6978d7558..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/changedValues/reducer.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info 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'; -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 deleted file mode 100644 index 291c0feb459..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/actions.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info 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 deleted file mode 100644 index e220fab09a4..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/loading/reducer.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info 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 deleted file mode 100644 index f19e0584984..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/reducer.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info 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 deleted file mode 100644 index d073581f67d..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/actions.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info 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 deleted file mode 100644 index 9b8da6ffd32..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/store/settingsPage/validationMessages/reducer.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info 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.ts b/server/sonar-web/src/main/js/apps/settings/store/values.ts new file mode 100644 index 00000000000..e00727a86a5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/store/values.ts @@ -0,0 +1,85 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info 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 { keyBy } from 'lodash'; +import { ActionType } from '../../../store/utils/actions'; +import { Action as AppStateAction, Actions as AppStateActions } from '../../../store/appState'; +import { SettingValue } from '../../../app/types'; + +enum Actions { + receiveValues = 'RECEIVE_VALUES' +} + +type Action = ActionType<typeof receiveValues, Actions.receiveValues>; + +interface SettingsState { + [key: string]: SettingValue; +} + +export interface State { + components: { [component: string]: SettingsState }; + global: SettingsState; +} + +export function receiveValues( + settings: Array<{ key: string; value?: string }>, + component?: string +) { + return { type: Actions.receiveValues, settings, component }; +} + +function components(state: State['components'] = {}, action: Action) { + const { component: key } = action; + if (!key) { + return state; + } + if (action.type === Actions.receiveValues) { + const settingsByKey = keyBy(action.settings, 'key'); + return { ...state, [key]: { ...(state[key] || {}), ...settingsByKey } }; + } + return state; +} + +function global(state: State['components'] = {}, action: Action | AppStateAction) { + if (action.type === Actions.receiveValues) { + if (action.component) { + return state; + } + const settingsByKey = keyBy(action.settings, 'key'); + return { ...state, ...settingsByKey }; + } + if (action.type === AppStateActions.SetAppState) { + const settingsByKey: SettingsState = {}; + Object.keys(action.appState.settings).forEach( + key => (settingsByKey[key] = { key, value: action.appState.settings[key] }) + ); + return { ...state, ...settingsByKey }; + } + return state; +} + +export default combineReducers({ components, global }); + +export function getValue(state: State, key: string, component?: string): SettingValue | undefined { + if (component) { + return state.components[component] && state.components[component][key]; + } + return state.global[key]; +} 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 deleted file mode 100644 index 5da0fdc6da8..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/store/values/reducer.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info 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. - */ -// @flow -import { combineReducers } from 'redux'; -import { keyBy } from 'lodash'; -import { RECEIVE_VALUES } from './actions'; - -/*:: -type SettingsState = { [key: string]: {} }; -type ComponentsState = { [key: string]: SettingsState }; -export type State = { components: ComponentsState, global: SettingsState }; -*/ - -const componentsSettings = (state /*: ComponentsState */ = {}, action /*: Object */) => { - if (!action.componentKey) { - return state; - } - - const key = action.componentKey; - if (action.type === RECEIVE_VALUES) { - const settingsByKey = keyBy(action.settings, 'key'); - return { ...state, [key]: { ...(state[key] || {}), ...settingsByKey } }; - } - - return state; -}; - -const globalSettings = (state /*: SettingsState */ = {}, action /*: Object */) => { - if (action.componentKey) { - return state; - } - - if (action.type === RECEIVE_VALUES) { - const settingsByKey = keyBy(action.settings, 'key'); - return { ...state, ...settingsByKey }; - } - - if (action.type === 'SET_APP_STATE') { - const settingsByKey = {}; - Object.keys(action.appState.settings).forEach( - key => (settingsByKey[key] = { value: action.appState.settings[key] }) - ); - return { ...state, ...settingsByKey }; - } - - return state; -}; - -export default combineReducers({ components: componentsSettings, global: globalSettings }); - -export const getValue = (state /*: State */, key /*: string */, componentKey /*: ?string */) => { - let settings = state.global; - if (componentKey) { - settings = state.components[componentKey]; - } - return settings && settings[key]; -}; diff --git a/server/sonar-web/src/main/js/apps/settings/types.js b/server/sonar-web/src/main/js/apps/settings/types.js deleted file mode 100644 index c0f10cb73ea..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/types.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 SonarSource SA - * mailto:info 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. - */ -// @flow -/*:: -export type Definition = { - key: string, - category: string -}; -*/ diff --git a/server/sonar-web/src/main/js/components/ui/Avatar.tsx b/server/sonar-web/src/main/js/components/ui/Avatar.tsx index ad31cabb65d..6f32f8930cb 100644 --- a/server/sonar-web/src/main/js/components/ui/Avatar.tsx +++ b/server/sonar-web/src/main/js/components/ui/Avatar.tsx @@ -55,10 +55,14 @@ function Avatar(props: Props) { ); } -const mapStateToProps = (state: Store) => ({ - enableGravatar: (getGlobalSettingValue(state, 'sonar.lf.enableGravatar') || {}).value === 'true', - gravatarServerUrl: (getGlobalSettingValue(state, 'sonar.lf.gravatarServerUrl') || {}).value -}); +const mapStateToProps = (state: Store) => { + const enableGravatar = getGlobalSettingValue(state, 'sonar.lf.enableGravatar'); + const gravatarServerUrl = getGlobalSettingValue(state, 'sonar.lf.gravatarServerUrl'); + return { + enableGravatar: Boolean(enableGravatar && enableGravatar.value === 'true'), + gravatarServerUrl: (gravatarServerUrl && gravatarServerUrl.value) || '' + }; +}; export default connect(mapStateToProps)(Avatar); diff --git a/server/sonar-web/src/main/js/store/appState.ts b/server/sonar-web/src/main/js/store/appState.ts index 19a5e546944..89f31e98e12 100644 --- a/server/sonar-web/src/main/js/store/appState.ts +++ b/server/sonar-web/src/main/js/store/appState.ts @@ -21,21 +21,27 @@ import { ActionType } from './utils/actions'; import { Extension, AppState } from '../app/types'; import { EditionKey } from '../apps/marketplace/utils'; -type Action = - | ActionType<typeof setAppState, 'SET_APP_STATE'> - | ActionType<typeof setAdminPages, 'SET_ADMIN_PAGES'> - | ActionType<typeof requireAuthorization, 'REQUIRE_AUTHORIZATION'>; +export const enum Actions { + SetAppState = 'SET_APP_STATE', + SetAdminPages = 'SET_ADMIN_PAGES', + RequireAuthorization = 'REQUIRE_AUTHORIZATION' +} + +export type Action = + | ActionType<typeof setAppState, Actions.SetAppState> + | ActionType<typeof setAdminPages, Actions.SetAdminPages> + | ActionType<typeof requireAuthorization, Actions.RequireAuthorization>; export function setAppState(appState: AppState) { - return { type: 'SET_APP_STATE', appState }; + return { type: Actions.SetAppState, appState }; } export function setAdminPages(adminPages: Extension[]) { - return { type: 'SET_ADMIN_PAGES', adminPages }; + return { type: Actions.SetAdminPages, adminPages }; } export function requireAuthorization() { - return { type: 'REQUIRE_AUTHORIZATION' }; + return { type: Actions.RequireAuthorization }; } const defaultValue: AppState = { @@ -46,21 +52,19 @@ const defaultValue: AppState = { organizationsEnabled: false, productionDatabase: true, qualifiers: [], + settings: {}, version: '' }; export default function(state: AppState = defaultValue, action: Action): AppState { - if (action.type === 'SET_APP_STATE') { + if (action.type === Actions.SetAppState) { return { ...state, ...action.appState }; } - - if (action.type === 'SET_ADMIN_PAGES') { + if (action.type === Actions.SetAdminPages) { return { ...state, adminPages: action.adminPages }; } - - if (action.type === 'REQUIRE_AUTHORIZATION') { + if (action.type === Actions.RequireAuthorization) { return { ...state, authorizationError: true }; } - return state; } diff --git a/server/sonar-web/src/main/js/store/globalMessages.ts b/server/sonar-web/src/main/js/store/globalMessages.ts index eabb58635a8..b1059021a6d 100644 --- a/server/sonar-web/src/main/js/store/globalMessages.ts +++ b/server/sonar-web/src/main/js/store/globalMessages.ts @@ -41,8 +41,8 @@ export function closeGlobalMessage(id: string) { return { type: 'CLOSE_GLOBAL_MESSAGE', id }; } -export function closeAllGlobalMessages(id: string) { - return { type: 'CLOSE_ALL_GLOBAL_MESSAGES', id }; +export function closeAllGlobalMessages() { + return { type: 'CLOSE_ALL_GLOBAL_MESSAGES' }; } type Action = diff --git a/server/sonar-web/src/main/js/store/rootReducer.ts b/server/sonar-web/src/main/js/store/rootReducer.ts index 3d3bcfacec7..336fe056b6b 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.ts +++ b/server/sonar-web/src/main/js/store/rootReducer.ts @@ -109,9 +109,9 @@ export function getSettingsAppDefaultCategory(state: Store) { export function getSettingsAppSettingsForCategory( state: Store, category: string, - componentKey: string + component?: string ) { - return fromSettingsApp.getSettingsForCategory(state.settingsApp, category, componentKey); + return fromSettingsApp.getSettingsForCategory(state.settingsApp, category, component); } export function getSettingsAppChangedValue(state: Store, key: string) { |