import { getStandards } from '../../helpers/security-standard';
import {
mockCurrentUser,
+ mockLoggedInUser,
mockPaging,
mockRawIssue,
mockRuleDetails
} from '../../helpers/testMocks';
import { BranchParameters } from '../../types/branch-like';
-import { RawFacet, RawIssue, RawIssuesResponse, ReferencedComponent } from '../../types/issues';
+import {
+ IssueType,
+ RawFacet,
+ RawIssue,
+ RawIssuesResponse,
+ ReferencedComponent
+} from '../../types/issues';
import { Standards } from '../../types/security';
import {
Dict,
} from '../../types/types';
import { NoticeType } from '../../types/users';
import { getComponentForSourceViewer, getSources } from '../components';
-import { getIssueFlowSnippets, searchIssues } from '../issues';
+import {
+ getIssueFlowSnippets,
+ searchIssues,
+ searchIssueTags,
+ setIssueAssignee,
+ setIssueSeverity,
+ setIssueTags,
+ setIssueTransition,
+ setIssueType
+} from '../issues';
import { getRuleDetails } from '../rules';
-import { dismissNotice, getCurrentUser } from '../users';
+import { dismissNotice, getCurrentUser, searchUsers } from '../users';
function mockReferenceComponent(override?: Partial<ReferencedComponent>) {
return {
},
{
issue: mockRawIssue(false, {
+ actions: ['set_type', 'set_tags', 'comment', 'set_severity', 'assign'],
+ transitions: ['confirm', 'resolve', 'falsepositive', 'wontfix'],
key: 'issue2',
component: 'project:file.bar',
message: 'Fix that',
);
(getCurrentUser as jest.Mock).mockImplementation(this.handleGetCurrentUser);
(dismissNotice as jest.Mock).mockImplementation(this.handleDismissNotification);
+ (setIssueType as jest.Mock).mockImplementation(this.handleSetIssueType);
+ (setIssueAssignee as jest.Mock).mockImplementation(this.handleSetIssueAssignee);
+ (setIssueSeverity as jest.Mock).mockImplementation(this.handleSetIssueSeverity);
+ (setIssueTransition as jest.Mock).mockImplementation(this.handleSetIssueTransition);
+ (setIssueTags as jest.Mock).mockImplementation(this.handleSetIssueTags);
+ (searchUsers as jest.Mock).mockImplementation(this.handleSearchUsers);
+ (searchIssueTags as jest.Mock).mockImplementation(this.handleSearchIssueTags);
}
async getStandards(): Promise<Standards> {
return Promise.reject();
};
+ handleSetIssueType = (data: { issue: string; type: IssueType }) => {
+ return this.getActionsResponse({ type: data.type }, data.issue);
+ };
+
+ handleSetIssueSeverity = (data: { issue: string; severity: string }) => {
+ return this.getActionsResponse({ severity: data.severity }, data.issue);
+ };
+
+ handleSetIssueAssignee = (data: { issue: string; assignee?: string }) => {
+ return this.getActionsResponse({ assignee: data.assignee }, data.issue);
+ };
+
+ handleSetIssueTransition = (data: { issue: string; transition: string }) => {
+ const statusMap: { [key: string]: string } = {
+ confirm: 'CONFIRMED',
+ unconfirm: 'REOPENED',
+ resolve: 'RESOLVED'
+ };
+ return this.getActionsResponse({ status: statusMap[data.transition] }, data.issue);
+ };
+
+ handleSetIssueTags = (data: { issue: string; tags: string }) => {
+ const tagsArr = data.tags.split(',');
+ return this.getActionsResponse({ tags: tagsArr }, data.issue);
+ };
+
+ handleSearchUsers = () => {
+ return this.reply({ users: [mockLoggedInUser()] });
+ };
+
+ handleSearchIssueTags = () => {
+ return this.reply(['accessibility', 'android']);
+ };
+
+ getActionsResponse = (overrides: Partial<RawIssue>, issueKey: string) => {
+ const issueDataSelected = this.list.find(l => l.issue.key === issueKey)!;
+
+ issueDataSelected.issue = {
+ ...issueDataSelected?.issue,
+ ...overrides
+ };
+ return this.reply({
+ issue: issueDataSelected.issue
+ });
+ };
+
reply<T>(response: T): Promise<T> {
return Promise.resolve(cloneDeep(response));
}
// ui elements
pageMainZIndex: '50',
- pageSideZIndex: '51',
+ pageSideZIndex: '50',
pageHeaderZIndex: '55',
globalBannerZIndex: '60',
// Select an issue with an advanced rule
expect(await screen.findByRole('region', { name: 'Fix that' })).toBeInTheDocument();
await user.click(screen.getByRole('region', { name: 'Fix that' }));
+ expect(screen.getByRole('button', { name: 'issue.tabs.code' })).toBeInTheDocument();
// Are rule headers present?
expect(screen.getByRole('heading', { level: 1, name: 'Fix that' })).toBeInTheDocument();
);
});
+it('should be able to perform action on issues', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderIssueApp();
+
+ // Select an issue with an advanced rule
+ await user.click(await screen.findByRole('region', { name: 'Fix that' }));
+
+ // changing issue type
+ expect(
+ screen.getByRole('button', {
+ name: `issue.type.type_x_click_to_change.issue.type.CODE_SMELL`
+ })
+ ).toBeInTheDocument();
+ await user.click(
+ screen.getByRole('button', {
+ name: `issue.type.type_x_click_to_change.issue.type.CODE_SMELL`
+ })
+ );
+ expect(screen.getByText('issue.type.BUG')).toBeInTheDocument();
+ expect(screen.getByText('issue.type.VULNERABILITY')).toBeInTheDocument();
+
+ await user.click(screen.getByText('issue.type.VULNERABILITY'));
+ expect(
+ screen.getByRole('button', {
+ name: `issue.type.type_x_click_to_change.issue.type.VULNERABILITY`
+ })
+ ).toBeInTheDocument();
+
+ // changing issue severity
+ expect(screen.getByText('severity.MAJOR')).toBeInTheDocument();
+
+ await user.click(
+ screen.getByRole('button', {
+ name: `issue.severity.severity_x_click_to_change.severity.MAJOR`
+ })
+ );
+ expect(screen.getByText('severity.MINOR')).toBeInTheDocument();
+ expect(screen.getByText('severity.INFO')).toBeInTheDocument();
+ await user.click(screen.getByText('severity.MINOR'));
+ expect(
+ screen.getByRole('button', {
+ name: `issue.severity.severity_x_click_to_change.severity.MINOR`
+ })
+ ).toBeInTheDocument();
+
+ // changing issue status
+ expect(screen.getByText('issue.status.OPEN')).toBeInTheDocument();
+
+ await user.click(screen.getByText('issue.status.OPEN'));
+ expect(screen.getByText('issue.transition.confirm')).toBeInTheDocument();
+ expect(screen.getByText('issue.transition.resolve')).toBeInTheDocument();
+
+ await user.click(screen.getByText('issue.transition.confirm'));
+ expect(
+ screen.getByRole('button', {
+ name: `issue.transition.status_x_click_to_change.issue.status.CONFIRMED`
+ })
+ ).toBeInTheDocument();
+ await user.keyboard('{Escape}');
+
+ // assigning issue to a different user
+ expect(
+ screen.getByRole('button', {
+ name: `issue.assign.unassigned_click_to_assign`
+ })
+ ).toBeInTheDocument();
+
+ await user.click(
+ screen.getByRole('button', {
+ name: `issue.assign.unassigned_click_to_assign`
+ })
+ );
+ expect(screen.getByRole('searchbox', { name: 'search_verb' })).toBeInTheDocument();
+
+ await user.click(screen.getByRole('searchbox', { name: 'search_verb' }));
+ await user.keyboard('luke');
+ expect(screen.getByText('Skywalker')).toBeInTheDocument();
+ await user.keyboard('{ArrowUp}{enter}');
+ expect(screen.getByText('luke')).toBeInTheDocument();
+
+ // changing tags
+ expect(screen.getByText('issue.no_tag')).toBeInTheDocument();
+ await user.click(screen.getByText('issue.no_tag'));
+ expect(screen.getByRole('searchbox', { name: 'search_verb' })).toBeInTheDocument();
+ expect(screen.getByText('android')).toBeInTheDocument();
+ expect(screen.getByText('accessibility')).toBeInTheDocument();
+
+ await user.click(screen.getByText('accessibility'));
+ expect(screen.getAllByText('accessibility')).toHaveLength(2); // one in the list of selector and one selected
+
+ await user.click(screen.getByRole('searchbox', { name: 'search_verb' }));
+ await user.keyboard('addNewTag');
+ expect(screen.getByText('+')).toBeInTheDocument();
+ expect(screen.getByText('addnewtag')).toBeInTheDocument();
+});
+
+it('should not allow performing actions when user does not have permission', async () => {
+ const user = userEvent.setup();
+ renderIssueApp();
+
+ await user.click(await screen.findByRole('region', { name: 'Fix this' }));
+
+ expect(
+ screen.queryByRole('button', {
+ name: `issue.assign.unassigned_click_to_assign`
+ })
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', {
+ name: `issue.type.type_x_click_to_change.issue.type.CODE_SMELL`
+ })
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', {
+ name: `issue.comment.add_comment`
+ })
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', {
+ name: `issue.transition.status_x_click_to_change.issue.status.OPEN`
+ })
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', {
+ name: `issue.severity.severity_x_click_to_change.severity.MAJOR`
+ })
+ ).not.toBeInTheDocument();
+});
+
+it('should open the actions popup using keyboard shortcut', async () => {
+ const user = userEvent.setup();
+ handler.setIsAdmin(true);
+ renderIssueApp();
+
+ // Select an issue with an advanced rule
+ await user.click(await screen.findByRole('region', { name: 'Fix that' }));
+
+ // open severity popup on key press 'i'
+ await user.keyboard('i');
+ expect(screen.getByText('severity.MINOR')).toBeInTheDocument();
+ expect(screen.getByText('severity.INFO')).toBeInTheDocument();
+
+ // open status popup on key press 'f'
+ await user.keyboard('f');
+ expect(screen.getByText('issue.transition.confirm')).toBeInTheDocument();
+ expect(screen.getByText('issue.transition.resolve')).toBeInTheDocument();
+
+ // open comment popup on key press 'c'
+ await user.keyboard('c');
+ expect(screen.getByText('issue.comment.submit')).toBeInTheDocument();
+ await user.click(screen.getByText('cancel'));
+
+ // open tags popup on key press 't'
+ await user.keyboard('t');
+ expect(screen.getByRole('searchbox', { name: 'search_verb' })).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_verb' })).toBeInTheDocument();
+});
+
describe('redirects', () => {
it('should work for hotspots', () => {
renderProjectIssuesApp(`project/issues?types=${IssueType.SecurityHotspot}`);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { Link } from 'react-router-dom';
+import { setIssueAssignee } from '../../../api/issues';
+import LinkIcon from '../../../components/icons/LinkIcon';
+import { updateIssue } from '../../../components/issue/actions';
+import IssueActionsBar from '../../../components/issue/components/IssueActionsBar';
+import IssueChangelog from '../../../components/issue/components/IssueChangelog';
+import { getBranchLikeQuery } from '../../../helpers/branch-like';
+import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
+import { KeyboardKeys } from '../../../helpers/keycodes';
+import { translate } from '../../../helpers/l10n';
+import { getComponentIssuesUrl, getRuleUrl } from '../../../helpers/urls';
+import { BranchLike } from '../../../types/branch-like';
+import { Issue, RuleDetails } from '../../../types/types';
+
+interface Props {
+ issue: Issue;
+ ruleDetails: RuleDetails;
+ branchLike?: BranchLike;
+ onIssueChange: (issue: Issue) => void;
+}
+
+interface State {
+ issuePopupName?: string;
+}
+
+export default class IssueHeader extends React.PureComponent<Props, State> {
+ state = { issuePopupName: undefined };
+
+ componentDidMount() {
+ document.addEventListener('keydown', this.handleKeyDown, { capture: true });
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.issue.key !== this.props.issue.key) {
+ this.setState({ issuePopupName: undefined });
+ }
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('keydown', this.handleKeyDown, { capture: true });
+ }
+
+ handleIssuePopupToggle = (popupName: string, open = true) => {
+ const name = open ? popupName : undefined;
+ this.setState({ issuePopupName: name });
+ };
+
+ handleAssignement = (login: string) => {
+ const { issue } = this.props;
+ if (issue.assignee !== login) {
+ updateIssue(
+ this.props.onIssueChange,
+ setIssueAssignee({ issue: issue.key, assignee: login })
+ );
+ }
+ this.handleIssuePopupToggle('assign', false);
+ };
+
+ handleKeyDown = (event: KeyboardEvent) => {
+ if (isInput(event) || isShortcut(event)) {
+ return true;
+ } else if (event.key === KeyboardKeys.KeyF) {
+ event.preventDefault();
+ return this.handleIssuePopupToggle('transition');
+ } else if (event.key === KeyboardKeys.KeyA) {
+ event.preventDefault();
+ return this.handleIssuePopupToggle('assign');
+ } else if (event.key === KeyboardKeys.KeyM && this.props.issue.actions.includes('assign')) {
+ event.preventDefault();
+ return this.handleAssignement('_me');
+ } else if (event.key === KeyboardKeys.KeyI) {
+ event.preventDefault();
+ return this.handleIssuePopupToggle('set-severity');
+ } else if (event.key === KeyboardKeys.KeyC) {
+ event.preventDefault();
+ return this.handleIssuePopupToggle('comment');
+ } else if (event.key === KeyboardKeys.KeyT) {
+ event.preventDefault();
+ return this.handleIssuePopupToggle('edit-tags');
+ }
+ return true;
+ };
+
+ render() {
+ const {
+ issue,
+ ruleDetails: { key, name },
+ branchLike
+ } = this.props;
+ const { issuePopupName } = this.state;
+ const issueUrl = getComponentIssuesUrl(issue.project, {
+ ...getBranchLikeQuery(branchLike),
+ issues: issue.key,
+ open: issue.key,
+ types: issue.type === 'SECURITY_HOTSPOT' ? issue.type : undefined
+ });
+ return (
+ <>
+ <div className="display-flex-center display-flex-space-between big-padded-top">
+ <h1 className="text-bold">{issue.message}</h1>
+ <div className="issue-meta issue-get-perma-link">
+ <Link
+ className="js-issue-permalink link-no-underline"
+ target="_blank"
+ title={translate('permalink')}
+ to={issueUrl}>
+ {translate('issue.action.permalink')}
+ <LinkIcon />
+ </Link>
+ </div>
+ </div>
+ <div className="display-flex-center display-flex-space-between spacer-top big-spacer-bottom">
+ <div>
+ <span className="note padded-right">{name}</span>
+ <Link className="small" to={getRuleUrl(key)} target="_blank">
+ {key}
+ </Link>
+ </div>
+ <div className="issue-meta-list">
+ <div className="issue-meta">
+ <IssueChangelog
+ creationDate={issue.creationDate}
+ isOpen={issuePopupName === 'changelog'}
+ issue={issue}
+ togglePopup={this.handleIssuePopupToggle}
+ />
+ </div>
+ {issue.textRange != null && (
+ <div className="issue-meta">
+ <span className="issue-meta-label" title={translate('line_number')}>
+ L{issue.textRange.endLine}
+ </span>
+ </div>
+ )}
+ </div>
+ </div>
+ <IssueActionsBar
+ currentPopup={issuePopupName}
+ issue={issue}
+ onAssign={this.handleAssignement}
+ onChange={this.props.onIssueChange}
+ togglePopup={this.handleIssuePopupToggle}
+ />
+ </>
+ );
+ }
+}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { Link } from 'react-router-dom';
import TabViewer from '../../../components/rules/TabViewer';
-import { getRuleUrl } from '../../../helpers/urls';
+import { BranchLike } from '../../../types/branch-like';
import { Issue, RuleDetails } from '../../../types/types';
+import IssueHeader from './IssueHeader';
interface IssueViewerTabsProps {
+ branchLike?: BranchLike;
issue: Issue;
codeTabContent: React.ReactNode;
ruleDetails: RuleDetails;
+ onIssueChange: (issue: Issue) => void;
}
export default function IssueViewerTabs(props: IssueViewerTabsProps) {
- const {
- ruleDetails,
- codeTabContent,
- ruleDetails: { name, key },
- issue: { ruleDescriptionContextKey, message }
- } = props;
+ const { ruleDetails, issue, codeTabContent, branchLike } = props;
return (
<>
- <div className="big-padded-top">
- <h1 className="text-bold">{message}</h1>
- <div className="spacer-top big-spacer-bottom">
- <span className="note padded-right">{name}</span>
- <Link className="small" to={getRuleUrl(key)} target="_blank">
- {key}
- </Link>
- </div>
- </div>
+ <IssueHeader
+ issue={issue}
+ ruleDetails={ruleDetails}
+ branchLike={branchLike}
+ onIssueChange={props.onIssueChange}
+ />
<TabViewer
ruleDetails={ruleDetails}
extendedDescription={ruleDetails.htmlNote}
- ruleDescriptionContextKey={ruleDescriptionContextKey}
+ ruleDescriptionContextKey={issue.ruleDescriptionContextKey}
codeTabContent={codeTabContent}
scrollInTab={true}
/>
branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
issues={issues}
locationsNavigator={this.state.locationsNavigator}
- onIssueChange={this.handleIssueChange}
onIssueSelect={this.openIssue}
onLocationSelect={this.selectLocation}
openIssue={openIssue}
}
issue={openIssue}
ruleDetails={openRuleDetails}
+ branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
+ onIssueChange={this.handleIssueChange}
/>
) : (
<DeferredSpinner loading={loading}>
branchLike: BranchLike | undefined;
issues: Issue[];
locationsNavigator: boolean;
- onIssueChange: (issue: Issue) => void;
onIssueSelect: (issueKey: string) => void;
onLocationSelect: (index: number) => void;
openIssue: Issue;
issue={openIssue}
issues={this.props.issues}
locations={locations}
- onIssueChange={this.props.onIssueChange}
onIssueSelect={this.props.onIssueSelect}
onLoaded={this.handleLoaded}
onLocationSelect={this.props.onLocationSelect}
branchLike={mockMainBranch()}
issues={[mockIssue()]}
locationsNavigator={true}
- onIssueChange={jest.fn()}
onIssueSelect={jest.fn()}
onLocationSelect={jest.fn()}
openIssue={mockIssue()}
},
]
}
- onIssueChange={[MockFunction]}
onIssueSelect={[MockFunction]}
onLoaded={[Function]}
onLocationSelect={[MockFunction]}
},
]
}
- onIssueChange={[MockFunction]}
onIssueSelect={[MockFunction]}
onLoaded={[Function]}
onLocationSelect={[MockFunction]}
]
}
locations={Array []}
- onIssueChange={[MockFunction]}
onIssueSelect={[MockFunction]}
onLoaded={[Function]}
onLocationSelect={[MockFunction]}
},
]
}
- onIssueChange={[MockFunction]}
onIssueSelect={[MockFunction]}
onLoaded={[Function]}
onLocationSelect={[MockFunction]}
*/
import * as React from 'react';
import { getSources } from '../../../api/components';
-import Issue from '../../../components/issue/Issue';
-import SecondaryIssue from '../../../components/issue/SecondaryIssue';
+import IssueMessageBox from '../../../components/issue/IssueMessageBox';
import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
highlightedLocationMessage: { index: number; text: string | undefined } | undefined;
isLastOccurenceOfPrimaryComponent: boolean;
issue: TypeIssue;
- issuePopup?: { issue: string; name: string };
issuesByLine: IssuesByLine;
lastSnippetGroup: boolean;
loadDuplications: (component: string, line: SourceLine) => void;
locations: FlowLocation[];
- onIssueChange: (issue: TypeIssue) => void;
onIssueSelect: (issueKey: string) => void;
- onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void;
onLocationSelect: (index: number) => void;
renderDuplicationPopup: (
component: SourceViewerFile,
};
renderIssuesList = (line: SourceLine) => {
- const {
- isLastOccurenceOfPrimaryComponent,
- issue,
- issuesByLine,
- snippetGroup,
- branchLike
- } = this.props;
+ const { isLastOccurenceOfPrimaryComponent, issue, issuesByLine, snippetGroup } = this.props;
const locations =
issue.component === snippetGroup.component.key && issue.textRange !== undefined
? locationsByLine([issue])
return (
issuesForLine.length > 0 && (
- <div className="issue-list">
- {issuesForLine.map(issueToDisplay => {
- if (issueToDisplay.key === issue.key && issueLocations && issueLocations.length) {
- return (
- <Issue
- branchLike={branchLike}
- displayWhyIsThisAnIssue={false}
- issue={issueToDisplay}
- key={issueToDisplay.key}
- onChange={this.props.onIssueChange}
- onPopupToggle={this.props.onIssuePopupToggle}
- openPopup={
- this.props.issuePopup && this.props.issuePopup.issue === issueToDisplay.key
- ? this.props.issuePopup.name
- : undefined
- }
- selected={issue.key === issueToDisplay.key}
- />
- );
- }
- return (
- <SecondaryIssue
- key={issueToDisplay.key}
- issue={issueToDisplay}
- onClick={this.props.onIssueSelect}
- />
- );
- })}
+ <div>
+ {issuesForLine.map(issueToDisplay => (
+ <IssueMessageBox
+ selected={!!(issueToDisplay.key === issue.key && issueLocations.length > 0)}
+ key={issueToDisplay.key}
+ issue={issueToDisplay}
+ onClick={this.props.onIssueSelect}
+ />
+ ))}
</div>
)
);
branchLike,
isLastOccurenceOfPrimaryComponent,
issue,
- issuePopup,
lastSnippetGroup,
snippetGroup
} = this.props;
/>
{issue.component === snippetGroup.component.key && issue.textRange === undefined && (
- <Issue
- issue={issue}
- onChange={this.props.onIssueChange}
- onPopupToggle={this.props.onIssuePopupToggle}
- openPopup={issuePopup && issuePopup.issue === issue.key ? issuePopup.name : undefined}
- selected={true}
- />
+ <IssueMessageBox selected={true} issue={issue} onClick={this.props.onIssueSelect} />
)}
{snippetLines.map((snippet, index) => (
<SnippetViewer
issue: Issue;
issues: Issue[];
locations: FlowLocation[];
- onIssueChange: (issue: Issue) => void;
onIssueSelect: (issueKey: string) => void;
onLoaded?: () => void;
onLocationSelect: (index: number) => void;
duplicatedFiles?: Dict<DuplicatedFile>;
duplications?: Duplication[];
duplicationsByLine: { [line: number]: number[] };
- issuePopup?: { issue: string; name: string };
loading: boolean;
notAccessible: boolean;
}
if (this.mounted) {
this.setState({
components,
- issuePopup: undefined,
loading: false
});
if (this.props.onLoaded) {
}
}
- handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
- this.setState((state: State) => {
- const samePopup =
- state.issuePopup && state.issuePopup.name === popupName && state.issuePopup.issue === issue;
- if (open !== false && !samePopup) {
- return { issuePopup: { issue, name: popupName } };
- } else if (open !== true && samePopup) {
- return { issuePopup: undefined };
- }
- return null;
- });
- };
-
renderDuplicationPopup = (component: SourceViewerFile, index: number, line: number) => {
const { duplicatedFiles, duplications } = this.state;
duplicationsByLine={duplicationsByLine}
highlightedLocationMessage={this.props.highlightedLocationMessage}
issue={issue}
- issuePopup={this.state.issuePopup}
issuesByLine={issuesByComponent[snippetGroup.component.key] || {}}
isLastOccurenceOfPrimaryComponent={i === lastOccurenceOfPrimaryComponent}
lastSnippetGroup={i === locationsByComponent.length - 1}
loadDuplications={this.fetchDuplications}
locations={snippetGroup.locations || []}
- onIssueChange={this.props.onIssueChange}
onIssueSelect={this.props.onIssueSelect}
- onIssuePopupToggle={this.handleIssuePopupToggle}
onLocationSelect={this.props.onLocationSelect}
renderDuplicationPopup={this.renderDuplicationPopup}
snippetGroup={snippetGroup}
import { range, times } from 'lodash';
import * as React from 'react';
import { getSources } from '../../../../api/components';
-import Issue from '../../../../components/issue/Issue';
+import IssueMessageBox from '../../../../components/issue/IssueMessageBox';
import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like';
import {
mockSnippetsByComponent,
}
});
- expect(wrapper.find(Issue).exists()).toBe(true);
+ expect(wrapper.find(IssueMessageBox).exists()).toBe(true);
});
it('should expand block', async () => {
lastSnippetGroup={false}
loadDuplications={jest.fn()}
locations={[]}
- onIssueChange={jest.fn()}
onIssueSelect={jest.fn()}
- onIssuePopupToggle={jest.fn()}
onLocationSelect={jest.fn()}
renderDuplicationPopup={jest.fn()}
snippetGroup={snippetGroup}
expect(wrapper).toMatchSnapshot();
});
-it('should handle issue popup', () => {
- const wrapper = shallowRender();
- // open
- wrapper.instance().handleIssuePopupToggle('1', 'popup1');
- expect(wrapper.state('issuePopup')).toEqual({ issue: '1', name: 'popup1' });
-
- // close
- wrapper.instance().handleIssuePopupToggle('1', 'popup1');
- expect(wrapper.state('issuePopup')).toBeUndefined();
-});
-
it('should handle duplication popup', async () => {
const files = { b: { key: 'b', name: 'B.tsx', project: 'foo', projectName: 'Foo' } };
const duplications = [{ blocks: [{ _ref: '1', from: 1, size: 2 }] }];
})}
issues={[]}
locations={[mockFlowLocation({ component: 'project:main.js' })]}
- onIssueChange={jest.fn()}
onLoaded={jest.fn()}
onIssueSelect={jest.fn()}
onLocationSelect={jest.fn()}
},
]
}
- onIssueChange={[MockFunction]}
- onIssuePopupToggle={[Function]}
onIssueSelect={[MockFunction]}
onLocationSelect={[MockFunction]}
renderDuplicationPopup={[Function]}
lastSnippetGroup={false}
loadDuplications={[Function]}
locations={Array []}
- onIssueChange={[MockFunction]}
- onIssuePopupToggle={[Function]}
onIssueSelect={[MockFunction]}
onLocationSelect={[MockFunction]}
renderDuplicationPopup={[Function]}
},
]
}
- onIssueChange={[MockFunction]}
- onIssuePopupToggle={[Function]}
onIssueSelect={[MockFunction]}
onLocationSelect={[MockFunction]}
renderDuplicationPopup={[Function]}
.source-line-code {
position: relative;
- padding: 0 10px;
}
.source-line-code pre {
white-space: pre-wrap;
}
-.source-line-code .issue-list {
- margin-left: -10px;
- margin-right: -10px;
-}
-
.source-line-code-inner {
min-height: 18px;
+ padding: 0 10px;
}
.source-line-code-inner:before,
cursor: initial;
}
-.issue.secondary-issue {
- background-color: var(--secondIssueBgColor);
-}
-
.issue.hotspot {
background-color: var(--hotspotBgColor);
}
-.issue.selected {
+.issue.selected,
+.issue-message-box.selected {
box-shadow: none;
outline: none;
border: 2px solid var(--blue) !important;
justify-content: space-between;
flex-wrap: wrap;
align-items: center;
- padding-left: var(--gridSize);
}
.issue-meta-list {
.issue .badge-error {
background-color: var(--badgeRedBackgroundOnIssue);
}
+
+.issue-message-box {
+ background-color: var(--issueBgColor);
+ border: 2px solid transparent;
+ margin: 10px 0px;
+}
+
+.issue-message-box.secondary-issue {
+ background-color: var(--secondIssueBgColor);
+}
+
+.issue-message-box.secondary-issue:hover {
+ border: 2px dashed var(--blue);
+ outline: 0;
+ cursor: pointer;
+}
+
+.issue-get-perma-link {
+ flex-shrink: 0;
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 classNames from 'classnames';
+import * as React from 'react';
+import { colors } from '../../app/theme';
+import { Issue } from '../../types/types';
+import IssueTypeIcon from '../icons/IssueTypeIcon';
+import './Issue.css';
+
+export interface IssueMessageBoxProps {
+ selected: boolean;
+ issue: Issue;
+ onClick: (issueKey: string) => void;
+}
+
+export default function IssueMessageBox(props: IssueMessageBoxProps) {
+ const { issue, selected } = props;
+ return (
+ <div
+ className={classNames('issue-message-box display-flex-row display-flex-center padded-right', {
+ 'selected big-padded-top big-padded-bottom': selected,
+ 'secondary-issue padded-top padded-bottom': !selected
+ })}
+ key={issue.key}
+ onClick={() => props.onClick(issue.key)}
+ role="region"
+ aria-label={issue.message}>
+ <IssueTypeIcon
+ className="big-spacer-right spacer-left"
+ fill={colors.baseFontColor}
+ query={issue.type}
+ />
+ {issue.message}
+ </div>
+ );
+}
togglePopup={this.props.togglePopup}
/>
<IssueActionsBar
+ className="padded-left"
currentPopup={currentPopup}
issue={issue}
onAssign={this.props.onAssign}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { colors } from '../../app/theme';
-import { Issue as TypeIssue } from '../../types/types';
-import IssueTypeIcon from '../icons/IssueTypeIcon';
-import './Issue.css';
-
-export interface SecondaryIssueProps {
- issue: TypeIssue;
- onClick: (issueKey: string) => void;
-}
-
-export default function SecondaryIssue(props: SecondaryIssueProps) {
- const { issue } = props;
- return (
- <div
- className="issue display-flex-row display-flex-center padded-right secondary-issue"
- key={issue.key}
- onClick={() => props.onClick(issue.key)}
- role="region"
- aria-label={issue.message}>
- <IssueTypeIcon
- className="big-spacer-right spacer-left"
- fill={colors.baseFontColor}
- query={issue.type}
- />
- {issue.message}
- </div>
- );
-}
togglePopup={[MockFunction]}
/>
<IssueActionsBar
+ className="padded-left"
issue={
Object {
"actions": Array [],
togglePopup={[MockFunction]}
/>
<IssueActionsBar
+ className="padded-left"
issue={
Object {
"actions": Array [],
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import classNames from 'classnames';
import * as React from 'react';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { IssueResponse } from '../../../types/issues';
onAssign: (login: string) => void;
onChange: (issue: Issue) => void;
togglePopup: (popup: string, show?: boolean) => void;
+ className?: string;
}
interface State {
};
render() {
- const { issue } = this.props;
+ const { issue, className } = this.props;
const canAssign = issue.actions.includes('assign');
const canComment = issue.actions.includes('comment');
const canSetSeverity = issue.actions.includes('set_severity');
const isSecurityHotspot = issue.type === 'SECURITY_HOTSPOT';
return (
- <div className="issue-actions">
+ <div className={classNames(className, 'issue-actions')}>
<div className="issue-meta-list">
<div className="issue-meta">
<IssueType
export interface RawIssue {
actions: string[];
+ transitions?: string[];
+ tags?: string[];
assignee?: string;
author?: string;
comments?: Array<Comment>;
issue.resolution.DEPRECATED.rule_deprecated=Rule deprecated
issue.unresolved.description=Unresolved issues have not been addressed in any way.
+issue.action.permalink=Get permalink
issue.effort=Effort:
issue.x_effort={0} effort
issue.filter_similar_issues=Filter Similar Issues