import { CustomIcon } from './icons/Icon';
interface Props {
- ariaLabel?: string;
checked: boolean;
children?: React.ReactNode;
className?: string;
disabled?: boolean;
id?: string;
+ label?: string;
loading?: boolean;
onCheck: (checked: boolean, id?: string) => void;
onClick?: (event: React.MouseEvent<HTMLInputElement>) => void;
}
export function Checkbox({
- ariaLabel,
checked,
disabled,
children,
className,
id,
+ label,
loading = false,
onCheck,
onFocus,
<CheckboxContainer className={className} disabled={disabled}>
{right && children}
<AccessibleCheckbox
- aria-label={ariaLabel ?? title}
+ aria-label={label ?? title}
checked={checked}
disabled={disabled ?? loading}
id={id}
<Tooltip overlay={color.overlay}>
<div>
<Checkbox
- ariaLabel={color.ariaLabel}
checked={color.selected}
+ label={color.ariaLabel}
onCheck={() => {
props.onColorClick(color);
}}
interface Props<V> {
className?: string;
+ components?: any;
customValue?: JSX.Element;
- options: LabelValueSelectOption<V>[];
+ isDisabled?: boolean;
+ menuIsOpen?: boolean;
+ onMenuClose?: () => void;
+ onMenuOpen?: () => void;
+ options: Array<LabelValueSelectOption<V>>;
setValue: ({ value }: LabelValueSelectOption<V>) => void;
size?: InputSizeKeys;
value: V;
export function DiscreetSelect<V>({
className,
customValue,
+ onMenuOpen,
options,
size = 'small',
setValue,
<StyledSelect
className={className}
onChange={setValue}
+ onMenuOpen={onMenuOpen}
options={options}
placeholder={customValue}
size={size}
& .react-select__control {
height: auto;
+ min-height: inherit;
color: ${themeContrast('discreetBackground')};
background: none;
outline: inherit;
color: ${themeColor('discreetButtonHover')};
background: ${themeColor('discreetBackground')};
outline: ${themeBorder('focus', 'discreetFocusBorder')};
- outline: none;
border-color: inherit;
box-shadow: none;
}
& .react-select__control--is-focused,
& .react-select__control--menu-is-open {
${tw`sw-border-none`};
- outline: none;
}
`;
closeOnClick?: boolean;
id: string;
isPortal?: boolean;
+ onClose?: VoidFunction;
onOpen?: VoidFunction;
+ openDropdown?: boolean;
overlay: React.ReactNode;
placement?: PopupPlacement;
size?: InputSizeKeys;
export class Dropdown extends React.PureComponent<Props, State> {
state: State = { open: false };
- componentDidUpdate(_: Props, prevState: State) {
+ componentDidUpdate(props: Props, prevState: State) {
if (!prevState.open && this.state.open && this.props.onOpen) {
this.props.onOpen();
}
+ if (props.openDropdown !== this.props.openDropdown && this.props.openDropdown) {
+ this.setState({ open: this.props.openDropdown });
+ }
}
handleClose = () => {
this.setState({ open: false });
+ if (this.props.onClose) {
+ this.props.onClose();
+ }
};
handleToggleClick: OnClickCallback = (event) => {
classNames={{
container: () => 'sw-relative sw-inline-block sw-align-middle',
placeholder: () => 'sw-truncate sw-leading-4',
+ menu: () => 'sw-z-dropdown-menu',
menuList: () => 'sw-overflow-y-auto sw-py-2 sw-max-h-[12.25rem]',
control: ({ isDisabled }) =>
classNames(
...props.classNames,
}}
components={{
- ...props.components,
Option: IconOption,
SingleValue,
IndicatorsContainer,
IndicatorSeparator: null,
+ ...props.components,
}}
isSearchable={props.isSearchable ?? false}
+ onMenuOpen={props.onMenuOpen}
styles={selectStyle({ size })}
/>
);
isDisabled,
minLength,
controlAriaLabel,
+ menuIsOpen,
...rest
} = props;
const [open, setOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState('');
+ React.useEffect(() => {
+ if (menuIsOpen) {
+ setOpen(true);
+ }
+ }, [menuIsOpen]);
+
const ref = React.useRef<Select<Option, IsMulti, Group>>(null);
const toggleDropdown = React.useCallback(
className?: string;
emptyText: string;
menuId?: string;
+ onClose?: VoidFunction;
+ open?: boolean;
overlay?: React.ReactNode;
popupPlacement?: PopupPlacement;
tags: string[];
tagsToDisplay?: number;
+ tooltip?: React.ComponentType<{ overlay: React.ReactNode }>;
}
export function Tags({
popupPlacement,
tags,
tagsToDisplay = 3,
+ tooltip,
+ open,
+ onClose,
}: Props) {
const displayedTags = tags.slice(0, tagsToDisplay);
const extraTags = tags.slice(tagsToDisplay);
+ const Tooltip = tooltip || React.Fragment;
- const displayedTagsContent = () => (
- <span className="sw-inline-flex sw-items-center sw-gap-1" title={tags.join(', ')}>
- {/* Display first 3 (tagsToDisplay) tags */}
- {displayedTags.map((tag) => (
- <TagLabel key={tag}>{tag}</TagLabel>
- ))}
+ const displayedTagsContent = (open = false) => (
+ <Tooltip overlay={open ? undefined : tags.join(', ')}>
+ <span className="sw-inline-flex sw-items-center sw-gap-1" title={tags.join(', ')}>
+ {/* Display first 3 (tagsToDisplay) tags */}
+ {displayedTags.map((tag) => (
+ <TagLabel key={tag}>{tag}</TagLabel>
+ ))}
- {/* Show ellipsis if there are more tags */}
- {extraTags.length > 0 ? <TagLabel>...</TagLabel> : null}
+ {/* Show ellipsis if there are more tags */}
+ {extraTags.length > 0 ? <TagLabel>...</TagLabel> : null}
- {/* Handle no tags with its own styling */}
- {tags.length === 0 && <LightLabel>{emptyText}</LightLabel>}
- </span>
+ {/* Handle no tags with its own styling */}
+ {tags.length === 0 && <LightLabel>{emptyText}</LightLabel>}
+ </span>
+ </Tooltip>
);
return (
allowResizing
closeOnClick={false}
id={menuId}
+ onClose={onClose}
+ openDropdown={open}
overlay={overlay}
placement={popupPlacement}
zLevel={PopupZLevel.Global}
>
- {({ a11yAttrs, onToggleClick }) => (
+ {({ a11yAttrs, onToggleClick, open }) => (
<WrapperButton
className="sw-flex sw-items-center sw-gap-1 sw-p-1 sw-h-auto sw-rounded-0"
onClick={onToggleClick}
{...a11yAttrs}
>
- {displayedTagsContent()}
+ {displayedTagsContent(open)}
<TagLabel className="sw-cursor-pointer">+</TagLabel>
</WrapperButton>
)}
export * from './OutsideClickHandler';
export { QualityGateIndicator } from './QualityGateIndicator';
export * from './RadioButton';
+export * from './SearchHighlighter';
export * from './SearchSelect';
export * from './SearchSelectDropdown';
export * from './SelectionCard';
renderIssueApp();
// Select an issue with an advanced rule
- await user.click(await screen.findByRole('region', { name: 'Fix that' }));
+ await user.click(await screen.findByRole('link', { name: 'Fix that' }));
expect(screen.getByRole('tab', { name: 'issue.tabs.code' })).toBeInTheDocument();
// Are rule headers present?
await user.click(await ui.issueItem5.find());
expect(ui.projectIssueItem6.getAll()).toHaveLength(2); // there will be 2 buttons one in concise issue and other in code viewer
- await user.click(ui.projectIssueItem6.getAll()[1]);
+ await user.click(ui.issueItemAction6.get());
expect(screen.getByRole('heading', { level: 1, name: 'Second issue' })).toBeInTheDocument();
});
const issueBoxFixThat = within(screen.getByRole('region', { name: 'Fix that' }));
expect(
- issueBoxFixThat.getByRole('button', {
- name: 'issue.type.type_x_click_to_change.issue.type.CODE_SMELL',
- })
+ issueBoxFixThat.getByLabelText('issue.type.type_x_click_to_change.issue.type.CODE_SMELL')
).toBeInTheDocument();
await user.click(
await user.click(screen.getByRole('button', { name: 'apply' }));
expect(
- issueBoxFixThat.getByRole('button', {
- name: 'issue.type.type_x_click_to_change.issue.type.BUG',
- })
+ issueBoxFixThat.getByLabelText('issue.type.type_x_click_to_change.issue.type.BUG')
).toBeInTheDocument();
});
});
describe('filtering', () => {
- it('should handle filtering from a specific issue properly', async () => {
- const user = userEvent.setup();
- renderIssueApp();
- await waitOnDataLoaded();
-
- // Ensure issue type filter is unchecked
- expect(ui.codeSmellIssueTypeFilter.get()).not.toBeChecked();
- expect(ui.vulnerabilityIssueTypeFilter.get()).not.toBeChecked();
- expect(ui.issueItem1.get()).toBeInTheDocument();
- expect(ui.issueItem2.get()).toBeInTheDocument();
-
- // Open filter similar issue dropdown for issue 2 (Code smell)
- await user.click(
- await within(ui.issueItem2.get()).findByRole('button', {
- name: 'issue.filter_similar_issues',
- })
- );
- await user.click(
- await within(ui.issueItem2.get()).findByRole('button', {
- name: 'issue.type.CODE_SMELL',
- })
- );
-
- expect(ui.codeSmellIssueTypeFilter.get()).toBeChecked();
- expect(ui.vulnerabilityIssueTypeFilter.get()).not.toBeChecked();
- expect(ui.issueItem1.query()).not.toBeInTheDocument();
- expect(ui.issueItem2.get()).toBeInTheDocument();
- expect(
- screen.queryByRole('button', { name: 'issues.facet.owaspTop10_2021' })
- ).not.toBeInTheDocument();
-
- // Clear filters
- await user.click(ui.clearAllFilters.get());
-
- // Open filter similar issue dropdown for issue 3 (Vulnerability)
- await user.click(
- await within(await ui.issueItem1.find()).findByRole('button', {
- name: 'issue.filter_similar_issues',
- })
- );
- await user.click(
- await within(await ui.issueItem1.find()).findByRole('button', {
- name: 'issue.type.VULNERABILITY',
- })
- );
-
- expect(ui.codeSmellIssueTypeFilter.get()).not.toBeChecked();
- expect(ui.vulnerabilityIssueTypeFilter.get()).toBeChecked();
- expect(ui.issueItem1.get()).toBeInTheDocument();
- expect(ui.issueItem2.query()).not.toBeInTheDocument();
- // Standards should now be expanded and Owasp should be visible
- expect(screen.getByRole('button', { name: 'issues.facet.owaspTop10_2021' })).toBeVisible();
- });
-
it('should combine sidebar filters properly', async () => {
const user = userEvent.setup();
renderIssueApp();
// Change issue type
await user.click(
- listItem.getByRole('button', {
- name: `issue.type.type_x_click_to_change.issue.type.CODE_SMELL`,
- })
+ listItem.getByLabelText('issue.type.type_x_click_to_change.issue.type.CODE_SMELL')
);
expect(listItem.getByText('issue.type.BUG')).toBeInTheDocument();
expect(listItem.getByText('issue.type.VULNERABILITY')).toBeInTheDocument();
await user.click(listItem.getByText('issue.type.VULNERABILITY'));
expect(
- listItem.getByRole('button', {
- name: `issue.type.type_x_click_to_change.issue.type.VULNERABILITY`,
- })
+ listItem.getByLabelText('issue.type.type_x_click_to_change.issue.type.VULNERABILITY')
).toBeInTheDocument();
// Change issue severity
expect(listItem.getByText('severity.MAJOR')).toBeInTheDocument();
await user.click(
- listItem.getByRole('button', {
- name: `issue.severity.severity_x_click_to_change.severity.MAJOR`,
- })
+ listItem.getByLabelText('issue.severity.severity_x_click_to_change.severity.MAJOR')
);
expect(listItem.getByText('severity.MINOR')).toBeInTheDocument();
expect(listItem.getByText('severity.INFO')).toBeInTheDocument();
await user.click(listItem.getByText('severity.MINOR'));
expect(
- listItem.getByRole('button', {
- name: `issue.severity.severity_x_click_to_change.severity.MINOR`,
- })
+ listItem.getByLabelText('issue.severity.severity_x_click_to_change.severity.MINOR')
).toBeInTheDocument();
// Change issue status
await user.click(listItem.getByText('issue.transition.confirm'));
expect(
- listItem.getByRole('button', {
- name: `issue.transition.status_x_click_to_change.issue.status.CONFIRMED`,
- })
+ listItem.getByLabelText('issue.transition.status_x_click_to_change.issue.status.CONFIRMED')
).toBeInTheDocument();
// As won't fix
).not.toBeInTheDocument();
// Assign issue to a different user
+
await user.click(
- listItem.getByRole('button', {
- name: `issue.assign.unassigned_click_to_assign`,
- })
+ listItem.getByRole('combobox', { name: 'issue.assign.unassigned_click_to_assign' })
);
- await user.click(listItem.getByRole('searchbox', { name: 'search.search_for_users' }));
- await user.keyboard('luke');
- expect(listItem.getByText('Skywalker')).toBeInTheDocument();
- await user.keyboard('{ArrowUp}{enter}');
+ await user.click(screen.getByLabelText('search.search_for_users'));
+
+ await act(async () => {
+ await user.keyboard('luke');
+ });
+ expect(screen.getByText('Skywalker')).toBeInTheDocument();
+
+ await user.click(screen.getByText('Skywalker'));
+ await listItem.findByRole('combobox', {
+ name: 'issue.assign.assigned_to_x_click_to_change.luke',
+ });
expect(
- listItem.getByRole('button', {
+ listItem.getByRole('combobox', {
name: 'issue.assign.assigned_to_x_click_to_change.luke',
})
).toBeInTheDocument();
- // Add comment to the issue
- await user.click(
- listItem.getByRole('button', {
- name: `issue.comment.add_comment`,
- })
- );
- await user.keyboard('comment');
- await user.click(listItem.getByRole('button', { name: 'issue.comment.formlink' }));
- expect(listItem.getByText('comment')).toBeInTheDocument();
-
- // Cancel editing the comment
- await user.click(listItem.getByRole('button', { name: 'issue.comment.edit' }));
- await user.keyboard('New ');
- await user.click(listItem.getByRole('button', { name: 'issue.comment.edit.cancel' }));
- expect(listItem.queryByText('New comment')).not.toBeInTheDocument();
-
- // Edit the comment
- await user.click(listItem.getByRole('button', { name: 'issue.comment.edit' }));
- await user.keyboard('New ');
- await user.click(listItem.getByText('save'));
- expect(listItem.getByText('New comment')).toBeInTheDocument();
-
- // Delete the comment
- await user.click(listItem.getByRole('button', { name: 'issue.comment.delete' }));
- await user.click(listItem.getByRole('button', { name: 'delete' })); // Confirm button
- expect(listItem.queryByText('New comment')).not.toBeInTheDocument();
-
- // Add comment using keyboard
- await user.click(
- listItem.getByRole('button', {
- name: `issue.comment.add_comment`,
- })
- );
- await user.keyboard('comment');
- await user.keyboard('{Control>}{enter}{/Control}');
- expect(listItem.getByText('comment')).toBeInTheDocument();
-
- // Edit the comment using keyboard
- await user.click(listItem.getByRole('button', { name: 'issue.comment.edit' }));
- await user.keyboard('New ');
- await user.keyboard('{Control>}{enter}{/Control}');
- expect(listItem.getByText('New comment')).toBeInTheDocument();
- await user.keyboard('{Escape}');
-
// Change tags
expect(listItem.getByText('issue.no_tag')).toBeInTheDocument();
await user.click(listItem.getByText('issue.no_tag'));
expect(listItem.getByTitle('accessibility, android')).toBeInTheDocument();
// Unselect
- await user.click(screen.getByText('accessibility'));
- expect(screen.getByTitle('android')).toBeInTheDocument();
+ await user.click(screen.getByRole('checkbox', { name: 'accessibility' }));
+ expect(listItem.getByTitle('android')).toBeInTheDocument();
await user.click(screen.getByRole('searchbox', { name: 'search.search_for_tags' }));
await user.keyboard('addNewTag');
expect(
- screen.getByRole('checkbox', { name: 'create_new_element: addnewtag' })
+ screen.getByRole('checkbox', { name: 'issue.create_tag: addnewtag' })
).toBeInTheDocument();
});
})
).not.toBeInTheDocument();
- await user.click(
- screen.getByRole('button', {
- name: `issue.comment.add_comment`,
- })
- );
- expect(screen.queryByRole('button', { name: 'issue.comment.submit' })).not.toBeInTheDocument();
expect(
screen.queryByRole('button', {
name: `issue.transition.status_x_click_to_change.issue.status.OPEN`,
renderIssueApp();
// Select an issue with an advanced rule
- await user.click(await ui.issueItem5.find());
+ await user.click(await ui.issueItemAction5.find());
// open severity popup on key press 'i'
+
await user.keyboard('i');
- expect(screen.getByRole('button', { name: 'severity.MINOR' })).toBeInTheDocument();
- expect(screen.getByRole('button', { name: 'severity.INFO' })).toBeInTheDocument();
+ expect(screen.getByText('severity.MINOR')).toBeInTheDocument();
+ expect(screen.getByText('severity.INFO')).toBeInTheDocument();
// open status popup on key press 'f'
await user.keyboard('f');
await user.keyboard('{Escape}');
// open tags popup on key press 't'
- await user.keyboard('t');
- expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument();
- expect(screen.getByText('android')).toBeInTheDocument();
- expect(screen.getByText('accessibility')).toBeInTheDocument();
- // closing tags popup
- await user.click(screen.getByText('issue.no_tag'));
-
- // open assign popup on key press 'a'
- await user.keyboard('a');
- expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument();
+
+ // needs to be fixed with the new header from ambroise!
+ // await user.keyboard('t');
+ // expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument();
+ // expect(screen.getByText('android')).toBeInTheDocument();
+ // expect(screen.getByText('accessibility')).toBeInTheDocument();
+ // // closing tags popup
+ // await user.click(screen.getByText('issue.no_tag'));
+
+ // // open assign popup on key press 'a'
+ // await user.keyboard('a');
+ // expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument();
});
it('should not open the actions popup using keyboard shortcut when keyboard shortcut flag is disabled', async () => {
const user = userEvent.setup();
renderIssueApp();
- await user.click(await ui.issueItem4.find());
+ await user.click(await ui.issueItemAction4.find());
expect(screen.getByRole('link', { name: 'location 1' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'location 2' })).toBeInTheDocument();
renderIssueApp();
// Select an issue with an advanced rule
- await user.click(await ui.issueItem7.find());
+ await user.click(await ui.issueItemAction7.find());
expect(
screen.getByRole('heading', {
* 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 { Badge, themeBorder, themeColor, themeContrast } from 'design-system';
import * as React from 'react';
import BranchIcon from '../../../components/icons/BranchIcon';
-import QualifierIcon from '../../../components/icons/QualifierIcon';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { collapsePath, limitComponentName } from '../../../helpers/path';
import { ComponentQualifier, isView } from '../../../types/component';
const projectName = [issue.projectName, issue.branch].filter((s) => !!s).join(' - ');
return (
- <div
+ <DivStyled
aria-label={translateWithParameters(
'issues.on_file_x',
`${displayProject ? issue.projectName + ', ' : ''}${componentName}`
)}
- className="component-name text-ellipsis"
+ className="sw-flex sw-box-border sw-body-sm sw-w-full sw-pb-1 sw-pt-6 sw-truncate"
>
- <QualifierIcon className="spacer-right" qualifier={issue.componentQualifier} />
-
{displayProject && (
<span title={projectName}>
{limitComponentName(issue.projectName)}
<span>{issue.branch}</span>
</>
) : (
- <span className="badge">{translate('branches.main_branch')}</span>
+ <Badge variant="default">{translate('branches.main_branch')}</Badge>
)}
</>
)}
- <span className="slash-separator" />
+ <SlashSeparator className="sw-mx-1" />
</span>
)}
<span title={componentName}>{collapsePath(componentName || '')}</span>
- </div>
+ </DivStyled>
);
}
+
+const DivStyled = styled.div`
+ color: ${themeContrast('subnavigation')};
+ background-color: ${themeColor('subnavigation')};
+ &:not(:last-child) {
+ border-bottom: ${themeBorder('default')};
+ }
+`;
+
+const SlashSeparator = styled.span`
+ &:after {
+ content: '/';
+ color: rgba(68, 68, 68, 0.3);
+ }
+`;
import { setIssueAssignee } from '../../../api/issues';
import Link from '../../../components/common/Link';
import LinkIcon from '../../../components/icons/LinkIcon';
+import { IssueMessageHighlighting } from '../../../components/issue/IssueMessageHighlighting';
import { updateIssue } from '../../../components/issue/actions';
import IssueActionsBar from '../../../components/issue/components/IssueActionsBar';
import IssueChangelog from '../../../components/issue/components/IssueChangelog';
import IssueMessageTags from '../../../components/issue/components/IssueMessageTags';
-import { IssueMessageHighlighting } from '../../../components/issue/IssueMessageHighlighting';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
import { KeyboardKeys } from '../../../helpers/keycodes';
*/
import classNames from 'classnames';
-import { FlagMessage, ToggleButton } from 'design-system';
+import { ButtonSecondary, Checkbox, FlagMessage, ToggleButton } from 'design-system';
import { debounce, keyBy, omit, without } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import EmptySearch from '../../../components/common/EmptySearch';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
-import Checkbox from '../../../components/controls/Checkbox';
import ListFooter from '../../../components/controls/ListFooter';
-import { Button } from '../../../components/controls/buttons';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import withIndexationGuard from '../../../components/hoc/withIndexationGuard';
import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
title={translate('issues.select_all_issues')}
/>
- <Button
+ <ButtonSecondary
innerRef={this.bulkButtonRef}
disabled={checked.length === 0}
id="issues-bulk-change"
onClick={this.handleOpenBulkChange}
>
{this.getButtonLabel(checked, checkAll, paging)}
- </Button>
+ </ButtonSecondary>
{bulkChangeModal && (
<BulkChangeModal
}}
loading={loadingMore}
total={paging.total}
+ useMIUIButtons={true}
/>
)}
return (
<React.Fragment key={index}>
<li>
- <div className="issues-workspace-list-component note">
- <ComponentBreadcrumbs component={component} issue={issues[0]} />
- </div>
+ <ComponentBreadcrumbs component={component} issue={issues[0]} />
</li>
<ul>
{issues.map((issue) => (
* 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 { themeBorder, themeColor } from 'design-system';
import * as React from 'react';
import Issue from '../../../components/issue/Issue';
import { BranchLike } from '../../../types/branch-like';
const { branchLike, issue } = this.props;
return (
- <li className="issues-workspace-list-item" ref={(node) => (this.nodeRef = node)}>
+ <IssueItem ref={(node) => (this.nodeRef = node)}>
<Issue
branchLike={branchLike}
checked={this.props.checked}
onChange={this.props.onChange}
onCheck={this.props.onCheck}
onClick={this.props.onClick}
- onFilter={this.handleFilter}
onPopupToggle={this.props.onPopupToggle}
openPopup={this.props.openPopup}
selected={this.props.selected}
/>
- </li>
+ </IssueItem>
);
}
}
+
+const IssueItem = styled.li`
+ box-sizing: border-box;
+ border: ${themeBorder('default', 'transparent')};
+ border-top: ${themeBorder('default')};
+ outline: none;
+
+ &.selected {
+ border: ${themeBorder('default', 'tableRowSelected')};
+ }
+
+ &:hover {
+ background: ${themeColor('tableRowHover')};
+ }
+
+ &:last-child {
+ border-bottom: ${themeBorder('default')};
+ }
+`;
* 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 HomePageSelect from '../../../components/controls/HomePageSelect';
-import PageShortcutsTooltip from '../../../components/ui/PageShortcutsTooltip';
import { translate } from '../../../helpers/l10n';
import { Paging } from '../../../types/types';
import IssuesCounter from './IssuesCounter';
const { canSetHome, effortTotal, paging, selectedIndex } = props;
return (
- <div className="display-flex-center display-flex-justify-end">
- <PageShortcutsTooltip
- leftAndRightLabel={translate('issues.to_navigate')}
- upAndDownLabel={translate('issues.to_select_issues')}
- />
+ <div className="sw-body-sm sw-flex sw-gap-6 sw-justify-end">
+ <KeyboardHint title={translate('issues.to_select_issues')} command="ArrowUp ArrowDown" />
+ <KeyboardHint title={translate('issues.to_navigate')} command="ArrowLeft ArrowRight" />
- <div className="spacer-left issues-page-actions">
- {paging != null && <IssuesCounter current={selectedIndex} total={paging.total} />}
- {effortTotal !== undefined && <TotalEffort effort={effortTotal} />}
- </div>
+ {paging != null && <IssuesCounter current={selectedIndex} total={paging.total} />}
+ {effortTotal !== undefined && <TotalEffort effort={effortTotal} />}
- {canSetHome && (
- <HomePageSelect className="huge-spacer-left" currentPage={{ type: 'ISSUES' }} />
- )}
+ {canSetHome && <HomePageSelect currentPage={{ type: 'ISSUES' }} />}
</div>
);
}
export default function TotalEffort({ effort }: { effort: number }) {
return (
- <div className="display-inline-block bordered-left spacer-left">
- <div className="spacer-left">
- <FormattedMessage
- defaultMessage={translate('issue.x_effort')}
- id="issue.x_effort"
- values={{ 0: <strong>{formatMeasure(effort, 'WORK_DUR')}</strong> }}
- />
- </div>
+ <div className="sw-inline-block">
+ <FormattedMessage
+ defaultMessage={translate('issue.x_effort')}
+ id="issue.x_effort"
+ values={{ 0: <strong>{formatMeasure(effort, 'WORK_DUR')}</strong> }}
+ />
</div>
);
}
export const ui = {
loading: byLabelText('loading'),
+ issueItemAction1: byRole('link', { name: 'Issue with no location message' }),
+ issueItemAction2: byRole('link', { name: 'FlowIssue' }),
+ issueItemAction3: byRole('link', { name: 'Issue on file' }),
+ issueItemAction4: byRole('link', { name: 'Fix this' }),
+ issueItemAction5: byRole('link', { name: 'Fix that' }),
+ issueItemAction6: byRole('link', { name: 'Second issue' }),
+ issueItemAction7: byRole('link', { name: 'Issue with tags' }),
+ issueItemAction8: byRole('link', { name: 'Issue on page 2' }),
+
issueItems: byRole('region'),
issueItem1: byRole('region', { name: 'Issue with no location message' }),
component: string;
componentMeasures?: Measure[];
displayAllIssues?: boolean;
- displayIssueLocationsCount?: boolean;
- displayIssueLocationsLink?: boolean;
displayLocationMarkers?: boolean;
highlightedLine?: number;
// `undefined` elements mean they are located in a different file,
<SourceViewerCode
branchLike={this.props.branchLike}
displayAllIssues={this.props.displayAllIssues}
- displayIssueLocationsCount={this.props.displayIssueLocationsCount}
- displayIssueLocationsLink={this.props.displayIssueLocationsLink}
displayLocationMarkers={this.props.displayLocationMarkers}
duplications={this.state.duplications}
duplicationsByLine={this.state.duplicationsByLine}
interface Props {
branchLike: BranchLike | undefined;
displayAllIssues?: boolean;
- displayIssueLocationsCount?: boolean;
- displayIssueLocationsLink?: boolean;
displayLocationMarkers?: boolean;
duplications: Duplication[] | undefined;
duplicationsByLine: { [line: number]: number[] };
line={line}
openIssuesByLine={openIssuesByLine}
branchLike={this.props.branchLike}
- displayIssueLocationsCount={this.props.displayIssueLocationsCount}
- displayIssueLocationsLink={this.props.displayIssueLocationsLink}
issuePopup={this.props.issuePopup}
onIssueChange={this.props.onIssueChange}
onIssueClick={this.props.onIssueSelect}
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { act } from 'react-dom/test-utils';
-import { byRole } from 'testing-library-selector';
+import { byText } from 'testing-library-selector';
import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock';
import { HttpStatus } from '../../../helpers/request';
import SourceViewer from '../SourceViewer';
import loadIssues from '../helpers/loadIssues';
+jest.mock('../../../api/components');
+jest.mock('../../../api/issues');
+// The following 2 mocks are needed, because IssuesServiceMock mocks more than it should.
+// This should be removed once IssuesServiceMock is cleaned up.
+jest.mock('../../../api/rules');
+jest.mock('../../../api/users');
+
jest.mock('../helpers/loadIssues', () => ({
__esModule: true,
default: jest.fn().mockResolvedValue([]),
});
const ui = {
- codeSmellTypeButton: byRole('button', { name: 'issue.type.CODE_SMELL' }),
- minorSeverityButton: byRole('button', { name: /severity.MINOR/ }),
+ codeSmellTypeButton: byText('issue.type.CODE_SMELL'),
+ minorSeverityButton: byText(/severity.MINOR/),
};
const componentsHandler = new ComponentsServiceMock();
//Open Issue type
await user.click(
- await screen.findByRole('button', { name: 'issue.type.type_x_click_to_change.issue.type.BUG' })
+ await screen.findByLabelText('issue.type.type_x_click_to_change.issue.type.BUG')
);
expect(ui.codeSmellTypeButton.get()).toBeInTheDocument();
// Open severity
await user.click(
- await screen.findByRole('button', {
- name: 'issue.severity.severity_x_click_to_change.severity.MAJOR',
- })
+ await screen.findByLabelText('issue.severity.severity_x_click_to_change.severity.MAJOR')
);
expect(ui.minorSeverityButton.get()).toBeInTheDocument();
// Change the severity
await user.click(
- await screen.findByRole('button', {
- name: 'issue.severity.severity_x_click_to_change.severity.MAJOR',
- })
+ await screen.findByLabelText('issue.severity.severity_x_click_to_change.severity.MAJOR')
);
expect(ui.minorSeverityButton.get()).toBeInTheDocument();
await user.click(ui.minorSeverityButton.get());
expect(
- screen.getByRole('button', {
- name: 'issue.severity.severity_x_click_to_change.severity.MINOR',
- })
+ screen.getByLabelText('issue.severity.severity_x_click_to_change.severity.MINOR')
).toBeInTheDocument();
});
name: 'source_viewer.issues_on_line.X_issues_of_type_Y.source_viewer.issues_on_line.show.2.issue.type.BUG.plural',
})
);
- const firstIssueBox = issueRow.getByRole('region', { name: 'First Issue' });
- const secondIssueBox = issueRow.getByRole('region', { name: 'Second Issue' });
+ const firstIssueBox = issueRow.getByRole('link', { name: 'First Issue' });
+ const secondIssueBox = issueRow.getByRole('link', { name: 'Second Issue' });
expect(firstIssueBox).toBeInTheDocument();
expect(secondIssueBox).toBeInTheDocument();
expect(
branchLike={undefined}
component={componentsHandler.getNonEmptyFileKey()}
displayAllIssues
- displayIssueLocationsCount
- displayIssueLocationsLink={false}
displayLocationMarkers
onIssueChange={jest.fn()}
onIssueSelect={jest.fn()}
branchLike={undefined}
component={componentsHandler.getNonEmptyFileKey()}
displayAllIssues
- displayIssueLocationsCount
- displayIssueLocationsLink={false}
displayLocationMarkers
onIssueChange={jest.fn()}
onIssueSelect={jest.fn()}
}
}
displayAllIssues={false}
- displayIssueLocationsCount={true}
- displayIssueLocationsLink={true}
displayLocationMarkers={true}
duplicationsByLine={{}}
hasSourcesAfter={false}
*/
import * as React from 'react';
import { BranchLike } from '../../../types/branch-like';
-import { Issue as TypeIssue, LinearIssueLocation, SourceLine } from '../../../types/types';
+import { LinearIssueLocation, SourceLine, Issue as TypeIssue } from '../../../types/types';
import Issue from '../../issue/Issue';
export interface LineIssuesListProps {
branchLike: BranchLike | undefined;
displayAllIssues?: boolean;
displayWhyIsThisAnIssue: boolean;
- displayIssueLocationsCount?: boolean;
- displayIssueLocationsLink?: boolean;
issuesForLine: TypeIssue[];
issuePopup: { issue: string; name: string } | undefined;
issueLocationsByLine: { [line: number]: LinearIssueLocation[] };
<Issue
branchLike={props.branchLike}
displayWhyIsThisAnIssue={displayWhyIsThisAnIssue}
- displayLocationsCount={props.displayIssueLocationsCount}
- displayLocationsLink={props.displayIssueLocationsLink}
issue={issue}
key={issue.key}
onChange={props.onIssueChange}
import { Issue as TypeIssue } from '../../types/types';
import { updateIssue } from './actions';
import IssueView from './components/IssueView';
-import './Issue.css';
interface Props {
branchLike?: BranchLike;
checked?: boolean;
displayWhyIsThisAnIssue?: boolean;
- displayLocationsCount?: boolean;
- displayLocationsLink?: boolean;
issue: TypeIssue;
onChange: (issue: TypeIssue) => void;
onCheck?: (issue: string) => void;
onClick?: (issueKey: string) => void;
- onFilter?: (property: string, issue: TypeIssue) => void;
onPopupToggle: (issue: string, popupName: string, open?: boolean) => void;
openPopup?: string;
selected: boolean;
checked={this.props.checked}
currentPopup={this.props.openPopup}
displayWhyIsThisAnIssue={this.props.displayWhyIsThisAnIssue}
- displayLocationsCount={this.props.displayLocationsCount}
- displayLocationsLink={this.props.displayLocationsLink}
issue={this.props.issue}
onAssign={this.handleAssignement}
onChange={this.props.onChange}
onCheck={this.props.onCheck}
onClick={this.props.onClick}
- onFilter={this.props.onFilter}
selected={this.props.selected}
togglePopup={this.togglePopup}
/>
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { act, screen, within } from '@testing-library/react';
+import { act, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { omit, pick } from 'lodash';
import * as React from 'react';
-import { byRole, byText } from 'testing-library-selector';
+import { byLabelText, byRole, byText } from 'testing-library-selector';
import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock';
import { KeyboardKeys } from '../../../helpers/keycodes';
-import { mockIssueComment } from '../../../helpers/mocks/issues';
import { mockIssue, mockLoggedInUser, mockRawIssue } from '../../../helpers/testMocks';
import { findTooltipWithContent, renderApp } from '../../../helpers/testReactTestingUtils';
import {
IssueType,
} from '../../../types/issues';
import { RuleStatus } from '../../../types/rules';
-import { IssueComment } from '../../../types/types';
import Issue from '../Issue';
jest.mock('../../../helpers/preferences', () => ({
});
describe('rendering', () => {
- it('should render correctly with comments', () => {
- const { ui } = getPageObject();
- renderIssue({ issue: mockIssue(false, { comments: [mockIssueCommentPosted4YearsAgo()] }) });
-
- const comments = within(ui.commentsList());
- expect(comments.getByText('Leïa Skywalker')).toBeInTheDocument();
- expect(comments.getByRole('listitem')).toHaveTextContent('This is a comment, bud');
- expect(comments.getByRole('listitem')).toHaveTextContent('issue.comment.posted_on4 years ago');
- });
-
- it('should render correctly for locations, issue message, line, permalink, why, and effort', async () => {
+ it('should render correctly for issue message and effort', async () => {
const { ui } = getPageObject();
const issue = mockIssue(true, { effort: '2 days', message: 'This is an issue' });
const onClick = jest.fn();
- renderIssue({ issue, displayLocationsCount: true, displayWhyIsThisAnIssue: true, onClick });
+ renderIssue({ issue, onClick });
- expect(ui.locationsBadge(7).get()).toBeInTheDocument();
- expect(ui.lineInfo(26).get()).toBeInTheDocument();
- expect(ui.permalink.get()).toHaveAttribute(
- 'href',
- `/project/issues?issues=${issue.key}&open=${issue.key}&id=${issue.project}`
- );
- expect(ui.whyLink.get()).toBeInTheDocument();
expect(ui.effort('2 days').get()).toBeInTheDocument();
await ui.clickIssueMessage();
expect(onClick).toHaveBeenCalledWith(issue.key);
it('should render correctly for external rule engines', () => {
renderIssue({ issue: mockIssue(true, { externalRuleEngine: 'ESLINT' }) });
- expect(screen.getByText('ESLINT')).toBeInTheDocument();
+ expect(screen.getByRole('status', { name: 'ESLINT' })).toBeInTheDocument();
});
it('should render the SonarLint icon correctly', () => {
expect(onCheck).toHaveBeenCalledWith(issue.key);
});
- it('should correctly render the changelog', async () => {
- const { ui } = getPageObject();
- renderIssue();
-
- await ui.showChangelog();
- expect(
- ui.changelogRow('status', IssueStatus.Confirmed, IssueStatus.Reopened).get()
- ).toBeInTheDocument();
- expect(ui.changelogRow('assign', 'luke.skywalker', 'darth.vader').get()).toBeInTheDocument();
- });
-
it('should correctly render any code variants', () => {
const { ui } = getPageObject();
renderIssue({ issue: mockIssue(false, { codeVariants: ['variant 1', 'variant 2'] }) });
expect(ui.updateAssigneeBtn('luke').get()).toBeInTheDocument();
});
- it('should allow commenting', async () => {
- const { ui } = getPageObject();
- const issue = mockRawIssue(false, {
- actions: [IssueActions.Comment],
- });
- issuesHandler.setIssueList([{ issue, snippets: {} }]);
- renderIssue({ issue: mockIssue(false, { ...pick(issue, 'actions', 'key') }) });
-
- // Create
- await ui.addComment('Original content');
- const comments = within(ui.commentsList());
- expect(comments.getByRole('listitem')).toHaveTextContent('Original content');
-
- // Update
- await ui.updateComment('New content');
- expect(comments.getByRole('listitem')).toHaveTextContent('New content');
-
- // Delete
- await ui.deleteComment();
- expect(comments.getByRole('listitem')).toHaveTextContent('New content');
- });
-
- it('should allow updating the tags', async () => {
- const { ui } = getPageObject();
- const issue = mockRawIssue(false, {
- tags: [],
- actions: [IssueActions.SetTags],
- });
- issuesHandler.setIssueList([{ issue, snippets: {} }]);
- renderIssue({ issue: mockIssue(false, { ...pick(issue, 'actions', 'key', 'tags') }) });
-
- await ui.addTag('accessibility');
- await ui.addTag('android', ['accessibility']);
- expect(ui.updateTagsBtn(['accessibility', 'android']).get()).toBeInTheDocument();
- });
+ // Should be re-enabled when tags are re-enabled with ambroise code
+ // eslint-disable-next-line jest/no-commented-out-tests
+ // it('should allow updating the tags', async () => {
+ // const { ui } = getPageObject();
+ // const issue = mockRawIssue(false, {
+ // tags: [],
+ // actions: [IssueActions.SetTags],
+ // });
+ // issuesHandler.setIssueList([{ issue, snippets: {} }]);
+ // renderIssue({ issue: mockIssue(false, { ...pick(issue, 'actions', 'key', 'tags') }) });
+
+ // await ui.addTag('accessibility');
+ // await ui.addTag('android', ['accessibility']);
+ // expect(ui.updateTagsBtn(['accessibility', 'android']).get()).toBeInTheDocument();
+ // });
});
it('should correctly handle keyboard shortcuts', async () => {
expect(ui.updateAssigneeBtn('leia').get()).toBeInTheDocument();
});
-it('should correctly handle similar issues filtering', async () => {
- const { ui, user } = getPageObject();
- const onFilter = jest.fn();
- const issue = mockIssue(false, {
- ruleName: 'Rule Foo',
- tags: ['accessibility', 'owasp'],
- projectName: 'Project Bar',
- componentLongName: 'main.js',
- });
- renderIssue({ onFilter, issue });
-
- await ui.showSimilarIssues();
- await user.click(ui.similarIssueTypeLink.get());
- expect(onFilter).toHaveBeenLastCalledWith('type', issue);
-
- await ui.showSimilarIssues();
- await user.click(ui.similarIssueSeverityLink.get());
- expect(onFilter).toHaveBeenLastCalledWith('severity', issue);
-
- await ui.showSimilarIssues();
- await user.click(ui.similarIssueStatusLink.get());
- expect(onFilter).toHaveBeenLastCalledWith('status', issue);
-
- await ui.showSimilarIssues();
- await user.click(ui.similarIssueResolutionLink.get());
- expect(onFilter).toHaveBeenLastCalledWith('resolution', issue);
-
- await ui.showSimilarIssues();
- await user.click(ui.similarIssueAssigneeLink.get());
- expect(onFilter).toHaveBeenLastCalledWith('assignee', issue);
-
- await ui.showSimilarIssues();
- await user.click(ui.similarIssueRuleLink.get());
- expect(onFilter).toHaveBeenLastCalledWith('rule', issue);
-
- await ui.showSimilarIssues();
- await user.click(ui.similarIssueTagLink('accessibility').get());
- expect(onFilter).toHaveBeenLastCalledWith('tag###accessibility', issue);
-
- await ui.showSimilarIssues();
- await user.click(ui.similarIssueTagLink('owasp').get());
- expect(onFilter).toHaveBeenLastCalledWith('tag###owasp', issue);
-
- await ui.showSimilarIssues();
- await user.click(ui.similarIssueProjectLink.get());
- expect(onFilter).toHaveBeenLastCalledWith('project', issue);
-
- await ui.showSimilarIssues();
- await user.click(ui.similarIssueFileLink.get());
- expect(onFilter).toHaveBeenLastCalledWith('file', issue);
-});
-
-function mockIssueCommentPosted4YearsAgo(overrides: Partial<IssueComment> = {}) {
- const date = new Date();
- date.setFullYear(date.getFullYear() - 4);
- return mockIssueComment({
- authorName: 'Leïa Skywalker',
- createdAt: date.toISOString(),
- ...overrides,
- });
-}
-
function getPageObject() {
const user = userEvent.setup();
effort: (effort: string) => byText(`issue.x_effort.${effort}`),
whyLink: byRole('link', { name: 'issue.why_this_issue.long' }),
checkbox: byRole('checkbox'),
- issueMessageBtn: byRole('button', { name: 'This is an issue' }),
+ issueMessageBtn: byRole('link', { name: 'This is an issue' }),
variants: (n: number) => byText(`issue.x_code_variants.${n}`),
// Changelog
// Type
updateTypeBtn: (currentType: IssueType) =>
- byRole('button', { name: `issue.type.type_x_click_to_change.issue.type.${currentType}` }),
- setTypeBtn: (type: IssueType) => byRole('button', { name: `issue.type.${type}` }),
+ byLabelText(`issue.type.type_x_click_to_change.issue.type.${currentType}`),
+ setTypeBtn: (type: IssueType) => byText(`issue.type.${type}`),
// Severity
updateSeverityBtn: (currentSeverity: IssueSeverity) =>
- byRole('button', {
- name: `issue.severity.severity_x_click_to_change.severity.${currentSeverity}`,
- }),
- setSeverityBtn: (severity: IssueSeverity) => byRole('button', { name: `severity.${severity}` }),
+ byLabelText(`issue.severity.severity_x_click_to_change.severity.${currentSeverity}`),
+ setSeverityBtn: (severity: IssueSeverity) => byText(`severity.${severity}`),
// Status
updateStatusBtn: (currentStatus: IssueStatus) =>
- byRole('button', {
- name: `issue.transition.status_x_click_to_change.issue.status.${currentStatus}`,
- }),
- setStatusBtn: (transition: IssueTransition) =>
- byRole('button', { name: `issue.transition.${transition}` }),
+ byLabelText(`issue.transition.status_x_click_to_change.issue.status.${currentStatus}`),
+ setStatusBtn: (transition: IssueTransition) => byText(`issue.transition.${transition}`),
// Assignee
- assigneeSearchInput: byRole('searchbox'),
+ assigneeSearchInput: byLabelText('search.search_for_users'),
updateAssigneeBtn: (currentAssignee: string) =>
- byRole('button', {
+ byRole('combobox', {
name: `issue.assign.assigned_to_x_click_to_change.${currentAssignee}`,
}),
- setAssigneeBtn: (name: RegExp) => byRole('button', { name }),
+ setAssigneeBtn: (name: RegExp) => byLabelText(name),
// Tags
tagsSearchInput: byRole('searchbox'),
updateTagsBtn: (currentTags?: string[]) =>
- byRole('button', {
- name: `tags_list_x.${currentTags ? currentTags.join(', ') : 'issue.no_tag'}`,
- }),
+ byText(`tags_list_x.${currentTags ? currentTags.join(', ') : 'issue.no_tag'}`),
toggleTagCheckbox: (name: string) => byRole('checkbox', { name }),
};
},
async updateAssignee(currentAssignee: string, newAssignee: string) {
await user.click(selectors.updateAssigneeBtn(currentAssignee).get());
- await user.type(selectors.assigneeSearchInput.get(), newAssignee);
+ await act(async () => {
+ await user.type(selectors.assigneeSearchInput.get(), newAssignee);
+ });
await act(async () => {
await user.click(selectors.setAssigneeBtn(new RegExp(newAssignee)).get());
});
async showChangelog() {
await user.click(selectors.toggleChangelogBtn.get());
},
- async showSimilarIssues() {
- await user.click(selectors.toggleSimilarIssuesBtn.get());
- },
+
async toggleCheckbox() {
await user.click(selectors.checkbox.get());
},
* 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 { Badge, CommentIcon, SeparatorCircleIcon, themeColor } from 'design-system';
import * as React from 'react';
import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { isDefined } from '../../../helpers/types';
import {
IssueActions,
IssueResolution,
IssueResponse,
IssueType as IssueTypeEnum,
} from '../../../types/issues';
+import { RuleStatus } from '../../../types/rules';
import { Issue, RawQuery } from '../../../types/types';
import Tooltip from '../../controls/Tooltip';
+import DateFromNow from '../../intl/DateFromNow';
import { updateIssue } from '../actions';
import IssueAssign from './IssueAssign';
import IssueCommentAction from './IssueCommentAction';
+import IssueMessageTags from './IssueMessageTags';
import IssueSeverity from './IssueSeverity';
-import IssueTags from './IssueTags';
import IssueTransition from './IssueTransition';
import IssueType from './IssueType';
onChange: (issue: Issue) => void;
togglePopup: (popup: string, show?: boolean) => void;
className?: string;
+ showComments?: boolean;
showCommentsInPopup?: boolean;
+ showLine?: boolean;
}
interface State {
};
render() {
- const { issue, className, showCommentsInPopup } = this.props;
+ const { issue, className, showComments, showCommentsInPopup, showLine } = this.props;
const canAssign = issue.actions.includes(IssueActions.Assign);
const canComment = issue.actions.includes(IssueActions.Comment);
const canSetSeverity = issue.actions.includes(IssueActions.SetSeverity);
const canSetType = issue.actions.includes(IssueActions.SetType);
- const canSetTags = issue.actions.includes(IssueActions.SetTags);
const hasTransitions = issue.transitions.length > 0;
+ const hasComments = issue.comments && issue.comments.length > 0;
+ const IssueMetaLiClass = classNames(
+ className,
+ 'sw-body-sm sw-overflow-hidden sw-whitespace-nowrap sw-max-w-abs-150'
+ );
return (
- <div className={classNames(className, 'issue-actions')}>
- <div className="issue-meta-list">
- <div className="issue-meta">
+ <div className="sw-flex sw-flex-wrap sw-items-center sw-justify-between">
+ <ul className="sw-flex sw-items-center sw-gap-3 sw-body-sm">
+ <li>
<IssueType
canSetType={canSetType}
- isOpen={this.props.currentPopup === 'set-type' && canSetType}
issue={issue}
setIssueProperty={this.setIssueProperty}
- togglePopup={this.props.togglePopup}
/>
- </div>
- <div className="issue-meta">
+ </li>
+ <li>
<IssueSeverity
+ isOpen={this.props.currentPopup === 'set-severity'}
+ togglePopup={this.props.togglePopup}
canSetSeverity={canSetSeverity}
- isOpen={this.props.currentPopup === 'set-severity' && canSetSeverity}
issue={issue}
setIssueProperty={this.setIssueProperty}
- togglePopup={this.props.togglePopup}
/>
- </div>
- <div className="issue-meta">
+ </li>
+ <li>
<IssueTransition
+ isOpen={this.props.currentPopup === 'transition'}
+ togglePopup={this.props.togglePopup}
hasTransitions={hasTransitions}
- isOpen={this.props.currentPopup === 'transition' && hasTransitions}
issue={issue}
onChange={this.handleTransition}
- togglePopup={this.props.togglePopup}
/>
- </div>
- <div className="issue-meta">
+ </li>
+ <li>
<IssueAssign
+ isOpen={this.props.currentPopup === 'assign'}
+ togglePopup={this.props.togglePopup}
canAssign={canAssign}
- isOpen={this.props.currentPopup === 'assign' && canAssign}
issue={issue}
onAssign={this.props.onAssign}
- togglePopup={this.props.togglePopup}
/>
- </div>
- {issue.effort && (
- <div className="issue-meta">
- <span className="issue-meta-label">
- {translateWithParameters('issue.x_effort', issue.effort)}
- </span>
- </div>
- )}
- {(canComment || showCommentsInPopup) && (
- <IssueCommentAction
- commentAutoTriggered={this.state.commentAutoTriggered}
- commentPlaceholder={this.state.commentPlaceholder}
- currentPopup={this.props.currentPopup}
- issueKey={issue.key}
- onChange={this.props.onChange}
- toggleComment={this.toggleComment}
- comments={issue.comments}
- canComment={canComment}
- showCommentsInPopup={showCommentsInPopup}
+ </li>
+ </ul>
+ {(canComment || showCommentsInPopup) && (
+ <IssueCommentAction
+ commentAutoTriggered={this.state.commentAutoTriggered}
+ commentPlaceholder={this.state.commentPlaceholder}
+ currentPopup={this.props.currentPopup}
+ issueKey={issue.key}
+ onChange={this.props.onChange}
+ toggleComment={this.toggleComment}
+ comments={issue.comments}
+ canComment={canComment}
+ showCommentsInPopup={showCommentsInPopup}
+ />
+ )}
+
+ <ul className="sw-flex sw-items-center sw-gap-2 sw-body-sm">
+ <li className={IssueMetaLiClass}>
+ <IssueMessageTags
+ engine={issue.externalRuleEngine}
+ quickFixAvailable={issue.quickFixAvailable}
+ ruleStatus={issue.ruleStatus as RuleStatus | undefined}
/>
+ </li>
+
+ {issue.externalRuleEngine && (
+ <li className={IssueMetaLiClass}>
+ <Tooltip
+ overlay={translateWithParameters(
+ 'issue.from_external_rule_engine',
+ issue.externalRuleEngine
+ )}
+ >
+ <Badge>{issue.externalRuleEngine}</Badge>
+ </Tooltip>
+ </li>
)}
- </div>
- <div className="display-flex-end list-inline">
+
{issue.codeVariants && issue.codeVariants.length > 0 && (
- <div className="issue-meta">
+ <IssueMetaLi>
<Tooltip overlay={issue.codeVariants.join(', ')}>
- <span className="issue-meta-label">
+ <>
{issue.codeVariants.length > 1
? translateWithParameters('issue.x_code_variants', issue.codeVariants.length)
: translate('issue.1_code_variant')}
- </span>
+ </>
</Tooltip>
- </div>
+ <SeparatorCircleIcon aria-hidden={true} as="li" />
+ </IssueMetaLi>
)}
- <div className="issue-meta js-issue-tags">
- <IssueTags
- canSetTags={canSetTags}
- isOpen={this.props.currentPopup === 'edit-tags' && canSetTags}
- issue={issue}
- onChange={this.props.onChange}
- togglePopup={this.props.togglePopup}
- />
- </div>
- </div>
+
+ {showComments && hasComments && (
+ <>
+ <IssueMetaLi className={IssueMetaLiClass}>
+ <CommentIcon aria-label={translate('issue.comment.formlink')} />
+ {issue.comments?.length}
+ </IssueMetaLi>
+ <SeparatorCircleIcon aria-hidden={true} as="li" />
+ </>
+ )}
+ {showLine && isDefined(issue.textRange) && (
+ <>
+ <Tooltip overlay={translate('line_number')}>
+ <IssueMetaLi className={IssueMetaLiClass}>
+ {translateWithParameters('issue.ncloc_x.short', issue.textRange.endLine)}
+ </IssueMetaLi>
+ </Tooltip>
+ <SeparatorCircleIcon aria-hidden={true} as="li" />
+ </>
+ )}
+ {issue.effort && (
+ <>
+ <IssueMetaLi className={IssueMetaLiClass}>
+ {translateWithParameters('issue.x_effort', issue.effort)}
+ </IssueMetaLi>
+ <SeparatorCircleIcon aria-hidden={true} as="li" />
+ </>
+ )}
+ <IssueMetaLi className={IssueMetaLiClass}>
+ <DateFromNow date={issue.creationDate} />
+ </IssueMetaLi>
+ </ul>
</div>
);
}
}
+
+const IssueMetaLi = styled.li`
+ color: ${themeColor('pageContentLight')};
+`;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { LabelValueSelectOption, SearchSelectDropdown } from 'design-system';
import * as React from 'react';
-import Toggler from '../../../components/controls/Toggler';
-import { ButtonLink } from '../../../components/controls/buttons';
-import DropdownIcon from '../../../components/icons/DropdownIcon';
+import { Options, SingleValue } from 'react-select';
+import { searchUsers } from '../../../api/users';
+import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Issue } from '../../../types/types';
-import LegacyAvatar from '../../ui/LegacyAvatar';
-import SetAssigneePopup from '../popups/SetAssigneePopup';
+import { isLoggedIn, isUserActive } from '../../../types/users';
+import Avatar from '../../ui/Avatar';
interface Props {
- isOpen: boolean;
issue: Issue;
+ isOpen: boolean;
canAssign: boolean;
onAssign: (login: string) => void;
togglePopup: (popup: string, show?: boolean) => void;
}
-export default class IssueAssign extends React.PureComponent<Props> {
- toggleAssign = (open?: boolean) => {
- this.props.togglePopup('assign', open);
+const minSearchLength = 2;
+
+const UNASSIGNED = { value: '', label: translate('unassigned') };
+
+const renderAvatar = (name?: string, avatar?: string) => (
+ <Avatar hash={avatar} name={name} size="xs" />
+);
+
+export default function IssueAssignee(props: Props) {
+ const {
+ canAssign,
+ issue: { assignee, assigneeName, assigneeLogin, assigneeAvatar },
+ } = props;
+
+ const assinedUser = assigneeName || assignee;
+ const { currentUser } = React.useContext(CurrentUserContext);
+
+ const allowCurrentUserSelection = isLoggedIn(currentUser) && currentUser?.login !== assigneeLogin;
+
+ const defaultOptions = allowCurrentUserSelection
+ ? [
+ UNASSIGNED,
+ {
+ value: currentUser.login,
+ label: currentUser.name,
+ Icon: renderAvatar(currentUser.name, currentUser.avatar),
+ },
+ ]
+ : [UNASSIGNED];
+
+ const controlLabel = assinedUser ? (
+ <>
+ {renderAvatar(assinedUser, assigneeAvatar)} {assinedUser}
+ </>
+ ) : (
+ UNASSIGNED.label
+ );
+
+ const toggleAssign = (open?: boolean) => {
+ props.togglePopup('assign', open);
};
- handleClose = () => {
- this.toggleAssign(false);
+ const handleClose = () => {
+ toggleAssign(false);
};
- renderAssignee() {
- const { issue } = this.props;
- const assigneeName = issue.assigneeName || issue.assignee;
+ const handleSearchAssignees = (
+ query: string,
+ cb: (options: Options<LabelValueSelectOption<string>>) => void
+ ) => {
+ searchUsers({ q: query })
+ .then((result) => {
+ const options: Array<LabelValueSelectOption<string>> = result.users
+ .filter(isUserActive)
+ .map((u) => ({
+ label: u.name ?? u.login,
+ value: u.login,
+ Icon: renderAvatar(u.name, u.avatar),
+ }));
+ cb(options);
+ })
+ .catch(() => {
+ cb([]);
+ });
+ };
+
+ const renderAssignee = () => {
+ const { issue } = props;
+ const assigneeName = (issue.assigneeActive && issue.assigneeName) || issue.assignee;
if (assigneeName) {
- const assigneeDisplay =
- issue.assigneeActive === false
- ? translateWithParameters('user.x_deleted', assigneeName)
- : assigneeName;
return (
- <>
- <span className="text-top">
- <LegacyAvatar
- className="little-spacer-right"
- hash={issue.assigneeAvatar}
- name=""
- size={16}
- />
- </span>
- <span className="issue-meta-label" title={assigneeDisplay}>
- {assigneeDisplay}
+ <span className="sw-flex sw-items-center sw-gap-1">
+ <Avatar className="sw-mr-1" hash={issue.assigneeAvatar} name={assigneeName} size="xs" />
+ <span className="sw-truncate sw-max-w-abs-300 fs-mask">
+ {issue.assigneeActive
+ ? assigneeName
+ : translateWithParameters('user.x_deleted', assigneeName)}
</span>
- </>
+ </span>
);
}
- return <span className="issue-meta-label">{translate('unassigned')}</span>;
- }
-
- render() {
- const { canAssign, isOpen, issue } = this.props;
- const assigneeName = issue.assigneeName || issue.assignee;
+ return <span className="sw-flex sw-items-center sw-gap-1">{translate('unassigned')}</span>;
+ };
- if (canAssign) {
- return (
- <div className="dropdown">
- <Toggler
- closeOnEscape
- onRequestClose={this.handleClose}
- open={isOpen}
- overlay={<SetAssigneePopup onSelect={this.props.onAssign} />}
- >
- <ButtonLink
- aria-expanded={isOpen}
- aria-label={
- assigneeName
- ? translateWithParameters(
- 'issue.assign.assigned_to_x_click_to_change',
- assigneeName
- )
- : translate('issue.assign.unassigned_click_to_assign')
- }
- className="issue-action issue-action-with-options js-issue-assign"
- onClick={this.toggleAssign}
- >
- {this.renderAssignee()}
- <DropdownIcon className="little-spacer-left" />
- </ButtonLink>
- </Toggler>
- </div>
- );
+ const handleAssign = (userOption: SingleValue<LabelValueSelectOption<string>>) => {
+ if (userOption) {
+ props.onAssign(userOption.value);
}
+ };
- return this.renderAssignee();
+ if (!canAssign) {
+ return renderAssignee();
}
+
+ return (
+ <SearchSelectDropdown
+ size="medium"
+ controlAriaLabel={
+ assinedUser
+ ? translateWithParameters('issue.assign.assigned_to_x_click_to_change', assinedUser)
+ : translate('issue.assign.unassigned_click_to_assign')
+ }
+ defaultOptions={defaultOptions}
+ onChange={handleAssign}
+ loadOptions={handleSearchAssignees}
+ menuIsOpen={props.isOpen}
+ minLength={minSearchLength}
+ onMenuOpen={() => toggleAssign(true)}
+ onMenuClose={handleClose}
+ isDiscreet
+ controlLabel={controlLabel}
+ tooShortText={translateWithParameters('search.tooShort', String(minSearchLength))}
+ placeholder={translate('search.search_for_users')}
+ aria-label={translate('search.search_for_users')}
+ />
+ );
}
*/
import * as React from 'react';
import { addIssueComment, deleteIssueComment, editIssueComment } from '../../../api/issues';
-import { ButtonLink } from '../../../components/controls/buttons';
import Toggler from '../../../components/controls/Toggler';
-import { translate } from '../../../helpers/l10n';
import { Issue, IssueComment } from '../../../types/types';
import { updateIssue } from '../actions';
import CommentListPopup from '../popups/CommentListPopup';
/>
)
}
- >
- <ButtonLink
- aria-expanded={this.props.currentPopup === 'comment'}
- aria-label={translate('issue.comment.add_comment')}
- className="issue-action js-issue-comment"
- onClick={this.handleCommentClick}
- >
- <span className="issue-meta-label">
- {showCommentsInPopup && comments && (
- <span>
- {comments.length}{' '}
- {translate(
- comments.length === 1
- ? 'issue.comment.formlink.total'
- : 'issue.comment.formlink.total.plural'
- )}
- </span>
- )}
- {!showCommentsInPopup && translate('issue.comment.formlink')}
- </span>
- </ButtonLink>
- </Toggler>
+ />
</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 { StandoutLink } from 'design-system';
import * as React from 'react';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { translate } from '../../../helpers/l10n';
import { getComponentIssuesUrl } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like';
-import { RuleStatus } from '../../../types/rules';
import { Issue } from '../../../types/types';
import Link from '../../common/Link';
-import { ButtonPlain } from '../../controls/buttons';
import { IssueMessageHighlighting } from '../IssueMessageHighlighting';
-import IssueMessageTags from './IssueMessageTags';
export interface IssueMessageProps {
onClick?: () => void;
export default function IssueMessage(props: IssueMessageProps) {
const { issue, branchLike, displayWhyIsThisAnIssue } = props;
- const { externalRuleEngine, quickFixAvailable, message, messageFormattings, ruleStatus } = issue;
+ const { message, messageFormattings } = issue;
const whyIsThisAnIssueUrl = getComponentIssuesUrl(issue.project, {
...getBranchLikeQuery(branchLike),
return (
<>
- <div className="display-inline-flex-center issue-message break-word">
- {props.onClick ? (
- <ButtonPlain preventDefault className="spacer-right" onClick={props.onClick}>
- <IssueMessageHighlighting message={message} messageFormattings={messageFormattings} />
- </ButtonPlain>
- ) : (
- <span className="spacer-right">
- <IssueMessageHighlighting message={message} messageFormattings={messageFormattings} />
- </span>
- )}
- <IssueMessageTags
- engine={externalRuleEngine}
- quickFixAvailable={quickFixAvailable}
- ruleStatus={ruleStatus as RuleStatus | undefined}
- />
- </div>
+ {props.onClick ? (
+ <StandoutLink onClick={props.onClick} preventDefault to={{}}>
+ <IssueMessageHighlighting message={message} messageFormattings={messageFormattings} />
+ </StandoutLink>
+ ) : (
+ <span className="spacer-right">
+ <IssueMessageHighlighting message={message} messageFormattings={messageFormattings} />
+ </span>
+ )}
+
{displayWhyIsThisAnIssue && (
<Link
aria-label={translate('issue.why_this_issue.long')}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { DiscreetSelect } from 'design-system';
import * as React from 'react';
import { setIssueSeverity } from '../../../api/issues';
-import Toggler from '../../../components/controls/Toggler';
-import { ButtonLink } from '../../../components/controls/buttons';
-import DropdownIcon from '../../../components/icons/DropdownIcon';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { IssueResponse } from '../../../types/issues';
import { Issue, RawQuery } from '../../../types/types';
-import SeverityHelper from '../../shared/SeverityHelper';
-import SetSeverityPopup from '../popups/SetSeverityPopup';
+import SeverityIcon from '../../icons/SeverityIcon';
interface Props {
canSetSeverity: boolean;
isOpen: boolean;
issue: Pick<Issue, 'severity'>;
+ togglePopup: (popup: string, show?: boolean) => void;
setIssueProperty: (
property: keyof Issue,
popup: string,
apiCall: (query: RawQuery) => Promise<IssueResponse>,
value: string
) => void;
- togglePopup: (popup: string, show?: boolean) => void;
}
export default class IssueSeverity extends React.PureComponent<Props> {
- toggleSetSeverity = (open?: boolean) => {
- this.props.togglePopup('set-severity', open);
+ setSeverity = ({ value }: { value: string }) => {
+ this.props.setIssueProperty('severity', 'set-severity', setIssueSeverity, value);
+ this.toggleSetSeverity(false);
};
- setSeverity = (severity: string) => {
- this.props.setIssueProperty('severity', 'set-severity', setIssueSeverity, severity);
+ toggleSetSeverity = (open?: boolean) => {
+ this.props.togglePopup('set-severity', open);
};
handleClose = () => {
render() {
const { issue } = this.props;
+ const SEVERITY = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'];
+ const typesOptions = SEVERITY.map((severity) => ({
+ label: translate('severity', severity),
+ value: severity,
+ Icon: <SeverityIcon severity={severity} aria-hidden={true} />,
+ }));
+
if (this.props.canSetSeverity) {
return (
- <div className="dropdown">
- <Toggler
- onRequestClose={this.handleClose}
- open={this.props.isOpen && this.props.canSetSeverity}
- overlay={<SetSeverityPopup issue={issue} onSelect={this.setSeverity} />}
- >
- <ButtonLink
- aria-label={translateWithParameters(
- 'issue.severity.severity_x_click_to_change',
- translate('severity', issue.severity)
- )}
- aria-expanded={this.props.isOpen}
- className="issue-action issue-action-with-options js-issue-set-severity"
- onClick={this.toggleSetSeverity}
- >
- <SeverityHelper className="issue-meta-label" severity={issue.severity} />
- <DropdownIcon className="little-spacer-left" />
- </ButtonLink>
- </Toggler>
- </div>
+ <DiscreetSelect
+ aria-label={translateWithParameters(
+ 'issue.severity.severity_x_click_to_change',
+ translate('severity', issue.severity)
+ )}
+ menuIsOpen={this.props.isOpen && this.props.canSetSeverity}
+ className="js-issue-type"
+ options={typesOptions}
+ onMenuClose={this.handleClose}
+ onMenuOpen={() => this.toggleSetSeverity(true)}
+ setValue={this.setSeverity}
+ value={issue.severity}
+ />
);
}
- return <SeverityHelper className="issue-meta-label" severity={issue.severity} />;
+ return (
+ <span className="sw-flex sw-items-center sw-gap-1">
+ <SeverityIcon
+ className="little-spacer-right"
+ severity={issue.severity}
+ aria-hidden={true}
+ />
+ {translate('severity', issue.severity)}
+ </span>
+ );
}
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { PopupPlacement, Tags } from 'design-system';
import * as React from 'react';
import { setIssueTags } from '../../../api/issues';
-import { ButtonLink } from '../../../components/controls/buttons';
-import Toggler from '../../../components/controls/Toggler';
import { translate } from '../../../helpers/l10n';
import { Issue } from '../../../types/types';
-import TagsList from '../../tags/TagsList';
+import Tooltip from '../../controls/Tooltip';
import { updateIssue } from '../actions';
-import SetIssueTagsPopup from '../popups/SetIssueTagsPopup';
+import IssueTagsPopup from '../popups/IssueTagsPopup';
interface Props {
canSetTags: boolean;
- isOpen: boolean;
issue: Pick<Issue, 'key' | 'tags'>;
onChange: (issue: Issue) => void;
togglePopup: (popup: string, show?: boolean) => void;
+ open?: boolean;
}
export default class IssueTags extends React.PureComponent<Props> {
};
render() {
- const { issue } = this.props;
+ const { issue, open } = this.props;
const { tags = [] } = issue;
- if (this.props.canSetTags) {
- return (
- <div className="dropdown">
- <Toggler
- onRequestClose={this.handleClose}
- open={this.props.isOpen}
- overlay={<SetIssueTagsPopup selectedTags={tags} setTags={this.setTags} />}
- >
- <ButtonLink
- aria-expanded={this.props.isOpen}
- className="issue-action issue-action-with-options js-issue-edit-tags"
- onClick={this.toggleSetTags}
- >
- <TagsList
- allowUpdate={this.props.canSetTags}
- tags={
- issue.tags && issue.tags.length > 0 ? issue.tags : [translate('issue.no_tag')]
- }
- />
- </ButtonLink>
- </Toggler>
- </div>
- );
- }
-
return (
- <TagsList
+ <Tags
allowUpdate={this.props.canSetTags}
- className="note"
- tags={issue.tags && issue.tags.length > 0 ? issue.tags : [translate('issue.no_tag')]}
+ ariaTagsListLabel={translate('issue.tags')}
+ className="js-issue-edit-tags"
+ emptyText={translate('issue.no_tag')}
+ menuId="issue-tags-menu"
+ overlay={<IssueTagsPopup selectedTags={tags} setTags={this.setTags} />}
+ popupPlacement={PopupPlacement.Bottom}
+ tags={tags}
+ tagsToDisplay={2}
+ tooltip={Tooltip}
+ open={open}
+ onClose={this.handleClose}
/>
);
}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import Link from '../../../components/common/Link';
-import Tooltip from '../../../components/controls/Tooltip';
-import LinkIcon from '../../../components/icons/LinkIcon';
-import { getBranchLikeQuery } from '../../../helpers/branch-like';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
-import { getComponentIssuesUrl } from '../../../helpers/urls';
+
import { BranchLike } from '../../../types/branch-like';
-import { IssueType } from '../../../types/issues';
-import { MetricType } from '../../../types/metrics';
+import { IssueActions } from '../../../types/issues';
import { Issue } from '../../../types/types';
-import LocationIndex from '../../common/LocationIndex';
-import IssueChangelog from './IssueChangelog';
import IssueMessage from './IssueMessage';
-import SimilarIssuesFilter from './SimilarIssuesFilter';
+import IssueTags from './IssueTags';
export interface IssueTitleBarProps {
+ currentPopup?: string;
branchLike?: BranchLike;
onClick?: () => void;
- currentPopup?: string;
displayWhyIsThisAnIssue?: boolean;
- displayLocationsCount?: boolean;
- displayLocationsLink?: boolean;
issue: Issue;
- onFilter?: (property: string, issue: Issue) => void;
+ onChange: (issue: Issue) => void;
togglePopup: (popup: string, show?: boolean) => void;
}
export default function IssueTitleBar(props: IssueTitleBarProps) {
- const { issue, displayWhyIsThisAnIssue } = props;
- const hasSimilarIssuesFilter = props.onFilter != null;
-
- const locationsCount =
- issue.secondaryLocations.length +
- issue.flows.reduce((sum, locations) => sum + locations.length, 0) +
- issue.flowsWithType.reduce((sum, { locations }) => sum + locations.length, 0);
-
- const locationsBadge = (
- <Tooltip
- overlay={translateWithParameters(
- 'issue.this_issue_involves_x_code_locations',
- formatMeasure(locationsCount, MetricType.Integer)
- )}
- >
- <LocationIndex>{locationsCount}</LocationIndex>
- </Tooltip>
- );
-
- const displayLocations = props.displayLocationsCount && locationsCount > 0;
-
- const issueUrl = getComponentIssuesUrl(issue.project, {
- ...getBranchLikeQuery(props.branchLike),
- issues: issue.key,
- open: issue.key,
- types: issue.type === IssueType.SecurityHotspot ? issue.type : undefined,
- });
+ const { issue, displayWhyIsThisAnIssue, currentPopup } = props;
+ const canSetTags = issue.actions.includes(IssueActions.SetTags);
return (
- <div className="issue-row">
- <IssueMessage
- issue={issue}
- branchLike={props.branchLike}
- displayWhyIsThisAnIssue={displayWhyIsThisAnIssue}
- onClick={props.onClick}
- />
- <div className="issue-row-meta">
- <div className="issue-meta-list">
- <div className="issue-meta">
- <IssueChangelog
- creationDate={issue.creationDate}
- isOpen={props.currentPopup === 'changelog'}
- issue={issue}
- togglePopup={props.togglePopup}
- />
- </div>
- {issue.textRange != null && (
- <div className="issue-meta">
- <span className="issue-meta-label" title={translate('line_number')}>
- L{issue.textRange.endLine}
- </span>
- </div>
- )}
- {displayLocations && (
- <div className="issue-meta">
- {props.displayLocationsLink ? (
- <Link target="_blank" to={issueUrl}>
- {locationsBadge}
- </Link>
- ) : (
- locationsBadge
- )}
- </div>
- )}
- <div className="issue-meta">
- <Link
- className="js-issue-permalink link-no-underline"
- target="_blank"
- title={translate('permalink')}
- to={issueUrl}
- >
- <LinkIcon />
- </Link>
- </div>
- {hasSimilarIssuesFilter && (
- <div className="issue-meta">
- <SimilarIssuesFilter
- isOpen={props.currentPopup === 'similarIssues'}
- issue={issue}
- onFilter={props.onFilter}
- togglePopup={props.togglePopup}
- />
- </div>
- )}
- </div>
+ <div className="sw-flex sw-items-center">
+ <div className="sw-w-full">
+ <IssueMessage
+ issue={issue}
+ branchLike={props.branchLike}
+ displayWhyIsThisAnIssue={displayWhyIsThisAnIssue}
+ onClick={props.onClick}
+ />
+ </div>
+ <div className="js-issue-tags sw-body-sm sw-grow-0 sw-whitespace-nowrap">
+ <IssueTags
+ canSetTags={canSetTags}
+ issue={issue}
+ onChange={props.onChange}
+ togglePopup={props.togglePopup}
+ open={currentPopup === 'edit-tags' && canSetTags}
+ />
</div>
</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 { DiscreetSelect } from 'design-system';
import * as React from 'react';
+import { GroupBase, OptionProps, components } from 'react-select';
import { setIssueTransition } from '../../../api/issues';
-import { ButtonLink } from '../../../components/controls/buttons';
-import Toggler from '../../../components/controls/Toggler';
-import DropdownIcon from '../../../components/icons/DropdownIcon';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Issue } from '../../../types/types';
+import { LabelValueSelectOption } from '../../controls/Select';
+import StatusIcon from '../../icons/StatusIcon';
import StatusHelper from '../../shared/StatusHelper';
import { updateIssue } from '../actions';
-import SetTransitionPopup from '../popups/SetTransitionPopup';
interface Props {
hasTransitions: boolean;
}
export default class IssueTransition extends React.PureComponent<Props> {
- setTransition = (transition: string) => {
+ setTransition = ({ value }: { value: string }) => {
updateIssue(
this.props.onChange,
- setIssueTransition({ issue: this.props.issue.key, transition })
+ setIssueTransition({ issue: this.props.issue.key, transition: value })
);
this.toggleSetTransition(false);
};
render() {
const { issue } = this.props;
+ const transitions = issue.transitions.map((transition) => ({
+ label: translate('issue.transition', transition),
+ value: transition,
+ Icon: <StatusIcon status={transition} />,
+ }));
+
if (this.props.hasTransitions) {
return (
- <div className="dropdown">
- <Toggler
- onRequestClose={this.handleClose}
- open={this.props.isOpen && this.props.hasTransitions}
- overlay={
- <SetTransitionPopup onSelect={this.setTransition} transitions={issue.transitions} />
- }
- >
- <ButtonLink
- aria-label={translateWithParameters(
- 'issue.transition.status_x_click_to_change',
- translate('issue.status', issue.status)
- )}
- aria-expanded={this.props.isOpen}
- className="issue-action issue-action-with-options js-issue-transition"
- onClick={this.toggleSetTransition}
- >
- <StatusHelper
- className="issue-meta-label"
- resolution={issue.resolution}
- status={issue.status}
- />
- <DropdownIcon className="little-spacer-left" />
- </ButtonLink>
- </Toggler>
- </div>
+ <DiscreetSelect
+ aria-label={translateWithParameters(
+ 'issue.transition.status_x_click_to_change',
+ translate('issue.status', issue.status)
+ )}
+ size="medium"
+ className="js-issue-transition"
+ components={{
+ SingleValue: <
+ V,
+ Option extends LabelValueSelectOption<V>,
+ IsMulti extends boolean = false,
+ Group extends GroupBase<Option> = GroupBase<Option>
+ >(
+ props: OptionProps<Option, IsMulti, Group>
+ ) => {
+ return (
+ <components.SingleValue {...props}>
+ <StatusHelper
+ className="sw-flex sw-items-center"
+ resolution={issue.resolution}
+ status={issue.status}
+ />
+ </components.SingleValue>
+ );
+ },
+ }}
+ menuIsOpen={this.props.isOpen && this.props.hasTransitions}
+ options={transitions}
+ setValue={this.setTransition}
+ onMenuClose={this.handleClose}
+ onMenuOpen={() => this.toggleSetTransition(true)}
+ value={issue.resolution ?? 'OPEN'}
+ customValue={<StatusHelper resolution={issue.resolution} status={issue.status} />}
+ />
);
}
+ const resolution = issue.resolution && ` (${translate('issue.resolution', issue.resolution)})`;
return (
- <StatusHelper
- className="issue-meta-label"
- resolution={issue.resolution}
- status={issue.status}
- />
+ <span className="sw-flex sw-items-center sw-gap-1">
+ <StatusIcon status={issue.status} />
+ {translate('issue.status', issue.status)}
+ {resolution}
+ </span>
);
}
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { DiscreetSelect } from 'design-system';
import * as React from 'react';
import { setIssueType } from '../../../api/issues';
-import { colors } from '../../../app/theme';
-import { ButtonLink } from '../../../components/controls/buttons';
-import Toggler from '../../../components/controls/Toggler';
-import DropdownIcon from '../../../components/icons/DropdownIcon';
import IssueTypeIcon from '../../../components/icons/IssueTypeIcon';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { IssueResponse } from '../../../types/issues';
import { Issue, RawQuery } from '../../../types/types';
-import SetTypePopup from '../popups/SetTypePopup';
interface Props {
canSetType: boolean;
- isOpen: boolean;
issue: Pick<Issue, 'type'>;
setIssueProperty: (
property: keyof Issue,
apiCall: (query: RawQuery) => Promise<IssueResponse>,
value: string
) => void;
- togglePopup: (popup: string, show?: boolean) => void;
}
export default class IssueType extends React.PureComponent<Props> {
- toggleSetType = (open?: boolean) => {
- this.props.togglePopup('set-type', open);
- };
-
- setType = (type: string) => {
- this.props.setIssueProperty('type', 'set-type', setIssueType, type);
- };
-
- handleClose = () => {
- this.toggleSetType(false);
+ setType = ({ value }: { value: string }) => {
+ this.props.setIssueProperty('type', 'set-type', setIssueType, value);
};
render() {
const { issue } = this.props;
+ const TYPES = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
+ const typesOptions = TYPES.map((type) => ({
+ label: translate('issue.type', type),
+ value: type,
+ Icon: <IssueTypeIcon query={type} />,
+ }));
if (this.props.canSetType) {
return (
- <div className="dropdown">
- <Toggler
- onRequestClose={this.handleClose}
- open={this.props.isOpen && this.props.canSetType}
- overlay={<SetTypePopup issue={issue} onSelect={this.setType} />}
- >
- <ButtonLink
- aria-label={translateWithParameters(
- 'issue.type.type_x_click_to_change',
- translate('issue.type', issue.type)
- )}
- aria-expanded={this.props.isOpen}
- className="issue-action issue-action-with-options js-issue-set-type"
- onClick={this.toggleSetType}
- >
- <IssueTypeIcon
- className="little-spacer-right"
- fill={colors.baseFontColor}
- query={issue.type}
- />
- {translate('issue.type', issue.type)}
- <DropdownIcon className="little-spacer-left" />
- </ButtonLink>
- </Toggler>
- </div>
+ <DiscreetSelect
+ aria-label={translateWithParameters(
+ 'issue.type.type_x_click_to_change',
+ translate('issue.type', issue.type)
+ )}
+ className="js-issue-type"
+ options={typesOptions}
+ setValue={this.setType}
+ value={issue.type}
+ />
);
}
return (
- <span>
- <IssueTypeIcon className="little-spacer-right" query={issue.type} />
+ <span className="sw-flex sw-items-center sw-gap-1">
+ <IssueTypeIcon query={issue.type} />
{translate('issue.type', issue.type)}
</span>
);
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
+import { Checkbox } from 'design-system';
import * as React from 'react';
import { deleteIssueComment, editIssueComment } from '../../../api/issues';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { BranchLike } from '../../../types/branch-like';
import { Issue } from '../../../types/types';
-import Checkbox from '../../controls/Checkbox';
import { updateIssue } from '../actions';
import IssueActionsBar from './IssueActionsBar';
-import IssueCommentLine from './IssueCommentLine';
import IssueTitleBar from './IssueTitleBar';
interface Props {
checked?: boolean;
currentPopup?: string;
displayWhyIsThisAnIssue?: boolean;
- displayLocationsCount?: boolean;
- displayLocationsLink?: boolean;
issue: Issue;
onAssign: (login: string) => void;
onChange: (issue: Issue) => void;
onCheck?: (issue: string) => void;
onClick?: (issueKey: string) => void;
- onFilter?: (property: string, issue: Issue) => void;
selected: boolean;
togglePopup: (popup: string, show: boolean | void) => void;
}
};
render() {
- const {
- issue,
- branchLike,
- checked,
- currentPopup,
- displayWhyIsThisAnIssue,
- displayLocationsLink,
- displayLocationsCount,
- } = this.props;
+ const { issue, branchLike, checked, currentPopup, displayWhyIsThisAnIssue } = this.props;
const hasCheckbox = this.props.onCheck != null;
- const issueClass = classNames('issue', {
+ const issueClass = classNames('sw-py-3 sw-flex sw-items-center sw-justify-between sw-w-full ', {
'no-click': this.props.onClick === undefined,
- 'issue-with-checkbox': hasCheckbox,
selected: this.props.selected,
});
return (
- <div
- className={issueClass}
- onClick={this.handleBoxClick}
- role="region"
- aria-label={issue.message}
- >
- {hasCheckbox && (
- <Checkbox
- checked={checked ?? false}
- className="issue-checkbox-container"
- onCheck={this.handleCheck}
- label={translateWithParameters('issues.action_select.label', issue.message)}
- title={translate('issues.action_select')}
- />
- )}
- <IssueTitleBar
- branchLike={branchLike}
- onClick={this.handleDetailClick}
- currentPopup={currentPopup}
- displayLocationsCount={displayLocationsCount}
- displayLocationsLink={displayLocationsLink}
- displayWhyIsThisAnIssue={displayWhyIsThisAnIssue}
- issue={issue}
- onFilter={this.props.onFilter}
- togglePopup={this.props.togglePopup}
- />
- <IssueActionsBar
- className="padded-left"
- currentPopup={currentPopup}
- issue={issue}
- onAssign={this.props.onAssign}
- onChange={this.props.onChange}
- togglePopup={this.props.togglePopup}
- />
- {issue.comments && issue.comments.length > 0 && (
- <ul className="issue-comments" data-testid="issue-comments">
- {issue.comments.map((comment) => (
- <IssueCommentLine
- comment={comment}
- key={comment.key}
- onDelete={this.deleteComment}
- onEdit={this.editComment}
- />
- ))}
- </ul>
- )}
+ <div className={issueClass} role="region" aria-label={issue.message}>
+ <div className="sw-flex sw-w-full sw-px-2 sw-gap-4">
+ {hasCheckbox && (
+ <Checkbox
+ checked={checked ?? false}
+ onCheck={this.handleCheck}
+ label={translateWithParameters('issues.action_select.label', issue.message)}
+ title={translate('issues.action_select')}
+ />
+ )}
+ <div className="sw-flex sw-flex-col sw-grow sw-gap-2">
+ <IssueTitleBar
+ currentPopup={currentPopup}
+ branchLike={branchLike}
+ onClick={this.handleDetailClick}
+ displayWhyIsThisAnIssue={displayWhyIsThisAnIssue}
+ issue={issue}
+ onChange={this.props.onChange}
+ togglePopup={this.props.togglePopup}
+ />
+ <IssueActionsBar
+ currentPopup={currentPopup}
+ issue={issue}
+ onAssign={this.props.onAssign}
+ onChange={this.props.onChange}
+ togglePopup={this.props.togglePopup}
+ showComments={true}
+ />
+ </div>
+ </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';
-import { ButtonLink } from '../../../components/controls/buttons';
-import Toggler from '../../../components/controls/Toggler';
-import DropdownIcon from '../../../components/icons/DropdownIcon';
-import FilterIcon from '../../../components/icons/FilterIcon';
-import { translate } from '../../../helpers/l10n';
-import { Issue } from '../../../types/types';
-import SimilarIssuesPopup from '../popups/SimilarIssuesPopup';
-
-interface Props {
- isOpen: boolean;
- issue: Issue;
- togglePopup: (popup: string, show?: boolean) => void;
- onFilter?: (property: string, issue: Issue) => void;
-}
-
-export default class SimilarIssuesFilter extends React.PureComponent<Props> {
- handleFilter = (property: string, issue: Issue) => {
- this.togglePopup(false);
- if (this.props.onFilter) {
- this.props.onFilter(property, issue);
- }
- };
-
- togglePopup = (open?: boolean) => {
- this.props.togglePopup('similarIssues', open);
- };
-
- handleClose = () => {
- this.togglePopup(false);
- };
-
- render() {
- return (
- <div className="dropdown">
- <Toggler
- onRequestClose={this.handleClose}
- open={this.props.isOpen}
- overlay={<SimilarIssuesPopup issue={this.props.issue} onFilter={this.handleFilter} />}
- >
- <ButtonLink
- aria-label={translate('issue.filter_similar_issues')}
- aria-expanded={this.props.isOpen}
- className="issue-action issue-action-with-options js-issue-filter"
- onClick={this.togglePopup}
- title={translate('issue.filter_similar_issues')}
- >
- <FilterIcon />
- <DropdownIcon />
- </ButtonLink>
- </Toggler>
- </div>
- );
- }
-}
--- /dev/null
+import { searchIssueTags } from '../../../api/issues';
+
+/*
+ * 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 { TagsSelector } from 'design-system';
+import { difference, noop, without } from 'lodash';
+import * as React from 'react';
+import { translate } from '../../../helpers/l10n';
+
+interface IssueTagsPopupProps {
+ selectedTags: string[];
+ setTags: (tags: string[]) => void;
+}
+
+function IssueTagsPopup({ selectedTags, setTags }: IssueTagsPopupProps) {
+ const [searchResult, setSearchResult] = React.useState<string[]>([]);
+ const LIST_SIZE = 10;
+
+ function onSearch(query: string) {
+ return searchIssueTags({
+ q: query,
+ ps: Math.min(selectedTags.length - 1 + LIST_SIZE, 100),
+ }).then((tags: string[]) => {
+ setSearchResult(tags);
+ }, noop);
+ }
+
+ function onSelect(tag: string) {
+ setTags([...selectedTags, tag]);
+ }
+
+ function onUnselect(tag: string) {
+ setTags(without(selectedTags, tag));
+ }
+
+ const availableTags = difference(searchResult, selectedTags);
+
+ return (
+ <TagsSelector
+ headerLabel={translate('issue.tags')}
+ searchInputAriaLabel={translate('search.search_for_tags')}
+ clearIconAriaLabel={translate('clear')}
+ createElementLabel={translate('issue.create_tag')}
+ noResultsLabel={translate('no_results')}
+ onSearch={onSearch}
+ onSelect={onSelect}
+ onUnselect={onUnselect}
+ selectedTags={selectedTags}
+ tags={availableTags}
+ />
+ );
+}
+
+export default IssueTagsPopup;
+++ /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 { map } from 'lodash';
-import * as React from 'react';
-import { searchUsers } from '../../../api/users';
-import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
-import { DropdownOverlay } from '../../../components/controls/Dropdown';
-import SearchBox from '../../../components/controls/SearchBox';
-import { translate } from '../../../helpers/l10n';
-import { CurrentUser, isLoggedIn, isUserActive, UserActive, UserBase } from '../../../types/users';
-import SelectList from '../../common/SelectList';
-import SelectListItem from '../../common/SelectListItem';
-import LegacyAvatar from '../../ui/LegacyAvatar';
-
-interface Props {
- currentUser: CurrentUser;
- onSelect: (login: string) => void;
-}
-
-interface State {
- currentUser: string;
- query: string;
- users: UserActive[];
-}
-
-const LIST_SIZE = 10;
-
-export class SetAssigneePopup extends React.PureComponent<Props, State> {
- defaultUsersArray: UserActive[];
-
- constructor(props: Props) {
- super(props);
- this.defaultUsersArray = [{ login: '', name: translate('unassigned') }];
-
- if (isLoggedIn(props.currentUser)) {
- this.defaultUsersArray = [props.currentUser, ...this.defaultUsersArray];
- }
-
- this.state = {
- query: '',
- users: this.defaultUsersArray,
- currentUser: this.defaultUsersArray.length > 0 ? this.defaultUsersArray[0].login : '',
- };
- }
-
- searchUsers = (query: string) => {
- searchUsers({ q: query, ps: LIST_SIZE }).then(this.handleSearchResult, () => {});
- };
-
- handleSearchResult = ({ users }: { users: UserBase[] }) => {
- const activeUsers = users.filter(isUserActive);
- this.setState({
- users: activeUsers,
- currentUser: activeUsers.length > 0 ? activeUsers[0].login : '',
- });
- };
-
- handleSearchChange = (query: string) => {
- if (query.length === 0) {
- this.setState({
- query,
- users: this.defaultUsersArray,
- currentUser: this.defaultUsersArray[0].login,
- });
- } else {
- this.setState({ query });
- this.searchUsers(query);
- }
- };
-
- render() {
- return (
- <DropdownOverlay noPadding>
- <div className="multi-select">
- <div className="menu-search">
- <SearchBox
- autoFocus
- className="little-spacer-top"
- minLength={2}
- onChange={this.handleSearchChange}
- placeholder={translate('search.search_for_users')}
- value={this.state.query}
- />
- </div>
- <SelectList
- currentItem={this.state.currentUser}
- items={map(this.state.users, 'login')}
- onSelect={this.props.onSelect}
- >
- {this.state.users.map((user) => (
- <SelectListItem item={user.login} key={user.login}>
- {!!user.login && (
- <LegacyAvatar
- className="spacer-right"
- hash={user.avatar}
- name={user.name}
- size={16}
- />
- )}
- <span className="text-middle" style={{ marginLeft: user.login ? 24 : undefined }}>
- {user.name}
- </span>
- </SelectListItem>
- ))}
- </SelectList>
- </div>
- </DropdownOverlay>
- );
- }
-}
-
-export default withCurrentUserContext(SetAssigneePopup);
+++ /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 { difference, without } from 'lodash';
-import * as React from 'react';
-import { searchIssueTags } from '../../../api/issues';
-import { DropdownOverlay } from '../../../components/controls/Dropdown';
-import { PopupPlacement } from '../../../components/ui/popups';
-import TagsSelector from '../../tags/TagsSelector';
-
-interface Props {
- selectedTags: string[];
- setTags: (tags: string[]) => void;
-}
-
-interface State {
- searchResult: string[];
-}
-
-const LIST_SIZE = 10;
-const MAX_LIST_SIZE = 100;
-
-export default class SetIssueTagsPopup extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { searchResult: [] };
-
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- onSearch = (query: string) => {
- return searchIssueTags({
- all: true,
- q: query,
- ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, MAX_LIST_SIZE),
- }).then(
- (tags: string[]) => {
- if (this.mounted) {
- this.setState({ searchResult: tags });
- }
- },
- () => {}
- );
- };
-
- onSelect = (tag: string) => {
- this.props.setTags([...this.props.selectedTags, tag]);
- };
-
- onUnselect = (tag: string) => {
- this.props.setTags(without(this.props.selectedTags, tag));
- };
-
- render() {
- const availableTags = difference(this.state.searchResult, this.props.selectedTags);
- return (
- <DropdownOverlay placement={PopupPlacement.BottomRight}>
- <TagsSelector
- listSize={LIST_SIZE}
- onSearch={this.onSearch}
- onSelect={this.onSelect}
- onUnselect={this.onUnselect}
- selectedTags={this.props.selectedTags}
- tags={availableTags}
- />
- </DropdownOverlay>
- );
- }
-}
+++ /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';
-import { DropdownOverlay } from '../../../components/controls/Dropdown';
-import SeverityIcon from '../../../components/icons/SeverityIcon';
-import { translate } from '../../../helpers/l10n';
-import { Issue } from '../../../types/types';
-import SelectList from '../../common/SelectList';
-import SelectListItem from '../../common/SelectListItem';
-
-type Props = {
- issue: Pick<Issue, 'severity'>;
- onSelect: (severity: string) => void;
-};
-
-const SEVERITY = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'];
-
-export default function SetSeverityPopup({ issue, onSelect }: Props) {
- return (
- <DropdownOverlay>
- <SelectList currentItem={issue.severity} items={SEVERITY} onSelect={onSelect}>
- {SEVERITY.map((severity) => (
- <SelectListItem className="display-flex-center" item={severity} key={severity}>
- <SeverityIcon className="little-spacer-right" severity={severity} aria-hidden />
- {translate('severity', severity)}
- </SelectListItem>
- ))}
- </SelectList>
- </DropdownOverlay>
- );
-}
+++ /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';
-import { FormattedMessage } from 'react-intl';
-import { DropdownOverlay } from '../../../components/controls/Dropdown';
-import { translate } from '../../../helpers/l10n';
-import Link from '../../common/Link';
-import SelectList from '../../common/SelectList';
-import SelectListItem from '../../common/SelectListItem';
-
-export interface Props {
- onSelect: (transition: string) => void;
- transitions: string[];
-}
-
-export default function SetTransitionPopup({ onSelect, transitions }: Props) {
- return (
- <DropdownOverlay>
- <SelectList currentItem={transitions[0]} items={transitions} onSelect={onSelect}>
- {transitions.map((transition) => {
- const [name, description] = translateTransition(transition);
- return (
- <SelectListItem item={transition} key={transition} title={description}>
- {name}
- </SelectListItem>
- );
- })}
- </SelectList>
- </DropdownOverlay>
- );
-}
-
-function translateTransition(transition: string) {
- return [
- translate('issue.transition', transition),
- <FormattedMessage
- key="description"
- defaultMessage={translate('issue.transition', transition, 'description')}
- id={`issue.transition.${transition}.description`}
- values={{
- community_plug_link: (
- <Link to="https://community.sonarsource.com/" target="_blank">
- {translate('issue.transition.community_plug_link')}
- </Link>
- ),
- }}
- />,
- ];
-}
+++ /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';
-import { DropdownOverlay } from '../../../components/controls/Dropdown';
-import IssueTypeIcon from '../../../components/icons/IssueTypeIcon';
-import { translate } from '../../../helpers/l10n';
-import { Issue, IssueType } from '../../../types/types';
-import SelectList from '../../common/SelectList';
-import SelectListItem from '../../common/SelectListItem';
-
-interface Props {
- issue: Pick<Issue, 'type'>;
- onSelect: (type: IssueType) => void;
-}
-
-const TYPES = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
-
-export default function SetTypePopup({ issue, onSelect }: Props) {
- return (
- <DropdownOverlay>
- <SelectList currentItem={issue.type} items={TYPES} onSelect={onSelect}>
- {TYPES.map((type) => (
- <SelectListItem className="display-flex-center" item={type} key={type}>
- <IssueTypeIcon className="little-spacer-right" query={type} />
- {translate('issue.type', type)}
- </SelectListItem>
- ))}
- </SelectList>
- </DropdownOverlay>
- );
-}
+++ /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';
-import { DropdownOverlay } from '../../../components/controls/Dropdown';
-import IssueTypeIcon from '../../../components/icons/IssueTypeIcon';
-import QualifierIcon from '../../../components/icons/QualifierIcon';
-import TagsIcon from '../../../components/icons/TagsIcon';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { fileFromPath, limitComponentName } from '../../../helpers/path';
-import { ComponentQualifier } from '../../../types/component';
-import { Issue } from '../../../types/types';
-import SelectList from '../../common/SelectList';
-import SelectListItem from '../../common/SelectListItem';
-import SeverityHelper from '../../shared/SeverityHelper';
-import StatusHelper from '../../shared/StatusHelper';
-import LegacyAvatar from '../../ui/LegacyAvatar';
-
-interface SimilarIssuesPopupProps {
- issue: Issue;
- onFilter: (property: string, issue: Issue) => void;
-}
-
-export default function SimilarIssuesPopup(props: SimilarIssuesPopupProps) {
- const { issue } = props;
-
- const items = [
- 'type',
- 'severity',
- 'status',
- 'resolution',
- 'assignee',
- 'rule',
- ...(issue.tags ?? []).map((tag) => `tag###${tag}`),
- 'project',
- 'file',
- ].filter((item) => item) as string[];
-
- const assignee = issue.assigneeName ?? issue.assignee;
-
- return (
- <DropdownOverlay noPadding>
- <div className="menu-search">
- <h6>{translate('issue.filter_similar_issues')}</h6>
- </div>
-
- <SelectList
- className="issues-similar-issues-menu"
- currentItem={items[0]}
- items={items}
- onSelect={(property: string) => {
- props.onFilter(property, issue);
- }}
- >
- <SelectListItem className="display-flex-center" item="type">
- <IssueTypeIcon className="little-spacer-right" query={issue.type} />
- {translate('issue.type', issue.type)}
- </SelectListItem>
-
- <SelectListItem item="severity">
- <SeverityHelper className="display-flex-center" severity={issue.severity} />
- </SelectListItem>
-
- <SelectListItem item="status">
- <StatusHelper
- className="display-flex-center"
- resolution={undefined}
- status={issue.status}
- />
- </SelectListItem>
-
- <SelectListItem item="resolution">
- {issue.resolution != null
- ? translate('issue.resolution', issue.resolution)
- : translate('unresolved')}
- </SelectListItem>
-
- <SelectListItem item="assignee">
- {assignee ? (
- <span>
- {translate('assigned_to')}
- <LegacyAvatar
- className="little-spacer-left little-spacer-right"
- hash={issue.assigneeAvatar}
- name={assignee}
- size={16}
- />
- {issue.assigneeActive === false
- ? translateWithParameters('user.x_deleted', assignee)
- : assignee}
- </span>
- ) : (
- translate('unassigned')
- )}
- </SelectListItem>
-
- <li className="divider" />
-
- <SelectListItem item="rule">{limitComponentName(issue.ruleName)}</SelectListItem>
-
- {issue.tags?.map((tag) => (
- <SelectListItem item={`tag###${tag}`} key={`tag###${tag}`}>
- <TagsIcon className="little-spacer-right text-middle" />
- <span className="text-middle">{tag}</span>
- </SelectListItem>
- ))}
-
- <li className="divider" />
-
- <SelectListItem item="project">
- <QualifierIcon className="little-spacer-right" qualifier={ComponentQualifier.Project} />
- {issue.projectName}
- </SelectListItem>
-
- <SelectListItem item="file">
- <QualifierIcon className="little-spacer-right" qualifier={issue.componentQualifier} />
- {fileFromPath(issue.componentLongName)}
- </SelectListItem>
- </SelectList>
- </DropdownOverlay>
- );
-}
issue.remove_tags=Remove Tags
issue.no_tag=No tags
issue.create_tag=Create Tag
+issue.create_tag_x=Create Tag '{0}'
+issue.tags=Tags
issue.assign.assigned_to_x_click_to_change=Assigned to {0}, click to change
issue.assign.unassigned_click_to_assign=Unassigned, click to assign issue
issue.assign.formlink=Assign
issue.location_x=Location {0}
issue.closed.file_level=This issue is {status}. It was detected in the file below and is no longer being detected.
issue.closed.project_level=This issue is {status}. It was detected in the project below and is no longer being detected.
+issues.assignee.change_user=Click to change assignee
issues.action_select=Select issue
issues.action_select.label=Select issue {0}
issues.bulk_change_X_issues=Bulk Change {0} Issue(s)
issues.select_all_issues=Select all Issues
issues.issues=issues
-issues.to_select_issues=to select issues
-issues.to_navigate=to navigate
+issues.to_select_issues=Select issues
+issues.to_navigate=Navigate to issue
issues.to_navigate_back=to navigate back
issues.to_navigate_issue_locations=to navigate issue locations
issues.to_switch_flows=to switch flows