interface DataPoint {
description: string;
- tooltip?: string;
+ tooltip?: string | JSX.Element;
x: number;
y: number;
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import styled from '@emotion/styled';
+import styled from '@emotion/styled';
import classNames from 'classnames';
import {
format,
showClearButton?: boolean;
size?: InputSizeKeys;
value?: Date;
- valueFormatter: (date?: Date) => string;
+ valueFormatter?: (date?: Date) => string;
}
interface State {
id,
placeholder,
showClearButton = true,
+ valueFormatter = (date?: Date) => (date ? format(date, 'MMM d, yyyy') : ''),
size,
} = this.props;
const { lastHovered, currentMonth, open } = this.state;
const selectedDays = selectedDay ? [selectedDay] : [];
let highlighted: Matcher = false;
const lastHoveredOrValue = lastHovered ?? selectedDay;
+
if (highlightFrom && lastHoveredOrValue) {
highlighted = { from: highlightFrom, to: lastHoveredOrValue };
selectedDays.push(highlightFrom);
}
+
if (highlightTo && lastHoveredOrValue) {
highlighted = { from: lastHoveredOrValue, to: highlightTo };
selectedDays.push(highlightTo);
readOnly
ref={inputRef}
size={size}
- title={this.props.valueFormatter(selectedDay)}
+ title={valueFormatter(selectedDay)}
type="text"
- value={this.props.valueFormatter(selectedDay)}
+ value={valueFormatter(selectedDay)}
/>
+
<StyledCalendarIcon fill="datePickerIcon" />
+
{selectedDay !== undefined && showClearButton && (
<StyledInteractiveIcon
Icon={CloseIcon}
const { goToMonth, nextMonth, previousMonth } = useCalendarNavigation();
const baseDate = startOfMonth(displayMonth); // reference date
+
const months = range(MONTHS_IN_A_YEAR).map((month) => {
const monthValue = setMonth(baseDate, month);
+
return {
label: format(monthValue, 'MMM'),
value: monthValue,
};
});
+
const startYear = fromYear ?? getYear(Date.now()) - YEARS_TO_DISPLAY;
+
const years = range(startYear, toYear ? toYear + 1 : undefined).map((year) => {
const yearValue = setYear(baseDate, year);
+
return {
label: String(year),
value: yearValue,
Icon={ChevronLeftIcon}
aria-label={ariaPreviousMonthLabel}
className="sw-mr-2"
- onClick={() => previousMonth && goToMonth(previousMonth)}
- size="small"
- />
- <InputSelect
- isClearable={false}
- onChange={(value) => {
- if (value) {
- goToMonth(value.value);
- }
- }}
- options={months}
- size="full"
- value={months.find((m) => isSameMonth(m.value, displayMonth))}
- />
- <InputSelect
- className="sw-ml-1"
- isClearable={false}
- onChange={(value) => {
- if (value) {
- goToMonth(value.value);
+ onClick={() => {
+ if (previousMonth) {
+ goToMonth(previousMonth);
}
}}
- options={years}
- size="full"
- value={years.find((y) => isSameYear(y.value, displayMonth))}
+ size="small"
/>
+
+ <span data-testid="month-select">
+ <InputSelect
+ isClearable={false}
+ onChange={(value) => {
+ if (value) {
+ goToMonth(value.value);
+ }
+ }}
+ options={months}
+ size="full"
+ value={months.find((m) => isSameMonth(m.value, displayMonth))}
+ />
+ </span>
+
+ <span data-testid="year-select">
+ <InputSelect
+ className="sw-ml-1"
+ data-testid="year-select"
+ isClearable={false}
+ onChange={(value) => {
+ if (value) {
+ goToMonth(value.value);
+ }
+ }}
+ options={years}
+ size="full"
+ value={years.find((y) => isSameYear(y.value, displayMonth))}
+ />
+ </span>
+
<InteractiveIcon
Icon={ChevronRightIcon}
aria-label={ariaNextMonthLabel}
className="sw-ml-2"
- onClick={() => nextMonth && goToMonth(nextMonth)}
+ onClick={() => {
+ if (nextMonth) {
+ goToMonth(nextMonth);
+ }
+ }}
size="small"
/>
</nav>
separatorText?: string;
toLabel: string;
value?: DateRange;
- valueFormatter: (date?: Date) => string;
+ valueFormatter?: (date?: Date) => string;
}
export class DateRangePicker extends React.PureComponent<Props> {
import { themeColor } from '../helpers';
import { Badge } from './Badge';
import { DeferredSpinner } from './DeferredSpinner';
-import { InteractiveIcon } from './InteractiveIcon';
+import { DestructiveIcon } from './InteractiveIcon';
import Tooltip from './Tooltip';
import { BareButton } from './buttons';
import { OpenCloseIndicator } from './icons';
clearIconLabel?: string;
count?: number;
countLabel?: string;
+ 'data-property'?: string;
disabled?: boolean;
+ hasEmbeddedFacets?: boolean;
id?: string;
inner?: boolean;
loading?: boolean;
clearIconLabel,
count,
countLabel,
+ 'data-property': dataProperty,
disabled = false,
+ hasEmbeddedFacets = false,
id: idProp,
inner = false,
loading = false,
const id = React.useMemo(() => idProp ?? uniqueId('filter-facet-'), [idProp]);
return (
- <Accordion className={classNames(className, { open })} inner={inner} role="listitem">
+ <Accordion
+ className={classNames(className, { open })}
+ data-property={dataProperty}
+ hasEmbeddedFacets={hasEmbeddedFacets}
+ inner={inner}
+ role="listitem"
+ >
<Header>
<ChevronAndTitle
aria-controls={`${id}-panel`}
<ClearIcon
Icon={CloseIcon}
aria-label={clearIconLabel ?? ''}
+ data-testid={`clear-${name}`}
onClick={onClear}
size="small"
/>
</Header>
{open && (
- <div aria-labelledby={`${id}-header`} id={`${id}-panel`} role="region">
+ <div aria-labelledby={`${id}-header`} id={`${id}-panel`} role="list">
{children}
</div>
)}
);
}
+FacetBox.displayName = 'FacetBox'; // so that tests don't see the obfuscated production name
+
const Accordion = styled.div<{
+ hasEmbeddedFacets?: boolean;
inner?: boolean;
}>`
${tw`sw-flex-col`};
${tw`sw-flex`};
${tw`sw-gap-3`};
- ${({ inner }) => (inner ? tw`sw-gap-1 sw-ml-3` : '')};
+ ${({ hasEmbeddedFacets }) => (hasEmbeddedFacets ? tw`sw-gap-0` : '')};
+
+ ${({ inner }) => (inner ? tw`sw-gap-1 sw-ml-3 sw-mt-1` : '')};
`;
const BadgeAndIcons = styled.div`
cursor: ${({ expandable }) => (expandable ? 'pointer' : 'default')};
`;
-const ClearIcon = styled(InteractiveIcon)`
+const ClearIcon = styled(DestructiveIcon)`
--color: ${themeColor('dangerButton')};
`;
export type FacetItemProps = Omit<ButtonProps, 'name' | 'onClick'> & {
active?: boolean;
- name: string;
+ name: string | React.ReactNode;
onClick: (x: string, multiple?: boolean) => void;
+ small?: boolean;
stat?: React.ReactNode;
/** Textual version of `name` */
tooltip?: string;
icon,
name,
onClick,
+ small,
stat,
tooltip,
value,
}: FacetItemProps) {
- const disabled = disabledProp || (stat as number) === 0;
+ const disabled = disabledProp || (stat !== undefined && stat === 0);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
return (
<StyledButton
active={active}
+ aria-checked={active}
+ aria-label={typeof name === 'string' ? name : undefined}
className={className}
data-facet={value}
disabled={disabled}
icon={icon}
onClick={handleClick}
- role="listitem"
+ role="checkbox"
+ small={small}
title={tooltip}
>
<span className="container">
);
}
-const StyledButton = styled(ButtonSecondary)<{ active?: boolean }>`
+FacetItem.displayName = 'FacetItem'; // so that tests don't see the obfuscated production name
+
+const StyledButton = styled(ButtonSecondary)<{ active?: boolean; small?: boolean }>`
${tw`sw-body-sm`};
- ${tw`sw-p-1`};
+ ${tw`sw-box-border`};
+ ${tw`sw-h-7`};
+ ${tw`sw-px-1`};
${tw`sw-rounded-1`};
+ ${tw`sw-w-full`};
+
+ ${({ small }) => (small ? tw`sw-body-xs sw-pr-0` : '')};
--background: ${({ active }) => (active ? themeColor('facetItemSelected') : 'transparent')};
--backgroundHover: ${({ active }) => (active ? themeColor('facetItemSelected') : 'transparent')};
${tw`sw-items-center`};
${tw`sw-justify-between`};
+ & span.name {
+ ${tw`sw-pr-1`};
+ ${tw`sw-truncate`};
+
+ & mark {
+ background-color: ${themeColor('searchHighlight')};
+ }
+ }
+
& span.stat {
color: ${themeColor('facetItemLight')};
}
);
}
+FlagMessage.displayName = 'FlagMessage'; // so that tests don't see the obfuscated production name
+
export const StyledFlag = styled.div<{
variantInfo: VariantInformation;
}>`
<StyledInteractiveIcon
Icon={CloseIcon}
aria-label={clearIconAriaLabel}
- className="js-input-search-clear"
+ className="it__search-box-clear"
onClick={handleClearClick}
size="small"
/>
);
}
+InputSearch.displayName = 'InputSearch'; // so that tests don't see the obfuscated production name
+
export const InputSearchWrapper = styled.div`
width: var(--inputSize);
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import styled from '@emotion/styled';
import tw from 'twin.macro';
import { themeColor, themeContrast } from '../helpers';
import { Key } from '../helpers/keyboard';
import { TriangleDownIcon, TriangleLeftIcon, TriangleRightIcon, TriangleUpIcon } from './icons';
-const COMMAND = '⌘';
-const CTRL = 'Ctrl';
-const OPTION = '⌥';
-const ALT = 'Alt';
-const NON_KEY_SYMBOLS = ['+', ' '];
+export const mappedKeys = {
+ [Key.Alt]: 'Alt',
+ [Key.ArrowDown]: <TriangleDownIcon />,
+ [Key.ArrowLeft]: <TriangleLeftIcon />,
+ [Key.ArrowRight]: <TriangleRightIcon />,
+ [Key.ArrowUp]: <TriangleUpIcon />,
+ [Key.Command]: '⌘',
+ [Key.Control]: 'Ctrl',
+ [Key.Option]: '⌥',
+};
export function KeyboardHintKeys({ command }: { command: string }) {
const keys = command
.split(' ')
.map((key, index) => {
const uniqueKey = `${key}-${index}`;
- if (NON_KEY_SYMBOLS.includes(key)) {
+
+ if (!(Object.keys(mappedKeys).includes(key) || Object.values(mappedKeys).includes(key))) {
return <span key={uniqueKey}>{key}</span>;
}
- return <KeyBox key={uniqueKey}>{getKey(key)}</KeyBox>;
+ return <KeyBox key={uniqueKey}>{mappedKeys[key as keyof typeof mappedKeys] || key}</KeyBox>;
});
return <div className="sw-flex sw-gap-1">{keys}</div>;
${tw`sw-px-1/2`}
${tw`sw-rounded-1/2`}
- color: ${themeContrast('keyboardHintKey')};
background-color: ${themeColor('keyboardHintKey')};
+ color: ${themeContrast('keyboardHintKey')};
`;
-
-function getKey(key: string) {
- switch (key) {
- case Key.Control:
- return CTRL;
- case Key.Command:
- return COMMAND;
- case Key.Alt:
- return ALT;
- case Key.Option:
- return OPTION;
- case Key.ArrowUp:
- return <TriangleUpIcon />;
- case Key.ArrowDown:
- return <TriangleDownIcon />;
- case Key.ArrowLeft:
- return <TriangleLeftIcon />;
- case Key.ArrowRight:
- return <TriangleRightIcon />;
- default:
- return key;
- }
-}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { getMonth, getYear, parseISO } from 'date-fns';
expect(getYear(newDate3)).toBe(2019);
});
-it('highlights the appropriate days', async () => {
+it('should clear the value', async () => {
const user = userEvent.setup();
- const value = parseISO('2022-06-14');
- renderDatePicker({ highlightFrom: parseISO('2022-06-12'), showClearButton: true, value });
+ const onChange = jest.fn((_: Date) => undefined);
+
+ const currentDate = parseISO('2022-06-13');
+
+ renderDatePicker({
+ currentMonth: currentDate,
+ onChange,
+ showClearButton: true,
+ value: currentDate,
+ // eslint-disable-next-line jest/no-conditional-in-test
+ valueFormatter: (date?: Date) => (date ? 'formatted date' : 'no date'),
+ });
+
+ await user.click(screen.getByRole('textbox'));
+
+ await user.click(screen.getByLabelText('clear'));
+
+ expect(onChange).toHaveBeenCalledWith(undefined);
+});
+
+it.each([
+ [{ highlightFrom: parseISO('2022-06-12'), value: parseISO('2022-06-14') }],
+ [{ alignRight: true, highlightTo: parseISO('2022-06-14'), value: parseISO('2022-06-12') }],
+])('highlights the appropriate days', async (props) => {
+ const user = userEvent.setup();
+
+ const hightlightClass = 'rdp-highlighted';
+
+ renderDatePicker(props);
await user.click(screen.getByRole('textbox'));
- expect(screen.getByText('11')).not.toHaveClass('rdp-highlighted');
- expect(screen.getByText('12')).toHaveClass('rdp-highlighted');
- expect(screen.getByText('13')).toHaveClass('rdp-highlighted');
- expect(screen.getByText('14')).toHaveClass('rdp-highlighted');
- expect(screen.getByText('15')).not.toHaveClass('rdp-highlighted');
+ expect(screen.getByText('11')).not.toHaveClass(hightlightClass);
+ expect(screen.getByText('12')).toHaveClass(hightlightClass);
+ expect(screen.getByText('13')).toHaveClass(hightlightClass);
+ expect(screen.getByText('14')).toHaveClass(hightlightClass);
+ expect(screen.getByText('15')).not.toHaveClass(hightlightClass);
});
function renderDatePicker(overrides: Partial<DatePicker['props']> = {}) {
const onClick = jest.fn();
- renderComponent({ disabled: true, onClick });
+ renderComponent({ disabled: true, hasEmbeddedFacets: true, onClick });
expect(screen.getByRole('listitem')).toBeInTheDocument();
- expect(screen.queryByRole('region')).not.toBeInTheDocument();
+ expect(screen.queryByRole('list')).not.toBeInTheDocument();
expect(screen.getByText('Test FacetBox')).toBeInTheDocument();
open: true,
});
- expect(screen.getByRole('region')).toBeInTheDocument();
+ expect(screen.getByRole('list')).toBeInTheDocument();
expect(screen.getByRole('button', { expanded: true })).toBeInTheDocument();
renderComponent({ disabled: true, onClick });
- expect(screen.getByRole('listitem')).toHaveAttribute('aria-disabled', 'true');
+ expect(screen.getByRole('checkbox')).toHaveAttribute('aria-disabled', 'true');
- await user.click(screen.getByRole('listitem'));
+ await user.click(screen.getByRole('checkbox'));
expect(onClick).not.toHaveBeenCalled();
});
renderComponent({ active: true, onClick, stat: 3, value: 'foo' });
- expect(screen.getByRole('listitem')).toHaveAttribute('aria-disabled', 'false');
+ expect(screen.getByRole('checkbox')).toHaveAttribute('aria-disabled', 'false');
- await user.click(screen.getByRole('listitem'));
+ await user.click(screen.getByRole('checkbox'));
expect(onClick).toHaveBeenCalledWith('foo', false);
await user.keyboard('{Meta>}');
- await user.click(screen.getByRole('listitem'));
+ await user.click(screen.getByRole('checkbox'));
expect(onClick).toHaveBeenLastCalledWith('foo', true);
});
+it('should add an aria label if the name is a string', () => {
+ renderComponent({ name: 'Foo' });
+
+ expect(screen.getByRole('checkbox')).toHaveAccessibleName('Foo');
+});
+
+it('should not add an aria label if the name is not a string', () => {
+ renderComponent({ name: <div>Foo</div>, small: true });
+
+ expect(screen.getByRole('checkbox')).not.toHaveAttribute('aria-label');
+});
+
function renderComponent(props: Partial<FacetItemProps> = {}) {
return render(<FacetItem name="Test facet item" onClick={jest.fn()} value="Value" {...props} />);
}
import { Key } from '../../helpers/keyboard';
import { render } from '../../helpers/testUtils';
import { FCProps } from '../../types/misc';
-import { KeyboardHintKeys } from '../KeyboardHintKeys';
+import { KeyboardHintKeys, mappedKeys } from '../KeyboardHintKeys';
-it.each([
- Key.Control,
- Key.Command,
- Key.Alt,
- Key.Option,
- Key.ArrowUp,
- Key.ArrowDown,
- Key.ArrowLeft,
- Key.ArrowRight,
-])('should render %s', (key) => {
+it.each(Object.keys(mappedKeys))('should render %s', (key) => {
const { container } = setupWithProps({ command: key });
expect(container).toMatchSnapshot();
});
it('should render multiple keys', () => {
- const { container } = setupWithProps({ command: `${Key.ArrowUp} ${Key.ArrowDown}` });
+ const { container } = setupWithProps({ command: `Use Ctrl + ${Key.ArrowUp} ${Key.ArrowDown}` });
expect(container).toMatchSnapshot();
});
padding-left: 0.125rem;
padding-right: 0.125rem;
border-radius: 0.125rem;
- color: rgb(62,67,87);
background-color: rgb(225,230,243);
+ color: rgb(62,67,87);
}
<div>
padding-left: 0.125rem;
padding-right: 0.125rem;
border-radius: 0.125rem;
- color: rgb(62,67,87);
background-color: rgb(225,230,243);
+ color: rgb(62,67,87);
}
<div>
color: rgb(106,117,144);
}
-.emotion-2 {
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- -webkit-align-items: center;
- -webkit-box-align: center;
- -ms-flex-align: center;
- align-items: center;
- -webkit-box-pack: center;
- -ms-flex-pack: center;
- -webkit-justify-content: center;
- justify-content: center;
- padding-left: 0.125rem;
- padding-right: 0.125rem;
- border-radius: 0.125rem;
- color: rgb(62,67,87);
- background-color: rgb(225,230,243);
-}
-
<div>
<div
class="emotion-0 emotion-1"
<div
class="sw-flex sw-gap-1"
>
- <span
- class="emotion-2 emotion-3"
- >
+ <span>
command
</span>
</div>
color: rgb(106,117,144);
}
-.emotion-2 {
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- -webkit-align-items: center;
- -webkit-box-align: center;
- -ms-flex-align: center;
- align-items: center;
- -webkit-box-pack: center;
- -ms-flex-pack: center;
- -webkit-justify-content: center;
- justify-content: center;
- padding-left: 0.125rem;
- padding-right: 0.125rem;
- border-radius: 0.125rem;
- color: rgb(62,67,87);
- background-color: rgb(225,230,243);
-}
-
<div>
<div
class="emotion-0 emotion-1"
<div
class="sw-flex sw-gap-1"
>
- <span
- class="emotion-2 emotion-3"
- >
+ <span>
click
</span>
</div>
color: rgb(106,117,144);
}
-.emotion-2 {
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- -webkit-align-items: center;
- -webkit-box-align: center;
- -ms-flex-align: center;
- align-items: center;
- -webkit-box-pack: center;
- -ms-flex-pack: center;
- -webkit-justify-content: center;
- justify-content: center;
- padding-left: 0.125rem;
- padding-right: 0.125rem;
- border-radius: 0.125rem;
- color: rgb(62,67,87);
- background-color: rgb(225,230,243);
-}
-
<div>
<div
class="emotion-0 emotion-1"
<div
class="sw-flex sw-gap-1"
>
- <span
- class="emotion-2 emotion-3"
- >
+ <span>
click
</span>
</div>
padding-left: 0.125rem;
padding-right: 0.125rem;
border-radius: 0.125rem;
- color: rgb(62,67,87);
background-color: rgb(225,230,243);
+ color: rgb(62,67,87);
}
<div>
padding-left: 0.125rem;
padding-right: 0.125rem;
border-radius: 0.125rem;
- color: rgb(62,67,87);
background-color: rgb(225,230,243);
+ color: rgb(62,67,87);
}
<div>
padding-left: 0.125rem;
padding-right: 0.125rem;
border-radius: 0.125rem;
- color: rgb(62,67,87);
background-color: rgb(225,230,243);
+ color: rgb(62,67,87);
}
<div>
padding-left: 0.125rem;
padding-right: 0.125rem;
border-radius: 0.125rem;
- color: rgb(62,67,87);
background-color: rgb(225,230,243);
+ color: rgb(62,67,87);
}
<div>
padding-left: 0.125rem;
padding-right: 0.125rem;
border-radius: 0.125rem;
- color: rgb(62,67,87);
background-color: rgb(225,230,243);
+ color: rgb(62,67,87);
}
<div>
padding-left: 0.125rem;
padding-right: 0.125rem;
border-radius: 0.125rem;
- color: rgb(62,67,87);
background-color: rgb(225,230,243);
+ color: rgb(62,67,87);
}
<div>
padding-left: 0.125rem;
padding-right: 0.125rem;
border-radius: 0.125rem;
- color: rgb(62,67,87);
background-color: rgb(225,230,243);
+ color: rgb(62,67,87);
}
<div>
padding-left: 0.125rem;
padding-right: 0.125rem;
border-radius: 0.125rem;
- color: rgb(62,67,87);
background-color: rgb(225,230,243);
+ color: rgb(62,67,87);
}
<div>
padding-left: 0.125rem;
padding-right: 0.125rem;
border-radius: 0.125rem;
- color: rgb(62,67,87);
background-color: rgb(225,230,243);
+ color: rgb(62,67,87);
}
<div>
<span>
+
</span>
- <span
- class="emotion-0 emotion-1"
- >
+ <span>
click
</span>
</div>
padding-left: 0.125rem;
padding-right: 0.125rem;
border-radius: 0.125rem;
- color: rgb(62,67,87);
background-color: rgb(225,230,243);
+ color: rgb(62,67,87);
}
<div>
<div
class="sw-flex sw-gap-1"
>
+ <span>
+ Use
+ </span>
+ <span
+ class="emotion-0 emotion-1"
+ >
+ Ctrl
+ </span>
+ <span>
+ +
+ </span>
<span
class="emotion-0 emotion-1"
>
padding-left: 0.125rem;
padding-right: 0.125rem;
border-radius: 0.125rem;
- color: rgb(62,67,87);
background-color: rgb(225,230,243);
+ color: rgb(62,67,87);
}
<div>
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { useTheme } from '@emotion/react';
import { themeColor } from '../../helpers/theme';
import { CustomIcon, IconProps } from './Icon';
export function TestFileIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
const theme = useTheme();
const fillColor = themeColor(fill)({ theme });
+
return (
<CustomIcon {...iconProps}>
<path
export { StatusOpenIcon } from './StatusOpenIcon';
export { StatusReopenedIcon } from './StatusReopenedIcon';
export { StatusResolvedIcon } from './StatusResolvedIcon';
+export { TestFileIcon } from './TestFileIcon';
export { TrashIcon } from './TrashIcon';
export { TriangleDownIcon } from './TriangleDownIcon';
export { TriangleLeftIcon } from './TriangleLeftIcon';
*/
import { cloneDeep, uniqueId } from 'lodash';
import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
-import { mockIssueChangelog } from '../../helpers/mocks/issues';
+
+import { RESOLUTIONS, SEVERITIES, SOURCE_SCOPES, STATUSES } from '../../helpers/constants';
+import { mockIssueAuthors, mockIssueChangelog } from '../../helpers/mocks/issues';
import { RequestData } from '../../helpers/request';
import { getStandards } from '../../helpers/security-standard';
import { mockLoggedInUser, mockPaging, mockRuleDetails } from '../../helpers/testMocks';
RawIssuesResponse,
ReferencedComponent,
} from '../../types/issues';
+import { MetricKey } from '../../types/metrics';
import { SearchRulesQuery } from '../../types/rules';
import { Standards } from '../../types/security';
import { Dict, Rule, RuleActivation, RuleDetails, SnippetsByComponent } from '../../types/types';
-import { LoggedInUser, NoticeType } from '../../types/users';
+import { LoggedInUser, NoticeType, User } from '../../types/users';
import {
addIssueComment,
bulkChangeIssues,
editIssueComment,
getIssueChangelog,
getIssueFlowSnippets,
+ searchIssueAuthors,
searchIssueTags,
searchIssues,
setIssueAssignee,
this.list = cloneDeep(this.defaultList);
- jest.mocked(searchIssues).mockImplementation(this.handleSearchIssues);
- (getRuleDetails as jest.Mock).mockImplementation(this.handleGetRuleDetails);
- jest.mocked(searchRules).mockImplementation(this.handleSearchRules);
- (getIssueFlowSnippets as jest.Mock).mockImplementation(this.handleGetIssueFlowSnippets);
- (bulkChangeIssues as jest.Mock).mockImplementation(this.handleBulkChangeIssues);
- (getCurrentUser as jest.Mock).mockImplementation(this.handleGetCurrentUser);
- (dismissNotice as jest.Mock).mockImplementation(this.handleDismissNotification);
- (setIssueType as jest.Mock).mockImplementation(this.handleSetIssueType);
- jest.mocked(setIssueAssignee).mockImplementation(this.handleSetIssueAssignee);
- (setIssueSeverity as jest.Mock).mockImplementation(this.handleSetIssueSeverity);
- (setIssueTransition as jest.Mock).mockImplementation(this.handleSetIssueTransition);
- (setIssueTags as jest.Mock).mockImplementation(this.handleSetIssueTags);
jest.mocked(addIssueComment).mockImplementation(this.handleAddComment);
- jest.mocked(editIssueComment).mockImplementation(this.handleEditComment);
+ jest.mocked(bulkChangeIssues).mockImplementation(this.handleBulkChangeIssues);
jest.mocked(deleteIssueComment).mockImplementation(this.handleDeleteComment);
- (searchUsers as jest.Mock).mockImplementation(this.handleSearchUsers);
- (searchIssueTags as jest.Mock).mockImplementation(this.handleSearchIssueTags);
+ jest.mocked(dismissNotice).mockImplementation(this.handleDismissNotification);
+ jest.mocked(editIssueComment).mockImplementation(this.handleEditComment);
+ jest.mocked(getCurrentUser).mockImplementation(this.handleGetCurrentUser);
jest.mocked(getIssueChangelog).mockImplementation(this.handleGetIssueChangelog);
+ jest.mocked(getIssueFlowSnippets).mockImplementation(this.handleGetIssueFlowSnippets);
+ jest.mocked(getRuleDetails).mockImplementation(this.handleGetRuleDetails);
+ jest.mocked(searchIssueAuthors).mockImplementation(this.handleSearchIssueAuthors);
+ jest.mocked(searchIssues).mockImplementation(this.handleSearchIssues);
+ jest.mocked(searchIssueTags).mockImplementation(this.handleSearchIssueTags);
+ jest.mocked(searchRules).mockImplementation(this.handleSearchRules);
+ jest.mocked(searchUsers).mockImplementation(this.handleSearchUsers);
+ jest.mocked(setIssueAssignee).mockImplementation(this.handleSetIssueAssignee);
+ jest.mocked(setIssueSeverity).mockImplementation(this.handleSetIssueSeverity);
+ jest.mocked(setIssueTags).mockImplementation(this.handleSetIssueTags);
+ jest.mocked(setIssueTransition).mockImplementation(this.handleSetIssueTransition);
+ jest.mocked(setIssueType).mockImplementation(this.handleSetIssueType);
}
reset = () => {
.forEach((data) => {
data.issue.type = query.set_type;
});
- return this.reply({});
+ return this.reply(undefined);
};
handleGetIssueFlowSnippets = (issueKey: string): Promise<Dict<SnippetsByComponent>> => {
],
};
}
+ if (name === 'scopes') {
+ return {
+ property: name,
+ values: SOURCE_SCOPES.map(({ scope }) => ({
+ val: scope,
+ count: 1, // if 0, the facet can't be clicked in tests
+ })),
+ };
+ }
if (name === 'codeVariants') {
return {
property: 'codeVariants',
}, [] as RawFacet['values']),
};
}
- if (name === 'projects') {
+ if (name === MetricKey.projects) {
return {
property: name,
values: [
}
return {
property: name,
- values: [],
+ values: (
+ { resolutions: RESOLUTIONS, severities: SEVERITIES, statuses: STATUSES, types }[name] ??
+ []
+ ).map((val) => ({
+ val,
+ count: 1, // if 0, the facet can't be clicked in tests
+ })),
};
});
};
};
handleSearchUsers = () => {
- return this.reply({ users: [mockLoggedInUser()] });
+ return this.reply({ paging: mockPaging(), users: [mockLoggedInUser() as unknown as User] });
+ };
+
+ handleSearchIssueAuthors = () => {
+ return this.reply(mockIssueAuthors());
};
handleSearchIssueTags = () => {
- return this.reply(['accessibility', 'android']);
+ return this.reply(['accessibility', 'android', 'unused']);
};
handleGetIssueChangelog = (_issue: string) => {
import * as React from 'react';
import { Profile } from '../../../api/quality-profiles';
import { Dict } from '../../../types/types';
-import StandardFacet from '../../issues/sidebar/StandardFacet';
import { Facets, OpenFacets, Query } from '../query';
import ActivationSeverityFacet from './ActivationSeverityFacet';
import AvailableSinceFacet from './AvailableSinceFacet';
import LanguageFacet from './LanguageFacet';
import ProfileFacet from './ProfileFacet';
import RepositoryFacet from './RepositoryFacet';
+import { StandardFacet } from './StandardFacet';
import StatusFacet from './StatusFacet';
import TagFacet from './TagFacet';
import TemplateFacet from './TemplateFacet';
--- /dev/null
+/*
+ * 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 react/no-unused-prop-types */
+
+import { omit, sortBy, without } from 'lodash';
+import * as React from 'react';
+import FacetBox from '../../../components/facet/FacetBox';
+import FacetHeader from '../../../components/facet/FacetHeader';
+import FacetItem from '../../../components/facet/FacetItem';
+import FacetItemsList from '../../../components/facet/FacetItemsList';
+import ListStyleFacet from '../../../components/facet/ListStyleFacet';
+import ListStyleFacetFooter from '../../../components/facet/ListStyleFacetFooter';
+import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';
+import { translate } from '../../../helpers/l10n';
+import { highlightTerm } from '../../../helpers/search';
+import {
+ getStandards,
+ renderCWECategory,
+ renderOwaspTop102021Category,
+ renderOwaspTop10Category,
+ renderSonarSourceSecurityCategory,
+} from '../../../helpers/security-standard';
+import { Facet } from '../../../types/issues';
+import { SecurityStandard, Standards } from '../../../types/security';
+import { Dict } from '../../../types/types';
+import { Query, STANDARDS, formatFacetStat } from '../../issues/utils';
+
+interface Props {
+ cwe: string[];
+ cweOpen: boolean;
+ cweStats: Dict<number> | undefined;
+ fetchingCwe: boolean;
+ fetchingOwaspTop10: boolean;
+ 'fetchingOwaspTop10-2021': boolean;
+ fetchingSonarSourceSecurity: boolean;
+ loadSearchResultCount?: (property: string, changes: Partial<Query>) => Promise<Facet>;
+ onChange: (changes: Partial<Query>) => void;
+ onToggle: (property: string) => void;
+ open: boolean;
+ owaspTop10: string[];
+ owaspTop10Open: boolean;
+ owaspTop10Stats: Dict<number> | undefined;
+ 'owaspTop10-2021': string[];
+ 'owaspTop10-2021Open': boolean;
+ 'owaspTop10-2021Stats': Dict<number> | undefined;
+ query: Partial<Query>;
+ sonarsourceSecurity: string[];
+ sonarsourceSecurityOpen: boolean;
+ sonarsourceSecurityStats: Dict<number> | undefined;
+}
+
+interface State {
+ standards: Standards;
+ showFullSonarSourceList: boolean;
+}
+
+type StatsProp =
+ | 'owaspTop10-2021Stats'
+ | 'owaspTop10Stats'
+ | 'cweStats'
+ | 'sonarsourceSecurityStats';
+type ValuesProp = 'owaspTop10-2021' | 'owaspTop10' | 'sonarsourceSecurity' | 'cwe';
+
+const INITIAL_FACET_COUNT = 15;
+export class StandardFacet extends React.PureComponent<Props, State> {
+ mounted = false;
+ property = STANDARDS;
+ state: State = {
+ showFullSonarSourceList: false,
+ standards: {
+ owaspTop10: {},
+ 'owaspTop10-2021': {},
+ cwe: {},
+ sonarsourceSecurity: {},
+ 'pciDss-3.2': {},
+ 'pciDss-4.0': {},
+ 'owaspAsvs-4.0': {},
+ },
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+
+ // load standards.json only if the facet is open, or there is a selected value
+ if (
+ this.props.open ||
+ this.props.owaspTop10.length > 0 ||
+ this.props['owaspTop10-2021'].length > 0 ||
+ this.props.cwe.length > 0 ||
+ this.props.sonarsourceSecurity.length > 0
+ ) {
+ this.loadStandards();
+ }
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (!prevProps.open && this.props.open) {
+ this.loadStandards();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ loadStandards = () => {
+ getStandards().then(
+ ({
+ 'owaspTop10-2021': owaspTop102021,
+ owaspTop10,
+ cwe,
+ sonarsourceSecurity,
+ 'pciDss-3.2': pciDss32,
+ 'pciDss-4.0': pciDss40,
+ 'owaspAsvs-4.0': owaspAsvs40,
+ }: Standards) => {
+ if (this.mounted) {
+ this.setState({
+ standards: {
+ 'owaspTop10-2021': owaspTop102021,
+ owaspTop10,
+ cwe,
+ sonarsourceSecurity,
+ 'pciDss-3.2': pciDss32,
+ 'pciDss-4.0': pciDss40,
+ 'owaspAsvs-4.0': owaspAsvs40,
+ },
+ });
+ }
+ },
+ () => {}
+ );
+ };
+
+ getValues = () => {
+ return [
+ ...this.props.sonarsourceSecurity.map((item) =>
+ renderSonarSourceSecurityCategory(this.state.standards, item, true)
+ ),
+
+ ...this.props.owaspTop10.map((item) =>
+ renderOwaspTop10Category(this.state.standards, item, true)
+ ),
+
+ ...this.props['owaspTop10-2021'].map((item) =>
+ renderOwaspTop102021Category(this.state.standards, item, true)
+ ),
+
+ ...this.props.cwe.map((item) => renderCWECategory(this.state.standards, item)),
+ ];
+ };
+
+ getFacetHeaderId = (property: string) => {
+ return `facet_${property}`;
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ handleOwaspTop10HeaderClick = () => {
+ this.props.onToggle('owaspTop10');
+ };
+
+ handleOwaspTop102021HeaderClick = () => {
+ this.props.onToggle('owaspTop10-2021');
+ };
+
+ handleSonarSourceSecurityHeaderClick = () => {
+ this.props.onToggle('sonarsourceSecurity');
+ };
+
+ handleClear = () => {
+ this.props.onChange({
+ [this.property]: [],
+ owaspTop10: [],
+ 'owaspTop10-2021': [],
+ cwe: [],
+ sonarsourceSecurity: [],
+ });
+ };
+
+ handleItemClick = (prop: ValuesProp, itemValue: string, multiple: boolean) => {
+ const items = this.props[prop];
+
+ if (multiple) {
+ const newValue = sortBy(
+ items.includes(itemValue) ? without(items, itemValue) : [...items, itemValue]
+ );
+
+ this.props.onChange({ [prop]: newValue });
+ } else {
+ this.props.onChange({
+ [prop]: items.includes(itemValue) && items.length < 2 ? [] : [itemValue],
+ });
+ }
+ };
+
+ handleOwaspTop10ItemClick = (itemValue: string, multiple: boolean) => {
+ this.handleItemClick(SecurityStandard.OWASP_TOP10, itemValue, multiple);
+ };
+
+ handleOwaspTop102021ItemClick = (itemValue: string, multiple: boolean) => {
+ this.handleItemClick(SecurityStandard.OWASP_TOP10_2021, itemValue, multiple);
+ };
+
+ handleSonarSourceSecurityItemClick = (itemValue: string, multiple: boolean) => {
+ this.handleItemClick(SecurityStandard.SONARSOURCE, itemValue, multiple);
+ };
+
+ handleCWESearch = (query: string) => {
+ return Promise.resolve({
+ results: Object.keys(this.state.standards.cwe).filter((cwe) =>
+ renderCWECategory(this.state.standards, cwe).toLowerCase().includes(query.toLowerCase())
+ ),
+ });
+ };
+
+ loadCWESearchResultCount = (categories: string[]) => {
+ const { loadSearchResultCount } = this.props;
+
+ return loadSearchResultCount
+ ? loadSearchResultCount('cwe', { cwe: categories })
+ : Promise.resolve({});
+ };
+
+ renderList = (
+ statsProp: StatsProp,
+ valuesProp: ValuesProp,
+ renderName: (standards: Standards, category: string) => string,
+ onClick: (x: string, multiple?: boolean) => void
+ ) => {
+ const stats = this.props[statsProp];
+ const values = this.props[valuesProp];
+
+ if (!stats) {
+ return null;
+ }
+
+ const categories = sortBy(Object.keys(stats), (key) => -stats[key]);
+
+ return this.renderFacetItemsList(
+ stats,
+ values,
+ categories,
+ valuesProp,
+ renderName,
+ renderName,
+ onClick
+ );
+ };
+
+ // eslint-disable-next-line max-params
+ renderFacetItemsList = (
+ stats: Dict<number | undefined>,
+ values: string[],
+ categories: string[],
+ listKey: ValuesProp,
+ renderName: (standards: Standards, category: string) => React.ReactNode,
+ renderTooltip: (standards: Standards, category: string) => string,
+ onClick: (x: string, multiple?: boolean) => void
+ ) => {
+ if (!categories.length) {
+ return (
+ <div className="search-navigator-facet-empty little-spacer-top">
+ {translate('no_results')}
+ </div>
+ );
+ }
+
+ const getStat = (category: string) => {
+ return stats ? stats[category] : undefined;
+ };
+
+ return (
+ <FacetItemsList labelledby={this.getFacetHeaderId(listKey)}>
+ {categories.map((category) => (
+ <FacetItem
+ active={values.includes(category)}
+ key={category}
+ name={renderName(this.state.standards, category)}
+ onClick={onClick}
+ stat={formatFacetStat(getStat(category))}
+ tooltip={renderTooltip(this.state.standards, category)}
+ value={category}
+ />
+ ))}
+ </FacetItemsList>
+ );
+ };
+
+ renderHint = (statsProp: StatsProp, valuesProp: ValuesProp) => {
+ const stats = this.props[statsProp] ?? {};
+ const values = this.props[valuesProp];
+
+ return <MultipleSelectionHint options={Object.keys(stats).length} values={values.length} />;
+ };
+
+ renderOwaspTop10List() {
+ return this.renderList(
+ 'owaspTop10Stats',
+ SecurityStandard.OWASP_TOP10,
+ renderOwaspTop10Category,
+ this.handleOwaspTop10ItemClick
+ );
+ }
+
+ renderOwaspTop102021List() {
+ return this.renderList(
+ 'owaspTop10-2021Stats',
+ SecurityStandard.OWASP_TOP10_2021,
+ renderOwaspTop102021Category,
+ this.handleOwaspTop102021ItemClick
+ );
+ }
+
+ renderSonarSourceSecurityList() {
+ const stats = this.props.sonarsourceSecurityStats;
+ const values = this.props.sonarsourceSecurity;
+
+ if (!stats) {
+ return null;
+ }
+
+ const sortedItems = sortBy(
+ Object.keys(stats),
+ (key) => -stats[key],
+ (key) => renderSonarSourceSecurityCategory(this.state.standards, key)
+ );
+
+ const limitedList = this.state.showFullSonarSourceList
+ ? sortedItems
+ : sortedItems.slice(0, INITIAL_FACET_COUNT);
+
+ // make sure all selected items are displayed
+ const selectedBelowLimit = this.state.showFullSonarSourceList
+ ? []
+ : sortedItems.slice(INITIAL_FACET_COUNT).filter((item) => values.includes(item));
+
+ const allItemShown = limitedList.length + selectedBelowLimit.length === sortedItems.length;
+
+ return (
+ <>
+ <FacetItemsList labelledby={this.getFacetHeaderId(SecurityStandard.SONARSOURCE)}>
+ {limitedList.map((item) => (
+ <FacetItem
+ active={values.includes(item)}
+ key={item}
+ name={renderSonarSourceSecurityCategory(this.state.standards, item)}
+ onClick={this.handleSonarSourceSecurityItemClick}
+ stat={formatFacetStat(stats[item])}
+ tooltip={renderSonarSourceSecurityCategory(this.state.standards, item)}
+ value={item}
+ />
+ ))}
+ </FacetItemsList>
+
+ {selectedBelowLimit.length > 0 && (
+ <>
+ {!allItemShown && <div className="note spacer-bottom text-center">⋯</div>}
+ <FacetItemsList labelledby={this.getFacetHeaderId(SecurityStandard.SONARSOURCE)}>
+ {selectedBelowLimit.map((item) => (
+ <FacetItem
+ active={true}
+ key={item}
+ name={renderSonarSourceSecurityCategory(this.state.standards, item)}
+ onClick={this.handleSonarSourceSecurityItemClick}
+ stat={formatFacetStat(stats[item])}
+ tooltip={renderSonarSourceSecurityCategory(this.state.standards, item)}
+ value={item}
+ />
+ ))}
+ </FacetItemsList>
+ </>
+ )}
+
+ {!allItemShown && (
+ <ListStyleFacetFooter
+ showMoreAriaLabel={translate('issues.facet.sonarsource.show_more')}
+ count={limitedList.length + selectedBelowLimit.length}
+ showMore={() => this.setState({ showFullSonarSourceList: true })}
+ total={sortedItems.length}
+ />
+ )}
+ </>
+ );
+ }
+
+ renderOwaspTop10Hint() {
+ return this.renderHint('owaspTop10Stats', SecurityStandard.OWASP_TOP10);
+ }
+
+ renderOwaspTop102021Hint() {
+ return this.renderHint('owaspTop10-2021Stats', SecurityStandard.OWASP_TOP10_2021);
+ }
+
+ renderSonarSourceSecurityHint() {
+ return this.renderHint('sonarsourceSecurityStats', SecurityStandard.SONARSOURCE);
+ }
+
+ renderSubFacets() {
+ const {
+ cwe,
+ cweOpen,
+ cweStats,
+ fetchingCwe,
+ fetchingOwaspTop10,
+ 'fetchingOwaspTop10-2021': fetchingOwaspTop102021,
+ fetchingSonarSourceSecurity,
+ owaspTop10,
+ owaspTop10Open,
+ 'owaspTop10-2021Open': owaspTop102021Open,
+ 'owaspTop10-2021': owaspTop102021,
+ query,
+ sonarsourceSecurity,
+ sonarsourceSecurityOpen,
+ } = this.props;
+
+ return (
+ <>
+ <FacetBox className="is-inner" property={SecurityStandard.SONARSOURCE}>
+ <FacetHeader
+ fetching={fetchingSonarSourceSecurity}
+ id={this.getFacetHeaderId(SecurityStandard.SONARSOURCE)}
+ name={translate('issues.facet.sonarsourceSecurity')}
+ onClick={this.handleSonarSourceSecurityHeaderClick}
+ open={sonarsourceSecurityOpen}
+ values={sonarsourceSecurity.map((item) =>
+ renderSonarSourceSecurityCategory(this.state.standards, item)
+ )}
+ />
+
+ {sonarsourceSecurityOpen && (
+ <>
+ {this.renderSonarSourceSecurityList()}
+ {this.renderSonarSourceSecurityHint()}
+ </>
+ )}
+ </FacetBox>
+
+ <FacetBox className="is-inner" property={SecurityStandard.OWASP_TOP10_2021}>
+ <FacetHeader
+ fetching={fetchingOwaspTop102021}
+ id={this.getFacetHeaderId(SecurityStandard.OWASP_TOP10_2021)}
+ name={translate('issues.facet.owaspTop10_2021')}
+ onClick={this.handleOwaspTop102021HeaderClick}
+ open={owaspTop102021Open}
+ values={owaspTop102021.map((item) =>
+ renderOwaspTop102021Category(this.state.standards, item)
+ )}
+ />
+
+ {owaspTop102021Open && (
+ <>
+ {this.renderOwaspTop102021List()}
+ {this.renderOwaspTop102021Hint()}
+ </>
+ )}
+ </FacetBox>
+
+ <FacetBox className="is-inner" property={SecurityStandard.OWASP_TOP10}>
+ <FacetHeader
+ fetching={fetchingOwaspTop10}
+ id={this.getFacetHeaderId(SecurityStandard.OWASP_TOP10)}
+ name={translate('issues.facet.owaspTop10')}
+ onClick={this.handleOwaspTop10HeaderClick}
+ open={owaspTop10Open}
+ values={owaspTop10.map((item) => renderOwaspTop10Category(this.state.standards, item))}
+ />
+
+ {owaspTop10Open && (
+ <>
+ {this.renderOwaspTop10List()}
+ {this.renderOwaspTop10Hint()}
+ </>
+ )}
+ </FacetBox>
+
+ <ListStyleFacet<string>
+ className="is-inner"
+ facetHeader={translate('issues.facet.cwe')}
+ fetching={fetchingCwe}
+ getFacetItemText={(item) => renderCWECategory(this.state.standards, item)}
+ getSearchResultKey={(item) => item}
+ getSearchResultText={(item) => renderCWECategory(this.state.standards, item)}
+ loadSearchResultCount={this.loadCWESearchResultCount}
+ onChange={this.props.onChange}
+ onSearch={this.handleCWESearch}
+ onToggle={this.props.onToggle}
+ open={cweOpen}
+ property={SecurityStandard.CWE}
+ query={omit(query, 'cwe')}
+ renderFacetItem={(item) => renderCWECategory(this.state.standards, item)}
+ renderSearchResult={(item, query) =>
+ highlightTerm(renderCWECategory(this.state.standards, item), query)
+ }
+ searchPlaceholder={translate('search.search_for_cwe')}
+ stats={cweStats}
+ values={cwe}
+ />
+ </>
+ );
+ }
+
+ render() {
+ const { open } = this.props;
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ id={this.getFacetHeaderId(this.property)}
+ name={translate('issues.facet', this.property)}
+ onClear={this.handleClear}
+ onClick={this.handleHeaderClick}
+ open={open}
+ values={this.getValues()}
+ />
+
+ {open && this.renderSubFacets()}
+ </FacetBox>
+ );
+ }
+}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { act, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import selectEvent from 'react-select-event';
// Status
await user.click(ui.statusFacet.get());
+
await user.click(ui.openStatusFilter.get());
expect(ui.issueItem6.query()).not.toBeInTheDocument(); // Issue 6 should vanish
// Rule
await user.click(ui.ruleFacet.get());
await user.click(screen.getByRole('checkbox', { name: 'other' }));
- expect(screen.getByRole('checkbox', { name: '(HTML) Advanced rule' })).toBeInTheDocument(); // Name should apply to the rule
+
+ // Name should apply to the rule
+ expect(screen.getByRole('checkbox', { name: '(HTML) Advanced rule' })).toBeInTheDocument();
// Tag
await user.click(ui.tagFacet.get());
+ await user.type(ui.tagFacetSearch.get(), 'unu');
await user.click(screen.getByRole('checkbox', { name: 'unused' }));
// Project
// Author
await user.click(ui.authorFacet.get());
+ await user.type(ui.authorFacetSearch.get(), 'email');
await user.click(screen.getByRole('checkbox', { name: 'email4@sonarsource.com' }));
await user.click(screen.getByRole('checkbox', { name: 'email3@sonarsource.com' })); // Change author
expect(ui.issueItem1.query()).not.toBeInTheDocument();
const user = userEvent.setup();
const currentUser = mockLoggedInUser();
issuesHandler.setCurrentUser(currentUser);
+
renderIssueApp(currentUser);
+
await waitOnDataLoaded();
// Select a specific date range such that only one issue matches
await user.click(ui.creationDateFacet.get());
await user.click(screen.getByPlaceholderText('start_date'));
- await user.selectOptions(ui.dateInputMonthSelect.get(), 'January');
- await user.selectOptions(ui.dateInputYearSelect.get(), '2023');
- await user.click(screen.getByText('1'));
+
+ const monthSelector = within(ui.dateInputMonthSelect.get()).getByRole('combobox');
+
+ await user.click(monthSelector);
+
+ await user.click(within(ui.dateInputMonthSelect.get()).getByText('Jan'));
+
+ const yearSelector = within(ui.dateInputYearSelect.get()).getByRole('combobox');
+
+ await user.click(yearSelector);
+
+ await user.click(within(ui.dateInputYearSelect.get()).getAllByText('2023')[-1]);
+
+ await user.click(screen.getByText('1', { selector: 'button' }));
await user.click(screen.getByText('10'));
expect(ui.issueItem1.get()).toBeInTheDocument();
expect(ui.issueItem3.get()).toBeInTheDocument();
// Only show my issues
- await user.click(screen.getByRole('button', { name: 'issues.my_issues' }));
+ await user.click(screen.getByRole('radio', { name: 'issues.my_issues' }));
expect(ui.issueItem2.query()).not.toBeInTheDocument();
expect(ui.issueItem3.get()).toBeInTheDocument();
// Show all issues again
- await user.click(screen.getByRole('button', { name: 'all' }));
+ await user.click(screen.getByRole('radio', { name: 'all' }));
expect(ui.issueItem2.get()).toBeInTheDocument();
expect(ui.issueItem3.get()).toBeInTheDocument();
});
renderIssueApp();
await user.click(await ui.ruleFacet.find());
+
await user.type(ui.ruleFacetSearch.get(), 'rule');
+
expect(within(ui.ruleFacetList.get()).getAllByRole('checkbox')).toHaveLength(2);
+
expect(
within(ui.ruleFacetList.get()).getByRole('checkbox', {
name: /Advanced rule/,
})
).toBeInTheDocument();
+
expect(
within(ui.ruleFacetList.get()).getByRole('checkbox', {
name: /Simple rule/,
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { debounce } from 'lodash';
import * as React from 'react';
import { components, OptionProps, SingleValueProps } from 'react-select';
searchAssignees(query)
.then(({ results }) =>
results.map((r) => {
- const userInfo = r.name || r.login;
+ const userInfo = r.name ?? r.login;
return {
avatar: r.avatar,
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import styled from '@emotion/styled';
+
import classNames from 'classnames';
+import { FlagMessage, ToggleButton } from 'design-system';
import { debounce, keyBy, omit, without } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { PageContext } from '../../../app/components/indexation/PageUnavailableDueToIndexation';
import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import EmptySearch from '../../../components/common/EmptySearch';
-import FiltersHeader from '../../../components/common/FiltersHeader';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
-import ButtonToggle from '../../../components/controls/ButtonToggle';
import Checkbox from '../../../components/controls/Checkbox';
-import HelpTooltip from '../../../components/controls/HelpTooltip';
import ListFooter from '../../../components/controls/ListFooter';
import { Button } from '../../../components/controls/buttons';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
import RuleTabViewer from '../../../components/rules/RuleTabViewer';
import '../../../components/search-navigator.css';
-import { Alert } from '../../../components/ui/Alert';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
import {
fillBranchLike,
import { CurrentUser, UserBase } from '../../../types/users';
import * as actions from '../actions';
import SubnavigationIssuesList from '../issues-subnavigation/SubnavigationIssuesList';
-import Sidebar from '../sidebar/Sidebar';
+import { FiltersHeader } from '../sidebar/FiltersHeader';
+import { Sidebar } from '../sidebar/Sidebar';
import '../styles.css';
import {
Query,
super(props);
const query = parseQuery(props.location.query);
this.bulkButtonRef = React.createRef();
+
this.state = {
bulkChangeModal: false,
checked: [],
referencedUsers: {},
selected: getOpen(props.location.query),
};
+
this.refreshBranchStatus = debounce(this.refreshBranchStatus, BRANCH_STATUS_REFRESH_INTERVAL);
}
addWhitePageClass();
addSideBarClass();
this.attachShortcuts();
- this.fetchFirstIssues(true);
+ this.fetchFirstIssues(true).catch(() => undefined);
}
componentDidUpdate(prevProps: Props, prevState: State) {
!areQueriesEqual(prevQuery, query) ||
areMyIssuesSelected(prevQuery) !== areMyIssuesSelected(query)
) {
- this.fetchFirstIssues(false);
+ this.fetchFirstIssues(false).catch(() => undefined);
this.setState({ checkAll: false });
} else if (openIssue && openIssue.key !== this.state.selected) {
this.setState({
selectedLocationIndex: undefined,
});
}
+
if (this.state.openIssue && this.state.openIssue.key !== prevState.openIssue?.key) {
- this.loadRule();
+ this.loadRule().catch(() => undefined);
}
}
componentWillUnmount() {
this.detachShortcuts();
this.mounted = false;
+
removeWhitePageClass();
removeSideBarClass();
}
switch (event.key) {
case KeyboardKeys.DownArrow: {
event.preventDefault();
+
if (event.altKey) {
this.selectNextLocation();
} else {
this.selectNextIssue();
}
+
break;
}
case KeyboardKeys.UpArrow: {
event.preventDefault();
+
if (event.altKey) {
this.selectPreviousLocation();
} else {
this.selectPreviousIssue();
}
+
break;
}
case KeyboardKeys.LeftArrow: {
event.preventDefault();
+
if (event.altKey) {
this.selectPreviousFlow();
} else {
this.closeIssue();
}
+
break;
}
case KeyboardKeys.RightArrow: {
event.preventDefault();
+
if (event.altKey) {
this.selectNextFlow();
} else {
this.openSelectedIssue();
}
+
break;
}
}
getSelectedIndex() {
const { issues = [], selected } = this.state;
const index = issues.findIndex((issue) => issue.key === selected);
+
return index !== -1 ? index : undefined;
}
selectNextIssue = () => {
const { issues } = this.state;
const selectedIndex = this.getSelectedIndex();
+
if (selectedIndex !== undefined && selectedIndex < issues.length - 1) {
if (this.state.openIssue) {
this.openIssue(issues[selectedIndex + 1].key);
async loadRule() {
const { openIssue } = this.state;
+
if (openIssue === undefined) {
return;
}
+
this.setState({ loadingRule: true });
+
const openRuleDetails = await getRuleDetails({ key: openIssue.rule })
.then((response) => response.rule)
.catch(() => undefined);
+
if (this.mounted) {
this.setState({ loadingRule: false, openRuleDetails });
}
selectPreviousIssue = () => {
const { issues } = this.state;
const selectedIndex = this.getSelectedIndex();
+
if (selectedIndex !== undefined && selectedIndex > 0) {
if (this.state.openIssue) {
this.openIssue(issues[selectedIndex - 1].key);
query: {
...serializeQuery(this.state.query),
...getBranchLikeQuery(this.props.branchLike),
- id: this.props.component && this.props.component.key,
+ id: this.props.component?.key,
myIssues: this.state.myIssues ? 'true' : undefined,
open: issueKey,
},
};
+
if (this.state.openIssue) {
if (path.query.open && path.query.open === this.state.openIssue.key) {
this.setState({
query: {
...serializeQuery(this.state.query),
...getBranchLikeQuery(this.props.branchLike),
- id: this.props.component && this.props.component.key,
+ id: this.props.component?.key,
myIssues: this.state.myIssues ? 'true' : undefined,
open: undefined,
},
openSelectedIssue = () => {
const { selected } = this.state;
+
if (selected) {
this.openIssue(selected);
}
const parsedIssues = response.issues.map((issue) =>
parseIssueFromResponse(issue, response.components, response.users, response.rules)
);
+
return { ...response, issues: parsedIssues } as FetchIssuesPromise;
});
};
const parameters: Dict<string | undefined> = {
...getBranchLikeQuery(this.props.branchLike),
- componentKeys: component && component.key,
+ componentKeys: component?.key,
s: 'FILE_LINE',
...serializeQuery(query),
ps: '100',
let fetchPromise;
this.setState({ checked: [], loading: true });
+
if (openIssueKey !== undefined) {
fetchPromise = this.fetchIssuesUntil(1, (pageIssues: Issue[], paging: Paging) => {
if (
) {
return true;
}
+
return pageIssues.some((issue) => issue.key === openIssueKey);
});
} else {
if (this.mounted && areQueriesEqual(prevQuery, this.props.location.query)) {
const openIssue = getOpenIssue(this.props, issues);
let selected: string | undefined = undefined;
+
if (issues.length > 0) {
selected = openIssue ? openIssue.key : issues[0].key;
}
+
this.setState(({ showVariantsFilter }) => ({
cannotShowOpenIssue: Boolean(openIssueKey && !openIssue),
effortTotal,
selectedLocationIndex: undefined,
}));
}
+
return issues;
},
() => {
if (this.mounted && areQueriesEqual(prevQuery, this.props.location.query)) {
this.setState({ loading: false });
}
+
return [];
}
);
const recursiveFetch = (p: number, prevIssues: Issue[]): Promise<FetchIssuesPromise> => {
return this.fetchIssuesPage(p).then(({ issues: pageIssues, paging, ...other }) => {
const issues = [...prevIssues, ...pageIssues];
+
+ // eslint-disable-next-line promise/no-callback-in-promise
return done(pageIssues, paging)
? { issues, paging, ...other }
: recursiveFetch(p + 1, issues);
const p = paging.pageIndex + 1;
this.setState({ checkAll: false, loadingMore: true });
+
return this.fetchIssuesPage(p).then(
(response) => {
if (this.mounted) {
isFiltered = () => {
const serialized = serializeQuery(this.state.query);
+
return !areQueriesEqual(serialized, DEFAULT_QUERY);
};
const issues = this.state.checked
.map((checked) => this.state.issues.find((issue) => issue.key === checked))
.filter((issue): issue is Issue => issue !== undefined);
+
const paging = { pageIndex: 1, pageSize: issues.length, total: issues.length };
+
return Promise.resolve({ issues, paging });
};
}
let count;
+
if (checkAll && paging) {
count = paging.total > MAX_PAGE_SIZE ? MAX_PAGE_SIZE : paging.total;
} else {
query: {
...serializeQuery({ ...this.state.query, ...changes }),
...getBranchLikeQuery(this.props.branchLike),
- id: this.props.component && this.props.component.key,
+ id: this.props.component?.key,
myIssues: this.state.myIssues ? 'true' : undefined,
},
});
+
this.setState(({ openFacets }) => ({
openFacets: {
...openFacets,
handleMyIssuesChange = (myIssues: boolean) => {
this.closeFacet('assignees');
+
if (!this.props.component) {
saveMyIssues(myIssues);
}
+
this.props.router.push({
pathname: this.props.location.pathname,
query: {
...serializeQuery({ ...this.state.query, assigned: true, assignees: [] }),
...getBranchLikeQuery(this.props.branchLike),
- id: this.props.component && this.props.component.key,
+ id: this.props.component?.key,
myIssues: myIssues ? 'true' : undefined,
},
});
const parameters = {
...getBranchLikeQuery(this.props.branchLike),
- componentKeys: component && component.key,
+ componentKeys: component?.key,
facets: property,
s: 'FILE_LINE',
...serializeQuery({ ...query, ...changes }),
handleFacetToggle = (property: string) => {
this.setState((state) => {
const willOpenProperty = !state.openFacets[property];
+
const newState = {
loadingFacets: state.loadingFacets,
openFacets: { ...state.openFacets, [property]: willOpenProperty },
newState.openFacets,
state.query
);
+
// Force loading of sonarsource security facet data
property = newState.openFacets.sonarsourceSecurity ? 'sonarsourceSecurity' : property;
}
// No need to load facets data for standard facet
if (property !== STANDARDS && !state.facets[property]) {
newState.loadingFacets[property] = true;
- this.fetchFacet(property);
+
+ this.fetchFacet(property).catch(() => undefined);
}
return newState;
query: {
...DEFAULT_QUERY,
...getBranchLikeQuery(this.props.branchLike),
- id: this.props.component && this.props.component.key,
+ id: this.props.component?.key,
myIssues: this.state.myIssues ? 'true' : undefined,
},
});
handleIssueChange = (issue: Issue) => {
this.refreshBranchStatus();
+
this.setState((state) => ({
issues: state.issues.map((candidate) => (candidate.key === issue.key ? issue : candidate)),
}));
handleBulkChangeDone = () => {
this.setState({ checkAll: false });
this.refreshBranchStatus();
- this.fetchFirstIssues(false);
+ this.fetchFirstIssues(false).catch(() => undefined);
this.handleCloseBulkChange();
};
selectLocation = (index: number) => {
const { selectedLocationIndex } = this.state;
+
if (index === selectedLocationIndex) {
this.setState({ selectedLocationIndex: undefined }, () => {
this.setState({ selectedLocationIndex: index });
if (openIssue) {
return { locationsNavigator: true, selectedLocationIndex: index };
}
+
return null;
});
}
refreshBranchStatus = () => {
const { branchLike, component } = this.props;
+
if (branchLike && component && isPullRequest(branchLike)) {
this.props.fetchBranchStatus(branchLike, component.key);
}
thirdState={thirdState}
title={translate('issues.select_all_issues')}
/>
+
<Button
innerRef={this.bulkButtonRef}
disabled={checked.length === 0}
);
}
- renderFacets() {
+ renderFacets(warning?: React.ReactNode) {
const { component, currentUser, branchLike } = this.props;
const {
query,
} = this.state;
return (
- <div className="layout-page-filters">
+ <div
+ className={
+ 'it__layout-page-filters sw-bg-white sw-box-border sw-h-full sw-overflow-y-auto ' +
+ 'sw-pt-6 sw-pl-3 sw-pr-4 sw-w-[300px] lg:sw-w-[390px]'
+ }
+ style={{ borderLeft: '1px solid #dddddd', borderTop: '1px solid #dddddd' }}
+ >
+ {warning}
+
{currentUser.isLoggedIn && (
- <div className="display-flex-justify-center big-spacer-bottom">
- <ButtonToggle
+ <div className="sw-flex sw-justify-start sw-mb-8">
+ <ToggleButton
+ onChange={this.handleMyIssuesChange}
options={[
{ value: true, label: translate('issues.my_issues') },
{ value: false, label: translate('all') },
]}
value={this.state.myIssues}
- onCheck={this.handleMyIssuesChange}
/>
</div>
)}
+
<FiltersHeader displayReset={this.isFiltered()} onReset={this.handleReset} />
+
<Sidebar
branchLike={branchLike}
component={component}
renderSide(openIssue: Issue | undefined) {
const { canBrowseAllChildProjects, qualifier = ComponentQualifier.Project } =
- this.props.component || {};
+ this.props.component ?? {};
- const { issues, paging } = this.state;
+ const {
+ issues,
+ loading,
+ loadingMore,
+ paging,
+ selected,
+ selectedFlowIndex,
+ selectedLocationIndex,
+ } = this.state;
+
+ const warning = !canBrowseAllChildProjects && isPortfolioLike(qualifier) && (
+ <FlagMessage
+ ariaLabel={translate('issues.not_all_issue_show')}
+ className="it__portfolio_warning sw-flex sw-my-4"
+ title={translate('issues.not_all_issue_show_why')}
+ variant="warning"
+ >
+ {translate('issues.not_all_issue_show')}
+ </FlagMessage>
+ );
return (
<ScreenPositionHelper className="layout-page-side-outer">
className="layout-page-side"
style={{ top }}
>
- <div className="layout-page-side-inner">
+ <div className="sw-flex sw-h-full sw-justify-end">
<A11ySkipTarget
anchor="issues_sidebar"
label={
}
weight={10}
/>
- {!canBrowseAllChildProjects && isPortfolioLike(qualifier) && (
- <div
- className={classNames('not-all-issue-warning', {
- 'open-issue-list': openIssue,
- })}
- >
- <Alert className={classNames('it__portfolio_warning')} variant="warning">
- <AlertContent>
- {translate('issues.not_all_issue_show')}
- <HelpTooltip
- className="spacer-left"
- overlay={translate('issues.not_all_issue_show_why')}
- />
- </AlertContent>
- </Alert>
- </div>
- )}
{openIssue ? (
- <SubnavigationIssuesList
- fetchMoreIssues={this.fetchMoreIssues}
- issues={issues}
- loading={this.state.loading}
- loadingMore={this.state.loadingMore}
- onFlowSelect={this.selectFlow}
- onIssueSelect={this.openIssue}
- onLocationSelect={this.selectLocation}
- paging={paging}
- selected={this.state.selected}
- selectedFlowIndex={this.state.selectedFlowIndex}
- selectedLocationIndex={this.state.selectedLocationIndex}
- />
+ <div>
+ <div className={classNames('not-all-issue-warning', 'open-issue-list')}>
+ {warning}
+ </div>
+
+ <SubnavigationIssuesList
+ fetchMoreIssues={this.fetchMoreIssues}
+ issues={issues}
+ loading={loading}
+ loadingMore={loadingMore}
+ onFlowSelect={this.selectFlow}
+ onIssueSelect={this.openIssue}
+ onLocationSelect={this.selectLocation}
+ paging={paging}
+ selected={selected}
+ selectedFlowIndex={selectedFlowIndex}
+ selectedLocationIndex={selectedLocationIndex}
+ />
+ </div>
) : (
- this.renderFacets()
+ this.renderFacets(warning)
)}
</div>
</nav>
}
let noIssuesMessage = null;
+
if (paging.total === 0 && !loading) {
if (this.isFiltered()) {
noIssuesMessage = <EmptySearch />;
return (
<div>
<h2 className="a11y-hidden">{translate('list_of_issues')}</h2>
+
{paging.total > 0 && (
<IssuesList
branchLike={branchLike}
{paging.total > 0 && (
<ListFooter
count={issues.length}
- loadMore={this.fetchMoreIssues}
+ loadMore={() => {
+ this.fetchMoreIssues().catch(() => undefined);
+ }}
loading={loadingMore}
total={paging.total}
/>
<A11ySkipTarget anchor="issues_main" />
{this.renderBulkChange()}
+
<PageActions
canSetHome={!this.props.component}
effortTotal={this.state.effortTotal}
return (
<div className="layout-page-main-inner">
<DeferredSpinner loading={loadingRule}>
+ {/* eslint-disable-next-line local-rules/no-conditional-rendering-of-deferredspinner */}
{openIssue && openRuleDetails ? (
<>
<IssueHeader
branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
onIssueChange={this.handleIssueChange}
/>
+
<RuleTabViewer
ruleDetails={openRuleDetails}
extendedDescription={openRuleDetails.htmlNote}
) : (
<DeferredSpinner loading={loading} ariaLabel={translate('issues.loading_issues')}>
{checkAll && paging && paging.total > MAX_PAGE_SIZE && (
- <Alert className="big-spacer-bottom" variant="warning">
+ <FlagMessage
+ ariaLabel={translate('issue_bulk_change.max_issues_reached')}
+ className="sw-mb-4"
+ variant="warning"
+ >
<FormattedMessage
defaultMessage={translate('issue_bulk_change.max_issues_reached')}
id="issue_bulk_change.max_issues_reached"
values={{ max: <strong>{MAX_PAGE_SIZE}</strong> }}
/>
- </Alert>
+ </FlagMessage>
)}
+
{cannotShowOpenIssue && (!paging || paging.total > 0) && (
- <Alert className="big-spacer-bottom" variant="warning">
+ <FlagMessage
+ ariaLabel={translateWithParameters(
+ 'issues.cannot_open_issue_max_initial_X_fetched',
+ MAX_INITAL_FETCH
+ )}
+ className="sw-mb-4"
+ variant="warning"
+ >
{translateWithParameters(
'issues.cannot_open_issue_max_initial_X_fetched',
MAX_INITAL_FETCH
)}
- </Alert>
+ </FlagMessage>
)}
+
{this.renderList()}
</DeferredSpinner>
)}
const { component } = this.props;
const { openIssue, paging } = this.state;
const selectedIndex = this.getSelectedIndex();
+
return (
<div
className={classNames('layout-page issues', { 'project-level': component !== undefined })}
id="issues-page"
>
<Suggestions suggestions="issues" />
+
{openIssue ? (
<Helmet
defer={false}
}
}
-const AlertContent = styled.div`
- display: flex;
- align-items: center;
-`;
-
export default withIndexationGuard(
withRouter(withCurrentUserContext(withBranchStatusActions(withComponentContext(App)))),
PageContext.Issues
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+import { Avatar } from 'design-system';
import { omit, sortBy, without } from 'lodash';
import * as React from 'react';
-import ListStyleFacet from '../../../components/facet/ListStyleFacet';
-import Avatar from '../../../components/ui/Avatar';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { highlightTerm } from '../../../helpers/search';
import { Facet } from '../../../types/issues';
import { Dict } from '../../../types/types';
-import { isUserActive, UserBase } from '../../../types/users';
+import { UserBase, isUserActive } from '../../../types/users';
import { Query, searchAssignees } from '../utils';
+import { ListStyleFacet } from './ListStyleFacet';
interface Props {
assigned: boolean;
referencedUsers: Dict<UserBase>;
}
-export default class AssigneeFacet extends React.PureComponent<Props> {
+export class AssigneeFacet extends React.PureComponent<Props> {
handleSearch = (query: string, page?: number) => {
return searchAssignees(query, page);
};
handleItemClick = (itemValue: string, multiple: boolean) => {
const { assignees } = this.props;
+
if (itemValue === '') {
// unassigned
this.props.onChange({ assigned: !this.props.assigned, assignees: [] });
const newValue = sortBy(
assignees.includes(itemValue) ? without(assignees, itemValue) : [...assignees, itemValue]
);
+
this.props.onChange({ assigned: true, assignees: newValue });
} else {
this.props.onChange({
getAssigneeName = (assignee: string) => {
if (assignee === '') {
return translate('unassigned');
- } else {
- const user = this.props.referencedUsers[assignee];
- if (!user) {
- return assignee;
- }
- return isUserActive(user) ? user.name : translateWithParameters('user.x_deleted', user.login);
}
+
+ const user = this.props.referencedUsers[assignee];
+
+ if (!user) {
+ return assignee;
+ }
+
+ return isUserActive(user) ? user.name : translateWithParameters('user.x_deleted', user.login);
};
loadSearchResultCount = (assignees: UserBase[]) => {
getSortedItems = () => {
const { stats = {} } = this.props;
+
return sortBy(
Object.keys(stats),
// put "not assigned" first
return assignee;
}
- const userName = user.name || user.login;
+ const userName = user.name ?? user.login;
return (
<>
- <Avatar className="little-spacer-right" hash={user.avatar} name={userName} size={16} />
+ <Avatar className="sw-mr-1" hash={user.avatar} name={userName} size="xs" />
+
{isUserActive(user) ? userName : translateWithParameters('user.x_deleted', userName)}
</>
);
const displayName = isUserActive(result)
? result.name
: translateWithParameters('user.x_deleted', result.login);
+
return (
<>
<Avatar
- className="little-spacer-right"
+ className="sw-mr-1"
hash={result.avatar}
- name={result.name || result.login}
- size={16}
+ name={result.name ?? result.login}
+ size="xs"
/>
+
{highlightTerm(displayName, query)}
</>
);
render() {
const values = [...this.props.assignees];
+
if (!this.props.assigned) {
values.push('');
}
fetching={this.props.fetching}
getFacetItemText={this.getAssigneeName}
getSearchResultKey={(user) => user.login}
- getSearchResultText={(user) => user.name || user.login}
+ getSearchResultText={(user) => user.name ?? user.login}
// put "not assigned" item first
getSortedItems={this.getSortedItems}
loadSearchResultCount={this.loadSearchResultCount}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { omit } from 'lodash';
import * as React from 'react';
import { searchIssueAuthors } from '../../../api/issues';
-import ListStyleFacet from '../../../components/facet/ListStyleFacet';
import { translate } from '../../../helpers/l10n';
import { highlightTerm } from '../../../helpers/search';
+import { ComponentQualifier } from '../../../types/component';
import { Facet } from '../../../types/issues';
import { Component, Dict } from '../../../types/types';
import { Query } from '../utils';
+import { ListStyleFacet } from './ListStyleFacet';
interface Props {
+ author: string[];
component: Component | undefined;
fetching: boolean;
loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
open: boolean;
query: Query;
stats: Dict<number> | undefined;
- author: string[];
}
const SEARCH_SIZE = 100;
-export default class AuthorFacet extends React.PureComponent<Props> {
- identity = (author: string) => {
- return author;
- };
-
+export class AuthorFacet extends React.PureComponent<Props> {
handleSearch = (query: string, _page: number) => {
const { component } = this.props;
+
const project =
- component && ['TRK', 'VW', 'APP'].includes(component.qualifier) ? component.key : undefined;
+ component &&
+ [
+ ComponentQualifier.Application,
+ ComponentQualifier.Portfolio,
+ ComponentQualifier.Project,
+ ].includes(component.qualifier as ComponentQualifier)
+ ? component.key
+ : undefined;
+
return searchIssueAuthors({
project,
ps: SEARCH_SIZE, // maximum
<ListStyleFacet<string>
facetHeader={translate('issues.facet.authors')}
fetching={this.props.fetching}
- getFacetItemText={this.identity}
- getSearchResultKey={this.identity}
- getSearchResultText={this.identity}
loadSearchResultCount={this.loadSearchResultCount}
onChange={this.props.onChange}
onSearch={this.handleSearch}
open={this.props.open}
property="author"
query={omit(this.props.query, 'author')}
- renderFacetItem={this.identity}
renderSearchResult={this.renderSearchResult}
searchPlaceholder={translate('search.search_for_authors')}
stats={this.props.stats}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { isSameDay } from 'date-fns';
+import { BarChart, DateRangePicker, FacetBox, FacetItem } from 'design-system';
import { max } from 'lodash';
import * as React from 'react';
-import { injectIntl, WrappedComponentProps } from 'react-intl';
-import BarChart from '../../../components/charts/BarChart';
-import DateRangeInput from '../../../components/controls/DateRangeInput';
-import FacetBox from '../../../components/facet/FacetBox';
-import FacetHeader from '../../../components/facet/FacetHeader';
-import FacetItem from '../../../components/facet/FacetItem';
+import { WrappedComponentProps, injectIntl } from 'react-intl';
import { longFormatterOption } from '../../../components/intl/DateFormatter';
import DateFromNow from '../../../components/intl/DateFromNow';
-import DateTimeFormatter, {
- formatterOption as dateTimeFormatterOption,
-} from '../../../components/intl/DateTimeFormatter';
+import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import { parseDate } from '../../../helpers/dates';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';
+import { MetricType } from '../../../types/metrics';
import { Component, Dict } from '../../../types/types';
import { Query } from '../utils';
stats: Dict<number> | undefined;
}
-export class CreationDateFacet extends React.PureComponent<Props & WrappedComponentProps> {
+export class CreationDateFacetClass extends React.PureComponent<Props & WrappedComponentProps> {
property = 'createdAt';
static defaultProps = {
this.props.createdInLast.length > 0 ||
this.props.inNewCodePeriod;
- handleHeaderClick = () => {
- this.props.onToggle(this.property);
- };
-
- handleClear = () => {
- this.resetTo({});
- };
-
resetTo = (changes: Partial<Query>) => {
this.props.onChange({
createdAfter: undefined,
});
};
- handleBarClick = ({
- createdAfter,
- createdBefore,
- }: {
- createdAfter: Date;
- createdBefore?: Date;
- }) => {
- this.resetTo({ createdAfter, createdBefore });
- };
-
handlePeriodChange = ({ from, to }: { from?: Date; to?: Date }) => {
this.resetTo({ createdAfter: from, createdBefore: to });
};
handlePeriodClick = (period: string) => this.resetTo({ createdInLast: period });
- getValues() {
- const { createdAfter, createdAfterIncludesTime, createdAt, createdBefore, createdInLast } =
- this.props;
- const { formatDate } = this.props.intl;
- const values = [];
- if (createdAfter) {
- values.push(
- formatDate(
- createdAfter,
- createdAfterIncludesTime ? dateTimeFormatterOption : longFormatterOption
- )
- );
- }
- if (createdAt) {
- values.push(formatDate(createdAt, longFormatterOption));
- }
- if (createdBefore) {
- values.push(formatDate(createdBefore, longFormatterOption));
- }
- if (createdInLast === '1w') {
- values.push(translate('issues.facet.createdAt.last_week'));
- }
- if (createdInLast === '1m') {
- values.push(translate('issues.facet.createdAt.last_month'));
- }
- if (createdInLast === '1y') {
- values.push(translate('issues.facet.createdAt.last_year'));
+ getCount() {
+ const { createdAfter, createdAt, createdBefore, createdInLast } = this.props;
+
+ let count = 0;
+
+ if (createdInLast || createdAt) {
+ count = 1;
+ } else {
+ if (createdAfter) {
+ count++;
+ }
+
+ if (createdBefore) {
+ count++;
+ }
}
- return values;
+
+ return count;
}
renderBarChart() {
const tooltip = (
// eslint-disable-next-line react/jsx-fragments
<React.Fragment>
- {formatMeasure(stats[start], 'SHORT_INT')}
+ {formatMeasure(stats[start], MetricType.ShortInteger)}
<br />
{formatDate(startDate, longFormatterOption)}
{!isSameDay(tooltipEndDate, startDate) &&
);
const description = translateWithParameters(
'issues.facet.createdAt.bar_description',
- formatMeasure(stats[start], 'SHORT_INT'),
+ formatMeasure(stats[start], MetricType.ShortInteger),
formatDate(startDate, longFormatterOption),
formatDate(tooltipEndDate, longFormatterOption)
);
};
});
- const barsWidth = Math.floor(250 / data.length);
+ const barsWidth = Math.floor(270 / data.length);
const width = barsWidth * data.length - 1 + 10;
const maxValue = max(data.map((d) => d.y));
- const xValues = data.map((d) => (d.y === maxValue ? formatMeasure(maxValue, 'SHORT_INT') : ''));
+ const xValues = data.map((d) =>
+ d.y === maxValue ? formatMeasure(maxValue, MetricType.ShortInteger) : ''
+ );
return (
<BarChart
barsWidth={barsWidth - 1}
data={data}
height={75}
- onBarClick={this.handleBarClick}
+ onBarClick={this.resetTo}
padding={[25, 0, 5, 10]}
width={width}
xValues={xValues}
renderPeriodSelectors() {
const { createdAfter, createdBefore } = this.props;
+
return (
- <div className="search-navigator-date-facet-selection">
- <DateRangeInput
- alignEndDateCalandarRight
- onChange={this.handlePeriodChange}
- value={{ from: createdAfter, to: createdBefore }}
- />
- </div>
+ <DateRangePicker
+ ariaNextMonthLabel={translate('next_')}
+ ariaPreviousMonthLabel={translate('previous_')}
+ clearButtonLabel={translate('clear')}
+ fromLabel={translate('start_date')}
+ onChange={this.handlePeriodChange}
+ separatorText={translate('to_')}
+ toLabel={translate('end_date')}
+ value={{ from: createdAfter, to: createdBefore }}
+ />
);
}
renderPredefinedPeriods() {
const { createdInLast } = this.props;
+
return (
- <div className="spacer-top issues-predefined-periods">
- <FacetItem
- active={!this.hasValue()}
- name={translate('issues.facet.createdAt.all')}
- onClick={this.handlePeriodClick}
- tooltip={translate('issues.facet.createdAt.all')}
- value=""
- />
-
- <FacetItem
- active={createdInLast === '1w'}
- name={translate('issues.facet.createdAt.last_week')}
- onClick={this.handlePeriodClick}
- tooltip={translate('issues.facet.createdAt.last_week')}
- value="1w"
- />
- <FacetItem
- active={createdInLast === '1m'}
- name={translate('issues.facet.createdAt.last_month')}
- onClick={this.handlePeriodClick}
- tooltip={translate('issues.facet.createdAt.last_month')}
- value="1m"
- />
- <FacetItem
- active={createdInLast === '1y'}
- name={translate('issues.facet.createdAt.last_year')}
- onClick={this.handlePeriodClick}
- tooltip={translate('issues.facet.createdAt.last_year')}
- value="1y"
- />
+ <div className="sw-flex sw-justify-start">
+ <div className="sw-flex sw-gap-1 sw-mt-2">
+ <FacetItem
+ active={!this.hasValue()}
+ className="it__search-navigator-facet"
+ name={translate('issues.facet.createdAt.all')}
+ onClick={this.handlePeriodClick}
+ small
+ tooltip={translate('issues.facet.createdAt.all')}
+ value=""
+ />
+
+ <FacetItem
+ active={createdInLast === '1w'}
+ className="it__search-navigator-facet"
+ name={translate('issues.facet.createdAt.last_week')}
+ onClick={this.handlePeriodClick}
+ small
+ tooltip={translate('issues.facet.createdAt.last_week')}
+ value="1w"
+ />
+
+ <FacetItem
+ active={createdInLast === '1m'}
+ className="it__search-navigator-facet"
+ name={translate('issues.facet.createdAt.last_month')}
+ onClick={this.handlePeriodClick}
+ small
+ tooltip={translate('issues.facet.createdAt.last_month')}
+ value="1m"
+ />
+
+ <FacetItem
+ active={createdInLast === '1y'}
+ className="it__search-navigator-facet"
+ name={translate('issues.facet.createdAt.last_year')}
+ onClick={this.handlePeriodClick}
+ small
+ tooltip={translate('issues.facet.createdAt.last_year')}
+ value="1y"
+ />
+ </div>
</div>
);
}
return (
<div>
- {this.renderBarChart()}
+ <div className="sw-flex sw-justify-center">{this.renderBarChart()}</div>
+
{this.renderPeriodSelectors()}
+
{this.renderPredefinedPeriods()}
</div>
);
render() {
const { fetching, open } = this.props;
+
+ const count = this.getCount();
const headerId = `facet_${this.property}`;
return (
- <FacetBox property={this.property}>
- <FacetHeader
- fetching={fetching}
- id={headerId}
- name={translate('issues.facet', this.property)}
- onClear={this.handleClear}
- onClick={this.handleHeaderClick}
- open={open}
- values={this.getValues()}
- />
-
- {open && this.renderInner()}
+ <FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ clearIconLabel={translate('clear')}
+ count={count}
+ countLabel={translateWithParameters('x_selected', count)}
+ data-property={this.property}
+ id={headerId}
+ loading={fetching}
+ name={translate('issues.facet', this.property)}
+ onClear={() => {
+ this.resetTo({});
+ }}
+ onClick={() => {
+ this.props.onToggle(this.property);
+ }}
+ open={open}
+ >
+ {this.renderInner()}
</FacetBox>
);
}
}
-export default injectIntl(CreationDateFacet);
+export const CreationDateFacet = injectIntl(CreationDateFacetClass);
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+import { DirectoryIcon } from 'design-system';
import { omit } from 'lodash';
import * as React from 'react';
import { getDirectories } from '../../../api/components';
-import ListStyleFacet from '../../../components/facet/ListStyleFacet';
-import QualifierIcon from '../../../components/icons/QualifierIcon';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { translate } from '../../../helpers/l10n';
import { collapsePath } from '../../../helpers/path';
import { BranchLike } from '../../../types/branch-like';
import { TreeComponentWithPath } from '../../../types/component';
import { Facet } from '../../../types/issues';
+import { MetricKey } from '../../../types/metrics';
import { Query } from '../utils';
+import { ListStyleFacet } from './ListStyleFacet';
interface Props {
branchLike?: BranchLike;
stats: Facet | undefined;
}
-export default class DirectoryFacet extends React.PureComponent<Props> {
+export class DirectoryFacet extends React.PureComponent<Props> {
getFacetItemText = (path: string) => {
return path;
};
};
loadSearchResultCount = (directories: TreeComponentWithPath[]) => {
- return this.props.loadSearchResultCount('directories', {
+ return this.props.loadSearchResultCount(MetricKey.directories, {
directories: directories.map((directory) => directory.path),
});
};
renderDirectory = (directory: React.ReactNode) => (
<>
- <QualifierIcon className="little-spacer-right" qualifier="DIR" />
+ <DirectoryIcon className="sw-mr-1" />
+
{directory}
</>
);
onSearch={this.handleSearch}
onToggle={this.props.onToggle}
open={this.props.open}
- property="directories"
- query={omit(this.props.query, 'directories')}
+ property={MetricKey.directories}
+ query={omit(this.props.query, MetricKey.directories)}
renderFacetItem={this.renderFacetItem}
renderSearchResult={this.renderSearchResult}
searchPlaceholder={translate('search.search_for_directories')}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import * as React from 'react';
+
+export function FacetItemsColumns({ children }: React.PropsWithChildren<{}>) {
+ return (
+ <div className="it__search-navigator-facet-list sw-flex sw-flex-wrap sw-gap-1" role="list">
+ <div className="sw-gap-1 sw-grid sw-grid-cols-2 sw-w-full">{children}</div>
+ </div>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import * as React from 'react';
+
+export type FacetItemsListProps =
+ | {
+ children?: React.ReactNode;
+ labelledby: string;
+ label?: never;
+ }
+ | {
+ children?: React.ReactNode;
+ labelledby?: never;
+ label: string;
+ };
+
+export function FacetItemsList({ children, labelledby, label }: FacetItemsListProps) {
+ const props = labelledby ? { 'aria-labelledby': labelledby } : { 'aria-label': label };
+
+ return (
+ <div
+ className="it__search-navigator-facet-list sw-flex sw-flex-col sw-gap-1"
+ role="list"
+ {...props}
+ >
+ {children}
+ </div>
+ );
+}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+import { FileIcon } from 'design-system';
import { omit } from 'lodash';
import * as React from 'react';
import { getFiles } from '../../../api/components';
-import ListStyleFacet from '../../../components/facet/ListStyleFacet';
-import QualifierIcon from '../../../components/icons/QualifierIcon';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { translate } from '../../../helpers/l10n';
import { collapsePath, splitPath } from '../../../helpers/path';
import { BranchLike } from '../../../types/branch-like';
import { TreeComponentWithPath } from '../../../types/component';
import { Facet } from '../../../types/issues';
+import { MetricKey } from '../../../types/metrics';
import { Query } from '../utils';
+import { ListStyleFacet } from './ListStyleFacet';
interface Props {
branchLike?: BranchLike;
}
const MAX_PATH_LENGTH = 15;
-export default class FileFacet extends React.PureComponent<Props> {
+
+export class FileFacet extends React.PureComponent<Props> {
getFacetItemText = (path: string) => {
return path;
};
};
loadSearchResultCount = (files: TreeComponentWithPath[]) => {
- return this.props.loadSearchResultCount('files', {
+ return this.props.loadSearchResultCount(MetricKey.files, {
files: files
.map((file) => {
return file.path;
renderFile = (file: React.ReactNode) => (
<>
- <QualifierIcon className="little-spacer-right" qualifier="FIL" />
+ <FileIcon className="sw-mr-1" />
+
{file}
</>
);
onSearch={this.handleSearch}
onToggle={this.props.onToggle}
open={this.props.open}
- property="files"
- query={omit(this.props.query, 'files')}
+ property={MetricKey.files}
+ query={omit(this.props.query, MetricKey.files)}
renderFacetItem={this.renderFacetItem}
renderSearchResult={this.renderSearchResult}
searchPlaceholder={translate('search.search_for_files')}
--- /dev/null
+/*
+ * 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 { BasicSeparator, DangerButtonSecondary, PageTitle } from 'design-system';
+import * as React from 'react';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ displayReset: boolean;
+ onReset: () => void;
+}
+
+export function FiltersHeader({ displayReset, onReset }: Props) {
+ return (
+ <div className="sw-mb-5">
+ <div className="sw-flex sw-h-9 sw-items-center sw-justify-between">
+ <PageTitle className="sw-body-md-highlight" text={translate('filters')} />
+
+ {displayReset && (
+ <DangerButtonSecondary onClick={onReset}>
+ {translate('clear_all_filters')}
+ </DangerButtonSecondary>
+ )}
+ </div>
+
+ <BasicSeparator className="sw-mt-4" />
+ </div>
+ );
+}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { omit, uniqBy } from 'lodash';
import * as React from 'react';
import withLanguagesContext from '../../../app/components/languages/withLanguagesContext';
-import ListStyleFacet from '../../../components/facet/ListStyleFacet';
import { translate } from '../../../helpers/l10n';
import { highlightTerm } from '../../../helpers/search';
import { Facet, ReferencedLanguage } from '../../../types/issues';
import { Language, Languages } from '../../../types/languages';
import { Dict } from '../../../types/types';
import { Query } from '../utils';
+import { ListStyleFacet } from './ListStyleFacet';
interface Props {
fetching: boolean;
stats: Dict<number> | undefined;
}
-class LanguageFacet extends React.PureComponent<Props> {
+class LanguageFacetClass extends React.PureComponent<Props> {
getLanguageName = (language: string) => {
const { referencedLanguages } = this.props;
return referencedLanguages[language] ? referencedLanguages[language].name : language;
handleSearch = (query: string) => {
const options = this.getAllPossibleOptions();
+
const results = options.filter((language) =>
language.name.toLowerCase().includes(query.toLowerCase())
);
+
const paging = { pageIndex: 1, pageSize: results.length, total: results.length };
+
return Promise.resolve({ paging, results });
};
}
}
-export default withLanguagesContext(LanguageFacet);
+export const LanguageFacet = withLanguagesContext(LanguageFacetClass);
--- /dev/null
+/*
+ * 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 { FacetBox, FacetItem, FlagMessage, InputSearch } from 'design-system';
+import { sortBy, without } from 'lodash';
+import * as React from 'react';
+import ListFooter from '../../../components/controls/ListFooter';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+import { queriesEqual } from '../../../helpers/query';
+import { MetricType } from '../../../types/metrics';
+import { Dict, Paging, RawQuery } from '../../../types/types';
+import { FacetItemsList } from './FacetItemsList';
+import { ListStyleFacetFooter } from './ListStyleFacetFooter';
+import { MultipleSelectionHint } from './MultipleSelectionHint';
+
+interface SearchResponse<S> {
+ maxResults?: boolean;
+ results: S[];
+ paging?: Paging;
+}
+
+export interface Props<S> {
+ disabled?: boolean;
+ facetHeader: string;
+ fetching: boolean;
+ getFacetItemText: (item: string) => string;
+ getSearchResultKey: (result: S) => string;
+ getSearchResultText: (result: S) => string;
+ getSortedItems?: () => string[];
+ inner?: boolean;
+ loadSearchResultCount?: (result: S[]) => Promise<Dict<number>>;
+ maxInitialItems: number;
+ maxItems: number;
+ minSearchLength: number;
+ onChange: (changes: Dict<string | string[]>) => void;
+ onClear?: () => void;
+ onItemClick?: (itemValue: string, multiple: boolean) => void;
+ onSearch: (query: string, page?: number) => Promise<SearchResponse<S>>;
+ onToggle: (property: string) => void;
+ open: boolean;
+ property: string;
+ query?: RawQuery;
+ renderFacetItem: (item: string) => string | JSX.Element;
+ renderSearchResult: (result: S, query: string) => React.ReactNode;
+ searchPlaceholder: string;
+ showLessAriaLabel?: string;
+ showMoreAriaLabel?: string;
+ stats: Dict<number> | undefined;
+ values: string[];
+}
+
+interface State<S> {
+ autoFocus: boolean;
+ query: string;
+ searching: boolean;
+ searchMaxResults?: boolean;
+ searchPaging?: Paging;
+ searchResults?: S[];
+ searchResultsCounts: Dict<number>;
+ showFullList: boolean;
+}
+
+export class ListStyleFacet<S> extends React.Component<Props<S>, State<S>> {
+ mounted = false;
+
+ static defaultProps = {
+ getFacetItemText: (item: string) => item,
+ getSearchResultKey: (result: unknown) => result,
+ getSearchResultText: (result: unknown) => result,
+ maxInitialItems: 15,
+ maxItems: 100,
+ minSearchLength: 2,
+ renderFacetItem: (item: string) => item,
+ renderSearchResult: (result: unknown, _query: string) => result,
+ };
+
+ state: State<S> = {
+ autoFocus: false,
+ query: '',
+ searching: false,
+ searchResultsCounts: {},
+ showFullList: false,
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentDidUpdate(prevProps: Props<S>) {
+ if (!prevProps.open && this.props.open) {
+ // focus search field *only* if it was manually open
+ this.setState({ autoFocus: true });
+ } else if (
+ (prevProps.open && !this.props.open) ||
+ !queriesEqual(prevProps.query || {}, this.props.query || {})
+ ) {
+ // reset state when closing the facet, or when query changes
+ this.setState({
+ query: '',
+ searchMaxResults: undefined,
+ searchResults: undefined,
+ searching: false,
+ searchResultsCounts: {},
+ showFullList: false,
+ });
+ } else if (
+ prevProps.stats !== this.props.stats &&
+ Object.keys(this.props.stats || {}).length < this.props.maxInitialItems
+ ) {
+ // show limited list if `stats` changed and there are less than 15 items
+ this.setState({ showFullList: false });
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleItemClick = (itemValue: string, multiple: boolean) => {
+ if (this.props.onItemClick) {
+ this.props.onItemClick(itemValue, multiple);
+ } else {
+ const { values } = this.props;
+
+ if (multiple) {
+ const newValue = sortBy(
+ values.includes(itemValue) ? without(values, itemValue) : [...values, itemValue]
+ );
+
+ this.props.onChange({ [this.props.property]: newValue });
+ } else {
+ this.props.onChange({
+ [this.props.property]: values.includes(itemValue) && values.length < 2 ? [] : [itemValue],
+ });
+ }
+ }
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.props.property);
+ };
+
+ handleClear = () => {
+ if (this.props.onClear) {
+ this.props.onClear();
+ } else {
+ this.props.onChange({ [this.props.property]: [] });
+ }
+ };
+
+ stopSearching = () => {
+ if (this.mounted) {
+ this.setState({ searching: false });
+ }
+ };
+
+ search = (query: string) => {
+ if (query.length >= this.props.minSearchLength) {
+ this.setState({ query, searching: true });
+
+ this.props
+ .onSearch(query)
+ .then(this.loadCountsForSearchResults)
+ .then(({ maxResults, paging, results, stats }) => {
+ if (this.mounted) {
+ this.setState((state) => ({
+ searching: false,
+ searchMaxResults: maxResults,
+ searchResults: results,
+ searchPaging: paging,
+ searchResultsCounts: { ...state.searchResultsCounts, ...stats },
+ }));
+ }
+ })
+ .catch(this.stopSearching);
+ } else {
+ this.setState({ query, searching: false, searchResults: [] });
+ }
+ };
+
+ searchMore = () => {
+ const { query, searchPaging, searchResults } = this.state;
+
+ if (query && searchResults && searchPaging) {
+ this.setState({ searching: true });
+
+ this.props
+ .onSearch(query, searchPaging.pageIndex + 1)
+ .then(this.loadCountsForSearchResults)
+ .then(({ paging, results, stats }) => {
+ if (this.mounted) {
+ this.setState((state) => ({
+ searching: false,
+ searchResults: [...searchResults, ...results],
+ searchPaging: paging,
+ searchResultsCounts: { ...state.searchResultsCounts, ...stats },
+ }));
+ }
+ })
+ .catch(this.stopSearching);
+ }
+ };
+
+ loadCountsForSearchResults = (response: SearchResponse<S>) => {
+ const { loadSearchResultCount = () => Promise.resolve({}) } = this.props;
+
+ const resultsToLoad = response.results.filter((result) => {
+ const key = this.props.getSearchResultKey(result);
+
+ return this.getStat(key) === undefined && this.state.searchResultsCounts[key] === undefined;
+ });
+
+ if (resultsToLoad.length > 0) {
+ return loadSearchResultCount(resultsToLoad).then((stats) => ({ ...response, stats }));
+ }
+
+ return { ...response, stats: {} };
+ };
+
+ getStat(item: string) {
+ const { stats } = this.props;
+
+ return stats?.[item];
+ }
+
+ getFacetHeaderId = (property: string) => {
+ return `facet_${property}`;
+ };
+
+ showFullList = () => {
+ this.setState({ showFullList: true });
+ };
+
+ hideFullList = () => {
+ this.setState({ showFullList: false });
+ };
+
+ renderList() {
+ const {
+ maxInitialItems,
+ maxItems,
+ property,
+ stats,
+ showMoreAriaLabel,
+ showLessAriaLabel,
+ values,
+ } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const sortedItems = this.props.getSortedItems
+ ? this.props.getSortedItems()
+ : sortBy(
+ Object.keys(stats),
+ (key) => -stats[key],
+ (key) => this.props.getFacetItemText(key)
+ );
+
+ const limitedList = this.state.showFullList
+ ? sortedItems
+ : sortedItems.slice(0, maxInitialItems);
+
+ // make sure all selected items are displayed
+ const selectedBelowLimit = this.state.showFullList
+ ? []
+ : sortedItems.slice(maxInitialItems).filter((item) => values.includes(item));
+
+ const mightHaveMoreResults = sortedItems.length >= maxItems;
+
+ return (
+ <>
+ <FacetItemsList labelledby={this.getFacetHeaderId(property)}>
+ {limitedList.map((item) => (
+ <FacetItem
+ active={this.props.values.includes(item)}
+ className="it__search-navigator-facet"
+ key={item}
+ name={this.props.renderFacetItem(item)}
+ onClick={this.handleItemClick}
+ stat={formatFacetStat(this.getStat(item)) ?? 0}
+ tooltip={this.props.getFacetItemText(item)}
+ value={item}
+ />
+ ))}
+ </FacetItemsList>
+
+ {selectedBelowLimit.length > 0 && (
+ <>
+ <div className="note spacer-bottom text-center">⋯</div>
+
+ <FacetItemsList labelledby={this.getFacetHeaderId(property)}>
+ {selectedBelowLimit.map((item) => (
+ <FacetItem
+ active={true}
+ className="it__search-navigator-facet"
+ key={item}
+ name={this.props.renderFacetItem(item)}
+ onClick={this.handleItemClick}
+ stat={formatFacetStat(this.getStat(item)) ?? 0}
+ tooltip={this.props.getFacetItemText(item)}
+ value={item}
+ />
+ ))}
+ </FacetItemsList>
+ </>
+ )}
+
+ <ListStyleFacetFooter
+ nbShown={limitedList.length + selectedBelowLimit.length}
+ showLess={this.state.showFullList ? this.hideFullList : undefined}
+ showLessAriaLabel={showLessAriaLabel}
+ showMore={this.showFullList}
+ showMoreAriaLabel={showMoreAriaLabel}
+ total={sortedItems.length}
+ />
+
+ {mightHaveMoreResults && this.state.showFullList && (
+ <FlagMessage
+ ariaLabel={translate('facet_might_have_more_results')}
+ className="sw-flex sw-my-4"
+ variant="warning"
+ >
+ {translate('facet_might_have_more_results')}
+ </FlagMessage>
+ )}
+ </>
+ );
+ }
+
+ renderSearch() {
+ return (
+ <InputSearch
+ className="it__search-box-input sw-mb-4 sw-w-full"
+ autoFocus={this.state.autoFocus}
+ onChange={this.search}
+ placeholder={this.props.searchPlaceholder}
+ size="auto"
+ value={this.state.query}
+ searchInputAriaLabel={translate('search_verb')}
+ clearIconAriaLabel={translate('clear')}
+ />
+ );
+ }
+
+ renderSearchResults() {
+ const { property, showMoreAriaLabel } = this.props;
+ const { searching, searchMaxResults, searchResults, searchPaging } = this.state;
+
+ if (!searching && !searchResults?.length) {
+ return <div className="note spacer-bottom">{translate('no_results')}</div>;
+ }
+
+ if (!searchResults) {
+ // initial search
+ return null;
+ }
+
+ return (
+ <>
+ <FacetItemsList labelledby={this.getFacetHeaderId(property)}>
+ {searchResults.map((result) => this.renderSearchResult(result))}
+ </FacetItemsList>
+
+ {searchMaxResults && (
+ <FlagMessage
+ ariaLabel={translate('facet_might_have_more_results')}
+ className="sw-flex sw-my-4"
+ variant="warning"
+ >
+ {translate('facet_might_have_more_results')}
+ </FlagMessage>
+ )}
+
+ {searchPaging && (
+ <ListFooter
+ className="sw-mb-2"
+ count={searchResults.length}
+ loadMore={this.searchMore}
+ loadMoreAriaLabel={showMoreAriaLabel}
+ ready={!searching}
+ total={searchPaging.total}
+ useMIUIButtons={true}
+ />
+ )}
+ </>
+ );
+ }
+
+ renderSearchResult(result: S) {
+ const key = this.props.getSearchResultKey(result);
+ const active = this.props.values.includes(key);
+ const stat = formatFacetStat(this.getStat(key) ?? this.state.searchResultsCounts[key]) ?? 0;
+
+ return (
+ <FacetItem
+ active={active}
+ className="it__search-navigator-facet"
+ key={key}
+ name={this.props.renderSearchResult(result, this.state.query)}
+ onClick={this.handleItemClick}
+ stat={stat}
+ tooltip={this.props.getSearchResultText(result)}
+ value={key}
+ />
+ );
+ }
+
+ render() {
+ const {
+ disabled,
+ facetHeader,
+ fetching,
+ inner,
+ open,
+ property,
+ stats = {},
+ values: propsValues,
+ } = this.props;
+
+ const { query, searching, searchResults } = this.state;
+
+ const values = propsValues.map((item) => this.props.getFacetItemText(item));
+
+ const loadingResults =
+ query !== '' && searching && (searchResults === undefined || searchResults.length === 0);
+
+ const showList = !query || loadingResults;
+
+ const nbSelectableItems = Object.keys(stats).length;
+ const nbSelectedItems = values.length;
+
+ return (
+ <FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ clearIconLabel={translate('clear')}
+ count={nbSelectedItems}
+ countLabel={translateWithParameters('x_selected', nbSelectedItems)}
+ disabled={disabled}
+ id={this.getFacetHeaderId(property)}
+ inner={inner}
+ loading={fetching}
+ name={facetHeader}
+ onClear={this.handleClear}
+ onClick={disabled ? undefined : this.handleHeaderClick}
+ open={open && !disabled}
+ >
+ {!disabled && (
+ <span className="it__search-navigator-facet-list">
+ {this.renderSearch()}
+
+ {showList ? this.renderList() : this.renderSearchResults()}
+
+ <MultipleSelectionHint
+ nbSelectableItems={nbSelectableItems}
+ nbSelectedItems={nbSelectedItems}
+ />
+ </span>
+ )}
+ </FacetBox>
+ );
+ }
+}
+
+function formatFacetStat(stat: number | undefined) {
+ return stat && formatMeasure(stat, MetricType.ShortInteger);
+}
--- /dev/null
+/*
+ * 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 { useTheme } from '@emotion/react';
+import { BaseLink, Theme, themeColor } from 'design-system';
+import * as React from 'react';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+import { MetricType } from '../../../types/metrics';
+
+export interface Props {
+ nbShown: number;
+ showLess?: () => void;
+ showLessAriaLabel?: string;
+ showMore: () => void;
+ showMoreAriaLabel?: string;
+ total: number;
+}
+
+export function ListStyleFacetFooter({
+ nbShown,
+ showLess,
+ showLessAriaLabel,
+ showMore,
+ showMoreAriaLabel,
+ total,
+}: Props) {
+ const theme = useTheme() as Theme;
+
+ const hasMore = total > nbShown;
+ const allShown = Boolean(total && total === nbShown);
+
+ return (
+ <div
+ className="sw-body-xs sw-mb-2 sw-mt-2 sw-text-center"
+ style={{ color: themeColor('graphCursorLineColor')({ theme }) }}
+ >
+ {translateWithParameters('x_show', formatMeasure(nbShown, MetricType.Integer))}
+
+ {hasMore && (
+ <BaseLink
+ aria-label={showMoreAriaLabel}
+ className="sw-ml-2"
+ onClick={(e) => {
+ e.preventDefault();
+ showMore();
+ }}
+ to="#"
+ >
+ {translate('show_more')}
+ </BaseLink>
+ )}
+
+ {showLess && allShown && (
+ <BaseLink
+ aria-label={showLessAriaLabel}
+ className="sw-ml-2"
+ onClick={(e) => {
+ e.preventDefault();
+ showLess();
+ }}
+ to="#"
+ >
+ {translate('show_less')}
+ </BaseLink>
+ )}
+ </div>
+ );
+}
--- /dev/null
+/*
+ * 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 { KeyboardHint } from 'design-system';
+import * as React from 'react';
+import { translate } from '../../../helpers/l10n';
+
+export function MultipleSelectionHint({
+ nbSelectableItems,
+ nbSelectedItems,
+}: {
+ nbSelectableItems: number;
+ nbSelectedItems: number;
+}) {
+ return nbSelectedItems > 0 && nbSelectedItems < nbSelectableItems ? (
+ <div className="sw-pt-4">
+ <KeyboardHint command={translate('shortcuts.section.global.facets.multiselection')} />
+ </div>
+ ) : null;
+}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+import { BasicSeparator, FacetItem } from 'design-system';
import * as React from 'react';
-import FacetBox from '../../../components/facet/FacetBox';
-import FacetItem from '../../../components/facet/FacetItem';
-import FacetItemsList from '../../../components/facet/FacetItemsList';
import { translate } from '../../../helpers/l10n';
-import { Dict } from '../../../types/types';
-import { formatFacetStat, Query } from '../utils';
+import { MeasuresPanelTabs } from '../../overview/branches/MeasuresPanel';
+import { Query } from '../utils';
+import { FacetItemsList } from './FacetItemsList';
export interface PeriodFilterProps {
- fetching: boolean;
onChange: (changes: Partial<Query>) => void;
- stats: Dict<number> | undefined;
newCodeSelected: boolean;
}
const PROPERTY = 'period';
-export default function PeriodFilter(props: PeriodFilterProps) {
- const { fetching, newCodeSelected, stats = {} } = props;
+export function PeriodFilter(props: PeriodFilterProps) {
+ const { newCodeSelected, onChange } = props;
- const { onChange } = props;
const handleClick = React.useCallback(() => {
// We need to clear creation date filters they conflict with the new code period
onChange({
}, [newCodeSelected, onChange]);
return (
- <FacetBox property={PROPERTY}>
- <FacetItemsList label={translate('issues.facet', PROPERTY)}>
- <FacetItem
- active={newCodeSelected}
- loading={fetching}
- name={translate('issues.new_code')}
- onClick={handleClick}
- stat={formatFacetStat(stats[Period.NewCode])}
- value={Period.NewCode}
- />
- </FacetItemsList>
- </FacetBox>
+ <FacetItemsList label={translate('issues.facet', PROPERTY)}>
+ <FacetItem
+ active={newCodeSelected}
+ className="it__search-navigator-facet"
+ name={translate('issues.new_code')}
+ onClick={handleClick}
+ value={newCodeSelected ? MeasuresPanelTabs.New : MeasuresPanelTabs.Overall}
+ />
+
+ <BasicSeparator className="sw-mb-5 sw-mt-4" />
+ </FacetItemsList>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+import { ProjectIcon } from 'design-system';
import { omit } from 'lodash';
import * as React from 'react';
import { getTree, searchProjects } from '../../../api/components';
-import ListStyleFacet from '../../../components/facet/ListStyleFacet';
-import QualifierIcon from '../../../components/icons/QualifierIcon';
import { translate } from '../../../helpers/l10n';
import { highlightTerm } from '../../../helpers/search';
import { ComponentQualifier } from '../../../types/component';
import { Facet, ReferencedComponent } from '../../../types/issues';
+import { MetricKey } from '../../../types/metrics';
import { Component, Dict, Paging } from '../../../types/types';
import { Query } from '../utils';
+import { ListStyleFacet } from './ListStyleFacet';
interface Props {
component: Component | undefined;
name: string;
}
-export default class ProjectFacet extends React.PureComponent<Props> {
+export class ProjectFacet extends React.PureComponent<Props> {
handleSearch = (
query: string,
page = 1
): Promise<{ results: SearchedProject[]; paging: Paging }> => {
const { component } = this.props;
+
if (
component &&
[
getProjectName = (project: string) => {
const { referencedComponents } = this.props;
+
return referencedComponents[project] ? referencedComponents[project].name : project;
};
loadSearchResultCount = (projects: SearchedProject[]) => {
- return this.props.loadSearchResultCount('projects', {
+ return this.props.loadSearchResultCount(MetricKey.projects, {
projects: projects.map((project) => project.key),
});
};
renderFacetItem = (projectKey: string) => {
return (
<span>
- <QualifierIcon className="little-spacer-right" qualifier={ComponentQualifier.Project} />
+ <ProjectIcon className="sw-mr-1" />
+
{this.getProjectName(projectKey)}
</span>
);
renderSearchResult = (project: Pick<SearchedProject, 'name'>, term: string) => (
<>
- <QualifierIcon className="little-spacer-right" qualifier={ComponentQualifier.Project} />
+ <ProjectIcon className="sw-mr-1" />
+
{highlightTerm(project.name, term)}
</>
);
onSearch={this.handleSearch}
onToggle={this.props.onToggle}
open={this.props.open}
- property="projects"
- query={omit(this.props.query, 'projects')}
+ property={MetricKey.projects}
+ query={omit(this.props.query, MetricKey.projects)}
renderFacetItem={this.renderFacetItem}
renderSearchResult={this.renderSearchResult}
searchPlaceholder={translate('search.search_for_projects')}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+import { FacetBox, FacetItem } from 'design-system';
import { orderBy, without } from 'lodash';
import * as React from 'react';
-import FacetBox from '../../../components/facet/FacetBox';
-import FacetHeader from '../../../components/facet/FacetHeader';
-import FacetItem from '../../../components/facet/FacetItem';
-import FacetItemsList from '../../../components/facet/FacetItemsList';
-import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';
-import { translate } from '../../../helpers/l10n';
-import { IssueResolution } from '../../../types/issues';
+import { RESOLUTIONS } from '../../../helpers/constants';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Dict } from '../../../types/types';
import { Query, formatFacetStat } from '../utils';
+import { FacetItemsColumns } from './FacetItemsColumns';
+import { MultipleSelectionHint } from './MultipleSelectionHint';
interface Props {
fetching: boolean;
stats: Dict<number> | undefined;
}
-const RESOLUTIONS = [
- IssueResolution.Unresolved,
- IssueResolution.FalsePositive,
- IssueResolution.Fixed,
- IssueResolution.Removed,
- IssueResolution.WontFix,
-];
-
-export default class ResolutionFacet extends React.PureComponent<Props> {
+export class ResolutionFacet extends React.PureComponent<Props> {
property = 'resolutions';
static defaultProps = {
handleItemClick = (itemValue: string, multiple: boolean) => {
const { resolutions } = this.props;
+
if (itemValue === '') {
// unresolved
this.props.onChange({ resolved: !this.props.resolved, resolutions: [] });
? without(resolutions, itemValue)
: [...resolutions, itemValue]
);
+
this.props.onChange({ resolved: true, [this.property]: newValue });
} else {
this.props.onChange({
getStat(resolution: string) {
const { stats } = this.props;
+
return stats ? stats[resolution] : undefined;
}
return (
<FacetItem
active={active}
- halfWidth
+ className="it__search-navigator-facet"
key={resolution}
name={this.getFacetItemName(resolution)}
onClick={this.handleItemClick}
- stat={formatFacetStat(stat)}
+ stat={formatFacetStat(stat) ?? 0}
tooltip={this.getFacetItemName(resolution)}
value={resolution}
/>
};
render() {
- const { fetching, open, resolutions, stats = {} } = this.props;
- const values = resolutions.map((resolution) => this.getFacetItemName(resolution));
+ const { fetching, open, resolutions } = this.props;
+
+ // below: -1 because "Unresolved" is mutually exclusive with the rest
+ const nbSelectableItems = RESOLUTIONS.filter(this.getStat.bind(this)).length - 1;
+
+ const nbSelectedItems = resolutions.length;
const headerId = `facet_${this.property}`;
return (
- <FacetBox property={this.property}>
- <FacetHeader
- fetching={fetching}
- id={headerId}
- name={translate('issues.facet', this.property)}
- onClear={this.handleClear}
- onClick={this.handleHeaderClick}
- open={open}
- values={values}
- />
+ <FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ clearIconLabel={translate('clear')}
+ count={nbSelectedItems}
+ countLabel={translateWithParameters('x_selected', nbSelectedItems)}
+ data-property={this.property}
+ id={headerId}
+ loading={fetching}
+ name={translate('issues.facet', this.property)}
+ onClear={this.handleClear}
+ onClick={this.handleHeaderClick}
+ open={open}
+ >
+ <FacetItemsColumns>{RESOLUTIONS.map(this.renderItem)}</FacetItemsColumns>
- {open && (
- <>
- <FacetItemsList labelledby={headerId}>
- {RESOLUTIONS.map(this.renderItem)}
- </FacetItemsList>
- <MultipleSelectionHint
- options={Object.keys(stats).length}
- values={resolutions.length}
- />
- </>
- )}
+ <MultipleSelectionHint
+ nbSelectableItems={nbSelectableItems}
+ nbSelectedItems={nbSelectedItems}
+ />
</FacetBox>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { omit } from 'lodash';
import * as React from 'react';
import { searchRules } from '../../../api/rules';
-import ListStyleFacet from '../../../components/facet/ListStyleFacet';
import { ISSUE_TYPES } from '../../../helpers/constants';
import { translate } from '../../../helpers/l10n';
import { Facet, IssueType, ReferencedRule } from '../../../types/issues';
import { Dict, Rule } from '../../../types/types';
import { Query } from '../utils';
+import { ListStyleFacet } from './ListStyleFacet';
interface Props {
fetching: boolean;
stats: Dict<number> | undefined;
}
-export default class RuleFacet extends React.PureComponent<Props> {
+export class RuleFacet extends React.PureComponent<Props> {
handleSearch = (query: string, page = 1) => {
const { languages, types } = this.props.query;
+
return searchRules({
f: 'name,langName',
languages: languages.length ? languages.join() : undefined,
getRuleName = (ruleKey: string) => {
const rule = this.props.referencedRules[ruleKey];
+
return rule ? this.formatRuleName(rule.name, rule.langName) : ruleKey;
};
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+import { FacetBox, FacetItem, FileIcon, TestFileIcon } from 'design-system';
import { without } from 'lodash';
import * as React from 'react';
-import FacetBox from '../../../components/facet/FacetBox';
-import FacetHeader from '../../../components/facet/FacetHeader';
-import FacetItem from '../../../components/facet/FacetItem';
-import FacetItemsList from '../../../components/facet/FacetItemsList';
-import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';
-import QualifierIcon from '../../../components/icons/QualifierIcon';
import { SOURCE_SCOPES } from '../../../helpers/constants';
-import { translate } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Dict } from '../../../types/types';
-import { formatFacetStat, Query } from '../utils';
+import { Query, formatFacetStat } from '../utils';
+import { FacetItemsList } from './FacetItemsList';
+import { MultipleSelectionHint } from './MultipleSelectionHint';
export interface ScopeFacetProps {
fetching: boolean;
stats: Dict<number> | undefined;
}
-export default function ScopeFacet(props: ScopeFacetProps) {
+export function ScopeFacet(props: ScopeFacetProps) {
const { fetching, open, scopes = [], stats = {} } = props;
- const values = scopes.map((scope) => translate('issue.scope', scope));
+ const nbSelectableItems = SOURCE_SCOPES.filter(({ scope }) => stats[scope]).length;
+ const nbSelectedItems = scopes.length;
const property = 'scopes';
const headerId = `facet_${property}`;
return (
- <FacetBox property={property}>
- <FacetHeader
- fetching={fetching}
- id={headerId}
- name={translate('issues.facet.scopes')}
- onClear={() => props.onChange({ scopes: [] })}
- onClick={() => props.onToggle('scopes')}
- open={open}
- values={values}
- />
-
- {open && (
- <>
- <FacetItemsList labelledby={headerId}>
- {SOURCE_SCOPES.map(({ scope, qualifier }) => {
- const active = scopes.includes(scope);
- const stat = stats[scope];
+ <FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ clearIconLabel={translate('clear')}
+ count={nbSelectedItems}
+ countLabel={translateWithParameters('x_selected', nbSelectedItems)}
+ data-property={property}
+ id={headerId}
+ loading={fetching}
+ name={translate('issues.facet.scopes')}
+ onClear={() => props.onChange({ scopes: [] })}
+ onClick={() => props.onToggle('scopes')}
+ open={open}
+ >
+ <>
+ <FacetItemsList labelledby={headerId}>
+ {SOURCE_SCOPES.map(({ scope }) => {
+ const active = scopes.includes(scope);
+ const stat = stats[scope];
- return (
- <FacetItem
- active={active}
- key={scope}
- name={
- <span className="display-flex-center">
- <QualifierIcon
- className="little-spacer-right"
- qualifier={qualifier}
- aria-hidden
- />{' '}
- {translate('issue.scope', scope)}
- </span>
+ return (
+ <FacetItem
+ active={active}
+ className="it__search-navigator-facet"
+ icon={{ MAIN: <FileIcon />, TEST: <TestFileIcon /> }[scope]}
+ key={scope}
+ name={translate('issue.scope', scope)}
+ onClick={(itemValue: string, multiple: boolean) => {
+ if (multiple) {
+ props.onChange({
+ scopes: active ? without(scopes, itemValue) : [...scopes, itemValue],
+ });
+ } else {
+ props.onChange({
+ scopes: active && scopes.length === 1 ? [] : [itemValue],
+ });
}
- onClick={(itemValue: string, multiple: boolean) => {
- if (multiple) {
- props.onChange({
- scopes: active ? without(scopes, itemValue) : [...scopes, itemValue],
- });
- } else {
- props.onChange({
- scopes: active && scopes.length === 1 ? [] : [itemValue],
- });
- }
- }}
- stat={formatFacetStat(stat)}
- value={scope}
- />
- );
- })}
- </FacetItemsList>
- <MultipleSelectionHint options={Object.keys(stats).length} values={scopes.length} />
- </>
- )}
+ }}
+ stat={formatFacetStat(stat) ?? 0}
+ value={scope}
+ />
+ );
+ })}
+ </FacetItemsList>
+
+ <MultipleSelectionHint
+ nbSelectableItems={nbSelectableItems}
+ nbSelectedItems={nbSelectedItems}
+ />
+ </>
</FacetBox>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+import {
+ FacetBox,
+ FacetItem,
+ SeverityBlockerIcon,
+ SeverityCriticalIcon,
+ SeverityInfoIcon,
+ SeverityMajorIcon,
+ SeverityMinorIcon,
+} from 'design-system';
import { orderBy, without } from 'lodash';
import * as React from 'react';
-import FacetBox from '../../../components/facet/FacetBox';
-import FacetHeader from '../../../components/facet/FacetHeader';
-import FacetItem from '../../../components/facet/FacetItem';
-import FacetItemsList from '../../../components/facet/FacetItemsList';
-import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';
-import SeverityHelper from '../../../components/shared/SeverityHelper';
-import { translate } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Dict } from '../../../types/types';
import { Query, formatFacetStat } from '../utils';
+import { FacetItemsColumns } from './FacetItemsColumns';
+import { MultipleSelectionHint } from './MultipleSelectionHint';
interface Props {
fetching: boolean;
stats: Dict<number> | undefined;
}
+// can't user SEVERITIES from 'helpers/constants' because of different order
const SEVERITIES = ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR'];
-export default class SeverityFacet extends React.PureComponent<Props> {
+export class SeverityFacet extends React.PureComponent<Props> {
property = 'severities';
static defaultProps = {
handleItemClick = (itemValue: string, multiple: boolean) => {
const { severities } = this.props;
+
if (multiple) {
const newValue = orderBy(
severities.includes(itemValue) ? without(severities, itemValue) : [...severities, itemValue]
);
+
this.props.onChange({ [this.property]: newValue });
} else {
this.props.onChange({
getStat(severity: string) {
const { stats } = this.props;
+
return stats ? stats[severity] : undefined;
}
return (
<FacetItem
active={active}
- halfWidth
+ className="it__search-navigator-facet"
+ icon={
+ {
+ BLOCKER: <SeverityBlockerIcon />,
+ CRITICAL: <SeverityCriticalIcon />,
+ INFO: <SeverityInfoIcon />,
+ MAJOR: <SeverityMajorIcon />,
+ MINOR: <SeverityMinorIcon />,
+ }[severity]
+ }
key={severity}
- name={<SeverityHelper severity={severity} />}
+ name={translate('severity', severity)}
onClick={this.handleItemClick}
- stat={formatFacetStat(stat)}
- tooltip={translate('severity', severity)}
+ stat={formatFacetStat(stat) ?? 0}
value={severity}
/>
);
};
render() {
- const { fetching, open, severities, stats = {} } = this.props;
- const values = severities.map((severity) => translate('severity', severity));
+ const { fetching, open, severities } = this.props;
+
const headerId = `facet_${this.property}`;
+ const nbSelectableItems = SEVERITIES.filter(this.getStat.bind(this)).length;
+ const nbSelectedItems = severities.length;
return (
- <FacetBox property={this.property}>
- <FacetHeader
- fetching={fetching}
- id={headerId}
- name={translate('issues.facet', this.property)}
- onClear={this.handleClear}
- onClick={this.handleHeaderClick}
- open={open}
- values={values}
- />
+ <FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ clearIconLabel={translate('clear')}
+ count={nbSelectedItems}
+ countLabel={translateWithParameters('x_selected', nbSelectedItems)}
+ data-property={this.property}
+ id={headerId}
+ loading={fetching}
+ name={translate('issues.facet', this.property)}
+ onClear={this.handleClear}
+ onClick={this.handleHeaderClick}
+ open={open}
+ >
+ <FacetItemsColumns>{SEVERITIES.map(this.renderItem)}</FacetItemsColumns>
- {open && (
- <>
- <FacetItemsList labelledby={headerId}>{SEVERITIES.map(this.renderItem)}</FacetItemsList>
- <MultipleSelectionHint options={Object.keys(stats).length} values={severities.length} />
- </>
- )}
+ <MultipleSelectionHint
+ nbSelectableItems={nbSelectableItems}
+ nbSelectedItems={nbSelectedItems}
+ />
</FacetBox>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+import { BasicSeparator } from 'design-system';
import * as React from 'react';
import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
import { isBranch, isPullRequest } from '../../../helpers/branch-like';
import { Component, Dict } from '../../../types/types';
import { UserBase } from '../../../types/users';
import { Query } from '../utils';
-import AssigneeFacet from './AssigneeFacet';
-import AuthorFacet from './AuthorFacet';
-import CreationDateFacet from './CreationDateFacet';
-import DirectoryFacet from './DirectoryFacet';
-import FileFacet from './FileFacet';
-import LanguageFacet from './LanguageFacet';
-import PeriodFilter from './PeriodFilter';
-import ProjectFacet from './ProjectFacet';
-import ResolutionFacet from './ResolutionFacet';
-import RuleFacet from './RuleFacet';
-import ScopeFacet from './ScopeFacet';
-import SeverityFacet from './SeverityFacet';
-import StandardFacet from './StandardFacet';
-import StatusFacet from './StatusFacet';
-import TagFacet from './TagFacet';
-import TypeFacet from './TypeFacet';
-import VariantFacet from './VariantFacet';
+import { AssigneeFacet } from './AssigneeFacet';
+import { AuthorFacet } from './AuthorFacet';
+import { CreationDateFacet } from './CreationDateFacet';
+import { DirectoryFacet } from './DirectoryFacet';
+import { FileFacet } from './FileFacet';
+import { LanguageFacet } from './LanguageFacet';
+import { PeriodFilter } from './PeriodFilter';
+import { ProjectFacet } from './ProjectFacet';
+import { ResolutionFacet } from './ResolutionFacet';
+import { RuleFacet } from './RuleFacet';
+import { ScopeFacet } from './ScopeFacet';
+import { SeverityFacet } from './SeverityFacet';
+import { StandardFacet } from './StandardFacet';
+import { StatusFacet } from './StatusFacet';
+import { TagFacet } from './TagFacet';
+import { TypeFacet } from './TypeFacet';
+import { VariantFacet } from './VariantFacet';
export interface Props {
appState: AppState;
referencedUsers: Dict<UserBase>;
}
-export class Sidebar extends React.PureComponent<Props> {
+export class SidebarClass extends React.PureComponent<Props> {
renderComponentFacets() {
const { component, facets, loadingFacets, openFacets, query, branchLike, showVariantsFilter } =
this.props;
+
const hasFileOrDirectory =
!isApplication(component?.qualifier) && !isPortfolioLike(component?.qualifier);
+
if (!component || !hasFileOrDirectory) {
return null;
}
+
const commonProps = {
componentKey: component.key,
loadSearchResultCount: this.props.loadSearchResultCount,
onToggle: this.props.onFacetToggle,
query,
};
+
return (
<>
{showVariantsFilter && isProject(component?.qualifier) && (
- <VariantFacet
- fetching={loadingFacets.codeVariants === true}
- open={!!openFacets.codeVariants}
- stats={facets.codeVariants}
- values={query.codeVariants}
- {...commonProps}
- />
+ <>
+ <BasicSeparator className="sw-my-4" />
+
+ <VariantFacet
+ fetching={loadingFacets.codeVariants === true}
+ open={!!openFacets.codeVariants}
+ stats={facets.codeVariants}
+ values={query.codeVariants}
+ {...commonProps}
+ />
+ </>
)}
+
{component.qualifier !== ComponentQualifier.Directory && (
- <DirectoryFacet
- branchLike={branchLike}
- directories={query.directories}
- fetching={loadingFacets.directories === true}
- open={!!openFacets.directories}
- stats={facets.directories}
- {...commonProps}
- />
+ <>
+ <BasicSeparator className="sw-my-4" />
+
+ <DirectoryFacet
+ branchLike={branchLike}
+ directories={query.directories}
+ fetching={loadingFacets.directories === true}
+ open={!!openFacets.directories}
+ stats={facets.directories}
+ {...commonProps}
+ />
+ </>
)}
+
+ <BasicSeparator className="sw-my-4" />
+
<FileFacet
branchLike={branchLike}
fetching={loadingFacets.files === true}
const displayPeriodFilter = component !== undefined && !isPortfolioLike(component.qualifier);
const displayProjectsFacet = !component || isView(component.qualifier);
- const displayAuthorFacet = !component || component.qualifier !== 'DEV';
+ const displayAuthorFacet = !component || component.qualifier !== ComponentQualifier.Developper;
return (
<>
{displayPeriodFilter && (
<PeriodFilter
- fetching={this.props.loadingFacets.period === true}
onChange={this.props.onFilterChange}
- stats={facets.period}
newCodeSelected={query.inNewCodePeriod}
/>
)}
+
<TypeFacet
fetching={this.props.loadingFacets.types === true}
onChange={this.props.onFilterChange}
stats={facets.types}
types={query.types}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<SeverityFacet
fetching={this.props.loadingFacets.severities === true}
onChange={this.props.onFilterChange}
severities={query.severities}
stats={facets.severities}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<ScopeFacet
fetching={this.props.loadingFacets.scopes === true}
onChange={this.props.onFilterChange}
stats={facets.scopes}
scopes={query.scopes}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<ResolutionFacet
fetching={this.props.loadingFacets.resolutions === true}
onChange={this.props.onFilterChange}
resolved={query.resolved}
stats={facets.resolutions}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<StatusFacet
fetching={this.props.loadingFacets.statuses === true}
onChange={this.props.onFilterChange}
stats={facets.statuses}
statuses={query.statuses}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<StandardFacet
cwe={query.cwe}
cweOpen={!!openFacets.cwe}
sonarsourceSecurityOpen={!!openFacets.sonarsourceSecurity}
sonarsourceSecurityStats={facets.sonarsourceSecurity}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<CreationDateFacet
component={component}
createdAfter={query.createdAfter}
inNewCodePeriod={query.inNewCodePeriod}
stats={facets.createdAt}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<LanguageFacet
fetching={this.props.loadingFacets.languages === true}
loadSearchResultCount={this.props.loadSearchResultCount}
selectedLanguages={query.languages}
stats={facets.languages}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<RuleFacet
fetching={this.props.loadingFacets.rules === true}
loadSearchResultCount={this.props.loadSearchResultCount}
referencedRules={this.props.referencedRules}
stats={facets.rules}
/>
+
+ <BasicSeparator className="sw-my-4" />
+
<TagFacet
component={component}
branch={branch}
stats={facets.tags}
tags={query.tags}
/>
+
{displayProjectsFacet && (
- <ProjectFacet
- component={component}
- fetching={this.props.loadingFacets.projects === true}
- loadSearchResultCount={this.props.loadSearchResultCount}
- onChange={this.props.onFilterChange}
- onToggle={this.props.onFacetToggle}
- open={!!openFacets.projects}
- projects={query.projects}
- query={query}
- referencedComponents={this.props.referencedComponentsByKey}
- stats={facets.projects}
- />
+ <>
+ <BasicSeparator className="sw-my-4" />
+
+ <ProjectFacet
+ component={component}
+ fetching={this.props.loadingFacets.projects === true}
+ loadSearchResultCount={this.props.loadSearchResultCount}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.projects}
+ projects={query.projects}
+ query={query}
+ referencedComponents={this.props.referencedComponentsByKey}
+ stats={facets.projects}
+ />
+ </>
)}
+
{this.renderComponentFacets()}
+
{!this.props.myIssues && !disableDeveloperAggregatedInfo && (
- <AssigneeFacet
- assigned={query.assigned}
- assignees={query.assignees}
- fetching={this.props.loadingFacets.assignees === true}
- loadSearchResultCount={this.props.loadSearchResultCount}
- onChange={this.props.onFilterChange}
- onToggle={this.props.onFacetToggle}
- open={!!openFacets.assignees}
- query={query}
- referencedUsers={this.props.referencedUsers}
- stats={facets.assignees}
- />
+ <>
+ <BasicSeparator className="sw-my-4" />
+
+ <AssigneeFacet
+ assigned={query.assigned}
+ assignees={query.assignees}
+ fetching={this.props.loadingFacets.assignees === true}
+ loadSearchResultCount={this.props.loadSearchResultCount}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.assignees}
+ query={query}
+ referencedUsers={this.props.referencedUsers}
+ stats={facets.assignees}
+ />
+ </>
)}
+
{displayAuthorFacet && !disableDeveloperAggregatedInfo && (
- <AuthorFacet
- author={query.author}
- component={component}
- fetching={this.props.loadingFacets.author === true}
- loadSearchResultCount={this.props.loadSearchResultCount}
- onChange={this.props.onFilterChange}
- onToggle={this.props.onFacetToggle}
- open={!!openFacets.author}
- query={query}
- stats={facets.author}
- />
+ <>
+ <BasicSeparator className="sw-my-4" />
+
+ <AuthorFacet
+ author={query.author}
+ component={component}
+ fetching={this.props.loadingFacets.author === true}
+ loadSearchResultCount={this.props.loadSearchResultCount}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.author}
+ query={query}
+ stats={facets.author}
+ />
+ </>
)}
</>
);
}
}
-export default withAppStateContext(Sidebar);
+export const Sidebar = withAppStateContext(SidebarClass);
*/
/* eslint-disable react/no-unused-prop-types */
+import { FacetBox, FacetItem } from 'design-system';
import { omit, sortBy, without } from 'lodash';
import * as React from 'react';
-import FacetBox from '../../../components/facet/FacetBox';
-import FacetHeader from '../../../components/facet/FacetHeader';
-import FacetItem from '../../../components/facet/FacetItem';
-import FacetItemsList from '../../../components/facet/FacetItemsList';
-import ListStyleFacet from '../../../components/facet/ListStyleFacet';
-import ListStyleFacetFooter from '../../../components/facet/ListStyleFacetFooter';
-import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';
-import { translate } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import { highlightTerm } from '../../../helpers/search';
import {
getStandards,
import { SecurityStandard, Standards } from '../../../types/security';
import { Dict } from '../../../types/types';
import { Query, STANDARDS, formatFacetStat } from '../utils';
+import { FacetItemsList } from './FacetItemsList';
+import { ListStyleFacet } from './ListStyleFacet';
+import { ListStyleFacetFooter } from './ListStyleFacetFooter';
+import { MultipleSelectionHint } from './MultipleSelectionHint';
interface Props {
cwe: string[];
| 'owaspTop10Stats'
| 'cweStats'
| 'sonarsourceSecurityStats';
+
type ValuesProp = 'owaspTop10-2021' | 'owaspTop10' | 'sonarsourceSecurity' | 'cwe';
const INITIAL_FACET_COUNT = 15;
-export default class StandardFacet extends React.PureComponent<Props, State> {
+
+export class StandardFacet extends React.PureComponent<Props, State> {
mounted = false;
property = STANDARDS;
+
state: State = {
showFullSonarSourceList: false,
standards: {
owaspTop10,
cwe,
sonarsourceSecurity,
- 'pciDss-3.2': pciDss3_2,
- 'pciDss-4.0': pciDss4_0,
- 'owaspAsvs-4.0': owaspAsvs4_0,
+ 'pciDss-3.2': pciDss32,
+ 'pciDss-4.0': pciDss40,
+ 'owaspAsvs-4.0': owaspAsvs40,
}: Standards) => {
if (this.mounted) {
this.setState({
owaspTop10,
cwe,
sonarsourceSecurity,
- 'pciDss-3.2': pciDss3_2,
- 'pciDss-4.0': pciDss4_0,
- 'owaspAsvs-4.0': owaspAsvs4_0,
+ 'pciDss-3.2': pciDss32,
+ 'pciDss-4.0': pciDss40,
+ 'owaspAsvs-4.0': owaspAsvs40,
},
});
}
handleItemClick = (prop: ValuesProp, itemValue: string, multiple: boolean) => {
const items = this.props[prop];
+
if (multiple) {
const newValue = sortBy(
items.includes(itemValue) ? without(items, itemValue) : [...items, itemValue]
);
+
this.props.onChange({ [prop]: newValue });
} else {
this.props.onChange({
loadCWESearchResultCount = (categories: string[]) => {
const { loadSearchResultCount } = this.props;
+
return loadSearchResultCount
? loadSearchResultCount('cwe', { cwe: categories })
: Promise.resolve({});
) => {
const stats = this.props[statsProp];
const values = this.props[valuesProp];
+
if (!stats) {
return null;
}
+
const categories = sortBy(Object.keys(stats), (key) => -stats[key]);
- return this.renderFacetItemsList(
- stats,
- values,
- categories,
- valuesProp,
- renderName,
- renderName,
- onClick
- );
+
+ return this.renderFacetItemsList(stats, values, categories, renderName, renderName, onClick);
};
// eslint-disable-next-line max-params
renderFacetItemsList = (
- stats: any,
+ stats: Dict<number | undefined>,
values: string[],
categories: string[],
- listKey: ValuesProp,
renderName: (standards: Standards, category: string) => React.ReactNode,
renderTooltip: (standards: Standards, category: string) => string,
onClick: (x: string, multiple?: boolean) => void
return stats ? stats[category] : undefined;
};
- return (
- <FacetItemsList labelledby={this.getFacetHeaderId(listKey)}>
- {categories.map((category) => (
- <FacetItem
- active={values.includes(category)}
- key={category}
- name={renderName(this.state.standards, category)}
- onClick={onClick}
- stat={formatFacetStat(getStat(category))}
- tooltip={renderTooltip(this.state.standards, category)}
- value={category}
- />
- ))}
- </FacetItemsList>
- );
+ return categories.map((category) => (
+ <FacetItem
+ active={values.includes(category)}
+ className="it__search-navigator-facet"
+ key={category}
+ name={renderName(this.state.standards, category)}
+ onClick={onClick}
+ stat={formatFacetStat(getStat(category)) ?? 0}
+ tooltip={renderTooltip(this.state.standards, category)}
+ value={category}
+ />
+ ));
};
renderHint = (statsProp: StatsProp, valuesProp: ValuesProp) => {
- const stats = this.props[statsProp] || {};
- const values = this.props[valuesProp];
- return <MultipleSelectionHint options={Object.keys(stats).length} values={values.length} />;
+ const nbSelectableItems = Object.keys(this.props[statsProp] ?? {}).length;
+ const nbSelectedItems = this.props[valuesProp].length;
+
+ return (
+ <MultipleSelectionHint
+ nbSelectableItems={nbSelectableItems}
+ nbSelectedItems={nbSelectedItems}
+ />
+ );
};
renderOwaspTop10List() {
: sortedItems.slice(INITIAL_FACET_COUNT).filter((item) => values.includes(item));
const allItemShown = limitedList.length + selectedBelowLimit.length === sortedItems.length;
+
return (
<>
- <FacetItemsList labelledby={this.getFacetHeaderId(SecurityStandard.SONARSOURCE)}>
- {limitedList.map((item) => (
- <FacetItem
- active={values.includes(item)}
- key={item}
- name={renderSonarSourceSecurityCategory(this.state.standards, item)}
- onClick={this.handleSonarSourceSecurityItemClick}
- stat={formatFacetStat(stats[item])}
- tooltip={renderSonarSourceSecurityCategory(this.state.standards, item)}
- value={item}
- />
- ))}
- </FacetItemsList>
+ {limitedList.map((item) => (
+ <FacetItem
+ active={values.includes(item)}
+ className="it__search-navigator-facet"
+ key={item}
+ name={renderSonarSourceSecurityCategory(this.state.standards, item)}
+ onClick={this.handleSonarSourceSecurityItemClick}
+ stat={formatFacetStat(stats[item]) ?? 0}
+ tooltip={renderSonarSourceSecurityCategory(this.state.standards, item)}
+ value={item}
+ />
+ ))}
+
{selectedBelowLimit.length > 0 && (
<>
{!allItemShown && <div className="note spacer-bottom text-center">⋯</div>}
- <FacetItemsList labelledby={this.getFacetHeaderId(SecurityStandard.SONARSOURCE)}>
- {selectedBelowLimit.map((item) => (
- <FacetItem
- active
- key={item}
- name={renderSonarSourceSecurityCategory(this.state.standards, item)}
- onClick={this.handleSonarSourceSecurityItemClick}
- stat={formatFacetStat(stats[item])}
- tooltip={renderSonarSourceSecurityCategory(this.state.standards, item)}
- value={item}
- />
- ))}
- </FacetItemsList>
+ {selectedBelowLimit.map((item) => (
+ <FacetItem
+ active
+ className="it__search-navigator-facet"
+ key={item}
+ name={renderSonarSourceSecurityCategory(this.state.standards, item)}
+ onClick={this.handleSonarSourceSecurityItemClick}
+ stat={formatFacetStat(stats[item]) ?? 0}
+ tooltip={renderSonarSourceSecurityCategory(this.state.standards, item)}
+ value={item}
+ />
+ ))}
</>
)}
+
{!allItemShown && (
<ListStyleFacetFooter
- showMoreAriaLabel={translate('issues.facet.sonarsource.show_more')}
- count={limitedList.length + selectedBelowLimit.length}
+ nbShown={limitedList.length + selectedBelowLimit.length}
showMore={() => this.setState({ showFullSonarSourceList: true })}
+ showMoreAriaLabel={translate('issues.facet.sonarsource.show_more')}
total={sortedItems.length}
/>
)}
sonarsourceSecurity,
sonarsourceSecurityOpen,
} = this.props;
+
+ const standards = [
+ {
+ count: sonarsourceSecurity.length,
+ loading: fetchingSonarSourceSecurity,
+ name: 'sonarsourceSecurity',
+ onClick: this.handleSonarSourceSecurityHeaderClick,
+ open: sonarsourceSecurityOpen,
+ panel: (
+ <>
+ {this.renderSonarSourceSecurityList()}
+ {this.renderSonarSourceSecurityHint()}
+ </>
+ ),
+ property: SecurityStandard.SONARSOURCE,
+ },
+ {
+ count: owaspTop102021.length,
+ loading: fetchingOwaspTop102021,
+ name: 'owaspTop10_2021',
+ onClick: this.handleOwaspTop102021HeaderClick,
+ open: owaspTop102021Open,
+ panel: (
+ <>
+ {this.renderOwaspTop102021List()}
+ {this.renderOwaspTop102021Hint()}
+ </>
+ ),
+ property: SecurityStandard.OWASP_TOP10_2021,
+ },
+ {
+ count: owaspTop10.length,
+ loading: fetchingOwaspTop10,
+ name: 'owaspTop10',
+ onClick: this.handleOwaspTop10HeaderClick,
+ open: owaspTop10Open,
+ panel: (
+ <>
+ {this.renderOwaspTop10List()}
+ {this.renderOwaspTop10Hint()}
+ </>
+ ),
+ property: SecurityStandard.OWASP_TOP10,
+ },
+ ];
+
return (
<>
- <FacetBox className="is-inner" property={SecurityStandard.SONARSOURCE}>
- <FacetHeader
- fetching={fetchingSonarSourceSecurity}
- id={this.getFacetHeaderId(SecurityStandard.SONARSOURCE)}
- name={translate('issues.facet.sonarsourceSecurity')}
- onClick={this.handleSonarSourceSecurityHeaderClick}
- open={sonarsourceSecurityOpen}
- values={sonarsourceSecurity.map((item) =>
- renderSonarSourceSecurityCategory(this.state.standards, item)
- )}
- />
- {sonarsourceSecurityOpen && (
- <>
- {this.renderSonarSourceSecurityList()}
- {this.renderSonarSourceSecurityHint()}
- </>
- )}
- </FacetBox>
- <FacetBox className="is-inner" property={SecurityStandard.OWASP_TOP10_2021}>
- <FacetHeader
- fetching={fetchingOwaspTop102021}
- id={this.getFacetHeaderId(SecurityStandard.OWASP_TOP10_2021)}
- name={translate('issues.facet.owaspTop10_2021')}
- onClick={this.handleOwaspTop102021HeaderClick}
- open={owaspTop102021Open}
- values={owaspTop102021.map((item) =>
- renderOwaspTop102021Category(this.state.standards, item)
- )}
- />
- {owaspTop102021Open && (
- <>
- {this.renderOwaspTop102021List()}
- {this.renderOwaspTop102021Hint()}
- </>
- )}
- </FacetBox>
- <FacetBox className="is-inner" property={SecurityStandard.OWASP_TOP10}>
- <FacetHeader
- fetching={fetchingOwaspTop10}
- id={this.getFacetHeaderId(SecurityStandard.OWASP_TOP10)}
- name={translate('issues.facet.owaspTop10')}
- onClick={this.handleOwaspTop10HeaderClick}
- open={owaspTop10Open}
- values={owaspTop10.map((item) => renderOwaspTop10Category(this.state.standards, item))}
- />
- {owaspTop10Open && (
- <>
- {this.renderOwaspTop10List()}
- {this.renderOwaspTop10Hint()}
- </>
- )}
- </FacetBox>
+ {standards.map(({ name, open, panel, property, ...standard }) => (
+ <FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ data-property={property}
+ id={this.getFacetHeaderId(property)}
+ inner={true}
+ key={property}
+ name={translate(`issues.facet.${name}`)}
+ open={open}
+ {...standard}
+ >
+ <FacetItemsList labelledby={this.getFacetHeaderId(property)}>{panel}</FacetItemsList>
+ </FacetBox>
+ ))}
+
<ListStyleFacet<string>
- className="is-inner"
facetHeader={translate('issues.facet.cwe')}
fetching={fetchingCwe}
getFacetItemText={(item) => renderCWECategory(this.state.standards, item)}
- getSearchResultKey={(item) => item}
getSearchResultText={(item) => renderCWECategory(this.state.standards, item)}
+ inner={true}
loadSearchResultCount={this.loadCWESearchResultCount}
onChange={this.props.onChange}
onSearch={this.handleCWESearch}
render() {
const { open } = this.props;
- return (
- <FacetBox property={this.property}>
- <FacetHeader
- id={this.getFacetHeaderId(this.property)}
- name={translate('issues.facet', this.property)}
- onClear={this.handleClear}
- onClick={this.handleHeaderClick}
- open={open}
- values={this.getValues()}
- />
+ const count = this.getValues().length;
- {open && this.renderSubFacets()}
+ return (
+ <FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ clearIconLabel={translate('clear')}
+ count={count}
+ countLabel={translateWithParameters('x_selected', count)}
+ data-property={this.property}
+ hasEmbeddedFacets={true}
+ id={this.getFacetHeaderId(this.property)}
+ name={translate('issues.facet', this.property)}
+ onClear={this.handleClear}
+ onClick={this.handleHeaderClick}
+ open={open}
+ >
+ {this.renderSubFacets()}
</FacetBox>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+import {
+ FacetBox,
+ FacetItem,
+ StatusConfirmedIcon,
+ StatusOpenIcon,
+ StatusReopenedIcon,
+ StatusResolvedIcon,
+} from 'design-system';
import { orderBy, without } from 'lodash';
import * as React from 'react';
-import FacetBox from '../../../components/facet/FacetBox';
-import FacetHeader from '../../../components/facet/FacetHeader';
-import FacetItem from '../../../components/facet/FacetItem';
-import FacetItemsList from '../../../components/facet/FacetItemsList';
-import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';
-import StatusHelper from '../../../components/shared/StatusHelper';
-import { translate } from '../../../helpers/l10n';
+import { STATUSES } from '../../../helpers/constants';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Dict } from '../../../types/types';
import { Query, formatFacetStat } from '../utils';
+import { FacetItemsColumns } from './FacetItemsColumns';
+import { MultipleSelectionHint } from './MultipleSelectionHint';
interface Props {
fetching: boolean;
statuses: string[];
}
-const STATUSES = ['OPEN', 'CONFIRMED', 'REOPENED', 'RESOLVED', 'CLOSED'];
-
-export default class StatusFacet extends React.PureComponent<Props> {
+export class StatusFacet extends React.PureComponent<Props> {
property = 'statuses';
static defaultProps = { open: true };
handleItemClick = (itemValue: string, multiple: boolean) => {
const { statuses } = this.props;
+
if (multiple) {
const newValue = orderBy(
statuses.includes(itemValue) ? without(statuses, itemValue) : [...statuses, itemValue]
);
+
this.props.onChange({ [this.property]: newValue });
} else {
this.props.onChange({
getStat(status: string) {
const { stats } = this.props;
+
return stats ? stats[status] : undefined;
}
return (
<FacetItem
active={active}
- halfWidth
+ className="it__search-navigator-facet"
+ icon={
+ {
+ CLOSED: <StatusResolvedIcon />,
+ CONFIRMED: <StatusConfirmedIcon />,
+ OPEN: <StatusOpenIcon />,
+ REOPENED: <StatusReopenedIcon />,
+ RESOLVED: <StatusResolvedIcon />,
+ }[status]
+ }
key={status}
- name={<StatusHelper resolution={undefined} status={status} />}
+ name={translate('issue.status', status)}
onClick={this.handleItemClick}
- stat={formatFacetStat(stat)}
+ stat={formatFacetStat(stat) ?? 0}
tooltip={translate('issue.status', status)}
value={status}
/>
};
render() {
- const { fetching, open, statuses, stats = {} } = this.props;
- const values = statuses.map((status) => translate('issue.status', status));
+ const { fetching, open, statuses } = this.props;
+
+ const nbSelectableItems = STATUSES.filter(this.getStat.bind(this)).length;
+ const nbSelectedItems = statuses.length;
const headerId = `facet_${this.property}`;
return (
- <FacetBox property={this.property}>
- <FacetHeader
- fetching={fetching}
- id={headerId}
- name={translate('issues.facet', this.property)}
- onClear={this.handleClear}
- onClick={this.handleHeaderClick}
- open={open}
- values={values}
- />
+ <FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ clearIconLabel={translate('clear')}
+ count={nbSelectedItems}
+ countLabel={translateWithParameters('x_selected', nbSelectedItems)}
+ data-property={this.property}
+ id={headerId}
+ loading={fetching}
+ name={translate('issues.facet', this.property)}
+ onClear={this.handleClear}
+ onClick={this.handleHeaderClick}
+ open={open}
+ >
+ <FacetItemsColumns>{STATUSES.map(this.renderItem)}</FacetItemsColumns>
- {open && (
- <>
- <FacetItemsList labelledby={headerId}>{STATUSES.map(this.renderItem)}</FacetItemsList>
- <MultipleSelectionHint options={Object.keys(stats).length} values={statuses.length} />
- </>
- )}
+ <MultipleSelectionHint
+ nbSelectableItems={nbSelectableItems}
+ nbSelectedItems={nbSelectedItems}
+ />
</FacetBox>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { omit } from 'lodash';
import * as React from 'react';
import { searchIssueTags } from '../../../api/issues';
-import { colors } from '../../../app/theme';
-import ListStyleFacet from '../../../components/facet/ListStyleFacet';
-import TagsIcon from '../../../components/icons/TagsIcon';
import { translate } from '../../../helpers/l10n';
import { highlightTerm } from '../../../helpers/search';
+import { ComponentQualifier } from '../../../types/component';
import { Facet } from '../../../types/issues';
import { Component, Dict } from '../../../types/types';
import { Query } from '../utils';
+import { ListStyleFacet } from './ListStyleFacet';
interface Props {
component: Component | undefined;
const SEARCH_SIZE = 100;
-export default class TagFacet extends React.PureComponent<Props> {
+export class TagFacet extends React.PureComponent<Props> {
handleSearch = (query: string) => {
const { component, branch } = this.props;
+
const project =
- component && ['TRK', 'VW', 'APP'].includes(component.qualifier) ? component.key : undefined;
+ component &&
+ [
+ ComponentQualifier.Project,
+ ComponentQualifier.Portfolio,
+ ComponentQualifier.Application,
+ ].includes(component.qualifier as ComponentQualifier)
+ ? component.key
+ : undefined;
+
return searchIssueTags({
project,
branch,
return this.props.loadSearchResultCount('tags', { tags });
};
- renderTag = (tag: string) => {
- return (
- <>
- <TagsIcon className="little-spacer-right" fill={colors.gray60} />
- {tag}
- </>
- );
- };
-
- renderSearchResult = (tag: string, term: string) => (
- <>
- <TagsIcon className="little-spacer-right" fill={colors.gray60} />
- {highlightTerm(tag, term)}
- </>
- );
-
render() {
return (
<ListStyleFacet<string>
facetHeader={translate('issues.facet.tags')}
fetching={this.props.fetching}
getFacetItemText={this.getTagName}
- getSearchResultKey={(tag) => tag}
- getSearchResultText={(tag) => tag}
loadSearchResultCount={this.loadSearchResultCount}
onChange={this.props.onChange}
onSearch={this.handleSearch}
open={this.props.open}
property="tags"
query={omit(this.props.query, 'tags')}
- renderFacetItem={this.renderTag}
- renderSearchResult={this.renderSearchResult}
+ renderSearchResult={highlightTerm}
searchPlaceholder={translate('search.search_for_tags')}
stats={this.props.stats}
values={this.props.tags}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+import { BugIcon, CodeSmellIcon, FacetBox, FacetItem, VulnerabilityIcon } from 'design-system';
import { orderBy, without } from 'lodash';
import * as React from 'react';
-import FacetBox from '../../../components/facet/FacetBox';
-import FacetHeader from '../../../components/facet/FacetHeader';
-import FacetItem from '../../../components/facet/FacetItem';
-import FacetItemsList from '../../../components/facet/FacetItemsList';
-import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';
-import IssueTypeIcon from '../../../components/icons/IssueTypeIcon';
import { ISSUE_TYPES } from '../../../helpers/constants';
-import { translate } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Dict } from '../../../types/types';
import { Query, formatFacetStat } from '../utils';
+import { FacetItemsList } from './FacetItemsList';
+import { MultipleSelectionHint } from './MultipleSelectionHint';
interface Props {
fetching: boolean;
types: string[];
}
-export default class TypeFacet extends React.PureComponent<Props> {
+const AVAILABLE_TYPES = ISSUE_TYPES.filter((t) => t !== 'SECURITY_HOTSPOT');
+
+export class TypeFacet extends React.PureComponent<Props> {
property = 'types';
static defaultProps = {
getStat(type: string) {
const { stats } = this.props;
+
return stats ? stats[type] : undefined;
}
return (
<FacetItem
active={active}
- key={type}
- name={
- <span className="display-flex-center">
- <IssueTypeIcon className="little-spacer-right" query={type} />{' '}
- {translate('issue.type', type)}
- </span>
+ className="it__search-navigator-facet"
+ icon={
+ { BUG: <BugIcon />, CODE_SMELL: <CodeSmellIcon />, VULNERABILITY: <VulnerabilityIcon /> }[
+ type
+ ]
}
+ key={type}
+ name={translate('issue.type', type)}
onClick={this.handleItemClick}
- stat={formatFacetStat(stat)}
+ stat={formatFacetStat(stat) ?? 0}
value={type}
/>
);
};
render() {
- const { fetching, open, types, stats = {} } = this.props;
- const values = types.map((type) => translate('issue.type', type));
+ const { fetching, open, types } = this.props;
+
+ const nbSelectableItems = AVAILABLE_TYPES.filter(this.getStat.bind(this)).length;
+ const nbSelectedItems = types.length;
const typeFacetHeaderId = `facet_${this.property}`;
return (
- <FacetBox property={this.property}>
- <FacetHeader
- fetching={fetching}
- id={typeFacetHeaderId}
- name={translate('issues.facet', this.property)}
- onClear={this.handleClear}
- onClick={this.handleHeaderClick}
- open={open}
- values={values}
- />
+ <FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ clearIconLabel={translate('clear')}
+ count={nbSelectedItems}
+ countLabel={translateWithParameters('x_selected', nbSelectedItems)}
+ data-property={this.property}
+ id={typeFacetHeaderId}
+ loading={fetching}
+ name={translate('issues.facet', this.property)}
+ onClear={this.handleClear}
+ onClick={this.handleHeaderClick}
+ open={open}
+ >
+ <FacetItemsList labelledby={typeFacetHeaderId}>
+ {AVAILABLE_TYPES.map(this.renderItem)}
+ </FacetItemsList>
- {open && (
- <>
- <FacetItemsList labelledby={typeFacetHeaderId}>
- {ISSUE_TYPES.filter((t) => t !== 'SECURITY_HOTSPOT').map(this.renderItem)}
- </FacetItemsList>
- <MultipleSelectionHint options={Object.keys(stats).length} values={types.length} />
- </>
- )}
+ <MultipleSelectionHint
+ nbSelectableItems={nbSelectableItems}
+ nbSelectedItems={nbSelectedItems}
+ />
</FacetBox>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+import { FacetBox, FacetItem } from 'design-system';
import { orderBy, sortBy, without } from 'lodash';
import * as React from 'react';
-import FacetBox from '../../../components/facet/FacetBox';
-import FacetHeader from '../../../components/facet/FacetHeader';
-import FacetItem from '../../../components/facet/FacetItem';
-import FacetItemsList from '../../../components/facet/FacetItemsList';
-import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';
-import { translate } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Dict } from '../../../types/types';
import { Query, formatFacetStat } from '../utils';
+import { FacetItemsList } from './FacetItemsList';
+import { MultipleSelectionHint } from './MultipleSelectionHint';
interface VariantFacetProps {
fetching: boolean;
const FACET_NAME = 'codeVariants';
-export default function VariantFacet(props: VariantFacetProps) {
+export function VariantFacet(props: VariantFacetProps) {
const { open, fetching, stats = {}, values, onToggle, onChange } = props;
const handleClear = React.useCallback(() => {
const newValues = orderBy(
values.includes(value) ? without(values, value) : [...values, value]
);
+
onChange({ [FACET_NAME]: newValues });
} else {
onChange({
});
}
},
+
[values, onChange]
);
const id = `facet_${FACET_NAME}`;
+ const nbSelectableItems = Object.keys(stats).length;
+ const nbSelectedItems = values.length;
+
return (
- <FacetBox property={FACET_NAME}>
- <FacetHeader
- fetching={fetching}
- name={translate('issues.facet', FACET_NAME)}
- id={id}
- onClear={handleClear}
- onClick={handleHeaderClick}
- open={open}
- values={values}
+ <FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ clearIconLabel={translate('clear')}
+ count={nbSelectedItems}
+ countLabel={translateWithParameters('x_selected', nbSelectedItems)}
+ data-property={FACET_NAME}
+ id={id}
+ loading={fetching}
+ name={translate('issues.facet', FACET_NAME)}
+ onClear={handleClear}
+ onClick={handleHeaderClick}
+ open={open}
+ >
+ <FacetItemsList labelledby={id}>
+ {nbSelectableItems === 0 && (
+ <div className="note spacer-bottom">{translate('no_results')}</div>
+ )}
+
+ {sortBy(
+ Object.keys(stats),
+ (key) => -stats[key],
+ (key) => key
+ ).map((codeVariant) => (
+ <FacetItem
+ active={values.includes(codeVariant)}
+ className="it__search-navigator-facet"
+ key={codeVariant}
+ name={codeVariant}
+ onClick={handleItemClick}
+ stat={formatFacetStat(stats[codeVariant])}
+ value={codeVariant}
+ />
+ ))}
+ </FacetItemsList>
+
+ <MultipleSelectionHint
+ nbSelectableItems={nbSelectableItems}
+ nbSelectedItems={nbSelectedItems}
/>
- {open && (
- <>
- <FacetItemsList labelledby={id}>
- {Object.keys(stats).length === 0 && (
- <div className="note spacer-bottom">{translate('no_results')}</div>
- )}
- {sortBy(
- Object.keys(stats),
- (key) => -stats[key],
- (key) => key
- ).map((codeVariant) => (
- <FacetItem
- active={values.includes(codeVariant)}
- key={codeVariant}
- name={codeVariant}
- onClick={handleItemClick}
- stat={formatFacetStat(stats[codeVariant])}
- value={codeVariant}
- />
- ))}
- </FacetItemsList>
- <MultipleSelectionHint options={Object.keys(stats).length} values={values.length} />
- </>
- )}
</FacetBox>
);
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { shallow, ShallowWrapper } from 'enzyme';
+import * as React from 'react';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { ListStyleFacet, Props } from '../ListStyleFacet';
+
+it('should render', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should select items', () => {
+ const onChange = jest.fn();
+ const wrapper = shallowRender({ onChange });
+ const instance = wrapper.instance() as ListStyleFacet<string>;
+
+ // select one item
+ instance.handleItemClick('b', false);
+ expect(onChange).toHaveBeenLastCalledWith({ foo: ['b'] });
+ wrapper.setProps({ values: ['b'] });
+
+ // select another item
+ instance.handleItemClick('a', false);
+ expect(onChange).toHaveBeenLastCalledWith({ foo: ['a'] });
+ wrapper.setProps({ values: ['a'] });
+
+ // unselect item
+ instance.handleItemClick('a', false);
+ expect(onChange).toHaveBeenLastCalledWith({ foo: [] });
+ wrapper.setProps({ values: [] });
+
+ // select multiple items
+ wrapper.setProps({ values: ['b'] });
+ instance.handleItemClick('c', true);
+ expect(onChange).toHaveBeenLastCalledWith({ foo: ['b', 'c'] });
+ wrapper.setProps({ values: ['b', 'c'] });
+
+ // unselect item
+ instance.handleItemClick('c', true);
+ expect(onChange).toHaveBeenLastCalledWith({ foo: ['b'] });
+});
+
+it('should toggle', () => {
+ const onToggle = jest.fn();
+ const wrapper = shallowRender({ onToggle });
+ wrapper.find('FacetBox').prop<Function>('onClick')();
+ expect(onToggle).toHaveBeenCalled();
+});
+
+it('should clear', () => {
+ const onChange = jest.fn();
+ const wrapper = shallowRender({ onChange, values: ['a'] });
+ wrapper.find('FacetBox').prop<Function>('onClear')();
+ expect(onChange).toHaveBeenCalledWith({ foo: [] });
+});
+
+it('should search', async () => {
+ const onSearch = jest.fn().mockResolvedValue({
+ results: ['d', 'e'],
+ paging: { pageIndex: 1, pageSize: 2, total: 3 },
+ });
+
+ const loadSearchResultCount = jest.fn().mockResolvedValue({ d: 7, e: 3 });
+ const wrapper = shallowRender({ loadSearchResultCount, onSearch });
+
+ // search
+ wrapper.find('InputSearch').prop<Function>('onChange')('query');
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+ expect(onSearch).toHaveBeenLastCalledWith('query');
+ expect(loadSearchResultCount).toHaveBeenLastCalledWith(['d', 'e']);
+
+ // load more results
+ onSearch.mockResolvedValue({
+ results: ['f'],
+ paging: { pageIndex: 2, pageSize: 2, total: 3 },
+ });
+
+ loadSearchResultCount.mockResolvedValue({ f: 5 });
+ wrapper.find('ListFooter').prop<Function>('loadMore')();
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+ expect(onSearch).toHaveBeenLastCalledWith('query', 2);
+
+ // clear search
+ onSearch.mockClear();
+ loadSearchResultCount.mockClear();
+ wrapper.find('InputSearch').prop<Function>('onChange')('');
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+ expect(onSearch).not.toHaveBeenCalled();
+ expect(loadSearchResultCount).not.toHaveBeenCalled();
+
+ // search for no results
+ onSearch.mockResolvedValue({ results: [], paging: { pageIndex: 1, pageSize: 2, total: 0 } });
+ wrapper.find('InputSearch').prop<Function>('onChange')('blabla');
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+ expect(onSearch).toHaveBeenLastCalledWith('blabla');
+ expect(loadSearchResultCount).not.toHaveBeenCalled();
+
+ // search fails
+ onSearch.mockRejectedValue(undefined);
+ wrapper.find('InputSearch').prop<Function>('onChange')('blabla');
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot(); // should render previous results
+ expect(onSearch).toHaveBeenLastCalledWith('blabla');
+ expect(loadSearchResultCount).not.toHaveBeenCalled();
+});
+
+it('should limit the number of items', () => {
+ const wrapper = shallowRender({ maxInitialItems: 2, maxItems: 5 });
+ expect(wrapper.find('FacetItem').length).toBe(2);
+
+ wrapper.find('ListStyleFacetFooter').prop<Function>('showMore')();
+ wrapper.update();
+ expect(wrapper.find('FacetItem').length).toBe(3);
+
+ wrapper.find('ListStyleFacetFooter').prop<Function>('showLess')();
+ wrapper.update();
+ expect(wrapper.find('FacetItem').length).toBe(2);
+});
+
+it('should show warning that there might be more results', () => {
+ const wrapper = shallowRender({ maxInitialItems: 2, maxItems: 3 });
+ wrapper.find('ListStyleFacetFooter').prop<Function>('showMore')();
+ wrapper.update();
+ expect(wrapper.find('FlagMessage').exists()).toBe(true);
+});
+
+// eslint-disable-next-line jest/expect-expect
+it('should reset state when closes', () => {
+ const wrapper = shallowRender();
+
+ wrapper.setState({
+ query: 'foobar',
+ searchResults: ['foo', 'bar'],
+ searching: true,
+ showFullList: true,
+ });
+
+ wrapper.setProps({ open: false });
+ checkInitialState(wrapper);
+});
+
+// eslint-disable-next-line jest/expect-expect
+it('should reset search when query changes', () => {
+ const wrapper = shallowRender({ query: { a: ['foo'] } });
+ wrapper.setState({ query: 'foo', searchResults: ['foo'], searchResultsCounts: { foo: 3 } });
+ wrapper.setProps({ query: { a: ['foo'], b: ['bar'] } });
+ checkInitialState(wrapper);
+});
+
+it('should collapse list when new stats have few results', () => {
+ const wrapper = shallowRender({ maxInitialItems: 2, maxItems: 3 });
+ wrapper.setState({ showFullList: true });
+
+ wrapper.setProps({ stats: { d: 1 } });
+ expect(wrapper.state('showFullList')).toBe(false);
+});
+
+it('should display all selected items', () => {
+ const wrapper = shallowRender({
+ maxInitialItems: 2,
+ stats: { a: 10, b: 5, c: 3 },
+ values: ['a', 'b', 'c'],
+ });
+
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should be disabled', () => {
+ const wrapper = shallowRender({ disabled: true });
+ expect(wrapper).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<Props<string>> = {}) {
+ return shallow(
+ <ListStyleFacet
+ facetHeader="facet header"
+ fetching={false}
+ onChange={jest.fn()}
+ onSearch={jest.fn()}
+ onToggle={jest.fn()}
+ open={true}
+ property="foo"
+ searchPlaceholder="search for foo..."
+ stats={{ a: 10, b: 8, c: 1 }}
+ values={[]}
+ {...props}
+ />
+ );
+}
+
+function checkInitialState(wrapper: ShallowWrapper) {
+ expect(wrapper.state('query')).toBe('');
+ expect(wrapper.state('searchResults')).toBeUndefined();
+ expect(wrapper.state('searching')).toBe(false);
+ expect(wrapper.state('searchResultsCounts')).toEqual({});
+ expect(wrapper.state('showFullList')).toBe(false);
+}
--- /dev/null
+/*
+ * 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 { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { ListStyleFacetFooter, Props } from '../ListStyleFacetFooter';
+
+it('should render "show more", not "show less"', async () => {
+ const showMore = jest.fn();
+
+ render({
+ nbShown: 7,
+ showLessAriaLabel: 'show less',
+ showMore,
+ showMoreAriaLabel: 'show more',
+ total: 42,
+ });
+
+ expect(screen.getByText('x_show.7')).toBeInTheDocument();
+ expect(screen.getByText('show_more')).toBeInTheDocument();
+ expect(screen.getByLabelText('show more')).toBeInTheDocument();
+ expect(screen.queryByText('show_less')).not.toBeInTheDocument();
+ expect(screen.queryByLabelText('show less')).not.toBeInTheDocument();
+
+ await userEvent.click(screen.getByLabelText('show more'));
+
+ expect(showMore).toHaveBeenCalled();
+});
+
+it('should render neither "show more" nor "show less"', () => {
+ render({ nbShown: 42, total: 42 });
+
+ expect(screen.getByText('x_show.42')).toBeInTheDocument();
+ expect(screen.queryByText('show_more')).not.toBeInTheDocument();
+ expect(screen.queryByText('show_less')).not.toBeInTheDocument();
+});
+
+it('should render "show less", not "show more"', async () => {
+ const showLess = jest.fn();
+
+ render({
+ nbShown: 42,
+ showLess,
+ showLessAriaLabel: 'show less',
+ showMoreAriaLabel: 'show more',
+ total: 42,
+ });
+
+ expect(screen.getByText('x_show.42')).toBeInTheDocument();
+ expect(screen.queryByText('show_more')).not.toBeInTheDocument();
+ expect(screen.queryByLabelText('show more')).not.toBeInTheDocument();
+ expect(screen.getByText('show_less')).toBeInTheDocument();
+ expect(screen.getByLabelText('show less')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByLabelText('show less'));
+
+ expect(showLess).toHaveBeenCalled();
+});
+
+function render(props: Partial<Props> = {}) {
+ return renderComponent(
+ <ListStyleFacetFooter nbShown={1} showMore={jest.fn()} total={42} {...props} />
+ );
+}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { screen } from '@testing-library/react';
import * as React from 'react';
import { mockComponent } from '../../../../helpers/mocks/component';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import { ComponentQualifier } from '../../../../types/component';
import { GlobalSettingKeys } from '../../../../types/settings';
-import { Sidebar } from '../Sidebar';
+import { SidebarClass as Sidebar } from '../Sidebar';
it('should render correct facets for Application', () => {
renderSidebar({ component: mockComponent({ qualifier: ComponentQualifier.Application }) });
+
expect(screen.getAllByRole('button').map((button) => button.textContent)).toStrictEqual([
'issues.facet.types',
'issues.facet.severities',
'issues.facet.tags',
'issues.facet.projects',
'issues.facet.assignees',
- 'clear',
+ '',
'issues.facet.authors',
]);
});
it('should render correct facets for Portfolio', () => {
renderSidebar({ component: mockComponent({ qualifier: ComponentQualifier.Portfolio }) });
+
expect(screen.getAllByRole('button').map((button) => button.textContent)).toStrictEqual([
'issues.facet.types',
'issues.facet.severities',
'issues.facet.tags',
'issues.facet.projects',
'issues.facet.assignees',
- 'clear',
+ '',
'issues.facet.authors',
]);
});
it('should render correct facets for SubPortfolio', () => {
renderSidebar({ component: mockComponent({ qualifier: ComponentQualifier.SubPortfolio }) });
+
expect(screen.getAllByRole('button').map((button) => button.textContent)).toStrictEqual([
'issues.facet.types',
'issues.facet.severities',
'issues.facet.tags',
'issues.facet.projects',
'issues.facet.assignees',
- 'clear',
+ '',
'issues.facet.authors',
]);
});
month: 'issues.facet.createdAt.last_month',
year: 'issues.facet.createdAt.last_year',
}[name] as string;
+
expect(screen.getByText(text)).toBeInTheDocument();
});
myIssues={false}
onFacetToggle={jest.fn()}
onFilterChange={jest.fn()}
- openFacets={{}}
+ openFacets={{ createdAt: true }}
showVariantsFilter={false}
query={mockQuery()}
referencedComponentsById={{}}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should be disabled 1`] = `
+<FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ clearIconLabel="clear"
+ count={0}
+ countLabel="x_selected.0"
+ disabled={true}
+ id="facet_foo"
+ loading={false}
+ name="facet header"
+ onClear={[Function]}
+ open={false}
+/>
+`;
+
+exports[`should display all selected items 1`] = `
+<FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ clearIconLabel="clear"
+ count={3}
+ countLabel="x_selected.3"
+ id="facet_foo"
+ loading={false}
+ name="facet header"
+ onClear={[Function]}
+ onClick={[Function]}
+ open={true}
+>
+ <span
+ className="it__search-navigator-facet-list"
+ >
+ <InputSearch
+ autoFocus={false}
+ className="it__search-box-input sw-mb-4 sw-w-full"
+ clearIconAriaLabel="clear"
+ onChange={[Function]}
+ placeholder="search for foo..."
+ searchInputAriaLabel="search_verb"
+ size="auto"
+ value=""
+ />
+ <FacetItemsList
+ labelledby="facet_foo"
+ >
+ <FacetItem
+ active={true}
+ className="it__search-navigator-facet"
+ key="a"
+ name="a"
+ onClick={[Function]}
+ stat="10"
+ tooltip="a"
+ value="a"
+ />
+ <FacetItem
+ active={true}
+ className="it__search-navigator-facet"
+ key="b"
+ name="b"
+ onClick={[Function]}
+ stat="5"
+ tooltip="b"
+ value="b"
+ />
+ </FacetItemsList>
+ <div
+ className="note spacer-bottom text-center"
+ >
+ ⋯
+ </div>
+ <FacetItemsList
+ labelledby="facet_foo"
+ >
+ <FacetItem
+ active={true}
+ className="it__search-navigator-facet"
+ key="c"
+ name="c"
+ onClick={[Function]}
+ stat="3"
+ tooltip="c"
+ value="c"
+ />
+ </FacetItemsList>
+ <ListStyleFacetFooter
+ nbShown={3}
+ showMore={[Function]}
+ total={3}
+ />
+ <MultipleSelectionHint
+ nbSelectableItems={3}
+ nbSelectedItems={3}
+ />
+ </span>
+</FacetBox>
+`;
+
+exports[`should render 1`] = `
+<FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ clearIconLabel="clear"
+ count={0}
+ countLabel="x_selected.0"
+ id="facet_foo"
+ loading={false}
+ name="facet header"
+ onClear={[Function]}
+ onClick={[Function]}
+ open={true}
+>
+ <span
+ className="it__search-navigator-facet-list"
+ >
+ <InputSearch
+ autoFocus={false}
+ className="it__search-box-input sw-mb-4 sw-w-full"
+ clearIconAriaLabel="clear"
+ onChange={[Function]}
+ placeholder="search for foo..."
+ searchInputAriaLabel="search_verb"
+ size="auto"
+ value=""
+ />
+ <FacetItemsList
+ labelledby="facet_foo"
+ >
+ <FacetItem
+ active={false}
+ className="it__search-navigator-facet"
+ key="a"
+ name="a"
+ onClick={[Function]}
+ stat="10"
+ tooltip="a"
+ value="a"
+ />
+ <FacetItem
+ active={false}
+ className="it__search-navigator-facet"
+ key="b"
+ name="b"
+ onClick={[Function]}
+ stat="8"
+ tooltip="b"
+ value="b"
+ />
+ <FacetItem
+ active={false}
+ className="it__search-navigator-facet"
+ key="c"
+ name="c"
+ onClick={[Function]}
+ stat="1"
+ tooltip="c"
+ value="c"
+ />
+ </FacetItemsList>
+ <ListStyleFacetFooter
+ nbShown={3}
+ showMore={[Function]}
+ total={3}
+ />
+ <MultipleSelectionHint
+ nbSelectableItems={3}
+ nbSelectedItems={0}
+ />
+ </span>
+</FacetBox>
+`;
+
+exports[`should search 1`] = `
+<FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ clearIconLabel="clear"
+ count={0}
+ countLabel="x_selected.0"
+ id="facet_foo"
+ loading={false}
+ name="facet header"
+ onClear={[Function]}
+ onClick={[Function]}
+ open={true}
+>
+ <span
+ className="it__search-navigator-facet-list"
+ >
+ <InputSearch
+ autoFocus={false}
+ className="it__search-box-input sw-mb-4 sw-w-full"
+ clearIconAriaLabel="clear"
+ onChange={[Function]}
+ placeholder="search for foo..."
+ searchInputAriaLabel="search_verb"
+ size="auto"
+ value="query"
+ />
+ <FacetItemsList
+ labelledby="facet_foo"
+ >
+ <FacetItem
+ active={false}
+ className="it__search-navigator-facet"
+ key="d"
+ name="d"
+ onClick={[Function]}
+ stat="7"
+ tooltip="d"
+ value="d"
+ />
+ <FacetItem
+ active={false}
+ className="it__search-navigator-facet"
+ key="e"
+ name="e"
+ onClick={[Function]}
+ stat="3"
+ tooltip="e"
+ value="e"
+ />
+ </FacetItemsList>
+ <ListFooter
+ className="sw-mb-2"
+ count={2}
+ loadMore={[Function]}
+ ready={true}
+ total={3}
+ useMIUIButtons={true}
+ />
+ <MultipleSelectionHint
+ nbSelectableItems={3}
+ nbSelectedItems={0}
+ />
+ </span>
+</FacetBox>
+`;
+
+exports[`should search 2`] = `
+<FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ clearIconLabel="clear"
+ count={0}
+ countLabel="x_selected.0"
+ id="facet_foo"
+ loading={false}
+ name="facet header"
+ onClear={[Function]}
+ onClick={[Function]}
+ open={true}
+>
+ <span
+ className="it__search-navigator-facet-list"
+ >
+ <InputSearch
+ autoFocus={false}
+ className="it__search-box-input sw-mb-4 sw-w-full"
+ clearIconAriaLabel="clear"
+ onChange={[Function]}
+ placeholder="search for foo..."
+ searchInputAriaLabel="search_verb"
+ size="auto"
+ value="query"
+ />
+ <FacetItemsList
+ labelledby="facet_foo"
+ >
+ <FacetItem
+ active={false}
+ className="it__search-navigator-facet"
+ key="d"
+ name="d"
+ onClick={[Function]}
+ stat="7"
+ tooltip="d"
+ value="d"
+ />
+ <FacetItem
+ active={false}
+ className="it__search-navigator-facet"
+ key="e"
+ name="e"
+ onClick={[Function]}
+ stat="3"
+ tooltip="e"
+ value="e"
+ />
+ <FacetItem
+ active={false}
+ className="it__search-navigator-facet"
+ key="f"
+ name="f"
+ onClick={[Function]}
+ stat="5"
+ tooltip="f"
+ value="f"
+ />
+ </FacetItemsList>
+ <ListFooter
+ className="sw-mb-2"
+ count={3}
+ loadMore={[Function]}
+ ready={true}
+ total={3}
+ useMIUIButtons={true}
+ />
+ <MultipleSelectionHint
+ nbSelectableItems={3}
+ nbSelectedItems={0}
+ />
+ </span>
+</FacetBox>
+`;
+
+exports[`should search 3`] = `
+<FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ clearIconLabel="clear"
+ count={0}
+ countLabel="x_selected.0"
+ id="facet_foo"
+ loading={false}
+ name="facet header"
+ onClear={[Function]}
+ onClick={[Function]}
+ open={true}
+>
+ <span
+ className="it__search-navigator-facet-list"
+ >
+ <InputSearch
+ autoFocus={false}
+ className="it__search-box-input sw-mb-4 sw-w-full"
+ clearIconAriaLabel="clear"
+ onChange={[Function]}
+ placeholder="search for foo..."
+ searchInputAriaLabel="search_verb"
+ size="auto"
+ value=""
+ />
+ <FacetItemsList
+ labelledby="facet_foo"
+ >
+ <FacetItem
+ active={false}
+ className="it__search-navigator-facet"
+ key="a"
+ name="a"
+ onClick={[Function]}
+ stat="10"
+ tooltip="a"
+ value="a"
+ />
+ <FacetItem
+ active={false}
+ className="it__search-navigator-facet"
+ key="b"
+ name="b"
+ onClick={[Function]}
+ stat="8"
+ tooltip="b"
+ value="b"
+ />
+ <FacetItem
+ active={false}
+ className="it__search-navigator-facet"
+ key="c"
+ name="c"
+ onClick={[Function]}
+ stat="1"
+ tooltip="c"
+ value="c"
+ />
+ </FacetItemsList>
+ <ListStyleFacetFooter
+ nbShown={3}
+ showMore={[Function]}
+ total={3}
+ />
+ <MultipleSelectionHint
+ nbSelectableItems={3}
+ nbSelectedItems={0}
+ />
+ </span>
+</FacetBox>
+`;
+
+exports[`should search 4`] = `
+<FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ clearIconLabel="clear"
+ count={0}
+ countLabel="x_selected.0"
+ id="facet_foo"
+ loading={false}
+ name="facet header"
+ onClear={[Function]}
+ onClick={[Function]}
+ open={true}
+>
+ <span
+ className="it__search-navigator-facet-list"
+ >
+ <InputSearch
+ autoFocus={false}
+ className="it__search-box-input sw-mb-4 sw-w-full"
+ clearIconAriaLabel="clear"
+ onChange={[Function]}
+ placeholder="search for foo..."
+ searchInputAriaLabel="search_verb"
+ size="auto"
+ value="blabla"
+ />
+ <div
+ className="note spacer-bottom"
+ >
+ no_results
+ </div>
+ <MultipleSelectionHint
+ nbSelectableItems={3}
+ nbSelectedItems={0}
+ />
+ </span>
+</FacetBox>
+`;
+
+exports[`should search 5`] = `
+<FacetBox
+ className="it__search-navigator-facet-box it__search-navigator-facet-header"
+ clearIconLabel="clear"
+ count={0}
+ countLabel="x_selected.0"
+ id="facet_foo"
+ loading={false}
+ name="facet header"
+ onClear={[Function]}
+ onClick={[Function]}
+ open={true}
+>
+ <span
+ className="it__search-navigator-facet-list"
+ >
+ <InputSearch
+ autoFocus={false}
+ className="it__search-box-input sw-mb-4 sw-w-full"
+ clearIconAriaLabel="clear"
+ onChange={[Function]}
+ placeholder="search for foo..."
+ searchInputAriaLabel="search_verb"
+ size="auto"
+ value="blabla"
+ />
+ <div
+ className="note spacer-bottom"
+ >
+ no_results
+ </div>
+ <MultipleSelectionHint
+ nbSelectableItems={3}
+ nbSelectedItems={0}
+ />
+ </span>
+</FacetBox>
+`;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
-.not-all-issue-warning {
- padding: 16px 16px 0;
- width: 100%;
- box-sizing: border-box;
-}
-
.not-all-issue-warning.open-issue-list {
+ background-color: var(--barBackgroundColor);
+ box-sizing: border-box;
+ display: inline-block;
+ padding: 16px 16px 0;
position: sticky;
top: 0;
z-index: 1000;
- background-color: var(--barBackgroundColor);
- display: inline-block;
}
.concise-issue-box .issue-message-highlight-CODE {
*/
import { waitFor } from '@testing-library/react';
import React from 'react';
-import { byLabelText, byRole } from 'testing-library-selector';
+import { byLabelText, byPlaceholderText, byRole, byTestId } from 'testing-library-selector';
import ComponentsServiceMock from '../../api/mocks/ComponentsServiceMock';
import IssuesServiceMock from '../../api/mocks/IssuesServiceMock';
import { mockComponent } from '../../helpers/mocks/component';
issueItem8: byRole('region', { name: 'Issue on page 2' }),
projectIssueItem6: byRole('button', { name: 'Second issue', exact: false }),
- clearIssueTypeFacet: byRole('button', { name: 'clear_x_filter.issues.facet.types' }),
- codeSmellIssueTypeFilter: byRole('checkbox', { name: 'issue.type.CODE_SMELL' }),
- vulnerabilityIssueTypeFilter: byRole('checkbox', { name: 'issue.type.VULNERABILITY' }),
- clearSeverityFacet: byRole('button', { name: 'clear_x_filter.issues.facet.severities' }),
- majorSeverityFilter: byRole('checkbox', { name: 'severity.MAJOR' }),
- scopeFacet: byRole('button', { name: 'issues.facet.scopes' }),
- clearScopeFacet: byRole('button', { name: 'clear_x_filter.issues.facet.scopes' }),
- mainScopeFilter: byRole('checkbox', { name: 'issue.scope.MAIN' }),
- resolutionFacet: byRole('button', { name: 'issues.facet.resolutions' }),
- clearResolutionFacet: byRole('button', { name: 'clear_x_filter.issues.facet.resolutions' }),
- fixedResolutionFilter: byRole('checkbox', { name: 'issue.resolution.FIXED' }),
- statusFacet: byRole('button', { name: 'issues.facet.statuses' }),
+ assigneeFacet: byRole('button', { name: 'issues.facet.assignees' }),
+ authorFacet: byRole('button', { name: 'issues.facet.authors' }),
+ codeVariantsFacet: byRole('button', { name: 'issues.facet.codeVariants' }),
creationDateFacet: byRole('button', { name: 'issues.facet.createdAt' }),
- clearCreationDateFacet: byRole('button', { name: 'clear_x_filter.issues.facet.createdAt' }),
- clearStatusFacet: byRole('button', { name: 'clear_x_filter.issues.facet.statuses' }),
- openStatusFilter: byRole('checkbox', { name: 'issue.status.OPEN' }),
- confirmedStatusFilter: byRole('checkbox', { name: 'issue.status.CONFIRMED' }),
languageFacet: byRole('button', { name: 'issues.facet.languages' }),
+ projectFacet: byRole('button', { name: 'issues.facet.projects' }),
+ resolutionFacet: byRole('button', { name: 'issues.facet.resolutions' }),
ruleFacet: byRole('button', { name: 'issues.facet.rules' }),
- clearRuleFacet: byRole('button', { name: 'clear_x_filter.issues.facet.rules' }),
+ scopeFacet: byRole('button', { name: 'issues.facet.scopes' }),
+ statusFacet: byRole('button', { name: 'issues.facet.statuses' }),
tagFacet: byRole('button', { name: 'issues.facet.tags' }),
- clearTagFacet: byRole('button', { name: 'clear_x_filter.issues.facet.tags' }),
- projectFacet: byRole('button', { name: 'issues.facet.projects' }),
- clearProjectFacet: byRole('button', { name: 'clear_x_filter.issues.facet.projects' }),
- assigneeFacet: byRole('button', { name: 'issues.facet.assignees' }),
- codeVariantsFacet: byRole('button', { name: 'issues.facet.codeVariants' }),
- clearAssigneeFacet: byRole('button', { name: 'clear_x_filter.issues.facet.assignees' }),
- authorFacet: byRole('button', { name: 'issues.facet.authors' }),
- clearAuthorFacet: byRole('button', { name: 'clear_x_filter.issues.facet.authors' }),
- clearCodeVariantsFacet: byRole('button', { name: 'clear_x_filter.issues.facet.codeVariants' }),
- dateInputMonthSelect: byRole('combobox', { name: 'Month:' }),
- dateInputYearSelect: byRole('combobox', { name: 'Year:' }),
+ clearAssigneeFacet: byTestId('clear-issues.facet.assignees'),
+ clearAuthorFacet: byTestId('clear-issues.facet.authors'),
+ clearCodeVariantsFacet: byTestId('clear-issues.facet.codeVariants'),
+ clearCreationDateFacet: byTestId('clear-issues.facet.createdAt'),
+ clearIssueTypeFacet: byTestId('clear-issues.facet.types'),
+ clearProjectFacet: byTestId('clear-issues.facet.projects'),
+ clearResolutionFacet: byTestId('clear-issues.facet.resolutions'),
+ clearRuleFacet: byTestId('clear-issues.facet.rules'),
+ clearScopeFacet: byTestId('clear-issues.facet.scopes'),
+ clearSeverityFacet: byTestId('clear-issues.facet.severities'),
+ clearStatusFacet: byTestId('clear-issues.facet.statuses'),
+ clearTagFacet: byTestId('clear-issues.facet.tags'),
+
+ codeSmellIssueTypeFilter: byRole('checkbox', { name: 'issue.type.CODE_SMELL' }),
+ confirmedStatusFilter: byRole('checkbox', { name: 'issue.status.CONFIRMED' }),
+ fixedResolutionFilter: byRole('checkbox', { name: 'issue.resolution.FIXED' }),
+ mainScopeFilter: byRole('checkbox', { name: 'issue.scope.MAIN' }),
+ majorSeverityFilter: byRole('checkbox', { name: 'severity.MAJOR' }),
+ openStatusFilter: byRole('checkbox', { name: 'issue.status.OPEN' }),
+ vulnerabilityIssueTypeFilter: byRole('checkbox', { name: 'issue.type.VULNERABILITY' }),
clearAllFilters: byRole('button', { name: 'clear_all_filters' }),
- ruleFacetList: byRole('list', { name: 'issues.facet.rules' }),
- languageFacetList: byRole('list', { name: 'issues.facet.languages' }),
- ruleFacetSearch: byRole('searchbox', { name: 'search.search_for_rules' }),
+ dateInputMonthSelect: byTestId('month-select'),
+ dateInputYearSelect: byTestId('year-select'),
+
+ authorFacetSearch: byPlaceholderText('search.search_for_authors'),
inNewCodeFilter: byRole('checkbox', { name: 'issues.new_code' }),
+ languageFacetList: byRole('list', { name: 'issues.facet.languages' }),
+ ruleFacetList: byRole('list', { name: 'issues.facet.rules' }),
+ ruleFacetSearch: byPlaceholderText('search.search_for_rules'),
+ tagFacetSearch: byPlaceholderText('search.search_for_tags'),
};
export async function waitOnDataLoaded() {
* 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 * as React from 'react';
export default class FacetItem extends React.PureComponent<Props> {
static defaultProps = {
halfWidth: false,
- loading: false,
};
handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
render() {
const { name, halfWidth, active, value, tooltip } = this.props;
+
const className = classNames('search-navigator-facet button-link', this.props.className, {
active,
});
active={true}
halfWidth={false}
key="a"
- loading={false}
name="a"
onClick={[Function]}
stat="10"
active={true}
halfWidth={false}
key="b"
- loading={false}
name="b"
onClick={[Function]}
stat="5"
active={true}
halfWidth={false}
key="c"
- loading={false}
name="c"
onClick={[Function]}
stat="3"
active={false}
halfWidth={false}
key="a"
- loading={false}
name="a"
onClick={[Function]}
stat="10"
active={false}
halfWidth={false}
key="b"
- loading={false}
name="b"
onClick={[Function]}
stat="8"
active={false}
halfWidth={false}
key="c"
- loading={false}
name="c"
onClick={[Function]}
stat="1"
active={false}
halfWidth={false}
key="d"
- loading={false}
name="d"
onClick={[Function]}
stat="7"
active={false}
halfWidth={false}
key="e"
- loading={false}
name="e"
onClick={[Function]}
stat="3"
active={false}
halfWidth={false}
key="d"
- loading={false}
name="d"
onClick={[Function]}
stat="7"
active={false}
halfWidth={false}
key="e"
- loading={false}
name="e"
onClick={[Function]}
stat="3"
active={false}
halfWidth={false}
key="f"
- loading={false}
name="f"
onClick={[Function]}
stat="5"
active={false}
halfWidth={false}
key="a"
- loading={false}
name="a"
onClick={[Function]}
stat="10"
active={false}
halfWidth={false}
key="b"
- loading={false}
name="b"
onClick={[Function]}
stat="8"
active={false}
halfWidth={false}
key="c"
- loading={false}
name="c"
onClick={[Function]}
stat="1"
padding: 0 10px 16px;
}
-.search-navigator-date-facet-selection {
- position: relative;
- padding-left: var(--gridSize);
- font-size: var(--smallFontSize);
-}
-
-.search-navigator-date-facet-selection:before,
-.search-navigator-date-facet-selection:after {
- display: table;
- content: '';
- line-height: 0;
-}
-
-.search-navigator-date-facet-selection:after {
- clear: both;
-}
-
-.search-navigator-date-facet-selection .date-input-control-input {
- width: 115px !important;
-}
-
-.search-navigator-date-facet-selection-dropdown-left {
- float: left;
- border-bottom: none;
-}
-
-.search-navigator-date-facet-selection-dropdown-right {
- float: right;
- border-bottom: none;
-}
-
-.search-navigator-date-facet-selection-input-left {
- position: absolute;
- left: 0;
- width: 100px;
- visibility: hidden;
-}
-
-.search-navigator-date-facet-selection-input-right {
- position: absolute;
- right: 0;
- width: 100px;
- visibility: hidden;
-}
-
.search-navigator-filters {
position: relative;
padding: 5px 10px;
import { colors } from '../app/theme';
import { AlmKeys } from '../types/alm-settings';
import { ComponentQualifier } from '../types/component';
-import { IssueScope, IssueSeverity, IssueType } from '../types/issues';
+import { IssueResolution, IssueScope, IssueSeverity, IssueType } from '../types/issues';
import { RuleType } from '../types/types';
export const SEVERITIES = Object.values(IssueSeverity);
-export const STATUSES = ['OPEN', 'REOPENED', 'CONFIRMED', 'RESOLVED', 'CLOSED'];
+
+export const STATUSES = ['OPEN', 'CONFIRMED', 'REOPENED', 'RESOLVED', 'CLOSED'];
+
export const ISSUE_TYPES: IssueType[] = [
IssueType.Bug,
IssueType.Vulnerability,
IssueType.CodeSmell,
IssueType.SecurityHotspot,
];
+
+export const RESOLUTIONS = [
+ IssueResolution.Unresolved,
+ IssueResolution.FalsePositive,
+ IssueResolution.Fixed,
+ IssueResolution.Removed,
+ IssueResolution.WontFix,
+];
+
export const SOURCE_SCOPES = [
{ scope: IssueScope.Main, qualifier: ComponentQualifier.File },
{ scope: IssueScope.Test, qualifier: ComponentQualifier.TestFile },
];
+
export const RULE_TYPES: RuleType[] = ['BUG', 'VULNERABILITY', 'CODE_SMELL', 'SECURITY_HOTSPOT'];
+
export const RULE_STATUSES = ['READY', 'BETA', 'DEPRECATED'];
export const RATING_COLORS = [
};
}
+export function mockIssueAuthors(overrides: string[] = []): string[] {
+ return [
+ 'email1@sonarsource.com',
+ 'email2@sonarsource.com',
+ 'email3@sonarsource.com',
+ 'email4@sonarsource.com',
+ ...overrides,
+ ];
+}
+
export function mockIssueChangelog(overrides: Partial<IssueChangelog> = {}): IssueChangelog {
return {
creationDate: '2018-10-01',
'line-height': theme('fontSize').sm[1],
'font-weight': theme('fontWeight.regular'),
},
+ '.body-xs': {
+ 'font-family': theme('fontFamily.sans'),
+ 'font-size': theme('fontSize.xs'),
+ 'line-height': theme('fontSize').xs[1],
+ 'font-weight': theme('fontWeight.regular'),
+ },
'.body-sm-highlight': {
'font-family': theme('fontFamily.sans'),
'font-size': theme('fontSize.sm'),
// Define font sizes
fontSize: {
code: ['0.875rem', '1.125rem'], // 14px / 18px
+ xs: ['0.75rem', '1rem'], // 12px / 16px
sm: ['0.875rem', '1.25rem'], // 14px / 20px
base: ['1rem', '1.5rem'], // 16px / 24px
md: ['1.313rem', '1.75rem'], // 21px / 28px
3: '3',
4: '4',
},
- // No responsive breakpoint for the webapp
- screens: {},
+ screens: {
+ sm: '1280px',
+ lg: '1920px',
+ },
// Defined spacing values based on our grid size
spacing: {
0: '0',
2: '0.5rem', // 8px
3: '0.75rem', // 12px
4: '1rem', // 16px
+ 5: '1.25rem', // 20px
6: '1.5rem', // 24px
7: '1.75rem', // 28px
8: '2rem', // 32px
never=Never
new=New
new_name=New name
+next_=next
none=None
no_tags=No tags
not_now=Not now
path=Path
permalink=Permanent Link
plugin=Plugin
+previous_=previous
project=Project
project_x=Project: {0}
projects=Projects