]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20814 Migrating project new code branch list to new UI
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Thu, 19 Oct 2023 14:57:34 +0000 (16:57 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 25 Oct 2023 20:02:59 +0000 (20:02 +0000)
server/sonar-web/design-system/src/components/FlagMessage.tsx
server/sonar-web/design-system/src/components/__tests__/FlagMessage-test.tsx
server/sonar-web/design-system/src/components/index.ts
server/sonar-web/src/main/js/apps/projectNewCode/components/BranchList.tsx
server/sonar-web/src/main/js/apps/projectNewCode/components/BranchListRow.tsx
server/sonar-web/src/main/js/apps/projectNewCode/components/ProjectNewCodeDefinitionApp.tsx
server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/ProjectNewCodeDefinitionApp-it.tsx
server/sonar-web/src/main/js/components/new-code-definition/BranchNCDAutoUpdateMessage.tsx

index 16a120119d01c6b115670520dde2381e261d6601..d395d2e8e7a482c22d12bae570473533c11aea4f 100644 (file)
 import styled from '@emotion/styled';
 import classNames from 'classnames';
 import * as React from 'react';
+import { useIntl } from 'react-intl';
 import tw from 'twin.macro';
 import { themeBorder, themeColor, themeContrast } from '../helpers/theme';
 import { ThemeColors } from '../types/theme';
-import { FlagErrorIcon, FlagInfoIcon, FlagSuccessIcon, FlagWarningIcon } from './icons';
+import { InteractiveIcon } from './InteractiveIcon';
+import { CloseIcon, FlagErrorIcon, FlagInfoIcon, FlagSuccessIcon, FlagWarningIcon } from './icons';
 
 export type Variant = 'error' | 'warning' | 'success' | 'info';
 
@@ -79,6 +81,31 @@ export function FlagMessage(props: Props & React.HTMLAttributes<HTMLDivElement>)
 
 FlagMessage.displayName = 'FlagMessage'; // so that tests don't see the obfuscated production name
 
+interface DismissableFlagMessageProps extends Props {
+  onDismiss: () => void;
+}
+
+export function DismissableFlagMessage(
+  props: DismissableFlagMessageProps & React.HTMLAttributes<HTMLDivElement>,
+) {
+  const { onDismiss, children, ...flagMessageProps } = props;
+  const intl = useIntl();
+  return (
+    <FlagMessage {...flagMessageProps}>
+      {children}
+      <DismissIcon
+        Icon={CloseIcon}
+        aria-label={intl.formatMessage({ id: 'dismiss' })}
+        className="sw-ml-3"
+        onClick={onDismiss}
+        size="small"
+      />
+    </FlagMessage>
+  );
+}
+
+DismissableFlagMessage.displayName = 'DismissableFlagMessage'; // so that tests don't see the obfuscated production name
+
 export const StyledFlag = styled.div<{
   backGroundColor: ThemeColors;
   borderColor: ThemeColors;
@@ -111,3 +138,13 @@ export const StyledFlag = styled.div<{
     color: ${themeContrast('flagMessageBackground')};
   }
 `;
+
+export const DismissIcon = styled(InteractiveIcon)`
+  --background: ${themeColor('productNews')};
+  --backgroundHover: ${themeColor('productNewsHover')};
+  --color: ${themeContrast('productNews')};
+  --colorHover: ${themeContrast('productNewsHover')};
+  --focus: ${themeColor('interactiveIconFocus', 0.2)};
+
+  height: 28px;
+`;
index 0d1e0dadedef271717ac608ecec190f34bc9cda8..3c8ae5f31d1f694cc4ff5099b2b319288583f07c 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { screen } from '@testing-library/react';
+import { IntlShape } from 'react-intl';
 import { render } from '../../helpers/testUtils';
 import { FCProps } from '../../types/misc';
-import { FlagMessage, Variant } from '../FlagMessage';
+import { DismissableFlagMessage, FlagMessage, Variant } from '../FlagMessage';
+
+jest.mock(
+  'react-intl',
+  () =>
+    ({
+      ...jest.requireActual('react-intl'),
+      useIntl: () => ({
+        formatMessage: ({ id }: { id: string }, values = {}) =>
+          [id, ...Object.values(values)].join('.'),
+      }),
+    }) as IntlShape,
+);
 
 it.each([
   ['error', '1px solid rgb(249,112,102)'],
@@ -35,10 +48,22 @@ it.each([
   expect(item).toHaveStyle({ border: color });
 });
 
+it('should render Dismissable flag message properly', () => {
+  const dismissFunc = jest.fn();
+  render(<DismissableFlagMessage onDismiss={dismissFunc} role="status" variant="error" />);
+  const item = screen.getByRole('status');
+  expect(item).toBeInTheDocument();
+  expect(item).toHaveStyle({ border: '1px solid rgb(249,112,102)' });
+  const dismissButton = screen.getByRole('button');
+  expect(dismissButton).toBeInTheDocument();
+  dismissButton.click();
+  expect(dismissFunc).toHaveBeenCalled();
+});
+
 function renderFlagMessage(props: Partial<FCProps<typeof FlagMessage>> = {}) {
   return render(
     <FlagMessage role="status" variant="error" {...props}>
       This is an error!
-    </FlagMessage>
+    </FlagMessage>,
   );
 }
index 16e93fbc755fc2238ca53e4628526f8dd1f58308..9064185c0b470085d6b299847cbe659a899473a1 100644 (file)
@@ -39,7 +39,7 @@ export * from './FacetBox';
 export * from './FacetItem';
 export { FailedQGConditionLink } from './FailedQGConditionLink';
 export * from './FavoriteButton';
-export { FlagMessage } from './FlagMessage';
+export { DismissableFlagMessage, FlagMessage } from './FlagMessage';
 export * from './FlowStep';
 export * from './HighlightedSection';
 export { Histogram } from './Histogram';
index c13ca443188efcb3c027c0ab33c8934b5818d456..5e4e4662785c03b71cb6f64cbd9625e8c3b23dc7 100644 (file)
@@ -17,6 +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 { ActionCell, ContentCell, Spinner, Table, TableRow } from 'design-system';
 import * as React from 'react';
 import {
   listBranchesNewCodeDefinition,
@@ -27,7 +28,6 @@ import {
   PreviouslyNonCompliantBranchNCD,
   isPreviouslyNonCompliantDaysNCD,
 } from '../../../components/new-code-definition/utils';
-import Spinner from '../../../components/ui/Spinner';
 import { isBranch, sortBranches } from '../../../helpers/branch-like';
 import { translate } from '../../../helpers/l10n';
 import { DEFAULT_NEW_CODE_DEFINITION_TYPE } from '../../../helpers/new-code-definition';
@@ -164,6 +164,14 @@ export default class BranchList extends React.PureComponent<Props, State> {
       return <Spinner />;
     }
 
+    const header = (
+      <TableRow>
+        <ContentCell>{translate('branch_list.branch')}</ContentCell>
+        <ContentCell>{translate('branch_list.current_setting')}</ContentCell>
+        <ActionCell>{translate('branch_list.actions')}</ActionCell>
+      </TableRow>
+    );
+
     return (
       <div>
         {previouslyNonCompliantBranchNCDs && (
@@ -172,29 +180,18 @@ export default class BranchList extends React.PureComponent<Props, State> {
             previouslyNonCompliantBranchNCDs={previouslyNonCompliantBranchNCDs}
           />
         )}
-        <table className="data zebra">
-          <thead>
-            <tr>
-              <th>{translate('branch_list.branch')}</th>
-              <th className="nowrap huge-spacer-right">
-                {translate('branch_list.current_setting')}
-              </th>
-              <th className="thin nowrap">{translate('branch_list.actions')}</th>
-            </tr>
-          </thead>
-          <tbody>
-            {branches.map((branch) => (
-              <BranchListRow
-                branch={branch}
-                existingBranches={branchList.map((b) => b.name)}
-                inheritedSetting={inheritedSetting}
-                key={branch.name}
-                onOpenEditModal={this.openEditModal}
-                onResetToDefault={this.resetToDefault}
-              />
-            ))}
-          </tbody>
-        </table>
+        <Table columnCount={3} header={header}>
+          {branches.map((branch) => (
+            <BranchListRow
+              branch={branch}
+              existingBranches={branchList.map((b) => b.name)}
+              inheritedSetting={inheritedSetting}
+              key={branch.name}
+              onOpenEditModal={this.openEditModal}
+              onResetToDefault={this.resetToDefault}
+            />
+          ))}
+        </Table>
         {editedBranch && (
           <BranchNewCodeDefinitionSettingModal
             branch={editedBranch}
index da33c27083a25963026dd54a0fb56137f9dac37d..54083c5a365c27be03c996e2df1e556201be7c68 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 {
+  ActionCell,
+  ActionsDropdown,
+  Badge,
+  ContentCell,
+  FlagWarningIcon,
+  InteractiveIcon,
+  ItemButton,
+  PencilIcon,
+  TableRowInteractive,
+} from 'design-system';
 import * as React from 'react';
-import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
 import Tooltip from '../../../components/controls/Tooltip';
 import BranchLikeIcon from '../../../components/icons/BranchLikeIcon';
-import WarningIcon from '../../../components/icons/WarningIcon';
 import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { isNewCodeDefinitionCompliant } from '../../../helpers/new-code-definition';
@@ -101,44 +110,52 @@ export default function BranchListRow(props: BranchListRowProps) {
   const isCompliant = isNewCodeDefinitionCompliant(inheritedSetting);
 
   return (
-    <tr className={settingWarning ? 'branch-setting-warning' : ''}>
-      <td className="nowrap">
-        <BranchLikeIcon branchLike={branch} className="little-spacer-right" />
+    <TableRowInteractive>
+      <ContentCell>
+        <BranchLikeIcon branchLike={branch} className="sw-mr-1" />
         {branch.name}
-        {branch.isMain && (
-          <div className="badge spacer-left">{translate('branches.main_branch')}</div>
-        )}
-      </td>
-      <td className="huge-spacer-right nowrap">
+        {branch.isMain && <Badge className="sw-ml-1">{translate('branches.main_branch')}</Badge>}
+      </ContentCell>
+      <ContentCell>
         <Tooltip overlay={settingWarning}>
           <span>
-            {settingWarning !== undefined && <WarningIcon className="little-spacer-right" />}
+            {settingWarning !== undefined && <FlagWarningIcon className="sw-mr-1" />}
             {branch.newCodePeriod
               ? renderNewCodePeriodSetting(branch.newCodePeriod)
               : translate('branch_list.default_setting')}
           </span>
         </Tooltip>
-      </td>
-      <td className="text-right">
-        <ActionsDropdown
-          label={translateWithParameters('branch_list.show_actions_for_x', branch.name)}
-        >
-          <ActionsDropdownItem onClick={() => props.onOpenEditModal(branch)}>
-            {translate('edit')}
-          </ActionsDropdownItem>
-          {branch.newCodePeriod && (
-            <ActionsDropdownItem
-              disabled={!isCompliant}
-              onClick={() => props.onResetToDefault(branch.name)}
-              tooltipOverlay={
+      </ContentCell>
+      <ActionCell>
+        {!branch.newCodePeriod && (
+          <InteractiveIcon
+            Icon={PencilIcon}
+            aria-label={translate('edit')}
+            onClick={() => props.onOpenEditModal(branch)}
+            className="sw-mr-2"
+            size="small"
+          />
+        )}
+        {branch.newCodePeriod && (
+          <ActionsDropdown allowResizing id="new-code-action">
+            <Tooltip
+              overlay={
                 isCompliant ? null : translate('project_baseline.compliance.warning.title.project')
               }
             >
-              {translate('reset_to_default')}
-            </ActionsDropdownItem>
-          )}
-        </ActionsDropdown>
-      </td>
-    </tr>
+              <ItemButton
+                disabled={!isCompliant}
+                onClick={() => props.onResetToDefault(branch.name)}
+              >
+                {translate('reset_to_default')}
+              </ItemButton>
+            </Tooltip>
+            <ItemButton onClick={() => props.onOpenEditModal(branch)}>
+              {translate('edit')}
+            </ItemButton>
+          </ActionsDropdown>
+        )}
+      </ActionCell>
+    </TableRowInteractive>
   );
 }
index c97144e92cfd8303cb71214df34f2d786f48d091..448f454a3051ab9eb0137ccc64bc85c2ae9318fe 100644 (file)
@@ -17,6 +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 { HeadingDark } from 'design-system';
 import React, { useCallback, useEffect, useMemo, useState } from 'react';
 import { Helmet } from 'react-helmet-async';
 import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
@@ -218,9 +220,10 @@ function ProjectNewCodeDefinitionApp(props: Readonly<ProjectNewCodeDefinitionApp
             )}
 
             {globalNewCodeDefinition && branchSupportEnabled && (
-              <div className="huge-spacer-top branch-baseline-selector">
-                <hr />
-                <h2>{translate('project_baseline.configure_branches')}</h2>
+              <div className="sw-mt-6">
+                <HeadingDark className="sw-mb-4">
+                  {translate('project_baseline.configure_branches')}
+                </HeadingDark>
                 <BranchList
                   branchList={branchList}
                   component={component}
index 2e271e26ec0507ae475df86759a98440fd1061de..7025d1bb8d58554cabb4827eb86292ca60647cda 100644 (file)
@@ -177,7 +177,7 @@ it('renders correctly branch modal', async () => {
   });
   await ui.appIsLoaded();
 
-  await ui.openBranchSettingModal('main');
+  await ui.openBranchSettingModal('main branches.main_branch branch_list.default_setting');
 
   expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
 });
@@ -188,13 +188,13 @@ it('can set a previous version setting for branch', async () => {
     featureList: [Feature.BranchSupport],
   });
   await ui.appIsLoaded();
-  await ui.setBranchPreviousVersionSetting('main');
+  await ui.setBranchPreviousVersionSetting('main branches.main_branch branch_list.default_setting');
 
   expect(
     within(byRole('table').get()).getByText('new_code_definition.previous_version'),
   ).toBeInTheDocument();
 
-  await user.click(await ui.branchActionsButton('main').find());
+  await user.click(await ui.branchActionsButton().find());
 
   expect(ui.resetToDefaultButton.get()).toBeInTheDocument();
   await user.click(ui.resetToDefaultButton.get());
@@ -211,7 +211,10 @@ it('can set a number of days setting for branch', async () => {
   });
   await ui.appIsLoaded();
 
-  await ui.setBranchNumberOfDaysSetting('main', '15');
+  await ui.setBranchNumberOfDaysSetting(
+    'main branches.main_branch branch_list.default_setting',
+    '15',
+  );
 
   expect(
     within(byRole('table').get()).getByText('new_code_definition.number_days: 15'),
@@ -219,7 +222,7 @@ it('can set a number of days setting for branch', async () => {
 });
 
 it('cannot set a specific analysis setting for branch', async () => {
-  const { ui } = getPageObjects();
+  const { ui, user } = getPageObjects();
   newCodeDefinitionMock.setListBranchesNewCode([
     mockNewCodePeriodBranch({
       branchKey: 'main',
@@ -232,8 +235,12 @@ it('cannot set a specific analysis setting for branch', async () => {
   });
   await ui.appIsLoaded();
 
-  await ui.openBranchSettingModal('main');
-
+  await user.click(
+    await byRole('row', { name: /main branches.main_branch/ })
+      .byLabelText('menu')
+      .find(),
+  );
+  await user.click(await byRole('menuitem', { name: 'edit' }).find());
   expect(ui.specificAnalysisRadio.get()).toBeChecked();
   expect(ui.specificAnalysisRadio.get()).toHaveClass('disabled');
   expect(ui.specificAnalysisWarning.get()).toBeInTheDocument();
@@ -251,7 +258,10 @@ it('can set a reference branch setting for branch', async () => {
   });
   await ui.appIsLoaded();
 
-  await ui.setBranchReferenceToBranchSetting('main', 'normal-branch');
+  await ui.setBranchReferenceToBranchSetting(
+    'main branches.main_branch branch_list.default_setting',
+    'normal-branch',
+  );
 
   expect(
     byRole('table').byText('baseline.reference_branch: normal-branch').get(),
@@ -396,12 +406,11 @@ function getPageObjects() {
     analysisListItem: byRole('radio', { name: /baseline.branch_analyses.analysis_for_x/ }),
     saveButton: byRole('button', { name: 'save' }),
     cancelButton: byRole('button', { name: 'cancel' }),
-    branchActionsButton: (branch: string) =>
-      byRole('button', { name: `branch_list.show_actions_for_x.${branch}` }),
+    branchActionsButton: () => byRole('button', { name: `menu` }),
     editButton: byRole('button', { name: 'edit' }),
-    resetToDefaultButton: byRole('button', { name: 'reset_to_default' }),
+    resetToDefaultButton: byRole('menuitem', { name: 'reset_to_default' }),
     branchNCDsBanner: byText(/new_code_definition.auto_update.branch.message/),
-    dismissButton: byLabelText('alert.dismiss'),
+    dismissButton: byLabelText('dismiss'),
   };
 
   async function appIsLoaded() {
@@ -448,8 +457,9 @@ function getPageObjects() {
   }
 
   async function openBranchSettingModal(branch: string) {
-    await user.click(await ui.branchActionsButton(branch).find());
-    await user.click(ui.editButton.get());
+    await user.click(
+      await byRole('row', { name: branch, exact: false }).byLabelText('edit').find(),
+    );
   }
 
   return {
index 197ef8d285b459f6ebe3e89383a76f933fba5865..be25ad81f2f8d357f58c19c7d90fd618dc054d90 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 { DismissableFlagMessage, Link } from 'design-system';
 import React, { useCallback, useEffect, useState } from 'react';
 import { FormattedMessage, useIntl } from 'react-intl';
 import { MessageTypes, checkMessageDismissed, setMessageDismissed } from '../../api/messages';
+import { useDocUrl } from '../../helpers/docs';
 import { Component } from '../../types/types';
-import DocLink from '../common/DocLink';
-import DismissableAlertComponent from '../ui/DismissableAlertComponent';
 import { PreviouslyNonCompliantBranchNCD } from './utils';
 
 interface NCDAutoUpdateMessageProps {
@@ -33,6 +34,9 @@ interface NCDAutoUpdateMessageProps {
 export default function NCDAutoUpdateMessage(props: NCDAutoUpdateMessageProps) {
   const { component, previouslyNonCompliantBranchNCDs } = props;
   const intl = useIntl();
+  const toUrl = useDocUrl(
+    '/project-administration/clean-as-you-code-settings/defining-new-code/#new-code-definition-options',
+  );
 
   const [dismissed, setDismissed] = useState(true);
 
@@ -79,24 +83,17 @@ export default function NCDAutoUpdateMessage(props: NCDAutoUpdateMessageProps) {
   );
 
   return (
-    <DismissableAlertComponent
-      className="sw-my-4"
-      onDismiss={handleBannerDismiss}
-      variant="info"
-      display="banner"
-    >
-      <FormattedMessage
-        id="new_code_definition.auto_update.branch.message"
-        values={{
-          date: new Date(previouslyNonCompliantBranchNCDs[0].updatedAt).toLocaleDateString(),
-          branchesList,
-          link: (
-            <DocLink to="/project-administration/clean-as-you-code-settings/defining-new-code/#new-code-definition-options">
-              {intl.formatMessage({ id: 'learn_more' })}
-            </DocLink>
-          ),
-        }}
-      />
-    </DismissableAlertComponent>
+    <DismissableFlagMessage className="sw-my-4" onDismiss={handleBannerDismiss} variant="info">
+      <div>
+        <FormattedMessage
+          id="new_code_definition.auto_update.branch.message"
+          values={{
+            date: new Date(previouslyNonCompliantBranchNCDs[0].updatedAt).toLocaleDateString(),
+            branchesList,
+            link: <Link to={toUrl}>{intl.formatMessage({ id: 'learn_more' })}</Link>,
+          }}
+        />
+      </div>
+    </DismissableFlagMessage>
   );
 }