aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/design-system/src/components/InteractiveIcon.tsx8
-rw-r--r--server/sonar-web/design-system/src/components/buttons.tsx12
-rw-r--r--server/sonar-web/design-system/src/helpers/colors.ts15
-rw-r--r--server/sonar-web/design-system/src/helpers/constants.ts3
-rw-r--r--server/sonar-web/design-system/src/helpers/index.ts2
-rw-r--r--server/sonar-web/design-system/src/theme/light.ts10
-rw-r--r--server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx24
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/routes.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/projects/__tests__/utils-test.ts44
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx24
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx65
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap31
-rw-r--r--server/sonar-web/src/main/js/apps/projects/utils.ts51
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx3
-rw-r--r--server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx3
-rw-r--r--server/sonar-web/src/main/js/components/controls/IdentityProviderLink.tsx3
-rw-r--r--server/sonar-web/src/main/js/components/ui/GenericAvatar.tsx3
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/colors-test.ts50
-rw-r--r--server/sonar-web/src/main/js/helpers/colors.ts50
22 files changed, 239 insertions, 174 deletions
diff --git a/server/sonar-web/design-system/src/components/InteractiveIcon.tsx b/server/sonar-web/design-system/src/components/InteractiveIcon.tsx
index ebd9cb73e9a..03bbe3bdf6e 100644
--- a/server/sonar-web/design-system/src/components/InteractiveIcon.tsx
+++ b/server/sonar-web/design-system/src/components/InteractiveIcon.tsx
@@ -17,11 +17,13 @@
* 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;
`;
diff --git a/server/sonar-web/design-system/src/components/buttons.tsx b/server/sonar-web/design-system/src/components/buttons.tsx
index 442026354ea..ab0fcf9fb0c 100644
--- a/server/sonar-web/design-system/src/components/buttons.tsx
+++ b/server/sonar-web/design-system/src/components/buttons.tsx
@@ -17,10 +17,12 @@
* 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')};
`;
diff --git a/server/sonar-web/design-system/src/helpers/colors.ts b/server/sonar-web/design-system/src/helpers/colors.ts
index d0cb5e215ca..1fd0b806052 100644
--- a/server/sonar-web/design-system/src/helpers/colors.ts
+++ b/server/sonar-web/design-system/src/helpers/colors.ts
@@ -17,33 +17,42 @@
* 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;
}
diff --git a/server/sonar-web/design-system/src/helpers/constants.ts b/server/sonar-web/design-system/src/helpers/constants.ts
index 68a385c3c1c..344d66b7ac5 100644
--- a/server/sonar-web/design-system/src/helpers/constants.ts
+++ b/server/sonar-web/design-system/src/helpers/constants.ts
@@ -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;
diff --git a/server/sonar-web/design-system/src/helpers/index.ts b/server/sonar-web/design-system/src/helpers/index.ts
index 764e245473d..427c828d24f 100644
--- a/server/sonar-web/design-system/src/helpers/index.ts
+++ b/server/sonar-web/design-system/src/helpers/index.ts
@@ -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';
diff --git a/server/sonar-web/design-system/src/theme/light.ts b/server/sonar-web/design-system/src/theme/light.ts
index 8b10b339326..56aaefd0d3a 100644
--- a/server/sonar-web/design-system/src/theme/light.ts
+++ b/server/sonar-web/design-system/src/theme/light.ts
@@ -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: {
diff --git a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx
index 182e8ae11dd..4527bef160e 100644
--- a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx
+++ b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx
@@ -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
diff --git a/server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.tsx b/server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.tsx
index 99ff1c64e91..34b3b2c16cc 100644
--- a/server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.tsx
+++ b/server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.tsx
@@ -17,10 +17,11 @@
* 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';
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
index d74f0b6bd07..7cb089ccf76 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
@@ -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>;
}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/routes.tsx b/server/sonar-web/src/main/js/apps/coding-rules/routes.tsx
index 5ba7f224b13..5a9221be36b 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/routes.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/routes.tsx
@@ -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,
diff --git a/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx b/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx
index 78107922aac..a1fe1c5d19e 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx
@@ -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) {
diff --git a/server/sonar-web/src/main/js/apps/projects/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/projects/__tests__/utils-test.ts
index b6c7496acb2..230acd91907 100644
--- a/server/sonar-web/src/main/js/apps/projects/__tests__/utils-test.ts
+++ b/server/sonar-web/src/main/js/apps/projects/__tests__/utils-test.ts
@@ -17,8 +17,10 @@
* 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' });
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
index 55aa17d9838..536f90a44cb 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
@@ -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;
}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
index 9be34752c05..88c55dc28d0 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
@@ -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;
}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
index aefede3dc66..9bad4f35742 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
@@ -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,
diff --git a/server/sonar-web/src/main/js/apps/projects/utils.ts b/server/sonar-web/src/main/js/apps/projects/utils.ts
index 51a7afbe3ff..235018b7a9a 100644
--- a/server/sonar-web/src/main/js/apps/projects/utils.ts
+++ b/server/sonar-web/src/main/js/apps/projects/utils.ts
@@ -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' },
diff --git a/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx b/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx
index df385e549ed..6d4d6872ce8 100644
--- a/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx
+++ b/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx
@@ -17,9 +17,10 @@
* 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';
diff --git a/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx b/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx
index 67d207a6fe9..af9e761fe73 100644
--- a/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx
+++ b/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/components/controls/IdentityProviderLink.tsx b/server/sonar-web/src/main/js/components/controls/IdentityProviderLink.tsx
index fea5968876b..979aa185378 100644
--- a/server/sonar-web/src/main/js/components/controls/IdentityProviderLink.tsx
+++ b/server/sonar-web/src/main/js/components/controls/IdentityProviderLink.tsx
@@ -17,9 +17,10 @@
* 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';
diff --git a/server/sonar-web/src/main/js/components/ui/GenericAvatar.tsx b/server/sonar-web/src/main/js/components/ui/GenericAvatar.tsx
index 1dcf18ccc6f..0a2711e12ec 100644
--- a/server/sonar-web/src/main/js/components/ui/GenericAvatar.tsx
+++ b/server/sonar-web/src/main/js/components/ui/GenericAvatar.tsx
@@ -17,9 +17,10 @@
* 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
index 84667c740d3..00000000000
--- a/server/sonar-web/src/main/js/helpers/__tests__/colors-test.ts
+++ /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
index 932b9c994ba..00000000000
--- a/server/sonar-web/src/main/js/helpers/colors.ts
+++ /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;
-}