]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-15376 Make secured settings hidden when set
authorMathieu Suen <mathieu.suen@sonarsource.com>
Wed, 8 Sep 2021 14:51:02 +0000 (16:51 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 15 Sep 2021 20:03:23 +0000 (20:03 +0000)
49 files changed:
server/sonar-web/src/main/js/apps/about/actions.ts
server/sonar-web/src/main/js/apps/audit-logs/components/AuditApp.tsx
server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/settings/components/AppContainer.tsx [deleted file]
server/sonar-web/src/main/js/apps/settings/components/Definition.tsx
server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx
server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/AppContainer-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/DefinitionActions-test.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AppContainer-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsApp-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SubCategoryDefinitionsList-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/inputs/Input.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSecured.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/SimpleInput.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForJSON-test.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForNumber-test.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSecured-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForString-test.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/PrimitiveInput-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/__snapshots__/PrimitiveInput-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.tsx
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/AlmSpecificForm-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/routes.ts
server/sonar-web/src/main/js/apps/settings/store/__tests__/actions-test.ts
server/sonar-web/src/main/js/apps/settings/store/__tests__/rootReducer-test.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/store/actions.ts
server/sonar-web/src/main/js/apps/settings/store/rootReducer.ts
server/sonar-web/src/main/js/apps/settings/store/values.ts
server/sonar-web/src/main/js/apps/settings/utils.ts
server/sonar-web/src/main/js/helpers/mocks/settings.ts
server/sonar-web/src/main/js/types/settings.ts

index 02c0e9fea40ecd26d799b04c485637b860c2be15..b1cd8ec733f59885afc50918842659c32f9cb448 100644 (file)
@@ -25,7 +25,7 @@ export function fetchAboutPageSettings() {
   return (dispatch: Dispatch) => {
     const keys = ['sonar.lf.aboutText'];
     return getValues({ keys: keys.join() }).then(values => {
-      dispatch(receiveValues(values));
+      dispatch(receiveValues(keys, values));
     });
   };
 }
index b33011d4050f77310ba64b8c256275601795e53c..651c516b531c6e611a281cdff59b1d86ea9a7ad4 100644 (file)
@@ -47,7 +47,7 @@ export class AuditApp extends React.PureComponent<Props, State> {
   componentDidMount() {
     const { hasGovernanceExtension } = this.props;
     if (hasGovernanceExtension) {
-      this.props.fetchValues('sonar.dbcleaner.auditHousekeeping');
+      this.props.fetchValues(['sonar.dbcleaner.auditHousekeeping']);
     }
   }
 
index 38d70015b5567081b6e739949c7d5e0910efffb8..5e9861a8cdd3842e9d6c4e0bae6f0f4d0fcee87b 100644 (file)
@@ -21,7 +21,8 @@ import { mockDefinition } from '../../../helpers/mocks/settings';
 import {
   Setting,
   SettingCategoryDefinition,
-  SettingFieldDefinition
+  SettingFieldDefinition,
+  SettingType
 } from '../../../types/settings';
 import { buildSettingLink, getDefaultValue, getEmptyValue } from '../utils';
 
@@ -42,7 +43,7 @@ describe('#getEmptyValue()', () => {
   it('should work for property sets', () => {
     const setting: SettingCategoryDefinition = {
       ...settingDefinition,
-      type: 'PROPERTY_SET',
+      type: SettingType.PROPERTY_SET,
       fields
     };
     expect(getEmptyValue(setting)).toEqual([{ foo: '', bar: null }]);
@@ -51,7 +52,7 @@ describe('#getEmptyValue()', () => {
   it('should work for multi values string', () => {
     const setting: SettingCategoryDefinition = {
       ...settingDefinition,
-      type: 'STRING',
+      type: SettingType.STRING,
       multiValues: true
     };
     expect(getEmptyValue(setting)).toEqual(['']);
@@ -60,7 +61,7 @@ describe('#getEmptyValue()', () => {
   it('should work for multi values boolean', () => {
     const setting: SettingCategoryDefinition = {
       ...settingDefinition,
-      type: 'BOOLEAN',
+      type: SettingType.BOOLEAN,
       multiValues: true
     };
     expect(getEmptyValue(setting)).toEqual([null]);
@@ -75,7 +76,8 @@ describe('#getDefaultValue()', () => {
     'should work for boolean field when passing "%s"',
     (parentValue?: string, expected?: string) => {
       const setting: Setting = {
-        definition: { key: 'test', options: [], type: 'BOOLEAN' },
+        hasValue: true,
+        definition: { key: 'test', options: [], type: SettingType.BOOLEAN },
         parentValue,
         key: 'test'
       };
diff --git a/server/sonar-web/src/main/js/apps/settings/components/AppContainer.tsx b/server/sonar-web/src/main/js/apps/settings/components/AppContainer.tsx
deleted file mode 100644 (file)
index 685699d..0000000
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 { find } from 'lodash';
-import * as React from 'react';
-import { Helmet } from 'react-helmet-async';
-import { connect } from 'react-redux';
-import { WithRouterProps } from 'react-router';
-import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
-import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
-import { translate } from '../../../helpers/l10n';
-import {
-  addSideBarClass,
-  addWhitePageClass,
-  removeSideBarClass,
-  removeWhitePageClass
-} from '../../../helpers/pages';
-import { getSettingsAppDefaultCategory, Store } from '../../../store/rootReducer';
-import { fetchSettings } from '../store/actions';
-import '../styles.css';
-import { ADDITIONAL_CATEGORIES } from './AdditionalCategories';
-import AllCategoriesList from './AllCategoriesList';
-import CategoryDefinitionsList from './CategoryDefinitionsList';
-import CATEGORY_OVERRIDES from './CategoryOverrides';
-import PageHeader from './PageHeader';
-
-interface Props {
-  component?: T.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() {
-    this.mounted = true;
-    addSideBarClass();
-    addWhitePageClass();
-    this.fetchSettings();
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (prevProps.component !== this.props.component) {
-      this.fetchSettings();
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-    removeSideBarClass();
-    removeWhitePageClass();
-  }
-
-  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.loading) {
-      return null;
-    }
-
-    const { query } = this.props.location;
-    const originalCategory = (query.category as string) || this.props.defaultCategory;
-    const overriddenCategory = CATEGORY_OVERRIDES[originalCategory.toLowerCase()];
-    const selectedCategory = overriddenCategory || originalCategory;
-    const foundAdditionalCategory = find(ADDITIONAL_CATEGORIES, c => c.key === selectedCategory);
-    const isProjectSettings = this.props.component;
-    const shouldRenderAdditionalCategory =
-      foundAdditionalCategory &&
-      ((isProjectSettings && foundAdditionalCategory.availableForProject) ||
-        (!isProjectSettings && foundAdditionalCategory.availableGlobally));
-
-    return (
-      <div id="settings-page">
-        <Suggestions suggestions="settings" />
-        <Helmet defer={false} title={translate('settings.page')} />
-        <PageHeader component={this.props.component} />
-
-        <div className="layout-page">
-          <ScreenPositionHelper className="layout-page-side-outer">
-            {({ top }) => (
-              <div className="layout-page-side" style={{ top }}>
-                <div className="layout-page-side-inner">
-                  <AllCategoriesList
-                    component={this.props.component}
-                    defaultCategory={this.props.defaultCategory}
-                    selectedCategory={selectedCategory}
-                  />
-                </div>
-              </div>
-            )}
-          </ScreenPositionHelper>
-
-          <div className="layout-page-main">
-            <div className="layout-page-main-inner">
-              <div className="big-padded">
-                {foundAdditionalCategory && shouldRenderAdditionalCategory ? (
-                  foundAdditionalCategory.renderComponent({
-                    component: this.props.component,
-                    selectedCategory: originalCategory
-                  })
-                ) : (
-                  <CategoryDefinitionsList
-                    category={selectedCategory}
-                    component={this.props.component}
-                  />
-                )}
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
-    );
-  }
-}
-
-const mapStateToProps = (state: Store) => ({
-  defaultCategory: getSettingsAppDefaultCategory(state)
-});
-
-const mapDispatchToProps = { fetchSettings: fetchSettings as any };
-
-export default connect(mapStateToProps, mapDispatchToProps)(App);
index 7f9994de43946d43673d6b717532a59e340c7c48..f237d48a30436bd6b0e54e01dcc889a6ba7b1fe9 100644 (file)
@@ -189,26 +189,26 @@ export class Definition extends React.PureComponent<Props, State> {
               </span>
             )}
           </div>
-
-          <Input
-            hasValueChanged={hasValueChanged}
-            onCancel={this.handleCancel}
-            onChange={this.handleChange}
-            onSave={this.handleSave}
-            setting={setting}
-            value={effectiveValue}
-          />
-
-          <DefinitionActions
-            changedValue={changedValue}
-            hasError={hasError}
-            hasValueChanged={hasValueChanged}
-            isDefault={isDefault}
-            onCancel={this.handleCancel}
-            onReset={this.handleReset}
-            onSave={this.handleSave}
-            setting={setting}
-          />
+          <form>
+            <Input
+              hasValueChanged={hasValueChanged}
+              onCancel={this.handleCancel}
+              onChange={this.handleChange}
+              onSave={this.handleSave}
+              setting={setting}
+              value={effectiveValue}
+            />
+            <DefinitionActions
+              changedValue={changedValue}
+              hasError={hasError}
+              hasValueChanged={hasValueChanged}
+              isDefault={isDefault}
+              onCancel={this.handleCancel}
+              onReset={this.handleReset}
+              onSave={this.handleSave}
+              setting={setting}
+            />
+          </form>
         </div>
       </div>
     );
index 6ccf74d70a2c1f9b2464c9be8a0f460fe36f3559..c995f082b7ff21f180a1e3646a3f73b216e8e87f 100644 (file)
@@ -22,7 +22,7 @@ import { Button, ResetButtonLink, SubmitButton } from '../../../components/contr
 import Modal from '../../../components/controls/Modal';
 import { translate } from '../../../helpers/l10n';
 import { Setting } from '../../../types/settings';
-import { getDefaultValue, getSettingValue, isEmptyValue } from '../utils';
+import { getDefaultValue, isEmptyValue } from '../utils';
 
 type Props = {
   changedValue: string;
@@ -74,13 +74,10 @@ export default class DefinitionActions extends React.PureComponent<Props, State>
   }
 
   render() {
-    const { setting, isDefault, changedValue, hasValueChanged } = this.props;
-
-    const hasValueToResetTo = !isEmptyValue(setting.definition, getSettingValue(setting));
+    const { setting, changedValue, isDefault, hasValueChanged } = this.props;
     const hasBeenChangedToEmptyValue =
       changedValue != null && isEmptyValue(setting.definition, changedValue);
-    const showReset =
-      hasValueToResetTo && (hasBeenChangedToEmptyValue || (!isDefault && !hasValueChanged));
+    const showReset = hasBeenChangedToEmptyValue || (!isDefault && setting.hasValue);
 
     return (
       <>
diff --git a/server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx b/server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx
new file mode 100644 (file)
index 0000000..5149787
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { find } from 'lodash';
+import * as React from 'react';
+import { Helmet } from 'react-helmet-async';
+import { connect } from 'react-redux';
+import { WithRouterProps } from 'react-router';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
+import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
+import { translate } from '../../../helpers/l10n';
+import {
+  addSideBarClass,
+  addWhitePageClass,
+  removeSideBarClass,
+  removeWhitePageClass
+} from '../../../helpers/pages';
+import { getSettingsAppDefaultCategory, Store } from '../../../store/rootReducer';
+import { fetchSettings } from '../store/actions';
+import '../styles.css';
+import { ADDITIONAL_CATEGORIES } from './AdditionalCategories';
+import AllCategoriesList from './AllCategoriesList';
+import CategoryDefinitionsList from './CategoryDefinitionsList';
+import CATEGORY_OVERRIDES from './CategoryOverrides';
+import PageHeader from './PageHeader';
+
+interface Props {
+  component?: T.Component;
+  defaultCategory: string;
+  fetchSettings(component?: string): Promise<void>;
+}
+
+interface State {
+  loading: boolean;
+}
+
+export class SettingsApp extends React.PureComponent<Props & WithRouterProps, State> {
+  mounted = false;
+  state: State = { loading: true };
+
+  componentDidMount() {
+    this.mounted = true;
+    addSideBarClass();
+    addWhitePageClass();
+    this.fetchSettings();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.component !== this.props.component) {
+      this.fetchSettings();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+    removeSideBarClass();
+    removeWhitePageClass();
+  }
+
+  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.loading) {
+      return null;
+    }
+
+    const { query } = this.props.location;
+    const originalCategory = (query.category as string) || this.props.defaultCategory;
+    const overriddenCategory = CATEGORY_OVERRIDES[originalCategory.toLowerCase()];
+    const selectedCategory = overriddenCategory || originalCategory;
+    const foundAdditionalCategory = find(ADDITIONAL_CATEGORIES, c => c.key === selectedCategory);
+    const isProjectSettings = this.props.component;
+    const shouldRenderAdditionalCategory =
+      foundAdditionalCategory &&
+      ((isProjectSettings && foundAdditionalCategory.availableForProject) ||
+        (!isProjectSettings && foundAdditionalCategory.availableGlobally));
+
+    return (
+      <div id="settings-page">
+        <Suggestions suggestions="settings" />
+        <Helmet defer={false} title={translate('settings.page')} />
+        <PageHeader component={this.props.component} />
+
+        <div className="layout-page">
+          <ScreenPositionHelper className="layout-page-side-outer">
+            {({ top }) => (
+              <div className="layout-page-side" style={{ top }}>
+                <div className="layout-page-side-inner">
+                  <AllCategoriesList
+                    component={this.props.component}
+                    defaultCategory={this.props.defaultCategory}
+                    selectedCategory={selectedCategory}
+                  />
+                </div>
+              </div>
+            )}
+          </ScreenPositionHelper>
+
+          <div className="layout-page-main">
+            <div className="layout-page-main-inner">
+              <div className="big-padded">
+                {foundAdditionalCategory && shouldRenderAdditionalCategory ? (
+                  foundAdditionalCategory.renderComponent({
+                    component: this.props.component,
+                    selectedCategory: originalCategory
+                  })
+                ) : (
+                  <CategoryDefinitionsList
+                    category={selectedCategory}
+                    component={this.props.component}
+                  />
+                )}
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
+
+const mapStateToProps = (state: Store) => ({
+  defaultCategory: getSettingsAppDefaultCategory(state)
+});
+
+const mapDispatchToProps = { fetchSettings: fetchSettings as any };
+
+export default connect(mapStateToProps, mapDispatchToProps)(SettingsApp);
index dfec70e0e8616bff61e0616676ae2597a775d82a..be3b88a7c789881f6c3602f34ad08cfd3fd06fda 100644 (file)
@@ -75,7 +75,7 @@ export class SubCategoryDefinitionsList extends React.PureComponent<
   };
 
   fetchValues() {
-    const keys = this.props.settings.map(setting => setting.definition.key).join();
+    const keys = this.props.settings.map(setting => setting.definition.key);
     return this.props.fetchValues(keys, this.props.component && this.props.component.key);
   }
 
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/AppContainer-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/AppContainer-test.tsx
deleted file mode 100644 (file)
index 03a755a..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import ScreenPositionHelper from '../../../../components/common/ScreenPositionHelper';
-import {
-  addSideBarClass,
-  addWhitePageClass,
-  removeSideBarClass,
-  removeWhitePageClass
-} from '../../../../helpers/pages';
-import { mockLocation, mockRouter } from '../../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-import {
-  ALM_INTEGRATION,
-  ANALYSIS_SCOPE_CATEGORY,
-  LANGUAGES_CATEGORY,
-  NEW_CODE_PERIOD_CATEGORY,
-  PULL_REQUEST_DECORATION_BINDING_CATEGORY
-} from '../AdditionalCategoryKeys';
-import { App } from '../AppContainer';
-
-jest.mock('../../../../helpers/pages', () => ({
-  addSideBarClass: jest.fn(),
-  addWhitePageClass: jest.fn(),
-  removeSideBarClass: jest.fn(),
-  removeWhitePageClass: jest.fn()
-}));
-
-it('should render default view correctly', async () => {
-  const wrapper = shallowRender();
-
-  expect(addSideBarClass).toBeCalled();
-  expect(addWhitePageClass).toBeCalled();
-
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot();
-  expect(wrapper.find(ScreenPositionHelper).dive()).toMatchSnapshot();
-
-  wrapper.unmount();
-
-  expect(removeSideBarClass).toBeCalled();
-  expect(removeWhitePageClass).toBeCalled();
-});
-
-it('should render newCodePeriod correctly', async () => {
-  const wrapper = shallowRender({
-    location: mockLocation({ query: { category: NEW_CODE_PERIOD_CATEGORY } })
-  });
-
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should render languages correctly', async () => {
-  const wrapper = shallowRender({
-    location: mockLocation({ query: { category: LANGUAGES_CATEGORY } })
-  });
-
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should render analysis scope correctly', async () => {
-  const wrapper = shallowRender({
-    location: mockLocation({ query: { category: ANALYSIS_SCOPE_CATEGORY } })
-  });
-
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should render ALM integration correctly', async () => {
-  const wrapper = shallowRender({
-    location: mockLocation({ query: { category: ALM_INTEGRATION } })
-  });
-
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should render pull request decoration binding correctly', async () => {
-  const wrapper = shallowRender({
-    location: mockLocation({ query: { category: PULL_REQUEST_DECORATION_BINDING_CATEGORY } })
-  });
-
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot();
-});
-
-function shallowRender(props: Partial<App['props']> = {}) {
-  return shallow(
-    <App
-      defaultCategory="general"
-      fetchSettings={jest.fn().mockResolvedValue({})}
-      location={mockLocation()}
-      params={{}}
-      router={mockRouter()}
-      routes={[]}
-      {...props}
-    />
-  );
-}
index 56841809b8b34c064e1c6c3a60b95eb42a98d6f6..06ec031c4fb26391e5076b9cfa428fb3c744f76d 100644 (file)
@@ -22,6 +22,8 @@ import * as React from 'react';
 import { mockSetting } from '../../../../helpers/mocks/settings';
 import { waitAndUpdate } from '../../../../helpers/testUtils';
 import { Definition } from '../Definition';
+import DefinitionActions from '../DefinitionActions';
+import Input from '../inputs/Input';
 
 const setting = mockSetting();
 
@@ -42,7 +44,7 @@ it('should correctly handle change of value', () => {
   const changeValue = jest.fn();
   const checkValue = jest.fn();
   const wrapper = shallowRender({ changeValue, checkValue });
-  wrapper.find('Input').prop<Function>('onChange')(5);
+  wrapper.find(Input).prop<Function>('onChange')(5);
   expect(changeValue).toHaveBeenCalledWith(setting.definition.key, 5);
   expect(checkValue).toHaveBeenCalledWith(setting.definition.key);
 });
@@ -51,7 +53,7 @@ it('should correctly cancel value change', () => {
   const cancelChange = jest.fn();
   const passValidation = jest.fn();
   const wrapper = shallowRender({ cancelChange, passValidation });
-  wrapper.find('Input').prop<Function>('onCancel')();
+  wrapper.find(Input).prop<Function>('onCancel')();
   expect(cancelChange).toHaveBeenCalledWith(setting.definition.key);
   expect(passValidation).toHaveBeenCalledWith(setting.definition.key);
 });
@@ -59,7 +61,7 @@ it('should correctly cancel value change', () => {
 it('should correctly save value change', async () => {
   const saveValue = jest.fn().mockResolvedValue({});
   const wrapper = shallowRender({ changedValue: 10, saveValue });
-  wrapper.find('DefinitionActions').prop<Function>('onSave')();
+  wrapper.find(DefinitionActions).prop('onSave')();
   await waitAndUpdate(wrapper);
   expect(saveValue).toHaveBeenCalledWith(setting.definition.key, undefined);
   expect(wrapper.find('AlertSuccessIcon').exists()).toBe(true);
@@ -72,7 +74,7 @@ it('should correctly reset', async () => {
   const cancelChange = jest.fn();
   const resetValue = jest.fn().mockResolvedValue({});
   const wrapper = shallowRender({ cancelChange, changedValue: 10, resetValue });
-  wrapper.find('DefinitionActions').prop<Function>('onReset')();
+  wrapper.find(DefinitionActions).prop('onReset')();
   await waitAndUpdate(wrapper);
   expect(resetValue).toHaveBeenCalledWith(setting.definition.key, undefined);
   expect(cancelChange).toHaveBeenCalledWith(setting.definition.key);
index 76efb7f237d82abe7e7b2adbe00f0b6bade8d074..cd408abedaed4caf77a27c9fda6666d53cdd0294 100644 (file)
@@ -19,7 +19,7 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { SettingCategoryDefinition } from '../../../../types/settings';
+import { SettingCategoryDefinition, SettingType } from '../../../../types/settings';
 import DefinitionActions from '../DefinitionActions';
 
 const definition: SettingCategoryDefinition = {
@@ -30,11 +30,12 @@ const definition: SettingCategoryDefinition = {
   name: 'foobar',
   options: [],
   subCategory: 'bar',
-  type: 'STRING'
+  type: SettingType.STRING
 };
 
 const settings = {
   key: 'key',
+  hasValue: true,
   definition,
   value: 'baz'
 };
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-test.tsx
new file mode 100644 (file)
index 0000000..71752a0
--- /dev/null
@@ -0,0 +1,120 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import ScreenPositionHelper from '../../../../components/common/ScreenPositionHelper';
+import {
+  addSideBarClass,
+  addWhitePageClass,
+  removeSideBarClass,
+  removeWhitePageClass
+} from '../../../../helpers/pages';
+import { mockLocation, mockRouter } from '../../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+import {
+  ALM_INTEGRATION,
+  ANALYSIS_SCOPE_CATEGORY,
+  LANGUAGES_CATEGORY,
+  NEW_CODE_PERIOD_CATEGORY,
+  PULL_REQUEST_DECORATION_BINDING_CATEGORY
+} from '../AdditionalCategoryKeys';
+import { SettingsApp } from '../SettingsApp';
+
+jest.mock('../../../../helpers/pages', () => ({
+  addSideBarClass: jest.fn(),
+  addWhitePageClass: jest.fn(),
+  removeSideBarClass: jest.fn(),
+  removeWhitePageClass: jest.fn()
+}));
+
+it('should render default view correctly', async () => {
+  const wrapper = shallowRender();
+
+  expect(addSideBarClass).toBeCalled();
+  expect(addWhitePageClass).toBeCalled();
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.find(ScreenPositionHelper).dive()).toMatchSnapshot();
+
+  wrapper.unmount();
+
+  expect(removeSideBarClass).toBeCalled();
+  expect(removeWhitePageClass).toBeCalled();
+});
+
+it('should render newCodePeriod correctly', async () => {
+  const wrapper = shallowRender({
+    location: mockLocation({ query: { category: NEW_CODE_PERIOD_CATEGORY } })
+  });
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render languages correctly', async () => {
+  const wrapper = shallowRender({
+    location: mockLocation({ query: { category: LANGUAGES_CATEGORY } })
+  });
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render analysis scope correctly', async () => {
+  const wrapper = shallowRender({
+    location: mockLocation({ query: { category: ANALYSIS_SCOPE_CATEGORY } })
+  });
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render ALM integration correctly', async () => {
+  const wrapper = shallowRender({
+    location: mockLocation({ query: { category: ALM_INTEGRATION } })
+  });
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render pull request decoration binding correctly', async () => {
+  const wrapper = shallowRender({
+    location: mockLocation({ query: { category: PULL_REQUEST_DECORATION_BINDING_CATEGORY } })
+  });
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<SettingsApp['props']> = {}) {
+  return shallow(
+    <SettingsApp
+      defaultCategory="general"
+      fetchSettings={jest.fn().mockResolvedValue({})}
+      location={mockLocation()}
+      params={{}}
+      router={mockRouter()}
+      routes={[]}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AppContainer-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AppContainer-test.tsx.snap
deleted file mode 100644 (file)
index 03291c1..0000000
+++ /dev/null
@@ -1,263 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render ALM integration correctly 1`] = `
-<div
-  id="settings-page"
->
-  <Suggestions
-    suggestions="settings"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="settings.page"
-  />
-  <PageHeader />
-  <div
-    className="layout-page"
-  >
-    <ScreenPositionHelper
-      className="layout-page-side-outer"
-    >
-      <Component />
-    </ScreenPositionHelper>
-    <div
-      className="layout-page-main"
-    >
-      <div
-        className="layout-page-main-inner"
-      >
-        <div
-          className="big-padded"
-        >
-          <withRouter(Connect(withAppState(AlmIntegration)))
-            selectedCategory="almintegration"
-          />
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render analysis scope correctly 1`] = `
-<div
-  id="settings-page"
->
-  <Suggestions
-    suggestions="settings"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="settings.page"
-  />
-  <PageHeader />
-  <div
-    className="layout-page"
-  >
-    <ScreenPositionHelper
-      className="layout-page-side-outer"
-    >
-      <Component />
-    </ScreenPositionHelper>
-    <div
-      className="layout-page-main"
-    >
-      <div
-        className="layout-page-main-inner"
-      >
-        <div
-          className="big-padded"
-        >
-          <AnalysisScope
-            selectedCategory="exclusions"
-          />
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render default view correctly 1`] = `
-<div
-  id="settings-page"
->
-  <Suggestions
-    suggestions="settings"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="settings.page"
-  />
-  <PageHeader />
-  <div
-    className="layout-page"
-  >
-    <ScreenPositionHelper
-      className="layout-page-side-outer"
-    >
-      <Component />
-    </ScreenPositionHelper>
-    <div
-      className="layout-page-main"
-    >
-      <div
-        className="layout-page-main-inner"
-      >
-        <div
-          className="big-padded"
-        >
-          <Connect(withRouter(SubCategoryDefinitionsList))
-            category="general"
-          />
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render default view correctly 2`] = `
-<div
-  className="layout-page-side-outer"
->
-  <div
-    className="layout-page-side"
-    style={
-      Object {
-        "top": 0,
-      }
-    }
-  >
-    <div
-      className="layout-page-side-inner"
-    >
-      <Connect(CategoriesList)
-        defaultCategory="general"
-        selectedCategory="general"
-      />
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render languages correctly 1`] = `
-<div
-  id="settings-page"
->
-  <Suggestions
-    suggestions="settings"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="settings.page"
-  />
-  <PageHeader />
-  <div
-    className="layout-page"
-  >
-    <ScreenPositionHelper
-      className="layout-page-side-outer"
-    >
-      <Component />
-    </ScreenPositionHelper>
-    <div
-      className="layout-page-main"
-    >
-      <div
-        className="layout-page-main-inner"
-      >
-        <div
-          className="big-padded"
-        >
-          <withRouter(Connect(Languages))
-            selectedCategory="languages"
-          />
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render newCodePeriod correctly 1`] = `
-<div
-  id="settings-page"
->
-  <Suggestions
-    suggestions="settings"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="settings.page"
-  />
-  <PageHeader />
-  <div
-    className="layout-page"
-  >
-    <ScreenPositionHelper
-      className="layout-page-side-outer"
-    >
-      <Component />
-    </ScreenPositionHelper>
-    <div
-      className="layout-page-main"
-    >
-      <div
-        className="layout-page-main-inner"
-      >
-        <div
-          className="big-padded"
-        >
-          <NewCodePeriod />
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render pull request decoration binding correctly 1`] = `
-<div
-  id="settings-page"
->
-  <Suggestions
-    suggestions="settings"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    title="settings.page"
-  />
-  <PageHeader />
-  <div
-    className="layout-page"
-  >
-    <ScreenPositionHelper
-      className="layout-page-side-outer"
-    >
-      <Component />
-    </ScreenPositionHelper>
-    <div
-      className="layout-page-main"
-    >
-      <div
-        className="layout-page-main-inner"
-      >
-        <div
-          className="big-padded"
-        >
-          <Connect(withRouter(SubCategoryDefinitionsList))
-            category="pull_request_decoration_binding"
-          />
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
-`;
index 1e27f6b5fa941a4fcbb86431a924ff1b6c034bfa..c7425dafd23e6d0bf10450a70f85940d028ba36b 100644 (file)
@@ -34,50 +34,54 @@ exports[`should render correctly 1`] = `
     <div
       className="settings-definition-state"
     />
-    <Input
-      hasValueChanged={false}
-      onCancel={[Function]}
-      onChange={[Function]}
-      onSave={[Function]}
-      setting={
-        Object {
-          "definition": Object {
-            "description": "When Foo then Bar",
+    <form>
+      <Input
+        hasValueChanged={false}
+        onCancel={[Function]}
+        onChange={[Function]}
+        onSave={[Function]}
+        setting={
+          Object {
+            "definition": Object {
+              "description": "When Foo then Bar",
+              "key": "foo",
+              "name": "Foo setting",
+              "options": Array [],
+              "type": "INTEGER",
+            },
+            "hasValue": true,
+            "inherited": true,
             "key": "foo",
-            "name": "Foo setting",
-            "options": Array [],
-            "type": "INTEGER",
-          },
-          "inherited": true,
-          "key": "foo",
-          "value": "42",
+            "value": "42",
+          }
         }
-      }
-      value="42"
-    />
-    <DefinitionActions
-      changedValue={null}
-      hasError={false}
-      hasValueChanged={false}
-      isDefault={true}
-      onCancel={[Function]}
-      onReset={[Function]}
-      onSave={[Function]}
-      setting={
-        Object {
-          "definition": Object {
-            "description": "When Foo then Bar",
+        value="42"
+      />
+      <DefinitionActions
+        changedValue={null}
+        hasError={false}
+        hasValueChanged={false}
+        isDefault={true}
+        onCancel={[Function]}
+        onReset={[Function]}
+        onSave={[Function]}
+        setting={
+          Object {
+            "definition": Object {
+              "description": "When Foo then Bar",
+              "key": "foo",
+              "name": "Foo setting",
+              "options": Array [],
+              "type": "INTEGER",
+            },
+            "hasValue": true,
+            "inherited": true,
             "key": "foo",
-            "name": "Foo setting",
-            "options": Array [],
-            "type": "INTEGER",
-          },
-          "inherited": true,
-          "key": "foo",
-          "value": "42",
+            "value": "42",
+          }
         }
-      }
-    />
+      />
+    </form>
   </div>
 </div>
 `;
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsApp-test.tsx.snap
new file mode 100644 (file)
index 0000000..03291c1
--- /dev/null
@@ -0,0 +1,263 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render ALM integration correctly 1`] = `
+<div
+  id="settings-page"
+>
+  <Suggestions
+    suggestions="settings"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="settings.page"
+  />
+  <PageHeader />
+  <div
+    className="layout-page"
+  >
+    <ScreenPositionHelper
+      className="layout-page-side-outer"
+    >
+      <Component />
+    </ScreenPositionHelper>
+    <div
+      className="layout-page-main"
+    >
+      <div
+        className="layout-page-main-inner"
+      >
+        <div
+          className="big-padded"
+        >
+          <withRouter(Connect(withAppState(AlmIntegration)))
+            selectedCategory="almintegration"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render analysis scope correctly 1`] = `
+<div
+  id="settings-page"
+>
+  <Suggestions
+    suggestions="settings"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="settings.page"
+  />
+  <PageHeader />
+  <div
+    className="layout-page"
+  >
+    <ScreenPositionHelper
+      className="layout-page-side-outer"
+    >
+      <Component />
+    </ScreenPositionHelper>
+    <div
+      className="layout-page-main"
+    >
+      <div
+        className="layout-page-main-inner"
+      >
+        <div
+          className="big-padded"
+        >
+          <AnalysisScope
+            selectedCategory="exclusions"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render default view correctly 1`] = `
+<div
+  id="settings-page"
+>
+  <Suggestions
+    suggestions="settings"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="settings.page"
+  />
+  <PageHeader />
+  <div
+    className="layout-page"
+  >
+    <ScreenPositionHelper
+      className="layout-page-side-outer"
+    >
+      <Component />
+    </ScreenPositionHelper>
+    <div
+      className="layout-page-main"
+    >
+      <div
+        className="layout-page-main-inner"
+      >
+        <div
+          className="big-padded"
+        >
+          <Connect(withRouter(SubCategoryDefinitionsList))
+            category="general"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render default view correctly 2`] = `
+<div
+  className="layout-page-side-outer"
+>
+  <div
+    className="layout-page-side"
+    style={
+      Object {
+        "top": 0,
+      }
+    }
+  >
+    <div
+      className="layout-page-side-inner"
+    >
+      <Connect(CategoriesList)
+        defaultCategory="general"
+        selectedCategory="general"
+      />
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render languages correctly 1`] = `
+<div
+  id="settings-page"
+>
+  <Suggestions
+    suggestions="settings"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="settings.page"
+  />
+  <PageHeader />
+  <div
+    className="layout-page"
+  >
+    <ScreenPositionHelper
+      className="layout-page-side-outer"
+    >
+      <Component />
+    </ScreenPositionHelper>
+    <div
+      className="layout-page-main"
+    >
+      <div
+        className="layout-page-main-inner"
+      >
+        <div
+          className="big-padded"
+        >
+          <withRouter(Connect(Languages))
+            selectedCategory="languages"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render newCodePeriod correctly 1`] = `
+<div
+  id="settings-page"
+>
+  <Suggestions
+    suggestions="settings"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="settings.page"
+  />
+  <PageHeader />
+  <div
+    className="layout-page"
+  >
+    <ScreenPositionHelper
+      className="layout-page-side-outer"
+    >
+      <Component />
+    </ScreenPositionHelper>
+    <div
+      className="layout-page-main"
+    >
+      <div
+        className="layout-page-main-inner"
+      >
+        <div
+          className="big-padded"
+        >
+          <NewCodePeriod />
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render pull request decoration binding correctly 1`] = `
+<div
+  id="settings-page"
+>
+  <Suggestions
+    suggestions="settings"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    title="settings.page"
+  />
+  <PageHeader />
+  <div
+    className="layout-page"
+  >
+    <ScreenPositionHelper
+      className="layout-page-side-outer"
+    >
+      <Component />
+    </ScreenPositionHelper>
+    <div
+      className="layout-page-main"
+    >
+      <div
+        className="layout-page-main-inner"
+      >
+        <div
+          className="big-padded"
+        >
+          <Connect(withRouter(SubCategoryDefinitionsList))
+            category="pull_request_decoration_binding"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
index cc29119b1e9fda85c6fb8662da0186707c113cd2..c0449cc8a04cacf8411750db006643b922953617 100644 (file)
@@ -28,6 +28,7 @@ exports[`should render correctly 1`] = `
               "subCategory": "email",
               "type": "INTEGER",
             },
+            "hasValue": true,
             "inherited": true,
             "key": "foo",
             "value": "42",
@@ -59,6 +60,7 @@ exports[`should render correctly 1`] = `
               "options": Array [],
               "subCategory": "qg",
             },
+            "hasValue": true,
             "inherited": true,
             "key": "foo",
             "value": "42",
@@ -96,6 +98,7 @@ exports[`should render correctly: subcategory 1`] = `
               "options": Array [],
               "subCategory": "qg",
             },
+            "hasValue": true,
             "inherited": true,
             "key": "foo",
             "value": "42",
index 32e18f71f198d49b44e034d515ea90740b5ad1b7..953c4212556c9b3bdb709101fd5a642d8d1fd7bb 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { DefaultInputProps, isCategoryDefinition } from '../../utils';
+import { SettingType } from '../../../../types/settings';
+import {
+  DefaultInputProps,
+  DefaultSpecializedInputProps,
+  getUniqueName,
+  isCategoryDefinition,
+  isDefaultOrInherited,
+  isSecuredDefinition
+} from '../../utils';
+import InputForSecured from './InputForSecured';
 import MultiValueInput from './MultiValueInput';
 import PrimitiveInput from './PrimitiveInput';
 import PropertySetInput from './PropertySetInput';
 
 export default function Input(props: DefaultInputProps) {
-  const { definition } = props.setting;
+  const { setting } = props;
+  const { definition } = setting;
+  const name = getUniqueName(definition);
+
+  let Input: React.ComponentType<DefaultSpecializedInputProps> = PrimitiveInput;
 
   if (isCategoryDefinition(definition) && definition.multiValues) {
-    return <MultiValueInput {...props} />;
+    Input = MultiValueInput;
+  }
+
+  if (definition.type === SettingType.PROPERTY_SET) {
+    Input = PropertySetInput;
   }
 
-  if (definition.type === 'PROPERTY_SET') {
-    return <PropertySetInput {...props} />;
+  if (isSecuredDefinition(definition)) {
+    return <InputForSecured input={Input} {...props} />;
   }
 
-  return <PrimitiveInput {...props} />;
+  return <Input {...props} name={name} isDefault={isDefaultOrInherited(setting)} />;
 }
index 2c5818efcddf8ae01b7f1ec97f31ce077f85521a..b7be3d7573cb4d3b02060fd8309e9e14b7acc518 100644 (file)
@@ -22,5 +22,5 @@ import { DefaultSpecializedInputProps } from '../../utils';
 import SimpleInput from './SimpleInput';
 
 export default function InputForNumber(props: DefaultSpecializedInputProps) {
-  return <SimpleInput {...props} className="input-small" type="text" />;
+  return <SimpleInput className="input-small" type="text" {...props} />;
 }
index c63b3106ea47095d73136afee589fcee9223e714..b93e4ac2384a5777dd7d5ad2d353bc1eb6699949 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { colors } from '../../../../app/theme';
-import { Button } from '../../../../components/controls/buttons';
-import LockIcon from '../../../../components/icons/LockIcon';
-import { translate } from '../../../../helpers/l10n';
 import { DefaultSpecializedInputProps } from '../../utils';
+import SimpleInput from './SimpleInput';
 
-interface State {
-  changing: boolean;
-}
-
-export default class InputForPassword extends React.PureComponent<
-  DefaultSpecializedInputProps,
-  State
-> {
-  state: State = {
-    changing: !this.props.value
-  };
-
-  componentWillReceiveProps(nextProps: DefaultSpecializedInputProps) {
-    /*
-     * Reset `changing` if:
-     *  - the value is reset (valueChanged -> !valueChanged)
-     *     or
-     *  - the value changes from outside the input (i.e. store update/reset/cancel)
-     */
-    if (
-      (this.props.hasValueChanged || this.props.value !== nextProps.value) &&
-      !nextProps.hasValueChanged
-    ) {
-      this.setState({ changing: !nextProps.value });
-    }
-  }
-
-  handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-    this.props.onChange(event.target.value);
-  };
-
-  handleChangeClick = () => {
-    this.setState({ changing: true });
-  };
-
-  renderInput() {
-    return (
-      <form>
-        <input className="hidden" type="password" />
-        <input
-          autoComplete="off"
-          autoFocus={this.state.changing && this.props.value}
-          className="js-password-input settings-large-input text-top"
-          name={this.props.name}
-          onChange={this.handleInputChange}
-          type="password"
-          value={this.props.value}
-        />
-      </form>
-    );
-  }
-
-  render() {
-    if (this.state.changing) {
-      return this.renderInput();
-    }
-
-    return (
-      <>
-        <LockIcon className="text-middle big-spacer-right" fill={colors.gray60} />
-        <Button className="text-middle" onClick={this.handleChangeClick}>
-          {translate('change_verb')}
-        </Button>
-      </>
-    );
-  }
+export default function InputForPassword(props: DefaultSpecializedInputProps) {
+  return (
+    <SimpleInput {...props} className="settings-large-input" type="password" autoComplete="off" />
+  );
 }
diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSecured.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSecured.tsx
new file mode 100644 (file)
index 0000000..eddff15
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 * as React from 'react';
+import { colors } from '../../../../app/theme';
+import { Button } from '../../../../components/controls/buttons';
+import LockIcon from '../../../../components/icons/LockIcon';
+import { translate } from '../../../../helpers/l10n';
+import {
+  DefaultInputProps,
+  DefaultSpecializedInputProps,
+  getUniqueName,
+  isDefaultOrInherited
+} from '../../utils';
+
+interface State {
+  changing: boolean;
+}
+
+interface Props extends DefaultInputProps {
+  input: React.ComponentType<DefaultSpecializedInputProps>;
+}
+
+export default class InputForSecured extends React.PureComponent<Props, State> {
+  state: State = {
+    changing: !this.props.setting.hasValue
+  };
+
+  componentWillReceiveProps(nextProps: Props) {
+    /*
+     * Reset `changing` if:
+     *  - the value is reset (valueChanged -> !valueChanged)
+     *     or
+     *  - the value changes from outside the input (i.e. store update/reset/cancel)
+     */
+    if (
+      (this.props.hasValueChanged || this.props.setting !== nextProps.setting) &&
+      !nextProps.hasValueChanged
+    ) {
+      this.setState({ changing: !nextProps.setting.hasValue });
+    }
+  }
+
+  handleInputChange = (value: string) => {
+    this.props.onChange(value);
+  };
+
+  handleChangeClick = () => {
+    this.setState({ changing: true });
+  };
+
+  renderInput() {
+    const { input: Input, setting, value } = this.props;
+    const name = getUniqueName(setting.definition);
+    return (
+      // The input hidden will prevent browser asking for saving login information
+      <>
+        <input className="hidden" type="password" />
+        <Input
+          autoComplete="off"
+          className="js-setting-input settings-large-input"
+          isDefault={isDefaultOrInherited(setting)}
+          name={name}
+          onChange={this.handleInputChange}
+          setting={setting}
+          type="password"
+          value={value}
+        />
+      </>
+    );
+  }
+
+  render() {
+    if (this.state.changing) {
+      return this.renderInput();
+    }
+
+    return (
+      <>
+        <LockIcon className="text-middle big-spacer-right" fill={colors.gray60} />
+        <Button className="text-middle" onClick={this.handleChangeClick}>
+          {translate('change_verb')}
+        </Button>
+      </>
+    );
+  }
+}
index 32ab643f6717f454c9c0c7ce252b138c480fcb4f..0084038c5de4a556554be3005c6ac812af3ec178 100644 (file)
@@ -22,5 +22,5 @@ import { DefaultSpecializedInputProps } from '../../utils';
 import SimpleInput from './SimpleInput';
 
 export default function InputForString(props: DefaultSpecializedInputProps) {
-  return <SimpleInput {...props} className="settings-large-input" type="text" />;
+  return <SimpleInput className="settings-large-input" type="text" {...props} />;
 }
index e6bb87e44ae308aa082d98948fdd9460eba37742..6a40749310db66e31ef974300a48dcea30c218e1 100644 (file)
  */
 import * as React from 'react';
 import { DeleteButton } from '../../../../components/controls/buttons';
-import { DefaultInputProps, getEmptyValue } from '../../utils';
+import { DefaultSpecializedInputProps, getEmptyValue } from '../../utils';
 import PrimitiveInput from './PrimitiveInput';
 
-export default class MultiValueInput extends React.PureComponent<DefaultInputProps> {
+export default class MultiValueInput extends React.PureComponent<DefaultSpecializedInputProps> {
   ensureValue = () => {
     return this.props.value || [];
   };
@@ -40,17 +40,15 @@ export default class MultiValueInput extends React.PureComponent<DefaultInputPro
   };
 
   renderInput(value: any, index: number, isLast: boolean) {
-    const { setting } = this.props;
+    const { setting, isDefault, name } = this.props;
     return (
       <li className="spacer-bottom" key={index}>
         <PrimitiveInput
+          isDefault={isDefault}
+          name={name}
           hasValueChanged={this.props.hasValueChanged}
           onChange={value => this.handleSingleInputChange(index, value)}
-          setting={{
-            ...setting,
-            definition: { ...setting.definition, multiValues: false },
-            values: undefined
-          }}
+          setting={setting}
           value={value}
         />
 
index 59a221e3bc7f1bdeb3ff82f370307cd348a0b48b..e3b94af38e8fd42f0646d769eb5c759828a9f99c 100644 (file)
  */
 import * as React from 'react';
 import { SettingType } from '../../../../types/settings';
-import {
-  DefaultInputProps,
-  DefaultSpecializedInputProps,
-  getUniqueName,
-  isDefaultOrInherited
-} from '../../utils';
+import { DefaultSpecializedInputProps } from '../../utils';
 import InputForBoolean from './InputForBoolean';
 import InputForJSON from './InputForJSON';
 import InputForNumber from './InputForNumber';
@@ -33,42 +28,30 @@ import InputForSingleSelectList from './InputForSingleSelectList';
 import InputForString from './InputForString';
 import InputForText from './InputForText';
 
-const typeMapping: {
-  [type in SettingType]?: React.ComponentType<DefaultSpecializedInputProps>;
-} = {
-  STRING: InputForString,
-  TEXT: InputForText,
-  JSON: InputForJSON,
-  PASSWORD: InputForPassword,
-  BOOLEAN: InputForBoolean,
-  INTEGER: InputForNumber,
-  LONG: InputForNumber,
-  FLOAT: InputForNumber
-};
-
-interface Props extends DefaultInputProps {
-  name?: string;
+function withOptions(options: string[]): React.ComponentType<DefaultSpecializedInputProps> {
+  return function Wrapped(props: DefaultSpecializedInputProps) {
+    return <InputForSingleSelectList options={options} {...props} />;
+  };
 }
 
-export default class PrimitiveInput extends React.PureComponent<Props> {
-  render() {
-    const { setting, ...other } = this.props;
-    const { definition } = setting;
-
-    const name = this.props.name || getUniqueName(definition);
+export default function PrimitiveInput(props: DefaultSpecializedInputProps) {
+  const { setting, name, isDefault, ...other } = props;
+  const { definition } = setting;
+  const typeMapping: {
+    [type in SettingType]?: React.ComponentType<DefaultSpecializedInputProps>;
+  } = {
+    STRING: InputForString,
+    TEXT: InputForText,
+    JSON: InputForJSON,
+    PASSWORD: InputForPassword,
+    BOOLEAN: InputForBoolean,
+    INTEGER: InputForNumber,
+    LONG: InputForNumber,
+    FLOAT: InputForNumber,
+    SINGLE_SELECT_LIST: withOptions(definition.options)
+  };
 
-    if (definition.type === 'SINGLE_SELECT_LIST') {
-      return (
-        <InputForSingleSelectList
-          isDefault={isDefaultOrInherited(setting)}
-          name={name}
-          options={definition.options}
-          {...other}
-        />
-      );
-    }
+  const InputComponent = (definition.type && typeMapping[definition.type]) || InputForString;
 
-    const InputComponent = (definition.type && typeMapping[definition.type]) || InputForString;
-    return <InputComponent isDefault={isDefaultOrInherited(setting)} name={name} {...other} />;
-  }
+  return <InputComponent isDefault={isDefault} name={name} setting={setting} {...other} />;
 }
index 7db52e8b28999cbe52c5c7c58220d3a91b844f18..2b9efd2e7d9b1bd95e96ca3ced1750dc329470e0 100644 (file)
  */
 import * as React from 'react';
 import { DeleteButton } from '../../../../components/controls/buttons';
-import { DefaultInputProps, getEmptyValue, getUniqueName, isCategoryDefinition } from '../../utils';
+import {
+  DefaultSpecializedInputProps,
+  getEmptyValue,
+  getUniqueName,
+  isCategoryDefinition
+} from '../../utils';
 import PrimitiveInput from './PrimitiveInput';
 
-export default class PropertySetInput extends React.PureComponent<DefaultInputProps> {
+export default class PropertySetInput extends React.PureComponent<DefaultSpecializedInputProps> {
   ensureValue() {
     return this.props.value || [];
   }
@@ -42,23 +47,31 @@ export default class PropertySetInput extends React.PureComponent<DefaultInputPr
   };
 
   renderFields(fieldValues: any, index: number, isLast: boolean) {
-    const { setting } = this.props;
+    const { setting, isDefault } = this.props;
     const { definition } = setting;
 
     return (
       <tr key={index}>
         {isCategoryDefinition(definition) &&
-          definition.fields.map(field => (
-            <td key={field.key}>
-              <PrimitiveInput
-                hasValueChanged={this.props.hasValueChanged}
-                name={getUniqueName(definition, field.key)}
-                onChange={value => this.handleInputChange(index, field.key, value)}
-                setting={{ ...setting, definition: field, value: fieldValues[field.key] }}
-                value={fieldValues[field.key]}
-              />
-            </td>
-          ))}
+          definition.fields.map(field => {
+            const newSetting = {
+              ...setting,
+              definition: field,
+              value: fieldValues[field.key]
+            };
+            return (
+              <td key={field.key}>
+                <PrimitiveInput
+                  isDefault={isDefault}
+                  hasValueChanged={this.props.hasValueChanged}
+                  name={getUniqueName(definition, field.key)}
+                  onChange={value => this.handleInputChange(index, field.key, value)}
+                  setting={newSetting}
+                  value={fieldValues[field.key]}
+                />
+              </td>
+            );
+          })}
         <td className="thin nowrap text-middle">
           {!isLast && (
             <DeleteButton
index 20c1d0bad81bb3a1dbfd7992cbcec1a9225ea416..2c5df6a506fd044ae3558529a1f70b93180dc783 100644 (file)
@@ -22,8 +22,6 @@ import * as React from 'react';
 import { DefaultSpecializedInputProps } from '../../utils';
 
 interface Props extends DefaultSpecializedInputProps {
-  className?: string;
-  type: string;
   value: string | number;
 }
 
@@ -41,14 +39,17 @@ export default class SimpleInput extends React.PureComponent<Props> {
   };
 
   render() {
+    const { autoComplete, autoFocus, className, name, value = '', type } = this.props;
     return (
       <input
-        className={classNames('text-top', this.props.className)}
-        name={this.props.name}
+        autoComplete={autoComplete}
+        autoFocus={autoFocus}
+        className={classNames('text-top', className)}
+        name={name}
         onChange={this.handleInputChange}
         onKeyDown={this.handleKeyDown}
-        type={this.props.type}
-        value={this.props.value || ''}
+        type={type}
+        value={value}
       />
     );
   }
index c800f03dee07346a29aa2939211410c1f4066fdb..4e5aac8760bef1de93d5eee72c68f2b000cade7b 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { Setting, SettingCategoryDefinition } from '../../../../../types/settings';
+import { mockDefinition, mockSetting } from '../../../../../helpers/mocks/settings';
+import { Setting, SettingType } from '../../../../../types/settings';
 import { DefaultInputProps } from '../../../utils';
 import Input from '../Input';
-
-const settingValue = {
-  key: 'example'
-};
-
-const settingDefinition: SettingCategoryDefinition = {
-  category: 'general',
-  fields: [],
-  key: 'example',
-  options: [],
-  subCategory: 'Branches',
-  type: 'STRING'
-};
+import InputForSecured from '../InputForSecured';
+import MultiValueInput from '../MultiValueInput';
+import PrimitiveInput from '../PrimitiveInput';
+import PropertySetInput from '../PropertySetInput';
 
 it('should render PrimitiveInput', () => {
-  const setting = { ...settingValue, definition: settingDefinition };
   const onChange = jest.fn();
-  const input = shallowRender({ onChange, setting }).find('PrimitiveInput');
+  const input = shallowRender({ onChange }).find(PrimitiveInput);
+  expect(input.length).toBe(1);
+  expect(input.prop('value')).toBe('foo');
+  expect(input.prop('onChange')).toBe(onChange);
+});
+
+it('should render Secured input', () => {
+  const setting: Setting = mockSetting({
+    key: 'foo.secured',
+    definition: mockDefinition({ key: 'foo.secured', type: SettingType.PROPERTY_SET })
+  });
+  const onChange = jest.fn();
+  const input = shallowRender({ onChange, setting }).find(InputForSecured);
   expect(input.length).toBe(1);
-  expect(input.prop('setting')).toBe(setting);
   expect(input.prop('value')).toBe('foo');
   expect(input.prop('onChange')).toBe(onChange);
 });
 
 it('should render MultiValueInput', () => {
-  const setting = { ...settingValue, definition: { ...settingDefinition, multiValues: true } };
+  const setting = mockSetting({
+    definition: mockDefinition({ multiValues: true })
+  });
   const onChange = jest.fn();
   const value = ['foo', 'bar'];
-  const input = shallowRender({ onChange, setting, value }).find('MultiValueInput');
+  const input = shallowRender({ onChange, setting, value }).find(MultiValueInput);
   expect(input.length).toBe(1);
   expect(input.prop('setting')).toBe(setting);
   expect(input.prop('value')).toBe(value);
@@ -58,14 +62,13 @@ it('should render MultiValueInput', () => {
 });
 
 it('should render PropertySetInput', () => {
-  const setting: Setting = {
-    ...settingValue,
-    definition: { ...settingDefinition, type: 'PROPERTY_SET' }
-  };
+  const setting: Setting = mockSetting({
+    definition: mockDefinition({ type: SettingType.PROPERTY_SET })
+  });
 
   const onChange = jest.fn();
   const value = [{ foo: 'bar' }];
-  const input = shallowRender({ onChange, setting, value }).find('PropertySetInput');
+  const input = shallowRender({ onChange, setting, value }).find(PropertySetInput);
   expect(input.length).toBe(1);
   expect(input.prop('setting')).toBe(setting);
   expect(input.prop('value')).toBe(value);
@@ -73,12 +76,5 @@ it('should render PropertySetInput', () => {
 });
 
 function shallowRender(props: Partial<DefaultInputProps> = {}) {
-  return shallow(
-    <Input
-      onChange={jest.fn()}
-      setting={{ ...settingValue, definition: settingDefinition }}
-      value="foo"
-      {...props}
-    />
-  );
+  return shallow(<Input onChange={jest.fn()} setting={mockSetting()} value="foo" {...props} />);
 }
index 31685630cc7194541791bfb3112883ac1290d21f..65a8da2f87d617100d112ffb36a80bafdf82aa60 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import { mockSetting } from '../../../../../helpers/mocks/settings';
 import { DefaultSpecializedInputProps } from '../../../utils';
 import InputForBoolean from '../InputForBoolean';
 
@@ -57,6 +58,13 @@ it('should call onChange', () => {
 
 function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) {
   return shallow(
-    <InputForBoolean isDefault={false} name="foo" onChange={jest.fn()} value={true} {...props} />
+    <InputForBoolean
+      isDefault={false}
+      name="foo"
+      onChange={jest.fn()}
+      setting={mockSetting()}
+      value={true}
+      {...props}
+    />
   );
 }
index 2884097b5371f8d59562eb2dd3fac1e4cb85d961..f91b74b8716fba99a19a2ee56027920137046e23 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import { mockSetting } from '../../../../../helpers/mocks/settings';
 import { change } from '../../../../../helpers/testUtils';
 import { DefaultSpecializedInputProps } from '../../../utils';
 import InputForJSON from '../InputForJSON';
@@ -67,6 +68,13 @@ it('should handle ignore formatting if empty', () => {
 
 function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) {
   return shallow<InputForJSON>(
-    <InputForJSON isDefault={false} name="foo" onChange={jest.fn()} value="" {...props} />
+    <InputForJSON
+      isDefault={false}
+      name="foo"
+      onChange={jest.fn()}
+      setting={mockSetting()}
+      value=""
+      {...props}
+    />
   );
 }
index ca1682699e2dc94131132486961bce20412a7429..ec65800f0210384910a5b6954f6a1dc04f08c52c 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import { mockSetting } from '../../../../../helpers/mocks/settings';
 import InputForNumber from '../InputForNumber';
 import SimpleInput from '../SimpleInput';
 
 it('should render SimpleInput', () => {
   const onChange = jest.fn();
   const simpleInput = shallow(
-    <InputForNumber isDefault={false} name="foo" onChange={onChange} value={17} />
+    <InputForNumber
+      isDefault={false}
+      name="foo"
+      onChange={onChange}
+      setting={mockSetting()}
+      value={17}
+    />
   ).find(SimpleInput);
   expect(simpleInput.length).toBe(1);
   expect(simpleInput.prop('name')).toBe('foo');
index bb1716d98791f1b384d92efa74939342ab5460d8..216332ca6a271dbed036cd89256ef8a6782a475a 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { change, click, submit } from '../../../../../helpers/testUtils';
-import { DefaultSpecializedInputProps } from '../../../utils';
+import { mockSetting } from '../../../../../helpers/mocks/settings';
 import InputForPassword from '../InputForPassword';
+import SimpleInput from '../SimpleInput';
 
-it('should render lock icon, but no form', () => {
+it('should render SimpleInput', () => {
   const onChange = jest.fn();
-  const input = shallowRender({ onChange });
-
-  expect(input.find('LockIcon').length).toBe(1);
-  expect(input.find('form').length).toBe(0);
-});
-
-it('should open form', () => {
-  const onChange = jest.fn();
-  const input = shallowRender({ onChange });
-  const button = input.find('Button');
-  expect(button.length).toBe(1);
-
-  click(button);
-  expect(input.find('form').length).toBe(1);
-});
-
-it('should set value', () => {
-  const onChange = jest.fn(() => Promise.resolve());
-  const input = shallowRender({ onChange });
-
-  click(input.find('Button'));
-  change(input.find('.js-password-input'), 'secret');
-  submit(input.find('form'));
-  expect(onChange).toBeCalledWith('secret');
-});
-
-it('should show form when empty, and enable handle typing', () => {
-  const input = shallowRender({ value: '' });
-  const onChange = (value: string) => input.setProps({ hasValueChanged: true, value });
-  input.setProps({ onChange });
-
-  expect(input.find('form').length).toBe(1);
-  change(input.find('input.js-password-input'), 'hello');
-  expect(input.find('form').length).toBe(1);
-  expect(input.find('input.js-password-input').prop('value')).toBe('hello');
-});
-
-it('should handle value reset', () => {
-  const input = shallowRender({ hasValueChanged: true, value: 'whatever' });
-  input.setState({ changing: true });
-
-  // reset
-  input.setProps({ hasValueChanged: false, value: 'original' });
-
-  expect(input.state('changing')).toBe(false);
-});
-
-it('should handle value reset to empty', () => {
-  const input = shallowRender({ hasValueChanged: true, value: 'whatever' });
-  input.setState({ changing: true });
-
-  // outside change
-  input.setProps({ hasValueChanged: false, value: '' });
-
-  expect(input.state('changing')).toBe(true);
-});
-
-function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) {
-  return shallow<InputForPassword>(
+  const simpleInput = shallow(
     <InputForPassword
-      hasValueChanged={false}
       isDefault={false}
       name="foo"
-      onChange={jest.fn()}
+      onChange={onChange}
+      setting={mockSetting()}
       value="bar"
-      {...props}
     />
-  );
-}
+  ).find(SimpleInput);
+  expect(simpleInput.length).toBe(1);
+  expect(simpleInput.prop('name')).toBe('foo');
+  expect(simpleInput.prop('value')).toBe('bar');
+  expect(simpleInput.prop('type')).toBe('password');
+  expect(simpleInput.prop('onChange')).toBeDefined();
+});
diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSecured-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSecured-test.tsx
new file mode 100644 (file)
index 0000000..1d9eeb3
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { mockSetting } from '../../../../../helpers/mocks/settings';
+import { change, click } from '../../../../../helpers/testUtils';
+import InputForSecured from '../InputForSecured';
+import InputForString from '../InputForString';
+
+it('should render lock icon, but no form', () => {
+  const onChange = jest.fn();
+  const input = shallowRender({ onChange });
+
+  expect(input.find('LockIcon').length).toBe(1);
+  expect(input.find('input').length).toBe(0);
+});
+
+it('should open form', () => {
+  const onChange = jest.fn();
+  const input = shallowRender({ onChange });
+  const button = input.find('Button');
+  expect(button.length).toBe(1);
+
+  click(button);
+  expect(input.find('input').length).toBe(1);
+});
+
+it('should set value', () => {
+  const onChange = jest.fn(() => Promise.resolve());
+  const input = shallowRender({ onChange });
+
+  click(input.find('Button'));
+  change(input.find(InputForString), 'secret');
+  expect(onChange).toBeCalledWith('secret');
+});
+
+it('should show input when empty, and enable handle typing', () => {
+  const input = shallowRender({ setting: mockSetting({ hasValue: false }) });
+  const onChange = (value: string) => input.setProps({ hasValueChanged: true, value });
+  input.setProps({ onChange });
+
+  expect(input.find('input').length).toBe(1);
+  change(input.find(InputForString), 'hello');
+  expect(input.find('input').length).toBe(1);
+  expect(input.find(InputForString).prop('value')).toBe('hello');
+});
+
+it('should handle value reset', () => {
+  const input = shallowRender({ hasValueChanged: true, value: 'whatever' });
+  input.setState({ changing: true });
+
+  // reset
+  input.setProps({ hasValueChanged: false, value: 'original' });
+
+  expect(input.state('changing')).toBe(false);
+});
+
+it('should handle value reset to empty', () => {
+  const input = shallowRender({ hasValueChanged: true, value: 'whatever' });
+  input.setState({ changing: true });
+
+  // outside change
+  input.setProps({ hasValueChanged: false, setting: mockSetting({ hasValue: false }) });
+
+  expect(input.state('changing')).toBe(true);
+});
+
+function shallowRender(props: Partial<InputForSecured['props']> = {}) {
+  return shallow<InputForSecured>(
+    <InputForSecured
+      input={InputForString}
+      hasValueChanged={false}
+      onChange={jest.fn()}
+      setting={mockSetting()}
+      value="bar"
+      {...props}
+    />
+  );
+}
index 8dde11bfbc0ef2bc3417d7cd23c9f0ea2b5f53d0..00d5fcb467ae6496ad33188f5febd1721f158f7c 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import { mockSetting } from '../../../../../helpers/mocks/settings';
 import { DefaultSpecializedInputProps } from '../../../utils';
 import InputForSingleSelectList from '../InputForSingleSelectList';
 
@@ -53,6 +54,7 @@ function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) {
       name="foo"
       onChange={jest.fn()}
       options={['foo', 'bar', 'baz']}
+      setting={mockSetting()}
       value="bar"
       {...props}
     />
index 154d4dcd3d415312eb3d2c601710f0faffb8e10b..65e5235c4bef91c7a5f7743c8ea86ab533d9f9e4 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import { mockSetting } from '../../../../../helpers/mocks/settings';
 import InputForString from '../InputForString';
 import SimpleInput from '../SimpleInput';
 
 it('should render SimpleInput', () => {
   const onChange = jest.fn();
   const simpleInput = shallow(
-    <InputForString isDefault={false} name="foo" onChange={onChange} value="bar" />
+    <InputForString
+      isDefault={false}
+      name="foo"
+      onChange={onChange}
+      setting={mockSetting()}
+      value="bar"
+    />
   ).find(SimpleInput);
   expect(simpleInput.length).toBe(1);
   expect(simpleInput.prop('name')).toBe('foo');
index 4b9a42653fbaac307ccf30d4c6fa4aef7754a651..1d12b2d64a0f37530346240f322018028ff214eb 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import { mockSetting } from '../../../../../helpers/mocks/settings';
 import { change } from '../../../../../helpers/testUtils';
 import { DefaultSpecializedInputProps } from '../../../utils';
 import InputForText from '../InputForText';
@@ -44,6 +45,13 @@ it('should call onChange', () => {
 
 function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) {
   return shallow(
-    <InputForText isDefault={false} name="foo" onChange={jest.fn()} value="bar" {...props} />
+    <InputForText
+      isDefault={false}
+      name="foo"
+      onChange={jest.fn()}
+      value="bar"
+      {...props}
+      setting={mockSetting()}
+    />
   );
 }
index 7f5dd70dc64525f1b61e80a18b4bfb5ee395032b..483dc0c30b74aaef2eb4cad9fc0a3ad99cdb78f9 100644 (file)
 import { shallow, ShallowWrapper } from 'enzyme';
 import * as React from 'react';
 import { click } from '../../../../../helpers/testUtils';
-import { SettingCategoryDefinition } from '../../../../../types/settings';
-import { DefaultInputProps } from '../../../utils';
+import { SettingCategoryDefinition, SettingType } from '../../../../../types/settings';
+import { DefaultSpecializedInputProps } from '../../../utils';
 import MultiValueInput from '../MultiValueInput';
 import PrimitiveInput from '../PrimitiveInput';
 
 const settingValue = {
-  key: 'example'
+  key: 'example',
+  hasValue: true
 };
 
 const settingDefinition: SettingCategoryDefinition = {
@@ -36,7 +37,7 @@ const settingDefinition: SettingCategoryDefinition = {
   multiValues: true,
   options: [],
   subCategory: 'Branches',
-  type: 'STRING'
+  type: SettingType.STRING
 };
 
 const assertValues = (inputs: ShallowWrapper<any>, values: string[]) => {
@@ -87,9 +88,11 @@ it('should add new value', () => {
   expect(onChange).toBeCalledWith(['foo', 'bar']);
 });
 
-function shallowRender(props: Partial<DefaultInputProps> = {}) {
+function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) {
   return shallow(
     <MultiValueInput
+      isDefault={true}
+      name="bar"
       onChange={jest.fn()}
       setting={{ ...settingValue, definition: settingDefinition }}
       value={['foo']}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/PrimitiveInput-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/PrimitiveInput-test.tsx
new file mode 100644 (file)
index 0000000..a09e881
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { mockDefinition, mockSetting } from '../../../../../helpers/mocks/settings';
+import { SettingType } from '../../../../../types/settings';
+import { DefaultSpecializedInputProps } from '../../../utils';
+import PrimitiveInput from '../PrimitiveInput';
+
+it.each(Object.values(SettingType).map(Array.of))(
+  'should render correctly for %s',
+  (type: SettingType) => {
+    const setting = mockSetting({ definition: mockDefinition({ type }) });
+    expect(shallowRender({ setting })).toMatchSnapshot();
+  }
+);
+
+function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) {
+  return shallow(
+    <PrimitiveInput
+      isDefault={true}
+      name="name"
+      onChange={jest.fn()}
+      setting={mockSetting()}
+      value={['foo']}
+      {...props}
+    />
+  );
+}
index 2f6125c67326ae03ba5b353b0734a1d325b3d3b3..f61a4ee4ce59480e2dce99b37fbfde6d89a89eea 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import { mockSetting } from '../../../../../helpers/mocks/settings';
 import { change } from '../../../../../helpers/testUtils';
 import SimpleInput from '../SimpleInput';
 
@@ -31,6 +32,7 @@ it('should render input', () => {
       name="foo"
       onChange={onChange}
       type="text"
+      setting={mockSetting()}
       value="bar"
     />
   ).find('input');
@@ -51,6 +53,7 @@ it('should call onChange', () => {
       name="foo"
       onChange={onChange}
       type="text"
+      setting={mockSetting()}
       value="bar"
     />
   ).find('input');
diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/__snapshots__/PrimitiveInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/__snapshots__/PrimitiveInput-test.tsx.snap
new file mode 100644 (file)
index 0000000..f8968e7
--- /dev/null
@@ -0,0 +1,320 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly for BOOLEAN 1`] = `
+<InputForBoolean
+  isDefault={true}
+  name="name"
+  onChange={[MockFunction]}
+  setting={
+    Object {
+      "definition": Object {
+        "category": "foo category",
+        "fields": Array [],
+        "key": "foo",
+        "options": Array [],
+        "subCategory": "foo subCat",
+        "type": "BOOLEAN",
+      },
+      "hasValue": true,
+      "inherited": true,
+      "key": "foo",
+      "value": "42",
+    }
+  }
+  value={
+    Array [
+      "foo",
+    ]
+  }
+/>
+`;
+
+exports[`should render correctly for FLOAT 1`] = `
+<InputForNumber
+  isDefault={true}
+  name="name"
+  onChange={[MockFunction]}
+  setting={
+    Object {
+      "definition": Object {
+        "category": "foo category",
+        "fields": Array [],
+        "key": "foo",
+        "options": Array [],
+        "subCategory": "foo subCat",
+        "type": "FLOAT",
+      },
+      "hasValue": true,
+      "inherited": true,
+      "key": "foo",
+      "value": "42",
+    }
+  }
+  value={
+    Array [
+      "foo",
+    ]
+  }
+/>
+`;
+
+exports[`should render correctly for INTEGER 1`] = `
+<InputForNumber
+  isDefault={true}
+  name="name"
+  onChange={[MockFunction]}
+  setting={
+    Object {
+      "definition": Object {
+        "category": "foo category",
+        "fields": Array [],
+        "key": "foo",
+        "options": Array [],
+        "subCategory": "foo subCat",
+        "type": "INTEGER",
+      },
+      "hasValue": true,
+      "inherited": true,
+      "key": "foo",
+      "value": "42",
+    }
+  }
+  value={
+    Array [
+      "foo",
+    ]
+  }
+/>
+`;
+
+exports[`should render correctly for JSON 1`] = `
+<InputForJSON
+  isDefault={true}
+  name="name"
+  onChange={[MockFunction]}
+  setting={
+    Object {
+      "definition": Object {
+        "category": "foo category",
+        "fields": Array [],
+        "key": "foo",
+        "options": Array [],
+        "subCategory": "foo subCat",
+        "type": "JSON",
+      },
+      "hasValue": true,
+      "inherited": true,
+      "key": "foo",
+      "value": "42",
+    }
+  }
+  value={
+    Array [
+      "foo",
+    ]
+  }
+/>
+`;
+
+exports[`should render correctly for LICENSE 1`] = `
+<InputForString
+  isDefault={true}
+  name="name"
+  onChange={[MockFunction]}
+  setting={
+    Object {
+      "definition": Object {
+        "category": "foo category",
+        "fields": Array [],
+        "key": "foo",
+        "options": Array [],
+        "subCategory": "foo subCat",
+        "type": "LICENSE",
+      },
+      "hasValue": true,
+      "inherited": true,
+      "key": "foo",
+      "value": "42",
+    }
+  }
+  value={
+    Array [
+      "foo",
+    ]
+  }
+/>
+`;
+
+exports[`should render correctly for LONG 1`] = `
+<InputForNumber
+  isDefault={true}
+  name="name"
+  onChange={[MockFunction]}
+  setting={
+    Object {
+      "definition": Object {
+        "category": "foo category",
+        "fields": Array [],
+        "key": "foo",
+        "options": Array [],
+        "subCategory": "foo subCat",
+        "type": "LONG",
+      },
+      "hasValue": true,
+      "inherited": true,
+      "key": "foo",
+      "value": "42",
+    }
+  }
+  value={
+    Array [
+      "foo",
+    ]
+  }
+/>
+`;
+
+exports[`should render correctly for PASSWORD 1`] = `
+<InputForPassword
+  isDefault={true}
+  name="name"
+  onChange={[MockFunction]}
+  setting={
+    Object {
+      "definition": Object {
+        "category": "foo category",
+        "fields": Array [],
+        "key": "foo",
+        "options": Array [],
+        "subCategory": "foo subCat",
+        "type": "PASSWORD",
+      },
+      "hasValue": true,
+      "inherited": true,
+      "key": "foo",
+      "value": "42",
+    }
+  }
+  value={
+    Array [
+      "foo",
+    ]
+  }
+/>
+`;
+
+exports[`should render correctly for PROPERTY_SET 1`] = `
+<InputForString
+  isDefault={true}
+  name="name"
+  onChange={[MockFunction]}
+  setting={
+    Object {
+      "definition": Object {
+        "category": "foo category",
+        "fields": Array [],
+        "key": "foo",
+        "options": Array [],
+        "subCategory": "foo subCat",
+        "type": "PROPERTY_SET",
+      },
+      "hasValue": true,
+      "inherited": true,
+      "key": "foo",
+      "value": "42",
+    }
+  }
+  value={
+    Array [
+      "foo",
+    ]
+  }
+/>
+`;
+
+exports[`should render correctly for SINGLE_SELECT_LIST 1`] = `
+<Wrapped
+  isDefault={true}
+  name="name"
+  onChange={[MockFunction]}
+  setting={
+    Object {
+      "definition": Object {
+        "category": "foo category",
+        "fields": Array [],
+        "key": "foo",
+        "options": Array [],
+        "subCategory": "foo subCat",
+        "type": "SINGLE_SELECT_LIST",
+      },
+      "hasValue": true,
+      "inherited": true,
+      "key": "foo",
+      "value": "42",
+    }
+  }
+  value={
+    Array [
+      "foo",
+    ]
+  }
+/>
+`;
+
+exports[`should render correctly for STRING 1`] = `
+<InputForString
+  isDefault={true}
+  name="name"
+  onChange={[MockFunction]}
+  setting={
+    Object {
+      "definition": Object {
+        "category": "foo category",
+        "fields": Array [],
+        "key": "foo",
+        "options": Array [],
+        "subCategory": "foo subCat",
+        "type": "STRING",
+      },
+      "hasValue": true,
+      "inherited": true,
+      "key": "foo",
+      "value": "42",
+    }
+  }
+  value={
+    Array [
+      "foo",
+    ]
+  }
+/>
+`;
+
+exports[`should render correctly for TEXT 1`] = `
+<InputForText
+  isDefault={true}
+  name="name"
+  onChange={[MockFunction]}
+  setting={
+    Object {
+      "definition": Object {
+        "category": "foo category",
+        "fields": Array [],
+        "key": "foo",
+        "options": Array [],
+        "subCategory": "foo subCat",
+        "type": "TEXT",
+      },
+      "hasValue": true,
+      "inherited": true,
+      "key": "foo",
+      "value": "42",
+    }
+  }
+  value={
+    Array [
+      "foo",
+    ]
+  }
+/>
+`;
index db66a5ebe05bf85a72353d462955c04b87efc919..ddd645f28a023e43a9672b83f4e0723a1aaa3805 100644 (file)
@@ -20,6 +20,7 @@
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { Link } from 'react-router';
+import Toggle from '../../../../components/controls/Toggle';
 import { Alert } from '../../../../components/ui/Alert';
 import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker';
 import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants';
@@ -30,7 +31,6 @@ import {
   AlmSettingsInstance,
   ProjectAlmBindingResponse
 } from '../../../../types/alm-settings';
-import InputForBoolean from '../inputs/InputForBoolean';
 
 export interface AlmSpecificFormProps {
   alm: AlmKeys;
@@ -108,12 +108,10 @@ function renderBooleanField(
   return renderFieldWrapper(
     renderLabel({ ...props, optional: true }),
     <div className="display-flex-center big-spacer-top">
-      <InputForBoolean
-        isDefault={true}
-        name={id}
-        onChange={v => onFieldChange(propKey, v)}
-        value={value}
-      />
+      <div className="display-inline-block text-top">
+        <Toggle name={id} onChange={v => onFieldChange(propKey, v)} value={value} />
+        {value == null && <span className="spacer-left note">{translate('settings.not_set')}</span>}
+      </div>
       {inputExtra}
     </div>,
     renderHelp(props)
index 1cb1269f6ddcf0ade6169a34368e2b1e94511802..77a828614025767a6b00a2712739f211412d6a72 100644 (file)
@@ -444,12 +444,15 @@ exports[`it should render correctly for github 1`] = `
       <div
         className="display-flex-center big-spacer-top"
       >
-        <InputForBoolean
-          isDefault={true}
-          name="github.summary_comment_setting"
-          onChange={[Function]}
-          value={true}
-        />
+        <div
+          className="display-inline-block text-top"
+        >
+          <Toggle
+            name="github.summary_comment_setting"
+            onChange={[Function]}
+            value={true}
+          />
+        </div>
       </div>
     </div>
   </div>
@@ -535,12 +538,15 @@ exports[`it should render correctly for github if an instance URL is provided 1`
       <div
         className="display-flex-center big-spacer-top"
       >
-        <InputForBoolean
-          isDefault={true}
-          name="github.summary_comment_setting"
-          onChange={[Function]}
-          value={true}
-        />
+        <div
+          className="display-inline-block text-top"
+        >
+          <Toggle
+            name="github.summary_comment_setting"
+            onChange={[Function]}
+            value={true}
+          />
+        </div>
       </div>
     </div>
   </div>
@@ -626,12 +632,15 @@ exports[`it should render correctly for github if an instance URL is provided 2`
       <div
         className="display-flex-center big-spacer-top"
       >
-        <InputForBoolean
-          isDefault={true}
-          name="github.summary_comment_setting"
-          onChange={[Function]}
-          value={true}
-        />
+        <div
+          className="display-inline-block text-top"
+        >
+          <Toggle
+            name="github.summary_comment_setting"
+            onChange={[Function]}
+            value={true}
+          />
+        </div>
       </div>
     </div>
   </div>
@@ -828,12 +837,15 @@ exports[`should render the monorepo field when the feature is supported 1`] = `
       <div
         className="display-flex-center big-spacer-top"
       >
-        <InputForBoolean
-          isDefault={true}
-          name="monorepo"
-          onChange={[Function]}
-          value={false}
-        />
+        <div
+          className="display-inline-block text-top"
+        >
+          <Toggle
+            name="monorepo"
+            onChange={[Function]}
+            value={false}
+          />
+        </div>
       </div>
     </div>
   </div>
index 53d18a0ed1af52a8f40348f7976ccf7bc831026e..326d588b0546181b594debee9eb25376327d896e 100644 (file)
@@ -21,7 +21,7 @@ import { lazyLoadComponent } from '../../components/lazyLoadComponent';
 
 const routes = [
   {
-    indexRoute: { component: lazyLoadComponent(() => import('./components/AppContainer')) }
+    indexRoute: { component: lazyLoadComponent(() => import('./components/SettingsApp')) }
   },
   {
     path: 'encryption',
index 9944b748fc9dd8c69c367d73b3065d9672bd865b..8862df66f2d7baa9846cfb6a4053595d143f9049 100644 (file)
@@ -21,21 +21,25 @@ import {
   getSettingsAppChangedValue,
   getSettingsAppDefinition
 } from '../../../../store/rootReducer';
-import { checkValue, fetchSettings } from '../actions';
+import { checkValue, fetchSettings, fetchValues } from '../actions';
 import { receiveDefinitions } from '../definitions';
 
-jest.mock('../../../../api/settings', () => ({
-  getDefinitions: jest.fn().mockResolvedValue([
-    {
-      key: 'SETTINGS_1_KEY',
-      type: 'SETTINGS_1_TYPE'
-    },
-    {
-      key: 'SETTINGS_2_KEY',
-      type: 'LICENSE'
-    }
-  ])
-}));
+jest.mock('../../../../api/settings', () => {
+  const { mockSettingValue } = jest.requireActual('../../../../helpers/mocks/settings');
+  return {
+    getValues: jest.fn().mockResolvedValue([mockSettingValue()]),
+    getDefinitions: jest.fn().mockResolvedValue([
+      {
+        key: 'SETTINGS_1_KEY',
+        type: 'SETTINGS_1_TYPE'
+      },
+      {
+        key: 'SETTINGS_2_KEY',
+        type: 'LICENSE'
+      }
+    ])
+  };
+});
 
 jest.mock('../definitions', () => ({
   receiveDefinitions: jest.fn()
@@ -59,6 +63,18 @@ it('#fetchSettings should filter LICENSE type settings', async () => {
   ]);
 });
 
+it('should fetchValue correclty', async () => {
+  const dispatch = jest.fn();
+  await fetchValues(['test'], 'foo')(dispatch);
+  expect(dispatch).toHaveBeenCalledWith({
+    component: 'foo',
+    settings: [{ key: 'test' }],
+    type: 'RECEIVE_VALUES',
+    updateKeys: ['test']
+  });
+  expect(dispatch).toHaveBeenCalledWith({ type: 'CLOSE_ALL_GLOBAL_MESSAGES' });
+});
+
 describe('checkValue', () => {
   const dispatch = jest.fn();
 
diff --git a/server/sonar-web/src/main/js/apps/settings/store/__tests__/rootReducer-test.ts b/server/sonar-web/src/main/js/apps/settings/store/__tests__/rootReducer-test.ts
new file mode 100644 (file)
index 0000000..6dbfdc3
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { getSettingsForCategory } from '../rootReducer';
+
+it('Should correclty assert if value is set', () => {
+  const settings = getSettingsForCategory(
+    {
+      definitions: {
+        foo: { category: 'cat', key: 'foo', fields: [], options: [], subCategory: 'test' },
+        bar: { category: 'cat', key: 'bar', fields: [], options: [], subCategory: 'test' }
+      },
+      globalMessages: [],
+      settingsPage: {
+        changedValues: {},
+        loading: {},
+        validationMessages: {}
+      },
+      values: { components: {}, global: { foo: { key: 'foo' } } }
+    },
+    'cat'
+  );
+  expect(settings[0].hasValue).toBe(true);
+  expect(settings[1].hasValue).toBe(false);
+});
index ae7dc188a0005ca29fae1d77bcf14bdf79323cc2..44b67e835e11838542c1ec89f12e615734f47352 100644 (file)
@@ -65,10 +65,10 @@ export function fetchSettings(component?: string) {
   };
 }
 
-export function fetchValues(keys: string, component?: string) {
+export function fetchValues(keys: string[], component?: string) {
   return (dispatch: Dispatch) =>
-    getValues({ keys, component }).then(settings => {
-      dispatch(receiveValues(settings, component));
+    getValues({ keys: keys.join(), component }).then(settings => {
+      dispatch(receiveValues(keys, settings, component));
       dispatch(closeAllGlobalMessages());
     });
 }
@@ -131,7 +131,7 @@ export function saveValue(key: string, component?: string) {
     return setSettingValue(definition, value, component)
       .then(() => getValues({ keys: key, component }))
       .then(values => {
-        dispatch(receiveValues(values, component));
+        dispatch(receiveValues([key], values, component));
         dispatch(cancelChange(key));
         dispatch(passValidation(key));
         dispatch(stopLoading(key));
@@ -148,9 +148,9 @@ export function resetValue(key: string, component?: string) {
       .then(() => getValues({ keys: key, component }))
       .then(values => {
         if (values.length > 0) {
-          dispatch(receiveValues(values, component));
+          dispatch(receiveValues([key], values, component));
         } else {
-          dispatch(receiveValues([{ key }], component));
+          dispatch(receiveValues([key], [], component));
         }
         dispatch(passValidation(key));
         dispatch(stopLoading(key));
index 0f8f236c8028d0022dd28d6a46d336f942d03008..3b00673beb64c9cda573ee725bd38216fe268692 100644 (file)
@@ -53,11 +53,16 @@ export function getValue(state: State, key: string, component?: string) {
 }
 
 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
-  }));
+  return fromDefinitions.getDefinitionsForCategory(state.definitions, category).map(definition => {
+    const value = getValue(state, definition.key, component);
+    const hasValue = value !== undefined && value.inherited !== true;
+    return {
+      key: definition.key,
+      hasValue,
+      ...value,
+      definition
+    };
+  });
 }
 
 export function getChangedValue(state: State, key: string) {
index 8b00453b9b6cca803f3e7c07905e027d4d04f78c..1146d12b3597270428b1712408d55e7008b452b6 100644 (file)
@@ -17,7 +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.
  */
-import { keyBy } from 'lodash';
+import { keyBy, omit } from 'lodash';
 import { combineReducers } from 'redux';
 import { Action as AppStateAction, Actions as AppStateActions } from '../../../store/appState';
 import { ActionType } from '../../../store/utils/actions';
@@ -37,10 +37,11 @@ export interface State {
 }
 
 export function receiveValues(
+  updateKeys: string[],
   settings: Array<{ key: string; value?: string }>,
   component?: string
 ) {
-  return { type: Actions.receiveValues, settings, component };
+  return { type: Actions.receiveValues, updateKeys, settings, component };
 }
 
 function components(state: State['components'] = {}, action: Action) {
@@ -50,7 +51,7 @@ function components(state: State['components'] = {}, action: Action) {
   }
   if (action.type === Actions.receiveValues) {
     const settingsByKey = keyBy(action.settings, 'key');
-    return { ...state, [key]: { ...(state[key] || {}), ...settingsByKey } };
+    return { ...state, [key]: { ...omit(state[key] || {}, action.updateKeys), ...settingsByKey } };
   }
   return state;
 }
@@ -61,8 +62,9 @@ function global(state: State['components'] = {}, action: Action | AppStateAction
       return state;
     }
     const settingsByKey = keyBy(action.settings, 'key');
-    return { ...state, ...settingsByKey };
+    return { ...omit(state, action.updateKeys), ...settingsByKey };
   }
+
   if (action.type === AppStateActions.SetAppState) {
     const settingsByKey: SettingsState = {};
     Object.keys(action.appState.settings).forEach(
index 9fd4d106634707049cb8bc738ead34a655c87bbf..5589c5e532d3387e729fc9fbf095b13e673e81e6 100644 (file)
@@ -25,12 +25,16 @@ import { Setting, SettingCategoryDefinition, SettingDefinition } from '../../typ
 
 export const DEFAULT_CATEGORY = 'general';
 
-export type DefaultSpecializedInputProps = T.Omit<DefaultInputProps, 'setting'> & {
+export type DefaultSpecializedInputProps = DefaultInputProps & {
+  className?: string;
+  autoComplete?: string;
   isDefault: boolean;
   name: string;
+  type?: string;
 };
 
 export interface DefaultInputProps {
+  autoFocus?: boolean;
   hasValueChanged?: boolean;
   onCancel?: () => void;
   onChange: (value: any) => void;
@@ -89,6 +93,10 @@ export function isEmptyValue(definition: SettingDefinition, value: any) {
   }
 }
 
+export function isSecuredDefinition(item: SettingDefinition): boolean {
+  return item.key.endsWith('.secured');
+}
+
 export function isCategoryDefinition(item: SettingDefinition): item is SettingCategoryDefinition {
   return Boolean((item as any).fields);
 }
index 14065d55238cfed074e1bce6a4d0068830574c6f..898122d1d4d3b6f06783a300902baf665e0766de 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { Setting, SettingCategoryDefinition, SettingWithCategory } from '../../types/settings';
+import {
+  Setting,
+  SettingCategoryDefinition,
+  SettingType,
+  SettingValue,
+  SettingWithCategory
+} from '../../types/settings';
 
 export function mockDefinition(
   overrides: Partial<SettingCategoryDefinition> = {}
@@ -36,30 +42,39 @@ export function mockSetting(overrides: Partial<Setting> = {}): Setting {
   return {
     key: 'foo',
     value: '42',
+    hasValue: true,
     inherited: true,
     definition: {
       key: 'foo',
       name: 'Foo setting',
       description: 'When Foo then Bar',
-      type: 'INTEGER',
+      type: SettingType.INTEGER,
       options: []
     },
     ...overrides
   };
 }
 
+export function mockSettingValue(overrides: Partial<SettingValue> = {}) {
+  return {
+    key: 'test',
+    ...overrides
+  };
+}
+
 export function mockSettingWithCategory(
   overrides: Partial<SettingWithCategory> = {}
 ): SettingWithCategory {
   return {
     key: 'foo',
     value: '42',
+    hasValue: true,
     inherited: true,
     definition: {
       key: 'foo',
       name: 'Foo setting',
       description: 'When Foo then Bar',
-      type: 'INTEGER',
+      type: SettingType.INTEGER,
       options: [],
       category: 'general',
       fields: [],
index b272b5b9943fc565c9ca2fd2431db82474b624a0..db89872a34543a8c73f3626f614f9c4090d8f777 100644 (file)
@@ -25,22 +25,22 @@ export const enum SettingsKey {
   ProjectReportFrequency = 'sonar.governance.report.project.branch.frequency'
 }
 
-export type Setting = SettingValue & { definition: SettingDefinition };
+export type Setting = SettingValue & { definition: SettingDefinition; hasValue: boolean };
 export type SettingWithCategory = Setting & { definition: SettingCategoryDefinition };
 
-export type SettingType =
-  | 'STRING'
-  | 'TEXT'
-  | 'JSON'
-  | 'PASSWORD'
-  | 'BOOLEAN'
-  | 'FLOAT'
-  | 'INTEGER'
-  | 'LICENSE'
-  | 'LONG'
-  | 'SINGLE_SELECT_LIST'
-  | 'PROPERTY_SET';
-
+export enum SettingType {
+  STRING = 'STRING',
+  TEXT = 'TEXT',
+  JSON = 'JSON',
+  PASSWORD = 'PASSWORD',
+  BOOLEAN = 'BOOLEAN',
+  FLOAT = 'FLOAT',
+  INTEGER = 'INTEGER',
+  LICENSE = 'LICENSE',
+  LONG = 'LONG',
+  SINGLE_SELECT_LIST = 'SINGLE_SELECT_LIST',
+  PROPERTY_SET = 'PROPERTY_SET'
+}
 export interface SettingDefinition {
   description?: string;
   key: string;