]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19709 Migrating project list header to MIUI
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Wed, 5 Jul 2023 09:21:57 +0000 (11:21 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 11 Jul 2023 20:03:24 +0000 (20:03 +0000)
38 files changed:
server/sonar-web/design-system/src/components/icons/SortAscendIcon.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/icons/SortDescendIcon.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/icons/index.ts
server/sonar-web/design-system/src/components/input/FormField.tsx
server/sonar-web/design-system/src/components/modal/Modal.tsx
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/app/components/GlobalMessagesContainer.tsx
server/sonar-web/src/main/js/app/components/extensions/CreateApplicationForm.tsx
server/sonar-web/src/main/js/app/components/extensions/__tests__/CreateApplicationForm-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/CreateApplicationForm-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx
server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx
server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenuItem.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectsSortingSelect.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/ApplicationCreation-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/components/__tests__/CreateApplication-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/PerspectiveSelect-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenuItem-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectsSortingSelect-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ApplicationCreation-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PerspectiveSelect-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenu-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenuItem-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsSortingSelect-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilterContainer-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilterContainer-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/components/ui/MandatoryFieldsExplanation.tsx
server/sonar-web/src/main/js/components/ui/__tests__/MandatoryFieldsExplanation-test.tsx [deleted file]
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/MandatoryFieldsExplanation-test.tsx.snap [deleted file]
server/sonar-web/tailwind.base.config.js

diff --git a/server/sonar-web/design-system/src/components/icons/SortAscendIcon.tsx b/server/sonar-web/design-system/src/components/icons/SortAscendIcon.tsx
new file mode 100644 (file)
index 0000000..11c3d4a
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { SortAscIcon } from '@primer/octicons-react';
+import { OcticonHoc } from './Icon';
+
+export const SortAscendIcon = OcticonHoc(SortAscIcon, 'SortAscendIcon');
diff --git a/server/sonar-web/design-system/src/components/icons/SortDescendIcon.tsx b/server/sonar-web/design-system/src/components/icons/SortDescendIcon.tsx
new file mode 100644 (file)
index 0000000..f319d5f
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { SortDescIcon } from '@primer/octicons-react';
+import { OcticonHoc } from './Icon';
+
+export const SortDescendIcon = OcticonHoc(SortDescIcon, 'SortDescendIcon');
index 1c00421fac7e88b20634285cbdea8ce5c38159f0..858487bffaa62e4441575e12ef4badaebdb8b9d5 100644 (file)
@@ -69,6 +69,8 @@ export { SeverityCriticalIcon } from './SeverityCriticalIcon';
 export { SeverityInfoIcon } from './SeverityInfoIcon';
 export { SeverityMajorIcon } from './SeverityMajorIcon';
 export { SeverityMinorIcon } from './SeverityMinorIcon';
+export { SortAscendIcon } from './SortAscendIcon';
+export { SortDescendIcon } from './SortDescendIcon';
 export { StarIcon } from './StarIcon';
 export { StatusConfirmedIcon } from './StatusConfirmedIcon';
 export { StatusOpenIcon } from './StatusOpenIcon';
index 3397bc413812ac71b48ab6023519dce06eb1d576..1f525a01ea7ec17021d9989cdeac934819361384 100644 (file)
@@ -33,6 +33,7 @@ interface Props {
   id?: string;
   label: string | ReactNode;
   required?: boolean;
+  requiredAriaLabel?: string;
   title?: string;
 }
 
@@ -47,13 +48,16 @@ export function FormField({
   htmlFor,
   title,
   ariaLabel,
+  requiredAriaLabel,
 }: Props) {
   return (
     <FieldWrapper className={className} id={id}>
       <label aria-label={ariaLabel} className="sw-mb-2" htmlFor={htmlFor} title={title}>
         <Highlight className="sw-flex sw-items-center sw-gap-2">
           {label}
-          {required && <RequiredIcon className="sw--ml-1" />}
+          {required && (
+            <RequiredIcon aria-label={requiredAriaLabel ?? 'required'} className="sw--ml-1" />
+          )}
           {help}
         </Highlight>
       </label>
index 12d52760a33b36f3f4eff7e766a0a425a28574b5..db54a6525558f1ab135c49553c297debfbaf56eb 100644 (file)
@@ -89,7 +89,7 @@ export function Modal({
 
       <ReactModal
         aria={{ labelledby: '#modal_header_title' }}
-        className={classNames('design-system-modal-contents', { large: isLarge })}
+        className={classNames('design-system-modal-contents modal', { large: isLarge })}
         isOpen={isOpen}
         onRequestClose={onClose}
         overlayClassName="design-system-modal-overlay"
index 9989a282df5c7951f6033d2a40d3bab3ca92f653..998e3cdb8390e66ba2ed3d6f0f012733fa48171a 100644 (file)
@@ -46,6 +46,7 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND = [
   '/project/activity',
   '/code',
   '/project/extension/securityreport/securityreport',
+  '/projects',
 ];
 
 export default function GlobalContainer() {
index 67b510af1b059212e298fac59425913bef31dec9..44284218ecb944d8f0e04630ae9ec16037137fba 100644 (file)
@@ -98,4 +98,5 @@ const MessagesContainer = styled.div`
   left: 50%;
   width: 350px;
   margin-left: -175px;
+  z-index: 8600;
 `;
index e9608ce2de914580b60425505c69b93132a1ba55..4ab475f2781dcd20d26f7ec443108625c508bb7d 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 {
+  ButtonSecondary,
+  FormField,
+  InputField,
+  InputTextArea,
+  Modal,
+  RadioButton,
+} from 'design-system';
 import * as React from 'react';
 import { createApplication } from '../../../api/application';
-import Radio from '../../../components/controls/Radio';
-import SimpleModal from '../../../components/controls/SimpleModal';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
-import DeferredSpinner from '../../../components/ui/DeferredSpinner';
-import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
 import { translate } from '../../../helpers/l10n';
 import { ComponentQualifier, Visibility } from '../../../types/component';
@@ -38,6 +41,7 @@ interface State {
   key: string;
   name: string;
   visibility: Visibility;
+  submitting: boolean;
 }
 
 export default class CreateApplicationForm extends React.PureComponent<Props, State> {
@@ -50,6 +54,7 @@ export default class CreateApplicationForm extends React.PureComponent<Props, St
       key: '',
       name: '',
       visibility: Visibility.Public,
+      submitting: false,
     };
   }
 
@@ -79,115 +84,114 @@ export default class CreateApplicationForm extends React.PureComponent<Props, St
 
   handleFormSubmit = () => {
     const { name, description, key, visibility } = this.state;
-    return createApplication(name, description, key.length > 0 ? key : undefined, visibility).then(
-      ({ application }) => {
+    this.setState({ submitting: true });
+    return createApplication(name, description, key.length > 0 ? key : undefined, visibility)
+      .then(({ application }) => {
         if (this.mounted) {
+          this.setState({ submitting: false });
           this.props.onCreate({
             key: application.key,
             qualifier: ComponentQualifier.Application,
           });
         }
-      }
+      })
+      .catch(() => {
+        this.setState({ submitting: false });
+      });
+  };
+
+  renderForm = () => {
+    const { name, description, key, visibility } = this.state;
+
+    return (
+      <form onSubmit={this.props.onClose} id="create-application-form">
+        <MandatoryFieldsExplanation className="modal-field" />
+
+        <FormField
+          htmlFor="view-edit-name"
+          label={translate('name')}
+          required
+          requiredAriaLabel={translate('field_required')}
+        >
+          <InputField
+            autoFocus
+            id="view-edit-name"
+            maxLength={100}
+            name="name"
+            onChange={this.handleNameChange}
+            type="text"
+            size="full"
+            value={name}
+          />
+        </FormField>
+        <FormField htmlFor="view-edit-description" label={translate('description')}>
+          <InputTextArea
+            id="view-edit-description"
+            name="description"
+            onChange={this.handleDescriptionChange}
+            size="full"
+            value={description}
+          />
+        </FormField>
+        <FormField
+          htmlFor="view-edit-key"
+          label={translate('key')}
+          description={translate('onboarding.create_application.key.description')}
+        >
+          <InputField
+            autoComplete="off"
+            id="view-edit-key"
+            maxLength={256}
+            name="key"
+            onChange={this.handleKeyChange}
+            type="text"
+            size="full"
+            value={key}
+          />
+        </FormField>
+
+        <FormField label={translate('visibility')}>
+          {[Visibility.Public, Visibility.Private].map((v) => (
+            <RadioButton
+              key={v}
+              checked={visibility === v}
+              value={v}
+              onCheck={this.handleVisibilityChange}
+            >
+              {translate('visibility', v)}
+            </RadioButton>
+          ))}
+        </FormField>
+      </form>
     );
   };
 
   render() {
-    const { name, description, key, visibility } = this.state;
+    const { submitting } = this.state;
     const header = translate('qualifiers.create.APP');
     const submitDisabled = !this.state.name.length;
 
     return (
-      <SimpleModal
-        header={header}
+      <Modal
         onClose={this.props.onClose}
-        onSubmit={this.handleFormSubmit}
-        size="small"
-      >
-        {({ onCloseClick, onFormSubmit, submitting }) => (
-          <form onSubmit={onFormSubmit}>
-            <div className="modal-head">
-              <h2>{header}</h2>
-            </div>
-
-            <div className="modal-body">
-              <MandatoryFieldsExplanation className="modal-field" />
-
-              <div className="modal-field">
-                <label htmlFor="view-edit-name">
-                  {translate('name')}
-                  <MandatoryFieldMarker />
-                </label>
-                <input
-                  autoFocus
-                  id="view-edit-name"
-                  maxLength={100}
-                  name="name"
-                  onChange={this.handleNameChange}
-                  size={50}
-                  type="text"
-                  value={name}
-                />
-              </div>
-              <div className="modal-field">
-                <label htmlFor="view-edit-description">{translate('description')}</label>
-                <textarea
-                  id="view-edit-description"
-                  name="description"
-                  onChange={this.handleDescriptionChange}
-                  value={description}
-                />
-              </div>
-              <div className="modal-field">
-                <label htmlFor="view-edit-key">{translate('key')}</label>
-                <input
-                  autoComplete="off"
-                  id="view-edit-key"
-                  maxLength={256}
-                  name="key"
-                  onChange={this.handleKeyChange}
-                  size={256}
-                  type="text"
-                  value={key}
-                />
-                <p className="modal-field-description">
-                  {translate('onboarding.create_application.key.description')}
-                </p>
-              </div>
-
-              <div className="modal-field">
-                <label>{translate('visibility')}</label>
-                <div className="little-spacer-top">
-                  {[Visibility.Public, Visibility.Private].map((v) => (
-                    <Radio
-                      className={`big-spacer-right visibility-${v}`}
-                      key={v}
-                      checked={visibility === v}
-                      value={v}
-                      onCheck={this.handleVisibilityChange}
-                    >
-                      {translate('visibility', v)}
-                    </Radio>
-                  ))}
-                </div>
-              </div>
-            </div>
-
-            <div className="modal-foot">
-              <DeferredSpinner className="spacer-right" loading={submitting} />
-              <SubmitButton disabled={submitting || submitDisabled}>
-                {translate('create')}
-              </SubmitButton>
-              <ResetButtonLink
-                className="js-modal-close"
-                id="view-edit-cancel"
-                onClick={onCloseClick}
-              >
-                {translate('cancel')}
-              </ResetButtonLink>
-            </div>
-          </form>
-        )}
-      </SimpleModal>
+        headerTitle={header}
+        isScrollable
+        loading={submitting}
+        body={this.renderForm()}
+        primaryButton={
+          <ButtonSecondary
+            disabled={submitting || submitDisabled}
+            form="create-application-form"
+            onClick={() => {
+              this.handleFormSubmit();
+            }}
+            type="submit"
+          >
+            {translate('create')}
+          </ButtonSecondary>
+        }
+        secondaryButtonLabel={translate('cancel')}
+      />
     );
   }
 }
diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/CreateApplicationForm-test.tsx b/server/sonar-web/src/main/js/app/components/extensions/__tests__/CreateApplicationForm-test.tsx
deleted file mode 100644 (file)
index 0b1da2c..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { createApplication } from '../../../../api/application';
-import SimpleModal from '../../../../components/controls/SimpleModal';
-import { mockEvent, waitAndUpdate } from '../../../../helpers/testUtils';
-import { ComponentQualifier, Visibility } from '../../../../types/component';
-import CreateApplicationForm from '../CreateApplicationForm';
-
-jest.mock('../../../../api/application', () => ({
-  createApplication: jest.fn().mockResolvedValue({ application: { key: 'foo' } }),
-}));
-
-beforeEach(jest.clearAllMocks);
-
-it('should render correctly', () => {
-  const wrapper = shallowRender();
-  expect(wrapper).toMatchSnapshot('default');
-  expect(wrapper.find(SimpleModal).dive()).toMatchSnapshot('form');
-});
-
-it('should correctly create application on form submit', async () => {
-  const onCreate = jest.fn();
-  const wrapper = shallowRender({ onCreate });
-  const instance = wrapper.instance();
-
-  instance.handleDescriptionChange(mockEvent({ currentTarget: { value: 'description' } }));
-  instance.handleKeyChange(mockEvent({ currentTarget: { value: 'key' } }));
-  instance.handleNameChange(mockEvent({ currentTarget: { value: 'name' } }));
-  instance.handleVisibilityChange(Visibility.Private);
-
-  wrapper.find(SimpleModal).props().onSubmit();
-  expect(createApplication).toHaveBeenCalledWith('name', 'description', 'key', Visibility.Private);
-  await waitAndUpdate(wrapper);
-
-  expect(onCreate).toHaveBeenCalledWith(
-    expect.objectContaining({
-      key: 'foo',
-      qualifier: ComponentQualifier.Application,
-    })
-  );
-
-  // Can call the WS without any key.
-  instance.handleKeyChange(mockEvent({ currentTarget: { value: '' } }));
-  instance.handleFormSubmit();
-  expect(createApplication).toHaveBeenCalledWith(
-    'name',
-    'description',
-    undefined,
-    Visibility.Private
-  );
-});
-
-function shallowRender(props?: Partial<CreateApplicationForm['props']>) {
-  return shallow<CreateApplicationForm>(
-    <CreateApplicationForm onClose={jest.fn()} onCreate={jest.fn()} {...props} />
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/CreateApplicationForm-test.tsx.snap b/server/sonar-web/src/main/js/app/components/extensions/__tests__/__snapshots__/CreateApplicationForm-test.tsx.snap
deleted file mode 100644 (file)
index a193f2c..0000000
+++ /dev/null
@@ -1,147 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly: default 1`] = `
-<SimpleModal
-  header="qualifiers.create.APP"
-  onClose={[MockFunction]}
-  onSubmit={[Function]}
-  size="small"
->
-  <Component />
-</SimpleModal>
-`;
-
-exports[`should render correctly: form 1`] = `
-<Modal
-  contentLabel="qualifiers.create.APP"
-  onRequestClose={[MockFunction]}
-  size="small"
->
-  <form
-    onSubmit={[Function]}
-  >
-    <div
-      className="modal-head"
-    >
-      <h2>
-        qualifiers.create.APP
-      </h2>
-    </div>
-    <div
-      className="modal-body"
-    >
-      <MandatoryFieldsExplanation
-        className="modal-field"
-      />
-      <div
-        className="modal-field"
-      >
-        <label
-          htmlFor="view-edit-name"
-        >
-          name
-          <MandatoryFieldMarker />
-        </label>
-        <input
-          autoFocus={true}
-          id="view-edit-name"
-          maxLength={100}
-          name="name"
-          onChange={[Function]}
-          size={50}
-          type="text"
-          value=""
-        />
-      </div>
-      <div
-        className="modal-field"
-      >
-        <label
-          htmlFor="view-edit-description"
-        >
-          description
-        </label>
-        <textarea
-          id="view-edit-description"
-          name="description"
-          onChange={[Function]}
-          value=""
-        />
-      </div>
-      <div
-        className="modal-field"
-      >
-        <label
-          htmlFor="view-edit-key"
-        >
-          key
-        </label>
-        <input
-          autoComplete="off"
-          id="view-edit-key"
-          maxLength={256}
-          name="key"
-          onChange={[Function]}
-          size={256}
-          type="text"
-          value=""
-        />
-        <p
-          className="modal-field-description"
-        >
-          onboarding.create_application.key.description
-        </p>
-      </div>
-      <div
-        className="modal-field"
-      >
-        <label>
-          visibility
-        </label>
-        <div
-          className="little-spacer-top"
-        >
-          <Radio
-            checked={true}
-            className="big-spacer-right visibility-public"
-            key="public"
-            onCheck={[Function]}
-            value="public"
-          >
-            visibility.public
-          </Radio>
-          <Radio
-            checked={false}
-            className="big-spacer-right visibility-private"
-            key="private"
-            onCheck={[Function]}
-            value="private"
-          >
-            visibility.private
-          </Radio>
-        </div>
-      </div>
-    </div>
-    <div
-      className="modal-foot"
-    >
-      <DeferredSpinner
-        className="spacer-right"
-        loading={false}
-      />
-      <SubmitButton
-        disabled={true}
-      >
-        create
-      </SubmitButton>
-      <ResetButtonLink
-        className="js-modal-close"
-        id="view-edit-cancel"
-        onClick={[Function]}
-      >
-        cancel
-      </ResetButtonLink>
-    </div>
-  </form>
-</Modal>
-`;
index 536f90a44cb991d91735665bb68e835c3575a306..8efacc6cbffc2660b51d7ddb9033a79f0f461014 100644 (file)
@@ -38,10 +38,10 @@ import { AppState } from '../../../types/appstate';
 import { ComponentQualifier } from '../../../types/component';
 import { RawQuery } from '../../../types/types';
 import { CurrentUser, isLoggedIn } from '../../../types/users';
-import { hasFilterParams, parseUrlQuery, Query } from '../query';
+import { Query, hasFilterParams, parseUrlQuery } from '../query';
 import '../styles.css';
 import { Facets, Project } from '../types';
-import { fetchProjects, parseSorting, SORTING_SWITCH } from '../utils';
+import { SORTING_SWITCH, fetchProjects, parseSorting } from '../utils';
 import PageHeader from './PageHeader';
 import PageSidebar from './PageSidebar';
 import ProjectsList from './ProjectsList';
@@ -237,22 +237,17 @@ export class AllProjects extends React.PureComponent<Props, State> {
   );
 
   renderHeader = () => (
-    <div className="layout-page-header-panel layout-page-main-header">
-      <div className="layout-page-header-panel-inner layout-page-main-header-inner">
-        <div className="layout-page-main-inner">
-          <PageHeader
-            currentUser={this.props.currentUser}
-            loading={this.state.loading}
-            onPerspectiveChange={this.handlePerspectiveChange}
-            onQueryChange={this.updateLocationQuery}
-            onSortChange={this.handleSortChange}
-            query={this.state.query}
-            selectedSort={this.getSort()}
-            total={this.state.total}
-            view={this.getView()}
-          />
-        </div>
-      </div>
+    <div style={{ height: '120px' }}>
+      <PageHeader
+        currentUser={this.props.currentUser}
+        onPerspectiveChange={this.handlePerspectiveChange}
+        onQueryChange={this.updateLocationQuery}
+        onSortChange={this.handleSortChange}
+        query={this.state.query}
+        selectedSort={this.getSort()}
+        total={this.state.total}
+        view={this.getView()}
+      />
     </div>
   );
 
index 186eb25f718b3f9cde8160e701d28e2e1fea3489..cd4cb176e5f5f6f50d03aa85514426e48d17e0de 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 { ButtonSecondary } from 'design-system';
 import * as React from 'react';
 import { getComponentNavigation } from '../../../api/navigation';
 import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
 import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
 import CreateApplicationForm from '../../../app/components/extensions/CreateApplicationForm';
-import { Button } from '../../../components/controls/buttons';
 import { Router, withRouter } from '../../../components/hoc/withRouter';
 import { translate } from '../../../helpers/l10n';
 import { getComponentAdminUrl, getComponentOverviewUrl } from '../../../helpers/urls';
@@ -70,10 +70,10 @@ export function ApplicationCreation(props: ApplicationCreationProps) {
   };
 
   return (
-    <div className={className}>
-      <Button className="button-primary" onClick={() => setShowForm(true)}>
+    <>
+      <ButtonSecondary onClick={() => setShowForm(true)} className={className}>
         {translate('projects.create_application')}
-      </Button>
+      </ButtonSecondary>
 
       {showForm && (
         <CreateApplicationForm
@@ -81,7 +81,7 @@ export function ApplicationCreation(props: ApplicationCreationProps) {
           onCreate={handleComponentCreate}
         />
       )}
-    </div>
+    </>
   );
 }
 
index 423a457cece32b6133299e18ed7c83dffbb5cd4d..7b3e5007f37bbf8e3020e00e3267ac983af8d772 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 styled from '@emotion/styled';
+import { InputSearch, LightLabel, LightPrimary } from 'design-system';
 import * as React from 'react';
 import HomePageSelect from '../../../components/controls/HomePageSelect';
-import { translate } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { RawQuery } from '../../../types/types';
 import { CurrentUser, isLoggedIn } from '../../../types/users';
-import SearchFilterContainer from '../filters/SearchFilterContainer';
 import ApplicationCreation from './ApplicationCreation';
 import PerspectiveSelect from './PerspectiveSelect';
 import ProjectCreationMenu from './ProjectCreationMenu';
@@ -31,7 +31,6 @@ import ProjectsSortingSelect from './ProjectsSortingSelect';
 
 interface Props {
   currentUser: CurrentUser;
-  loading: boolean;
   onPerspectiveChange: (x: { view: string }) => void;
   onQueryChange: (change: RawQuery) => void;
   onSortChange: (sort: string, desc: boolean) => void;
@@ -41,53 +40,67 @@ interface Props {
   view: string;
 }
 
+const MIN_SEARCH_QUERY_LENGTH = 2;
+
 export default function PageHeader(props: Props) {
-  const { loading, total, currentUser, view } = props;
+  const { total, currentUser, view } = props;
   const defaultOption = isLoggedIn(currentUser) ? 'name' : 'analysis_date';
 
+  const handleSearch = (search?: string) => {
+    props.onQueryChange({ search });
+  };
+
   return (
-    <div className="page-header">
-      <div className="display-flex-center projects-header-row display-flex-space-between">
-        <SearchFilterContainer onQueryChange={props.onQueryChange} query={props.query} />
-        <div className="display-flex-center">
-          <ProjectCreationMenu className="little-spacer-right" />
-          <ApplicationCreation className="little-spacer-right" />
-          <HomePageSelect
-            className="spacer-left little-spacer-right"
-            currentPage={{ type: 'PROJECTS' }}
+    <StyledHeader className="it__page-header sw-flex sw-flex-col sw-z-project-list-header new-background sw-fixed sw-py-6 sw-pl-5">
+      <div className="sw-flex sw-justify-end sw-mb-4">
+        <ProjectCreationMenu />
+        <ApplicationCreation className="sw-ml-2" />
+      </div>
+      <div className="sw-flex sw-justify-between">
+        <div className="sw-flex">
+          <InputSearch
+            className="sw-w-abs-300 sw-mr-4 it__page-header-search"
+            minLength={MIN_SEARCH_QUERY_LENGTH}
+            onChange={handleSearch}
+            size="large"
+            placeholder={translate('projects.search')}
+            value={props.query.search ?? ''}
+            tooShortText={translateWithParameters('select2.tooShort', MIN_SEARCH_QUERY_LENGTH)}
+            searchInputAriaLabel={translate('search_verb')}
+            clearIconAriaLabel={translate('clear')}
+          />
+          <PerspectiveSelect onChange={props.onPerspectiveChange} view={view} />
+          <ProjectsSortingSelect
+            defaultOption={defaultOption}
+            onChange={props.onSortChange}
+            selectedSort={props.selectedSort}
+            view={view}
           />
         </div>
-      </div>
-      <div className="spacer-top projects-header-row display-flex-space-between">
-        <div
-          className={classNames('display-flex-center', {
-            'is-loading': loading,
-          })}
-        >
+        <div className="sw-flex sw-items-center">
           {total != null && (
-            <span className="projects-total-label">
-              <strong id="projects-total">{total}</strong> {translate('projects_')}
-            </span>
+            <>
+              <LightPrimary id="projects-total" className="sw-font-semibold sw-mr-1">
+                {total}
+              </LightPrimary>
+              <LightLabel>{translate('projects_')}</LightLabel>
+            </>
           )}
-        </div>
-
-        <div className="display-flex-center">
-          <PerspectiveSelect
-            className="projects-topbar-item"
-            onChange={props.onPerspectiveChange}
-            view={view}
-          />
-
-          <div className={classNames('projects-topbar-item')}>
-            <ProjectsSortingSelect
-              defaultOption={defaultOption}
-              onChange={props.onSortChange}
-              selectedSort={props.selectedSort}
-              view={view}
-            />
-          </div>
+          <HomePageSelect currentPage={{ type: 'PROJECTS' }} />
         </div>
       </div>
-    </div>
+    </StyledHeader>
   );
 }
+
+const StyledHeader = styled.div`
+  @media (max-width: 1320px) {
+    left: 301px;
+  }
+
+  right: 0;
+  left: calc(50vw - 369px);
+  top: 52px;
+  min-width: 740px;
+  max-width: 980px;
+`;
index a279b9d7857021b8c050a71d75ea6fcb2aa79dff..5185fbd61dd8a3f06a11766ee1580a0f1417fe31 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 { omit } from 'lodash';
+import { InputSelect, LabelValueSelectOption, StyledPageTitle } from 'design-system';
 import * as React from 'react';
-import { components, OptionProps } from 'react-select';
-import Select from '../../../components/controls/Select';
-import ListIcon from '../../../components/icons/ListIcon';
 import { translate } from '../../../helpers/l10n';
 import { VIEWS } from '../utils';
 
 interface Props {
-  className?: string;
   onChange: (x: { view: string }) => void;
   view: string;
 }
@@ -41,19 +37,6 @@ export default class PerspectiveSelect extends React.PureComponent<Props> {
     this.props.onChange({ view: option.value });
   };
 
-  perspectiveOptionRender = (props: OptionProps<PerspectiveOption, false>) => {
-    const { data, className } = props;
-    return (
-      <components.Option
-        {...omit(props, ['children', 'className'])}
-        className={`it__projects-perspective-option-${data.value} ${className}`}
-      >
-        <ListIcon className="little-spacer-right" />
-        {props.children}
-      </components.Option>
-    );
-  };
-
   render() {
     const { view } = this.props;
     const options: PerspectiveOption[] = [
@@ -63,18 +46,21 @@ export default class PerspectiveSelect extends React.PureComponent<Props> {
       })),
     ];
     return (
-      <div className={this.props.className}>
-        <label id="aria-projects-perspective">{translate('projects.perspective')}:</label>
-        <Select
+      <div className="sw-flex sw-items-center">
+        <StyledPageTitle
+          id="aria-projects-perspective"
+          as="label"
+          className="sw-body-sm-highlight sw-mr-2"
+        >
+          {translate('projects.perspective')}
+        </StyledPageTitle>
+        <InputSelect
           aria-labelledby="aria-projects-perspective"
-          className="little-spacer-left input-medium it__projects-perspective-select"
-          isClearable={false}
-          onChange={this.handleChange}
-          components={{
-            Option: this.perspectiveOptionRender,
-          }}
+          className="sw-mr-4 sw-body-sm"
+          onChange={(data: LabelValueSelectOption<string>) => this.handleChange(data)}
           options={options}
-          isSearchable={false}
+          placeholder={translate('project_activity.filter_events')}
+          size="small"
           value={options.find((option) => option.value === view)}
         />
       </div>
index 150e478be6a30e6cb6ed70665de07a56db6b69a6..edd30c5145ccdebdffcce6090c1f80c34603c8cd 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 {
+  ButtonSecondary,
+  ChevronDownIcon,
+  Dropdown,
+  ItemDivider,
+  ItemLink,
+  PopupPlacement,
+  PopupZLevel,
+} from 'design-system';
 import * as React from 'react';
 import { getAlmSettings } from '../../../api/alm-settings';
 import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
-import Link from '../../../components/common/Link';
-import { Button } from '../../../components/controls/buttons';
-import Dropdown from '../../../components/controls/Dropdown';
-import DropdownIcon from '../../../components/icons/DropdownIcon';
-import EllipsisIcon from '../../../components/icons/EllipsisIcon';
 import { IMPORT_COMPATIBLE_ALMS } from '../../../helpers/constants';
 import { translate } from '../../../helpers/l10n';
 import { hasGlobalPermission } from '../../../helpers/users';
@@ -34,7 +38,6 @@ import { LoggedInUser } from '../../../types/users';
 import ProjectCreationMenuItem from './ProjectCreationMenuItem';
 
 interface Props {
-  className?: string;
   currentUser: LoggedInUser;
 }
 
@@ -96,7 +99,7 @@ export class ProjectCreationMenu extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { className, currentUser } = this.props;
+    const { currentUser } = this.props;
     const { boundAlms } = this.state;
 
     const canCreateProject = hasGlobalPermission(currentUser, Permissions.ProjectCreation);
@@ -107,30 +110,26 @@ export class ProjectCreationMenu extends React.PureComponent<Props, State> {
 
     return (
       <Dropdown
-        className={className}
-        onOpen={this.fetchAlmBindings}
+        id="project-creation-menu"
+        size="auto"
+        placement={PopupPlacement.BottomRight}
+        zLevel={PopupZLevel.Global}
         overlay={
-          <ul className="menu">
+          <>
             {[...boundAlms, 'manual'].map((alm) => (
-              <li className="little-spacer-bottom" key={alm}>
-                <ProjectCreationMenuItem alm={alm} />
-              </li>
+              <ProjectCreationMenuItem alm={alm} key={alm} />
             ))}
+            <ItemDivider />
             {boundAlms.length < IMPORT_COMPATIBLE_ALMS.length && (
-              <li className="bordered-top little-padded-top">
-                <Link className="display-flex-center" to={{ pathname: '/projects/create' }}>
-                  <EllipsisIcon className="spacer-right" size={16} />
-                  {translate('more')}
-                </Link>
-              </li>
+              <ItemLink to={{ pathname: '/projects/create' }}>{translate('more')}</ItemLink>
             )}
-          </ul>
+          </>
         }
       >
-        <Button className="button-primary">
+        <ButtonSecondary>
           {translate('projects.add')}
-          <DropdownIcon className="spacer-left " />
-        </Button>
+          <ChevronDownIcon className="sw-ml-1" />
+        </ButtonSecondary>
       </Dropdown>
     );
   }
index b10a4502436a2c676709e3812af1181bdcabd87a..78ad05eb6b3f343b77db42ae34feeb1e369645bb 100644 (file)
@@ -17,9 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { ItemLink } from 'design-system';
 import * as React from 'react';
-import Link from '../../../components/common/Link';
-import ChevronsIcon from '../../../components/icons/ChevronsIcon';
 import { translate } from '../../../helpers/l10n';
 import { getBaseUrl } from '../../../helpers/system';
 import { queryToSearch } from '../../../helpers/urls';
@@ -36,13 +35,11 @@ export default function ProjectCreationMenuItem(props: ProjectCreationMenuItemPr
     almIcon = 'bitbucket';
   }
   return (
-    <Link
+    <ItemLink
       className="display-flex-center"
       to={{ pathname: '/projects/create', search: queryToSearch({ mode: alm }) }}
     >
-      {alm === 'manual' ? (
-        <ChevronsIcon className="spacer-right" />
-      ) : (
+      {alm !== 'manual' && (
         <img
           alt={alm}
           className="spacer-right"
@@ -51,6 +48,6 @@ export default function ProjectCreationMenuItem(props: ProjectCreationMenuItemPr
         />
       )}
       {translate('my_account.add_project', alm)}
-    </Link>
+    </ItemLink>
   );
 }
index 5becf6153408bd97012b5bc8ee19c88f42a1f402..664c3a5f569bd9bb263c9345bbfa77239eaadf68 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 {
+  InputSelect,
+  InteractiveIcon,
+  LabelValueSelectOption,
+  SortAscendIcon,
+  SortDescendIcon,
+  StyledPageTitle,
+} from 'design-system';
 import { omit, sortBy } from 'lodash';
 import * as React from 'react';
-import { components, OptionProps } from 'react-select';
-import { colors } from '../../../app/theme';
-import { ButtonIcon } from '../../../components/controls/buttons';
-import Select from '../../../components/controls/Select';
+import { OptionProps, components } from 'react-select';
 import Tooltip from '../../../components/controls/Tooltip';
-import SortAscIcon from '../../../components/icons/SortAscIcon';
-import SortDescIcon from '../../../components/icons/SortDescIcon';
 import { translate } from '../../../helpers/l10n';
-import { parseSorting, SORTING_LEAK_METRICS, SORTING_METRICS } from '../utils';
+import { SORTING_LEAK_METRICS, SORTING_METRICS, parseSorting } from '../utils';
 
 interface Props {
   className?: string;
@@ -92,18 +95,24 @@ export default class ProjectsSortingSelect extends React.PureComponent<Props> {
     const { sortDesc, value } = this.getSorting();
 
     return (
-      <div className={this.props.className}>
-        <label id="aria-projects-sort">{translate('projects.sort_by')}:</label>
-        <Select
+      <div className="sw-flex sw-items-center">
+        <StyledPageTitle
+          id="aria-projects-sort"
+          as="label"
+          className="sw-body-sm-highlight sw-mr-2"
+        >
+          {translate('projects.sort_by')}
+        </StyledPageTitle>
+        <InputSelect
           aria-labelledby="aria-projects-sort"
-          className="little-spacer-left input-medium it__projects-sort-select"
-          isClearable={false}
-          onChange={this.handleSortChange}
+          className="sw-body-sm"
+          onChange={(data: LabelValueSelectOption<string>) => this.handleSortChange(data)}
+          options={this.getOptions()}
           components={{
             Option: this.projectsSortingSelectOption,
           }}
-          options={this.getOptions()}
-          isSearchable={false}
+          placeholder={translate('project_activity.filter_events')}
+          size="small"
           value={value}
         />
         <Tooltip
@@ -112,21 +121,19 @@ export default class ProjectsSortingSelect extends React.PureComponent<Props> {
             sortDesc ? translate('projects.sort_descending') : translate('projects.sort_ascending')
           }
         >
-          <ButtonIcon
+          <InteractiveIcon
+            Icon={sortDesc ? SortDescendIcon : SortAscendIcon}
             aria-label={
               sortDesc
                 ? translate('projects.sort_descending')
                 : translate('projects.sort_ascending')
             }
-            className="js-projects-sorting-invert spacer-left"
-            color={colors.gray52}
+            className="js-projects-invert-sort sw-ml-2"
             onClick={this.handleDescToggle}
             innerRef={(sortButtonRef) => {
               this.sortOrderButtonNode = sortButtonRef;
             }}
-          >
-            {sortDesc ? <SortDescIcon className="" /> : <SortAscIcon className="" />}
-          </ButtonIcon>
+          />
         </Tooltip>
       </div>
     );
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ApplicationCreation-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ApplicationCreation-test.tsx
deleted file mode 100644 (file)
index 2349825..0000000
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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, ShallowWrapper } from 'enzyme';
-import * as React from 'react';
-import { getComponentNavigation } from '../../../../api/navigation';
-import CreateApplicationForm from '../../../../app/components/extensions/CreateApplicationForm';
-import { Button } from '../../../../components/controls/buttons';
-import { mockAppState, mockLoggedInUser, mockRouter } from '../../../../helpers/testMocks';
-import { queryToSearch } from '../../../../helpers/urls';
-import { ComponentQualifier } from '../../../../types/component';
-import { ApplicationCreation, ApplicationCreationProps } from '../ApplicationCreation';
-
-jest.mock('../../../../api/navigation', () => ({
-  getComponentNavigation: jest.fn().mockResolvedValue({}),
-}));
-
-it('should render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot('available');
-  expect(
-    shallowRender({ appState: mockAppState({ qualifiers: [ComponentQualifier.Portfolio] }) })
-  ).toMatchSnapshot('unavailable');
-  expect(
-    shallowRender({ currentUser: mockLoggedInUser({ permissions: { global: ['otherrights'] } }) })
-  ).toMatchSnapshot('not allowed');
-});
-
-it('should show form and callback when submitted - admin', async () => {
-  (getComponentNavigation as jest.Mock).mockResolvedValueOnce({
-    configuration: { showSettings: true },
-  });
-  const routerPush = jest.fn();
-  const wrapper = shallowRender({ router: mockRouter({ push: routerPush }) });
-
-  await openAndSubmitForm(wrapper);
-
-  expect(routerPush).toHaveBeenCalledWith({
-    pathname: '/project/admin/extension/developer-server/application-console',
-    search: queryToSearch({
-      id: 'new app',
-    }),
-  });
-});
-
-it('should show form and callback when submitted - user', async () => {
-  (getComponentNavigation as jest.Mock).mockResolvedValueOnce({
-    configuration: { showSettings: false },
-  });
-  const routerPush = jest.fn();
-  const wrapper = shallowRender({ router: mockRouter({ push: routerPush }) });
-
-  await openAndSubmitForm(wrapper);
-
-  expect(routerPush).toHaveBeenCalledWith({
-    pathname: '/dashboard',
-    search: queryToSearch({
-      id: 'new app',
-    }),
-  });
-});
-
-async function openAndSubmitForm(wrapper: ShallowWrapper) {
-  wrapper.find(Button).simulate('click');
-
-  const creationForm = wrapper.find(CreateApplicationForm);
-  expect(creationForm.exists()).toBe(true);
-
-  await creationForm
-    .props()
-    .onCreate({ key: 'new app', qualifier: ComponentQualifier.Application });
-  expect(getComponentNavigation).toHaveBeenCalled();
-  expect(wrapper.find(CreateApplicationForm).exists()).toBe(false);
-}
-
-function shallowRender(overrides: Partial<ApplicationCreationProps> = {}) {
-  return shallow(
-    <ApplicationCreation
-      appState={mockAppState({ qualifiers: [ComponentQualifier.Application] })}
-      currentUser={mockLoggedInUser({ permissions: { global: ['applicationcreator'] } })}
-      router={mockRouter()}
-      {...overrides}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/CreateApplication-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/CreateApplication-test.tsx
new file mode 100644 (file)
index 0000000..535a1bc
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import { createApplication } from '../../../../api/application';
+import { getComponentNavigation } from '../../../../api/navigation';
+import { mockAppState, mockLoggedInUser, mockRouter } from '../../../../helpers/testMocks';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { byRole, byText } from '../../../../helpers/testSelector';
+import { queryToSearch } from '../../../../helpers/urls';
+import { ComponentQualifier, Visibility } from '../../../../types/component';
+import { FCProps } from '../../../../types/misc';
+import { LoggedInUser } from '../../../../types/users';
+import { ApplicationCreation } from '../ApplicationCreation';
+
+jest.mock('../../../../api/application', () => ({
+  createApplication: jest.fn().mockResolvedValue({}),
+}));
+jest.mock('../../../../api/navigation', () => ({
+  getComponentNavigation: jest.fn().mockResolvedValue({}),
+}));
+
+const ui = {
+  buttonAddApplication: byRole('button', { name: 'projects.create_application' }),
+  createApplicationHeader: byText('qualifiers.create.APP'),
+  mandatoryFieldWarning: byText('fields_marked_with_x_required'),
+  formNameField: byText('name'),
+  formKeyField: byText('key'),
+  formDescriptionField: byText('description'),
+  formVisibilityField: byText('visibility'),
+  formRadioButtonPrivate: byRole('radio', { name: 'visibility.private' }),
+  formCreateButton: byRole('button', { name: 'create' }),
+  formCancelButton: byRole('button', { name: 'cancel' }),
+};
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
+it('should be able to create application when user is logged in and has permission', async () => {
+  const user = userEvent.setup();
+  const routerPush = jest.fn();
+  const router = mockRouter({ push: routerPush });
+
+  jest.mocked(getComponentNavigation).mockResolvedValueOnce({
+    configuration: { showSettings: true },
+    name: 'name',
+    breadcrumbs: [{ key: 'b-key', qualifier: 'qual', name: 'b-name' }],
+    key: 'key',
+  });
+  jest.mocked(createApplication).mockResolvedValueOnce({
+    application: {
+      key: 'app',
+      name: 'app',
+      description: 'app',
+      visibility: Visibility.Public,
+    },
+  });
+
+  renderApplicationCreation(
+    { router },
+    mockLoggedInUser({ permissions: { global: ['admin', 'applicationcreator'] } })
+  );
+
+  await user.click(ui.buttonAddApplication.get());
+  expect(ui.createApplicationHeader.get()).toBeInTheDocument();
+  expect(ui.mandatoryFieldWarning.get()).toBeInTheDocument();
+  expect(ui.formNameField.get()).toBeInTheDocument();
+  expect(ui.formDescriptionField.get()).toBeInTheDocument();
+  expect(ui.formKeyField.get()).toBeInTheDocument();
+  expect(ui.formVisibilityField.get()).toBeInTheDocument();
+  expect(ui.formCreateButton.get()).toBeInTheDocument();
+  expect(ui.formCancelButton.get()).toBeInTheDocument();
+  await user.click(ui.formCancelButton.get());
+  expect(ui.createApplicationHeader.query()).not.toBeInTheDocument();
+
+  await user.click(ui.buttonAddApplication.get());
+  await user.click(ui.formNameField.get());
+  await user.keyboard('app');
+  await user.click(ui.formDescriptionField.get());
+  await user.keyboard('app description');
+  await user.click(ui.formKeyField.get());
+  await user.keyboard('app-key');
+  await user.click(ui.formRadioButtonPrivate.get());
+  await user.click(ui.formCreateButton.get());
+  expect(createApplication).toHaveBeenCalledWith(
+    'app',
+    'app description',
+    'app-key',
+    Visibility.Private
+  );
+  expect(routerPush).toHaveBeenCalledWith({
+    pathname: '/project/admin/extension/developer-server/application-console',
+    search: queryToSearch({
+      id: 'app',
+    }),
+  });
+});
+
+function renderApplicationCreation(
+  props: Partial<FCProps<typeof ApplicationCreation>> = {},
+  currentUser: LoggedInUser = mockLoggedInUser()
+) {
+  return renderComponent(
+    <ApplicationCreation
+      currentUser={currentUser}
+      router={mockRouter()}
+      appState={mockAppState({ qualifiers: [ComponentQualifier.Application] })}
+      {...props}
+    />
+  );
+}
index 8b43a0372a3dafb13431d8743493251a09a0ccec..119085817fceb35a4571fe9dee1a33c600afb57e 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 userEvent from '@testing-library/user-event';
 import * as React from 'react';
+import { getAlmSettings } from '../../../../api/alm-settings';
+import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
+import { mockAppState, mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
+import { AlmKeys } from '../../../../types/alm-settings';
+import { ComponentQualifier } from '../../../../types/component';
+import { FCProps } from '../../../../types/misc';
+import { CurrentUser } from '../../../../types/users';
 import PageHeader from '../PageHeader';
 
-it('should render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot();
+jest.mock('../../../../api/alm-settings', () => ({
+  getAlmSettings: jest.fn().mockResolvedValue([]),
+}));
+jest.mock('../../../../api/components', () => ({
+  searchProjects: jest.fn().mockResolvedValue({}),
+}));
+
+const ui = {
+  buttonAddProject: byRole('button', { name: 'projects.add' }),
+  buttonAddApplication: byRole('button', { name: 'projects.create_application' }),
+  searchBar: byLabelText('search_verb'),
+  selectPerspective: byLabelText('projects.perspective'),
+  selectSort: byLabelText('projects.sort_by'),
+  buttonSortProject: byLabelText('projects.sort_ascending'),
+  projectNumber: byText(12),
+  projectsText: byText('projects_'),
+  buttonHomePage: byLabelText('homepage.check'),
+  selectOptionAzure: byText('my_account.add_project.azure'),
+  selectOptionAzureImage: byRole('img', { name: 'azure' }),
+  selectOptionGitlab: byText('my_account.add_project.gitlab'),
+  selectOptionGitlabImage: byRole('img', { name: 'gitlab' }),
+  selectOptionBitbucket: byText('my_account.add_project.bitbucket'),
+  selectOptionBitbucketCloud: byText('my_account.add_project.bitbucketcloud'),
+  selectOptionManual: byText('my_account.add_project.manual'),
+  selectOptionMore: byText('more'),
+  selectOptionNewCode: byText('projects.view.new_code'),
+  selectOptionAnalysisDate: byText('projects.sorting.analysis_date'),
+  mandatoryFieldWarning: byText('fields_marked_with_x_required'),
+};
+
+beforeEach(() => {
+  jest.clearAllMocks();
 });
 
-it('should render correctly while loading', () => {
-  expect(shallowRender({ loading: true, total: 2 })).toMatchSnapshot();
+it('should work correctly for logged in user with edit permission', async () => {
+  const user = userEvent.setup();
+  const onQueryChangeMock = jest.fn();
+  const onPerspectiveChangeMock = jest.fn();
+  const onSortChangeMock = jest.fn();
+
+  jest.mocked(getAlmSettings).mockResolvedValueOnce([
+    { alm: AlmKeys.Azure, key: 'azure', url: 'https://azure.foo' },
+    { alm: AlmKeys.GitLab, key: 'gitlab', url: 'https://gitlab.foo' },
+  ]);
+
+  renderPageHeader(
+    {
+      total: 12,
+      onQueryChange: onQueryChangeMock,
+      onPerspectiveChange: onPerspectiveChangeMock,
+      onSortChange: onSortChangeMock,
+    },
+    mockLoggedInUser({ permissions: { global: ['admin', 'provisioning', 'applicationcreator'] } })
+  );
+  expect(getAlmSettings).toHaveBeenCalled();
+  expect(ui.buttonAddProject.get()).toBeInTheDocument();
+  expect(ui.buttonAddApplication.get()).toBeInTheDocument();
+  expect(ui.searchBar.get()).toBeInTheDocument();
+  expect(ui.selectPerspective.get()).toBeInTheDocument();
+  expect(ui.selectSort.get()).toBeInTheDocument();
+  expect(ui.buttonSortProject.get()).toBeInTheDocument();
+  expect(ui.projectNumber.get()).toBeInTheDocument();
+  expect(ui.projectsText.get()).toBeInTheDocument();
+  await expect(ui.buttonHomePage.get()).toHaveATooltipWithContent('homepage.check');
+
+  // project creation
+  await user.click(ui.buttonAddProject.get());
+  expect(ui.selectOptionAzure.get()).toHaveAttribute('href', '/projects/create?mode=azure');
+  expect(ui.selectOptionAzureImage.get()).toBeInTheDocument();
+  expect(ui.selectOptionGitlab.get()).toHaveAttribute('href', '/projects/create?mode=gitlab');
+  expect(ui.selectOptionGitlabImage.get()).toBeInTheDocument();
+  expect(ui.selectOptionManual.get()).toHaveAttribute('href', '/projects/create?mode=manual');
+  expect(ui.selectOptionMore.get()).toHaveAttribute('href', '/projects/create');
+
+  // search projects
+  await user.click(ui.searchBar.get());
+  await user.keyboard('2');
+  expect(onQueryChangeMock).toHaveBeenCalledWith({ search: 'test2' });
+
+  // perpective select
+  await user.click(ui.selectPerspective.get());
+  await user.click(ui.selectOptionNewCode.get());
+  expect(onPerspectiveChangeMock).toHaveBeenCalledWith({ view: 'leak' });
+
+  // sort ascending
+  await user.click(ui.buttonSortProject.get());
+  expect(onSortChangeMock).toHaveBeenCalledWith('size', true);
+
+  // sort select
+  await user.click(ui.selectSort.get());
+  await user.click(ui.selectOptionAnalysisDate.get());
+  expect(onSortChangeMock).toHaveBeenCalledWith('analysis_date', false);
 });
 
-it('should not render projects total', () => {
-  expect(shallowRender({ total: undefined }).find('#projects-total').exists()).toBe(false);
+it('should work correctly for logged in user without edit permission', async () => {
+  renderPageHeader({ total: 12 }, mockLoggedInUser());
+  expect(getAlmSettings).not.toHaveBeenCalled();
+  expect(ui.buttonAddProject.query()).not.toBeInTheDocument();
+  expect(ui.buttonAddApplication.query()).not.toBeInTheDocument();
+  expect(ui.searchBar.get()).toBeInTheDocument();
+  expect(ui.selectPerspective.get()).toBeInTheDocument();
+  expect(ui.selectSort.get()).toBeInTheDocument();
+  expect(ui.buttonSortProject.get()).toBeInTheDocument();
+  expect(ui.projectNumber.get()).toBeInTheDocument();
+  expect(ui.projectsText.get()).toBeInTheDocument();
+  await expect(ui.buttonHomePage.get()).toHaveATooltipWithContent('homepage.check');
 });
 
-it('should render switch the default sorting option for anonymous users', () => {
-  expect(
-    shallowRender({
-      currentUser: { isLoggedIn: true },
-      open: true,
-    }).find('ProjectsSortingSelect')
-  ).toMatchSnapshot();
-
-  expect(
-    shallowRender({
-      currentUser: { isLoggedIn: false },
-      open: true,
-      view: 'leak',
-    }).find('ProjectsSortingSelect')
-  ).toMatchSnapshot();
+it('should work correctly for anonymous user', () => {
+  renderPageHeader({ total: 12 }, mockCurrentUser());
+  expect(getAlmSettings).not.toHaveBeenCalled();
+  expect(ui.buttonAddProject.query()).not.toBeInTheDocument();
+  expect(ui.buttonAddApplication.query()).not.toBeInTheDocument();
+  expect(ui.searchBar.get()).toBeInTheDocument();
+  expect(ui.selectPerspective.get()).toBeInTheDocument();
+  expect(ui.selectSort.get()).toBeInTheDocument();
+  expect(ui.buttonSortProject.get()).toBeInTheDocument();
+  expect(ui.projectNumber.get()).toBeInTheDocument();
+  expect(ui.projectsText.get()).toBeInTheDocument();
+  expect(ui.buttonHomePage.query()).not.toBeInTheDocument();
+});
+
+it('should not render total if not defined', () => {
+  renderPageHeader({ total: undefined }, mockCurrentUser());
+  expect(ui.projectsText.query()).not.toBeInTheDocument();
+});
+
+it('should render alm correctly even with wrong data', async () => {
+  const user = userEvent.setup();
+
+  jest.mocked(getAlmSettings).mockResolvedValueOnce([
+    { alm: AlmKeys.Azure, key: 'azure1' },
+    { alm: AlmKeys.Azure, key: 'azure2' },
+    { alm: AlmKeys.BitbucketServer, url: 'b1', key: 'bbs' },
+    { alm: AlmKeys.BitbucketCloud, key: 'bbc' },
+    { alm: AlmKeys.GitLab, key: 'gitlab', url: 'https://gitlab.foo' },
+  ]);
+
+  renderPageHeader(
+    {},
+    mockLoggedInUser({ permissions: { global: ['admin', 'provisioning', 'applicationcreator'] } })
+  );
+
+  await user.click(ui.buttonAddProject.get());
+  expect(ui.selectOptionAzure.query()).not.toBeInTheDocument();
+  expect(ui.selectOptionGitlab.get()).toHaveAttribute('href', '/projects/create?mode=gitlab');
+  expect(ui.selectOptionBitbucket.get()).toHaveAttribute('href', '/projects/create?mode=bitbucket');
+  expect(ui.selectOptionBitbucketCloud.get()).toHaveAttribute(
+    'href',
+    '/projects/create?mode=bitbucketcloud'
+  );
 });
 
-function shallowRender(props?: {}) {
-  return shallow(
-    <PageHeader
-      currentUser={{ isLoggedIn: false, dismissedNotices: {} }}
-      loading={false}
-      onPerspectiveChange={jest.fn()}
-      onQueryChange={jest.fn()}
-      onSortChange={jest.fn()}
-      query={{ search: 'test' }}
-      selectedSort="size"
-      total={12}
-      view="overall"
-      {...props}
-    />
+function renderPageHeader(
+  props: Partial<FCProps<typeof PageHeader>> = {},
+  currentUser: CurrentUser = mockLoggedInUser()
+) {
+  return renderComponent(
+    <CurrentUserContextProvider currentUser={currentUser}>
+      <PageHeader
+        currentUser={currentUser}
+        onPerspectiveChange={jest.fn()}
+        onQueryChange={jest.fn()}
+        onSortChange={jest.fn()}
+        query={{ search: 'test' }}
+        selectedSort="size"
+        view="overall"
+        {...props}
+      />
+    </CurrentUserContextProvider>,
+    '/',
+    { appState: mockAppState({ qualifiers: [ComponentQualifier.Application] }) }
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PerspectiveSelect-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PerspectiveSelect-test.tsx
deleted file mode 100644 (file)
index 390d1f9..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 ListIcon from '../../../../components/icons/ListIcon';
-import { mockReactSelectOptionProps } from '../../../../helpers/mocks/react-select';
-import PerspectiveSelect from '../PerspectiveSelect';
-
-it('should render correctly', () => {
-  expect(shallow(<PerspectiveSelect onChange={jest.fn()} view="overall" />)).toMatchSnapshot();
-});
-
-it('should render option correctly', () => {
-  const wrapper = shallowRender();
-  const OptionRender = wrapper.instance().perspectiveOptionRender;
-  const option = shallow(
-    <OptionRender {...mockReactSelectOptionProps({ value: 'test', label: 'test' })} />
-  );
-  expect(option.find(ListIcon).type).toBeDefined();
-});
-
-it('should handle perspective change correctly', () => {
-  const onChange = jest.fn();
-  const instance = shallowRender({ onChange }).instance();
-  instance.handleChange({ label: 'overall', value: 'overall' });
-  instance.handleChange({ label: 'leak', value: 'leak' });
-  expect(onChange.mock.calls).toMatchSnapshot();
-});
-
-function shallowRender(overrides: Partial<PerspectiveSelect['props']> = {}) {
-  return shallow<PerspectiveSelect>(
-    <PerspectiveSelect onChange={jest.fn()} view="overall" {...overrides} />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx
deleted file mode 100644 (file)
index e011eae..0000000
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { getAlmSettings } from '../../../../api/alm-settings';
-import { mockLoggedInUser } from '../../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-import { AlmKeys } from '../../../../types/alm-settings';
-import { ProjectCreationMenu } from '../ProjectCreationMenu';
-
-jest.mock('../../../../api/alm-settings', () => ({
-  getAlmSettings: jest.fn().mockResolvedValue([]),
-}));
-
-beforeEach(() => {
-  jest.clearAllMocks();
-});
-
-it('should render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot('default');
-  expect(
-    shallowRender({ currentUser: mockLoggedInUser({ permissions: { global: [] } }) })
-  ).toMatchSnapshot('not allowed');
-});
-
-it('should fetch alm bindings on mount', async () => {
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  expect(getAlmSettings).toHaveBeenCalled();
-});
-
-it('should not fetch alm bindings if user cannot create projects', async () => {
-  const wrapper = shallowRender({ currentUser: mockLoggedInUser({ permissions: { global: [] } }) });
-  await waitAndUpdate(wrapper);
-  expect(getAlmSettings).not.toHaveBeenCalled();
-});
-
-it('should filter alm bindings appropriately', async () => {
-  (getAlmSettings as jest.Mock)
-    .mockResolvedValueOnce([
-      // Only faulty configs.
-      { alm: AlmKeys.Azure }, // Missing some configuration; will be ignored.
-      { alm: AlmKeys.GitLab }, // Missing some configuration; will be ignored.
-    ])
-    .mockResolvedValueOnce([
-      // All correct configs.
-      { alm: AlmKeys.Azure, url: 'http://ado.example.com' },
-      { alm: AlmKeys.BitbucketServer, url: 'b1' },
-      { alm: AlmKeys.GitHub },
-      { alm: AlmKeys.GitLab, url: 'gitlab.com' },
-    ])
-    .mockResolvedValueOnce([
-      // All correct configs.
-      { alm: AlmKeys.Azure, url: 'http://ado.example.com' },
-      { alm: AlmKeys.BitbucketCloud },
-      { alm: AlmKeys.GitHub },
-      { alm: AlmKeys.GitLab, url: 'gitlab.com' },
-    ])
-    .mockResolvedValueOnce([
-      // Special case for BBS with BBC
-      { alm: AlmKeys.Azure, url: 'http://ado.example.com' },
-      { alm: AlmKeys.BitbucketServer, url: 'b1' },
-      { alm: AlmKeys.BitbucketCloud },
-      { alm: AlmKeys.GitHub },
-      { alm: AlmKeys.GitLab, url: 'gitlab.com' },
-    ])
-    .mockResolvedValueOnce([
-      // Only duplicate ALMs; should all be ignored.
-      { alm: AlmKeys.Azure, url: 'http://ado.example.com' },
-      { alm: AlmKeys.Azure, url: 'http://ado.example.com' },
-      { alm: AlmKeys.BitbucketServer, url: 'b1' },
-      { alm: AlmKeys.BitbucketServer, url: 'b1' },
-      { alm: AlmKeys.GitHub },
-      { alm: AlmKeys.GitHub },
-      { alm: AlmKeys.GitLab, url: 'gitlab.com' },
-      { alm: AlmKeys.GitLab, url: 'gitlab.com' },
-    ]);
-
-  let wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  expect(wrapper.state().boundAlms).toEqual([]);
-
-  wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  expect(wrapper.state().boundAlms).toEqual([
-    AlmKeys.Azure,
-    AlmKeys.BitbucketServer,
-    AlmKeys.GitHub,
-    AlmKeys.GitLab,
-  ]);
-
-  wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  expect(wrapper.state().boundAlms).toEqual([
-    AlmKeys.Azure,
-    AlmKeys.BitbucketCloud,
-    AlmKeys.GitHub,
-    AlmKeys.GitLab,
-  ]);
-
-  wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  expect(wrapper.state().boundAlms).toEqual([
-    AlmKeys.Azure,
-    AlmKeys.BitbucketServer,
-    AlmKeys.BitbucketCloud,
-    AlmKeys.GitHub,
-    AlmKeys.GitLab,
-  ]);
-
-  wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  expect(wrapper.state().boundAlms).toEqual([
-    AlmKeys.Azure,
-    AlmKeys.BitbucketServer,
-    AlmKeys.GitHub,
-    AlmKeys.GitLab,
-  ]);
-});
-
-function shallowRender(overrides: Partial<ProjectCreationMenu['props']> = {}) {
-  return shallow<ProjectCreationMenu>(
-    <ProjectCreationMenu
-      currentUser={mockLoggedInUser({ permissions: { global: ['provisioning'] } })}
-      {...overrides}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenuItem-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenuItem-test.tsx
deleted file mode 100644 (file)
index ccdc243..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { AlmKeys } from '../../../../types/alm-settings';
-import ProjectCreationMenuItem, { ProjectCreationMenuItemProps } from '../ProjectCreationMenuItem';
-
-it('should render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot('bitbucket');
-  expect(shallowRender({ alm: 'manual' })).toMatchSnapshot('manual');
-});
-
-function shallowRender(overrides: Partial<ProjectCreationMenuItemProps> = {}) {
-  return shallow(<ProjectCreationMenuItem alm={AlmKeys.BitbucketServer} {...overrides} />);
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectsSortingSelect-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectsSortingSelect-test.tsx
deleted file mode 100644 (file)
index 4fde703..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { GroupBase } from 'react-select';
-import { mockReactSelectOptionProps } from '../../../../helpers/mocks/react-select';
-import { click } from '../../../../helpers/testUtils';
-import ProjectsSortingSelect, { Option } from '../ProjectsSortingSelect';
-
-it('should render correctly for overall view', () => {
-  expect(shallowRender()).toMatchSnapshot();
-});
-
-it('should render correctly for leak view', () => {
-  expect(
-    shallowRender({ defaultOption: 'analysis_date', selectedSort: 'new_coverage', view: 'leak' })
-  ).toMatchSnapshot();
-});
-
-it('should handle the descending sort direction', () => {
-  expect(shallowRender({ selectedSort: '-vulnerability' })).toMatchSnapshot();
-});
-
-it('should render option correctly', () => {
-  const wrapper = shallowRender();
-  const SortOption = wrapper.instance().projectsSortingSelectOption;
-  expect(
-    shallow(
-      <SortOption
-        {...mockReactSelectOptionProps<Option, false, GroupBase<Option>>({
-          label: 'foo',
-          value: 'foo',
-          short: 'fo',
-        })}
-      />
-    )
-  ).toMatchSnapshot();
-  expect(
-    shallow(
-      <SortOption
-        {...mockReactSelectOptionProps<Option, false, GroupBase<Option>>({
-          label: 'foo',
-          value: 'foo',
-        })}
-      />
-    )
-  ).toMatchSnapshot();
-});
-
-it('changes sorting', () => {
-  const onChange = jest.fn();
-  const instance = shallowRender({
-    selectedSort: '-vulnerability',
-    onChange,
-  }).instance() as ProjectsSortingSelect;
-  instance.handleSortChange({ label: 'size', value: 'size' });
-  expect(onChange).toHaveBeenCalledWith('size', true);
-});
-
-it('reverses sorting', () => {
-  const onChange = jest.fn();
-  const wrapper = shallowRender({ selectedSort: '-size', onChange });
-  click(wrapper.find('ButtonIcon'));
-  expect(onChange).toHaveBeenCalledWith('size', false);
-
-  const node = document.createElement('div');
-  node.focus = jest.fn();
-  wrapper.instance().sortOrderButtonNode = node;
-
-  click(wrapper.find('ButtonIcon'));
-
-  expect(node.focus).toHaveBeenCalled();
-});
-
-function shallowRender(overrides: Partial<ProjectsSortingSelect['props']> = {}) {
-  return shallow<ProjectsSortingSelect>(
-    <ProjectsSortingSelect
-      defaultOption="name"
-      onChange={jest.fn()}
-      selectedSort="name"
-      view="overall"
-      {...overrides}
-    />
-  );
-}
index 9bad4f35742cfad5042b0f5b220a8730e9ba73c7..ed8713be74e54166cee31ea0ac5af0dd5e4ff82a 100644 (file)
@@ -93,56 +93,51 @@ exports[`renders 1`] = `
         list_of_projects
       </h2>
       <div
-        className="layout-page-header-panel layout-page-main-header"
+        style={
+          {
+            "height": "120px",
+          }
+        }
       >
-        <div
-          className="layout-page-header-panel-inner layout-page-main-header-inner"
-        >
-          <div
-            className="layout-page-main-inner"
-          >
-            <PageHeader
-              currentUser={
-                {
-                  "dismissedNotices": {},
-                  "isLoggedIn": true,
-                }
-              }
-              loading={false}
-              onPerspectiveChange={[Function]}
-              onQueryChange={[Function]}
-              onSortChange={[Function]}
-              query={
-                {
-                  "coverage": undefined,
-                  "duplications": undefined,
-                  "gate": undefined,
-                  "languages": undefined,
-                  "maintainability": undefined,
-                  "new_coverage": undefined,
-                  "new_duplications": undefined,
-                  "new_lines": undefined,
-                  "new_maintainability": undefined,
-                  "new_reliability": undefined,
-                  "new_security": undefined,
-                  "new_security_review_rating": undefined,
-                  "qualifier": undefined,
-                  "reliability": undefined,
-                  "search": undefined,
-                  "security": undefined,
-                  "security_review_rating": undefined,
-                  "size": undefined,
-                  "sort": undefined,
-                  "tags": undefined,
-                  "view": undefined,
-                }
-              }
-              selectedSort="name"
-              total={0}
-              view="overall"
-            />
-          </div>
-        </div>
+        <PageHeader
+          currentUser={
+            {
+              "dismissedNotices": {},
+              "isLoggedIn": true,
+            }
+          }
+          onPerspectiveChange={[Function]}
+          onQueryChange={[Function]}
+          onSortChange={[Function]}
+          query={
+            {
+              "coverage": undefined,
+              "duplications": undefined,
+              "gate": undefined,
+              "languages": undefined,
+              "maintainability": undefined,
+              "new_coverage": undefined,
+              "new_duplications": undefined,
+              "new_lines": undefined,
+              "new_maintainability": undefined,
+              "new_reliability": undefined,
+              "new_security": undefined,
+              "new_security_review_rating": undefined,
+              "qualifier": undefined,
+              "reliability": undefined,
+              "search": undefined,
+              "security": undefined,
+              "security_review_rating": undefined,
+              "size": undefined,
+              "sort": undefined,
+              "tags": undefined,
+              "view": undefined,
+            }
+          }
+          selectedSort="name"
+          total={0}
+          view="overall"
+        />
       </div>
       <div
         className="layout-page-main-inner"
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ApplicationCreation-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ApplicationCreation-test.tsx.snap
deleted file mode 100644 (file)
index 9647b1e..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly: available 1`] = `
-<div>
-  <Button
-    className="button-primary"
-    onClick={[Function]}
-  >
-    projects.create_application
-  </Button>
-</div>
-`;
-
-exports[`should render correctly: not allowed 1`] = `""`;
-
-exports[`should render correctly: unavailable 1`] = `""`;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.tsx.snap
deleted file mode 100644 (file)
index fcc1c01..0000000
+++ /dev/null
@@ -1,169 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<div
-  className="page-header"
->
-  <div
-    className="display-flex-center projects-header-row display-flex-space-between"
-  >
-    <SearchFilterContainer
-      onQueryChange={[MockFunction]}
-      query={
-        {
-          "search": "test",
-        }
-      }
-    />
-    <div
-      className="display-flex-center"
-    >
-      <withCurrentUserContext(ProjectCreationMenu)
-        className="little-spacer-right"
-      />
-      <withCurrentUserContext(withRouter(withAppStateContext(ApplicationCreation)))
-        className="little-spacer-right"
-      />
-      <withCurrentUserContext(HomePageSelect)
-        className="spacer-left little-spacer-right"
-        currentPage={
-          {
-            "type": "PROJECTS",
-          }
-        }
-      />
-    </div>
-  </div>
-  <div
-    className="spacer-top projects-header-row display-flex-space-between"
-  >
-    <div
-      className="display-flex-center"
-    >
-      <span
-        className="projects-total-label"
-      >
-        <strong
-          id="projects-total"
-        >
-          12
-        </strong>
-         
-        projects_
-      </span>
-    </div>
-    <div
-      className="display-flex-center"
-    >
-      <PerspectiveSelect
-        className="projects-topbar-item"
-        onChange={[MockFunction]}
-        view="overall"
-      />
-      <div
-        className="projects-topbar-item"
-      >
-        <ProjectsSortingSelect
-          defaultOption="analysis_date"
-          onChange={[MockFunction]}
-          selectedSort="size"
-          view="overall"
-        />
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render correctly while loading 1`] = `
-<div
-  className="page-header"
->
-  <div
-    className="display-flex-center projects-header-row display-flex-space-between"
-  >
-    <SearchFilterContainer
-      onQueryChange={[MockFunction]}
-      query={
-        {
-          "search": "test",
-        }
-      }
-    />
-    <div
-      className="display-flex-center"
-    >
-      <withCurrentUserContext(ProjectCreationMenu)
-        className="little-spacer-right"
-      />
-      <withCurrentUserContext(withRouter(withAppStateContext(ApplicationCreation)))
-        className="little-spacer-right"
-      />
-      <withCurrentUserContext(HomePageSelect)
-        className="spacer-left little-spacer-right"
-        currentPage={
-          {
-            "type": "PROJECTS",
-          }
-        }
-      />
-    </div>
-  </div>
-  <div
-    className="spacer-top projects-header-row display-flex-space-between"
-  >
-    <div
-      className="display-flex-center is-loading"
-    >
-      <span
-        className="projects-total-label"
-      >
-        <strong
-          id="projects-total"
-        >
-          2
-        </strong>
-         
-        projects_
-      </span>
-    </div>
-    <div
-      className="display-flex-center"
-    >
-      <PerspectiveSelect
-        className="projects-topbar-item"
-        onChange={[MockFunction]}
-        view="overall"
-      />
-      <div
-        className="projects-topbar-item"
-      >
-        <ProjectsSortingSelect
-          defaultOption="analysis_date"
-          onChange={[MockFunction]}
-          selectedSort="size"
-          view="overall"
-        />
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`should render switch the default sorting option for anonymous users 1`] = `
-<ProjectsSortingSelect
-  defaultOption="name"
-  onChange={[MockFunction]}
-  selectedSort="size"
-  view="overall"
-/>
-`;
-
-exports[`should render switch the default sorting option for anonymous users 2`] = `
-<ProjectsSortingSelect
-  defaultOption="analysis_date"
-  onChange={[MockFunction]}
-  selectedSort="size"
-  view="leak"
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PerspectiveSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PerspectiveSelect-test.tsx.snap
deleted file mode 100644 (file)
index f334c54..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should handle perspective change correctly 1`] = `
-[
-  [
-    {
-      "view": "overall",
-    },
-  ],
-  [
-    {
-      "view": "leak",
-    },
-  ],
-]
-`;
-
-exports[`should render correctly 1`] = `
-<div>
-  <label
-    id="aria-projects-perspective"
-  >
-    projects.perspective
-    :
-  </label>
-  <Select
-    aria-labelledby="aria-projects-perspective"
-    className="little-spacer-left input-medium it__projects-perspective-select"
-    components={
-      {
-        "Option": [Function],
-      }
-    }
-    isClearable={false}
-    isSearchable={false}
-    onChange={[Function]}
-    options={
-      [
-        {
-          "label": "projects.view.overall",
-          "value": "overall",
-        },
-        {
-          "label": "projects.view.new_code",
-          "value": "leak",
-        },
-      ]
-    }
-    value={
-      {
-        "label": "projects.view.overall",
-        "value": "overall",
-      }
-    }
-  />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenu-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenu-test.tsx.snap
deleted file mode 100644 (file)
index 7cb9171..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly: default 1`] = `
-<Dropdown
-  onOpen={[Function]}
-  overlay={
-    <ul
-      className="menu"
-    >
-      <li
-        className="little-spacer-bottom"
-      >
-        <ProjectCreationMenuItem
-          alm="manual"
-        />
-      </li>
-      <li
-        className="bordered-top little-padded-top"
-      >
-        <ForwardRef(Link)
-          className="display-flex-center"
-          to={
-            {
-              "pathname": "/projects/create",
-            }
-          }
-        >
-          <EllipsisIcon
-            className="spacer-right"
-            size={16}
-          />
-          more
-        </ForwardRef(Link)>
-      </li>
-    </ul>
-  }
->
-  <Button
-    className="button-primary"
-  >
-    projects.add
-    <DropdownIcon
-      className="spacer-left "
-    />
-  </Button>
-</Dropdown>
-`;
-
-exports[`should render correctly: not allowed 1`] = `""`;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenuItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenuItem-test.tsx.snap
deleted file mode 100644 (file)
index 6a5d0e5..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly: bitbucket 1`] = `
-<ForwardRef(Link)
-  className="display-flex-center"
-  to={
-    {
-      "pathname": "/projects/create",
-      "search": "?mode=bitbucket",
-    }
-  }
->
-  <img
-    alt="bitbucket"
-    className="spacer-right"
-    src="/images/alm/bitbucket.svg"
-    width={16}
-  />
-  my_account.add_project.bitbucket
-</ForwardRef(Link)>
-`;
-
-exports[`should render correctly: manual 1`] = `
-<ForwardRef(Link)
-  className="display-flex-center"
-  to={
-    {
-      "pathname": "/projects/create",
-      "search": "?mode=manual",
-    }
-  }
->
-  <ChevronsIcon
-    className="spacer-right"
-  />
-  my_account.add_project.manual
-</ForwardRef(Link)>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsSortingSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsSortingSelect-test.tsx.snap
deleted file mode 100644 (file)
index 864ca53..0000000
+++ /dev/null
@@ -1,300 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should handle the descending sort direction 1`] = `
-<div>
-  <label
-    id="aria-projects-sort"
-  >
-    projects.sort_by
-    :
-  </label>
-  <Select
-    aria-labelledby="aria-projects-sort"
-    className="little-spacer-left input-medium it__projects-sort-select"
-    components={
-      {
-        "Option": [Function],
-      }
-    }
-    isClearable={false}
-    isSearchable={false}
-    onChange={[Function]}
-    options={
-      [
-        {
-          "className": undefined,
-          "label": "projects.sorting.name",
-          "value": "name",
-        },
-        {
-          "className": undefined,
-          "label": "projects.sorting.analysis_date",
-          "value": "analysis_date",
-        },
-        {
-          "className": undefined,
-          "label": "projects.sorting.reliability",
-          "value": "reliability",
-        },
-        {
-          "className": undefined,
-          "label": "projects.sorting.security",
-          "value": "security",
-        },
-        {
-          "className": undefined,
-          "label": "projects.sorting.security_review",
-          "value": "security_review",
-        },
-        {
-          "className": undefined,
-          "label": "projects.sorting.maintainability",
-          "value": "maintainability",
-        },
-        {
-          "className": undefined,
-          "label": "projects.sorting.coverage",
-          "value": "coverage",
-        },
-        {
-          "className": undefined,
-          "label": "projects.sorting.duplications",
-          "value": "duplications",
-        },
-        {
-          "className": undefined,
-          "label": "projects.sorting.size",
-          "value": "size",
-        },
-      ]
-    }
-  />
-  <Tooltip
-    mouseLeaveDelay={1}
-    overlay="projects.sort_descending"
-  >
-    <ButtonIcon
-      aria-label="projects.sort_descending"
-      className="js-projects-sorting-invert spacer-left"
-      color="#525252"
-      innerRef={[Function]}
-      onClick={[Function]}
-    >
-      <SortDescIcon />
-    </ButtonIcon>
-  </Tooltip>
-</div>
-`;
-
-exports[`should render correctly for leak view 1`] = `
-<div>
-  <label
-    id="aria-projects-sort"
-  >
-    projects.sort_by
-    :
-  </label>
-  <Select
-    aria-labelledby="aria-projects-sort"
-    className="little-spacer-left input-medium it__projects-sort-select"
-    components={
-      {
-        "Option": [Function],
-      }
-    }
-    isClearable={false}
-    isSearchable={false}
-    onChange={[Function]}
-    options={
-      [
-        {
-          "className": undefined,
-          "label": "projects.sorting.analysis_date",
-          "value": "analysis_date",
-        },
-        {
-          "className": undefined,
-          "label": "projects.sorting.name",
-          "value": "name",
-        },
-        {
-          "className": "projects-leak-sorting-option",
-          "label": "projects.sorting.new_reliability",
-          "value": "new_reliability",
-        },
-        {
-          "className": "projects-leak-sorting-option",
-          "label": "projects.sorting.new_security",
-          "value": "new_security",
-        },
-        {
-          "className": "projects-leak-sorting-option",
-          "label": "projects.sorting.new_security_review",
-          "value": "new_security_review",
-        },
-        {
-          "className": "projects-leak-sorting-option",
-          "label": "projects.sorting.new_maintainability",
-          "value": "new_maintainability",
-        },
-        {
-          "className": "projects-leak-sorting-option",
-          "label": "projects.sorting.new_coverage",
-          "value": "new_coverage",
-        },
-        {
-          "className": "projects-leak-sorting-option",
-          "label": "projects.sorting.new_duplications",
-          "value": "new_duplications",
-        },
-        {
-          "className": "projects-leak-sorting-option",
-          "label": "projects.sorting.new_lines",
-          "value": "new_lines",
-        },
-      ]
-    }
-    value={
-      {
-        "className": "projects-leak-sorting-option",
-        "label": "projects.sorting.new_coverage",
-        "value": "new_coverage",
-      }
-    }
-  />
-  <Tooltip
-    mouseLeaveDelay={1}
-    overlay="projects.sort_ascending"
-  >
-    <ButtonIcon
-      aria-label="projects.sort_ascending"
-      className="js-projects-sorting-invert spacer-left"
-      color="#525252"
-      innerRef={[Function]}
-      onClick={[Function]}
-    >
-      <SortAscIcon />
-    </ButtonIcon>
-  </Tooltip>
-</div>
-`;
-
-exports[`should render correctly for overall view 1`] = `
-<div>
-  <label
-    id="aria-projects-sort"
-  >
-    projects.sort_by
-    :
-  </label>
-  <Select
-    aria-labelledby="aria-projects-sort"
-    className="little-spacer-left input-medium it__projects-sort-select"
-    components={
-      {
-        "Option": [Function],
-      }
-    }
-    isClearable={false}
-    isSearchable={false}
-    onChange={[Function]}
-    options={
-      [
-        {
-          "className": undefined,
-          "label": "projects.sorting.name",
-          "value": "name",
-        },
-        {
-          "className": undefined,
-          "label": "projects.sorting.analysis_date",
-          "value": "analysis_date",
-        },
-        {
-          "className": undefined,
-          "label": "projects.sorting.reliability",
-          "value": "reliability",
-        },
-        {
-          "className": undefined,
-          "label": "projects.sorting.security",
-          "value": "security",
-        },
-        {
-          "className": undefined,
-          "label": "projects.sorting.security_review",
-          "value": "security_review",
-        },
-        {
-          "className": undefined,
-          "label": "projects.sorting.maintainability",
-          "value": "maintainability",
-        },
-        {
-          "className": undefined,
-          "label": "projects.sorting.coverage",
-          "value": "coverage",
-        },
-        {
-          "className": undefined,
-          "label": "projects.sorting.duplications",
-          "value": "duplications",
-        },
-        {
-          "className": undefined,
-          "label": "projects.sorting.size",
-          "value": "size",
-        },
-      ]
-    }
-    value={
-      {
-        "className": undefined,
-        "label": "projects.sorting.name",
-        "value": "name",
-      }
-    }
-  />
-  <Tooltip
-    mouseLeaveDelay={1}
-    overlay="projects.sort_ascending"
-  >
-    <ButtonIcon
-      aria-label="projects.sort_ascending"
-      className="js-projects-sorting-invert spacer-left"
-      color="#525252"
-      innerRef={[Function]}
-      onClick={[Function]}
-    >
-      <SortAscIcon />
-    </ButtonIcon>
-  </Tooltip>
-</div>
-`;
-
-exports[`should render option correctly 1`] = `
-<Option
-  className="it__project-sort-option-foo undefined"
-  data={
-    {
-      "label": "foo",
-      "short": "fo",
-      "value": "foo",
-    }
-  }
->
-  fo
-</Option>
-`;
-
-exports[`should render option correctly 2`] = `
-<Option
-  className="it__project-sort-option-foo undefined"
-  data={
-    {
-      "label": "foo",
-      "value": "foo",
-    }
-  }
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.tsx b/server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.tsx
deleted file mode 100644 (file)
index b22eaac..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import * as React from 'react';
-import SearchBox from '../../../components/controls/SearchBox';
-import { translate } from '../../../helpers/l10n';
-
-interface Props {
-  query: { search?: string };
-  onQueryChange: (change: { search?: string }) => void;
-}
-
-export default class SearchFilterContainer extends React.PureComponent<Props> {
-  handleSearch = (search?: string) => {
-    this.props.onQueryChange({ search });
-  };
-
-  render() {
-    return (
-      <div className="projects-topbar-item projects-topbar-item-search">
-        <SearchBox
-          minLength={2}
-          onChange={this.handleSearch}
-          placeholder={translate('projects.search')}
-          value={this.props.query.search ?? ''}
-        />
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilterContainer-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilterContainer-test.tsx
deleted file mode 100644 (file)
index 31144b6..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 SearchFilterContainer from '../SearchFilterContainer';
-
-it('searches', () => {
-  const onQueryChange = jest.fn();
-  const wrapper = shallow(<SearchFilterContainer onQueryChange={onQueryChange} query={{}} />);
-  expect(wrapper).toMatchSnapshot();
-  wrapper.find('SearchBox').prop<Function>('onChange')('foo');
-  expect(onQueryChange).toHaveBeenCalledWith({ search: 'foo' });
-});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilterContainer-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilterContainer-test.tsx.snap
deleted file mode 100644 (file)
index b20d22b..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`searches 1`] = `
-<div
-  className="projects-topbar-item projects-topbar-item-search"
->
-  <SearchBox
-    minLength={2}
-    onChange={[Function]}
-    placeholder="projects.search"
-    value=""
-  />
-</div>
-`;
index fe61c5bcbd0f7f6ce424c4dff96c2f4c9eb9cd29..9d1b6ed22920ed44712f79928b8deeade6d1794b 100644 (file)
@@ -17,7 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import classNames from 'classnames';
+import { LightLabel, RequiredIcon } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { translate } from '../../helpers/l10n';
@@ -28,12 +28,12 @@ export interface MandatoryFieldsExplanationProps {
 
 export default function MandatoryFieldsExplanation({ className }: MandatoryFieldsExplanationProps) {
   return (
-    <div aria-hidden className={classNames('text-muted', className)}>
+    <LightLabel aria-hidden className={className}>
       <FormattedMessage
         id="fields_marked_with_x_required"
         defaultMessage={translate('fields_marked_with_x_required')}
-        values={{ star: <em className="mandatory">*</em> }}
+        values={{ star: <RequiredIcon className="sw-m-0" /> }}
       />
-    </div>
+    </LightLabel>
   );
 }
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/MandatoryFieldsExplanation-test.tsx b/server/sonar-web/src/main/js/components/ui/__tests__/MandatoryFieldsExplanation-test.tsx
deleted file mode 100644 (file)
index 8c2f733..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 MandatoryFieldsExplanation, {
-  MandatoryFieldsExplanationProps,
-} from '../MandatoryFieldsExplanation';
-
-it('should render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot('default');
-  expect(shallowRender({ className: 'foo-bar' })).toMatchSnapshot('with className');
-});
-
-function shallowRender(props: Partial<MandatoryFieldsExplanationProps> = {}) {
-  return shallow<MandatoryFieldsExplanationProps>(<MandatoryFieldsExplanation {...props} />);
-}
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/MandatoryFieldsExplanation-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/MandatoryFieldsExplanation-test.tsx.snap
deleted file mode 100644 (file)
index ef33757..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly: default 1`] = `
-<div
-  aria-hidden={true}
-  className="text-muted"
->
-  <FormattedMessage
-    defaultMessage="fields_marked_with_x_required"
-    id="fields_marked_with_x_required"
-    values={
-      {
-        "star": <em
-          className="mandatory"
-        >
-          *
-        </em>,
-      }
-    }
-  />
-</div>
-`;
-
-exports[`should render correctly: with className 1`] = `
-<div
-  aria-hidden={true}
-  className="text-muted foo-bar"
->
-  <FormattedMessage
-    defaultMessage="fields_marked_with_x_required"
-    id="fields_marked_with_x_required"
-    values={
-      {
-        "star": <em
-          className="mandatory"
-        >
-          *
-        </em>,
-      }
-    }
-  />
-</div>
-`;
index 4a59ca860bd5ca2fe325fdd563b3c4287ead19e1..b7d497c8c88a55599985d087a332c44917d638e5 100644 (file)
@@ -104,6 +104,7 @@ module.exports = {
     },
     zIndex: {
       normal: '2',
+      'project-list-header': '30',
       filterbar: '50',
       'content-popup': '52',
       'filterbar-header': '55',