@@ -32,16 +32,17 @@ export interface MenuItemProps { | |||
component: Component; | |||
onSelect: (branchLike: BranchLike) => void; | |||
selected: boolean; | |||
indent: boolean; | |||
setSelectedNode?: (node: HTMLLIElement) => void; | |||
} | |||
export function MenuItem(props: MenuItemProps) { | |||
const { branchLike, component, setSelectedNode, onSelect, selected } = props; | |||
const { branchLike, component, setSelectedNode, onSelect, selected, indent } = props; | |||
const displayName = getBranchLikeDisplayName(branchLike); | |||
return ( | |||
<ItemButton | |||
className={classNames({ active: selected })} | |||
className={classNames({ active: selected, 'sw-pl-6': indent })} | |||
innerRef={selected ? setSelectedNode : undefined} | |||
onClick={() => { | |||
onSelect(branchLike); |
@@ -47,7 +47,7 @@ export function MenuItemList(props: MenuItemListProps) { | |||
const { branchLikeTree, component, hasResults, onSelect, selectedBranchLike } = props; | |||
const renderItem = (branchLike: BranchLike) => ( | |||
const renderItem = (branchLike: BranchLike, indent = false) => ( | |||
<MenuItem | |||
branchLike={branchLike} | |||
component={component} | |||
@@ -55,6 +55,7 @@ export function MenuItemList(props: MenuItemListProps) { | |||
onSelect={onSelect} | |||
selected={isSameBranchLike(branchLike, selectedBranchLike)} | |||
setSelectedNode={(node) => (selectedNode = node)} | |||
indent={indent} | |||
/> | |||
); | |||
@@ -77,7 +78,8 @@ export function MenuItemList(props: MenuItemListProps) { | |||
<ItemDivider /> | |||
<ItemHeader>{translate('branch_like_navigation.pull_requests')}</ItemHeader> | |||
<ItemDivider /> | |||
{tree.pullRequests.map((pr) => renderItem(pr))} | |||
{tree.pullRequests.map((pr) => renderItem(pr, true))} | |||
{tree.pullRequests.length > 0 && <ItemDivider />} | |||
</> | |||
)} | |||
</React.Fragment> |
@@ -17,13 +17,16 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* 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 { dismissAnalysisWarning, getTask } from '../../api/ce'; | |||
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 { sanitizeStringRestricted } from '../../helpers/sanitize'; | |||
import { TaskWarning } from '../../types/tasks'; | |||
@@ -123,48 +126,51 @@ export class AnalysisWarningsModal extends React.PureComponent<Props, State> { | |||
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 | |||
// eslint-disable-next-line react/no-danger | |||
dangerouslySetInnerHTML={{ | |||
__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> | |||
))} | |||
</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')} | |||
/> | |||
); | |||
} | |||
} |
@@ -19,11 +19,10 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { dismissAnalysisWarning, getTask } from '../../../api/ce'; | |||
import { getTask } from '../../../api/ce'; | |||
import { mockTaskWarning } from '../../../helpers/mocks/tasks'; | |||
import { mockCurrentUser } from '../../../helpers/testMocks'; | |||
import { waitAndUpdate } from '../../../helpers/testUtils'; | |||
import { ButtonLink } from '../../controls/buttons'; | |||
import { AnalysisWarningsModal } from '../AnalysisWarningsModal'; | |||
jest.mock('../../../api/ce', () => ({ | |||
@@ -60,26 +59,6 @@ it('should fetch task warnings if it has to', async () => { | |||
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 () => { | |||
const wrapper = shallowRender(); | |||
@@ -1,274 +1,216 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
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 | |||
loading={false} | |||
> | |||
<div | |||
className="panel panel-vertical" | |||
key="message foo" | |||
> | |||
<WarningIcon | |||
className="pull-left spacer-right" | |||
/> | |||
<React.Fragment> | |||
<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 | |||
className="panel panel-vertical" | |||
key="message-bar" | |||
> | |||
<WarningIcon | |||
className="pull-left spacer-right" | |||
/> | |||
<div /> | |||
</React.Fragment> | |||
<React.Fragment> | |||
<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 | |||
className="panel panel-vertical" | |||
key="multiline message | |||
secondline | |||
third line" | |||
> | |||
<WarningIcon | |||
className="pull-left spacer-right" | |||
/> | |||
<div /> | |||
</React.Fragment> | |||
<React.Fragment> | |||
<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 /> | |||
</React.Fragment> | |||
</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`] = ` | |||
<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 | |||
loading={false} | |||
> | |||
<div | |||
className="panel panel-vertical" | |||
key="foo" | |||
> | |||
<WarningIcon | |||
className="pull-left spacer-right" | |||
/> | |||
<React.Fragment> | |||
<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 | |||
className="panel panel-vertical" | |||
key="foo" | |||
> | |||
<WarningIcon | |||
className="pull-left spacer-right" | |||
/> | |||
<div /> | |||
</React.Fragment> | |||
<React.Fragment> | |||
<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 /> | |||
</React.Fragment> | |||
</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`] = ` | |||
<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 | |||
loading={false} | |||
> | |||
<div | |||
className="panel panel-vertical" | |||
key="foo" | |||
> | |||
<WarningIcon | |||
className="pull-left spacer-right" | |||
/> | |||
<React.Fragment> | |||
<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 /> | |||
</React.Fragment> | |||
</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`] = ` | |||
<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 | |||
loading={false} | |||
> | |||
<div | |||
className="panel panel-vertical" | |||
key="foo" | |||
> | |||
<WarningIcon | |||
className="pull-left spacer-right" | |||
/> | |||
<React.Fragment> | |||
<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 | |||
className="spacer-top display-flex-inline" | |||
className="sw-mt-4" | |||
> | |||
<ButtonLink | |||
<DangerButtonSecondary | |||
disabled={false} | |||
onClick={[Function]} | |||
> | |||
dismiss_permanently | |||
</ButtonLink> | |||
</DangerButtonSecondary> | |||
<DeferredSpinner | |||
className="sw-ml-2" | |||
loading={false} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
</React.Fragment> | |||
</DeferredSpinner> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<ResetButtonLink | |||
onClick={[MockFunction]} | |||
> | |||
close | |||
</ResetButtonLink> | |||
</footer> | |||
</Modal> | |||
} | |||
headerTitle="warnings" | |||
onClose={[MockFunction]} | |||
primaryButton={null} | |||
secondaryButtonLabel="close" | |||
/> | |||
`; |