]> source.dussan.org Git - sonarqube.git/commitdiff
Fix some code smells in MMF-3035
authorDavid Cho-Lerat <david.cho-lerat@sonarsource.com>
Mon, 13 Mar 2023 11:35:13 +0000 (12:35 +0100)
committersonartech <sonartech@sonarsource.com>
Tue, 14 Mar 2023 20:03:27 +0000 (20:03 +0000)
22 files changed:
server/sonar-web/design-system/src/components/InteractiveIcon.tsx
server/sonar-web/design-system/src/components/buttons.tsx
server/sonar-web/design-system/src/helpers/colors.ts
server/sonar-web/design-system/src/helpers/constants.ts
server/sonar-web/design-system/src/helpers/index.ts
server/sonar-web/design-system/src/theme/light.ts
server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx
server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
server/sonar-web/src/main/js/apps/coding-rules/routes.tsx
server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx
server/sonar-web/src/main/js/apps/projects/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/utils.ts
server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx
server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx
server/sonar-web/src/main/js/components/controls/IdentityProviderLink.tsx
server/sonar-web/src/main/js/components/ui/GenericAvatar.tsx
server/sonar-web/src/main/js/helpers/__tests__/colors-test.ts [deleted file]
server/sonar-web/src/main/js/helpers/colors.ts [deleted file]

index ebd9cb73e9a72adc3dc7b329769e7533224b65f3..03bbe3bdf6ef5d1744e6d4443ef7555e714d4cab 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 { css } from '@emotion/react';
 import styled from '@emotion/styled';
 import classNames from 'classnames';
 import React from 'react';
 import tw from 'twin.macro';
+import { OPACITY_20_PERCENT } from '../helpers/constants';
 import { themeBorder, themeColor, themeContrast } from '../helpers/theme';
 import { isDefined } from '../helpers/types';
 import { ThemedProps } from '../types/theme';
@@ -156,7 +158,7 @@ export const InteractiveIcon: React.FC<InteractiveIconProps> = styled(Interactiv
   --color: ${({ currentColor, theme }) =>
     currentColor ? 'currentColor' : themeContrast('interactiveIcon')({ theme })};
   --colorHover: ${themeContrast('interactiveIconHover')};
-  --focus: ${themeColor('interactiveIconFocus', 0.2)};
+  --focus: ${themeColor('interactiveIconFocus', OPACITY_20_PERCENT)};
 `;
 
 export const DiscreetInteractiveIcon: React.FC<InteractiveIconProps> = styled(InteractiveIcon)`
@@ -168,7 +170,7 @@ export const DestructiveIcon: React.FC<InteractiveIconProps> = styled(Interactiv
   --backgroundHover: ${themeColor('destructiveIconHover')};
   --color: ${themeContrast('destructiveIcon')};
   --colorHover: ${themeContrast('destructiveIconHover')};
-  --focus: ${themeColor('destructiveIconFocus', 0.2)};
+  --focus: ${themeColor('destructiveIconFocus', OPACITY_20_PERCENT)};
 `;
 
 export const DismissProductNewsIcon: React.FC<InteractiveIconProps> = styled(InteractiveIcon)`
@@ -176,7 +178,7 @@ export const DismissProductNewsIcon: React.FC<InteractiveIconProps> = styled(Int
   --backgroundHover: ${themeColor('productNewsHover')};
   --color: ${themeContrast('productNews')};
   --colorHover: ${themeContrast('productNewsHover')};
-  --focus: ${themeColor('interactiveIconFocus', 0.2)};
+  --focus: ${themeColor('interactiveIconFocus', OPACITY_20_PERCENT)};
 
   height: 28px;
 `;
index 442026354eaed46a25a67ac0d0907eabd7f2e9cb..ab0fcf9fb0cc26f4a9ead54807256edc1c63c81b 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 { css } from '@emotion/react';
 import styled from '@emotion/styled';
 import React from 'react';
 import tw from 'twin.macro';
+import { OPACITY_20_PERCENT } from '../helpers/constants';
 import { themeBorder, themeColor, themeContrast } from '../helpers/theme';
 import { ThemedProps } from '../types/theme';
 import { BaseLink, LinkProps } from './Link';
@@ -162,7 +164,7 @@ export const ButtonPrimary: React.FC<ButtonProps> = styled(Button)`
   --background: ${themeColor('button')};
   --backgroundHover: ${themeColor('buttonHover')};
   --color: ${themeContrast('primary')};
-  --focus: ${themeColor('button', 0.2)};
+  --focus: ${themeColor('button', OPACITY_20_PERCENT)};
   --border: ${themeBorder('default', 'transparent')};
 `;
 
@@ -170,7 +172,7 @@ export const ButtonSecondary: React.FC<ButtonProps> = styled(Button)`
   --background: ${themeColor('buttonSecondary')};
   --backgroundHover: ${themeColor('buttonSecondaryHover')};
   --color: ${themeContrast('buttonSecondary')};
-  --focus: ${themeColor('buttonSecondaryBorder', 0.2)};
+  --focus: ${themeColor('buttonSecondaryBorder', OPACITY_20_PERCENT)};
   --border: ${themeBorder('default', 'buttonSecondaryBorder')};
 `;
 
@@ -178,7 +180,7 @@ export const DangerButtonPrimary: React.FC<ButtonProps> = styled(Button)`
   --background: ${themeColor('dangerButton')};
   --backgroundHover: ${themeColor('dangerButtonHover')};
   --color: ${themeContrast('dangerButton')};
-  --focus: ${themeColor('dangerButtonFocus', 0.2)};
+  --focus: ${themeColor('dangerButtonFocus', OPACITY_20_PERCENT)};
   --border: ${themeBorder('default', 'transparent')};
 `;
 
@@ -186,7 +188,7 @@ export const DangerButtonSecondary: React.FC<ButtonProps> = styled(Button)`
   --background: ${themeColor('dangerButtonSecondary')};
   --backgroundHover: ${themeColor('dangerButtonSecondaryHover')};
   --color: ${themeContrast('dangerButtonSecondary')};
-  --focus: ${themeColor('dangerButtonSecondaryFocus', 0.2)};
+  --focus: ${themeColor('dangerButtonSecondaryFocus', OPACITY_20_PERCENT)};
   --border: ${themeBorder('default', 'dangerButtonSecondaryBorder')};
 `;
 
@@ -209,7 +211,7 @@ const ThirdPartyButtonStyled: React.FC<ButtonProps> = styled(Button)`
   --background: ${themeColor('thirdPartyButton')};
   --backgroundHover: ${themeColor('thirdPartyButtonHover')};
   --color: ${themeContrast('thirdPartyButton')};
-  --focus: ${themeColor('thirdPartyButtonBorder', 0.2)};
+  --focus: ${themeColor('thirdPartyButtonBorder', OPACITY_20_PERCENT)};
   --border: ${themeBorder('default', 'thirdPartyButtonBorder')};
 `;
 
index d0cb5e215ca3903625244de45fd5042f305975ff..1fd0b806052ade3cc8f3e7cc2da504bb35b9f7fa 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 { CSSColor } from '../types/theme';
 
 /* eslint-disable no-bitwise, no-mixed-operators */
+
 export function stringToColor(str: string) {
   let hash = 0;
+
   for (let i = 0; i < str.length; i++) {
     hash = str.charCodeAt(i) + ((hash << 5) - hash);
   }
+
   let color = '#';
+
   for (let i = 0; i < 3; i++) {
     const value = (hash >> (i * 8)) & 0xff;
-    color += ('00' + value.toString(16)).substr(-2);
+    color += value.toString(16).padStart(2, '0');
   }
+
   return color;
 }
 
 export function isDarkColor(color: string) {
-  color = color.substr(1);
+  color = color.substring(1);
+
   if (color.length === 3) {
     // shortcut notation: #f90
     color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
   }
-  const rgb = parseInt(color.substr(1), 16);
+
+  const rgb = parseInt(color.substring(1), 16);
   const r = (rgb >> 16) & 0xff;
   const g = (rgb >> 8) & 0xff;
   const b = (rgb >> 0) & 0xff;
   const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
+
   return luma < 140;
 }
 
index 68a385c3c1cfe504cb8aff75b809c3c2427bd832..344d66b7ac5b697948a40e16813928fbf9991c23 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 { theme } from 'twin.macro';
 
 export const DEFAULT_LOCALE = 'en';
@@ -66,3 +67,5 @@ export const LAYOUT_NOTIFICATIONSBAR_WIDTH = 350;
 export const CORE_CONCEPTS_WIDTH = 350;
 
 export const DARK_THEME_ID = 'dark-theme';
+
+export const OPACITY_20_PERCENT = 0.2;
index 764e245473d75c89706b75d1d92307a5e2f56424..427c828d24fae5ce88a2218856239c3c494ca3aa 100644 (file)
@@ -17,5 +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.
  */
+
+export * from './colors';
 export * from './constants';
 export * from './positioning';
index 8b10b3393264350f0d45804fcb14c3d6bc1134b5..56aaefd0d3ae85c3a284682b12dcc0daf6821311 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 { OPACITY_20_PERCENT } from '../helpers/constants';
 import COLORS from './colors';
 
 const primary = {
@@ -106,7 +108,7 @@ const lightTheme = {
     radioHover: COLORS.indigo[50],
     radioFocus: COLORS.indigo[50],
     radioFocusBorder: COLORS.indigo[300],
-    radioFocusOutline: [...COLORS.indigo[300], 0.2],
+    radioFocusOutline: [...COLORS.indigo[300], OPACITY_20_PERCENT],
     radioChecked: COLORS.indigo[50],
     radioDisabled: secondary.default,
     radioDisabledBackground: secondary.light,
@@ -158,7 +160,7 @@ const lightTheme = {
     toggle: COLORS.white,
     toggleBorder: secondary.default,
     toggleHover: secondary.light,
-    toggleFocus: [...secondary.default, 0.2],
+    toggleFocus: [...secondary.default, OPACITY_20_PERCENT],
 
     // code snippet
     codeSnippetBackground: COLORS.blueGrey[25],
@@ -240,7 +242,7 @@ const lightTheme = {
     interactiveIconHover: COLORS.indigo[50],
     interactiveIconFocus: primary.default,
     bannerIcon: 'transparent',
-    bannerIconHover: [...COLORS.red[600], 0.2],
+    bannerIconHover: [...COLORS.red[600], OPACITY_20_PERCENT],
     bannerIconFocus: danger.default,
     discreetInteractiveIcon: secondary.dark,
     destructiveIcon: 'transparent',
@@ -673,7 +675,7 @@ const lightTheme = {
   borders: {
     default: ['1px', 'solid', ...COLORS.grey[50]],
     active: ['3px', 'solid', ...primary.light],
-    focus: ['4px', 'solid', ...secondary.default, 0.2],
+    focus: ['4px', 'solid', ...secondary.default, OPACITY_20_PERCENT],
   },
 
   avatar: {
index 182e8ae11dded43ddb72780330913245add00a14..4527bef160e53605a47b479d0b4490ce7b579017 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 {
   DropdownMenu,
   InputSearch,
@@ -38,7 +39,6 @@ import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
 import { KeyboardKeys } from '../../../helpers/keycodes';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { getKeyboardShortcutEnabled } from '../../../helpers/preferences';
-import { scrollToElement } from '../../../helpers/scrolling';
 import { getComponentOverviewUrl } from '../../../helpers/urls';
 import { ComponentQualifier } from '../../../types/component';
 import { Dict } from '../../../types/types';
@@ -71,6 +71,7 @@ export class GlobalSearch extends React.PureComponent<Props, State> {
     super(props);
     this.nodes = {};
     this.search = debounce(this.search, 250);
+
     this.state = {
       loading: false,
       more: {},
@@ -119,6 +120,7 @@ export class GlobalSearch extends React.PureComponent<Props, State> {
     if (!this.state.open && !this.state.query) {
       this.search('');
     }
+
     this.setState({ open: true });
   };
 
@@ -126,6 +128,7 @@ export class GlobalSearch extends React.PureComponent<Props, State> {
     if (this.input) {
       this.input.blur();
     }
+
     if (clear) {
       this.setState({
         more: {},
@@ -145,6 +148,7 @@ export class GlobalSearch extends React.PureComponent<Props, State> {
       if (more[qualifier]) {
         next.push('qualifier###' + qualifier);
       }
+
       return next;
     }, []);
 
@@ -158,6 +162,7 @@ export class GlobalSearch extends React.PureComponent<Props, State> {
     if (query.length === 0 || query.length >= MIN_SEARCH_QUERY_LENGTH) {
       this.setState({ loading: true });
       const recentlyBrowsed = RecentHistory.get().map((component) => component.key);
+
       getSuggestions(query, recentlyBrowsed).then((response) => {
         // compare `this.state.query` and `query` to handle two request done almost at the same time
         // in this case only the request that matches the current query should be taken
@@ -185,16 +190,19 @@ export class GlobalSearch extends React.PureComponent<Props, State> {
 
   searchMore = (qualifier: string) => {
     const { query } = this.state;
+
     if (query.length === 1) {
       return;
     }
 
     this.setState({ loading: true, loadingMore: qualifier });
     const recentlyBrowsed = RecentHistory.get().map((component) => component.key);
+
     getSuggestions(query, recentlyBrowsed, qualifier).then((response) => {
       if (this.mounted) {
         const group = response.results.find((group) => group.q === qualifier);
         const moreResults = (group ? group.items : []).map((item) => ({ ...item, qualifier }));
+
         this.setState((state) => ({
           loading: false,
           loadingMore: undefined,
@@ -205,6 +213,7 @@ export class GlobalSearch extends React.PureComponent<Props, State> {
           },
           selected: moreResults.length > 0 ? moreResults[0].key : state.selected,
         }));
+
         this.focusInput();
       }
     }, this.stopLoading);
@@ -222,6 +231,7 @@ export class GlobalSearch extends React.PureComponent<Props, State> {
         const index = list.indexOf(selected);
         return index > 0 ? { selected: list[index - 1] } : null;
       }
+
       return null;
     });
   };
@@ -233,6 +243,7 @@ export class GlobalSearch extends React.PureComponent<Props, State> {
         const index = list.indexOf(selected);
         return index >= 0 && index < list.length - 1 ? { selected: list[index + 1] } : null;
       }
+
       return null;
     });
   };
@@ -245,7 +256,7 @@ export class GlobalSearch extends React.PureComponent<Props, State> {
     }
 
     if (selected.startsWith('qualifier###')) {
-      this.searchMore(selected.substr(12));
+      this.searchMore(selected.substring('qualifier###'.length));
     } else {
       let qualifier = ComponentQualifier.Project;
 
@@ -266,11 +277,7 @@ export class GlobalSearch extends React.PureComponent<Props, State> {
       const node = this.nodes[this.state.selected];
 
       if (node && this.node) {
-        scrollToElement(node, {
-          topOffset: 30,
-          bottomOffset: 60,
-          parent: this.node,
-        });
+        node.scrollIntoView();
       }
     }
   };
@@ -279,6 +286,7 @@ export class GlobalSearch extends React.PureComponent<Props, State> {
     if (!getKeyboardShortcutEnabled() || isInput(event) || isShortcut(event)) {
       return true;
     }
+
     if (event.key === KeyboardKeys.KeyS) {
       event.preventDefault();
       this.focusInput();
@@ -348,6 +356,7 @@ export class GlobalSearch extends React.PureComponent<Props, State> {
 
   render() {
     const { open, query, results, more, loadingMore, selected, loading } = this.state;
+
     if (!open && !query) {
       return (
         <Tooltip mouseEnterDelay={INTERACTIVE_TOOLTIP_DELAY} overlay={translate('search_verb')}>
@@ -364,6 +373,7 @@ export class GlobalSearch extends React.PureComponent<Props, State> {
     }
 
     const list = this.getPlainComponentsList(results, more);
+
     const search = (
       <div role="search" className="sw-min-w-abs-200 sw-max-w-abs-350 sw-w-full">
         <PortalPopup
index 99ff1c64e914f4af330f0deb157b4720ba7eb674..34b3b2c16cc989a986587aa339665dfad013811a 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 { getTextColor } from 'design-system';
 import * as React from 'react';
 import { getIdentityProviders } from '../../../api/users';
 import { colors } from '../../../app/theme';
-import { getTextColor } from '../../../helpers/colors';
 import { getBaseUrl } from '../../../helpers/system';
 import { IdentityProvider } from '../../../types/types';
 import { LoggedInUser } from '../../../types/users';
index d74f0b6bd0760bc662330bc474d9828387543253..7cb089ccf7624f32b97e99745f621b8a6df7bfde 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 * as React from 'react';
 import Link from '../../../components/common/Link';
 import { ButtonLink } from '../../../components/controls/buttons';
@@ -217,7 +218,7 @@ export default class RuleDetailsMeta extends React.PureComponent<Props> {
     const EXTERNAL_PREFIX = 'external_';
     const { ruleDetails } = this.props;
     const displayedKey = ruleDetails.key.startsWith(EXTERNAL_PREFIX)
-      ? ruleDetails.key.substr(EXTERNAL_PREFIX.length)
+      ? ruleDetails.key.substring(EXTERNAL_PREFIX.length)
       : ruleDetails.key;
     return <span className="note text-middle">{displayedKey}</span>;
   }
index 5ba7f224b13afd162e5bae43c59ec8fadd2e2e22..5a9221be36b02f04eed3933ba00e21bbd92477b4 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 React, { useEffect } from 'react';
 import { Route, useLocation, useNavigate } from 'react-router-dom';
 import { RawQuery } from '../../types/types';
@@ -44,7 +45,7 @@ function HashEditWrapper() {
   useEffect(() => {
     const { hash } = location;
     if (hash.length > 1) {
-      const query = parseHash(hash.substr(1));
+      const query = parseHash(hash.substring(1));
       const normalizedQuery = {
         ...serializeQuery(parseQuery(query)),
         open: query.open,
index 78107922aacc17c61fd42cd38e8e1bb6fca39328..a1fe1c5d19e9c807487058b4e5bf078f07d64ead 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 * as React from 'react';
 import Issue from '../../../components/issue/Issue';
 import { BranchLike } from '../../../types/branch-like';
@@ -59,7 +60,7 @@ export default class ListItem extends React.PureComponent<Props> {
     const issuesReset = { issues: [] };
 
     if (property.startsWith('tag###')) {
-      const tag = property.substr(6);
+      const tag = property.substring('tag###'.length);
       onFilterChange({ ...issuesReset, tags: [tag] });
     } else {
       switch (property) {
index b6c7496acb2a96b26acd5ef727de0f1cee2594f2..230acd91907ed5ce90588f40cf2a5e2320d0f93a 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 { searchProjects } from '../../../api/components';
 import { mockComponent } from '../../../helpers/mocks/component';
+import { Component } from '../../../types/types';
 import * as utils from '../utils';
 
 jest.mock('../../../api/components', () => ({
@@ -55,11 +57,13 @@ describe('parseSorting', () => {
 });
 
 describe('formatDuration', () => {
-  const ONE_MINUTE = 60000;
+  const ONE_SECOND = 1000;
+  const ONE_MINUTE = 60 * ONE_SECOND;
   const ONE_HOUR = 60 * ONE_MINUTE;
   const ONE_DAY = 24 * ONE_HOUR;
   const ONE_MONTH = 30 * ONE_DAY;
   const ONE_YEAR = 12 * ONE_MONTH;
+
   it('render years and months only', () => {
     expect(utils.formatDuration(ONE_YEAR * 4 + ONE_MONTH * 2 + ONE_DAY * 10)).toEqual(
       'duration.years.4 duration.months.2 '
@@ -81,13 +85,14 @@ describe('formatDuration', () => {
   });
 
   it('render less than a minute', () => {
-    expect(utils.formatDuration(1000)).toEqual('duration.seconds');
+    expect(utils.formatDuration(ONE_SECOND)).toEqual('duration.seconds');
   });
 });
 
 describe('fetchProjects', () => {
   it('correctly converts the passed arguments to the desired query format', async () => {
-    await utils.fetchProjects({}, true);
+    await utils.fetchProjects({ isFavorite: true, query: {} });
+
     expect(searchProjects).toHaveBeenCalledWith({
       f: 'analysisDate,leakPeriodDate',
       facets: utils.FACETS.join(),
@@ -96,7 +101,8 @@ describe('fetchProjects', () => {
       ps: 50,
     });
 
-    await utils.fetchProjects({ view: 'leak' }, false, 3);
+    await utils.fetchProjects({ isFavorite: false, pageIndex: 3, query: { view: 'leak' } });
+
     expect(searchProjects).toHaveBeenCalledWith({
       f: 'analysisDate,leakPeriodDate',
       facets: utils.LEAK_FACETS.join(),
@@ -107,6 +113,7 @@ describe('fetchProjects', () => {
 
   it('correctly treats result data', async () => {
     const components = [mockComponent({ key: 'foo' }), mockComponent({ key: 'bar' })];
+
     (searchProjects as jest.Mock).mockResolvedValue({
       components,
       facets: [
@@ -121,20 +128,25 @@ describe('fetchProjects', () => {
       ],
       paging: { total: 2 },
     });
-    await utils.fetchProjects({}, true).then((r) => {
+
+    await utils.fetchProjects({ isFavorite: true, query: {} }).then((r) => {
       expect(r).toEqual({
         facets: {
           new_coverage: { NO_DATA: 0 },
           languages: { css: 10, js: 2 },
         },
-        projects: components.map((component: any) => {
-          if (component.key === 'foo') {
-            component.measures = { new_coverage: '10' };
-          } else {
-            component.measures = { languages: '20' };
+        projects: components.map(
+          (component: Component & { measures: { languages?: string; new_coverage?: string } }) => {
+            // eslint-disable-next-line jest/no-conditional-in-test
+            if (component.key === 'foo') {
+              component.measures = { new_coverage: '10' };
+            } else {
+              component.measures = { languages: '20' };
+            }
+
+            return component;
           }
-          return component;
-        }),
+        ),
         total: 2,
       });
     });
@@ -148,3 +160,11 @@ describe('defineMetrics', () => {
     expect(utils.defineMetrics({})).toBe(utils.METRICS);
   });
 });
+
+describe('convertToSorting', () => {
+  it('handles asc and desc sort', () => {
+    expect(utils.convertToSorting({ sort: '-size' })).toStrictEqual({ asc: false, s: 'ncloc' });
+    expect(utils.convertToSorting({})).toStrictEqual({ s: undefined });
+    expect(utils.convertToSorting({ sort: 'search' })).toStrictEqual({ s: 'query' });
+  });
+});
index 55aa17d9838118c8b6bc5fff38ce6aa804c698dd..536f90a44cb991d91735665bb68e835c3575a306 100644 (file)
@@ -81,7 +81,9 @@ export class AllProjects extends React.PureComponent<Props, State> {
       handleRequiredAuthentication();
       return;
     }
+
     this.handleQueryChange();
+
     addSideBarClass();
   }
 
@@ -97,8 +99,11 @@ export class AllProjects extends React.PureComponent<Props, State> {
   }
 
   fetchProjects = (query: Query) => {
+    const { isFavorite } = this.props;
+
     this.setState({ loading: true, query });
-    fetchProjects(query, this.props.isFavorite).then((response) => {
+
+    fetchProjects({ isFavorite, query }).then((response) => {
       if (this.mounted) {
         this.setState({
           facets: response.facets,
@@ -112,10 +117,13 @@ export class AllProjects extends React.PureComponent<Props, State> {
   };
 
   fetchMoreProjects = () => {
+    const { isFavorite } = this.props;
     const { pageIndex, projects, query } = this.state;
-    if (pageIndex && projects && query) {
+
+    if (pageIndex && projects && Object.keys(query).length !== 0) {
       this.setState({ loading: true });
-      fetchProjects(query, this.props.isFavorite, pageIndex + 1).then((response) => {
+
+      fetchProjects({ isFavorite, query, pageIndex: pageIndex + 1 }).then((response) => {
         if (this.mounted) {
           this.setState({
             loading: false,
@@ -127,9 +135,9 @@ export class AllProjects extends React.PureComponent<Props, State> {
     }
   };
 
-  getSort = () => this.state.query.sort || 'name';
+  getSort = () => this.state.query.sort ?? 'name';
 
-  getView = () => this.state.query.view || 'overall';
+  getView = () => this.state.query.view ?? 'overall';
 
   handleClearAll = () => {
     this.props.router.push({ pathname: this.props.location.pathname });
@@ -147,7 +155,7 @@ export class AllProjects extends React.PureComponent<Props, State> {
     });
   };
 
-  handlePerspectiveChange = ({ view }: { view: string }) => {
+  handlePerspectiveChange = ({ view }: { view?: string }) => {
     const query: {
       view: string | undefined;
       sort?: string | undefined;
@@ -158,6 +166,7 @@ export class AllProjects extends React.PureComponent<Props, State> {
     if (this.state.query.view === 'leak' || view === 'leak') {
       if (this.state.query.sort) {
         const sort = parseSorting(this.state.query.sort);
+
         if (SORTING_SWITCH[sort.sortValue]) {
           query.sort = (sort.sortDesc ? '-' : '') + SORTING_SWITCH[sort.sortValue];
         }
@@ -306,12 +315,15 @@ function getStorageOptions() {
     sort?: string;
     view?: string;
   } = {};
+
   if (get(LS_PROJECTS_SORT)) {
     options.sort = get(LS_PROJECTS_SORT) || undefined;
   }
+
   if (get(LS_PROJECTS_VIEW)) {
     options.view = get(LS_PROJECTS_VIEW) || undefined;
   }
+
   return options;
 }
 
index 9be34752c05dcd0a11fce626ae6ba975c141b147..88c55dc28d04fd7ae82ebc5cfb936cf29ecdd0a6 100644 (file)
@@ -76,8 +76,10 @@ it('renders', () => {
 
 it('fetches projects', () => {
   shallowRender();
-  expect(fetchProjects).toHaveBeenLastCalledWith(
-    {
+
+  expect(fetchProjects).toHaveBeenLastCalledWith({
+    isFavorite: false,
+    query: {
       coverage: undefined,
       duplications: undefined,
       gate: undefined,
@@ -97,46 +99,58 @@ it('fetches projects', () => {
       tags: undefined,
       view: undefined,
     },
-    false
-  );
+  });
 });
 
 it('changes sort', () => {
   const push = jest.fn();
-  const wrapper = shallowRender({}, push);
-  wrapper.find('PageHeader').prop<Function>('onSortChange')('size', false);
+  const wrapper = shallowRender({ push });
+
+  wrapper.find('PageHeader').prop<(sort: string, desc: boolean) => void>('onSortChange')(
+    'size',
+    false
+  );
+
   expect(push).toHaveBeenLastCalledWith({ pathname: '/projects', query: { sort: 'size' } });
   expect(save).toHaveBeenLastCalledWith(LS_PROJECTS_SORT, 'size');
 });
 
 it('changes perspective to leak', () => {
   const push = jest.fn();
-  const wrapper = shallowRender({}, push);
-  wrapper.find('PageHeader').prop<Function>('onPerspectiveChange')({ view: 'leak' });
+  const wrapper = shallowRender({ push });
+
+  wrapper.find('PageHeader').prop<({ view }: { view?: string }) => void>('onPerspectiveChange')({
+    view: 'leak',
+  });
+
   expect(push).toHaveBeenLastCalledWith({
     pathname: '/projects',
     query: { view: 'leak' },
   });
+
   expect(save).toHaveBeenCalledWith(LS_PROJECTS_SORT, undefined);
   expect(save).toHaveBeenCalledWith(LS_PROJECTS_VIEW, 'leak');
 });
 
 it('updates sorting when changing perspective from leak', () => {
   const push = jest.fn();
-  const wrapper = shallowRender({}, push);
+  const wrapper = shallowRender({ push });
   wrapper.setState({ query: { sort: 'new_coverage', view: 'leak' } });
-  wrapper.find('PageHeader').prop<Function>('onPerspectiveChange')({
+
+  wrapper.find('PageHeader').prop<({ view }: { view?: string }) => void>('onPerspectiveChange')({
     view: undefined,
   });
+
   expect(push).toHaveBeenLastCalledWith({
     pathname: '/projects',
     query: { sort: 'coverage', view: undefined },
   });
+
   expect(save).toHaveBeenCalledWith(LS_PROJECTS_SORT, 'coverage');
   expect(save).toHaveBeenCalledWith(LS_PROJECTS_VIEW, undefined);
 });
 
-it('handles favorite projects', () => {
+it('handles updating the favorite status of a project', () => {
   const wrapper = shallowRender();
   expect(wrapper.state('projects')).toMatchSnapshot();
 
@@ -144,11 +158,28 @@ it('handles favorite projects', () => {
   expect(wrapper.state('projects')).toMatchSnapshot();
 });
 
-function shallowRender(
-  props: Partial<AllProjects['props']> = {},
-  push = jest.fn(),
-  replace = jest.fn()
-) {
+it('handles showing favorite projects on load', () => {
+  const wrapper = shallowRender({
+    props: { currentUser: { dismissedNotices: {}, isLoggedIn: false }, isFavorite: true },
+  });
+
+  expect(wrapper.state('projects')).toMatchSnapshot();
+
+  wrapper.instance().handleFavorite('foo', true);
+  expect(wrapper.state('projects')).toMatchSnapshot();
+});
+
+const defaults = { props: {}, push: () => undefined, replace: () => undefined };
+
+function shallowRender({
+  props = defaults.props,
+  push = defaults.push,
+  replace = defaults.replace,
+}: {
+  props?: Partial<AllProjects['props']>;
+  push?: () => void;
+  replace?: () => void;
+} = defaults) {
   const wrapper = shallow<AllProjects>(
     <AllProjects
       currentUser={{ isLoggedIn: true, dismissedNotices: {} }}
@@ -161,6 +192,7 @@ function shallowRender(
       {...props}
     />
   );
+
   wrapper.setState({
     loading: false,
     projects: [
@@ -175,5 +207,6 @@ function shallowRender(
     ],
     total: 0,
   });
+
   return wrapper;
 }
index aefede3dc663f2ac4974a7b67479fe0ef14436d0..9bad4f35742cfad5042b0f5b220a8730e9ba73c7 100644 (file)
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`handles favorite projects 1`] = `
+exports[`handles showing favorite projects on load 1`] = `
 [
   {
     "key": "foo",
@@ -13,7 +13,34 @@ exports[`handles favorite projects 1`] = `
 ]
 `;
 
-exports[`handles favorite projects 2`] = `
+exports[`handles showing favorite projects on load 2`] = `
+[
+  {
+    "isFavorite": true,
+    "key": "foo",
+    "measures": {},
+    "name": "Foo",
+    "qualifier": "TRK",
+    "tags": [],
+    "visibility": "public",
+  },
+]
+`;
+
+exports[`handles updating the favorite status of a project 1`] = `
+[
+  {
+    "key": "foo",
+    "measures": {},
+    "name": "Foo",
+    "qualifier": "TRK",
+    "tags": [],
+    "visibility": "public",
+  },
+]
+`;
+
+exports[`handles updating the favorite status of a project 2`] = `
 [
   {
     "isFavorite": true,
index 51a7afbe3ff4accd9218b8dc6d9ffda0c3019b23..235018b7a9a4980251a3659038b7988ad6474046 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 { invert } from 'lodash';
 import { Facet, searchProjects } from '../../api/components';
 import { getMeasuresForProjects } from '../../api/measures';
@@ -150,22 +151,33 @@ export const LEAK_FACETS = [
 const REVERSED_FACETS = ['coverage', 'new_coverage'];
 
 export function localizeSorting(sort?: string): string {
-  return translate('projects.sort', sort || 'name');
+  return translate('projects.sort', sort ?? 'name');
 }
 
 export function parseSorting(sort: string): { sortValue: string; sortDesc: boolean } {
-  const desc = sort[0] === '-';
-  return { sortValue: desc ? sort.substr(1) : sort, sortDesc: desc };
+  const desc = sort.startsWith('-');
+
+  return { sortValue: desc ? sort.substring(1) : sort, sortDesc: desc };
 }
 
-export function fetchProjects(query: Query, isFavorite: boolean, pageIndex = 1) {
+export function fetchProjects({
+  isFavorite,
+  query,
+  pageIndex = 1,
+}: {
+  query: Query;
+  isFavorite: boolean;
+  pageIndex?: number;
+}) {
   const ps = PAGE_SIZE;
+
   const data = convertToQueryData(query, isFavorite, {
     p: pageIndex > 1 ? pageIndex : undefined,
     ps,
     facets: defineFacets(query).join(),
     f: 'analysisDate,leakPeriodDate',
   });
+
   return searchProjects(data)
     .then((response) =>
       Promise.all([fetchProjectMeasures(response.components, query), Promise.resolve(response)])
@@ -183,6 +195,7 @@ export function fetchProjects(query: Query, isFavorite: boolean, pageIndex = 1)
                 componentMeasures[measure.metric] = value;
               }
             });
+
           return { ...component, measures: componentMeasures };
         }),
         total: paging.total,
@@ -194,6 +207,7 @@ export function defineMetrics(query: Query): string[] {
   if (query.view === 'leak') {
     return LEAK_METRICS;
   }
+
   return METRICS;
 }
 
@@ -201,6 +215,7 @@ function defineFacets(query: Query): string[] {
   if (query.view === 'leak') {
     return LEAK_FACETS;
   }
+
   return FACETS;
 }
 
@@ -212,12 +227,15 @@ function convertToQueryData(query: Query, isFavorite: boolean, defaultData = {})
   if (filter) {
     data.filter = filter;
   }
+
   if (sort.s) {
     data.s = sort.s;
   }
+
   if (sort.asc !== undefined) {
     data.asc = sort.asc;
   }
+
   return data;
 }
 
@@ -228,14 +246,17 @@ export function fetchProjectMeasures(projects: Array<{ key: string }>, query: Qu
 
   const projectKeys = projects.map((project) => project.key);
   const metrics = defineMetrics(query);
+
   return getMeasuresForProjects(projectKeys, metrics);
 }
 
 function mapFacetValues(values: Array<{ val: string; count: number }>) {
   const map: Dict<number> = {};
+
   values.forEach((value) => {
     map[value.val] = value.count;
   });
+
   return map;
 }
 
@@ -266,22 +287,27 @@ const metricToPropertyMap = invert(propertyToMetricMap);
 
 function getFacetsMap(facets: Facet[]) {
   const map: Dict<Dict<number>> = {};
+
   facets.forEach((facet) => {
     const property = metricToPropertyMap[facet.property];
     const { values } = facet;
+
     if (REVERSED_FACETS.includes(property)) {
       values.reverse();
     }
+
     map[property] = mapFacetValues(values);
   });
+
   return map;
 }
 
-function convertToSorting({ sort }: Query): { s?: string; asc?: boolean } {
-  if (sort && sort[0] === '-') {
-    return { s: propertyToMetricMap[sort.substr(1)], asc: false };
+export function convertToSorting({ sort }: Query): { s?: string; asc?: boolean } {
+  if (sort?.startsWith('-')) {
+    return { s: propertyToMetricMap[sort.substring(1)], asc: false };
   }
-  return { s: propertyToMetricMap[sort || ''] };
+
+  return { s: propertyToMetricMap[sort ?? ''] };
 }
 
 const ONE_MINUTE = 60000;
@@ -294,15 +320,18 @@ function format(periods: Array<{ value: number; label: string }>) {
   let result = '';
   let count = 0;
   let lastId = -1;
+
   for (let i = 0; i < periods.length && count < 2; i++) {
     if (periods[i].value > 0) {
       count++;
+
       if (lastId < 0 || lastId + 1 === i) {
         lastId = i;
         result += translateWithParameters(periods[i].label, periods[i].value) + ' ';
       }
     }
   }
+
   return result;
 }
 
@@ -310,15 +339,21 @@ export function formatDuration(ms: number) {
   if (ms < ONE_MINUTE) {
     return translate('duration.seconds');
   }
+
   const years = Math.floor(ms / ONE_YEAR);
   ms -= years * ONE_YEAR;
+
   const months = Math.floor(ms / ONE_MONTH);
   ms -= months * ONE_MONTH;
+
   const days = Math.floor(ms / ONE_DAY);
   ms -= days * ONE_DAY;
+
   const hours = Math.floor(ms / ONE_HOUR);
   ms -= hours * ONE_HOUR;
+
   const minutes = Math.floor(ms / ONE_MINUTE);
+
   return format([
     { value: years, label: 'duration.years' },
     { value: months, label: 'duration.months' },
index df385e549edf45f53dfd93193ce6e09d5692d78b..6d4d6872ce8b1db3499347284775cc7156de6d79 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 { getTextColor } from 'design-system';
 import * as React from 'react';
 import { colors } from '../../../app/theme';
-import { getTextColor } from '../../../helpers/colors';
 import { getBaseUrl } from '../../../helpers/system';
 import { IdentityProvider } from '../../../types/types';
 import { User } from '../../../types/users';
index 67d207a6fe92534f2772ca48a910632d637e6f30..af9e761fe731745dff18d0892a1b9f4404dca716 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 classNames from 'classnames';
 import { scaleLinear } from 'd3-scale';
 import * as React from 'react';
@@ -119,7 +120,7 @@ export default class TreeMapRect extends React.PureComponent<Props> {
                     </>
                   )}
 
-                  {this.props.label.substr(this.props.prefix.length)}
+                  {this.props.label.substring(this.props.prefix.length)}
                 </div>
 
                 <div className="treemap-text-suffix little-spacer-top">{this.props.value}</div>
index fea5968876bc84a8ced6c76beb3b15dcccd462f0..979aa1853783a13a6c312395436c2229a5c3adf9 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 { isDarkColor } from 'design-system';
 import * as React from 'react';
-import { isDarkColor } from '../../helpers/colors';
 import { getBaseUrl } from '../../helpers/system';
 import './IdentityProviderLink.css';
 
index 1dcf18ccc6ffa59920749ba12de9a759698168fe..0a2711e12ec391c4ef65a3d47ad890df6c9a3701 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 { getTextColor, stringToColor } from 'design-system';
 import * as React from 'react';
-import { getTextColor, stringToColor } from '../../helpers/colors';
 
 interface Props {
   className?: string;
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/colors-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/colors-test.ts
deleted file mode 100644 (file)
index 84667c7..0000000
+++ /dev/null
@@ -1,50 +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 colors from '../colors';
-
-describe('#stringToColor', () => {
-  it('should return a color for a text', () => {
-    expect(colors.stringToColor('skywalker')).toBe('#97f047');
-  });
-});
-
-describe('#isDarkColor', () => {
-  it('should be dark', () => {
-    expect(colors.isDarkColor('#000000')).toBe(true);
-    expect(colors.isDarkColor('#222222')).toBe(true);
-    expect(colors.isDarkColor('#000')).toBe(true);
-  });
-  it('should be light', () => {
-    expect(colors.isDarkColor('#FFFFFF')).toBe(false);
-    expect(colors.isDarkColor('#CDCDCD')).toBe(false);
-    expect(colors.isDarkColor('#FFF')).toBe(false);
-  });
-});
-
-describe('#getTextColor', () => {
-  it('should return dark color', () => {
-    expect(colors.getTextColor('#FFF', 'dark', 'light')).toBe('dark');
-    expect(colors.getTextColor('#FFF')).toBe('#222');
-  });
-  it('should return light color', () => {
-    expect(colors.getTextColor('#000', 'dark', 'light')).toBe('light');
-    expect(colors.getTextColor('#000')).toBe('#fff');
-  });
-});
diff --git a/server/sonar-web/src/main/js/helpers/colors.ts b/server/sonar-web/src/main/js/helpers/colors.ts
deleted file mode 100644 (file)
index 932b9c9..0000000
+++ /dev/null
@@ -1,50 +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.
- */
-/* eslint-disable no-bitwise, no-mixed-operators */
-export function stringToColor(str: string) {
-  let hash = 0;
-  for (let i = 0; i < str.length; i++) {
-    hash = str.charCodeAt(i) + ((hash << 5) - hash);
-  }
-  let color = '#';
-  for (let i = 0; i < 3; i++) {
-    const value = (hash >> (i * 8)) & 0xff;
-    color += ('00' + value.toString(16)).substr(-2);
-  }
-  return color;
-}
-
-export function isDarkColor(color: string) {
-  color = color.substr(1);
-  if (color.length === 3) {
-    // shortcut notation: #f90
-    color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
-  }
-  const rgb = parseInt(color.substr(1), 16);
-  const r = (rgb >> 16) & 0xff;
-  const g = (rgb >> 8) & 0xff;
-  const b = (rgb >> 0) & 0xff;
-  const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
-  return luma < 140;
-}
-
-export function getTextColor(background: string, dark = '#222', light = '#fff') {
-  return isDarkColor(background) ? light : dark;
-}