]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-15912 Extract settings local state from redux
authorJeremy Davis <jeremy.davis@sonarsource.com>
Wed, 2 Feb 2022 17:08:55 +0000 (18:08 +0100)
committersonartech <sonartech@sonarsource.com>
Mon, 7 Feb 2022 20:02:54 +0000 (20:02 +0000)
61 files changed:
server/sonar-web/src/main/js/api/settings.ts
server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx
server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.tsx
server/sonar-web/src/main/js/apps/settings/components/AnalysisScope.tsx
server/sonar-web/src/main/js/apps/settings/components/CategoryDefinitionsList.tsx
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/DefinitionRenderer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.tsx
server/sonar-web/src/main/js/apps/settings/components/Languages.tsx
server/sonar-web/src/main/js/apps/settings/components/PageHeader.tsx
server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx
server/sonar-web/src/main/js/apps/settings/components/SettingsAppRenderer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/SettingsSearch.tsx
server/sonar-web/src/main/js/apps/settings/components/SettingsSearchRenderer.tsx
server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/AdditionalCategories-test.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/AnalysisScope-test.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/CategoryDefinitionsList-test.tsx [new file with mode: 0644]
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__/DefinitionRenderer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/__tests__/Languages-test.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/PageHeader-test.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-test.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsAppRenderer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/__tests__/SubCategoryDefinitionsList-test.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AdditionalCategories-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AnalysisScope-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/DefinitionActions-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/DefinitionRenderer-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Languages-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/PageHeader-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsApp-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsAppRenderer-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegration.tsx
server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegrationRenderer.tsx
server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmTab.tsx
server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmTabRenderer.tsx
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegration-test.tsx
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegrationRenderer-test.tsx
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmTab-test.tsx
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmTabRenderer-test.tsx
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmIntegration-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmIntegrationRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmTab-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmTabRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.tsx
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 [deleted file]
server/sonar-web/src/main/js/apps/settings/store/actions.ts
server/sonar-web/src/main/js/apps/settings/store/definitions.ts [deleted file]
server/sonar-web/src/main/js/apps/settings/store/rootReducer.ts
server/sonar-web/src/main/js/apps/settings/store/settingsPage.ts [deleted file]
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/store/rootReducer.ts
server/sonar-web/src/main/js/types/settings.ts

index be80d712392ba333f1af7845bca845775cbf69ed..e621314c27eee52da26b027bc107e34e2b35588f 100644 (file)
@@ -23,13 +23,13 @@ import { isCategoryDefinition } from '../apps/settings/utils';
 import { getJSON, post, postJSON, RequestData } from '../helpers/request';
 import { BranchParameters } from '../types/branch-like';
 import {
-  SettingCategoryDefinition,
+  ExtendedSettingDefinition,
   SettingDefinition,
   SettingValue,
   SettingValueResponse
 } from '../types/settings';
 
-export function getDefinitions(component?: string): Promise<SettingCategoryDefinition[]> {
+export function getDefinitions(component?: string): Promise<ExtendedSettingDefinition[]> {
   return getJSON('/api/settings/list_definitions', { component }).then(
     r => r.definitions,
     throwGlobalError
index 0529190a6299e5aebfc2de2d5925b528a27b418a..8314209db8c77f19ddf9da72b6f89021dd03ce3d 100644 (file)
@@ -20,8 +20,8 @@
 import { mockComponent } from '../../../helpers/mocks/component';
 import { mockDefinition } from '../../../helpers/mocks/settings';
 import {
+  ExtendedSettingDefinition,
   Setting,
-  SettingCategoryDefinition,
   SettingFieldDefinition,
   SettingType
 } from '../../../types/settings';
@@ -32,7 +32,7 @@ const fields = [
   { key: 'bar', type: 'SINGLE_SELECT_LIST' } as SettingFieldDefinition
 ];
 
-const settingDefinition: SettingCategoryDefinition = {
+const settingDefinition: ExtendedSettingDefinition = {
   category: 'test',
   fields: [],
   key: 'test',
@@ -42,7 +42,7 @@ const settingDefinition: SettingCategoryDefinition = {
 
 describe('#getEmptyValue()', () => {
   it('should work for property sets', () => {
-    const setting: SettingCategoryDefinition = {
+    const setting: ExtendedSettingDefinition = {
       ...settingDefinition,
       type: SettingType.PROPERTY_SET,
       fields
@@ -51,7 +51,7 @@ describe('#getEmptyValue()', () => {
   });
 
   it('should work for multi values string', () => {
-    const setting: SettingCategoryDefinition = {
+    const setting: ExtendedSettingDefinition = {
       ...settingDefinition,
       type: SettingType.STRING,
       multiValues: true
@@ -60,7 +60,7 @@ describe('#getEmptyValue()', () => {
   });
 
   it('should work for multi values boolean', () => {
-    const setting: SettingCategoryDefinition = {
+    const setting: ExtendedSettingDefinition = {
       ...settingDefinition,
       type: SettingType.BOOLEAN,
       multiValues: true
index 718c9a415f9f7e44cbeb9a9500a005b97bdc994a..ed870f1cd043dd9f1df26aaa40409f698e20249e 100644 (file)
@@ -19,6 +19,7 @@
  */
 import * as React from 'react';
 import { translate } from '../../../helpers/l10n';
+import { ExtendedSettingDefinition } from '../../../types/settings';
 import { Component } from '../../../types/types';
 import {
   ALM_INTEGRATION,
@@ -34,6 +35,8 @@ import NewCodePeriod from './NewCodePeriod';
 import PullRequestDecorationBinding from './pullRequestDecorationBinding/PRDecorationBinding';
 
 export interface AdditionalCategoryComponentProps {
+  categories: string[];
+  definitions: ExtendedSettingDefinition[];
   component: Component | undefined;
   selectedCategory: string;
 }
index 3eff66e3c0e0383bf02bd34375c3392d49ecf7a3..c05f1fb30fcc7e3100cb036d513ad0710e9f831f 100644 (file)
@@ -23,7 +23,7 @@ import * as React from 'react';
 import { connect } from 'react-redux';
 import { IndexLink } from 'react-router';
 import { getGlobalSettingsUrl, getProjectSettingsUrl } from '../../../helpers/urls';
-import { getAppState, getSettingsAppAllCategories, Store } from '../../../store/rootReducer';
+import { getAppState, Store } from '../../../store/rootReducer';
 import { Component } from '../../../types/types';
 import { getCategoryName } from '../utils';
 import { ADDITIONAL_CATEGORIES } from './AdditionalCategories';
@@ -85,7 +85,6 @@ export function CategoriesList(props: CategoriesListProps) {
 }
 
 const mapStateToProps = (state: Store) => ({
-  categories: getSettingsAppAllCategories(state),
   branchesEnabled: getAppState(state).branchesEnabled
 });
 
index e26e39c994a5e80bada7d9c1097126d466cfe9db..eb196609ea85f72beee31eec5ccb9c2d6cd93e08 100644 (file)
@@ -24,7 +24,7 @@ import { AdditionalCategoryComponentProps } from './AdditionalCategories';
 import CategoryDefinitionsList from './CategoryDefinitionsList';
 
 export function AnalysisScope(props: AdditionalCategoryComponentProps) {
-  const { component, selectedCategory } = props;
+  const { component, definitions, selectedCategory } = props;
 
   return (
     <>
@@ -55,7 +55,11 @@ export function AnalysisScope(props: AdditionalCategoryComponentProps) {
       </table>
 
       <div className="settings-sub-category">
-        <CategoryDefinitionsList category={selectedCategory} component={component} />
+        <CategoryDefinitionsList
+          category={selectedCategory}
+          component={component}
+          definitions={definitions}
+        />
       </div>
     </>
   );
index 13fe594d2b3cbfecc24ac2f4bd24eafe81ba29a4..473ef4189959ecab279bf6f5449e615f26613ea2 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 { connect } from 'react-redux';
-import { getSettingsAppSettingsForCategory, Store } from '../../../store/rootReducer';
+import { keyBy } from 'lodash';
+import * as React from 'react';
+import { getValues } from '../../../api/settings';
+import {
+  ExtendedSettingDefinition,
+  SettingDefinitionAndValue,
+  SettingValue
+} from '../../../types/settings';
 import { Component } from '../../../types/types';
-import { fetchValues } from '../store/actions';
 import SubCategoryDefinitionsList from './SubCategoryDefinitionsList';
 
 interface Props {
   category: string;
   component?: Component;
+  definitions: ExtendedSettingDefinition[];
+  subCategory?: string;
 }
 
-const mapStateToProps = (state: Store, ownProps: Props) => ({
-  settings: getSettingsAppSettingsForCategory(
-    state,
-    ownProps.category,
-    ownProps.component && ownProps.component.key
-  )
-});
+interface State {
+  settings: SettingDefinitionAndValue[];
+}
+
+export default class CategoryDefinitionsList extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { settings: [] };
+
+  componentDidMount() {
+    this.mounted = true;
+
+    this.loadSettingValues();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.category !== this.props.category) {
+      this.loadSettingValues();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  async loadSettingValues() {
+    const { category, component, definitions } = this.props;
 
-const mapDispatchToProps = { fetchValues };
+    const categoryDefinitions = definitions.filter(
+      definition => definition.category.toLowerCase() === category.toLowerCase()
+    );
 
-export default connect(mapStateToProps, mapDispatchToProps)(SubCategoryDefinitionsList);
+    const keys = categoryDefinitions.map(definition => definition.key).join(',');
+
+    const values: SettingValue[] = await getValues({
+      keys,
+      component: component?.key
+    }).catch(() => []);
+    const valuesByDefinitionKey = keyBy(values, 'key');
+
+    const settings: SettingDefinitionAndValue[] = categoryDefinitions.map(definition => {
+      const settingValue = valuesByDefinitionKey[definition.key];
+      return {
+        definition,
+        settingValue
+      };
+    });
+
+    this.setState({ settings });
+  }
+
+  render() {
+    const { category, component, subCategory } = this.props;
+    const { settings } = this.state;
+
+    return (
+      <SubCategoryDefinitionsList
+        category={category}
+        component={component}
+        settings={settings}
+        subCategory={subCategory}
+      />
+    );
+  }
+}
index 5b9f35330b709cf64e823d9ae9e69e7a11d6db8b..34e1dcc04bb87410b2ee46629a395f49f694af5e 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 classNames from 'classnames';
 import * as React from 'react';
-import { connect } from 'react-redux';
-import AlertErrorIcon from '../../../components/icons/AlertErrorIcon';
-import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon';
+import { getValues, resetSettingValue, setSettingValue } from '../../../api/settings';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { sanitizeStringRestricted } from '../../../helpers/sanitize';
-import {
-  getSettingsAppChangedValue,
-  getSettingsAppValidationMessage,
-  isSettingsAppLoading,
-  Store
-} from '../../../store/rootReducer';
-import { Setting } from '../../../types/settings';
+import { parseError } from '../../../helpers/request';
+import { ExtendedSettingDefinition, SettingType, SettingValue } from '../../../types/settings';
 import { Component } from '../../../types/types';
-import { checkValue, resetValue, saveValue } from '../store/actions';
-import { cancelChange, changeValue, passValidation } from '../store/settingsPage';
-import {
-  getPropertyDescription,
-  getPropertyName,
-  getSettingValue,
-  isDefaultOrInherited
-} from '../utils';
-import DefinitionActions from './DefinitionActions';
-import Input from './inputs/Input';
+import { isEmptyValue, isURLKind } from '../utils';
+import DefinitionRenderer from './DefinitionRenderer';
 
 interface Props {
-  cancelChange: (key: string) => void;
-  changeValue: (key: string, value: any) => void;
-  changedValue: any;
-  checkValue: (key: string) => boolean;
   component?: Component;
-  loading: boolean;
-  passValidation: (key: string) => void;
-  resetValue: (key: string, component?: string) => Promise<void>;
-  saveValue: (key: string, component?: string) => Promise<void>;
-  setting: Setting;
-  validationMessage?: string;
+  definition: ExtendedSettingDefinition;
+  initialSettingValue?: SettingValue;
 }
 
 interface State {
+  changedValue?: string;
+  loading: boolean;
   success: boolean;
+  validationMessage?: string;
+  settingValue?: SettingValue;
 }
 
 const SAFE_SET_STATE_DELAY = 3000;
 
-const formNoop = (e: React.FormEvent<HTMLFormElement>) => e.preventDefault();
-
-export class Definition extends React.PureComponent<Props, State> {
+export default class Definition extends React.PureComponent<Props, State> {
   timeout?: number;
   mounted = false;
-  state = { success: false };
+
+  constructor(props: Props) {
+    super(props);
+
+    this.state = {
+      loading: false,
+      success: false,
+      settingValue: props.initialSettingValue
+    };
+  }
 
   componentDidMount() {
     this.mounted = true;
@@ -76,161 +62,140 @@ export class Definition extends React.PureComponent<Props, State> {
 
   componentWillUnmount() {
     this.mounted = false;
+    clearTimeout(this.timeout);
   }
 
-  safeSetState(changes: State) {
-    if (this.mounted) {
-      this.setState(changes);
-    }
-  }
-
-  handleChange = (value: any) => {
+  handleChange = (changedValue: any) => {
     clearTimeout(this.timeout);
-    this.props.changeValue(this.props.setting.definition.key, value);
-    this.handleCheck();
+
+    this.setState({ changedValue, success: false }, this.handleCheck);
   };
 
-  handleReset = () => {
-    const { component, setting } = this.props;
-    const { definition } = setting;
-    const componentKey = component && component.key;
-    return this.props.resetValue(definition.key, componentKey).then(() => {
-      this.props.cancelChange(definition.key);
-      this.safeSetState({ success: true });
+  handleReset = async () => {
+    const { component, definition } = this.props;
+
+    this.setState({ loading: true, success: false });
+
+    try {
+      await resetSettingValue({ keys: definition.key, component: component?.key });
+      const result = await getValues({ keys: definition.key, component: component?.key });
+      const settingValue = result[0];
+
+      this.setState({
+        changedValue: undefined,
+        loading: false,
+        success: true,
+        validationMessage: undefined,
+        settingValue
+      });
+
       this.timeout = window.setTimeout(
-        () => this.safeSetState({ success: false }),
+        () => this.setState({ success: false }),
         SAFE_SET_STATE_DELAY
       );
-    });
+    } catch (e) {
+      const validationMessage = await parseError(e as Response);
+      this.setState({ loading: false, validationMessage });
+    }
   };
 
   handleCancel = () => {
-    const { setting } = this.props;
-    this.props.cancelChange(setting.definition.key);
-    this.props.passValidation(setting.definition.key);
+    this.setState({ changedValue: undefined, validationMessage: undefined });
   };
 
   handleCheck = () => {
-    const { setting } = this.props;
-    this.props.checkValue(setting.definition.key);
+    const { definition } = this.props;
+    const { changedValue } = this.state;
+
+    if (isEmptyValue(definition, changedValue)) {
+      if (definition.defaultValue === undefined) {
+        this.setState({
+          validationMessage: translate('settings.state.value_cant_be_empty_no_default')
+        });
+      } else {
+        this.setState({ validationMessage: translate('settings.state.value_cant_be_empty') });
+      }
+      return false;
+    }
+
+    if (isURLKind(definition)) {
+      try {
+        // eslint-disable-next-line no-new
+        new URL(changedValue ?? '');
+      } catch (e) {
+        this.setState({
+          validationMessage: translateWithParameters(
+            'settings.state.url_not_valid',
+            changedValue ?? ''
+          )
+        });
+        return false;
+      }
+    }
+
+    if (definition.type === SettingType.JSON) {
+      try {
+        JSON.parse(changedValue ?? '');
+      } catch (e) {
+        this.setState({ validationMessage: (e as Error).message });
+
+        return false;
+      }
+    }
+
+    this.setState({ validationMessage: undefined });
+    return true;
   };
 
-  handleSave = () => {
-    if (this.props.changedValue != null) {
-      this.safeSetState({ success: false });
-      const { component, setting } = this.props;
-      this.props.saveValue(setting.definition.key, component && component.key).then(
-        () => {
-          this.safeSetState({ success: true });
-          this.timeout = window.setTimeout(
-            () => this.safeSetState({ success: false }),
-            SAFE_SET_STATE_DELAY
-          );
-        },
-        () => {
-          /* Do nothing */
-        }
-      );
+  handleSave = async () => {
+    const { component, definition } = this.props;
+    const { changedValue } = this.state;
+
+    if (changedValue !== undefined) {
+      this.setState({ success: false });
+
+      if (isEmptyValue(definition, changedValue)) {
+        this.setState({ validationMessage: translate('settings.state.value_cant_be_empty') });
+
+        return;
+      }
+
+      this.setState({ loading: true });
+
+      try {
+        await setSettingValue(definition, changedValue, component?.key);
+        const result = await getValues({ keys: definition.key, component: component?.key });
+        const settingValue = result[0];
+
+        this.setState({
+          changedValue: undefined,
+          loading: false,
+          success: true,
+          settingValue
+        });
+
+        this.timeout = window.setTimeout(
+          () => this.setState({ success: false }),
+          SAFE_SET_STATE_DELAY
+        );
+      } catch (e) {
+        const validationMessage = await parseError(e as Response);
+        this.setState({ loading: false, validationMessage });
+      }
     }
   };
 
   render() {
-    const { changedValue, loading, setting, validationMessage } = this.props;
-    const { definition } = setting;
-    const propertyName = getPropertyName(definition);
-    const hasError = validationMessage != null;
-    const hasValueChanged = changedValue != null;
-    const effectiveValue = hasValueChanged ? changedValue : getSettingValue(setting);
-    const isDefault = isDefaultOrInherited(setting);
-    const description = getPropertyDescription(definition);
+    const { definition } = this.props;
     return (
-      <div
-        className={classNames('settings-definition', {
-          'settings-definition-changed': hasValueChanged
-        })}
-        data-key={definition.key}>
-        <div className="settings-definition-left">
-          <h3 className="settings-definition-name" title={propertyName}>
-            {propertyName}
-          </h3>
-
-          {description && (
-            <div
-              className="markdown small spacer-top"
-              // eslint-disable-next-line react/no-danger
-              dangerouslySetInnerHTML={{ __html: sanitizeStringRestricted(description) }}
-            />
-          )}
-
-          <div className="settings-definition-key note little-spacer-top">
-            {translateWithParameters('settings.key_x', definition.key)}
-          </div>
-        </div>
-
-        <div className="settings-definition-right">
-          <div className="settings-definition-state">
-            {loading && (
-              <span className="text-info">
-                <i className="spinner spacer-right" />
-                {translate('settings.state.saving')}
-              </span>
-            )}
-
-            {!loading && validationMessage && (
-              <span className="text-danger">
-                <AlertErrorIcon className="spacer-right" />
-                <span>
-                  {translateWithParameters('settings.state.validation_failed', validationMessage)}
-                </span>
-              </span>
-            )}
-
-            {!loading && !hasError && this.state.success && (
-              <span className="text-success">
-                <AlertSuccessIcon className="spacer-right" />
-                {translate('settings.state.saved')}
-              </span>
-            )}
-          </div>
-          <form onSubmit={formNoop}>
-            <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>
+      <DefinitionRenderer
+        definition={definition}
+        onCancel={this.handleCancel}
+        onChange={this.handleChange}
+        onReset={this.handleReset}
+        onSave={this.handleSave}
+        {...this.state}
+      />
     );
   }
 }
-
-const mapStateToProps = (state: Store, ownProps: Pick<Props, 'setting'>) => ({
-  changedValue: getSettingsAppChangedValue(state, ownProps.setting.definition.key),
-  loading: isSettingsAppLoading(state, ownProps.setting.definition.key),
-  validationMessage: getSettingsAppValidationMessage(state, ownProps.setting.definition.key)
-});
-
-const mapDispatchToProps = {
-  cancelChange: cancelChange as any,
-  changeValue: changeValue as any,
-  checkValue: checkValue as any,
-  passValidation: passValidation as any,
-  resetValue: resetValue as any,
-  saveValue: saveValue as any
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(Definition);
index 95120d8c5231354c9c01f2aa7fe4ad2d20df4d4a..cc9eb8e328a48dfe9b591ac96c955c9caff43f7c 100644 (file)
@@ -25,7 +25,7 @@ import { Setting } from '../../../types/settings';
 import { getDefaultValue, isEmptyValue } from '../utils';
 
 type Props = {
-  changedValue: string;
+  changedValue?: string;
   hasError: boolean;
   hasValueChanged: boolean;
   isDefault: boolean;
@@ -76,7 +76,7 @@ export default class DefinitionActions extends React.PureComponent<Props, State>
   render() {
     const { setting, changedValue, isDefault, hasValueChanged } = this.props;
     const hasBeenChangedToEmptyValue =
-      changedValue != null && isEmptyValue(setting.definition, changedValue);
+      changedValue !== undefined && isEmptyValue(setting.definition, changedValue);
     const showReset = hasBeenChangedToEmptyValue || (!isDefault && setting.hasValue);
 
     return (
diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/DefinitionRenderer.tsx
new file mode 100644 (file)
index 0000000..a3397c4
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 classNames from 'classnames';
+import * as React from 'react';
+import AlertErrorIcon from '../../../components/icons/AlertErrorIcon';
+import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { sanitizeStringRestricted } from '../../../helpers/sanitize';
+import { ExtendedSettingDefinition, SettingValue } from '../../../types/settings';
+import {
+  combineDefinitionAndSettingValue,
+  getPropertyDescription,
+  getPropertyName,
+  getSettingValue,
+  isDefaultOrInherited
+} from '../utils';
+import DefinitionActions from './DefinitionActions';
+import Input from './inputs/Input';
+
+export interface DefinitionRendererProps {
+  definition: ExtendedSettingDefinition;
+  changedValue?: string;
+  loading: boolean;
+  success: boolean;
+  validationMessage?: string;
+  settingValue?: SettingValue;
+  onCancel: () => void;
+  onChange: (value: any) => void;
+  onSave: () => void;
+  onReset: () => void;
+}
+
+const formNoop = (e: React.FormEvent<HTMLFormElement>) => e.preventDefault();
+
+export default function DefinitionRenderer(props: DefinitionRendererProps) {
+  const { changedValue, loading, validationMessage, settingValue, success, definition } = props;
+
+  const propertyName = getPropertyName(definition);
+  const hasError = validationMessage != null;
+  const hasValueChanged = changedValue != null;
+  const effectiveValue = hasValueChanged ? changedValue : getSettingValue(definition, settingValue);
+  const isDefault = isDefaultOrInherited(settingValue);
+  const description = getPropertyDescription(definition);
+
+  const settingDefinitionAndValue = combineDefinitionAndSettingValue(definition, settingValue);
+
+  return (
+    <div
+      className={classNames('settings-definition', {
+        'settings-definition-changed': hasValueChanged
+      })}
+      data-key={definition.key}>
+      <div className="settings-definition-left">
+        <h3 className="settings-definition-name" title={propertyName}>
+          {propertyName}
+        </h3>
+
+        {description && (
+          <div
+            className="markdown small spacer-top"
+            // eslint-disable-next-line react/no-danger
+            dangerouslySetInnerHTML={{ __html: sanitizeStringRestricted(description) }}
+          />
+        )}
+
+        <div className="settings-definition-key note little-spacer-top">
+          {translateWithParameters('settings.key_x', definition.key)}
+        </div>
+      </div>
+
+      <div className="settings-definition-right">
+        <div className="settings-definition-state">
+          {loading && (
+            <span className="text-info">
+              <i className="spinner spacer-right" />
+              {translate('settings.state.saving')}
+            </span>
+          )}
+
+          {!loading && validationMessage && (
+            <span className="text-danger">
+              <AlertErrorIcon className="spacer-right" />
+              <span>
+                {translateWithParameters('settings.state.validation_failed', validationMessage)}
+              </span>
+            </span>
+          )}
+
+          {!loading && !hasError && success && (
+            <span className="text-success">
+              <AlertSuccessIcon className="spacer-right" />
+              {translate('settings.state.saved')}
+            </span>
+          )}
+        </div>
+        <form onSubmit={formNoop}>
+          <Input
+            hasValueChanged={hasValueChanged}
+            onCancel={props.onCancel}
+            onChange={props.onChange}
+            onSave={props.onSave}
+            setting={settingDefinitionAndValue}
+            value={effectiveValue}
+          />
+          <DefinitionActions
+            changedValue={changedValue}
+            hasError={hasError}
+            hasValueChanged={hasValueChanged}
+            isDefault={isDefault}
+            onCancel={props.onCancel}
+            onReset={props.onReset}
+            onSave={props.onSave}
+            setting={settingDefinitionAndValue}
+          />
+        </form>
+      </div>
+    </div>
+  );
+}
index e7eaac670c01985f938a021c6a06e2c79cff540d..9caf6c4ee9aa2f6a0419e82b646d080d04ad4c02 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { Setting } from '../../../types/settings';
+import { SettingDefinitionAndValue } from '../../../types/settings';
 import { Component } from '../../../types/types';
 import Definition from './Definition';
 
 interface Props {
   component?: Component;
   scrollToDefinition: (element: HTMLLIElement) => void;
-  settings: Setting[];
+  settings: SettingDefinitionAndValue[];
 }
 
 export default function DefinitionsList(props: Props) {
@@ -37,7 +37,11 @@ export default function DefinitionsList(props: Props) {
           key={setting.definition.key}
           data-key={setting.definition.key}
           ref={props.scrollToDefinition}>
-          <Definition component={component} setting={setting} />
+          <Definition
+            component={component}
+            definition={setting.definition}
+            initialSettingValue={setting.settingValue}
+          />
         </li>
       ))}
     </ul>
index 852fe5ff20a07ea2ec83117056ccf2842e73a525..7ffb1d264151e35a45cc9f6af298d8758dc0b7be 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { connect } from 'react-redux';
 import SelectLegacy from '../../../components/controls/SelectLegacy';
 import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
 import { translate } from '../../../helpers/l10n';
-import { getSettingsAppAllCategories, Store } from '../../../store/rootReducer';
 import { getCategoryName } from '../utils';
 import { AdditionalCategoryComponentProps } from './AdditionalCategories';
 import { LANGUAGES_CATEGORY } from './AdditionalCategoryKeys';
@@ -30,7 +28,6 @@ import CategoryDefinitionsList from './CategoryDefinitionsList';
 import CATEGORY_OVERRIDES from './CategoryOverrides';
 
 export interface LanguagesProps extends AdditionalCategoryComponentProps {
-  categories: string[];
   location: Location;
   router: Router;
 }
@@ -42,7 +39,7 @@ interface SelectOption {
 }
 
 export function Languages(props: LanguagesProps) {
-  const { categories, component, location, router, selectedCategory } = props;
+  const { categories, component, definitions, location, router, selectedCategory } = props;
   const { availableLanguages, selectedLanguage } = getLanguages(categories, selectedCategory);
 
   const handleOnChange = (newOption: SelectOption) => {
@@ -66,7 +63,11 @@ export function Languages(props: LanguagesProps) {
       </div>
       {selectedLanguage && (
         <div className="settings-sub-category">
-          <CategoryDefinitionsList category={selectedLanguage} component={component} />
+          <CategoryDefinitionsList
+            category={selectedLanguage}
+            component={component}
+            definitions={definitions}
+          />
         </div>
       )}
     </>
@@ -100,8 +101,4 @@ function getLanguages(categories: string[], selectedCategory: string) {
   };
 }
 
-export default withRouter(
-  connect((state: Store) => ({
-    categories: getSettingsAppAllCategories(state)
-  }))(Languages)
-);
+export default withRouter(Languages);
index 1e7bde614399a8a30e8f3dd3a2c0debde8074d96..a171e075a9c67091761281da7791c2e059f96a89 100644 (file)
 import * as React from 'react';
 import InstanceMessage from '../../../components/common/InstanceMessage';
 import { translate } from '../../../helpers/l10n';
+import { ExtendedSettingDefinition } from '../../../types/settings';
 import { Component } from '../../../types/types';
 import SettingsSearch from './SettingsSearch';
 
 export interface PageHeaderProps {
   component?: Component;
+  definitions: ExtendedSettingDefinition[];
 }
 
-export default function PageHeader({ component }: PageHeaderProps) {
+export default function PageHeader({ component, definitions }: PageHeaderProps) {
   const title = component ? translate('project_settings.page') : translate('settings.page');
 
   const description = component ? (
@@ -42,7 +44,11 @@ export default function PageHeader({ component }: PageHeaderProps) {
         <div className="top-bar-inner bordered-bottom big-padded-top padded-bottom">
           <h1 className="page-title">{title}</h1>
           <div className="page-description spacer-top">{description}</div>
-          <SettingsSearch className="big-spacer-top" component={component} />
+          <SettingsSearch
+            className="big-spacer-top"
+            component={component}
+            definitions={definitions}
+          />
         </div>
       </div>
     </header>
index d53f254d0e0cb805de7f0871e435c6a9dbe30380..9018e7d926464d66fd5e8945b98cd79f8c0e1f7d 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 { 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 { getDefinitions } from '../../../api/settings';
 import {
   addSideBarClass,
   addWhitePageClass,
   removeSideBarClass,
   removeWhitePageClass
 } from '../../../helpers/pages';
-import { getSettingsAppDefaultCategory, Store } from '../../../store/rootReducer';
+import { ExtendedSettingDefinition } from '../../../types/settings';
 import { Component } from '../../../types/types';
-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';
+import SettingsAppRenderer from './SettingsAppRenderer';
 
 interface Props {
   component?: Component;
-  defaultCategory: string;
-  fetchSettings(component?: string): Promise<void>;
 }
 
 interface State {
+  definitions: ExtendedSettingDefinition[];
   loading: boolean;
 }
 
-export class SettingsApp extends React.PureComponent<Props & WithRouterProps, State> {
+export default class SettingsApp extends React.PureComponent<Props, State> {
   mounted = false;
-  state: State = { loading: true };
+  state: State = { definitions: [], loading: true };
 
   componentDidMount() {
     this.mounted = true;
@@ -74,81 +62,20 @@ export class SettingsApp extends React.PureComponent<Props & WithRouterProps, St
     removeWhitePageClass();
   }
 
-  fetchSettings = () => {
+  fetchSettings = async () => {
     const { component } = this.props;
-    this.props.fetchSettings(component && component.key).then(this.stopLoading, this.stopLoading);
-  };
 
-  stopLoading = () => {
+    const definitions: ExtendedSettingDefinition[] = await getDefinitions(
+      component?.key
+    ).catch(() => []);
+
     if (this.mounted) {
-      this.setState({ loading: false });
+      this.setState({ definitions, 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 { component } = this.props;
+    return <SettingsAppRenderer component={component} {...this.state} />;
   }
 }
-
-const mapStateToProps = (state: Store) => ({
-  defaultCategory: getSettingsAppDefaultCategory(state)
-});
-
-const mapDispatchToProps = { fetchSettings: fetchSettings as any };
-
-export default connect(mapStateToProps, mapDispatchToProps)(SettingsApp);
diff --git a/server/sonar-web/src/main/js/apps/settings/components/SettingsAppRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/SettingsAppRenderer.tsx
new file mode 100644 (file)
index 0000000..9008a85
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { uniqBy } from 'lodash';
+import * as React from 'react';
+import { Helmet } from 'react-helmet-async';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
+import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
+import { Location, withRouter } from '../../../components/hoc/withRouter';
+import { translate } from '../../../helpers/l10n';
+import { ExtendedSettingDefinition } from '../../../types/settings';
+import { Component } from '../../../types/types';
+import { getDefaultCategory } from '../utils';
+import { ADDITIONAL_CATEGORIES } from './AdditionalCategories';
+import AllCategoriesList from './AllCategoriesList';
+import CategoryDefinitionsList from './CategoryDefinitionsList';
+import CATEGORY_OVERRIDES from './CategoryOverrides';
+import PageHeader from './PageHeader';
+
+export interface SettingsAppRendererProps {
+  definitions: ExtendedSettingDefinition[];
+  component?: Component;
+  loading: boolean;
+  location: Location;
+}
+
+export function SettingsAppRenderer(props: SettingsAppRendererProps) {
+  const { definitions, component, loading, location } = props;
+
+  const categories = React.useMemo(() => {
+    return uniqBy(
+      definitions.map(definition => definition.category),
+      category => category.toLowerCase()
+    );
+  }, [definitions]);
+
+  if (loading) {
+    return null;
+  }
+
+  const { query } = location;
+  const defaultCategory = getDefaultCategory(categories);
+  const originalCategory = (query.category as string) || defaultCategory;
+  const overriddenCategory = CATEGORY_OVERRIDES[originalCategory.toLowerCase()];
+  const selectedCategory = overriddenCategory || originalCategory;
+  const foundAdditionalCategory = ADDITIONAL_CATEGORIES.find(c => c.key === selectedCategory);
+  const isProjectSettings = 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={component} definitions={definitions} />
+
+      <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
+                  categories={categories}
+                  component={component}
+                  defaultCategory={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({
+                  categories,
+                  component,
+                  definitions,
+                  selectedCategory: originalCategory
+                })
+              ) : (
+                <CategoryDefinitionsList
+                  category={selectedCategory}
+                  component={component}
+                  definitions={definitions}
+                />
+              )}
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+export default withRouter(SettingsAppRenderer);
index 93e8bf1048dd2a7a0663452988a8ed3698c38d26..92460003ab96b69d6a8a9d932ca54e661be956b5 100644 (file)
 import { debounce, keyBy } from 'lodash';
 import lunr, { LunrIndex } from 'lunr';
 import * as React from 'react';
-import { connect } from 'react-redux';
 import { InjectedRouter } from 'react-router';
 import { withRouter } from '../../../components/hoc/withRouter';
 import { KeyboardCodes } from '../../../helpers/keycodes';
-import { getSettingsAppAllDefinitions, Store } from '../../../store/rootReducer';
-import { SettingCategoryDefinition } from '../../../types/settings';
+import { ExtendedSettingDefinition } from '../../../types/settings';
 import { Component, Dict } from '../../../types/types';
 import {
   ADDITIONAL_PROJECT_SETTING_DEFINITIONS,
@@ -37,12 +35,12 @@ import SettingsSearchRenderer from './SettingsSearchRenderer';
 interface Props {
   className?: string;
   component?: Component;
-  definitions: SettingCategoryDefinition[];
+  definitions: ExtendedSettingDefinition[];
   router: InjectedRouter;
 }
 
 interface State {
-  results?: SettingCategoryDefinition[];
+  results?: ExtendedSettingDefinition[];
   searchQuery: string;
   selectedResult?: string;
   showResults: boolean;
@@ -51,7 +49,7 @@ interface State {
 const DEBOUNCE_DELAY = 250;
 
 export class SettingsSearch extends React.Component<Props, State> {
-  definitionsByKey: Dict<SettingCategoryDefinition>;
+  definitionsByKey: Dict<ExtendedSettingDefinition>;
   index: LunrIndex;
   state: State = {
     searchQuery: '',
@@ -71,7 +69,7 @@ export class SettingsSearch extends React.Component<Props, State> {
     this.definitionsByKey = keyBy(definitions, 'key');
   }
 
-  buildSearchIndex(definitions: SettingCategoryDefinition[]) {
+  buildSearchIndex(definitions: ExtendedSettingDefinition[]) {
     return lunr(function() {
       this.ref('key');
       this.field('key');
@@ -196,8 +194,4 @@ export class SettingsSearch extends React.Component<Props, State> {
   }
 }
 
-const mapStateToProps = (state: Store) => ({
-  definitions: getSettingsAppAllDefinitions(state)
-});
-
-export default withRouter(connect(mapStateToProps)(SettingsSearch));
+export default withRouter(SettingsSearch);
index eca54f6a47a4bbfed5528f0d23d99de267545cc5..29e268b6e63f32f350d46434196e30c4eff67d62 100644 (file)
@@ -25,14 +25,14 @@ import OutsideClickHandler from '../../../components/controls/OutsideClickHandle
 import SearchBox from '../../../components/controls/SearchBox';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { scrollToElement } from '../../../helpers/scrolling';
-import { SettingCategoryDefinition } from '../../../types/settings';
+import { ExtendedSettingDefinition } from '../../../types/settings';
 import { Component } from '../../../types/types';
 import { buildSettingLink, isRealSettingKey } from '../utils';
 
 export interface SettingsSearchRendererProps {
   className?: string;
   component?: Component;
-  results?: SettingCategoryDefinition[];
+  results?: ExtendedSettingDefinition[];
   searchQuery: string;
   selectedResult?: string;
   showResults: boolean;
index 145aa24e2a05aecab519f66913ef93c4b4a54dc8..64c255172c58b9e948a610ae8100119aa884b825 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 { groupBy, isEqual, sortBy } from 'lodash';
+import { groupBy, sortBy } from 'lodash';
 import * as React from 'react';
 import { Location, withRouter } from '../../../components/hoc/withRouter';
 import { sanitizeStringRestricted } from '../../../helpers/sanitize';
 import { scrollToElement } from '../../../helpers/scrolling';
-import { SettingWithCategory } from '../../../types/settings';
+import { SettingDefinitionAndValue } from '../../../types/settings';
 import { Component } from '../../../types/types';
 import { getSubCategoryDescription, getSubCategoryName } from '../utils';
 import DefinitionsList from './DefinitionsList';
@@ -31,9 +31,8 @@ import EmailForm from './EmailForm';
 export interface SubCategoryDefinitionsListProps {
   category: string;
   component?: Component;
-  fetchValues: Function;
   location: Location;
-  settings: Array<SettingWithCategory>;
+  settings: Array<SettingDefinitionAndValue>;
   subCategory?: string;
 }
 
@@ -43,17 +42,7 @@ const SCROLL_OFFSET_BOTTOM = 500;
 export class SubCategoryDefinitionsList extends React.PureComponent<
   SubCategoryDefinitionsListProps
 > {
-  componentDidMount() {
-    this.fetchValues();
-  }
-
   componentDidUpdate(prevProps: SubCategoryDefinitionsListProps) {
-    const prevKeys = prevProps.settings.map(setting => setting.definition.key);
-    const keys = this.props.settings.map(setting => setting.definition.key);
-    if (prevProps.component !== this.props.component || !isEqual(prevKeys, keys)) {
-      this.fetchValues();
-    }
-
     const { hash } = this.props.location;
     if (hash && prevProps.location.hash !== hash) {
       const query = `[data-key=${hash.substr(1).replace(/[.#/]/g, '\\$&')}]`;
@@ -75,11 +64,6 @@ export class SubCategoryDefinitionsList extends React.PureComponent<
     }
   };
 
-  fetchValues() {
-    const keys = this.props.settings.map(setting => setting.definition.key);
-    return this.props.fetchValues(keys, this.props.component && this.props.component.key);
-  }
-
   renderEmailForm = (subCategoryKey: string) => {
     const isEmailSettings = this.props.category === 'general' && subCategoryKey === 'email';
     if (!isEmailSettings) {
index 90e8fc01846c242e98308cc7a210ed51b68a69d4..68e86362105551f463d95e86bc48011dbe01a4f6 100644 (file)
@@ -26,7 +26,9 @@ it('should render additional categories component correctly', () => {
   ADDITIONAL_CATEGORIES.forEach(cat => {
     expect(
       cat.renderComponent({
+        categories: [],
         component: mockComponent(),
+        definitions: [],
         selectedCategory: 'TEST'
       })
     ).toMatchSnapshot();
@@ -39,5 +41,12 @@ it('should not render pull request decoration binding component when the compone
     c => c.key === PULL_REQUEST_DECORATION_BINDING_CATEGORY
   );
 
-  expect(category!.renderComponent({ component: undefined, selectedCategory: '' })).toBeUndefined();
+  expect(
+    category!.renderComponent({
+      categories: [],
+      component: undefined,
+      definitions: [],
+      selectedCategory: ''
+    })
+  ).toBeUndefined();
 });
index f14e6507eda53fdf36760b0cc87f47e5a66599d7..5e24a1d166a2d6356e297ea273d18ed4d842abf9 100644 (file)
@@ -27,5 +27,12 @@ it('should render correctly', () => {
 });
 
 function shallowRender() {
-  return shallow(<AnalysisScope component={mockComponent()} selectedCategory="TEST" />);
+  return shallow(
+    <AnalysisScope
+      categories={[]}
+      component={mockComponent()}
+      definitions={[]}
+      selectedCategory="TEST"
+    />
+  );
 }
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/CategoryDefinitionsList-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/CategoryDefinitionsList-test.tsx
new file mode 100644 (file)
index 0000000..1a2c7b5
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { getValues } from '../../../../api/settings';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { mockDefinition, mockSettingValue } from '../../../../helpers/mocks/settings';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+import CategoryDefinitionsList from '../CategoryDefinitionsList';
+
+jest.mock('../../../../api/settings', () => ({
+  getValues: jest.fn().mockResolvedValue([])
+}));
+
+it('should load settings values', async () => {
+  const settings = [mockSettingValue({ key: 'yes' }), mockSettingValue({ key: 'yesagain' })];
+  (getValues as jest.Mock).mockResolvedValueOnce(settings);
+
+  const definitions = [
+    mockDefinition({ category: 'general', key: 'yes' }),
+    mockDefinition({ category: 'other', key: 'nope' }),
+    mockDefinition({ category: 'general', key: 'yesagain' })
+  ];
+
+  const wrapper = shallowRender({
+    definitions
+  });
+
+  await waitAndUpdate(wrapper);
+
+  expect(getValues).toBeCalledWith({ keys: 'yes,yesagain', component: undefined });
+
+  expect(wrapper.state().settings).toEqual([
+    { definition: definitions[0], settingValue: settings[0] },
+    { definition: definitions[2], settingValue: settings[1] }
+  ]);
+});
+
+it('should reload on category change', async () => {
+  const definitions = [
+    mockDefinition({ category: 'general', key: 'yes' }),
+    mockDefinition({ category: 'other', key: 'nope' }),
+    mockDefinition({ category: 'general', key: 'yesagain' })
+  ];
+  const wrapper = shallowRender({ component: mockComponent({ key: 'comp-key' }), definitions });
+
+  await waitAndUpdate(wrapper);
+
+  expect(getValues).toBeCalledWith({ keys: 'yes,yesagain', component: 'comp-key' });
+
+  wrapper.setProps({ category: 'other' });
+
+  await waitAndUpdate(wrapper);
+
+  expect(getValues).toBeCalledWith({ keys: 'nope', component: 'comp-key' });
+});
+
+function shallowRender(props: Partial<CategoryDefinitionsList['props']> = {}) {
+  return shallow<CategoryDefinitionsList>(
+    <CategoryDefinitionsList category="general" definitions={[]} {...props} />
+  );
+}
index 990b851e3c6c2fe987e31de6ccee0817531765ce..8c15a8133355f5369fb618b6f271cba7409353be 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { mockSetting } from '../../../../helpers/mocks/settings';
+import { getValues, resetSettingValue, setSettingValue } from '../../../../api/settings';
+import { mockDefinition, mockSettingValue } from '../../../../helpers/mocks/settings';
 import { waitAndUpdate } from '../../../../helpers/testUtils';
-import { Definition } from '../Definition';
-import DefinitionActions from '../DefinitionActions';
-import Input from '../inputs/Input';
+import { SettingType } from '../../../../types/settings';
+import Definition from '../Definition';
 
-const setting = mockSetting();
+jest.mock('../../../../api/settings', () => ({
+  getValues: jest.fn().mockResolvedValue([]),
+  resetSettingValue: jest.fn().mockResolvedValue(undefined),
+  setSettingValue: jest.fn().mockResolvedValue(undefined)
+}));
 
 beforeAll(() => {
   jest.useFakeTimers();
@@ -35,67 +39,133 @@ afterAll(() => {
   jest.useRealTimers();
 });
 
-it('should render correctly', () => {
-  const wrapper = shallowRender();
-  expect(wrapper).toMatchSnapshot();
+beforeEach(() => {
+  jest.clearAllMocks();
 });
 
-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);
-  expect(changeValue).toHaveBeenCalledWith(setting.definition.key, 5);
-  expect(checkValue).toHaveBeenCalledWith(setting.definition.key);
+describe('Handle change (and check)', () => {
+  it.each([
+    ['empty, no default', mockDefinition(), '', 'settings.state.value_cant_be_empty_no_default'],
+    [
+      'empty, default',
+      mockDefinition({ defaultValue: 'dflt' }),
+      '',
+      'settings.state.value_cant_be_empty'
+    ],
+    [
+      'invalid url',
+      mockDefinition({ key: 'sonar.core.serverBaseURL' }),
+      '%invalid',
+      'settings.state.url_not_valid.%invalid'
+    ],
+    [
+      'valid url',
+      mockDefinition({ key: 'sonar.core.serverBaseURL' }),
+      'http://www.sonarqube.org',
+      undefined
+    ],
+    [
+      'invalid JSON',
+      mockDefinition({ type: SettingType.JSON }),
+      '{{broken: "json}',
+      'Unexpected token { in JSON at position 1'
+    ],
+    ['valid JSON', mockDefinition({ type: SettingType.JSON }), '{"validJson": true}', undefined]
+  ])(
+    'should handle change (and check value): %s',
+    (_caseName, definition, changedValue, expectedValidationMessage) => {
+      const wrapper = shallowRender({ definition });
+
+      wrapper.instance().handleChange(changedValue);
+
+      expect(wrapper.state().changedValue).toBe(changedValue);
+      expect(wrapper.state().success).toBe(false);
+      expect(wrapper.state().validationMessage).toBe(expectedValidationMessage);
+    }
+  );
 });
 
-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')();
-  expect(cancelChange).toHaveBeenCalledWith(setting.definition.key);
-  expect(passValidation).toHaveBeenCalledWith(setting.definition.key);
+it('should handle cancel', () => {
+  const wrapper = shallowRender();
+  wrapper.setState({ changedValue: 'whatever', validationMessage: 'something wrong' });
+
+  wrapper.instance().handleCancel();
+
+  expect(wrapper.state().changedValue).toBeUndefined();
+  expect(wrapper.state().validationMessage).toBeUndefined();
 });
 
-it('should correctly save value change', async () => {
-  const saveValue = jest.fn().mockResolvedValue({});
-  const wrapper = shallowRender({ changedValue: 10, saveValue });
-  wrapper.find(DefinitionActions).prop('onSave')();
-  await waitAndUpdate(wrapper);
-  expect(saveValue).toHaveBeenCalledWith(setting.definition.key, undefined);
-  expect(wrapper.find('AlertSuccessIcon').exists()).toBe(true);
-  expect(wrapper.state().success).toBe(true);
-  jest.runAllTimers();
-  expect(wrapper.state().success).toBe(false);
+describe('handleSave', () => {
+  it('should ignore when value unchanged', () => {
+    const wrapper = shallowRender();
+
+    wrapper.instance().handleSave();
+
+    expect(wrapper.state().loading).toBe(false);
+    expect(setSettingValue).not.toBeCalled();
+  });
+
+  it('should handle an empty value', () => {
+    const wrapper = shallowRender();
+
+    wrapper.setState({ changedValue: '' });
+
+    wrapper.instance().handleSave();
+
+    expect(wrapper.state().loading).toBe(false);
+    expect(wrapper.state().validationMessage).toBe('settings.state.value_cant_be_empty');
+    expect(setSettingValue).not.toBeCalled();
+  });
+
+  it('should save and update setting value', async () => {
+    const settingValue = mockSettingValue();
+    (getValues as jest.Mock).mockResolvedValueOnce([settingValue]);
+    const definition = mockDefinition();
+    const wrapper = shallowRender({ definition });
+
+    wrapper.setState({ changedValue: 'new value' });
+
+    wrapper.instance().handleSave();
+
+    expect(wrapper.state().loading).toBe(true);
+
+    await waitAndUpdate(wrapper);
+
+    expect(setSettingValue).toBeCalledWith(definition, 'new value', undefined);
+    expect(getValues).toBeCalledWith({ keys: definition.key, component: undefined });
+    expect(wrapper.state().changedValue).toBeUndefined();
+    expect(wrapper.state().loading).toBe(false);
+    expect(wrapper.state().success).toBe(true);
+    expect(wrapper.state().settingValue).toBe(settingValue);
+
+    jest.runAllTimers();
+    expect(wrapper.state().success).toBe(false);
+  });
 });
 
-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('onReset')();
+it('should reset and update setting value', async () => {
+  const settingValue = mockSettingValue();
+  (getValues as jest.Mock).mockResolvedValueOnce([settingValue]);
+  const definition = mockDefinition();
+  const wrapper = shallowRender({ definition });
+
+  wrapper.instance().handleReset();
+
+  expect(wrapper.state().loading).toBe(true);
+
   await waitAndUpdate(wrapper);
-  expect(resetValue).toHaveBeenCalledWith(setting.definition.key, undefined);
-  expect(cancelChange).toHaveBeenCalledWith(setting.definition.key);
+
+  expect(resetSettingValue).toBeCalledWith({ keys: definition.key, component: undefined });
+  expect(getValues).toBeCalledWith({ keys: definition.key, component: undefined });
+  expect(wrapper.state().changedValue).toBeUndefined();
+  expect(wrapper.state().loading).toBe(false);
   expect(wrapper.state().success).toBe(true);
+  expect(wrapper.state().settingValue).toBe(settingValue);
+
   jest.runAllTimers();
   expect(wrapper.state().success).toBe(false);
 });
 
 function shallowRender(props: Partial<Definition['props']> = {}) {
-  return shallow<Definition>(
-    <Definition
-      cancelChange={jest.fn()}
-      changeValue={jest.fn()}
-      changedValue={null}
-      checkValue={jest.fn()}
-      loading={false}
-      passValidation={jest.fn()}
-      resetValue={jest.fn().mockResolvedValue({})}
-      saveValue={jest.fn().mockResolvedValue({})}
-      setting={setting}
-      {...props}
-    />
-  );
+  return shallow<Definition>(<Definition definition={mockDefinition()} {...props} />);
 }
index 0dc3685ce572984359c59e8542089d808bae8f32..c8bafc2f9649bdb02031f5f6245c7b45f8da2f6f 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { SettingCategoryDefinition, SettingType } from '../../../../types/settings';
+import { ExtendedSettingDefinition, SettingType } from '../../../../types/settings';
 import DefinitionActions from '../DefinitionActions';
 
-const definition: SettingCategoryDefinition = {
+const definition: ExtendedSettingDefinition = {
   category: 'baz',
   description: 'lorem',
   fields: [],
@@ -77,9 +77,9 @@ function shallowRender(changedValue: string, hasError: boolean, isDefault: boole
       hasError={hasError}
       hasValueChanged={changedValue !== ''}
       isDefault={isDefault}
-      onCancel={() => {}}
-      onReset={() => {}}
-      onSave={() => {}}
+      onCancel={jest.fn()}
+      onReset={jest.fn()}
+      onSave={jest.fn()}
       setting={settings}
     />
   );
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/DefinitionRenderer-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/DefinitionRenderer-test.tsx
new file mode 100644 (file)
index 0000000..f84052b
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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, mockSettingValue } from '../../../../helpers/mocks/settings';
+import DefinitionRenderer, { DefinitionRendererProps } from '../DefinitionRenderer';
+
+it('should render correctly', () => {
+  expect(shallowRender({ loading: true })).toMatchSnapshot('loading');
+  expect(
+    shallowRender({ definition: mockDefinition({ description: 'description' }) })
+  ).toMatchSnapshot('with description');
+  expect(
+    shallowRender({
+      validationMessage: 'validation message'
+    })
+  ).toMatchSnapshot('in error');
+  expect(shallowRender({ success: true })).toMatchSnapshot('success');
+  expect(
+    shallowRender({ settingValue: mockSettingValue({ key: 'foo', value: 'original value' }) })
+  ).toMatchSnapshot('original value');
+
+  expect(shallowRender({ changedValue: 'new value' })).toMatchSnapshot('changed value');
+});
+
+function shallowRender(props: Partial<DefinitionRendererProps> = {}) {
+  return shallow<DefinitionRendererProps>(
+    <DefinitionRenderer
+      definition={mockDefinition()}
+      loading={false}
+      onCancel={jest.fn()}
+      onChange={jest.fn()}
+      onReset={jest.fn()}
+      onSave={jest.fn()}
+      success={false}
+      {...props}
+    />
+  );
+}
index 153de7c172cc06a6cbe540bf920c455f5e956709..9e9b03df383ba92224e34f0c9b7471c126c7177a 100644 (file)
@@ -58,6 +58,7 @@ function shallowRender(props: Partial<LanguagesProps> = {}) {
     <Languages
       categories={['Java', 'JavaScript', 'COBOL']}
       component={undefined}
+      definitions={[]}
       location={mockLocation()}
       router={mockRouter()}
       selectedCategory="java"
index cb8cbfbdb8925707547bd6cd8f3f0077c7392d61..f7e1296f5f5dcda1b0d226965cbc2c4d6cfc80b4 100644 (file)
@@ -28,5 +28,5 @@ it('should render correctly', () => {
 });
 
 function shallowRender(props: Partial<PageHeaderProps> = {}) {
-  return shallow<PageHeaderProps>(<PageHeader {...props} />);
+  return shallow<PageHeaderProps>(<PageHeader definitions={[]} {...props} />);
 }
index 9a3db94a5ea701d74ab18f943a2dd99b5abf0bb9..72bb8ce2befe78c4a6f2c0d7e0f6ac9c00e76b2d 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import ScreenPositionHelper from '../../../../components/common/ScreenPositionHelper';
+import { getDefinitions } from '../../../../api/settings';
+import { mockComponent } from '../../../../helpers/mocks/component';
 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';
+import SettingsApp from '../SettingsApp';
 
 jest.mock('../../../../helpers/pages', () => ({
   addSideBarClass: jest.fn(),
@@ -44,6 +37,10 @@ jest.mock('../../../../helpers/pages', () => ({
   removeWhitePageClass: jest.fn()
 }));
 
+jest.mock('../../../../api/settings', () => ({
+  getDefinitions: jest.fn().mockResolvedValue([])
+}));
+
 it('should render default view correctly', async () => {
   const wrapper = shallowRender();
 
@@ -52,7 +49,8 @@ it('should render default view correctly', async () => {
 
   await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
-  expect(wrapper.find(ScreenPositionHelper).dive()).toMatchSnapshot();
+
+  expect(getDefinitions).toBeCalledWith(undefined);
 
   wrapper.unmount();
 
@@ -60,61 +58,14 @@ it('should render default view correctly', async () => {
   expect(removeWhitePageClass).toBeCalled();
 });
 
-it('should render newCodePeriod correctly', async () => {
-  const wrapper = shallowRender({
-    location: mockLocation({ query: { category: NEW_CODE_PERIOD_CATEGORY } })
-  });
+it('should fetch definitions for component', async () => {
+  const key = 'component-key';
+  const wrapper = shallowRender({ component: mockComponent({ key }) });
 
   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();
+  expect(getDefinitions).toBeCalledWith(key);
 });
 
 function shallowRender(props: Partial<SettingsApp['props']> = {}) {
-  return shallow(
-    <SettingsApp
-      defaultCategory="general"
-      fetchSettings={jest.fn().mockResolvedValue({})}
-      location={mockLocation()}
-      params={{}}
-      router={mockRouter()}
-      routes={[]}
-      {...props}
-    />
-  );
+  return shallow(<SettingsApp {...props} />);
 }
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsAppRenderer-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsAppRenderer-test.tsx
new file mode 100644 (file)
index 0000000..dc200d4
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { mockDefinition } from '../../../../helpers/mocks/settings';
+import { mockLocation } from '../../../../helpers/testMocks';
+import {
+  ALM_INTEGRATION,
+  ANALYSIS_SCOPE_CATEGORY,
+  LANGUAGES_CATEGORY,
+  NEW_CODE_PERIOD_CATEGORY,
+  PULL_REQUEST_DECORATION_BINDING_CATEGORY
+} from '../AdditionalCategoryKeys';
+import { SettingsAppRenderer, SettingsAppRendererProps } from '../SettingsAppRenderer';
+
+it('should render loading correctly', () => {
+  expect(shallowRender({ loading: true }).type()).toBeNull();
+});
+
+it('should render default view correctly', () => {
+  const wrapper = shallowRender();
+
+  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.find(ScreenPositionHelper).dive()).toMatchSnapshot('All Categories List');
+});
+
+it.each([
+  [NEW_CODE_PERIOD_CATEGORY],
+  [LANGUAGES_CATEGORY],
+  [ANALYSIS_SCOPE_CATEGORY],
+  [ALM_INTEGRATION],
+  [PULL_REQUEST_DECORATION_BINDING_CATEGORY]
+])('should render %s correctly', category => {
+  const wrapper = shallowRender({
+    location: mockLocation({ query: { category } })
+  });
+
+  expect(wrapper).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<SettingsAppRendererProps> = {}) {
+  const definitions = [mockDefinition(), mockDefinition({ key: 'bar', category: 'general' })];
+  return shallow(
+    <SettingsAppRenderer
+      definitions={definitions}
+      loading={false}
+      location={mockLocation()}
+      {...props}
+    />
+  );
+}
index eb39e92dd9b8032058bda6fd85c76245afeda86d..2bfbbf61873137c2e2443c82ca41c742e006a4d9 100644 (file)
@@ -61,7 +61,6 @@ function shallowRender(props: Partial<SubCategoryDefinitionsListProps> = {}) {
   return shallow<SubCategoryDefinitionsListProps>(
     <SubCategoryDefinitionsList
       category="general"
-      fetchValues={jest.fn().mockResolvedValue({})}
       location={mockLocation()}
       settings={[
         mockSettingWithCategory(),
index 32357a8f1c0aa91774124835e77b1ff612fba4d8..7d3b5667d2bdaf6f0a0e35307f2cf32474221310 100644 (file)
@@ -1,7 +1,8 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`should render additional categories component correctly 1`] = `
-<withRouter(Connect(Languages))
+<withRouter(Languages)
+  categories={Array []}
   component={
     Object {
       "breadcrumbs": Array [],
@@ -24,6 +25,7 @@ exports[`should render additional categories component correctly 1`] = `
       "tags": Array [],
     }
   }
+  definitions={Array []}
   selectedCategory="TEST"
 />
 `;
@@ -32,6 +34,7 @@ exports[`should render additional categories component correctly 2`] = `<NewCode
 
 exports[`should render additional categories component correctly 3`] = `
 <AnalysisScope
+  categories={Array []}
   component={
     Object {
       "breadcrumbs": Array [],
@@ -54,12 +57,14 @@ exports[`should render additional categories component correctly 3`] = `
       "tags": Array [],
     }
   }
+  definitions={Array []}
   selectedCategory="TEST"
 />
 `;
 
 exports[`should render additional categories component correctly 4`] = `
 <withRouter(Connect(withAppState(AlmIntegration)))
+  categories={Array []}
   component={
     Object {
       "breadcrumbs": Array [],
@@ -82,6 +87,7 @@ exports[`should render additional categories component correctly 4`] = `
       "tags": Array [],
     }
   }
+  definitions={Array []}
   selectedCategory="TEST"
 />
 `;
index 3bb17d7ac96e297362aa4728abfc402d3ccec5e4..5e237ce1b23532dcf8272bb3d333a65324a9041e 100644 (file)
@@ -48,7 +48,7 @@ exports[`should render correctly 1`] = `
   <div
     className="settings-sub-category"
   >
-    <Connect(withRouter(SubCategoryDefinitionsList))
+    <CategoryDefinitionsList
       category="TEST"
       component={
         Object {
@@ -72,6 +72,7 @@ exports[`should render correctly 1`] = `
           "tags": Array [],
         }
       }
+      definitions={Array []}
     />
   </div>
 </Fragment>
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap
deleted file mode 100644 (file)
index 3bf97fc..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<div
-  className="settings-definition"
-  data-key="foo"
->
-  <div
-    className="settings-definition-left"
-  >
-    <h3
-      className="settings-definition-name"
-      title="Foo setting"
-    >
-      Foo setting
-    </h3>
-    <div
-      className="markdown small spacer-top"
-      dangerouslySetInnerHTML={
-        Object {
-          "__html": "When Foo then Bar",
-        }
-      }
-    />
-    <div
-      className="settings-definition-key note little-spacer-top"
-    >
-      settings.key_x.foo
-    </div>
-  </div>
-  <div
-    className="settings-definition-right"
-  >
-    <div
-      className="settings-definition-state"
-    />
-    <form
-      onSubmit={[Function]}
-    >
-      <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",
-            "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",
-              "key": "foo",
-              "name": "Foo setting",
-              "options": Array [],
-              "type": "INTEGER",
-            },
-            "hasValue": true,
-            "inherited": true,
-            "key": "foo",
-            "value": "42",
-          }
-        }
-      />
-    </form>
-  </div>
-</div>
-`;
index 78ad17c3397aa61a1a43f820d3458d4036ebdda6..4751477bfc87eb07b590eb431ef2d9ecc509dbce 100644 (file)
@@ -8,13 +8,13 @@ exports[`disables save button on error 1`] = `
     <Button
       className="spacer-right button-success"
       disabled={true}
-      onClick={[Function]}
+      onClick={[MockFunction]}
     >
       save
     </Button>
     <ResetButtonLink
       className="spacer-right"
-      onClick={[Function]}
+      onClick={[MockFunction]}
     >
       cancel
     </ResetButtonLink>
@@ -30,13 +30,13 @@ exports[`displays cancel button when value changed and has error 1`] = `
     <Button
       className="spacer-right button-success"
       disabled={true}
-      onClick={[Function]}
+      onClick={[MockFunction]}
     >
       save
     </Button>
     <ResetButtonLink
       className="spacer-right"
-      onClick={[Function]}
+      onClick={[MockFunction]}
     >
       cancel
     </ResetButtonLink>
@@ -52,13 +52,13 @@ exports[`displays cancel button when value changed and no error 1`] = `
     <Button
       className="spacer-right button-success"
       disabled={false}
-      onClick={[Function]}
+      onClick={[MockFunction]}
     >
       save
     </Button>
     <ResetButtonLink
       className="spacer-right"
-      onClick={[Function]}
+      onClick={[MockFunction]}
     >
       cancel
     </ResetButtonLink>
@@ -128,13 +128,13 @@ exports[`displays save button when it can be saved 1`] = `
     <Button
       className="spacer-right button-success"
       disabled={false}
-      onClick={[Function]}
+      onClick={[MockFunction]}
     >
       save
     </Button>
     <ResetButtonLink
       className="spacer-right"
-      onClick={[Function]}
+      onClick={[MockFunction]}
     >
       cancel
     </ResetButtonLink>
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/DefinitionRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/DefinitionRenderer-test.tsx.snap
new file mode 100644 (file)
index 0000000..25ae668
--- /dev/null
@@ -0,0 +1,471 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: changed value 1`] = `
+<div
+  className="settings-definition settings-definition-changed"
+  data-key="foo"
+>
+  <div
+    className="settings-definition-left"
+  >
+    <h3
+      className="settings-definition-name"
+    />
+    <div
+      className="settings-definition-key note little-spacer-top"
+    >
+      settings.key_x.foo
+    </div>
+  </div>
+  <div
+    className="settings-definition-right"
+  >
+    <div
+      className="settings-definition-state"
+    />
+    <form
+      onSubmit={[Function]}
+    >
+      <Input
+        hasValueChanged={true}
+        onCancel={[MockFunction]}
+        onChange={[MockFunction]}
+        onSave={[MockFunction]}
+        setting={
+          Object {
+            "definition": Object {
+              "category": "foo category",
+              "fields": Array [],
+              "key": "foo",
+              "options": Array [],
+              "subCategory": "foo subCat",
+            },
+            "hasValue": false,
+            "key": "foo",
+          }
+        }
+        value="new value"
+      />
+      <DefinitionActions
+        changedValue="new value"
+        hasError={false}
+        hasValueChanged={true}
+        isDefault={false}
+        onCancel={[MockFunction]}
+        onReset={[MockFunction]}
+        onSave={[MockFunction]}
+        setting={
+          Object {
+            "definition": Object {
+              "category": "foo category",
+              "fields": Array [],
+              "key": "foo",
+              "options": Array [],
+              "subCategory": "foo subCat",
+            },
+            "hasValue": false,
+            "key": "foo",
+          }
+        }
+      />
+    </form>
+  </div>
+</div>
+`;
+
+exports[`should render correctly: in error 1`] = `
+<div
+  className="settings-definition"
+  data-key="foo"
+>
+  <div
+    className="settings-definition-left"
+  >
+    <h3
+      className="settings-definition-name"
+    />
+    <div
+      className="settings-definition-key note little-spacer-top"
+    >
+      settings.key_x.foo
+    </div>
+  </div>
+  <div
+    className="settings-definition-right"
+  >
+    <div
+      className="settings-definition-state"
+    >
+      <span
+        className="text-danger"
+      >
+        <AlertErrorIcon
+          className="spacer-right"
+        />
+        <span>
+          settings.state.validation_failed.validation message
+        </span>
+      </span>
+    </div>
+    <form
+      onSubmit={[Function]}
+    >
+      <Input
+        hasValueChanged={false}
+        onCancel={[MockFunction]}
+        onChange={[MockFunction]}
+        onSave={[MockFunction]}
+        setting={
+          Object {
+            "definition": Object {
+              "category": "foo category",
+              "fields": Array [],
+              "key": "foo",
+              "options": Array [],
+              "subCategory": "foo subCat",
+            },
+            "hasValue": false,
+            "key": "foo",
+          }
+        }
+      />
+      <DefinitionActions
+        hasError={true}
+        hasValueChanged={false}
+        isDefault={false}
+        onCancel={[MockFunction]}
+        onReset={[MockFunction]}
+        onSave={[MockFunction]}
+        setting={
+          Object {
+            "definition": Object {
+              "category": "foo category",
+              "fields": Array [],
+              "key": "foo",
+              "options": Array [],
+              "subCategory": "foo subCat",
+            },
+            "hasValue": false,
+            "key": "foo",
+          }
+        }
+      />
+    </form>
+  </div>
+</div>
+`;
+
+exports[`should render correctly: loading 1`] = `
+<div
+  className="settings-definition"
+  data-key="foo"
+>
+  <div
+    className="settings-definition-left"
+  >
+    <h3
+      className="settings-definition-name"
+    />
+    <div
+      className="settings-definition-key note little-spacer-top"
+    >
+      settings.key_x.foo
+    </div>
+  </div>
+  <div
+    className="settings-definition-right"
+  >
+    <div
+      className="settings-definition-state"
+    >
+      <span
+        className="text-info"
+      >
+        <i
+          className="spinner spacer-right"
+        />
+        settings.state.saving
+      </span>
+    </div>
+    <form
+      onSubmit={[Function]}
+    >
+      <Input
+        hasValueChanged={false}
+        onCancel={[MockFunction]}
+        onChange={[MockFunction]}
+        onSave={[MockFunction]}
+        setting={
+          Object {
+            "definition": Object {
+              "category": "foo category",
+              "fields": Array [],
+              "key": "foo",
+              "options": Array [],
+              "subCategory": "foo subCat",
+            },
+            "hasValue": false,
+            "key": "foo",
+          }
+        }
+      />
+      <DefinitionActions
+        hasError={false}
+        hasValueChanged={false}
+        isDefault={false}
+        onCancel={[MockFunction]}
+        onReset={[MockFunction]}
+        onSave={[MockFunction]}
+        setting={
+          Object {
+            "definition": Object {
+              "category": "foo category",
+              "fields": Array [],
+              "key": "foo",
+              "options": Array [],
+              "subCategory": "foo subCat",
+            },
+            "hasValue": false,
+            "key": "foo",
+          }
+        }
+      />
+    </form>
+  </div>
+</div>
+`;
+
+exports[`should render correctly: original value 1`] = `
+<div
+  className="settings-definition"
+  data-key="foo"
+>
+  <div
+    className="settings-definition-left"
+  >
+    <h3
+      className="settings-definition-name"
+    />
+    <div
+      className="settings-definition-key note little-spacer-top"
+    >
+      settings.key_x.foo
+    </div>
+  </div>
+  <div
+    className="settings-definition-right"
+  >
+    <div
+      className="settings-definition-state"
+    />
+    <form
+      onSubmit={[Function]}
+    >
+      <Input
+        hasValueChanged={false}
+        onCancel={[MockFunction]}
+        onChange={[MockFunction]}
+        onSave={[MockFunction]}
+        setting={
+          Object {
+            "definition": Object {
+              "category": "foo category",
+              "fields": Array [],
+              "key": "foo",
+              "options": Array [],
+              "subCategory": "foo subCat",
+            },
+            "hasValue": true,
+            "key": "foo",
+            "value": "original value",
+          }
+        }
+        value="original value"
+      />
+      <DefinitionActions
+        hasError={false}
+        hasValueChanged={false}
+        isDefault={false}
+        onCancel={[MockFunction]}
+        onReset={[MockFunction]}
+        onSave={[MockFunction]}
+        setting={
+          Object {
+            "definition": Object {
+              "category": "foo category",
+              "fields": Array [],
+              "key": "foo",
+              "options": Array [],
+              "subCategory": "foo subCat",
+            },
+            "hasValue": true,
+            "key": "foo",
+            "value": "original value",
+          }
+        }
+      />
+    </form>
+  </div>
+</div>
+`;
+
+exports[`should render correctly: success 1`] = `
+<div
+  className="settings-definition"
+  data-key="foo"
+>
+  <div
+    className="settings-definition-left"
+  >
+    <h3
+      className="settings-definition-name"
+    />
+    <div
+      className="settings-definition-key note little-spacer-top"
+    >
+      settings.key_x.foo
+    </div>
+  </div>
+  <div
+    className="settings-definition-right"
+  >
+    <div
+      className="settings-definition-state"
+    >
+      <span
+        className="text-success"
+      >
+        <AlertSuccessIcon
+          className="spacer-right"
+        />
+        settings.state.saved
+      </span>
+    </div>
+    <form
+      onSubmit={[Function]}
+    >
+      <Input
+        hasValueChanged={false}
+        onCancel={[MockFunction]}
+        onChange={[MockFunction]}
+        onSave={[MockFunction]}
+        setting={
+          Object {
+            "definition": Object {
+              "category": "foo category",
+              "fields": Array [],
+              "key": "foo",
+              "options": Array [],
+              "subCategory": "foo subCat",
+            },
+            "hasValue": false,
+            "key": "foo",
+          }
+        }
+      />
+      <DefinitionActions
+        hasError={false}
+        hasValueChanged={false}
+        isDefault={false}
+        onCancel={[MockFunction]}
+        onReset={[MockFunction]}
+        onSave={[MockFunction]}
+        setting={
+          Object {
+            "definition": Object {
+              "category": "foo category",
+              "fields": Array [],
+              "key": "foo",
+              "options": Array [],
+              "subCategory": "foo subCat",
+            },
+            "hasValue": false,
+            "key": "foo",
+          }
+        }
+      />
+    </form>
+  </div>
+</div>
+`;
+
+exports[`should render correctly: with description 1`] = `
+<div
+  className="settings-definition"
+  data-key="foo"
+>
+  <div
+    className="settings-definition-left"
+  >
+    <h3
+      className="settings-definition-name"
+    />
+    <div
+      className="markdown small spacer-top"
+      dangerouslySetInnerHTML={
+        Object {
+          "__html": "description",
+        }
+      }
+    />
+    <div
+      className="settings-definition-key note little-spacer-top"
+    >
+      settings.key_x.foo
+    </div>
+  </div>
+  <div
+    className="settings-definition-right"
+  >
+    <div
+      className="settings-definition-state"
+    />
+    <form
+      onSubmit={[Function]}
+    >
+      <Input
+        hasValueChanged={false}
+        onCancel={[MockFunction]}
+        onChange={[MockFunction]}
+        onSave={[MockFunction]}
+        setting={
+          Object {
+            "definition": Object {
+              "category": "foo category",
+              "description": "description",
+              "fields": Array [],
+              "key": "foo",
+              "options": Array [],
+              "subCategory": "foo subCat",
+            },
+            "hasValue": false,
+            "key": "foo",
+          }
+        }
+      />
+      <DefinitionActions
+        hasError={false}
+        hasValueChanged={false}
+        isDefault={false}
+        onCancel={[MockFunction]}
+        onReset={[MockFunction]}
+        onSave={[MockFunction]}
+        setting={
+          Object {
+            "definition": Object {
+              "category": "foo category",
+              "description": "description",
+              "fields": Array [],
+              "key": "foo",
+              "options": Array [],
+              "subCategory": "foo subCat",
+            },
+            "hasValue": false,
+            "key": "foo",
+          }
+        }
+      />
+    </form>
+  </div>
+</div>
+`;
index 5fb0112cb1dfb763fc061113226f8ba3cd92d64c..71ac56dcb64a969fa6a7e126fbeddaf482d5f536 100644 (file)
@@ -39,8 +39,9 @@ exports[`should render correctly 1`] = `
   <div
     className="settings-sub-category"
   >
-    <Connect(withRouter(SubCategoryDefinitionsList))
+    <CategoryDefinitionsList
       category="java"
+      definitions={Array []}
     />
   </div>
 </Fragment>
index 9f54727d6a1d6e8dbc9100ee488cac087380d950..ab693c7f5d5abdae80b4792a34cc2ca5c7fdd5a7 100644 (file)
@@ -20,7 +20,7 @@ exports[`should render correctly: for project 1`] = `
       >
         project_settings.page.description
       </div>
-      <withRouter(Connect(SettingsSearch))
+      <withRouter(SettingsSearch)
         className="big-spacer-top"
         component={
           Object {
@@ -44,6 +44,7 @@ exports[`should render correctly: for project 1`] = `
             "tags": Array [],
           }
         }
+        definitions={Array []}
       />
     </div>
   </div>
@@ -72,8 +73,9 @@ exports[`should render correctly: global 1`] = `
           message="settings.page.description"
         />
       </div>
-      <withRouter(Connect(SettingsSearch))
+      <withRouter(SettingsSearch)
         className="big-spacer-top"
+        definitions={Array []}
       />
     </div>
   </div>
index 3f03841d252e03d859452f824113153666c2c20e..dd622ed928c379a6f9b3a49edf1429b8569b60cf 100644 (file)
@@ -1,269 +1,8 @@
 // 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}
-    prioritizeSeoTags={false}
-    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}
-    prioritizeSeoTags={false}
-    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}
-    prioritizeSeoTags={false}
-    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}
-    prioritizeSeoTags={false}
-    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}
-    prioritizeSeoTags={false}
-    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}
-    prioritizeSeoTags={false}
-    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>
+<withRouter(SettingsAppRenderer)
+  definitions={Array []}
+  loading={false}
+/>
 `;
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SettingsAppRenderer-test.tsx.snap
new file mode 100644 (file)
index 0000000..1b1ea74
--- /dev/null
@@ -0,0 +1,497 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render almintegration correctly 1`] = `
+<div
+  id="settings-page"
+>
+  <Suggestions
+    suggestions="settings"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    prioritizeSeoTags={false}
+    title="settings.page"
+  />
+  <PageHeader
+    definitions={
+      Array [
+        Object {
+          "category": "foo category",
+          "fields": Array [],
+          "key": "foo",
+          "options": Array [],
+          "subCategory": "foo subCat",
+        },
+        Object {
+          "category": "general",
+          "fields": Array [],
+          "key": "bar",
+          "options": Array [],
+          "subCategory": "foo subCat",
+        },
+      ]
+    }
+  />
+  <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)))
+            categories={
+              Array [
+                "foo category",
+                "general",
+              ]
+            }
+            definitions={
+              Array [
+                Object {
+                  "category": "foo category",
+                  "fields": Array [],
+                  "key": "foo",
+                  "options": Array [],
+                  "subCategory": "foo subCat",
+                },
+                Object {
+                  "category": "general",
+                  "fields": Array [],
+                  "key": "bar",
+                  "options": Array [],
+                  "subCategory": "foo subCat",
+                },
+              ]
+            }
+            selectedCategory="almintegration"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render default view correctly 1`] = `
+<div
+  id="settings-page"
+>
+  <Suggestions
+    suggestions="settings"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    prioritizeSeoTags={false}
+    title="settings.page"
+  />
+  <PageHeader
+    definitions={
+      Array [
+        Object {
+          "category": "foo category",
+          "fields": Array [],
+          "key": "foo",
+          "options": Array [],
+          "subCategory": "foo subCat",
+        },
+        Object {
+          "category": "general",
+          "fields": Array [],
+          "key": "bar",
+          "options": Array [],
+          "subCategory": "foo subCat",
+        },
+      ]
+    }
+  />
+  <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"
+        >
+          <CategoryDefinitionsList
+            category="general"
+            definitions={
+              Array [
+                Object {
+                  "category": "foo category",
+                  "fields": Array [],
+                  "key": "foo",
+                  "options": Array [],
+                  "subCategory": "foo subCat",
+                },
+                Object {
+                  "category": "general",
+                  "fields": Array [],
+                  "key": "bar",
+                  "options": Array [],
+                  "subCategory": "foo subCat",
+                },
+              ]
+            }
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render default view correctly: All Categories List 1`] = `
+<div
+  className="layout-page-side-outer"
+>
+  <div
+    className="layout-page-side"
+    style={
+      Object {
+        "top": 0,
+      }
+    }
+  >
+    <div
+      className="layout-page-side-inner"
+    >
+      <Connect(CategoriesList)
+        categories={
+          Array [
+            "foo category",
+            "general",
+          ]
+        }
+        defaultCategory="general"
+        selectedCategory="general"
+      />
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render exclusions correctly 1`] = `
+<div
+  id="settings-page"
+>
+  <Suggestions
+    suggestions="settings"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    prioritizeSeoTags={false}
+    title="settings.page"
+  />
+  <PageHeader
+    definitions={
+      Array [
+        Object {
+          "category": "foo category",
+          "fields": Array [],
+          "key": "foo",
+          "options": Array [],
+          "subCategory": "foo subCat",
+        },
+        Object {
+          "category": "general",
+          "fields": Array [],
+          "key": "bar",
+          "options": Array [],
+          "subCategory": "foo subCat",
+        },
+      ]
+    }
+  />
+  <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
+            categories={
+              Array [
+                "foo category",
+                "general",
+              ]
+            }
+            definitions={
+              Array [
+                Object {
+                  "category": "foo category",
+                  "fields": Array [],
+                  "key": "foo",
+                  "options": Array [],
+                  "subCategory": "foo subCat",
+                },
+                Object {
+                  "category": "general",
+                  "fields": Array [],
+                  "key": "bar",
+                  "options": Array [],
+                  "subCategory": "foo subCat",
+                },
+              ]
+            }
+            selectedCategory="exclusions"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render languages correctly 1`] = `
+<div
+  id="settings-page"
+>
+  <Suggestions
+    suggestions="settings"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    prioritizeSeoTags={false}
+    title="settings.page"
+  />
+  <PageHeader
+    definitions={
+      Array [
+        Object {
+          "category": "foo category",
+          "fields": Array [],
+          "key": "foo",
+          "options": Array [],
+          "subCategory": "foo subCat",
+        },
+        Object {
+          "category": "general",
+          "fields": Array [],
+          "key": "bar",
+          "options": Array [],
+          "subCategory": "foo subCat",
+        },
+      ]
+    }
+  />
+  <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(Languages)
+            categories={
+              Array [
+                "foo category",
+                "general",
+              ]
+            }
+            definitions={
+              Array [
+                Object {
+                  "category": "foo category",
+                  "fields": Array [],
+                  "key": "foo",
+                  "options": Array [],
+                  "subCategory": "foo subCat",
+                },
+                Object {
+                  "category": "general",
+                  "fields": Array [],
+                  "key": "bar",
+                  "options": Array [],
+                  "subCategory": "foo subCat",
+                },
+              ]
+            }
+            selectedCategory="languages"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render new_code_period correctly 1`] = `
+<div
+  id="settings-page"
+>
+  <Suggestions
+    suggestions="settings"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    prioritizeSeoTags={false}
+    title="settings.page"
+  />
+  <PageHeader
+    definitions={
+      Array [
+        Object {
+          "category": "foo category",
+          "fields": Array [],
+          "key": "foo",
+          "options": Array [],
+          "subCategory": "foo subCat",
+        },
+        Object {
+          "category": "general",
+          "fields": Array [],
+          "key": "bar",
+          "options": Array [],
+          "subCategory": "foo subCat",
+        },
+      ]
+    }
+  />
+  <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}
+    prioritizeSeoTags={false}
+    title="settings.page"
+  />
+  <PageHeader
+    definitions={
+      Array [
+        Object {
+          "category": "foo category",
+          "fields": Array [],
+          "key": "foo",
+          "options": Array [],
+          "subCategory": "foo subCat",
+        },
+        Object {
+          "category": "general",
+          "fields": Array [],
+          "key": "bar",
+          "options": Array [],
+          "subCategory": "foo subCat",
+        },
+      ]
+    }
+  />
+  <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"
+        >
+          <CategoryDefinitionsList
+            category="pull_request_decoration_binding"
+            definitions={
+              Array [
+                Object {
+                  "category": "foo category",
+                  "fields": Array [],
+                  "key": "foo",
+                  "options": Array [],
+                  "subCategory": "foo subCat",
+                },
+                Object {
+                  "category": "general",
+                  "fields": Array [],
+                  "key": "bar",
+                  "options": Array [],
+                  "subCategory": "foo subCat",
+                },
+              ]
+            }
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
index 56bcdafdb2a2effa79330d0b104c16e37e7896b7..f26b1f7ffc2c76dcee6ba7125c6a2ecfc85c52dc 100644 (file)
@@ -34,11 +34,13 @@ import {
   AlmSettingsBindingStatus,
   AlmSettingsBindingStatusType
 } from '../../../../types/alm-settings';
+import { ExtendedSettingDefinition } from '../../../../types/settings';
 import { AppState, Dict } from '../../../../types/types';
 import AlmIntegrationRenderer from './AlmIntegrationRenderer';
 
 interface Props extends Pick<WithRouterProps, 'location' | 'router'> {
   appState: Pick<AppState, 'branchesEnabled' | 'multipleAlmEnabled'>;
+  definitions: ExtendedSettingDefinition[];
 }
 
 export type AlmTabs = AlmKeys.Azure | AlmKeys.GitHub | AlmKeys.GitLab | AlmKeys.BitbucketServer;
@@ -208,7 +210,8 @@ export class AlmIntegration extends React.PureComponent<Props, State> {
 
   render() {
     const {
-      appState: { branchesEnabled, multipleAlmEnabled }
+      appState: { branchesEnabled, multipleAlmEnabled },
+      definitions: settingsDefinitions
     } = this.props;
     const {
       currentAlmTab,
@@ -237,6 +240,7 @@ export class AlmIntegration extends React.PureComponent<Props, State> {
         loadingAlmDefinitions={loadingAlmDefinitions}
         loadingProjectCount={loadingProjectCount}
         projectCount={projectCount}
+        settingsDefinitions={settingsDefinitions}
       />
     );
   }
index 769a2b2d79e6d192be1ca7673c846b3b73df06ec..414d9608583c5f570fe0d758697506f00422400c 100644 (file)
@@ -26,6 +26,7 @@ import {
   AlmSettingsBindingDefinitions,
   AlmSettingsBindingStatus
 } from '../../../../types/alm-settings';
+import { ExtendedSettingDefinition } from '../../../../types/settings';
 import { Dict } from '../../../../types/types';
 import { AlmTabs } from './AlmIntegration';
 import AlmTab from './AlmTab';
@@ -47,6 +48,7 @@ export interface AlmIntegrationRendererProps {
   onSelectAlmTab: (alm: AlmTabs) => void;
   onUpdateDefinitions: () => void;
   projectCount?: number;
+  settingsDefinitions: ExtendedSettingDefinition[];
 }
 
 const tabs = [
@@ -118,7 +120,8 @@ export default function AlmIntegrationRenderer(props: AlmIntegrationRendererProp
     loadingProjectCount,
     branchesEnabled,
     multipleAlmEnabled,
-    projectCount
+    projectCount,
+    settingsDefinitions
   } = props;
 
   const bindingDefinitions = {
@@ -151,6 +154,7 @@ export default function AlmIntegrationRenderer(props: AlmIntegrationRendererProp
         onCheck={props.onCheckConfiguration}
         onDelete={props.onDelete}
         onUpdateDefinitions={props.onUpdateDefinitions}
+        settingsDefinitions={settingsDefinitions}
       />
 
       {definitionKeyForDeletion && (
index 95c05874c1b40de86ea86ae391139b139a371e88..a9b33a1d995170526a6d4847767df417e444e386 100644 (file)
@@ -23,6 +23,7 @@ import {
   AlmBindingDefinitionBase,
   AlmSettingsBindingStatus
 } from '../../../../types/alm-settings';
+import { ExtendedSettingDefinition } from '../../../../types/settings';
 import { Dict } from '../../../../types/types';
 import { AlmTabs } from './AlmIntegration';
 import AlmTabRenderer from './AlmTabRenderer';
@@ -38,6 +39,7 @@ interface Props {
   onCheck: (definitionKey: string) => void;
   onDelete: (definitionKey: string) => void;
   onUpdateDefinitions: () => void;
+  settingsDefinitions: ExtendedSettingDefinition[];
 }
 
 interface State {
@@ -91,7 +93,8 @@ export default class AlmTab extends React.PureComponent<Props, State> {
       definitionStatus,
       loadingAlmDefinitions,
       loadingProjectCount,
-      multipleAlmEnabled
+      multipleAlmEnabled,
+      settingsDefinitions
     } = this.props;
     const { editDefinition, editedDefinition } = this.state;
 
@@ -112,6 +115,7 @@ export default class AlmTab extends React.PureComponent<Props, State> {
         onEdit={this.handleEdit}
         onCancel={this.handleCancel}
         afterSubmit={this.handleAfterSubmit}
+        settingsDefinitions={settingsDefinitions}
       />
     );
   }
index 3806f21ae58800773f444974d3b3952b27383f47..df5639fb5be1500971610a1da8b97162ff6c2af7 100644 (file)
@@ -28,6 +28,7 @@ import {
   AlmSettingsBindingStatus,
   isBitbucketCloudBindingDefinition
 } from '../../../../types/alm-settings';
+import { ExtendedSettingDefinition } from '../../../../types/settings';
 import { Dict } from '../../../../types/types';
 import { ALM_INTEGRATION } from '../AdditionalCategoryKeys';
 import CategoryDefinitionsList from '../CategoryDefinitionsList';
@@ -52,6 +53,7 @@ export interface AlmTabRendererProps {
   onDelete: (definitionKey: string) => void;
   onEdit: (definitionKey: string) => void;
   afterSubmit: (config: AlmBindingDefinitionBase) => void;
+  settingsDefinitions: ExtendedSettingDefinition[];
 }
 
 export default function AlmTabRenderer(props: AlmTabRendererProps) {
@@ -64,7 +66,8 @@ export default function AlmTabRenderer(props: AlmTabRendererProps) {
     editedDefinition,
     loadingAlmDefinitions,
     loadingProjectCount,
-    multipleAlmEnabled
+    multipleAlmEnabled,
+    settingsDefinitions
   } = props;
 
   const preventCreation = loadingProjectCount || (!multipleAlmEnabled && definitions.length > 0);
@@ -116,7 +119,11 @@ export default function AlmTabRenderer(props: AlmTabRendererProps) {
       <div className="huge-spacer-top huge-spacer-bottom bordered-top" />
 
       <div className="big-padded">
-        <CategoryDefinitionsList category={ALM_INTEGRATION} subCategory={almTab} />
+        <CategoryDefinitionsList
+          category={ALM_INTEGRATION}
+          definitions={settingsDefinitions}
+          subCategory={almTab}
+        />
       </div>
     </div>
   );
index cd455c6ee689073d46cbf93a2b1da26c07c63365..c6f66f233bb582f74a0a55a71122d4b371664fcb 100644 (file)
@@ -190,6 +190,7 @@ function shallowRender(props: Partial<AlmIntegration['props']> = {}) {
   return shallow<AlmIntegration>(
     <AlmIntegration
       appState={{ branchesEnabled: true }}
+      definitions={[]}
       location={mockLocation()}
       router={mockRouter()}
       {...props}
index 150027f8e3ca59b13c6281c887fb2b6af7f397b5..bb363871be3d76fb94031ed272e1c271ba863848 100644 (file)
@@ -51,6 +51,7 @@ function shallowRender(props: Partial<AlmIntegrationRendererProps> = {}) {
       onDelete={jest.fn()}
       onSelectAlmTab={jest.fn()}
       onUpdateDefinitions={jest.fn()}
+      settingsDefinitions={[]}
       {...props}
     />
   );
index 7fbd2c73f0ccf69732c029ba1e37737666f39d00..260132fa317d54a3d3b371f0c70120205971bfb0 100644 (file)
@@ -86,6 +86,7 @@ function shallowRender(props: Partial<AlmTab['props']> = {}) {
       onCheck={jest.fn()}
       onDelete={jest.fn()}
       onUpdateDefinitions={jest.fn()}
+      settingsDefinitions={[]}
       {...props}
     />
   );
index 76bb366affb452d53d78f186f256241e6c354c4d..6b55ee46e7a29b45b5ab283b9ce10f1ac7b87546 100644 (file)
@@ -104,6 +104,7 @@ function shallowRender(props: Partial<AlmTabRendererProps> = {}) {
       onDelete={jest.fn()}
       onEdit={jest.fn()}
       afterSubmit={jest.fn()}
+      settingsDefinitions={[]}
       {...props}
     />
   );
index 193dfaf0360d768bbfd942f1750a001248614f3c..561988375f728f31e291c8fd80dd1e78d466ee7d 100644 (file)
@@ -23,5 +23,6 @@ exports[`should render correctly 1`] = `
   onDelete={[Function]}
   onSelectAlmTab={[Function]}
   onUpdateDefinitions={[Function]}
+  settingsDefinitions={Array []}
 />
 `;
index dbbe5ea825c945679bd8e479a4047c338aa9972b..8a6aa95355abf7bfddcbc13eb9baa31d55d167c5 100644 (file)
@@ -83,6 +83,7 @@ exports[`should render correctly: azure 1`] = `
     onCheck={[MockFunction]}
     onDelete={[MockFunction]}
     onUpdateDefinitions={[MockFunction]}
+    settingsDefinitions={Array []}
   />
 </Fragment>
 `;
@@ -170,6 +171,7 @@ exports[`should render correctly: bitbucket 1`] = `
     onCheck={[MockFunction]}
     onDelete={[MockFunction]}
     onUpdateDefinitions={[MockFunction]}
+    settingsDefinitions={Array []}
   />
 </Fragment>
 `;
@@ -257,6 +259,7 @@ exports[`should render correctly: default 1`] = `
     onCheck={[MockFunction]}
     onDelete={[MockFunction]}
     onUpdateDefinitions={[MockFunction]}
+    settingsDefinitions={Array []}
   />
 </Fragment>
 `;
@@ -344,6 +347,7 @@ exports[`should render correctly: delete modal 1`] = `
     onCheck={[MockFunction]}
     onDelete={[MockFunction]}
     onUpdateDefinitions={[MockFunction]}
+    settingsDefinitions={Array []}
   />
   <DeleteModal
     id="keyToDelete"
@@ -436,6 +440,7 @@ exports[`should render correctly: gitlab 1`] = `
     onCheck={[MockFunction]}
     onDelete={[MockFunction]}
     onUpdateDefinitions={[MockFunction]}
+    settingsDefinitions={Array []}
   />
 </Fragment>
 `;
@@ -523,6 +528,7 @@ exports[`should render correctly: loading 1`] = `
     onCheck={[MockFunction]}
     onDelete={[MockFunction]}
     onUpdateDefinitions={[MockFunction]}
+    settingsDefinitions={Array []}
   />
 </Fragment>
 `;
index ae9342d08d08415a6b48f6f847fe72694173a51c..38897cfa773893c5db0f2715034807e38e3f8b02 100644 (file)
@@ -22,5 +22,6 @@ exports[`should render correctly 1`] = `
   onCreate={[Function]}
   onDelete={[MockFunction]}
   onEdit={[Function]}
+  settingsDefinitions={Array []}
 />
 `;
index 4cdb93a907154c3feefee0c71e61eb6a1d8f5b2d..ba36a1e7f3f8aeeb1bb6db9845306986aadb94df 100644 (file)
@@ -49,8 +49,9 @@ exports[`should render correctly for multi-ALM binding: editing a definition 1`]
   <div
     className="big-padded"
   >
-    <Connect(withRouter(SubCategoryDefinitionsList))
+    <CategoryDefinitionsList
       category="almintegration"
+      definitions={Array []}
       subCategory="azure"
     />
   </div>
@@ -106,8 +107,9 @@ exports[`should render correctly for multi-ALM binding: loaded 1`] = `
   <div
     className="big-padded"
   >
-    <Connect(withRouter(SubCategoryDefinitionsList))
+    <CategoryDefinitionsList
       category="almintegration"
+      definitions={Array []}
       subCategory="azure"
     />
   </div>
@@ -163,8 +165,9 @@ exports[`should render correctly for multi-ALM binding: loading ALM definitions
   <div
     className="big-padded"
   >
-    <Connect(withRouter(SubCategoryDefinitionsList))
+    <CategoryDefinitionsList
       category="almintegration"
+      definitions={Array []}
       subCategory="azure"
     />
   </div>
@@ -220,8 +223,9 @@ exports[`should render correctly for multi-ALM binding: loading project count 1`
   <div
     className="big-padded"
   >
-    <Connect(withRouter(SubCategoryDefinitionsList))
+    <CategoryDefinitionsList
       category="almintegration"
+      definitions={Array []}
       subCategory="azure"
     />
   </div>
@@ -277,8 +281,9 @@ exports[`should render correctly for single-ALM binding 1`] = `
   <div
     className="big-padded"
   >
-    <Connect(withRouter(SubCategoryDefinitionsList))
+    <CategoryDefinitionsList
       category="almintegration"
+      definitions={Array []}
       subCategory="azure"
     />
   </div>
@@ -334,8 +339,9 @@ exports[`should render correctly for single-ALM binding 2`] = `
   <div
     className="big-padded"
   >
-    <Connect(withRouter(SubCategoryDefinitionsList))
+    <CategoryDefinitionsList
       category="almintegration"
+      definitions={Array []}
       subCategory="azure"
     />
   </div>
@@ -391,8 +397,9 @@ exports[`should render correctly for single-ALM binding 3`] = `
   <div
     className="big-padded"
   >
-    <Connect(withRouter(SubCategoryDefinitionsList))
+    <CategoryDefinitionsList
       category="almintegration"
+      definitions={Array []}
       subCategory="azure"
     />
   </div>
@@ -438,8 +445,9 @@ exports[`should render correctly with validation: create a first 1`] = `
   <div
     className="big-padded"
   >
-    <Connect(withRouter(SubCategoryDefinitionsList))
+    <CategoryDefinitionsList
       category="almintegration"
+      definitions={Array []}
       subCategory="azure"
     />
   </div>
@@ -499,8 +507,9 @@ exports[`should render correctly with validation: create a second 1`] = `
   <div
     className="big-padded"
   >
-    <Connect(withRouter(SubCategoryDefinitionsList))
+    <CategoryDefinitionsList
       category="almintegration"
+      definitions={Array []}
       subCategory="azure"
     />
   </div>
@@ -560,8 +569,9 @@ exports[`should render correctly with validation: default 1`] = `
   <div
     className="big-padded"
   >
-    <Connect(withRouter(SubCategoryDefinitionsList))
+    <CategoryDefinitionsList
       category="almintegration"
+      definitions={Array []}
       subCategory="azure"
     />
   </div>
@@ -607,8 +617,9 @@ exports[`should render correctly with validation: empty 1`] = `
   <div
     className="big-padded"
   >
-    <Connect(withRouter(SubCategoryDefinitionsList))
+    <CategoryDefinitionsList
       category="almintegration"
+      definitions={Array []}
       subCategory="azure"
     />
   </div>
@@ -666,8 +677,9 @@ exports[`should render correctly with validation: pass the correct key for bitbu
   <div
     className="big-padded"
   >
-    <Connect(withRouter(SubCategoryDefinitionsList))
+    <CategoryDefinitionsList
       category="almintegration"
+      definitions={Array []}
       subCategory="bitbucket"
     />
   </div>
index 3b4e43d81312c9c158cf0fd02732541c46abd664..19285a1f93523f43de6287f20e0ff980f5e295df 100644 (file)
  */
 import * as React from 'react';
 import SelectLegacy from '../../../../components/controls/SelectLegacy';
-import { SettingCategoryDefinition } from '../../../../types/settings';
+import { ExtendedSettingDefinition } from '../../../../types/settings';
 import { DefaultSpecializedInputProps } from '../../utils';
 
-type Props = DefaultSpecializedInputProps & Pick<SettingCategoryDefinition, 'options'>;
+type Props = DefaultSpecializedInputProps & Pick<ExtendedSettingDefinition, 'options'>;
 
 export default class InputForSingleSelectList extends React.PureComponent<Props> {
   handleInputChange = ({ value }: { value: string }) => {
index 343e1671a7290b9125e461bb6d508e07a38e68c9..50ad48b98bd0aa68e39228506acc8b9f3378f54c 100644 (file)
@@ -20,7 +20,7 @@
 import { shallow, ShallowWrapper } from 'enzyme';
 import * as React from 'react';
 import { click } from '../../../../../helpers/testUtils';
-import { SettingCategoryDefinition, SettingType } from '../../../../../types/settings';
+import { ExtendedSettingDefinition, SettingType } from '../../../../../types/settings';
 import { DefaultSpecializedInputProps } from '../../../utils';
 import MultiValueInput from '../MultiValueInput';
 import PrimitiveInput from '../PrimitiveInput';
@@ -30,7 +30,7 @@ const settingValue = {
   hasValue: true
 };
 
-const settingDefinition: SettingCategoryDefinition = {
+const settingDefinition: ExtendedSettingDefinition = {
   category: 'general',
   fields: [],
   key: 'example',
index a843f475629b4b3c4a41ecb7e8994e11e8a2136b..bb2bdd3af202453175a9e829af998d998669e5e1 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 {
-  getSettingsAppChangedValue,
-  getSettingsAppDefinition
-} from '../../../../store/rootReducer';
-import { checkValue, fetchSettings, fetchValues } from '../actions';
-import { receiveDefinitions } from '../definitions';
+import { fetchValues } from '../actions';
 
 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'
-      }
-    ])
+    getValues: jest.fn().mockResolvedValue([mockSettingValue()])
   };
 });
 
-jest.mock('../definitions', () => ({
-  receiveDefinitions: jest.fn()
-}));
-
-jest.mock('../../../../store/rootReducer', () => ({
-  getSettingsAppDefinition: jest.fn(),
-  getSettingsAppChangedValue: jest.fn()
-}));
-
-it('#fetchSettings should filter LICENSE type settings', async () => {
-  const dispatch = jest.fn();
-
-  await fetchSettings()(dispatch);
-
-  expect(receiveDefinitions).toHaveBeenCalledWith([
-    {
-      key: 'SETTINGS_1_KEY',
-      type: 'SETTINGS_1_TYPE'
-    }
-  ]);
-});
-
 it('should fetchValue correclty', async () => {
   const dispatch = jest.fn();
   await fetchValues(['test'], 'foo')(dispatch);
@@ -74,109 +37,3 @@ it('should fetchValue correclty', async () => {
   });
   expect(dispatch).toHaveBeenCalledWith({ type: 'CLOSE_ALL_GLOBAL_MESSAGES' });
 });
-
-describe('checkValue', () => {
-  const dispatch = jest.fn();
-
-  beforeEach(() => {
-    jest.clearAllMocks();
-  });
-
-  it('should correctly identify empty strings', () => {
-    (getSettingsAppDefinition as jest.Mock).mockReturnValue({
-      defaultValue: 'hello',
-      type: 'TEXT'
-    });
-
-    (getSettingsAppChangedValue as jest.Mock).mockReturnValue(undefined);
-    const key = 'key';
-    expect(checkValue(key)(dispatch, jest.fn())).toBe(false);
-    expect(dispatch).toBeCalledWith({
-      type: 'settingsPage/FAIL_VALIDATION',
-      key,
-      message: 'settings.state.value_cant_be_empty'
-    });
-  });
-
-  it('should correctly identify empty with no default', () => {
-    (getSettingsAppDefinition as jest.Mock).mockReturnValue({
-      type: 'TEXT'
-    });
-
-    (getSettingsAppChangedValue as jest.Mock).mockReturnValue(undefined);
-
-    const key = 'key';
-    expect(checkValue(key)(dispatch, jest.fn())).toBe(false);
-    expect(dispatch).toBeCalledWith({
-      type: 'settingsPage/FAIL_VALIDATION',
-      key,
-      message: 'settings.state.value_cant_be_empty_no_default'
-    });
-  });
-
-  it('should correctly identify non-empty strings', () => {
-    (getSettingsAppDefinition as jest.Mock).mockReturnValue({
-      type: 'TEXT'
-    });
-
-    (getSettingsAppChangedValue as jest.Mock).mockReturnValue('not empty');
-    const key = 'key';
-    expect(checkValue(key)(dispatch, jest.fn())).toBe(true);
-    expect(dispatch).toBeCalledWith({
-      type: 'settingsPage/PASS_VALIDATION',
-      key
-    });
-  });
-
-  it('should correctly identify misformed JSON', () => {
-    (getSettingsAppDefinition as jest.Mock).mockReturnValue({
-      type: 'JSON'
-    });
-
-    (getSettingsAppChangedValue as jest.Mock).mockReturnValue('{JSON: "asd;{');
-    const key = 'key';
-    expect(checkValue(key)(dispatch, jest.fn())).toBe(false);
-    expect(dispatch).toBeCalledWith({
-      type: 'settingsPage/FAIL_VALIDATION',
-      key,
-      message: 'Unexpected token J in JSON at position 1'
-    });
-  });
-
-  it('should correctly identify correct JSON', () => {
-    (getSettingsAppDefinition as jest.Mock).mockReturnValue({
-      type: 'JSON'
-    });
-
-    (getSettingsAppChangedValue as jest.Mock).mockReturnValue(
-      '{"number": 42, "question": "answer"}'
-    );
-    const key = 'key';
-    expect(checkValue(key)(dispatch, jest.fn())).toBe(true);
-    expect(dispatch).toBeCalledWith({
-      type: 'settingsPage/PASS_VALIDATION',
-      key
-    });
-  });
-
-  it('should correctly identify URL', () => {
-    (getSettingsAppDefinition as jest.Mock).mockReturnValue({
-      key: 'sonar.core.serverBaseURL'
-    });
-
-    (getSettingsAppChangedValue as jest.Mock).mockReturnValue('http://test');
-    const key = 'sonar.core.serverBaseURL';
-    expect(checkValue(key)(dispatch, jest.fn())).toBe(true);
-    expect(dispatch).toBeCalledWith({
-      type: 'settingsPage/PASS_VALIDATION',
-      key
-    });
-
-    (getSettingsAppChangedValue as jest.Mock).mockReturnValue('not valid');
-    expect(checkValue(key)(dispatch, jest.fn())).toBe(false);
-    expect(dispatch).toBeCalledWith({
-      type: 'settingsPage/PASS_VALIDATION',
-      key
-    });
-  });
-});
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
deleted file mode 100644 (file)
index b886a88..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 217047f167eab4e5ef0856a4e48fb3f0a8cca8bf..ad890c6b47bd2d4f77b74c3467576dfe876b0125 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { Dispatch } from 'redux';
-import {
-  getDefinitions,
-  getValues,
-  resetSettingValue,
-  setSettingValue
-} from '../../../api/settings';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { parseError } from '../../../helpers/request';
+import { getValues } from '../../../api/settings';
 import { closeAllGlobalMessages } from '../../../store/globalMessages';
-import {
-  getSettingsAppChangedValue,
-  getSettingsAppDefinition,
-  Store
-} from '../../../store/rootReducer';
-import { SettingDefinition } from '../../../types/settings';
-import { isEmptyValue } from '../utils';
-import { receiveDefinitions } from './definitions';
-import {
-  cancelChange,
-  failValidation,
-  passValidation,
-  startLoading,
-  stopLoading
-} from './settingsPage';
 import { receiveValues } from './values';
 
-function isURLKind(definition: SettingDefinition) {
-  return [
-    'sonar.core.serverBaseURL',
-    'sonar.auth.github.apiUrl',
-    'sonar.auth.github.webUrl',
-    'sonar.auth.gitlab.url',
-    'sonar.lf.gravatarServerUrl',
-    'sonar.lf.logoUrl',
-    'sonar.auth.saml.loginUrl'
-  ].includes(definition.key);
-}
-
-export function fetchSettings(component?: string) {
-  return (dispatch: Dispatch) => {
-    return getDefinitions(component).then(definitions => {
-      const filtered = definitions.filter(definition => definition.type !== 'LICENSE');
-      dispatch(receiveDefinitions(filtered));
-    });
-  };
-}
-
 export function fetchValues(keys: string[], component?: string) {
   return (dispatch: Dispatch) =>
     getValues({ keys: keys.join(), component }).then(settings => {
@@ -72,100 +29,3 @@ export function fetchValues(keys: string[], component?: string) {
       dispatch(closeAllGlobalMessages());
     });
 }
-
-export function checkValue(key: string) {
-  return (dispatch: Dispatch, getState: () => Store) => {
-    const state = getState();
-    const definition = getSettingsAppDefinition(state, key);
-    const value = getSettingsAppChangedValue(state, key);
-
-    if (isEmptyValue(definition, value)) {
-      if (definition.defaultValue === undefined) {
-        dispatch(failValidation(key, translate('settings.state.value_cant_be_empty_no_default')));
-      } else {
-        dispatch(failValidation(key, translate('settings.state.value_cant_be_empty')));
-      }
-      return false;
-    }
-
-    if (isURLKind(definition)) {
-      try {
-        // eslint-disable-next-line no-new
-        new URL(value);
-      } catch (e) {
-        dispatch(
-          failValidation(key, translateWithParameters('settings.state.url_not_valid', value))
-        );
-        return false;
-      }
-    }
-
-    if (definition.type === 'JSON') {
-      try {
-        JSON.parse(value);
-      } catch (e) {
-        if (e instanceof Error) {
-          dispatch(failValidation(key, e.message));
-        }
-        return false;
-      }
-    }
-
-    dispatch(passValidation(key));
-    return true;
-  };
-}
-
-export function saveValue(key: string, component?: string) {
-  return (dispatch: Dispatch, getState: () => Store) => {
-    dispatch(startLoading(key));
-    const state = getState();
-    const definition = getSettingsAppDefinition(state, key);
-    const value = getSettingsAppChangedValue(state, key);
-
-    if (isEmptyValue(definition, value)) {
-      dispatch(failValidation(key, translate('settings.state.value_cant_be_empty')));
-      dispatch(stopLoading(key));
-      return Promise.reject();
-    }
-
-    return setSettingValue(definition, value, component)
-      .then(() => getValues({ keys: key, component }))
-      .then(values => {
-        dispatch(receiveValues([key], values, component));
-        dispatch(cancelChange(key));
-        dispatch(passValidation(key));
-        dispatch(stopLoading(key));
-      })
-      .catch(handleError(key, dispatch));
-  };
-}
-
-export function resetValue(key: string, component?: string) {
-  return (dispatch: Dispatch) => {
-    dispatch(startLoading(key));
-
-    return resetSettingValue({ keys: key, component })
-      .then(() => getValues({ keys: key, component }))
-      .then(values => {
-        if (values.length > 0) {
-          dispatch(receiveValues([key], values, component));
-        } else {
-          dispatch(receiveValues([key], [], component));
-        }
-        dispatch(passValidation(key));
-        dispatch(stopLoading(key));
-      })
-      .catch(handleError(key, dispatch));
-  };
-}
-
-function handleError(key: string, dispatch: Dispatch) {
-  return (response: Response) => {
-    dispatch(stopLoading(key));
-    return parseError(response).then(message => {
-      dispatch(failValidation(key, message));
-      return Promise.reject();
-    });
-  };
-}
diff --git a/server/sonar-web/src/main/js/apps/settings/store/definitions.ts b/server/sonar-web/src/main/js/apps/settings/store/definitions.ts
deleted file mode 100644 (file)
index a517367..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { keyBy, sortBy, uniqBy } from 'lodash';
-import { ActionType } from '../../../store/utils/actions';
-import { SettingCategoryDefinition } from '../../../types/settings';
-import { Dict } from '../../../types/types';
-import { DEFAULT_CATEGORY, getCategoryName } from '../utils';
-
-const enum Actions {
-  ReceiveDefinitions = 'RECEIVE_DEFINITIONS'
-}
-
-type Action = ActionType<typeof receiveDefinitions, Actions.ReceiveDefinitions>;
-
-export type State = Dict<SettingCategoryDefinition>;
-
-export function receiveDefinitions(definitions: SettingCategoryDefinition[]) {
-  return { type: Actions.ReceiveDefinitions, definitions };
-}
-
-export default function components(state: State = {}, action: Action) {
-  if (action.type === Actions.ReceiveDefinitions) {
-    return keyBy(action.definitions, 'key');
-  }
-  return state;
-}
-
-export function getDefinition(state: State, key: string) {
-  return state[key];
-}
-
-export function getAllDefinitions(state: State) {
-  return Object.keys(state).map(key => state[key]);
-}
-
-export function getDefinitionsForCategory(state: State, category: string) {
-  return getAllDefinitions(state).filter(
-    definition => definition.category.toLowerCase() === category.toLowerCase()
-  );
-}
-
-export function getAllCategories(state: State) {
-  return uniqBy(
-    getAllDefinitions(state).map(definition => definition.category),
-    category => category.toLowerCase()
-  );
-}
-
-export function getDefaultCategory(state: State) {
-  const categories = getAllCategories(state);
-  if (categories.includes(DEFAULT_CATEGORY)) {
-    return DEFAULT_CATEGORY;
-  } else {
-    const sortedCategories = sortBy(categories, category =>
-      getCategoryName(category).toLowerCase()
-    );
-    return sortedCategories[0];
-  }
-}
index 6f8ffdefe02433a7b7431a91d45b190c936f23c8..4bc466337b5da9d62039ee91d51431f4dd9500fe 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { combineReducers } from 'redux';
-import globalMessages, * as fromGlobalMessages from '../../../store/globalMessages';
-import definitions, * as fromDefinitions from './definitions';
-import settingsPage, * as fromSettingsPage from './settingsPage';
 import values, * as fromValues from './values';
 
 interface State {
-  definitions: fromDefinitions.State;
-  globalMessages: fromGlobalMessages.State;
-  settingsPage: fromSettingsPage.State;
   values: fromValues.State;
 }
 
-export default combineReducers({ definitions, values, settingsPage, globalMessages });
-
-export function getDefinition(state: State, key: string) {
-  return fromDefinitions.getDefinition(state.definitions, key);
-}
-
-export function getAllDefinitions(state: State) {
-  return fromDefinitions.getAllDefinitions(state.definitions);
-}
-
-export function getAllCategories(state: State) {
-  return fromDefinitions.getAllCategories(state.definitions);
-}
-
-export function getDefaultCategory(state: State) {
-  return fromDefinitions.getDefaultCategory(state.definitions);
-}
+export default combineReducers({ values });
 
 export function getValue(state: State, key: string, component?: string) {
   return fromValues.getValue(state.values, key, component);
 }
-
-export function getSettingsForCategory(state: State, category: string, component?: string) {
-  return fromDefinitions.getDefinitionsForCategory(state.definitions, category).map(definition => {
-    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) {
-  return fromSettingsPage.getChangedValue(state.settingsPage, key);
-}
-
-export function isLoading(state: State, key: string) {
-  return fromSettingsPage.isLoading(state.settingsPage, key);
-}
-
-export function getValidationMessage(state: State, key: string) {
-  return fromSettingsPage.getValidationMessage(state.settingsPage, key);
-}
-
-export function getGlobalMessages(state: State) {
-  return fromGlobalMessages.getGlobalMessages(state.globalMessages);
-}
diff --git a/server/sonar-web/src/main/js/apps/settings/store/settingsPage.ts b/server/sonar-web/src/main/js/apps/settings/store/settingsPage.ts
deleted file mode 100644 (file)
index 113eacf..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { omit } from 'lodash';
-import { combineReducers } from 'redux';
-import { ActionType } from '../../../store/utils/actions';
-import { Dict } from '../../../types/types';
-
-const enum Actions {
-  CancelChange = 'settingsPage/CANCEL_CHANGE',
-  ChangeValue = 'settingsPage/CHANGE_VALUE',
-  FailValidation = 'settingsPage/FAIL_VALIDATION',
-  PassValidation = 'settingsPage/PASS_VALIDATION',
-  StartLoading = 'settingsPage/START_LOADING',
-  StopLoading = 'settingsPage/STOP_LOADING'
-}
-
-type Action =
-  | ActionType<typeof cancelChange, Actions.CancelChange>
-  | ActionType<typeof changeValue, Actions.ChangeValue>
-  | ActionType<typeof failValidation, Actions.FailValidation>
-  | ActionType<typeof passValidation, Actions.PassValidation>
-  | ActionType<typeof startLoading, Actions.StartLoading>
-  | ActionType<typeof stopLoading, Actions.StopLoading>;
-
-export interface State {
-  changedValues: Dict<any>;
-  loading: Dict<boolean>;
-  validationMessages: Dict<string>;
-}
-
-export function cancelChange(key: string) {
-  return { type: Actions.CancelChange, key };
-}
-
-export function changeValue(key: string, value: any) {
-  return { type: Actions.ChangeValue, key, value };
-}
-
-function changedValues(state: State['changedValues'] = {}, action: Action) {
-  if (action.type === Actions.ChangeValue) {
-    return { ...state, [action.key]: action.value };
-  }
-  if (action.type === Actions.CancelChange) {
-    return omit(state, action.key);
-  }
-  return state;
-}
-
-export function failValidation(key: string, message: string) {
-  return { type: Actions.FailValidation, key, message };
-}
-
-export function passValidation(key: string) {
-  return { type: Actions.PassValidation, key };
-}
-
-function validationMessages(state: State['validationMessages'] = {}, action: Action) {
-  if (action.type === Actions.FailValidation) {
-    return { ...state, [action.key]: action.message };
-  }
-  if (action.type === Actions.PassValidation) {
-    return omit(state, action.key);
-  }
-  return state;
-}
-
-export function startLoading(key: string) {
-  return { type: Actions.StartLoading, key };
-}
-
-export function stopLoading(key: string) {
-  return { type: Actions.StopLoading, key };
-}
-
-function loading(state: State['loading'] = {}, action: Action) {
-  if (action.type === Actions.StartLoading) {
-    return { ...state, [action.key]: true };
-  }
-  if (action.type === Actions.StopLoading) {
-    return { ...state, [action.key]: false };
-  }
-  return state;
-}
-
-export default combineReducers({ changedValues, loading, validationMessages });
-
-export function getChangedValue(state: State, key: string) {
-  return state.changedValues[key];
-}
-
-export function getValidationMessage(state: State, key: string): string | undefined {
-  return state.validationMessages[key];
-}
-
-export function isLoading(state: State, key: string) {
-  return Boolean(state.loading[key]);
-}
index 60da5c92f0a0d85422dbfea381f14c3e6d5aa5aa..3cec4487f833237fd37dc37e942383633c568c84 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { LocationDescriptor } from 'history';
+import { sortBy } from 'lodash';
 import { hasMessage, translate } from '../../helpers/l10n';
 import { getGlobalSettingsUrl, getProjectSettingsUrl } from '../../helpers/urls';
 import { AlmKeys } from '../../types/alm-settings';
-import { Setting, SettingCategoryDefinition, SettingDefinition } from '../../types/settings';
+import {
+  ExtendedSettingDefinition,
+  Setting,
+  SettingDefinition,
+  SettingValue,
+  SettingWithCategory
+} from '../../types/settings';
 import { Component, Dict } from '../../types/types';
 
 export const DEFAULT_CATEGORY = 'general';
@@ -74,14 +81,35 @@ export function getUniqueName(definition: SettingDefinition, index?: string) {
   return `settings[${definition.key}]${indexSuffix}`;
 }
 
-export function getSettingValue({ definition, fieldValues, value, values }: Setting) {
+export function getSettingValue(definition: SettingDefinition, settingValue?: SettingValue) {
+  const { fieldValues, value, values } = settingValue || {};
   if (isCategoryDefinition(definition) && definition.multiValues) {
     return values;
   } else if (definition.type === 'PROPERTY_SET') {
     return fieldValues;
-  } else {
-    return value;
   }
+  return value;
+}
+
+export function combineDefinitionAndSettingValue(
+  definition: ExtendedSettingDefinition,
+  value?: SettingValue
+): SettingWithCategory {
+  const hasValue = value !== undefined && value.inherited !== true;
+  return {
+    key: definition.key,
+    hasValue,
+    ...value,
+    definition
+  };
+}
+
+export function getDefaultCategory(categories: string[]) {
+  if (categories.includes(DEFAULT_CATEGORY)) {
+    return DEFAULT_CATEGORY;
+  }
+  const sortedCategories = sortBy(categories, category => getCategoryName(category).toLowerCase());
+  return sortedCategories[0];
 }
 
 export function isEmptyValue(definition: SettingDefinition, value: any) {
@@ -89,20 +117,32 @@ export function isEmptyValue(definition: SettingDefinition, value: any) {
     return true;
   } else if (definition.type === 'BOOLEAN') {
     return false;
-  } else {
-    return value.length === 0;
   }
+
+  return value.length === 0;
+}
+
+export function isURLKind(definition: SettingDefinition) {
+  return [
+    'sonar.core.serverBaseURL',
+    'sonar.auth.github.apiUrl',
+    'sonar.auth.github.webUrl',
+    'sonar.auth.gitlab.url',
+    'sonar.lf.gravatarServerUrl',
+    'sonar.lf.logoUrl',
+    'sonar.auth.saml.loginUrl'
+  ].includes(definition.key);
 }
 
 export function isSecuredDefinition(item: SettingDefinition): boolean {
   return item.key.endsWith('.secured');
 }
 
-export function isCategoryDefinition(item: SettingDefinition): item is SettingCategoryDefinition {
+export function isCategoryDefinition(item: SettingDefinition): item is ExtendedSettingDefinition {
   return Boolean((item as any).fields);
 }
 
-export function getEmptyValue(item: SettingDefinition | SettingCategoryDefinition): any {
+export function getEmptyValue(item: SettingDefinition | ExtendedSettingDefinition): any {
   if (isCategoryDefinition(item)) {
     if (item.multiValues) {
       return [getEmptyValue({ ...item, multiValues: false })];
@@ -121,8 +161,8 @@ export function getEmptyValue(item: SettingDefinition | SettingCategoryDefinitio
   return '';
 }
 
-export function isDefaultOrInherited(setting: Setting) {
-  return Boolean(setting.inherited);
+export function isDefaultOrInherited(setting?: Pick<SettingValue, 'inherited'>) {
+  return Boolean(setting && setting.inherited);
 }
 
 export function getDefaultValue(setting: Setting) {
@@ -170,7 +210,7 @@ export function isRealSettingKey(key: string) {
 }
 
 export function buildSettingLink(
-  definition: SettingCategoryDefinition,
+  definition: ExtendedSettingDefinition,
   component?: Component
 ): LocationDescriptor {
   const { category, key } = definition;
@@ -198,7 +238,7 @@ export function buildSettingLink(
   };
 }
 
-export const ADDITIONAL_PROJECT_SETTING_DEFINITIONS: SettingCategoryDefinition[] = [
+export const ADDITIONAL_PROJECT_SETTING_DEFINITIONS: ExtendedSettingDefinition[] = [
   {
     name: 'DevOps Platform Integration',
     description: `
@@ -213,7 +253,7 @@ export const ADDITIONAL_PROJECT_SETTING_DEFINITIONS: SettingCategoryDefinition[]
   }
 ];
 
-export const ADDITIONAL_SETTING_DEFINITIONS: SettingCategoryDefinition[] = [
+export const ADDITIONAL_SETTING_DEFINITIONS: ExtendedSettingDefinition[] = [
   {
     name: 'Default New Code behavior',
     description: `
index 3c5e8e154af764b8be55ff9f8b1d53632e2cfdd7..367b4c0cd951e369d851362ecfe8983729f80e95 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import {
+  ExtendedSettingDefinition,
   Setting,
-  SettingCategoryDefinition,
   SettingType,
   SettingValue,
   SettingWithCategory
 } from '../../types/settings';
 
 export function mockDefinition(
-  overrides: Partial<SettingCategoryDefinition> = {}
-): SettingCategoryDefinition {
+  overrides: Partial<ExtendedSettingDefinition> = {}
+): ExtendedSettingDefinition {
   return {
     key: 'foo',
     category: 'foo category',
index 84ae0d1ba3ed3acf2e1863491b0b944b063914f3..36d4d72d9f927a7b6327fb820bebd74249173f60 100644 (file)
@@ -66,42 +66,6 @@ export function getGlobalSettingValue(state: Store, key: string) {
   return fromSettingsApp.getValue(state.settingsApp, key);
 }
 
-export function getSettingsAppAllDefinitions(state: Store) {
-  return fromSettingsApp.getAllDefinitions(state.settingsApp);
-}
-
-export function getSettingsAppDefinition(state: Store, key: string) {
-  return fromSettingsApp.getDefinition(state.settingsApp, key);
-}
-
-export function getSettingsAppAllCategories(state: Store) {
-  return fromSettingsApp.getAllCategories(state.settingsApp);
-}
-
-export function getSettingsAppDefaultCategory(state: Store) {
-  return fromSettingsApp.getDefaultCategory(state.settingsApp);
-}
-
-export function getSettingsAppSettingsForCategory(
-  state: Store,
-  category: string,
-  component?: string
-) {
-  return fromSettingsApp.getSettingsForCategory(state.settingsApp, category, component);
-}
-
-export function getSettingsAppChangedValue(state: Store, key: string) {
-  return fromSettingsApp.getChangedValue(state.settingsApp, key);
-}
-
-export function isSettingsAppLoading(state: Store, key: string) {
-  return fromSettingsApp.isLoading(state.settingsApp, key);
-}
-
-export function getSettingsAppValidationMessage(state: Store, key: string) {
-  return fromSettingsApp.getValidationMessage(state.settingsApp, key);
-}
-
 export function getBranchStatusByBranchLike(
   state: Store,
   component: string,
index 22ea8367d4d6ea5dda180e0b2456234957da0f16..415ae2a4761315ba954107aca6dd1b2fe3b37ba0 100644 (file)
@@ -45,8 +45,13 @@ export const enum SettingsKey {
   PluginRiskConsent = 'sonar.plugins.risk.consent'
 }
 
+export type SettingDefinitionAndValue = {
+  definition: ExtendedSettingDefinition;
+  settingValue?: SettingValue;
+};
+
 export type Setting = SettingValue & { definition: SettingDefinition; hasValue: boolean };
-export type SettingWithCategory = Setting & { definition: SettingCategoryDefinition };
+export type SettingWithCategory = Setting & { definition: ExtendedSettingDefinition };
 
 export enum SettingType {
   STRING = 'STRING',
@@ -75,7 +80,7 @@ export interface SettingFieldDefinition extends SettingDefinition {
   name: string;
 }
 
-export interface SettingCategoryDefinition extends SettingDefinition {
+export interface ExtendedSettingDefinition extends SettingDefinition {
   category: string;
   defaultValue?: string;
   deprecatedKey?: string;