component: Component; | component: Component; | ||||
onSelect: (branchLike: BranchLike) => void; | onSelect: (branchLike: BranchLike) => void; | ||||
selected: boolean; | selected: boolean; | ||||
indent: boolean; | |||||
setSelectedNode?: (node: HTMLLIElement) => void; | setSelectedNode?: (node: HTMLLIElement) => void; | ||||
} | } | ||||
export function MenuItem(props: MenuItemProps) { | export function MenuItem(props: MenuItemProps) { | ||||
const { branchLike, component, setSelectedNode, onSelect, selected } = props; | |||||
const { branchLike, component, setSelectedNode, onSelect, selected, indent } = props; | |||||
const displayName = getBranchLikeDisplayName(branchLike); | const displayName = getBranchLikeDisplayName(branchLike); | ||||
return ( | return ( | ||||
<ItemButton | <ItemButton | ||||
className={classNames({ active: selected })} | |||||
className={classNames({ active: selected, 'sw-pl-6': indent })} | |||||
innerRef={selected ? setSelectedNode : undefined} | innerRef={selected ? setSelectedNode : undefined} | ||||
onClick={() => { | onClick={() => { | ||||
onSelect(branchLike); | onSelect(branchLike); |
const { branchLikeTree, component, hasResults, onSelect, selectedBranchLike } = props; | const { branchLikeTree, component, hasResults, onSelect, selectedBranchLike } = props; | ||||
const renderItem = (branchLike: BranchLike) => ( | |||||
const renderItem = (branchLike: BranchLike, indent = false) => ( | |||||
<MenuItem | <MenuItem | ||||
branchLike={branchLike} | branchLike={branchLike} | ||||
component={component} | component={component} | ||||
onSelect={onSelect} | onSelect={onSelect} | ||||
selected={isSameBranchLike(branchLike, selectedBranchLike)} | selected={isSameBranchLike(branchLike, selectedBranchLike)} | ||||
setSelectedNode={(node) => (selectedNode = node)} | setSelectedNode={(node) => (selectedNode = node)} | ||||
indent={indent} | |||||
/> | /> | ||||
); | ); | ||||
<ItemDivider /> | <ItemDivider /> | ||||
<ItemHeader>{translate('branch_like_navigation.pull_requests')}</ItemHeader> | <ItemHeader>{translate('branch_like_navigation.pull_requests')}</ItemHeader> | ||||
<ItemDivider /> | <ItemDivider /> | ||||
{tree.pullRequests.map((pr) => renderItem(pr))} | |||||
{tree.pullRequests.map((pr) => renderItem(pr, true))} | |||||
{tree.pullRequests.length > 0 && <ItemDivider />} | |||||
</> | </> | ||||
)} | )} | ||||
</React.Fragment> | </React.Fragment> |
* along with this program; if not, write to the Free Software Foundation, | * along with this program; if not, write to the Free Software Foundation, | ||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import { | |||||
DangerButtonSecondary, | |||||
DeferredSpinner, | |||||
FlagMessage, | |||||
HtmlFormatter, | |||||
Modal, | |||||
} from 'design-system'; | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { dismissAnalysisWarning, getTask } from '../../api/ce'; | import { dismissAnalysisWarning, getTask } from '../../api/ce'; | ||||
import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext'; | import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext'; | ||||
import Modal from '../../components/controls/Modal'; | |||||
import { ButtonLink, ResetButtonLink } from '../../components/controls/buttons'; | |||||
import WarningIcon from '../../components/icons/WarningIcon'; | |||||
import DeferredSpinner from '../../components/ui/DeferredSpinner'; | |||||
import { translate } from '../../helpers/l10n'; | import { translate } from '../../helpers/l10n'; | ||||
import { sanitizeStringRestricted } from '../../helpers/sanitize'; | import { sanitizeStringRestricted } from '../../helpers/sanitize'; | ||||
import { TaskWarning } from '../../types/tasks'; | import { TaskWarning } from '../../types/tasks'; | ||||
const header = translate('warnings'); | const header = translate('warnings'); | ||||
return ( | |||||
<Modal contentLabel={header} onRequestClose={this.props.onClose}> | |||||
<header className="modal-head"> | |||||
<h2>{header}</h2> | |||||
</header> | |||||
<div className="modal-body modal-container js-analysis-warnings"> | |||||
<DeferredSpinner loading={loading}> | |||||
{warnings.map(({ dismissable, key, message }) => ( | |||||
<div className="panel panel-vertical" key={key}> | |||||
<WarningIcon className="pull-left spacer-right" /> | |||||
<div className="overflow-hidden markdown"> | |||||
const body = ( | |||||
<DeferredSpinner loading={loading}> | |||||
{warnings.map(({ dismissable, key, message }) => ( | |||||
<React.Fragment key={key}> | |||||
<div className="sw-flex sw-items-center sw-mt-2"> | |||||
<FlagMessage variant="warning"> | |||||
<HtmlFormatter> | |||||
<span | <span | ||||
// eslint-disable-next-line react/no-danger | // eslint-disable-next-line react/no-danger | ||||
dangerouslySetInnerHTML={{ | dangerouslySetInnerHTML={{ | ||||
__html: sanitizeStringRestricted(message.trim().replace(/\n/g, '<br />')), | __html: sanitizeStringRestricted(message.trim().replace(/\n/g, '<br />')), | ||||
}} | }} | ||||
/> | /> | ||||
</HtmlFormatter> | |||||
</FlagMessage> | |||||
</div> | |||||
<div> | |||||
{dismissable && currentUser.isLoggedIn && ( | |||||
<div className="sw-mt-4"> | |||||
<DangerButtonSecondary | |||||
disabled={Boolean(dismissedWarning)} | |||||
onClick={() => { | |||||
this.handleDismissMessage(key); | |||||
}} | |||||
> | |||||
{translate('dismiss_permanently')} | |||||
</DangerButtonSecondary> | |||||
{dismissable && currentUser.isLoggedIn && ( | |||||
<div className="spacer-top display-flex-inline"> | |||||
<ButtonLink | |||||
disabled={Boolean(dismissedWarning)} | |||||
onClick={() => { | |||||
this.handleDismissMessage(key); | |||||
}} | |||||
> | |||||
{translate('dismiss_permanently')} | |||||
</ButtonLink> | |||||
{dismissedWarning === key && <i className="spinner spacer-left" />} | |||||
</div> | |||||
)} | |||||
<DeferredSpinner className="sw-ml-2" loading={dismissedWarning === key} /> | |||||
</div> | </div> | ||||
</div> | |||||
))} | |||||
</DeferredSpinner> | |||||
</div> | |||||
<footer className="modal-foot"> | |||||
<ResetButtonLink onClick={this.props.onClose}>{translate('close')}</ResetButtonLink> | |||||
</footer> | |||||
</Modal> | |||||
)} | |||||
</div> | |||||
</React.Fragment> | |||||
))} | |||||
</DeferredSpinner> | |||||
); | |||||
return ( | |||||
<Modal | |||||
headerTitle={header} | |||||
onClose={this.props.onClose} | |||||
body={body} | |||||
primaryButton={null} | |||||
secondaryButtonLabel={translate('close')} | |||||
/> | |||||
); | ); | ||||
} | } | ||||
} | } |
*/ | */ | ||||
import { shallow } from 'enzyme'; | import { shallow } from 'enzyme'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { dismissAnalysisWarning, getTask } from '../../../api/ce'; | |||||
import { getTask } from '../../../api/ce'; | |||||
import { mockTaskWarning } from '../../../helpers/mocks/tasks'; | import { mockTaskWarning } from '../../../helpers/mocks/tasks'; | ||||
import { mockCurrentUser } from '../../../helpers/testMocks'; | import { mockCurrentUser } from '../../../helpers/testMocks'; | ||||
import { waitAndUpdate } from '../../../helpers/testUtils'; | import { waitAndUpdate } from '../../../helpers/testUtils'; | ||||
import { ButtonLink } from '../../controls/buttons'; | |||||
import { AnalysisWarningsModal } from '../AnalysisWarningsModal'; | import { AnalysisWarningsModal } from '../AnalysisWarningsModal'; | ||||
jest.mock('../../../api/ce', () => ({ | jest.mock('../../../api/ce', () => ({ | ||||
expect(getTask).toHaveBeenCalledWith('abcd1234', ['warnings']); | expect(getTask).toHaveBeenCalledWith('abcd1234', ['warnings']); | ||||
}); | }); | ||||
it('should correctly handle dismissing warnings', async () => { | |||||
const onWarningDismiss = jest.fn(); | |||||
const wrapper = shallowRender({ | |||||
componentKey: 'foo', | |||||
onWarningDismiss, | |||||
warnings: [mockTaskWarning({ key: 'bar', dismissable: true })], | |||||
}); | |||||
const { onClick } = wrapper.find(ButtonLink).at(0).props(); | |||||
if (onClick) { | |||||
onClick(); | |||||
} | |||||
await waitAndUpdate(wrapper); | |||||
expect(dismissAnalysisWarning).toHaveBeenCalledWith('foo', 'bar'); | |||||
expect(onWarningDismiss).toHaveBeenCalled(); | |||||
}); | |||||
it('should correctly handle updates', async () => { | it('should correctly handle updates', async () => { | ||||
const wrapper = shallowRender(); | const wrapper = shallowRender(); | ||||
// Jest Snapshot v1, https://goo.gl/fbAQLP | // Jest Snapshot v1, https://goo.gl/fbAQLP | ||||
exports[`should fetch task warnings if it has to 1`] = ` | exports[`should fetch task warnings if it has to 1`] = ` | ||||
<Modal | |||||
contentLabel="warnings" | |||||
onRequestClose={[MockFunction]} | |||||
> | |||||
<header | |||||
className="modal-head" | |||||
> | |||||
<h2> | |||||
warnings | |||||
</h2> | |||||
</header> | |||||
<div | |||||
className="modal-body modal-container js-analysis-warnings" | |||||
> | |||||
<Gl | |||||
body={ | |||||
<DeferredSpinner | <DeferredSpinner | ||||
loading={false} | loading={false} | ||||
> | > | ||||
<div | |||||
className="panel panel-vertical" | |||||
key="message foo" | |||||
> | |||||
<WarningIcon | |||||
className="pull-left spacer-right" | |||||
/> | |||||
<React.Fragment> | |||||
<div | <div | ||||
className="overflow-hidden markdown" | |||||
className="sw-flex sw-items-center sw-mt-2" | |||||
> | > | ||||
<span | |||||
dangerouslySetInnerHTML={ | |||||
{ | |||||
"__html": "message foo", | |||||
} | |||||
} | |||||
/> | |||||
<FlagMessage | |||||
variant="warning" | |||||
> | |||||
<HtmlFormatter> | |||||
<span | |||||
dangerouslySetInnerHTML={ | |||||
{ | |||||
"__html": "message foo", | |||||
} | |||||
} | |||||
/> | |||||
</HtmlFormatter> | |||||
</FlagMessage> | |||||
</div> | </div> | ||||
</div> | |||||
<div | |||||
className="panel panel-vertical" | |||||
key="message-bar" | |||||
> | |||||
<WarningIcon | |||||
className="pull-left spacer-right" | |||||
/> | |||||
<div /> | |||||
</React.Fragment> | |||||
<React.Fragment> | |||||
<div | <div | ||||
className="overflow-hidden markdown" | |||||
className="sw-flex sw-items-center sw-mt-2" | |||||
> | > | ||||
<span | |||||
dangerouslySetInnerHTML={ | |||||
{ | |||||
"__html": "message-bar", | |||||
} | |||||
} | |||||
/> | |||||
<FlagMessage | |||||
variant="warning" | |||||
> | |||||
<HtmlFormatter> | |||||
<span | |||||
dangerouslySetInnerHTML={ | |||||
{ | |||||
"__html": "message-bar", | |||||
} | |||||
} | |||||
/> | |||||
</HtmlFormatter> | |||||
</FlagMessage> | |||||
</div> | </div> | ||||
</div> | |||||
<div | |||||
className="panel panel-vertical" | |||||
key="multiline message | |||||
secondline | |||||
third line" | |||||
> | |||||
<WarningIcon | |||||
className="pull-left spacer-right" | |||||
/> | |||||
<div /> | |||||
</React.Fragment> | |||||
<React.Fragment> | |||||
<div | <div | ||||
className="overflow-hidden markdown" | |||||
className="sw-flex sw-items-center sw-mt-2" | |||||
> | > | ||||
<span | |||||
dangerouslySetInnerHTML={ | |||||
{ | |||||
"__html": "multiline message<br>secondline<br> third line", | |||||
} | |||||
} | |||||
/> | |||||
<FlagMessage | |||||
variant="warning" | |||||
> | |||||
<HtmlFormatter> | |||||
<span | |||||
dangerouslySetInnerHTML={ | |||||
{ | |||||
"__html": "multiline message<br>secondline<br> third line", | |||||
} | |||||
} | |||||
/> | |||||
</HtmlFormatter> | |||||
</FlagMessage> | |||||
</div> | </div> | ||||
</div> | |||||
<div /> | |||||
</React.Fragment> | |||||
</DeferredSpinner> | </DeferredSpinner> | ||||
</div> | |||||
<footer | |||||
className="modal-foot" | |||||
> | |||||
<ResetButtonLink | |||||
onClick={[MockFunction]} | |||||
> | |||||
close | |||||
</ResetButtonLink> | |||||
</footer> | |||||
</Modal> | |||||
} | |||||
headerTitle="warnings" | |||||
onClose={[MockFunction]} | |||||
primaryButton={null} | |||||
secondaryButtonLabel="close" | |||||
/> | |||||
`; | `; | ||||
exports[`should render correctly: default 1`] = ` | exports[`should render correctly: default 1`] = ` | ||||
<Modal | |||||
contentLabel="warnings" | |||||
onRequestClose={[MockFunction]} | |||||
> | |||||
<header | |||||
className="modal-head" | |||||
> | |||||
<h2> | |||||
warnings | |||||
</h2> | |||||
</header> | |||||
<div | |||||
className="modal-body modal-container js-analysis-warnings" | |||||
> | |||||
<Gl | |||||
body={ | |||||
<DeferredSpinner | <DeferredSpinner | ||||
loading={false} | loading={false} | ||||
> | > | ||||
<div | |||||
className="panel panel-vertical" | |||||
key="foo" | |||||
> | |||||
<WarningIcon | |||||
className="pull-left spacer-right" | |||||
/> | |||||
<React.Fragment> | |||||
<div | <div | ||||
className="overflow-hidden markdown" | |||||
className="sw-flex sw-items-center sw-mt-2" | |||||
> | > | ||||
<span | |||||
dangerouslySetInnerHTML={ | |||||
{ | |||||
"__html": "warning 1", | |||||
} | |||||
} | |||||
/> | |||||
<FlagMessage | |||||
variant="warning" | |||||
> | |||||
<HtmlFormatter> | |||||
<span | |||||
dangerouslySetInnerHTML={ | |||||
{ | |||||
"__html": "warning 1", | |||||
} | |||||
} | |||||
/> | |||||
</HtmlFormatter> | |||||
</FlagMessage> | |||||
</div> | </div> | ||||
</div> | |||||
<div | |||||
className="panel panel-vertical" | |||||
key="foo" | |||||
> | |||||
<WarningIcon | |||||
className="pull-left spacer-right" | |||||
/> | |||||
<div /> | |||||
</React.Fragment> | |||||
<React.Fragment> | |||||
<div | <div | ||||
className="overflow-hidden markdown" | |||||
className="sw-flex sw-items-center sw-mt-2" | |||||
> | > | ||||
<span | |||||
dangerouslySetInnerHTML={ | |||||
{ | |||||
"__html": "warning 2", | |||||
} | |||||
} | |||||
/> | |||||
<FlagMessage | |||||
variant="warning" | |||||
> | |||||
<HtmlFormatter> | |||||
<span | |||||
dangerouslySetInnerHTML={ | |||||
{ | |||||
"__html": "warning 2", | |||||
} | |||||
} | |||||
/> | |||||
</HtmlFormatter> | |||||
</FlagMessage> | |||||
</div> | </div> | ||||
</div> | |||||
<div /> | |||||
</React.Fragment> | |||||
</DeferredSpinner> | </DeferredSpinner> | ||||
</div> | |||||
<footer | |||||
className="modal-foot" | |||||
> | |||||
<ResetButtonLink | |||||
onClick={[MockFunction]} | |||||
> | |||||
close | |||||
</ResetButtonLink> | |||||
</footer> | |||||
</Modal> | |||||
} | |||||
headerTitle="warnings" | |||||
onClose={[MockFunction]} | |||||
primaryButton={null} | |||||
secondaryButtonLabel="close" | |||||
/> | |||||
`; | `; | ||||
exports[`should render correctly: do not show dismissable links for anonymous 1`] = ` | exports[`should render correctly: do not show dismissable links for anonymous 1`] = ` | ||||
<Modal | |||||
contentLabel="warnings" | |||||
onRequestClose={[MockFunction]} | |||||
> | |||||
<header | |||||
className="modal-head" | |||||
> | |||||
<h2> | |||||
warnings | |||||
</h2> | |||||
</header> | |||||
<div | |||||
className="modal-body modal-container js-analysis-warnings" | |||||
> | |||||
<Gl | |||||
body={ | |||||
<DeferredSpinner | <DeferredSpinner | ||||
loading={false} | loading={false} | ||||
> | > | ||||
<div | |||||
className="panel panel-vertical" | |||||
key="foo" | |||||
> | |||||
<WarningIcon | |||||
className="pull-left spacer-right" | |||||
/> | |||||
<React.Fragment> | |||||
<div | <div | ||||
className="overflow-hidden markdown" | |||||
className="sw-flex sw-items-center sw-mt-2" | |||||
> | > | ||||
<span | |||||
dangerouslySetInnerHTML={ | |||||
{ | |||||
"__html": "Lorem ipsum", | |||||
} | |||||
} | |||||
/> | |||||
<FlagMessage | |||||
variant="warning" | |||||
> | |||||
<HtmlFormatter> | |||||
<span | |||||
dangerouslySetInnerHTML={ | |||||
{ | |||||
"__html": "Lorem ipsum", | |||||
} | |||||
} | |||||
/> | |||||
</HtmlFormatter> | |||||
</FlagMessage> | |||||
</div> | </div> | ||||
</div> | |||||
<div /> | |||||
</React.Fragment> | |||||
</DeferredSpinner> | </DeferredSpinner> | ||||
</div> | |||||
<footer | |||||
className="modal-foot" | |||||
> | |||||
<ResetButtonLink | |||||
onClick={[MockFunction]} | |||||
> | |||||
close | |||||
</ResetButtonLink> | |||||
</footer> | |||||
</Modal> | |||||
} | |||||
headerTitle="warnings" | |||||
onClose={[MockFunction]} | |||||
primaryButton={null} | |||||
secondaryButtonLabel="close" | |||||
/> | |||||
`; | `; | ||||
exports[`should render correctly: with dismissable warnings 1`] = ` | exports[`should render correctly: with dismissable warnings 1`] = ` | ||||
<Modal | |||||
contentLabel="warnings" | |||||
onRequestClose={[MockFunction]} | |||||
> | |||||
<header | |||||
className="modal-head" | |||||
> | |||||
<h2> | |||||
warnings | |||||
</h2> | |||||
</header> | |||||
<div | |||||
className="modal-body modal-container js-analysis-warnings" | |||||
> | |||||
<Gl | |||||
body={ | |||||
<DeferredSpinner | <DeferredSpinner | ||||
loading={false} | loading={false} | ||||
> | > | ||||
<div | |||||
className="panel panel-vertical" | |||||
key="foo" | |||||
> | |||||
<WarningIcon | |||||
className="pull-left spacer-right" | |||||
/> | |||||
<React.Fragment> | |||||
<div | <div | ||||
className="overflow-hidden markdown" | |||||
className="sw-flex sw-items-center sw-mt-2" | |||||
> | > | ||||
<span | |||||
dangerouslySetInnerHTML={ | |||||
{ | |||||
"__html": "Lorem ipsum", | |||||
} | |||||
} | |||||
/> | |||||
<FlagMessage | |||||
variant="warning" | |||||
> | |||||
<HtmlFormatter> | |||||
<span | |||||
dangerouslySetInnerHTML={ | |||||
{ | |||||
"__html": "Lorem ipsum", | |||||
} | |||||
} | |||||
/> | |||||
</HtmlFormatter> | |||||
</FlagMessage> | |||||
</div> | |||||
<div> | |||||
<div | <div | ||||
className="spacer-top display-flex-inline" | |||||
className="sw-mt-4" | |||||
> | > | ||||
<ButtonLink | |||||
<DangerButtonSecondary | |||||
disabled={false} | disabled={false} | ||||
onClick={[Function]} | onClick={[Function]} | ||||
> | > | ||||
dismiss_permanently | dismiss_permanently | ||||
</ButtonLink> | |||||
</DangerButtonSecondary> | |||||
<DeferredSpinner | |||||
className="sw-ml-2" | |||||
loading={false} | |||||
/> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | |||||
</React.Fragment> | |||||
</DeferredSpinner> | </DeferredSpinner> | ||||
</div> | |||||
<footer | |||||
className="modal-foot" | |||||
> | |||||
<ResetButtonLink | |||||
onClick={[MockFunction]} | |||||
> | |||||
close | |||||
</ResetButtonLink> | |||||
</footer> | |||||
</Modal> | |||||
} | |||||
headerTitle="warnings" | |||||
onClose={[MockFunction]} | |||||
primaryButton={null} | |||||
secondaryButtonLabel="close" | |||||
/> | |||||
`; | `; |