import { themeBorder, themeColor } from '../helpers';
export const HtmlFormatter = styled.div`
- ${tw`sw-my-6`}
${tw`sw-body-sm`}
a {
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import styled from '@emotion/styled';
+import { ElementType } from 'react';
import tw from 'twin.macro';
import { themeColor, themeContrast } from '../helpers/theme';
);
}
-export function PageTitle({ text, className }: { className?: string; text: string }) {
+export function PageTitle({
+ text,
+ className,
+ as = 'h1',
+}: {
+ as?: ElementType;
+ className?: string;
+ text: string;
+}) {
return (
- <StyledPageTitle className={className} title={text}>
+ <StyledPageTitle as={as} className={className} title={text}>
{text}
</StyledPageTitle>
);
`;
export const StyledPageTitle = styled(StyledText)`
+ ${tw`sw-block`};
${tw`sw-text-base`}
color: ${themeColor('facetHeader')};
`;
--- /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 { TrashIcon as BaseTrashIcon } from '@primer/octicons-react';
+import { OcticonHoc } from './Icon';
+
+export const TrashIcon = OcticonHoc(BaseTrashIcon);
export { StatusOpenIcon } from './StatusOpenIcon';
export { StatusReopenedIcon } from './StatusReopenedIcon';
export { StatusResolvedIcon } from './StatusResolvedIcon';
+export { TrashIcon } from './TrashIcon';
export { TriangleDownIcon } from './TriangleDownIcon';
export { TriangleLeftIcon } from './TriangleLeftIcon';
export { TriangleRightIcon } from './TriangleRightIcon';
margin: 0 1px 0 0;
}
-.markdown-tips {
- font-size: var(--smallFontSize);
- color: var(--secondFontColor);
-}
-
.rule-desc,
.markdown {
line-height: 1.5;
fixTab: byRole('tab', { name: 'hotspots.tabs.fix_recommendations' }),
fixContent: byText('This is how to fix'),
showAllHotspotLink: byRole('link', { name: 'hotspot.filters.show_all' }),
+ activityTab: byRole('tab', { name: 'hotspots.tabs.activity' }),
+ addCommentButton: byRole('button', { name: 'hotspots.status.add_comment' }),
};
const hotspotsHandler = new SecurityHotspotServiceMock();
await user.click(await ui.activeAssignee.find());
await user.click(ui.inputAssignee.get());
- await user.keyboard('User');
+ await act(async () => {
+ await user.keyboard('User');
+ });
expect(searchUsers).toHaveBeenLastCalledWith({ q: 'User' });
await user.keyboard('{Enter}');
await user.click(ui.reviewButton.get());
await user.click(ui.toReviewStatus.get());
- await user.click(screen.getByRole('textbox', { name: 'hotspots.status.add_comment' }));
+ await user.click(screen.getByRole('textbox', { name: 'hotspots.status.add_comment_optional' }));
await user.keyboard(comment);
await act(async () => {
await user.click(ui.changeStatus.get());
});
+ await user.click(ui.activityTab.get());
expect(setSecurityHotspotStatus).toHaveBeenLastCalledWith('test-1', {
comment: 'COMMENT-TEXT',
resolution: undefined,
it('should be able to add, edit and remove own comments', async () => {
const uiComment = {
- saveButton: byRole('button', { name: 'save' }),
+ saveButton: byRole('button', { name: 'hotspots.comment.submit' }),
deleteButton: byRole('button', { name: 'delete' }),
};
const user = userEvent.setup();
const comment = 'This is a comment from john doe';
renderSecurityHotspotsApp();
- const commentSection = await ui.hotspotCommentBox.find();
+ await user.click(await ui.activityTab.find());
+ await user.click(ui.addCommentButton.get());
+
+ const commentSection = ui.hotspotCommentBox.get();
const submitButton = ui.commentSubmitButton.get();
// Add a new comment
--- /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 { ButtonPrimary, FormField, InputTextArea, Modal } from 'design-system';
+import * as React from 'react';
+import FormattingTips from '../../../components/common/FormattingTips';
+import { translate } from '../../../helpers/l10n';
+
+export interface HotspotCommentPopupProps {
+ value?: string;
+ onSubmit: (comment: string) => void;
+ onCancel: () => void;
+}
+
+export default function HotspotCommentModal(props: HotspotCommentPopupProps) {
+ const [comment, setComment] = React.useState(props.value ?? '');
+
+ return (
+ <Modal
+ headerTitle={translate(
+ props.value !== undefined ? 'issue.comment.edit' : 'hotspots.status.add_comment'
+ )}
+ onClose={props.onCancel}
+ body={
+ <FormField htmlFor="security-hotspot-comment" label={translate('hotspots.comment.field')}>
+ <InputTextArea
+ className="sw-mb-2 sw-resize-y"
+ id="security-hotspot-comment"
+ size="full"
+ onChange={(event) => setComment(event.target.value)}
+ rows={3}
+ value={comment}
+ />
+ <FormattingTips />
+ </FormField>
+ }
+ primaryButton={
+ <ButtonPrimary onClick={() => props.onSubmit(comment)} disabled={!comment}>
+ {translate('hotspots.comment.submit')}
+ </ButtonPrimary>
+ }
+ secondaryButtonLabel={translate('cancel')}
+ />
+ );
+}
+++ /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 FormattingTips from '../../../components/common/FormattingTips';
-import { Button, ResetButtonLink } from '../../../components/controls/buttons';
-import { translate } from '../../../helpers/l10n';
-
-export interface HotspotCommentPopupProps {
- markdownComment: string;
- onCommentEditSubmit: (comment: string) => void;
- onCancelEdit: () => void;
-}
-
-export default function HotspotCommentPopup(props: HotspotCommentPopupProps) {
- const [comment, setComment] = React.useState(props.markdownComment);
-
- return (
- <div className="issue-comment-bubble-popup">
- <div className="issue-comment-form-text">
- <textarea
- autoFocus={true}
- onChange={(event) => setComment(event.target.value)}
- rows={2}
- value={comment}
- />
- </div>
- <div className="spacer-top display-flex-space-between">
- <div className="issue-comment-form-tips">
- <FormattingTips />
- </div>
- <div className="">
- <Button
- className="little-spacer-right"
- onClick={() => props.onCommentEditSubmit(comment)}
- >
- {translate('save')}
- </Button>
- <ResetButtonLink
- onClick={() => {
- setComment('');
- props.onCancelEdit();
- }}
- >
- {translate('cancel')}
- </ResetButtonLink>
- </div>
- </div>
- </div>
- );
-}
*/
import classNames from 'classnames';
import * as React from 'react';
-import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
-import { ButtonLink } from '../../../components/controls/buttons';
import { IssueMessageHighlighting } from '../../../components/issue/IssueMessageHighlighting';
-import { translate } from '../../../helpers/l10n';
import { Hotspot } from '../../../types/security-hotspots';
-import { CurrentUser, isLoggedIn } from '../../../types/users';
import './HotspotPrimaryLocationBox.css';
const SCROLL_DELAY = 100;
export interface HotspotPrimaryLocationBoxProps {
hotspot: Hotspot;
- onCommentClick: () => void;
- currentUser: CurrentUser;
secondaryLocationSelected: boolean;
}
-export function HotspotPrimaryLocationBox(props: HotspotPrimaryLocationBoxProps) {
- const { hotspot, currentUser, secondaryLocationSelected } = props;
+export default function HotspotPrimaryLocationBox(props: HotspotPrimaryLocationBoxProps) {
+ const { hotspot, secondaryLocationSelected } = props;
const locationRef = React.useRef<HTMLDivElement>(null);
messageFormattings={hotspot.messageFormattings}
/>
</div>
- {isLoggedIn(currentUser) && (
- <ButtonLink
- className="nowrap big-spacer-left it__hs-add-comment"
- onClick={props.onCommentClick}
- >
- {translate('hotspots.comment.open')}
- </ButtonLink>
- )}
</div>
);
}
-
-export default withCurrentUserContext(HotspotPrimaryLocationBox);
* 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 styled from '@emotion/styled';
+import {
+ DangerButtonPrimary,
+ DestructiveIcon,
+ HtmlFormatter,
+ InteractiveIcon,
+ LightLabel,
+ Modal,
+ PencilIcon,
+ TrashIcon,
+ themeBorder,
+} from 'design-system';
import * as React from 'react';
-import { Button, ButtonLink, DeleteButton, EditButton } from '../../../components/controls/buttons';
-import Dropdown, { DropdownOverlay } from '../../../components/controls/Dropdown';
-import Toggler from '../../../components/controls/Toggler';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import IssueChangelogDiff from '../../../components/issue/components/IssueChangelogDiff';
import Avatar from '../../../components/ui/Avatar';
-import { PopupPlacement } from '../../../components/ui/popups';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { sanitizeUserInput } from '../../../helpers/sanitize';
import { Hotspot, ReviewHistoryType } from '../../../types/security-hotspots';
import { getHotspotReviewHistory } from '../utils';
-import HotspotCommentPopup from './HotspotCommentPopup';
+import HotspotCommentModal from './HotspotCommentModal';
export interface HotspotReviewHistoryProps {
hotspot: Hotspot;
onDeleteComment: (key: string) => void;
onEditComment: (key: string, comment: string) => void;
- onShowFullHistory: () => void;
- showFullHistory: boolean;
}
-export const MAX_RECENT_ACTIVITY = 5;
-
export default function HotspotReviewHistory(props: HotspotReviewHistoryProps) {
- const { hotspot, showFullHistory } = props;
- const fullReviewHistory = getHotspotReviewHistory(hotspot);
- const [editedCommentKey, setEditedCommentKey] = React.useState('');
-
- const reviewHistory = showFullHistory
- ? fullReviewHistory
- : fullReviewHistory.slice(0, MAX_RECENT_ACTIVITY);
+ const { hotspot } = props;
+ const history = getHotspotReviewHistory(hotspot);
+ const [editCommentKey, setEditCommentKey] = React.useState('');
+ const [deleteCommentKey, setDeleteCommentKey] = React.useState('');
return (
- <>
- <ul>
- {reviewHistory.map((historyElt, historyIndex) => {
- const { user, type, diffs, date, html, key, updatable, markdown } = historyElt;
- return (
- <li
- className={classNames('padded-top padded-bottom', {
- 'bordered-top': historyIndex > 0,
- })}
- key={historyIndex}
- >
- <div className="display-flex-center">
- {user.name && (
- <>
- <Avatar
- className="little-spacer-right"
- hash={user.avatar}
- name={user.name}
- size={20}
- />
- <strong>
- {user.active
- ? user.name
- : translateWithParameters('user.x_deleted', user.name)}
- </strong>
- {type === ReviewHistoryType.Creation && (
- <span className="little-spacer-left">
- {translate('hotspots.review_history.created')}
- </span>
- )}
- {type === ReviewHistoryType.Comment && (
- <span className="little-spacer-left">
- {translate('hotspots.review_history.comment_added')}
- </span>
- )}
- <span className="little-spacer-left little-spacer-right">-</span>
- </>
- )}
- <DateTimeFormatter date={date} />
- </div>
-
- {type === ReviewHistoryType.Diff && diffs && (
- <div className="spacer-top">
- {diffs.map((diff, diffIndex) => (
- <IssueChangelogDiff diff={diff} key={diffIndex} />
- ))}
+ <ul>
+ {history.map((historyElt, historyIndex) => {
+ const { user, type, diffs, date, html, key, updatable, markdown } = historyElt;
+ return (
+ <li className="sw-p-2 sw-body-sm" key={historyIndex}>
+ <div className="sw-body-sm-highlight sw-mb-1">
+ <DateTimeFormatter date={date} />
+ </div>
+ <LightLabel as="div" className="sw-flex sw-gap-2">
+ {user.name && (
+ <div className="sw-flex sw-items-center sw-gap-1">
+ <Avatar hash={user.avatar} name={user.name} size={20} />
+ <span className="sw-body-sm-highlight">
+ {user.active ? user.name : translateWithParameters('user.x_deleted', user.name)}
+ </span>
</div>
)}
- {type === ReviewHistoryType.Comment && key && html && markdown && (
- <div className="spacer-top display-flex-space-between">
- <div
- className="markdown"
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: sanitizeUserInput(html) }}
+ {type === ReviewHistoryType.Creation && translate('hotspots.review_history.created')}
+
+ {type === ReviewHistoryType.Comment &&
+ translate('hotspots.review_history.comment_added')}
+ </LightLabel>
+
+ {type === ReviewHistoryType.Diff && diffs && (
+ <div className="sw-mt-2">
+ {diffs.map((diff, diffIndex) => (
+ <IssueChangelogDiff diff={diff} key={diffIndex} />
+ ))}
+ </div>
+ )}
+
+ {type === ReviewHistoryType.Comment && key && html && markdown && (
+ <div className="sw-mt-2 sw-flex sw-justify-between sw-h-5">
+ <CommentBox
+ className="sw-pl-2 sw-ml-2 sw-body-sm"
+ // eslint-disable-next-line react/no-danger
+ dangerouslySetInnerHTML={{ __html: sanitizeUserInput(html) }}
+ />
+
+ {updatable && (
+ <div className="sw-flex sw-gap-6">
+ <InteractiveIcon
+ Icon={PencilIcon}
+ aria-label={translate('issue.comment.edit')}
+ onClick={() => setEditCommentKey(key)}
+ size="small"
+ stopPropagation={false}
+ />
+ <DestructiveIcon
+ Icon={TrashIcon}
+ aria-label={translate('issue.comment.delete')}
+ onClick={() => setDeleteCommentKey(key)}
+ size="small"
+ stopPropagation={false}
+ />
+ </div>
+ )}
+
+ {editCommentKey && (
+ <HotspotCommentModal
+ value={markdown}
+ onCancel={() => setEditCommentKey('')}
+ onSubmit={(comment) => {
+ setEditCommentKey('');
+ props.onEditComment(key, comment);
+ }}
/>
- {updatable && (
- <div>
- <div className="dropdown">
- <Toggler
- onRequestClose={() => {
- setEditedCommentKey('');
- }}
- open={key === editedCommentKey}
- overlay={
- <DropdownOverlay placement={PopupPlacement.BottomRight}>
- <HotspotCommentPopup
- markdownComment={markdown}
- onCancelEdit={() => setEditedCommentKey('')}
- onCommentEditSubmit={(comment) => {
- setEditedCommentKey('');
- props.onEditComment(key, comment);
- }}
- />
- </DropdownOverlay>
- }
- >
- <EditButton
- title="issue.comment.edit"
- className="button-small"
- onClick={() => setEditedCommentKey(key)}
- />
- </Toggler>
- </div>
- <Dropdown
- onOpen={() => setEditedCommentKey('')}
- overlay={
- <div className="padded abs-width-150">
- <p>{translate('issue.comment.delete_confirm_message')}</p>
- <Button
- className="button-red big-spacer-top pull-right"
- onClick={() => props.onDeleteComment(key)}
- >
- {translate('delete')}
- </Button>
- </div>
- }
- overlayPlacement={PopupPlacement.BottomRight}
+ )}
+
+ {deleteCommentKey && (
+ <Modal
+ headerTitle={translate('issue.comment.delete')}
+ onClose={() => setDeleteCommentKey('')}
+ body={<p>{translate('issue.comment.delete_confirm_message')}</p>}
+ primaryButton={
+ <DangerButtonPrimary
+ onClick={() => {
+ setDeleteCommentKey('');
+ props.onDeleteComment(key);
+ }}
>
- <DeleteButton title="issue.comment.delete" className="button-small" />
- </Dropdown>
- </div>
- )}
- </div>
- )}
- </li>
- );
- })}
- </ul>
- {!showFullHistory && fullReviewHistory.length > MAX_RECENT_ACTIVITY && (
- <ButtonLink className="spacer-top" onClick={props.onShowFullHistory}>
- {translate('show_all')}
- </ButtonLink>
- )}
- </>
+ {translate('delete')}
+ </DangerButtonPrimary>
+ }
+ secondaryButtonLabel={translate('cancel')}
+ />
+ )}
+ </div>
+ )}
+ </li>
+ );
+ })}
+ </ul>
);
}
+
+const CommentBox = styled(HtmlFormatter)`
+ border-left: ${themeBorder('default', 'activityCommentPipe')};
+`;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { ButtonSecondary, PageTitle } from 'design-system';
import * as React from 'react';
import {
commentSecurityHotspot,
deleteSecurityHotspotComment,
editSecurityHotspotComment,
} from '../../../api/security-hotspots';
-import FormattingTips from '../../../components/common/FormattingTips';
-import { Button } from '../../../components/controls/buttons';
import { translate } from '../../../helpers/l10n';
import { Hotspot } from '../../../types/security-hotspots';
import { CurrentUser, isLoggedIn } from '../../../types/users';
+import HotspotCommentModal from './HotspotCommentModal';
import HotspotReviewHistory from './HotspotReviewHistory';
interface Props {
currentUser: CurrentUser;
hotspot: Hotspot;
- commentTextRef: React.RefObject<HTMLTextAreaElement>;
onCommentUpdate: () => void;
}
interface State {
- comment: string;
- showFullHistory: boolean;
+ showAddCommentModal: boolean;
}
export default class HotspotReviewHistoryAndComments extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
- comment: '',
- showFullHistory: false,
+ showAddCommentModal: false,
};
}
- componentDidUpdate(prevProps: Props) {
- if (prevProps.hotspot.key !== this.props.hotspot.key) {
- this.setState({
- comment: '',
- showFullHistory: false,
- });
- }
- }
-
- handleCommentChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
- this.setState({ comment: event.target.value });
- };
-
- handleSubmitComment = () => {
- return commentSecurityHotspot(this.props.hotspot.key, this.state.comment).then(() => {
- this.setState({ comment: '' });
+ handleSubmitComment = (comment: string) => {
+ return commentSecurityHotspot(this.props.hotspot.key, comment).then(() => {
+ this.setState({ showAddCommentModal: false });
this.props.onCommentUpdate();
});
};
});
};
- handleShowFullHistory = () => {
- this.setState({ showFullHistory: true });
+ handleShowCommentModal = () => {
+ this.setState({ showAddCommentModal: true });
+ };
+
+ handleHideCommentModal = () => {
+ this.setState({ showAddCommentModal: false });
};
render() {
- const { currentUser, hotspot, commentTextRef } = this.props;
- const { comment, showFullHistory } = this.state;
+ const { currentUser, hotspot } = this.props;
+ const { showAddCommentModal } = this.state;
return (
- <div className="padded it__hs-review-history">
+ <div className="it__hs-review-history">
+ <PageTitle
+ as="h2"
+ className="sw-body-md-highlight"
+ text={translate('hotspot.section.activity')}
+ />
+
{isLoggedIn(currentUser) && (
- <>
- <label htmlFor="security-hotspot-comment">{translate('hotspots.comment.field')}</label>
- <textarea
- id="security-hotspot-comment"
- className="form-field fixed-width width-100 spacer-bottom"
- onChange={this.handleCommentChange}
- ref={commentTextRef}
- rows={2}
- value={comment}
- />
- <div className="display-flex-space-between display-flex-center ">
- <FormattingTips className="huge-spacer-bottom" />
- <div>
- <Button
- className="huge-spacer-bottom"
- id="hotspot-comment-box-submit"
- onClick={this.handleSubmitComment}
- >
- {translate('hotspots.comment.submit')}
- </Button>
- </div>
- </div>
- </>
+ <ButtonSecondary className="sw-mt-4 sw-mb-2" onClick={this.handleShowCommentModal}>
+ {translate('hotspots.status.add_comment')}
+ </ButtonSecondary>
)}
- <h2 className="spacer-top big-spacer-bottom">{translate('hotspot.section.activity')}</h2>
-
<HotspotReviewHistory
hotspot={hotspot}
onDeleteComment={this.handleDeleteComment}
onEditComment={this.handleEditComment}
- onShowFullHistory={this.handleShowFullHistory}
- showFullHistory={showFullHistory}
/>
+
+ {showAddCommentModal && (
+ <HotspotCommentModal
+ onCancel={this.handleHideCommentModal}
+ onSubmit={(comment) => {
+ this.handleSubmitComment(comment);
+ }}
+ />
+ )}
</div>
);
}
branchLike?: BranchLike;
component: Component;
hotspot: Hotspot;
- onCommentButtonClick: () => void;
onLocationSelect: (index: number) => void;
selectedHotspotLocation?: number;
}
hotspot={hotspot}
loading={loading}
locations={locations}
- onCommentButtonClick={this.props.onCommentButtonClick}
onExpandBlock={this.handleExpansion}
onSymbolClick={this.handleSymbolClick}
onLocationSelect={this.props.onLocationSelect}
loading: boolean;
locations: { [line: number]: LinearIssueLocation[] };
selectedHotspotLocation?: number;
- onCommentButtonClick: () => void;
onExpandBlock: (direction: ExpandDirection) => Promise<void>;
onSymbolClick: (symbols: string[]) => void;
onLocationSelect: (index: number) => void;
() => (
<HotspotPrimaryLocationBox
hotspot={hotspot}
- onCommentClick={props.onCommentButtonClick}
secondaryLocationSelected={secondaryLocationSelected}
/>
),
- [hotspot, secondaryLocationSelected, props.onCommentButtonClick]
+ [hotspot, secondaryLocationSelected]
);
const renderHotspotBoxInLine = (line: SourceLine) =>
export default class HotspotViewer extends React.PureComponent<Props, State> {
mounted = false;
state: State;
- commentTextRef: React.RefObject<HTMLTextAreaElement>;
constructor(props: Props) {
super(props);
- this.commentTextRef = React.createRef<HTMLTextAreaElement>();
this.state = { loading: false };
}
}
};
- handleScrollToCommentForm = () => {
- if (this.commentTextRef.current) {
- this.commentTextRef.current.scrollIntoView({
- block: 'center',
- behavior: 'smooth',
- inline: 'center',
- });
- this.commentTextRef.current.focus({ preventScroll: true });
- }
- };
-
handleSwitchFilterToStatusOfUpdatedHotspot = () => {
const { lastStatusChangedTo } = this.state;
if (lastStatusChangedTo) {
<HotspotViewerRenderer
standards={standards}
component={component}
- commentTextRef={this.commentTextRef}
hotspot={hotspot}
ruleDescriptionSections={ruleDescriptionSections}
loading={loading}
- onShowCommentForm={this.handleScrollToCommentForm}
onUpdateHotspot={this.handleHotspotUpdate}
onLocationClick={this.props.onLocationClick}
selectedHotspotLocation={selectedHotspotLocation}
hotspot?: Hotspot;
ruleDescriptionSections?: RuleDescriptionSection[];
loading: boolean;
- commentTextRef: React.RefObject<HTMLTextAreaElement>;
onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise<void>;
- onShowCommentForm: () => void;
onLocationClick: (index: number) => void;
selectedHotspotLocation?: number;
standards?: Standards;
currentUser,
hotspot,
loading,
- commentTextRef,
selectedHotspotLocation,
ruleDescriptionSections,
standards,
const branchLike = hotspot && fillBranchLike(hotspot.project.branch, hotspot.project.pullRequest);
return (
- <DeferredSpinner className="big-spacer-left big-spacer-top" loading={loading}>
+ <>
+ <DeferredSpinner className="sw-ml-4 sw-mt-4" loading={loading} />
+
{hotspot && (
<div className="sw-box-border sw-p-6">
<HotspotHeader
branchLike={branchLike}
/>
<HotspotViewerTabs
+ activityTabContent={
+ <HotspotReviewHistoryAndComments
+ currentUser={currentUser}
+ hotspot={hotspot}
+ onCommentUpdate={props.onUpdateHotspot}
+ />
+ }
codeTabContent={
<HotspotSnippetContainer
branchLike={branchLike}
component={component}
hotspot={hotspot}
- onCommentButtonClick={props.onShowCommentForm}
onLocationSelect={props.onLocationClick}
selectedHotspotLocation={selectedHotspotLocation}
/>
ruleDescriptionSections={ruleDescriptionSections}
selectedHotspotLocation={selectedHotspotLocation}
/>
- <HotspotReviewHistoryAndComments
- commentTextRef={commentTextRef}
- currentUser={currentUser}
- hotspot={hotspot}
- onCommentUpdate={props.onUpdateHotspot}
- />
</div>
)}
- </DeferredSpinner>
+ </>
);
}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { ToggleButton, getTabId, getTabPanelId } from 'design-system';
-import { groupBy } from 'lodash';
+import { groupBy, omit } from 'lodash';
import * as React from 'react';
import RuleDescription from '../../../components/rules/RuleDescription';
import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
import { RuleDescriptionSection, RuleDescriptionSections } from '../../coding-rules/rule';
interface Props {
+ activityTabContent: React.ReactNode;
codeTabContent: React.ReactNode;
hotspot: Hotspot;
ruleDescriptionSections?: RuleDescriptionSection[];
interface Tab {
value: TabKeys;
label: string;
- content: React.ReactNode;
}
export enum TabKeys {
}
componentDidUpdate(prevProps: Props) {
- if (
- this.props.hotspot.key !== prevProps.hotspot.key ||
- prevProps.codeTabContent !== this.props.codeTabContent
- ) {
+ if (this.props.hotspot.key !== prevProps.hotspot.key) {
const tabs = this.computeTabs();
this.setState({
currentTab: tabs[0],
};
computeTabs() {
- const { ruleDescriptionSections, codeTabContent } = this.props;
+ const { ruleDescriptionSections } = this.props;
const descriptionSectionsByKey = groupBy(ruleDescriptionSections, (section) => section.key);
- const rootCauseDescriptionSections =
- descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
- descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE];
return [
{
value: TabKeys.Code,
label: translate('hotspots.tabs.code'),
- content: codeTabContent,
+ show: true,
},
{
value: TabKeys.RiskDescription,
label: translate('hotspots.tabs.risk_description'),
- content: rootCauseDescriptionSections && (
- <RuleDescription sections={rootCauseDescriptionSections} />
- ),
+ show:
+ descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
+ descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE],
},
{
value: TabKeys.VulnerabilityDescription,
label: translate('hotspots.tabs.vulnerability_description'),
- content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
- <RuleDescription
- sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
- />
- ),
+ show: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] !== undefined,
},
{
value: TabKeys.FixRecommendation,
label: translate('hotspots.tabs.fix_recommendations'),
- content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
- <RuleDescription
- sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
- />
- ),
+ show: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] !== undefined,
+ },
+ {
+ value: TabKeys.Activity,
+ label: translate('hotspots.tabs.activity'),
+ show: true,
},
- ].filter((tab) => tab.content);
+ ]
+ .filter((tab) => tab.show)
+ .map((tab) => omit(tab, 'show'));
}
selectNeighboringTab(shift: number) {
}
render() {
+ const { ruleDescriptionSections, codeTabContent, activityTabContent } = this.props;
const { tabs, currentTab } = this.state;
+
+ const descriptionSectionsByKey = groupBy(ruleDescriptionSections, (section) => section.key);
+ const rootCauseDescriptionSections =
+ descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
+ descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE];
+
return (
<>
<ToggleButton
id={getTabPanelId(currentTab.value)}
role="tabpanel"
>
- {currentTab.content}
+ {currentTab.value === TabKeys.Code && codeTabContent}
+
+ {currentTab.value === TabKeys.RiskDescription && rootCauseDescriptionSections && (
+ <RuleDescription sections={rootCauseDescriptionSections} />
+ )}
+
+ {currentTab.value === TabKeys.VulnerabilityDescription &&
+ descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
+ <RuleDescription
+ sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
+ />
+ )}
+
+ {currentTab.value === TabKeys.FixRecommendation &&
+ descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
+ <RuleDescription
+ sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
+ />
+ )}
+
+ {currentTab.value === TabKeys.Activity && activityTabContent}
</div>
</>
);
{renderOption(HotspotStatusOption.ACKNOWLEDGED)}
{renderOption(HotspotStatusOption.FIXED)}
{renderOption(HotspotStatusOption.SAFE)}
- <FormField htmlFor="comment-textarea" label={translate('hotspots.status.add_comment')}>
+ <FormField
+ htmlFor="comment-textarea"
+ label={translate('hotspots.status.add_comment_optional')}
+ >
<InputTextArea
className="sw-mb-2 sw-resize-y"
id="comment-textarea"
import { translate } from '../../helpers/l10n';
import { getFormattingHelpUrl } from '../../helpers/urls';
-interface Props {
+export interface FormattingTipsProps {
className?: string;
}
-export default function FormattingTips({ className }: Props) {
+export default function FormattingTips({ className }: FormattingTipsProps) {
const handleClick = React.useCallback((evt: React.MouseEvent<HTMLAnchorElement>) => {
evt.preventDefault();
window.open(
}
const StyledHtmlFormatter = styled(HtmlFormatter)`
+ margin-top: 1.5rem;
+ margin-bottom: 1.5rem;
+
.code-difference-container {
display: flex;
flex-direction: column;
hotspots.tabs.risk_description=What's the risk?
hotspots.tabs.vulnerability_description=Assess the risk
hotspots.tabs.fix_recommendations=How can I fix it?
+hotspots.tabs.activity=Activity
hotspots.review_history.created=created Security Hotspot
hotspots.review_history.comment_added=added a comment
hotspots.comment.field=Comment:
hotspots.status.review=Review
hotspots.status.review_title=Review Security Hotspot
hotspots.status.select=Select a status
-hotspots.status.add_comment=Add a comment (Optional)
+hotspots.status.add_comment=Add a comment
+hotspots.status.add_comment_optional=Add a comment (Optional)
hotspots.status.change_status=Change status
hotspots.status_option.TO_REVIEW=To review
hotspot.filters.status.safe=Safe
hotspot.filters.by_file_or_list_x=Your hotspots are currently filtered, {show_all_link}
hotspot.filters.show_all=show all hotspots
-hotspot.section.activity=Recent activity:
+hotspot.section.activity=Activity
hotspots.reviewed.tooltip=Percentage of open Security Hotspots that have been reviewed (Acknowledged, Fixed or Safe)
hotspots.review_hotspot=Review Hotspot