]> source.dussan.org Git - sonarqube.git/commitdiff
SONARCLOUD-578 import all checkbox
authorSiegfried Ehret <49895321+siegfried-ehret-sonarsource@users.noreply.github.com>
Mon, 29 Apr 2019 14:46:35 +0000 (16:46 +0200)
committerSonarTech <sonartech@sonarsource.com>
Mon, 29 Apr 2019 18:21:08 +0000 (20:21 +0200)
* Fix redux devtools warning
* Add checkbox to import all repositories

server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/style.css
server/sonar-web/src/main/js/store/utils/configureStore.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 6451e98e024e75587014e99f86ec53665e527d39..e1a6e2736a1dfa436a2bf9f15dd8e7f26a733bfa 100644 (file)
  */
 import * as React from 'react';
 import * as classNames from 'classnames';
+import { keyBy } from 'lodash';
 import AlmRepositoryItem from './AlmRepositoryItem';
 import SetupProjectBox from './SetupProjectBox';
 import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import Checkbox from '../../../components/controls/Checkbox';
 import SearchBox from '../../../components/controls/SearchBox';
 import UpgradeOrganizationBox from '../components/UpgradeOrganizationBox';
 import { Alert } from '../../../components/ui/Alert';
 import { getRepositories } from '../../../api/alm-integration';
-import { isDefined } from '../../../helpers/types';
 import { translateWithParameters, translate } from '../../../helpers/l10n';
+import { isPaidOrganization } from '../../../helpers/organizations';
+import { isDefined } from '../../../helpers/types';
 
 interface Props {
   almApplication: T.AlmApplication;
@@ -39,6 +42,7 @@ interface Props {
 type SelectedRepositories = T.Dict<T.AlmRepository | undefined>;
 
 interface State {
+  checkAllRepositories: boolean;
   highlight: boolean;
   loading: boolean;
   repositories: T.AlmRepository[];
@@ -50,6 +54,7 @@ interface State {
 export default class RemoteRepositories extends React.PureComponent<Props, State> {
   mounted = false;
   state: State = {
+    checkAllRepositories: false,
     highlight: false,
     loading: true,
     repositories: [],
@@ -91,6 +96,9 @@ export default class RemoteRepositories extends React.PureComponent<Props, State
     );
   };
 
+  filterBySearch = (repo: T.AlmRepository) =>
+    repo.label.toLowerCase().includes(this.state.search.toLowerCase());
+
   handleHighlightUpgradeBox = (highlight: boolean) => {
     this.setState({ highlight });
   };
@@ -120,7 +128,31 @@ export default class RemoteRepositories extends React.PureComponent<Props, State
   };
 
   handleSearch = (search: string) => {
-    this.setState({ search });
+    this.setState({ search, checkAllRepositories: false, selectedRepositories: {} });
+  };
+
+  onCheckAllRepositories = () => {
+    this.setState(({ checkAllRepositories, repositories, search }) => {
+      const { organization } = this.props;
+
+      const isPaidOrg = isPaidOrganization(organization);
+      const filterByPlan = (repo: T.AlmRepository) => (isPaidOrg ? true : !repo.private);
+
+      const nextState = {
+        selectedRepositories: {},
+        checkAllRepositories: !checkAllRepositories
+      };
+
+      if (nextState.checkAllRepositories) {
+        const validRepositories = (search
+          ? repositories.filter(this.filterBySearch)
+          : repositories
+        ).filter(filterByPlan);
+        nextState.selectedRepositories = keyBy(validRepositories, 'installationKey');
+      }
+
+      return nextState;
+    });
   };
 
   toggleRepository = (repository: T.AlmRepository) => {
@@ -137,27 +169,38 @@ export default class RemoteRepositories extends React.PureComponent<Props, State
   render() {
     const { highlight, loading, repositories, search, selectedRepositories } = this.state;
     const { almApplication, organization } = this.props;
-    const isPaidOrg = organization.subscription === 'PAID';
+    const isPaidOrg = isPaidOrganization(organization);
     const hasPrivateRepositories = repositories.some(repository => Boolean(repository.private));
     const showSearchBox = repositories.length > 5;
+    const showCheckAll = repositories.length > 1;
     const showUpgradebox =
       !isPaidOrg && hasPrivateRepositories && organization.actions && organization.actions.admin;
-    const filteredRepositories = repositories.filter(
-      repo => !search || repo.label.toLowerCase().includes(search.toLowerCase())
-    );
+    const filteredRepositories = search ? repositories.filter(this.filterBySearch) : repositories;
+
     return (
       <div className="create-project">
         <div className="flex-1 huge-spacer-right">
-          {showSearchBox && (
-            <div className="spacer-bottom">
+          <div className="spacer-bottom create-project-actions">
+            <div>
+              {showCheckAll && (
+                <Checkbox
+                  checked={this.state.checkAllRepositories}
+                  disabled={filteredRepositories.length === 0}
+                  onCheck={this.onCheckAllRepositories}>
+                  {translate('onboarding.create_project.select_all_repositories')}
+                </Checkbox>
+              )}
+            </div>
+            {showSearchBox && (
               <SearchBox
                 minLength={1}
                 onChange={this.handleSearch}
                 placeholder={translate('search.search_for_repositories')}
                 value={this.state.search}
               />
-            </div>
-          )}
+            )}
+          </div>
+
           {this.state.successfullyUpgraded && (
             <Alert variant="success">
               {translateWithParameters(
index 025209632ceb83e590cc1b378eea8790d0552be1..a4e7c3d0455ab221fd93f59827f311ec8e37fafd 100644 (file)
@@ -33,9 +33,7 @@ jest.mock('../../../../api/alm-integration', () => ({
     repositories: [
       {
         label: 'Cool Project',
-        installationKey: 'github/cool',
-        linkedProjectKey: 'proj_cool',
-        linkedProjectName: 'Proj Cool'
+        installationKey: 'github/cool'
       },
       { label: 'Awesome Project', installationKey: 'github/awesome' }
     ]
@@ -102,6 +100,41 @@ it('should display a search box to filter repositories', async () => {
   expect(wrapper.find('AlmRepositoryItem')).toHaveLength(1);
 });
 
+it('should allow to select all repositories', async () => {
+  (getRepositories as jest.Mock<any>).mockResolvedValueOnce({
+    repositories: times(6, i => ({ label: `Project ${i}`, installationKey: `key-${i}` }))
+  });
+
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+
+  expect(wrapper.find('Checkbox')).toHaveLength(1);
+  expect(wrapper.state('checkAllRepositories')).toBe(false);
+  expect(wrapper.state('selectedRepositories')).toEqual({});
+});
+
+it('should select all repositories', async () => {
+  (getRepositories as jest.Mock<any>).mockResolvedValueOnce({
+    repositories: times(6, i => ({ label: `Project ${i}`, installationKey: `key-${i}` }))
+  });
+
+  const wrapper = shallowRender();
+  const instance = wrapper.instance() as RemoteRepositories;
+  await waitAndUpdate(wrapper);
+
+  instance.onCheckAllRepositories();
+  await waitAndUpdate(wrapper);
+
+  expect(wrapper.state('checkAllRepositories')).toBe(true);
+  expect(wrapper.state('selectedRepositories')).toMatchSnapshot();
+
+  instance.onCheckAllRepositories();
+  await waitAndUpdate(wrapper);
+
+  expect(wrapper.state('checkAllRepositories')).toBe(false);
+  expect(wrapper.state('selectedRepositories')).toEqual({});
+});
+
 function shallowRender(props: Partial<RemoteRepositories['props']> = {}) {
   return shallow(
     <RemoteRepositories
index ed810b7ac31b2450613a6503ca61f00e4f9f4d73..cc41eb68a891b1fec7b206c6309ed6bcb2065dfb 100644 (file)
@@ -7,6 +7,11 @@ exports[`should display the list of repositories 1`] = `
   <div
     className="flex-1 huge-spacer-right"
   >
+    <div
+      className="spacer-bottom create-project-actions"
+    >
+      <div />
+    </div>
     <DeferredSpinner
       loading={true}
       timeout={100}
@@ -55,6 +60,20 @@ exports[`should display the list of repositories 2`] = `
   <div
     className="flex-1 huge-spacer-right"
   >
+    <div
+      className="spacer-bottom create-project-actions"
+    >
+      <div>
+        <Checkbox
+          checked={false}
+          disabled={false}
+          onCheck={[Function]}
+          thirdState={false}
+        >
+          onboarding.create_project.select_all_repositories
+        </Checkbox>
+      </div>
+    </div>
     <DeferredSpinner
       loading={false}
       timeout={100}
@@ -77,8 +96,6 @@ exports[`should display the list of repositories 2`] = `
             Object {
               "installationKey": "github/cool",
               "label": "Cool Project",
-              "linkedProjectKey": "proj_cool",
-              "linkedProjectName": "Proj Cool",
             }
           }
           selected={false}
@@ -158,3 +175,32 @@ exports[`should display the organization upgrade box 1`] = `
   }
 />
 `;
+
+exports[`should select all repositories 1`] = `
+Object {
+  "key-0": Object {
+    "installationKey": "key-0",
+    "label": "Project 0",
+  },
+  "key-1": Object {
+    "installationKey": "key-1",
+    "label": "Project 1",
+  },
+  "key-2": Object {
+    "installationKey": "key-2",
+    "label": "Project 2",
+  },
+  "key-3": Object {
+    "installationKey": "key-3",
+    "label": "Project 3",
+  },
+  "key-4": Object {
+    "installationKey": "key-4",
+    "label": "Project 4",
+  },
+  "key-5": Object {
+    "installationKey": "key-5",
+    "label": "Project 5",
+  },
+}
+`;
index 1c394a4ea7834939825a561965a34befb1d87a10..2bc6012245876a6d4a7233b2fd5d1c3bc0bccc60 100644 (file)
   border-color: var(--sonarcloudBlue600);
   background-color: var(--sonarcloudBlue600);
 }
+
+.create-project-actions {
+  min-width: 500px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 0 8px 16px;
+  border-left: 1px solid transparent;
+}
+
+.create-project-actions .icon-checkbox {
+  margin-right: 8px;
+}
index 2d560719467f2a127fa43b2fe67c8a6ff5210009..f163fd5f1e1b32a38b3fa9b64705752cad14654a 100644 (file)
@@ -30,8 +30,8 @@ if (process.env.NODE_ENV === 'development') {
   const { createLogger } = require('redux-logger');
   middlewares.push(createLogger());
 
-  const { devToolsExtension } = window as any;
-  composed.push(devToolsExtension ? devToolsExtension() : (f: Function) => f);
+  const { __REDUX_DEVTOOLS_EXTENSION__ } = window as any;
+  composed.push(__REDUX_DEVTOOLS_EXTENSION__ ? __REDUX_DEVTOOLS_EXTENSION__() : (f: Function) => f);
 }
 
 const finalCreateStore = compose(
index 7fb78674797620a1f3a71d69eb286fa335457d48..11bb38d2367286332a71b7f5146eb91bebe52739 100644 (file)
@@ -2824,6 +2824,7 @@ onboarding.create_project.display_name.help=Some scanners might override the val
 onboarding.create_project.repository_imported=Already imported: {link}
 onboarding.create_project.see_project=See the project
 onboarding.create_project.select_repositories=Select repositories
+onboarding.create_project.select_all_repositories=Select all repositories
 onboarding.create_project.subscribe_to_import_private_repositories=You need to subscribe your organization to a paid plan to import private projects
 onboarding.create_project.encourage_to_subscribe=Subscribe your organization to our paid plan to get unlimited private projects.
 onboarding.create_project.subscribtion_success_x={0} has been successfully upgraded to paid plan. You can now import and analyze private projects.