* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
import { getSources } from '../../../api/components';
import IssueMessageBox from '../../../components/issue/IssueMessageBox';
import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
+import { Alert } from '../../../components/ui/Alert';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
+import { translate } from '../../../helpers/l10n';
import { BranchLike } from '../../../types/branch-like';
-import { isFile } from '../../../types/component';
+import { ComponentQualifier, isFile } from '../../../types/component';
+import { IssueStatus } from '../../../types/issues';
import {
Dict,
Duplication,
const isFlow = issue.secondaryLocations.length === 0;
const includeIssueLocation = isFlow ? isLastOccurenceOfPrimaryComponent : true;
+ const issueIsClosed = issue.status === IssueStatus.Closed;
+ const issueIsFileLevel = issue.componentQualifier === ComponentQualifier.File;
+ const closedIssueMessageKey = issueIsFileLevel
+ ? 'issue.closed.file_level'
+ : 'issue.closed.project_level';
+
return (
<>
+ {issueIsClosed && (
+ <Alert variant="success">
+ <FormattedMessage
+ id={closedIssueMessageKey}
+ defaultMessage={translate(closedIssueMessageKey)}
+ values={{
+ status: (
+ <strong>
+ {translate('issue.status', issue.status)} (
+ {issue.resolution ? translate('issue.resolution', issue.resolution) : '-'})
+ </strong>
+ ),
+ }}
+ />
+ </Alert>
+ )}
+
<IssueSourceViewerHeader
branchLike={branchLike}
+ className={issueIsClosed && !issueIsFileLevel ? 'null-spacer-bottom' : ''}
expandable={!fullyShown && isFile(snippetGroup.component.q)}
loading={loading}
onExpand={this.expandComponent}
sourceViewerFile={snippetGroup.component}
/>
- {issue.component === snippetGroup.component.key && issue.textRange === undefined && (
- <IssueSourceViewerScrollContext.Consumer>
- {(ctx) => (
- <IssueMessageBox
- selected={true}
- issue={issue}
- onClick={this.props.onIssueSelect}
- ref={ctx?.registerPrimaryLocationRef}
- />
- )}
- </IssueSourceViewerScrollContext.Consumer>
- )}
+ {issue.component === snippetGroup.component.key &&
+ issue.textRange === undefined &&
+ !issueIsClosed && (
+ <IssueSourceViewerScrollContext.Consumer>
+ {(ctx) => (
+ <IssueMessageBox
+ selected={true}
+ issue={issue}
+ onClick={this.props.onIssueSelect}
+ ref={ctx?.registerPrimaryLocationRef}
+ />
+ )}
+ </IssueSourceViewerScrollContext.Consumer>
+ )}
+
{snippetLines.map((snippet, index) => (
<SnippetViewer
key={snippets[index].index}
import { HttpStatus } from '../../../helpers/request';
import { BranchLike } from '../../../types/branch-like';
import { isFile } from '../../../types/component';
+import { IssueStatus } from '../../../types/issues';
import {
Dict,
DuplicatedFile,
this.setState({ loading: true });
try {
- const components = await getIssueFlowSnippets(issue.key);
+ const components =
+ issue.status === IssueStatus.Closed ? {} : await getIssueFlowSnippets(issue.key);
if (components[issue.component] === undefined) {
const issueComponent = await getComponentForSourceViewer({
component: issue.component,
components[issue.component].sources = sources;
}
}
+
if (this.mounted) {
this.setState({
components,
export interface Props {
branchLike: BranchLike | undefined;
+ className?: string;
expandable?: boolean;
displayProjectName?: boolean;
linkToProject?: boolean;
export default function IssueSourceViewerHeader(props: Props) {
const {
branchLike,
+ className,
expandable,
displayProjectName = true,
linkToProject = true,
return (
<div
- className="issue-source-viewer-header display-flex-row display-flex-space-between"
+ className={classNames(
+ 'issue-source-viewer-header display-flex-row display-flex-space-between',
+ className
+ )}
role="separator"
aria-label={sourceViewerFile.path}
>
import { shallow } from 'enzyme';
import { range, times } from 'lodash';
import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
import { getSources } from '../../../../api/components';
import IssueMessageBox from '../../../../components/issue/IssueMessageBox';
import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like';
} from '../../../../helpers/mocks/sources';
import { mockFlowLocation, mockIssue } from '../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { ComponentQualifier } from '../../../../types/component';
+import { IssueStatus } from '../../../../types/issues';
import { SnippetGroup } from '../../../../types/types';
import ComponentSourceSnippetGroupViewer from '../ComponentSourceSnippetGroupViewer';
import SnippetViewer from '../SnippetViewer';
expect(wrapper.find('ContextConsumer').dive().find(IssueMessageBox).exists()).toBe(true);
});
+it.each([
+ ['file-level', ComponentQualifier.File, 'issue.closed.file_level'],
+ ['project-level', ComponentQualifier.Project, 'issue.closed.project_level'],
+])(
+ 'should render a closed %s issue correctly',
+ async (_level, componentQualifier, expectedLabel) => {
+ // issue with secondary locations and no primary location
+ const issue = mockIssue(true, {
+ component: 'project:main.js',
+ componentQualifier,
+ flows: [],
+ textRange: undefined,
+ status: IssueStatus.Closed,
+ });
+
+ const wrapper = shallowRender({
+ issue,
+ snippetGroup: {
+ locations: [],
+ ...mockSnippetsByComponent('main.js', 'project', range(1, 10)),
+ },
+ });
+
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.find<FormattedMessage>(FormattedMessage).prop('id')).toEqual(expectedLabel);
+ expect(wrapper.find('ContextConsumer').exists()).toBe(false);
+ }
+);
+
it('should expand block', async () => {
(getSources as jest.Mock).mockResolvedValueOnce(
Object.values(mockSnippetsByComponent('a', 'project', range(6, 59)).sources)
} from '../../../../helpers/mocks/sources';
import { mockFlowLocation, mockIssue } from '../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { IssueStatus } from '../../../../types/issues';
import ComponentSourceSnippetGroupViewer from '../ComponentSourceSnippetGroupViewer';
import CrossComponentSourceViewer from '../CrossComponentSourceViewer';
expect(getIssueFlowSnippets).toHaveBeenCalledWith('foo');
});
+it('Should handle a closed issue', async () => {
+ const wrapper = shallowRender({ issue: mockIssue(true, { status: IssueStatus.Closed }) });
+ wrapper.instance().fetchIssueFlowSnippets();
+ await waitAndUpdate(wrapper);
+ expect(getIssueFlowSnippets).not.toHaveBeenCalled();
+});
+
it('Should handle no access rights', async () => {
(getIssueFlowSnippets as jest.Mock).mockRejectedValueOnce({ status: 403 });
"name": "master",
}
}
+ className=""
expandable={true}
loading={false}
onExpand={[Function]}
Test = 'TEST',
}
+export enum IssueStatus {
+ Open = 'OPEN',
+ Confirmed = 'CONFIRMED',
+ Reopened = 'REOPENED',
+ Resolved = 'RESOLVED',
+ Closed = 'CLOSED',
+}
+
interface Comment {
createdAt: string;
htmlText: string;
issue.tabs.code=Where is the issue?
issue.x_data_flows={0} data flow(s)
issue.execution_flow=Full execution flow
+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.action_select=Select issue
issues.action_select.label=Select issue {0}