]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20708 Update New Code period page to support bulk import
authorguillaume-peoch-sonarsource <guillaume.peoch@sonarsource.com>
Wed, 11 Oct 2023 09:04:43 +0000 (11:04 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 18 Oct 2023 20:03:05 +0000 (20:03 +0000)
13 files changed:
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreateRenderer.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/GitHub-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx
server/sonar-web/src/main/js/apps/create/project/components/AlmRepoItem.tsx
server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx
server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index a6cb0f013fd1d64fdd44756b9b0379a5e40da23c..ead4d43953663c5327c6e72f4646a30aeb3644c0 100644 (file)
@@ -57,6 +57,7 @@ interface State {
   gitlabSettings: AlmSettingsInstance[];
   loading: boolean;
   creatingAlmDefinition?: AlmKeys;
+  nbrOfProjects?: number;
 }
 
 const PROJECT_MODE_FOR_ALM_KEY = {
@@ -137,10 +138,12 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
     this.setState({ creatingAlmDefinition: alm });
   };
 
-  handleProjectSetupDone = (createProject: CreateProjectApiCallback) => {
+  handleProjectSetupDone = (createProject: CreateProjectApiCallback, nbrOfProjects?: number) => {
     const { location, router } = this.props;
     this.createProjectFnRef = createProject;
 
+    this.setState({ nbrOfProjects });
+
     location.query.setncd = 'true';
     router.push(location);
   };
@@ -273,7 +276,7 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
 
   render() {
     const { location, router } = this.props;
-    const { creatingAlmDefinition } = this.state;
+    const { creatingAlmDefinition, nbrOfProjects } = this.state;
     const mode: CreateProjectModes | undefined = location.query?.mode;
     const isProjectSetupDone = location.query?.setncd === 'true';
     const gridLayoutStyle = mode ? 'sw-col-start-2 sw-col-span-10' : 'sw-col-span-12';
@@ -297,6 +300,7 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
             <NewCodeDefinitionSelection
               router={router}
               createProjectFnRef={this.createProjectFnRef}
+              numberOfProjects={nbrOfProjects}
             />
           </div>
 
index 70ca875fe663c5472f270fce0fc527e5b5fe6f5d..67e742ceae294f6b7eb09e619927948ff7195ee3 100644 (file)
@@ -37,7 +37,7 @@ import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer';
 interface Props {
   canAdmin: boolean;
   loadingBindings: boolean;
-  onProjectSetupDone: (createProject: CreateProjectApiCallback) => void;
+  onProjectSetupDone: (createProject: CreateProjectApiCallback, nbrOfProjects: number) => void;
   almInstances: AlmSettingsInstance[];
   location: Location;
   router: Router;
@@ -259,16 +259,17 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
     }
   };
 
-  handleImportRepository = (repoKey: string) => {
+  handleImportRepository = (repoKeys: string[]) => {
     const { selectedOrganization, selectedAlmInstance } = this.state;
 
-    if (selectedAlmInstance && selectedOrganization && repoKey !== '') {
+    if (selectedAlmInstance && selectedOrganization && repoKeys.length > 0) {
       this.props.onProjectSetupDone(
         setupGithubProjectCreation({
           almSetting: selectedAlmInstance.key,
           organization: selectedOrganization.key,
-          repositoryKey: repoKey,
+          repositoryKey: repoKeys.join(','), // TBD
         }),
+        repoKeys.length,
       );
     }
   };
index 35847eb760e538a5e480867e5a2479339728e871..df96843b995f67328eb77a7ff210f53141ef9aed 100644 (file)
@@ -52,7 +52,7 @@ export interface GitHubProjectCreateRendererProps {
   loadingBindings: boolean;
   loadingOrganizations: boolean;
   loadingRepositories: boolean;
-  onImportRepository: (key: string) => void;
+  onImportRepository: (key: string[]) => void;
   onLoadMore: () => void;
   onSearch: (q: string) => void;
   onSelectOrganization: (key: string) => void;
@@ -183,7 +183,7 @@ export default function GitHubProjectCreateRenderer(props: GitHubProjectCreateRe
   }
 
   const handleImport = () => {
-    props.onImportRepository(Array.from(selected).toString()); // TBD
+    props.onImportRepository(Array.from(selected));
   };
 
   const handleCheckAll = () => {
index b5b52e25470f4f2542835071bc6c2181292dcd78..a3f008a94dddbd1620ae9b596240493741c8015b 100644 (file)
@@ -116,13 +116,13 @@ it('should show import project feature when PAT is already set', async () => {
   await user.click(importButton);
 
   expect(
-    screen.getByRole('heading', { name: 'onboarding.create_project.new_code_definition.title' }),
+    screen.getByRole('heading', { name: 'onboarding.create_x_project.new_code_definition.title1' }),
   ).toBeInTheDocument();
 
   await user.click(screen.getByRole('radio', { name: 'new_code_definition.global_setting' }));
   await user.click(
     screen.getByRole('button', {
-      name: 'onboarding.create_project.new_code_definition.create_project',
+      name: 'onboarding.create_project.new_code_definition.create_x_projects1',
     }),
   );
 
index 85b5ad18a8fc6ca67a056ec7b7ad6a1e3ff39b16..51a28d6a081e528dc81299fc3422dc3533fe4d07 100644 (file)
@@ -136,13 +136,13 @@ it('should show import project feature when PAT is already set', async () => {
   await user.click(importButton);
 
   expect(
-    screen.getByRole('heading', { name: 'onboarding.create_project.new_code_definition.title' }),
+    screen.getByRole('heading', { name: 'onboarding.create_x_project.new_code_definition.title1' }),
   ).toBeInTheDocument();
 
   await user.click(screen.getByRole('radio', { name: 'new_code_definition.global_setting' }));
   await user.click(
     screen.getByRole('button', {
-      name: 'onboarding.create_project.new_code_definition.create_project',
+      name: 'onboarding.create_project.new_code_definition.create_x_projects1',
     }),
   );
 
index d7644408fa7c891248b62ce80dc9afb27ea73f67..31cc747648e823756428ee25ba780d12528a6581 100644 (file)
@@ -152,13 +152,13 @@ it('should show import project feature when PAT is already set', async () => {
   await user.click(setupButton);
 
   expect(
-    screen.getByRole('heading', { name: 'onboarding.create_project.new_code_definition.title' }),
+    screen.getByRole('heading', { name: 'onboarding.create_x_project.new_code_definition.title1' }),
   ).toBeInTheDocument();
 
   await user.click(screen.getByRole('radio', { name: 'new_code_definition.global_setting' }));
   await user.click(
     screen.getByRole('button', {
-      name: 'onboarding.create_project.new_code_definition.create_project',
+      name: 'onboarding.create_project.new_code_definition.create_x_projects1',
     }),
   );
 
index 4311a26b0543c8b9e70304609f0b33b42139f120..c80ae871168554ebb70642782f1f640e39bc675f 100644 (file)
@@ -52,9 +52,18 @@ const ui = {
   project3Checkbox: byRole('listitem', { name: 'Github repo 3' }).byRole('checkbox'),
   checkAll: byRole('checkbox', { name: 'onboarding.create_project.select_all_repositories' }),
   importButton: byRole('button', { name: 'onboarding.create_project.import' }),
-  newCodeTitle: byRole('heading', { name: 'onboarding.create_project.new_code_definition.title' }),
+  newCodeTitle: byRole('heading', {
+    name: 'onboarding.create_x_project.new_code_definition.title1',
+  }),
+  newCodeMultipleProjectTitle: byRole('heading', {
+    name: 'onboarding.create_x_project.new_code_definition.title2',
+  }),
+  changePeriodLaterInfo: byText('onboarding.create_projects.new_code_definition.change_info'),
   createProjectButton: byRole('button', {
-    name: 'onboarding.create_project.new_code_definition.create_project',
+    name: 'onboarding.create_project.new_code_definition.create_x_projects1',
+  }),
+  createProjectsButton: byRole('button', {
+    name: 'onboarding.create_project.new_code_definition.create_x_projects2',
   }),
   globalSettingRadio: byRole('radio', { name: 'new_code_definition.global_setting' }),
 };
@@ -202,9 +211,12 @@ it('should import several projects', async () => {
   expect(ui.importButton.get()).toBeInTheDocument();
   await user.click(ui.importButton.get());
 
-  expect(await ui.newCodeTitle.find()).toBeInTheDocument();
+  expect(await ui.newCodeMultipleProjectTitle.find()).toBeInTheDocument();
+  expect(ui.changePeriodLaterInfo.get()).toBeInTheDocument();
+  expect(ui.createProjectsButton.get()).toBeDisabled();
 
-  // TBD
+  await user.click(ui.globalSettingRadio.get());
+  expect(ui.createProjectsButton.get()).toBeEnabled();
 });
 
 it('should show search filter when the authentication is successful', async () => {
index 442abc088b872aee13380f101e24ae986b73026a..3516ac4ef33f0a53c22a7f0ec9c8dbdfbe4b5401 100644 (file)
@@ -123,13 +123,13 @@ it('should show import project feature when PAT is already set', async () => {
   await user.click(importButton);
 
   expect(
-    screen.getByRole('heading', { name: 'onboarding.create_project.new_code_definition.title' }),
+    screen.getByRole('heading', { name: 'onboarding.create_x_project.new_code_definition.title1' }),
   ).toBeInTheDocument();
 
   await user.click(screen.getByRole('radio', { name: 'new_code_definition.global_setting' }));
   await user.click(
     screen.getByRole('button', {
-      name: 'onboarding.create_project.new_code_definition.create_project',
+      name: 'onboarding.create_project.new_code_definition.create_x_projects1',
     }),
   );
 
index 874dc3cc1a1b057fe3bebc3728d3c7228be66840..e54eb0096a7100c810267d9e98220319ba8c51fb 100644 (file)
@@ -53,10 +53,10 @@ const ui = {
     name: /onboarding.create_project.display_name/,
   }),
   projectNextButton: byRole('button', { name: 'next' }),
-  newCodeDefinitionHeader: byText('onboarding.create_project.new_code_definition.title'),
+  newCodeDefinitionHeader: byText('onboarding.create_x_project.new_code_definition.title1'),
   inheritGlobalNcdRadio: byRole('radio', { name: 'new_code_definition.global_setting' }),
   projectCreateButton: byRole('button', {
-    name: 'onboarding.create_project.new_code_definition.create_project',
+    name: 'onboarding.create_project.new_code_definition.create_x_projects1',
   }),
   overrideNcdRadio: byRole('radio', { name: 'new_code_definition.specific_setting' }),
   ncdOptionPreviousVersionRadio: byRole('radio', {
index 4ae059d21c13b7f2b2fa43e5a325eff818555da9..f604a644227bb8579998af228066599463879739 100644 (file)
@@ -70,7 +70,7 @@ export default function AlmRepoItem({
   onCheck,
   onImport,
 }: AlmRepoItemProps) {
-  const labelId = `${almKey.replace(/\s/g, '_')}-label`;
+  const labelId = `${almKey.toString().replace(/\s/g, '_')}-label`;
   return (
     <RepositoryItem
       selected={selected}
index b63dd993932d67a0ec9b0e69a0b9715f54803e6f..c9c56f65fa39803731bf41500570013508fa9d25 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 { ButtonPrimary, Link, Spinner, Title } from 'design-system';
+import { ButtonPrimary, ButtonSecondary, FlagMessage, Link, Spinner, Title } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
+import { useNavigate } from 'react-router-dom';
 import { Router } from '../../../../components/hoc/withRouter';
 import NewCodeDefinitionSelector from '../../../../components/new-code-definition/NewCodeDefinitionSelector';
 import { useDocUrl } from '../../../../helpers/docs';
@@ -32,16 +33,22 @@ import { CreateProjectApiCallback } from '../types';
 interface Props {
   createProjectFnRef: CreateProjectApiCallback | null;
   router: Router;
+  numberOfProjects?: number;
 }
 
 export default function NewCodeDefinitionSelection(props: Props) {
-  const { createProjectFnRef, router } = props;
+  const { createProjectFnRef, router, numberOfProjects } = props;
 
   const [submitting, setSubmitting] = React.useState(false);
   const [selectedDefinition, selectDefinition] = React.useState<NewCodeDefinitiondWithCompliance>();
 
+  const navigate = useNavigate();
+
   const getDocUrl = useDocUrl();
 
+  const isMultipleProjects = numberOfProjects !== undefined && numberOfProjects !== 1;
+  const projectCount = isMultipleProjects ? numberOfProjects : 1;
+
   const handleProjectCreation = React.useCallback(async () => {
     if (createProjectFnRef && selectedDefinition) {
       setSubmitting(true);
@@ -58,7 +65,15 @@ export default function NewCodeDefinitionSelection(props: Props) {
 
   return (
     <div id="project-ncd-selection" className="sw-body-sm">
-      <Title>{translate('onboarding.create_project.new_code_definition.title')}</Title>
+      <Title>
+        <FormattedMessage
+          defaultMessage={translate('onboarding.create_x_project.new_code_definition.title')}
+          id="onboarding.create_x_project.new_code_definition.title"
+          values={{
+            count: projectCount,
+          }}
+        />
+      </Title>
 
       <p className="sw-mb-2">
         <FormattedMessage
@@ -74,15 +89,35 @@ export default function NewCodeDefinitionSelection(props: Props) {
         />
       </p>
 
-      <NewCodeDefinitionSelector onNcdChanged={selectDefinition} />
+      <NewCodeDefinitionSelector
+        onNcdChanged={selectDefinition}
+        isMultipleProjects={isMultipleProjects}
+      />
+
+      {isMultipleProjects && (
+        <FlagMessage variant="info">
+          {translate('onboarding.create_projects.new_code_definition.change_info')}
+        </FlagMessage>
+      )}
 
       <div className="sw-mt-10 sw-mb-8">
+        <ButtonSecondary className="sw-mr-2" onClick={() => navigate(-1)}>
+          {translate('back')}
+        </ButtonSecondary>
         <ButtonPrimary
           onClick={handleProjectCreation}
           disabled={!selectedDefinition?.isCompliant || submitting}
           type="submit"
         >
-          {translate('onboarding.create_project.new_code_definition.create_project')}
+          <FormattedMessage
+            defaultMessage={translate(
+              'onboarding.create_project.new_code_definition.create_x_projects',
+            )}
+            id="onboarding.create_project.new_code_definition.create_x_projects"
+            values={{
+              count: projectCount,
+            }}
+          />
           <Spinner className="sw-ml-2" loading={submitting} />
         </ButtonPrimary>
       </div>
index 78f310ae0f2b2aec87a9dc34dab89ab24b88567f..2a2f2a810ea55d9944c48a4a452f9bc8d9520f8e 100644 (file)
@@ -45,10 +45,11 @@ import { NewCodeDefinitionLevels } from './utils';
 
 interface Props {
   onNcdChanged: (ncd: NewCodeDefinitiondWithCompliance) => void;
+  isMultipleProjects?: boolean;
 }
 
 export default function NewCodeDefinitionSelector(props: Props) {
-  const { onNcdChanged } = props;
+  const { onNcdChanged, isMultipleProjects } = props;
 
   const [globalNcd, setGlobalNcd] = React.useState<NewCodeDefinition | null>(null);
   const [selectedNcdType, setSelectedNcdType] = React.useState<NewCodeDefinitionType | null>(null);
@@ -101,7 +102,9 @@ export default function NewCodeDefinitionSelector(props: Props) {
     <PageContentFontWrapper>
       <p className="sw-mt-10">
         <strong className="sw-body-md-highlight">
-          {translate('new_code_definition.question')}
+          {isMultipleProjects
+            ? translate('new_code_definition.question.multiple_projects')
+            : translate('new_code_definition.question')}
         </strong>
       </p>
       <div className="sw-mt-7 sw-ml-1" role="radiogroup">
@@ -124,13 +127,19 @@ export default function NewCodeDefinitionSelector(props: Props) {
         </StyledGlobalSettingWrapper>
 
         <RadioButton
-          aria-label={translate('new_code_definition.specific_setting')}
+          aria-label={
+            isMultipleProjects
+              ? translate('new_code_definition.specific_setting.multiple_projects')
+              : translate('new_code_definition.specific_setting')
+          }
           checked={Boolean(selectedNcdType && selectedNcdType !== NewCodeDefinitionType.Inherited)}
           className="sw-mt-12 sw-font-semibold"
           onCheck={() => handleNcdChanged(NewCodeDefinitionType.PreviousVersion)}
           value="specific"
         >
-          {translate('new_code_definition.specific_setting')}
+          {isMultipleProjects
+            ? translate('new_code_definition.specific_setting.multiple_projects')
+            : translate('new_code_definition.specific_setting')}
         </RadioButton>
       </div>
 
index bad1ff52ecbd638f250359174f900977fdc3f170..fb19ad89c0f6d48dc03424094c1d5632cb2eea5c 100644 (file)
@@ -4031,8 +4031,10 @@ footer.web_api=Web API
 #
 #------------------------------------------------------------------------------
 new_code_definition.question=Choose the baseline for new code for this project
+new_code_definition.question.multiple_projects=Choose the baseline for new code for those projects
 new_code_definition.global_setting=Use the global setting
 new_code_definition.specific_setting=Define a specific setting for this project
+new_code_definition.specific_setting.multiple_projects=Define a specific setting for your projects
 
 new_code_definition.previous_version=Previous version
 new_code_definition.previous_version.usecase=Recommended for projects following regular versions or releases.
@@ -4192,9 +4194,12 @@ onboarding.create_project.x_repositories_selected={count} {count, plural, one {r
 onboarding.create_project.x_repository_created={count} {count, plural, one {repository} other {repositories}} will be created as a project on SonarQube
 
 onboarding.create_project.new_code_definition.title=Set up project for Clean as You Code
+onboarding.create_x_project.new_code_definition.title=Set up {count, plural, one {project} other {# projects}} for Clean as You Code
 onboarding.create_project.new_code_definition.description=The new code definition sets which part of your code will be considered new code. This helps you focus attention on the most recent changes to your project, enabling you to follow the Clean as You Code methodology. Learn more: {link}
 onboarding.create_project.new_code_definition.description.link=Defining New Code
 onboarding.create_project.new_code_definition.create_project=Create project
+onboarding.create_project.new_code_definition.create_x_projects=Create {count, plural, one {project} other {# projects}}
+onboarding.create_projects.new_code_definition.change_info=You can change this for each project individually at any time in the project administration.
 
 onboarding.create_project.success=Congratulations! Your project has been created.