]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12500 Bug fix : can't update language's setting at the project level
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Wed, 20 Nov 2019 08:06:17 +0000 (09:06 +0100)
committerSonarTech <sonartech@sonarsource.com>
Mon, 25 Nov 2019 14:12:41 +0000 (15:12 +0100)
server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx
server/sonar-web/src/main/js/apps/settings/components/AnalysisScope.tsx
server/sonar-web/src/main/js/apps/settings/components/AppContainer.tsx
server/sonar-web/src/main/js/apps/settings/components/Languages.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/AdditionalCategories-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/__tests__/AnalysisScope-test.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/AppContainer-test.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/Languages-test.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AdditionalCategories-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AppContainer-test.tsx.snap

index 58adf6ca8b3260c3e7318fcca22fb175bd416bfb..072620bac6fc3753e23ae56ac6e5398b5b82a676 100644 (file)
@@ -34,17 +34,17 @@ import PullRequestDecoration from './pullRequestDecoration/PullRequestDecoration
 import PullRequestDecorationBinding from './pullRequestDecorationBinding/PRDecorationBinding';
 
 export interface AdditionalCategoryComponentProps {
-  parentComponent: T.Component | undefined;
+  component: T.Component | undefined;
   selectedCategory: string;
 }
 
 export interface AdditionalCategory {
-  key: string;
-  name: string;
-  renderComponent: (props: AdditionalCategoryComponentProps) => React.ReactNode;
   availableGlobally: boolean;
   availableForProject: boolean;
   displayTab: boolean;
+  key: string;
+  name: string;
+  renderComponent: (props: AdditionalCategoryComponentProps) => React.ReactNode;
   requiresBranchesEnabled?: boolean;
 }
 
@@ -110,6 +110,5 @@ function getPullRequestDecorationComponent() {
 }
 
 function getPullRequestDecorationBindingComponent(props: AdditionalCategoryComponentProps) {
-  const { parentComponent } = props;
-  return parentComponent && <PullRequestDecorationBinding component={parentComponent} />;
+  return props.component && <PullRequestDecorationBinding component={props.component} />;
 }
index 4bdf1167b2f02f8ad9bd2d161520fe6e3dff9c89..2097da92bc44bd1de3fffb8dd1f052cae3b10c87 100644 (file)
@@ -25,7 +25,7 @@ import { AdditionalCategoryComponentProps } from './AdditionalCategories';
 import CategoryDefinitionsList from './CategoryDefinitionsList';
 
 export function AnalysisScope(props: AdditionalCategoryComponentProps) {
-  const { parentComponent, selectedCategory } = props;
+  const { component, selectedCategory } = props;
 
   return (
     <>
@@ -56,7 +56,7 @@ export function AnalysisScope(props: AdditionalCategoryComponentProps) {
       </table>
 
       <div className="settings-sub-category">
-        <CategoryDefinitionsList category={selectedCategory} component={parentComponent} />
+        <CategoryDefinitionsList category={selectedCategory} component={component} />
       </div>
     </>
   );
index 86a8dd1d1136c72d7e0af4edc833a24ce21ef36f..df745050276106ae739c31619ed3271cebed3281 100644 (file)
@@ -109,7 +109,7 @@ export class App extends React.PureComponent<Props & WithRouterProps, State> {
           <div className="side-tabs-main">
             {foundAdditionalCategory && shouldRenderAdditionalCategory ? (
               foundAdditionalCategory.renderComponent({
-                parentComponent: this.props.component,
+                component: this.props.component,
                 selectedCategory: originalCategory
               })
             ) : (
index 35a88f9280a15c3e783015a3b87520d74883971b..7ec8a55a8cb96e1bfdbd4956313be64a35f396c9 100644 (file)
@@ -25,102 +25,80 @@ import { translate } from 'sonar-ui-common/helpers/l10n';
 import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
 import { getSettingsAppAllCategories, Store } from '../../../store/rootReducer';
 import { getCategoryName } from '../utils';
+import { AdditionalCategoryComponentProps } from './AdditionalCategories';
 import { LANGUAGES_CATEGORY } from './AdditionalCategoryKeys';
 import CategoryDefinitionsList from './CategoryDefinitionsList';
 import { CATEGORY_OVERRIDES } from './CategoryOverrides';
 
-export interface LanguagesProps {
+export interface LanguagesProps extends AdditionalCategoryComponentProps {
   categories: string[];
-  component?: T.Component;
   location: Location;
-  selectedCategory: string;
   router: Router;
 }
 
-interface LanguagesState {
-  availableLanguages: SelectOption[];
-  selectedLanguage: string | undefined;
-}
-
 interface SelectOption {
   label: string;
   originalValue: string;
   value: string;
 }
 
-export class Languages extends React.PureComponent<LanguagesProps, LanguagesState> {
-  constructor(props: LanguagesProps) {
-    super(props);
-
-    this.state = {
-      availableLanguages: [],
-      selectedLanguage: undefined
-    };
-  }
-
-  componentDidMount() {
-    const { selectedCategory, categories } = this.props;
-    const lowerCasedLanguagesCategory = LANGUAGES_CATEGORY.toLowerCase();
-    const lowerCasedSelectedCategory = selectedCategory.toLowerCase();
-
-    const availableLanguages = categories
-      .filter(c => CATEGORY_OVERRIDES[c.toLowerCase()] === lowerCasedLanguagesCategory)
-      .map(c => ({
-        label: getCategoryName(c),
-        value: c.toLowerCase(),
-        originalValue: c
-      }));
-
-    let selectedLanguage = undefined;
-
-    if (
-      lowerCasedSelectedCategory !== lowerCasedLanguagesCategory &&
-      availableLanguages.find(c => c.value === lowerCasedSelectedCategory)
-    ) {
-      selectedLanguage = lowerCasedSelectedCategory;
-    }
-
-    this.setState({
-      availableLanguages,
-      selectedLanguage
-    });
-  }
-
-  handleOnChange = (newOption: SelectOption) => {
-    this.setState({ selectedLanguage: newOption.value });
-
-    const { location, router } = this.props;
+export function Languages(props: LanguagesProps) {
+  const { categories, component, location, router, selectedCategory } = props;
+  const { availableLanguages, selectedLanguage } = getLanguages(categories, selectedCategory);
 
+  const handleOnChange = (newOption: SelectOption) => {
     router.push({
       ...location,
       query: { ...location.query, category: newOption.originalValue }
     });
   };
 
-  render() {
-    const { component } = this.props;
-    const { availableLanguages, selectedLanguage } = this.state;
-
-    return (
-      <>
-        <h2 className="settings-sub-category-name">{translate('property.category.languages')}</h2>
-        <div data-test="language-select">
-          <Select
-            className="input-large"
-            onChange={this.handleOnChange}
-            options={availableLanguages}
-            placeholder={translate('settings.languages.select_a_language_placeholder')}
-            value={selectedLanguage}
-          />
+  return (
+    <>
+      <h2 className="settings-sub-category-name">{translate('property.category.languages')}</h2>
+      <div data-test="language-select">
+        <Select
+          className="input-large"
+          onChange={handleOnChange}
+          options={availableLanguages}
+          placeholder={translate('settings.languages.select_a_language_placeholder')}
+          value={selectedLanguage}
+        />
+      </div>
+      {selectedLanguage && (
+        <div className="settings-sub-category">
+          <CategoryDefinitionsList category={selectedLanguage} component={component} />
         </div>
-        {selectedLanguage && (
-          <div className="settings-sub-category">
-            <CategoryDefinitionsList category={selectedLanguage} component={component} />
-          </div>
-        )}
-      </>
-    );
+      )}
+    </>
+  );
+}
+
+function getLanguages(categories: string[], selectedCategory: string) {
+  const lowerCasedLanguagesCategory = LANGUAGES_CATEGORY.toLowerCase();
+  const lowerCasedSelectedCategory = selectedCategory.toLowerCase();
+
+  const availableLanguages = categories
+    .filter(c => CATEGORY_OVERRIDES[c.toLowerCase()] === lowerCasedLanguagesCategory)
+    .map(c => ({
+      label: getCategoryName(c),
+      value: c.toLowerCase(),
+      originalValue: c
+    }));
+
+  let selectedLanguage = undefined;
+
+  if (
+    lowerCasedSelectedCategory !== lowerCasedLanguagesCategory &&
+    availableLanguages.find(c => c.value === lowerCasedSelectedCategory)
+  ) {
+    selectedLanguage = lowerCasedSelectedCategory;
   }
+
+  return {
+    availableLanguages,
+    selectedLanguage
+  };
 }
 
 export default withRouter(
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/AdditionalCategories-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/AdditionalCategories-test.tsx
new file mode 100644 (file)
index 0000000..77afccd
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { find } from 'lodash';
+import { mockComponent } from '../../../../helpers/testMocks';
+import { ADDITIONAL_CATEGORIES } from '../AdditionalCategories';
+import { PULL_REQUEST_DECORATION_BINDING_CATEGORY } from '../AdditionalCategoryKeys';
+
+it('should render additional categories component correctly', () => {
+  ADDITIONAL_CATEGORIES.forEach(cat => {
+    expect(
+      cat.renderComponent({
+        component: mockComponent(),
+        selectedCategory: 'TEST'
+      })
+    ).toMatchSnapshot();
+  });
+});
+
+it('should not render pull request decoration binding component when the component is not defined', () => {
+  const category = find(
+    ADDITIONAL_CATEGORIES,
+    c => c.key === PULL_REQUEST_DECORATION_BINDING_CATEGORY
+  );
+
+  if (!category) {
+    fail('category should be defined');
+  } else {
+    expect(
+      category.renderComponent({ component: undefined, selectedCategory: '' })
+    ).toBeUndefined();
+  }
+});
index 3711f71960bc9360ef8061ed65c478b560fef10f..df679218c1cf7af66e13cb4b2e83ca65f72160f5 100644 (file)
@@ -28,5 +28,5 @@ it('should render correctly', () => {
 });
 
 function shallowRender() {
-  return shallow(<AnalysisScope parentComponent={mockComponent()} selectedCategory="TEST" />);
+  return shallow(<AnalysisScope component={mockComponent()} selectedCategory="TEST" />);
 }
index b7c605015b9e47f72bb797482910e64e2efbdb1d..a0f3990d0f64c584861dc1ee29ccd1980e810219 100644 (file)
@@ -24,7 +24,9 @@ import { mockLocation, mockRouter } from '../../../../helpers/testMocks';
 import {
   ANALYSIS_SCOPE_CATEGORY,
   LANGUAGES_CATEGORY,
-  NEW_CODE_PERIOD_CATEGORY
+  NEW_CODE_PERIOD_CATEGORY,
+  PULL_REQUEST_DECORATION_BINDING_CATEGORY,
+  PULL_REQUEST_DECORATION_CATEGORY
 } from '../AdditionalCategoryKeys';
 import { App } from '../AppContainer';
 
@@ -62,6 +64,24 @@ it('should render analysis scope correctly', async () => {
   expect(wrapper).toMatchSnapshot();
 });
 
+it('should render pull request decoration correctly', async () => {
+  const wrapper = shallowRender({
+    location: mockLocation({ query: { category: PULL_REQUEST_DECORATION_CATEGORY } })
+  });
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render pull request decoration binding correctly', async () => {
+  const wrapper = shallowRender({
+    location: mockLocation({ query: { category: PULL_REQUEST_DECORATION_BINDING_CATEGORY } })
+  });
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
+
 function shallowRender(props: Partial<App['props']> = {}) {
   return shallow(
     <App
index 1ab345b09e9e81f606b7089113d8b8a0d801fb1b..738b13c1693e8abb4d50cc4acd93f8a726ddee05 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 { shallow } from 'enzyme';
 import * as React from 'react';
-import { mockLocation, mockRouter } from '../../../../helpers/testMocks';
+import Select from 'sonar-ui-common/components/controls/Select';
+import { mockComponent, mockLocation, mockRouter } from '../../../../helpers/testMocks';
+import CategoryDefinitionsList from '../CategoryDefinitionsList';
 import { Languages, LanguagesProps } from '../Languages';
 
 it('should render correctly', () => {
@@ -36,17 +39,29 @@ it('should correctly handle a change of the selected language', () => {
   const push = jest.fn();
   const router = mockRouter({ push });
   const wrapper = shallowRender({ router });
-  expect(wrapper.state().selectedLanguage).toBe('java');
+  expect(wrapper.find(CategoryDefinitionsList).props().category).toBe('java');
+
+  const { onChange } = wrapper.find(Select).props();
+
+  if (!onChange) {
+    fail('onChange should be defined');
+  } else {
+    onChange({ label: '', originalValue: 'CoBoL', value: 'cobol' });
+    expect(push).toHaveBeenCalledWith(expect.objectContaining({ query: { category: 'CoBoL' } }));
+  }
+});
 
-  wrapper.instance().handleOnChange({ label: '', originalValue: 'CoBoL', value: 'cobol' });
-  expect(wrapper.state().selectedLanguage).toBe('cobol');
-  expect(push).toHaveBeenCalledWith(expect.objectContaining({ query: { category: 'CoBoL' } }));
+it('should correctly show the subcategory for a component', () => {
+  const component = mockComponent();
+  const wrapper = shallowRender({ component });
+  expect(wrapper.find(CategoryDefinitionsList).props().component).toBe(component);
 });
 
 function shallowRender(props: Partial<LanguagesProps> = {}) {
-  return shallow<Languages>(
+  return shallow(
     <Languages
       categories={['Java', 'JavaScript', 'COBOL']}
+      component={undefined}
       location={mockLocation()}
       router={mockRouter()}
       selectedCategory="java"
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AdditionalCategories-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AdditionalCategories-test.tsx.snap
new file mode 100644 (file)
index 0000000..2a421ce
--- /dev/null
@@ -0,0 +1,91 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render additional categories component correctly 1`] = `
+<withRouter(Connect(Languages))
+  component={
+    Object {
+      "breadcrumbs": Array [],
+      "key": "my-project",
+      "name": "MyProject",
+      "organization": "foo",
+      "qualifier": "TRK",
+      "qualityGate": Object {
+        "isDefault": true,
+        "key": "30",
+        "name": "Sonar way",
+      },
+      "qualityProfiles": Array [
+        Object {
+          "deleted": false,
+          "key": "my-qp",
+          "language": "ts",
+          "name": "Sonar way",
+        },
+      ],
+      "tags": Array [],
+    }
+  }
+  selectedCategory="TEST"
+/>
+`;
+
+exports[`should render additional categories component correctly 2`] = `<NewCodePeriod />`;
+
+exports[`should render additional categories component correctly 3`] = `
+<AnalysisScope
+  component={
+    Object {
+      "breadcrumbs": Array [],
+      "key": "my-project",
+      "name": "MyProject",
+      "organization": "foo",
+      "qualifier": "TRK",
+      "qualityGate": Object {
+        "isDefault": true,
+        "key": "30",
+        "name": "Sonar way",
+      },
+      "qualityProfiles": Array [
+        Object {
+          "deleted": false,
+          "key": "my-qp",
+          "language": "ts",
+          "name": "Sonar way",
+        },
+      ],
+      "tags": Array [],
+    }
+  }
+  selectedCategory="TEST"
+/>
+`;
+
+exports[`should render additional categories component correctly 4`] = `<PullRequestDecoration />`;
+
+exports[`should render additional categories component correctly 5`] = `
+<PRDecorationBinding
+  component={
+    Object {
+      "breadcrumbs": Array [],
+      "key": "my-project",
+      "name": "MyProject",
+      "organization": "foo",
+      "qualifier": "TRK",
+      "qualityGate": Object {
+        "isDefault": true,
+        "key": "30",
+        "name": "Sonar way",
+      },
+      "qualityProfiles": Array [
+        Object {
+          "deleted": false,
+          "key": "my-qp",
+          "language": "ts",
+          "name": "Sonar way",
+        },
+      ],
+      "tags": Array [],
+    }
+  }
+/>
+`;
index df09da04ef259f334afb219625b62dafa1adf08d..d3f5cdc9a8b53556fa25627489af1b6ee3509a93 100644 (file)
@@ -141,3 +141,73 @@ exports[`should render newCodePeriod correctly 1`] = `
   </div>
 </div>
 `;
+
+exports[`should render pull request decoration binding correctly 1`] = `
+<div
+  className="page page-limited"
+  id="settings-page"
+>
+  <Suggestions
+    suggestions="settings"
+  />
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="settings.page"
+  />
+  <PageHeader />
+  <div
+    className="side-tabs-layout settings-layout"
+  >
+    <div
+      className="side-tabs-side"
+    >
+      <Connect(CategoriesList)
+        defaultCategory="general"
+        selectedCategory="pull_request_decoration_binding"
+      />
+    </div>
+    <div
+      className="side-tabs-main"
+    >
+      <Connect(SubCategoryDefinitionsList)
+        category="pull_request_decoration_binding"
+      />
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render pull request decoration correctly 1`] = `
+<div
+  className="page page-limited"
+  id="settings-page"
+>
+  <Suggestions
+    suggestions="settings"
+  />
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="settings.page"
+  />
+  <PageHeader />
+  <div
+    className="side-tabs-layout settings-layout"
+  >
+    <div
+      className="side-tabs-side"
+    >
+      <Connect(CategoriesList)
+        defaultCategory="general"
+        selectedCategory="pull_request_decoration"
+      />
+    </div>
+    <div
+      className="side-tabs-main"
+    >
+      <PullRequestDecoration />
+    </div>
+  </div>
+</div>
+`;