@@ -18,7 +18,6 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import { connect } from 'react-redux'; | |||
import Helmet from 'react-helmet'; | |||
import { fetchLanguages } from '../../store/rootActions'; | |||
@@ -51,21 +50,6 @@ type Props = StateProps & DispatchProps & OwnProps; | |||
class App extends React.PureComponent<Props> { | |||
mounted = false; | |||
static childContextTypes = { | |||
branchesEnabled: PropTypes.bool.isRequired, | |||
canAdmin: PropTypes.bool.isRequired, | |||
organizationsEnabled: PropTypes.bool | |||
}; | |||
getChildContext() { | |||
const { appState } = this.props; | |||
return { | |||
branchesEnabled: (appState && appState.branchesEnabled) || false, | |||
canAdmin: (appState && appState.canAdmin) || false, | |||
organizationsEnabled: (appState && appState.organizationsEnabled) || false | |||
}; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.props.fetchLanguages(); |
@@ -0,0 +1,24 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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 { createContext } from 'react'; | |||
export type OnboardingContextShape = (organization?: T.Organization) => void; | |||
export const OnboardingContext = createContext<OnboardingContextShape>(() => {}); |
@@ -18,9 +18,9 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import { connect } from 'react-redux'; | |||
import { withRouter, WithRouterProps } from 'react-router'; | |||
import { OnboardingContext } from './OnboardingContext'; | |||
import { differenceInDays, parseDate, toShortNotSoISOString } from '../../helpers/dates'; | |||
import { getCurrentUser, getAppState, Store } from '../../store/rootReducer'; | |||
import { skipOnboarding } from '../../store/users'; | |||
@@ -75,16 +75,8 @@ interface State { | |||
const LICENSE_PROMPT = 'sonarqube.license.prompt'; | |||
export class StartupModal extends React.PureComponent<Props, State> { | |||
static childContextTypes = { | |||
openProjectOnboarding: PropTypes.func | |||
}; | |||
state: State = { automatic: false }; | |||
getChildContext() { | |||
return { openProjectOnboarding: this.openProjectOnboarding }; | |||
} | |||
componentDidMount() { | |||
this.tryAutoOpenLicense().catch(this.tryAutoOpenOnboarding); | |||
} | |||
@@ -172,7 +164,7 @@ export class StartupModal extends React.PureComponent<Props, State> { | |||
render() { | |||
const { automatic, modal } = this.state; | |||
return ( | |||
<> | |||
<OnboardingContext.Provider value={this.openProjectOnboarding}> | |||
{this.props.children} | |||
{modal === ModalKey.license && <LicensePromptModal onClose={this.closeLicense} />} | |||
{modal === ModalKey.onboarding && ( | |||
@@ -188,7 +180,7 @@ export class StartupModal extends React.PureComponent<Props, State> { | |||
{modal === ModalKey.teamOnboarding && ( | |||
<TeamOnboardingModal onFinish={this.closeOnboarding} /> | |||
)} | |||
</> | |||
</OnboardingContext.Provider> | |||
); | |||
} | |||
} |
@@ -18,7 +18,6 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import { connect } from 'react-redux'; | |||
import GlobalNavBranding, { SonarCloudNavBranding } from './GlobalNavBranding'; | |||
import GlobalNavMenu from './GlobalNavMenu'; | |||
@@ -32,6 +31,7 @@ import { lazyLoad } from '../../../../components/lazyLoad'; | |||
import { getCurrentUser, getAppState, Store } from '../../../../store/rootReducer'; | |||
import { isSonarCloud } from '../../../../helpers/system'; | |||
import { isLoggedIn } from '../../../../helpers/users'; | |||
import { OnboardingContext } from '../../OnboardingContext'; | |||
import './GlobalNav.css'; | |||
const GlobalNavPlus = lazyLoad(() => import('./GlobalNavPlus'), 'GlobalNavPlus'); | |||
@@ -48,8 +48,6 @@ interface OwnProps { | |||
type Props = StateProps & OwnProps; | |||
export class GlobalNav extends React.PureComponent<Props> { | |||
static contextTypes = { openProjectOnboarding: PropTypes.func }; | |||
render() { | |||
const { appState, currentUser } = this.props; | |||
return ( | |||
@@ -63,11 +61,15 @@ export class GlobalNav extends React.PureComponent<Props> { | |||
<EmbedDocsPopupHelper /> | |||
<Search appState={appState} currentUser={currentUser} /> | |||
{isLoggedIn(currentUser) && ( | |||
<GlobalNavPlus | |||
appState={appState} | |||
currentUser={currentUser} | |||
openProjectOnboarding={this.context.openProjectOnboarding} | |||
/> | |||
<OnboardingContext.Consumer data-test="global-nav-plus"> | |||
{openProjectOnboarding => ( | |||
<GlobalNavPlus | |||
appState={appState} | |||
currentUser={currentUser} | |||
openProjectOnboarding={openProjectOnboarding} | |||
/> | |||
)} | |||
</OnboardingContext.Consumer> | |||
)} | |||
<GlobalNavUserContainer appState={appState} currentUser={currentUser} /> | |||
</ul> |
@@ -28,11 +28,12 @@ import { translate } from '../../../../helpers/l10n'; | |||
import { isSonarCloud } from '../../../../helpers/system'; | |||
import { getPortfolioAdminUrl, getPortfolioUrl } from '../../../../helpers/urls'; | |||
import { hasGlobalPermission } from '../../../../helpers/users'; | |||
import { OnboardingContextShape } from '../../OnboardingContext'; | |||
interface Props { | |||
appState: Pick<T.AppState, 'qualifiers'>; | |||
currentUser: T.LoggedInUser; | |||
openProjectOnboarding: () => void; | |||
openProjectOnboarding: OnboardingContextShape; | |||
} | |||
interface State { |
@@ -47,5 +47,5 @@ function runTest(mockedIsSonarCloud: boolean) { | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setProps({ currentUser: { isLoggedIn: true } }); | |||
expect(wrapper.find('GlobalNavPlus').exists()).toBe(true); | |||
expect(wrapper.find('[data-test="global-nav-plus"]').exists()).toBe(true); | |||
} |
@@ -23,6 +23,7 @@ import ComponentName from './ComponentName'; | |||
import ComponentMeasure from './ComponentMeasure'; | |||
import ComponentLink from './ComponentLink'; | |||
import ComponentPin from './ComponentPin'; | |||
import { WorkspaceContext } from '../../../components/workspace/context'; | |||
const TOP_OFFSET = 200; | |||
const BOTTOM_OFFSET = 10; | |||
@@ -90,7 +91,17 @@ export default class Component extends React.PureComponent<Props> { | |||
switch (component.qualifier) { | |||
case 'FIL': | |||
case 'UTS': | |||
componentAction = <ComponentPin branchLike={branchLike} component={component} />; | |||
componentAction = ( | |||
<WorkspaceContext.Consumer> | |||
{({ openComponent }) => ( | |||
<ComponentPin | |||
branchLike={branchLike} | |||
component={component} | |||
openComponent={openComponent} | |||
/> | |||
)} | |||
</WorkspaceContext.Consumer> | |||
); | |||
break; | |||
default: | |||
componentAction = <ComponentLink branchLike={branchLike} component={component} />; |
@@ -18,27 +18,21 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import PinIcon from '../../../components/icons-components/PinIcon'; | |||
import { WorkspaceContext } from '../../../components/workspace/context'; | |||
import { WorkspaceContextShape } from '../../../components/workspace/context'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
branchLike?: T.BranchLike; | |||
component: T.ComponentMeasure; | |||
openComponent: WorkspaceContextShape['openComponent']; | |||
} | |||
export default class ComponentPin extends React.PureComponent<Props> { | |||
context!: { workspace: WorkspaceContext }; | |||
static contextTypes = { | |||
workspace: PropTypes.object.isRequired | |||
}; | |||
handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.context.workspace.openComponent({ | |||
this.props.openComponent({ | |||
branchLike: this.props.branchLike, | |||
key: this.props.component.key, | |||
name: this.props.component.path, |
@@ -18,7 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { intlShape } from 'react-intl'; | |||
import { injectIntl, InjectedIntlProps } from 'react-intl'; | |||
import { Query } from '../query'; | |||
import DateInput from '../../../components/controls/DateInput'; | |||
import FacetBox from '../../../components/facet/FacetBox'; | |||
@@ -33,14 +33,14 @@ interface Props { | |||
value?: Date; | |||
} | |||
export default class AvailableSinceFacet extends React.PureComponent<Props> { | |||
static contextTypes = { | |||
intl: intlShape | |||
class AvailableSinceFacet extends React.PureComponent<Props & InjectedIntlProps> { | |||
handleHeaderClick = () => { | |||
this.props.onToggle('availableSince'); | |||
}; | |||
handleHeaderClick = () => this.props.onToggle('availableSince'); | |||
handleClear = () => this.props.onChange({ availableSince: undefined }); | |||
handleClear = () => { | |||
this.props.onChange({ availableSince: undefined }); | |||
}; | |||
handlePeriodChange = (date: Date | undefined) => { | |||
this.props.onChange({ availableSince: date }); | |||
@@ -48,7 +48,7 @@ export default class AvailableSinceFacet extends React.PureComponent<Props> { | |||
getValues = () => | |||
this.props.value | |||
? [this.context.intl.formatDate(this.props.value, longFormatterOption)] | |||
? [this.props.intl.formatDate(this.props.value, longFormatterOption)] | |||
: undefined; | |||
render() { | |||
@@ -74,3 +74,5 @@ export default class AvailableSinceFacet extends React.PureComponent<Props> { | |||
); | |||
} | |||
} | |||
export default injectIntl(AvailableSinceFacet); |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import * as PropTypes from 'prop-types'; | |||
import { injectIntl, InjectedIntlProps } from 'react-intl'; | |||
import DateFromNow from '../../../components/intl/DateFromNow'; | |||
import DateFormatter, { longFormatterOption } from '../../../components/intl/DateFormatter'; | |||
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; | |||
@@ -34,13 +34,9 @@ interface Props { | |||
period: T.Period; | |||
} | |||
export default class LeakPeriodLegend extends React.PureComponent<Props> { | |||
static contextTypes = { | |||
intl: PropTypes.object.isRequired | |||
}; | |||
export class LeakPeriodLegend extends React.PureComponent<Props & InjectedIntlProps> { | |||
formatDate = (date: string) => { | |||
return this.context.intl.formatDate(date, longFormatterOption); | |||
return this.props.intl.formatDate(date, longFormatterOption); | |||
}; | |||
render() { | |||
@@ -81,3 +77,5 @@ export default class LeakPeriodLegend extends React.PureComponent<Props> { | |||
return <Tooltip overlay={tooltip}>{label}</Tooltip>; | |||
} | |||
} | |||
export default injectIntl(LeakPeriodLegend); |
@@ -19,7 +19,8 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import LeakPeriodLegend from '../LeakPeriodLegend'; | |||
import { InjectedIntlProps } from 'react-intl'; | |||
import { LeakPeriodLegend } from '../LeakPeriodLegend'; | |||
import { differenceInDays } from '../../../../helpers/dates'; | |||
jest.mock('../../../../helpers/dates', () => { | |||
@@ -69,9 +70,11 @@ it('should render a more precise date', () => { | |||
}); | |||
function getWrapper(component: T.ComponentMeasure, period: T.Period) { | |||
return shallow(<LeakPeriodLegend component={component} period={period} />, { | |||
context: { | |||
intl: { formatDate: (date: string) => 'formatted.' + date } | |||
} | |||
}); | |||
return shallow( | |||
<LeakPeriodLegend | |||
component={component} | |||
intl={{ formatDate: (x: any) => x } as InjectedIntlProps['intl']} | |||
period={period} | |||
/> | |||
); | |||
} |
@@ -51,7 +51,7 @@ exports[`should render correctly 1`] = ` | |||
<div | |||
className="measure-details-primary-actions" | |||
> | |||
<LeakPeriodLegend | |||
<InjectIntl(LeakPeriodLegend) | |||
className="spacer-left" | |||
component={ | |||
Object { | |||
@@ -105,7 +105,7 @@ exports[`should render correctly for leak 1`] = ` | |||
<div | |||
className="measure-details-primary-actions" | |||
> | |||
<LeakPeriodLegend | |||
<InjectIntl(LeakPeriodLegend) | |||
className="spacer-left" | |||
component={ | |||
Object { | |||
@@ -234,7 +234,7 @@ exports[`should work with measure without value 1`] = ` | |||
<div | |||
className="measure-details-primary-actions" | |||
> | |||
<LeakPeriodLegend | |||
<InjectIntl(LeakPeriodLegend) | |||
className="spacer-left" | |||
component={ | |||
Object { |
@@ -18,10 +18,10 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import { App } from '../App'; | |||
import { shallowWithIntl, waitAndUpdate } from '../../../../helpers/testUtils'; | |||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | |||
const replace = jest.fn(); | |||
const issues = [ | |||
{ key: 'foo' } as T.Issue, | |||
{ key: 'bar' } as T.Issue, | |||
@@ -65,10 +65,7 @@ const PROPS = { | |||
}; | |||
it('should render a list of issue', async () => { | |||
const wrapper = shallowWithIntl<App>(<App {...PROPS} />, { | |||
context: { router: { replace } } | |||
}); | |||
const wrapper = shallow<App>(<App {...PROPS} />); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().issues.length).toBe(4); | |||
expect(wrapper.state().referencedComponentsById).toEqual({ 'foo-uuid': referencedComponent }); | |||
@@ -76,10 +73,7 @@ it('should render a list of issue', async () => { | |||
}); | |||
it('should be able to check/uncheck a group of issues with the Shift key', async () => { | |||
const wrapper = shallowWithIntl<App>(<App {...PROPS} />, { | |||
context: { router: { replace } } | |||
}); | |||
const wrapper = shallow<App>(<App {...PROPS} />); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().issues.length).toBe(4); | |||
@@ -98,10 +92,7 @@ it('should be able to check/uncheck a group of issues with the Shift key', async | |||
}); | |||
it('should avoid non-existing keys', async () => { | |||
const wrapper = shallowWithIntl<App>(<App {...PROPS} />, { | |||
context: { router: { replace } } | |||
}); | |||
const wrapper = shallow<App>(<App {...PROPS} />); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().issues.length).toBe(4); | |||
@@ -114,10 +105,7 @@ it('should avoid non-existing keys', async () => { | |||
}); | |||
it('should be able to uncheck all issue with global checkbox', async () => { | |||
const wrapper = shallowWithIntl<App>(<App {...PROPS} />, { | |||
context: { router: { replace } } | |||
}); | |||
const wrapper = shallow<App>(<App {...PROPS} />); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().issues.length).toBe(4); | |||
@@ -131,10 +119,7 @@ it('should be able to uncheck all issue with global checkbox', async () => { | |||
}); | |||
it('should be able to check all issue with global checkbox', async () => { | |||
const wrapper = shallowWithIntl<App>(<App {...PROPS} />, { | |||
context: { router: { replace } } | |||
}); | |||
const wrapper = shallow<App>(<App {...PROPS} />); | |||
await waitAndUpdate(wrapper); | |||
const instance = wrapper.instance(); |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { max } from 'lodash'; | |||
import { intlShape } from 'react-intl'; | |||
import { injectIntl, InjectedIntlProps } from 'react-intl'; | |||
import { Query } from '../utils'; | |||
import FacetBox from '../../../components/facet/FacetBox'; | |||
import FacetHeader from '../../../components/facet/FacetHeader'; | |||
@@ -48,17 +48,13 @@ interface Props { | |||
stats: { [x: string]: number } | undefined; | |||
} | |||
export default class CreationDateFacet extends React.PureComponent<Props> { | |||
class CreationDateFacet extends React.PureComponent<Props & InjectedIntlProps> { | |||
property = 'createdAt'; | |||
static defaultProps = { | |||
open: true | |||
}; | |||
static contextTypes = { | |||
intl: intlShape | |||
}; | |||
hasValue = () => | |||
this.props.createdAfter !== undefined || | |||
this.props.createdAt.length > 0 || | |||
@@ -105,7 +101,7 @@ export default class CreationDateFacet extends React.PureComponent<Props> { | |||
getValues() { | |||
const { createdAfter, createdAt, createdBefore, createdInLast, sinceLeakPeriod } = this.props; | |||
const { formatDate } = this.context.intl; | |||
const { formatDate } = this.props.intl; | |||
const values = []; | |||
if (createdAfter) { | |||
values.push(formatDate(createdAfter, longFormatterOption)); | |||
@@ -144,7 +140,7 @@ export default class CreationDateFacet extends React.PureComponent<Props> { | |||
return null; | |||
} | |||
const { formatDate } = this.context.intl; | |||
const { formatDate } = this.props.intl; | |||
const data = periods.map((start, index) => { | |||
const startDate = parseDate(start); | |||
let endDate; | |||
@@ -296,3 +292,5 @@ export default class CreationDateFacet extends React.PureComponent<Props> { | |||
); | |||
} | |||
} | |||
export default injectIntl(CreationDateFacet); |
@@ -6,7 +6,7 @@ Array [ | |||
"SeverityFacet", | |||
"ResolutionFacet", | |||
"StatusFacet", | |||
"CreationDateFacet", | |||
"InjectIntl(CreationDateFacet)", | |||
"Connect(LanguageFacet)", | |||
"RuleFacet", | |||
"StandardFacet", | |||
@@ -25,7 +25,7 @@ Array [ | |||
"SeverityFacet", | |||
"ResolutionFacet", | |||
"StatusFacet", | |||
"CreationDateFacet", | |||
"InjectIntl(CreationDateFacet)", | |||
"Connect(LanguageFacet)", | |||
"RuleFacet", | |||
"StandardFacet", | |||
@@ -42,7 +42,7 @@ Array [ | |||
"SeverityFacet", | |||
"ResolutionFacet", | |||
"StatusFacet", | |||
"CreationDateFacet", | |||
"InjectIntl(CreationDateFacet)", | |||
"Connect(LanguageFacet)", | |||
"RuleFacet", | |||
"StandardFacet", | |||
@@ -59,7 +59,7 @@ Array [ | |||
"SeverityFacet", | |||
"ResolutionFacet", | |||
"StatusFacet", | |||
"CreationDateFacet", | |||
"InjectIntl(CreationDateFacet)", | |||
"Connect(LanguageFacet)", | |||
"RuleFacet", | |||
"StandardFacet", | |||
@@ -78,7 +78,7 @@ Array [ | |||
"SeverityFacet", | |||
"ResolutionFacet", | |||
"StatusFacet", | |||
"CreationDateFacet", | |||
"InjectIntl(CreationDateFacet)", | |||
"Connect(LanguageFacet)", | |||
"RuleFacet", | |||
"StandardFacet", | |||
@@ -97,7 +97,7 @@ Array [ | |||
"SeverityFacet", | |||
"ResolutionFacet", | |||
"StatusFacet", | |||
"CreationDateFacet", | |||
"InjectIntl(CreationDateFacet)", | |||
"Connect(LanguageFacet)", | |||
"RuleFacet", | |||
"StandardFacet", |
@@ -18,25 +18,24 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { withRouter, WithRouterProps } from 'react-router'; | |||
import { Button } from '../../../components/ui/buttons'; | |||
import OnboardingProjectIcon from '../../../components/icons-components/OnboardingProjectIcon'; | |||
import OnboardingAddMembersIcon from '../../../components/icons-components/OnboardingAddMembersIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { OnboardingContextShape } from '../../../app/components/OnboardingContext'; | |||
import { withRouter, Router } from '../../../components/hoc/withRouter'; | |||
import '../../tutorials/styles.css'; | |||
import './OrganizationJustCreated.css'; | |||
interface Props { | |||
openProjectOnboarding: OnboardingContextShape; | |||
organization: T.Organization; | |||
router: Pick<Router, 'push'>; | |||
} | |||
export class OrganizationJustCreated extends React.PureComponent<Props & WithRouterProps> { | |||
static contextTypes = { | |||
openProjectOnboarding: () => null | |||
}; | |||
export class OrganizationJustCreated extends React.PureComponent<Props> { | |||
handleNewProjectClick = () => { | |||
this.context.openProjectOnboarding(this.props.organization); | |||
this.props.openProjectOnboarding(this.props.organization); | |||
}; | |||
handleAddMembersClick = () => { |
@@ -32,6 +32,7 @@ import { | |||
getMyOrganizations, | |||
Store | |||
} from '../../../store/rootReducer'; | |||
import { OnboardingContext } from '../../../app/components/OnboardingContext'; | |||
interface OwnProps { | |||
children?: React.ReactNode; | |||
@@ -89,7 +90,14 @@ export class OrganizationPage extends React.PureComponent<Props, State> { | |||
const { location } = this.props; | |||
const justCreated = Boolean(location.state && location.state.justCreated); | |||
return justCreated ? ( | |||
<OrganizationJustCreated organization={organization} /> | |||
<OnboardingContext.Consumer> | |||
{openProjectOnboarding => ( | |||
<OrganizationJustCreated | |||
openProjectOnboarding={openProjectOnboarding} | |||
organization={organization} | |||
/> | |||
)} | |||
</OnboardingContext.Consumer> | |||
) : ( | |||
this.props.children | |||
); |
@@ -25,24 +25,39 @@ import { click } from '../../../../helpers/testUtils'; | |||
const organization: T.Organization = { key: 'foo', name: 'Foo' }; | |||
it('should render', () => { | |||
// @ts-ignore | |||
expect(shallow(<OrganizationJustCreated organization={organization} />)).toMatchSnapshot(); | |||
expect( | |||
shallow( | |||
<OrganizationJustCreated | |||
openProjectOnboarding={jest.fn()} | |||
organization={organization} | |||
router={{ push: jest.fn() }} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should create new project', () => { | |||
const openProjectOnboarding = jest.fn(); | |||
// @ts-ignore | |||
const wrapper = shallow(<OrganizationJustCreated organization={organization} />, { | |||
context: { openProjectOnboarding } | |||
}); | |||
const wrapper = shallow( | |||
<OrganizationJustCreated | |||
openProjectOnboarding={openProjectOnboarding} | |||
organization={organization} | |||
router={{ push: jest.fn() }} | |||
/> | |||
); | |||
click(wrapper.find('Button').first()); | |||
expect(openProjectOnboarding).toBeCalledWith({ key: 'foo', name: 'Foo' }); | |||
}); | |||
it('should add members', () => { | |||
const router = { push: jest.fn() }; | |||
// @ts-ignore | |||
const wrapper = shallow(<OrganizationJustCreated organization={organization} router={router} />); | |||
const wrapper = shallow( | |||
<OrganizationJustCreated | |||
openProjectOnboarding={jest.fn()} | |||
organization={organization} | |||
router={router} | |||
/> | |||
); | |||
click(wrapper.find('Button').last()); | |||
expect(router.push).toBeCalledWith('/organizations/foo/members'); | |||
}); |
@@ -18,7 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import { injectIntl, InjectedIntlProps } from 'react-intl'; | |||
import DateFromNow from '../../../components/intl/DateFromNow'; | |||
import DateFormatter, { longFormatterOption } from '../../../components/intl/DateFormatter'; | |||
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; | |||
@@ -31,13 +31,9 @@ interface Props { | |||
period: T.Period; | |||
} | |||
export default class LeakPeriodLegend extends React.PureComponent<Props> { | |||
static contextTypes = { | |||
intl: PropTypes.object.isRequired | |||
}; | |||
export class LeakPeriodLegend extends React.PureComponent<Props & InjectedIntlProps> { | |||
formatDate = (date: string) => { | |||
return this.context.intl.formatDate(date, longFormatterOption); | |||
return this.props.intl.formatDate(date, longFormatterOption); | |||
}; | |||
render() { | |||
@@ -102,3 +98,5 @@ export default class LeakPeriodLegend extends React.PureComponent<Props> { | |||
); | |||
} | |||
} | |||
export default injectIntl(LeakPeriodLegend); |
@@ -18,8 +18,9 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { InjectedIntlProps } from 'react-intl'; | |||
import { shallow } from 'enzyme'; | |||
import LeakPeriodLegend from '../LeakPeriodLegend'; | |||
import { LeakPeriodLegend } from '../LeakPeriodLegend'; | |||
import { differenceInDays } from '../../../../helpers/dates'; | |||
jest.mock('../../../../helpers/dates', () => { | |||
@@ -93,9 +94,10 @@ it('should render a more precise date', () => { | |||
}); | |||
function getWrapper(period: T.Period) { | |||
return shallow(<LeakPeriodLegend period={period} />, { | |||
context: { | |||
intl: { formatDate: (date: string) => 'formatted.' + date } | |||
} | |||
}); | |||
return shallow( | |||
<LeakPeriodLegend | |||
intl={{ formatDate: (date: string) => 'formatted.' + date } as InjectedIntlProps['intl']} | |||
period={period} | |||
/> | |||
); | |||
} |
@@ -18,13 +18,13 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallowWithIntl } from '../../../../helpers/testUtils'; | |||
import { shallow } from 'enzyme'; | |||
import ProjectActivityDateInput from '../ProjectActivityDateInput'; | |||
import { parseDate } from '../../../../helpers/dates'; | |||
it('should render correctly the date inputs', () => { | |||
expect( | |||
shallowWithIntl( | |||
shallow( | |||
<ProjectActivityDateInput | |||
from={parseDate('2016-10-27T12:21:15+0000')} | |||
onChange={() => {}} |
@@ -18,24 +18,21 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { Button } from '../../../components/ui/buttons'; | |||
import { isSonarCloud } from '../../../helpers/system'; | |||
import { hasGlobalPermission, isLoggedIn } from '../../../helpers/users'; | |||
import { OnboardingContextShape } from '../../../app/components/OnboardingContext'; | |||
interface Props { | |||
organization?: T.Organization; | |||
currentUser: T.CurrentUser; | |||
openProjectOnboarding: OnboardingContextShape; | |||
organization?: T.Organization; | |||
} | |||
export default class EmptyInstance extends React.PureComponent<Props> { | |||
static contextTypes = { | |||
openProjectOnboarding: PropTypes.func | |||
}; | |||
analyzeNewProject = () => { | |||
this.context.openProjectOnboarding(this.props.organization); | |||
this.props.openProjectOnboarding(this.props.organization); | |||
}; | |||
render() { |
@@ -20,7 +20,6 @@ | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import { connect } from 'react-redux'; | |||
import * as PropTypes from 'prop-types'; | |||
import { sortBy } from 'lodash'; | |||
import DropdownIcon from '../../../components/icons-components/DropdownIcon'; | |||
import Dropdown from '../../../components/controls/Dropdown'; | |||
@@ -29,67 +28,59 @@ import { Button } from '../../../components/ui/buttons'; | |||
import { getMyOrganizations, Store } from '../../../store/rootReducer'; | |||
import { isSonarCloud } from '../../../helpers/system'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { OnboardingContextShape } from '../../../app/components/OnboardingContext'; | |||
interface OwnProps { | |||
openProjectOnboarding: OnboardingContextShape; | |||
} | |||
interface StateProps { | |||
organizations: T.Organization[]; | |||
} | |||
export class NoFavoriteProjects extends React.PureComponent<StateProps> { | |||
static contextTypes = { | |||
openProjectOnboarding: PropTypes.func | |||
}; | |||
onAnalyzeProjectClick = () => { | |||
this.context.openProjectOnboarding(); | |||
}; | |||
export function NoFavoriteProjects(props: StateProps & OwnProps) { | |||
return ( | |||
<div className="projects-empty-list"> | |||
<h3>{translate('projects.no_favorite_projects')}</h3> | |||
{isSonarCloud() ? ( | |||
<div className="spacer-top"> | |||
<p>{translate('projects.no_favorite_projects.how_to_add_projects')}</p> | |||
<div className="huge-spacer-top"> | |||
<Button onClick={props.openProjectOnboarding}> | |||
{translate('provisioning.analyze_new_project')} | |||
</Button> | |||
render() { | |||
const { organizations } = this.props; | |||
return ( | |||
<div className="projects-empty-list"> | |||
<h3>{translate('projects.no_favorite_projects')}</h3> | |||
{isSonarCloud() ? ( | |||
<div className="spacer-top"> | |||
<p>{translate('projects.no_favorite_projects.how_to_add_projects')}</p> | |||
<div className="huge-spacer-top"> | |||
<Button onClick={this.onAnalyzeProjectClick}> | |||
{translate('provisioning.analyze_new_project')} | |||
</Button> | |||
<Dropdown | |||
className="display-inline-block big-spacer-left" | |||
overlay={ | |||
<ul className="menu"> | |||
{sortBy(organizations, org => org.name.toLowerCase()).map(organization => ( | |||
<OrganizationListItem key={organization.key} organization={organization} /> | |||
))} | |||
</ul> | |||
}> | |||
<a className="button" href="#"> | |||
{translate('projects.no_favorite_projects.favorite_projects_from_orgs')} | |||
<DropdownIcon className="little-spacer-left" /> | |||
</a> | |||
</Dropdown> | |||
<Link className="button big-spacer-left" to="/explore/projects"> | |||
{translate('projects.no_favorite_projects.favorite_public_projects')} | |||
</Link> | |||
</div> | |||
</div> | |||
) : ( | |||
<div> | |||
<p className="big-spacer-top"> | |||
{translate('projects.no_favorite_projects.engagement')} | |||
</p> | |||
<p className="big-spacer-top"> | |||
<Link className="button" to="/projects/all"> | |||
{translate('projects.explore_projects')} | |||
</Link> | |||
</p> | |||
<Dropdown | |||
className="display-inline-block big-spacer-left" | |||
overlay={ | |||
<ul className="menu"> | |||
{sortBy(props.organizations, org => org.name.toLowerCase()).map(organization => ( | |||
<OrganizationListItem key={organization.key} organization={organization} /> | |||
))} | |||
</ul> | |||
}> | |||
<a className="button" href="#"> | |||
{translate('projects.no_favorite_projects.favorite_projects_from_orgs')} | |||
<DropdownIcon className="little-spacer-left" /> | |||
</a> | |||
</Dropdown> | |||
<Link className="button big-spacer-left" to="/explore/projects"> | |||
{translate('projects.no_favorite_projects.favorite_public_projects')} | |||
</Link> | |||
</div> | |||
)} | |||
</div> | |||
); | |||
} | |||
</div> | |||
) : ( | |||
<div> | |||
<p className="big-spacer-top">{translate('projects.no_favorite_projects.engagement')}</p> | |||
<p className="big-spacer-top"> | |||
<Link className="button" to="/projects/all"> | |||
{translate('projects.explore_projects')} | |||
</Link> | |||
</p> | |||
</div> | |||
)} | |||
</div> | |||
); | |||
} | |||
const mapStateToProps = (state: Store): StateProps => ({ |
@@ -28,6 +28,7 @@ import EmptyFavoriteSearch from './EmptyFavoriteSearch'; | |||
import EmptySearch from '../../../components/common/EmptySearch'; | |||
import { Project } from '../types'; | |||
import { Query } from '../query'; | |||
import { OnboardingContext } from '../../../app/components/OnboardingContext'; | |||
interface Props { | |||
cardType?: string; | |||
@@ -50,9 +51,21 @@ export default class ProjectsList extends React.PureComponent<Props> { | |||
return isFavorite ? <EmptyFavoriteSearch query={query} /> : <EmptySearch />; | |||
} | |||
return isFavorite ? ( | |||
<NoFavoriteProjects /> | |||
<OnboardingContext.Consumer> | |||
{openProjectOnboarding => ( | |||
<NoFavoriteProjects openProjectOnboarding={openProjectOnboarding} /> | |||
)} | |||
</OnboardingContext.Consumer> | |||
) : ( | |||
<EmptyInstance currentUser={currentUser} organization={organization} /> | |||
<OnboardingContext.Consumer> | |||
{openProjectOnboarding => ( | |||
<EmptyInstance | |||
currentUser={currentUser} | |||
openProjectOnboarding={openProjectOnboarding} | |||
organization={organization} | |||
/> | |||
)} | |||
</OnboardingContext.Consumer> | |||
); | |||
} | |||
@@ -29,12 +29,19 @@ jest.mock('../../../../helpers/system', () => ({ | |||
it('renders correctly for SQ', () => { | |||
(isSonarCloud as jest.Mock<any>).mockReturnValue(false); | |||
expect( | |||
shallow(<EmptyInstance currentUser={{ isLoggedIn: false }} organization={undefined} />) | |||
shallow( | |||
<EmptyInstance | |||
currentUser={{ isLoggedIn: false }} | |||
openProjectOnboarding={jest.fn()} | |||
organization={undefined} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
expect( | |||
shallow( | |||
<EmptyInstance | |||
currentUser={{ isLoggedIn: true, permissions: { global: ['provisioning'] } }} | |||
openProjectOnboarding={jest.fn()} | |||
organization={undefined} | |||
/> | |||
) | |||
@@ -47,6 +54,7 @@ it('renders correctly for SC', () => { | |||
shallow( | |||
<EmptyInstance | |||
currentUser={{ isLoggedIn: false }} | |||
openProjectOnboarding={jest.fn()} | |||
organization={{ key: 'foo', name: 'Foo' }} | |||
/> | |||
) | |||
@@ -55,6 +63,7 @@ it('renders correctly for SC', () => { | |||
shallow( | |||
<EmptyInstance | |||
currentUser={{ isLoggedIn: false }} | |||
openProjectOnboarding={jest.fn()} | |||
organization={{ actions: { provision: true }, key: 'foo', name: 'Foo' }} | |||
/> | |||
) |
@@ -26,7 +26,9 @@ jest.mock('../../../../helpers/system', () => ({ isSonarCloud: jest.fn() })); | |||
it('renders', () => { | |||
(isSonarCloud as jest.Mock).mockImplementation(() => false); | |||
expect(shallow(<NoFavoriteProjects organizations={[]} />)).toMatchSnapshot(); | |||
expect( | |||
shallow(<NoFavoriteProjects openProjectOnboarding={jest.fn()} organizations={[]} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('renders for SonarCloud', () => { | |||
@@ -35,5 +37,7 @@ it('renders for SonarCloud', () => { | |||
{ actions: { admin: true }, key: 'org1', name: 'org1', projectVisibility: 'public' }, | |||
{ actions: { admin: false }, key: 'org2', name: 'org2', projectVisibility: 'public' } | |||
]; | |||
expect(shallow(<NoFavoriteProjects organizations={organizations} />)).toMatchSnapshot(); | |||
expect( | |||
shallow(<NoFavoriteProjects openProjectOnboarding={jest.fn()} organizations={organizations} />) | |||
).toMatchSnapshot(); | |||
}); |
@@ -46,7 +46,7 @@ exports[`renders for SonarCloud 1`] = ` | |||
className="huge-spacer-top" | |||
> | |||
<Button | |||
onClick={[Function]} | |||
onClick={[MockFunction]} | |||
> | |||
provisioning.analyze_new_project | |||
</Button> |
@@ -21,13 +21,9 @@ exports[`renders different types of "no projects" 1`] = ` | |||
<div | |||
className="projects-list" | |||
> | |||
<EmptyInstance | |||
currentUser={ | |||
Object { | |||
"isLoggedIn": true, | |||
} | |||
} | |||
/> | |||
<ContextConsumer> | |||
<Component /> | |||
</ContextConsumer> | |||
</div> | |||
`; | |||
@@ -43,6 +39,8 @@ exports[`renders different types of "no projects" 3`] = ` | |||
<div | |||
className="projects-list" | |||
> | |||
<Connect(NoFavoriteProjects) /> | |||
<ContextConsumer> | |||
<Component /> | |||
</ContextConsumer> | |||
</div> | |||
`; |
@@ -23,9 +23,7 @@ import SearchFilterContainer from '../SearchFilterContainer'; | |||
it('searches', () => { | |||
const onQueryChange = jest.fn(); | |||
const wrapper = shallow(<SearchFilterContainer onQueryChange={onQueryChange} query={{}} />, { | |||
context: { router: { push: jest.fn() } } | |||
}); | |||
const wrapper = shallow(<SearchFilterContainer onQueryChange={onQueryChange} query={{}} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.find('SearchBox').prop<Function>('onChange')('foo'); | |||
expect(onQueryChange).toBeCalledWith({ search: 'foo' }); |
@@ -34,8 +34,7 @@ it('should render items without the ones in the facet', () => { | |||
options={options} | |||
property="languages" | |||
query={{ languages: ['java'] }} | |||
/>, | |||
{ context: { router: { push: jest.fn() } } } | |||
/> | |||
); | |||
expect(wrapper.find('Select').prop('options')).toMatchSnapshot(); | |||
}); | |||
@@ -48,8 +47,7 @@ it('should render items without the ones in the facet', () => { | |||
options={options} | |||
property="languages" | |||
query={{ languages: ['java'] }} | |||
/>, | |||
{ context: { router: { push: jest.fn() } } } | |||
/> | |||
); | |||
(wrapper.find('Select').prop('onChange') as Function)({ value: 'js' }); | |||
expect(onQueryChange).toBeCalledWith({ languages: 'java,js' }); |
@@ -85,7 +85,6 @@ const getSecurityHotspots = require('../../../../api/security-reports') | |||
.getSecurityHotspots as jest.Mock<any>; | |||
const component = { key: 'foo', name: 'Foo', qualifier: 'TRK' } as T.Component; | |||
const context = { router: { push: jest.fn() } }; | |||
const location = { pathname: 'foo', query: {} }; | |||
const locationWithCWE = { pathname: 'foo', query: { showCWE: 'true' } }; | |||
const owaspParams = { type: 'owasp_top_10' }; | |||
@@ -103,10 +102,7 @@ it('renders error on wrong type parameters', () => { | |||
location={location} | |||
params={wrongParams} | |||
router={{ push: jest.fn() }} | |||
/>, | |||
{ | |||
context | |||
} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
@@ -118,10 +114,7 @@ it('renders owaspTop10', async () => { | |||
location={location} | |||
params={owaspParams} | |||
router={{ push: jest.fn() }} | |||
/>, | |||
{ | |||
context | |||
} | |||
/> | |||
); | |||
await waitAndUpdate(wrapper); | |||
expect(getSecurityHotspots).toBeCalledWith({ | |||
@@ -140,8 +133,7 @@ it('renders with cwe', () => { | |||
location={locationWithCWE} | |||
params={owaspParams} | |||
router={{ push: jest.fn() }} | |||
/>, | |||
{ context } | |||
/> | |||
); | |||
expect(getSecurityHotspots).toBeCalledWith({ | |||
project: 'foo', | |||
@@ -159,10 +151,7 @@ it('handle checkbox for cwe display', async () => { | |||
location={location} | |||
params={owaspParams} | |||
router={{ push: jest.fn() }} | |||
/>, | |||
{ | |||
context | |||
} | |||
/> | |||
); | |||
expect(getSecurityHotspots).toBeCalledWith({ | |||
project: 'foo', | |||
@@ -191,10 +180,7 @@ it('renders sansTop25', () => { | |||
location={location} | |||
params={sansParams} | |||
router={{ push: jest.fn() }} | |||
/>, | |||
{ | |||
context | |||
} | |||
/> | |||
); | |||
expect(getSecurityHotspots).toBeCalledWith({ | |||
project: 'foo', |
@@ -18,14 +18,9 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import TokenStep from '../TokenStep'; | |||
import { | |||
change, | |||
click, | |||
submit, | |||
waitAndUpdate, | |||
shallowWithIntl | |||
} from '../../../../helpers/testUtils'; | |||
import { change, click, submit, waitAndUpdate } from '../../../../helpers/testUtils'; | |||
jest.mock('../../../../api/user-tokens', () => ({ | |||
getTokens: () => Promise.resolve([{ name: 'foo' }]), | |||
@@ -38,7 +33,7 @@ jest.mock('../../../../components/icons-components/ClearIcon'); | |||
const currentUser = { login: 'user' }; | |||
it('generates token', async () => { | |||
const wrapper = shallowWithIntl( | |||
const wrapper = shallow( | |||
<TokenStep | |||
currentUser={currentUser} | |||
finished={false} | |||
@@ -58,7 +53,7 @@ it('generates token', async () => { | |||
}); | |||
it('revokes token', async () => { | |||
const wrapper = shallowWithIntl( | |||
const wrapper = shallow( | |||
<TokenStep | |||
currentUser={currentUser} | |||
finished={false} | |||
@@ -83,7 +78,7 @@ it('revokes token', async () => { | |||
it('continues', async () => { | |||
const onContinue = jest.fn(); | |||
const wrapper = shallowWithIntl( | |||
const wrapper = shallow( | |||
<TokenStep | |||
currentUser={currentUser} | |||
finished={false} | |||
@@ -101,7 +96,7 @@ it('continues', async () => { | |||
it('uses existing token', async () => { | |||
const onContinue = jest.fn(); | |||
const wrapper = shallowWithIntl( | |||
const wrapper = shallow( | |||
<TokenStep | |||
currentUser={currentUser} | |||
finished={false} |
@@ -48,10 +48,6 @@ export class OnboardingModal extends React.PureComponent<Props> { | |||
} | |||
} | |||
handleOpenProjectOnboarding = () => { | |||
this.props.onOpenProjectOnboarding(); | |||
}; | |||
render() { | |||
if (!isLoggedIn(this.props.currentUser)) { | |||
return null; | |||
@@ -70,7 +66,7 @@ export class OnboardingModal extends React.PureComponent<Props> { | |||
<p className="spacer-top">{translate('onboarding.header.description')}</p> | |||
</div> | |||
<div className="modal-simple-body text-center onboarding-choices"> | |||
<Button className="onboarding-choice" onClick={this.handleOpenProjectOnboarding}> | |||
<Button className="onboarding-choice" onClick={this.props.onOpenProjectOnboarding}> | |||
<OnboardingProjectIcon className="big-spacer-bottom" /> | |||
<h6 className="onboarding-choice-name">{translate('onboarding.analyze_your_code')}</h6> | |||
</Button> |
@@ -18,12 +18,12 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import { connect } from 'react-redux'; | |||
import { InjectedRouter } from 'react-router'; | |||
import OnboardingModal from './OnboardingModal'; | |||
import { skipOnboarding } from '../../../store/users'; | |||
import TeamOnboardingModal from '../teamOnboarding/TeamOnboardingModal'; | |||
import { OnboardingContext } from '../../../app/components/OnboardingContext'; | |||
interface DispatchProps { | |||
skipOnboarding: () => void; | |||
@@ -43,10 +43,6 @@ interface State { | |||
} | |||
export class OnboardingPage extends React.PureComponent<OwnProps & DispatchProps, State> { | |||
static contextTypes = { | |||
openProjectOnboarding: PropTypes.func.isRequired | |||
}; | |||
state: State = { modal: ModalKey.onboarding }; | |||
closeOnboarding = () => { | |||
@@ -63,11 +59,15 @@ export class OnboardingPage extends React.PureComponent<OwnProps & DispatchProps | |||
return ( | |||
<> | |||
{modal === ModalKey.onboarding && ( | |||
<OnboardingModal | |||
onClose={this.closeOnboarding} | |||
onOpenProjectOnboarding={this.context.openProjectOnboarding} | |||
onOpenTeamOnboarding={this.openTeamOnboarding} | |||
/> | |||
<OnboardingContext.Consumer> | |||
{openProjectOnboarding => ( | |||
<OnboardingModal | |||
onClose={this.closeOnboarding} | |||
onOpenProjectOnboarding={openProjectOnboarding} | |||
onOpenTeamOnboarding={this.openTeamOnboarding} | |||
/> | |||
)} | |||
</OnboardingContext.Consumer> | |||
)} | |||
{modal === ModalKey.teamOnboarding && ( | |||
<TeamOnboardingModal onFinish={this.closeOnboarding} /> |
@@ -39,15 +39,13 @@ it('should correctly open the different tutorials', () => { | |||
const onClose = jest.fn(); | |||
const onOpenProjectOnboarding = jest.fn(); | |||
const onOpenTeamOnboarding = jest.fn(); | |||
const push = jest.fn(); | |||
const wrapper = shallow( | |||
<OnboardingModal | |||
currentUser={{ isLoggedIn: true }} | |||
onClose={onClose} | |||
onOpenProjectOnboarding={onOpenProjectOnboarding} | |||
onOpenTeamOnboarding={onOpenTeamOnboarding} | |||
/>, | |||
{ context: { router: { push } } } | |||
/> | |||
); | |||
click(wrapper.find('ResetButtonLink')); |
@@ -25,7 +25,7 @@ exports[`renders correctly 1`] = ` | |||
> | |||
<Button | |||
className="onboarding-choice" | |||
onClick={[Function]} | |||
onClick={[MockFunction]} | |||
> | |||
<OnboardingProjectIcon | |||
className="big-spacer-bottom" |
@@ -85,9 +85,6 @@ function getWrapper(props: Partial<UsersApp['props']> = {}) { | |||
organizationsEnabled={true} | |||
router={{ push: jest.fn() }} | |||
{...props} | |||
/>, | |||
{ | |||
context: { router: {} } | |||
} | |||
/> | |||
); | |||
} |
@@ -40,6 +40,7 @@ import { | |||
import { isSameBranchLike, getBranchLikeQuery } from '../../helpers/branches'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { Alert } from '../ui/Alert'; | |||
import { WorkspaceContext } from '../workspace/context'; | |||
import './styles.css'; | |||
// TODO react-virtualized | |||
@@ -607,14 +608,19 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> | |||
/* eslint-enable no-underscore-dangle */ | |||
return ( | |||
<DuplicationPopup | |||
blocks={blocks} | |||
branchLike={this.props.branchLike} | |||
duplicatedFiles={duplicatedFiles} | |||
inRemovedComponent={inRemovedComponent} | |||
onClose={this.closeLinePopup} | |||
sourceViewerFile={component} | |||
/> | |||
<WorkspaceContext.Consumer> | |||
{({ openComponent }) => ( | |||
<DuplicationPopup | |||
blocks={blocks} | |||
branchLike={this.props.branchLike} | |||
duplicatedFiles={duplicatedFiles} | |||
inRemovedComponent={inRemovedComponent} | |||
onClose={this.closeLinePopup} | |||
openComponent={openComponent} | |||
sourceViewerFile={component} | |||
/> | |||
)} | |||
</WorkspaceContext.Consumer> | |||
); | |||
}; | |||
@@ -698,12 +704,15 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> | |||
return ( | |||
<div className={className} ref={node => (this.node = node)}> | |||
{this.state.component && ( | |||
<SourceViewerHeader | |||
branchLike={this.props.branchLike} | |||
sourceViewerFile={this.state.component} | |||
/> | |||
)} | |||
<WorkspaceContext.Consumer> | |||
{({ openComponent }) => ( | |||
<SourceViewerHeader | |||
branchLike={this.props.branchLike} | |||
openComponent={openComponent} | |||
sourceViewerFile={component} | |||
/> | |||
)} | |||
</WorkspaceContext.Consumer> | |||
{sourceRemoved && ( | |||
<Alert className="spacer-top" variant="warning"> | |||
{translate('code_viewer.no_source_code_displayed_due_to_source_removed')} |
@@ -20,7 +20,6 @@ | |||
import { stringify } from 'querystring'; | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import * as PropTypes from 'prop-types'; | |||
import MeasuresOverlay from './components/MeasuresOverlay'; | |||
import QualifierIcon from '../icons-components/QualifierIcon'; | |||
import Dropdown from '../controls/Dropdown'; | |||
@@ -28,7 +27,7 @@ import Favorite from '../controls/Favorite'; | |||
import ListIcon from '../icons-components/ListIcon'; | |||
import { ButtonIcon } from '../ui/buttons'; | |||
import { PopupPlacement } from '../ui/popups'; | |||
import { WorkspaceContext } from '../workspace/context'; | |||
import { WorkspaceContextShape } from '../workspace/context'; | |||
import { | |||
getPathUrlAsString, | |||
getBranchLikeUrl, | |||
@@ -43,6 +42,7 @@ import { omitNil } from '../../helpers/request'; | |||
interface Props { | |||
branchLike: T.BranchLike | undefined; | |||
openComponent: WorkspaceContextShape['openComponent']; | |||
sourceViewerFile: T.SourceViewerFile; | |||
} | |||
@@ -51,12 +51,6 @@ interface State { | |||
} | |||
export default class SourceViewerHeader extends React.PureComponent<Props, State> { | |||
context!: { workspace: WorkspaceContext }; | |||
static contextTypes = { | |||
workspace: PropTypes.object.isRequired | |||
}; | |||
state: State = { measuresOverlay: false }; | |||
handleShowMeasuresClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
@@ -71,7 +65,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State | |||
openInWorkspace = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
const { key } = this.props.sourceViewerFile; | |||
this.context.workspace.openComponent({ branchLike: this.props.branchLike, key }); | |||
this.props.openComponent({ branchLike: this.props.branchLike, key }); | |||
}; | |||
render() { |
@@ -19,12 +19,11 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import * as PropTypes from 'prop-types'; | |||
import { groupBy, sortBy } from 'lodash'; | |||
import { DropdownOverlay } from '../../controls/Dropdown'; | |||
import QualifierIcon from '../../icons-components/QualifierIcon'; | |||
import { PopupPlacement } from '../../ui/popups'; | |||
import { WorkspaceContext } from '../../workspace/context'; | |||
import { WorkspaceContextShape } from '../../workspace/context'; | |||
import { isShortLivingBranch, isPullRequest } from '../../../helpers/branches'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { collapsedDirFromPath, fileFromPath } from '../../../helpers/path'; | |||
@@ -37,17 +36,11 @@ interface Props { | |||
duplicatedFiles?: { [ref: string]: T.DuplicatedFile }; | |||
inRemovedComponent: boolean; | |||
onClose: () => void; | |||
popupPosition?: any; | |||
openComponent: WorkspaceContextShape['openComponent']; | |||
sourceViewerFile: T.SourceViewerFile; | |||
} | |||
export default class DuplicationPopup extends React.PureComponent<Props> { | |||
context!: { workspace: WorkspaceContext }; | |||
static contextTypes = { | |||
workspace: PropTypes.object.isRequired | |||
}; | |||
shouldLink() { | |||
const { branchLike } = this.props; | |||
return !isShortLivingBranch(branchLike) && !isPullRequest(branchLike); | |||
@@ -65,7 +58,7 @@ export default class DuplicationPopup extends React.PureComponent<Props> { | |||
event.currentTarget.blur(); | |||
const { key, line } = event.currentTarget.dataset; | |||
if (this.shouldLink() && key) { | |||
this.context.workspace.openComponent({ | |||
this.props.openComponent({ | |||
branchLike: this.props.branchLike, | |||
key, | |||
line: line ? Number(line) : undefined |
@@ -20,7 +20,7 @@ | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import { DayModifiers, Modifier, Modifiers } from 'react-day-picker'; | |||
import { intlShape, InjectedIntlProps } from 'react-intl'; | |||
import { InjectedIntlProps, injectIntl } from 'react-intl'; | |||
import { range } from 'lodash'; | |||
import * as addMonths from 'date-fns/add_months'; | |||
import * as setMonth from 'date-fns/set_month'; | |||
@@ -41,7 +41,7 @@ import './styles.css'; | |||
const DayPicker = lazyLoad(() => import('react-day-picker')); | |||
export interface Props { | |||
interface Props { | |||
className?: string; | |||
currentMonth?: Date; | |||
highlightFrom?: Date; | |||
@@ -64,13 +64,8 @@ interface State { | |||
type Week = [string, string, string, string, string, string, string]; | |||
export default class DateInput extends React.PureComponent<Props, State> { | |||
context!: InjectedIntlProps; | |||
input?: HTMLInputElement | null; | |||
static contextTypes = { | |||
intl: intlShape | |||
}; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { currentMonth: props.value || props.currentMonth || new Date(), open: false }; | |||
@@ -129,10 +124,7 @@ export default class DateInput extends React.PureComponent<Props, State> { | |||
render() { | |||
const { highlightFrom, highlightTo, minDate, value } = this.props; | |||
const { formatDate } = this.context.intl; | |||
const { lastHovered } = this.state; | |||
const formattedValue = | |||
value && formatDate(value, { year: 'numeric', month: 'short', day: 'numeric' }); | |||
const after = this.props.maxDate || new Date(); | |||
@@ -158,17 +150,17 @@ export default class DateInput extends React.PureComponent<Props, State> { | |||
return ( | |||
<OutsideClickHandler onClickOutside={this.closeCalendar}> | |||
<span className={classNames('date-input-control', this.props.className)}> | |||
<input | |||
<InputWrapper | |||
className={classNames('date-input-control-input', this.props.inputClassName, { | |||
'is-filled': this.props.value !== undefined | |||
})} | |||
innerRef={node => (this.input = node)} | |||
name={this.props.name} | |||
onFocus={this.openCalendar} | |||
placeholder={this.props.placeholder} | |||
readOnly={true} | |||
ref={node => (this.input = node)} | |||
type="text" | |||
value={formattedValue || ''} | |||
value={value} | |||
/> | |||
<CalendarIcon className="date-input-control-icon" fill="" /> | |||
{this.props.value !== undefined && ( | |||
@@ -230,3 +222,15 @@ export default class DateInput extends React.PureComponent<Props, State> { | |||
function NullComponent() { | |||
return null; | |||
} | |||
type InputWrapperProps = T.Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value'> & | |||
InjectedIntlProps & { | |||
innerRef: React.Ref<HTMLInputElement>; | |||
value: Date | undefined; | |||
}; | |||
const InputWrapper = injectIntl(({ innerRef, intl, value, ...other }: InputWrapperProps) => { | |||
const formattedValue = | |||
value && intl.formatDate(value, { year: 'numeric', month: 'short', day: 'numeric' }); | |||
return <input {...other} ref={innerRef} value={formattedValue || ''} />; | |||
}); |
@@ -18,13 +18,13 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import * as addDays from 'date-fns/add_days'; | |||
import * as setMonth from 'date-fns/set_month'; | |||
import * as setYear from 'date-fns/set_year'; | |||
import * as subDays from 'date-fns/sub_days'; | |||
import * as subMonths from 'date-fns/sub_months'; | |||
import DateInput, { Props } from '../DateInput'; | |||
import { shallowWithIntl } from '../../../helpers/testUtils'; | |||
import DateInput from '../DateInput'; | |||
import { parseDate } from '../../../helpers/dates'; | |||
jest.mock('../../lazyLoad', () => ({ | |||
@@ -111,8 +111,8 @@ it('should hightlightTo range', () => { | |||
expect(dayPicker.prop('selectedDays')).toEqual([dateB]); | |||
}); | |||
function shallowRender(props?: Partial<Props>) { | |||
const wrapper = shallowWithIntl<DateInput>( | |||
function shallowRender(props?: Partial<DateInput['props']>) { | |||
const wrapper = shallow<DateInput>( | |||
<DateInput | |||
currentMonth={dateA} | |||
maxDate={dateB} |
@@ -7,13 +7,13 @@ exports[`should render 1`] = ` | |||
<span | |||
className="date-input-control" | |||
> | |||
<input | |||
<InjectIntl(Component) | |||
className="date-input-control-input" | |||
innerRef={[Function]} | |||
onFocus={[Function]} | |||
placeholder="placeholder" | |||
readOnly={true} | |||
type="text" | |||
value="" | |||
/> | |||
<CalendarIcon | |||
className="date-input-control-icon" | |||
@@ -30,13 +30,14 @@ exports[`should render 2`] = ` | |||
<span | |||
className="date-input-control" | |||
> | |||
<input | |||
<InjectIntl(Component) | |||
className="date-input-control-input is-filled" | |||
innerRef={[Function]} | |||
onFocus={[Function]} | |||
placeholder="placeholder" | |||
readOnly={true} | |||
type="text" | |||
value="Jan 17, 2018" | |||
value={2018-01-17T00:00:00.000Z} | |||
/> | |||
<CalendarIcon | |||
className="date-input-control-icon" | |||
@@ -62,13 +63,14 @@ exports[`should render 3`] = ` | |||
<span | |||
className="date-input-control" | |||
> | |||
<input | |||
<InjectIntl(Component) | |||
className="date-input-control-input is-filled" | |||
innerRef={[Function]} | |||
onFocus={[Function]} | |||
placeholder="placeholder" | |||
readOnly={true} | |||
type="text" | |||
value="Jan 17, 2018" | |||
value={2018-01-17T00:00:00.000Z} | |||
/> | |||
<CalendarIcon | |||
className="date-input-control-icon" | |||
@@ -269,13 +271,13 @@ exports[`should select a day 1`] = ` | |||
<span | |||
className="date-input-control" | |||
> | |||
<input | |||
<InjectIntl(Component) | |||
className="date-input-control-input" | |||
innerRef={[Function]} | |||
onFocus={[Function]} | |||
placeholder="placeholder" | |||
readOnly={true} | |||
type="text" | |||
value="" | |||
/> | |||
<CalendarIcon | |||
className="date-input-control-icon" |
@@ -22,25 +22,20 @@ import EllipsisIcon from '../../icons-components/EllipsisIcon'; | |||
import Tooltip from '../../controls/Tooltip'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { Button } from '../../ui/buttons'; | |||
import { WorkspaceContext } from '../../workspace/context'; | |||
import { WorkspaceContextShape } from '../../workspace/context'; | |||
interface Props { | |||
engine?: string; | |||
manualVulnerability: boolean; | |||
message: string; | |||
openRule: WorkspaceContextShape['openRule']; | |||
organization: string; | |||
rule: string; | |||
} | |||
export default class IssueMessage extends React.PureComponent<Props> { | |||
context!: { workspace: WorkspaceContext }; | |||
static contextTypes = { | |||
workspace: () => null | |||
}; | |||
handleClick = () => { | |||
this.context.workspace.openRule({ | |||
this.props.openRule({ | |||
key: this.props.rule, | |||
organization: this.props.organization | |||
}); |
@@ -29,6 +29,7 @@ import { getBranchLikeQuery } from '../../../helpers/branches'; | |||
import { getComponentIssuesUrl } from '../../../helpers/urls'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { WorkspaceContext } from '../../workspace/context'; | |||
interface Props { | |||
branchLike?: T.BranchLike; | |||
@@ -69,13 +70,18 @@ export default function IssueTitleBar(props: Props) { | |||
return ( | |||
<div className="issue-row"> | |||
<IssueMessage | |||
engine={issue.externalRuleEngine} | |||
manualVulnerability={issue.fromHotspot && issue.type === 'VULNERABILITY'} | |||
message={issue.message} | |||
organization={issue.organization} | |||
rule={issue.rule} | |||
/> | |||
<WorkspaceContext.Consumer> | |||
{({ openRule }) => ( | |||
<IssueMessage | |||
engine={issue.externalRuleEngine} | |||
manualVulnerability={issue.fromHotspot && issue.type === 'VULNERABILITY'} | |||
message={issue.message} | |||
openRule={openRule} | |||
organization={issue.organization} | |||
rule={issue.rule} | |||
/> | |||
)} | |||
</WorkspaceContext.Consumer> | |||
<div className="issue-row-meta"> | |||
<ul className="issue-meta-list"> |
@@ -26,6 +26,7 @@ it('should render with the message and a link to open the rule', () => { | |||
<IssueMessage | |||
manualVulnerability={false} | |||
message="Reduce the number of conditional operators (4) used in the expression" | |||
openRule={jest.fn()} | |||
organization="myorg" | |||
rule="javascript:S1067" | |||
/> |
@@ -10,12 +10,9 @@ exports[`should render the titlebar correctly 1`] = ` | |||
<div | |||
className="issue-row" | |||
> | |||
<IssueMessage | |||
manualVulnerability={false} | |||
message="Reduce the number of conditional operators (4) used in the expression" | |||
organization="myorg" | |||
rule="javascript:S1067" | |||
/> | |||
<ContextConsumer> | |||
<Component /> | |||
</ContextConsumer> | |||
<div | |||
className="issue-row-meta" | |||
> | |||
@@ -109,12 +106,9 @@ exports[`should render the titlebar with the filter 1`] = ` | |||
<div | |||
className="issue-row" | |||
> | |||
<IssueMessage | |||
manualVulnerability={false} | |||
message="Reduce the number of conditional operators (4) used in the expression" | |||
organization="myorg" | |||
rule="javascript:S1067" | |||
/> | |||
<ContextConsumer> | |||
<Component /> | |||
</ContextConsumer> | |||
<div | |||
className="issue-row-meta" | |||
> |
@@ -18,7 +18,6 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import { omit, uniqBy } from 'lodash'; | |||
import { WorkspaceContext, ComponentDescriptor, RuleDescriptor } from './context'; | |||
import WorkspacePortal from './WorkspacePortal'; | |||
@@ -48,19 +47,11 @@ const TYPE_KEY = '__type__'; | |||
export default class Workspace extends React.PureComponent<{}, State> { | |||
mounted = false; | |||
static childContextTypes = { | |||
workspace: PropTypes.object | |||
}; | |||
constructor(props: {}) { | |||
super(props); | |||
this.state = { height: INITIAL_HEIGHT, open: {}, ...this.loadWorkspace() }; | |||
} | |||
getChildContext = (): { workspace: WorkspaceContext } => { | |||
return { workspace: { openComponent: this.openComponent, openRule: this.openRule } }; | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
@@ -187,7 +178,8 @@ export default class Workspace extends React.PureComponent<{}, State> { | |||
const height = this.state.maximized ? window.innerHeight * MAX_HEIGHT : this.state.height; | |||
return ( | |||
<> | |||
<WorkspaceContext.Provider | |||
value={{ openComponent: this.openComponent, openRule: this.openRule }}> | |||
{this.props.children} | |||
<WorkspacePortal> | |||
{(components.length > 0 || rules.length > 0) && ( | |||
@@ -228,7 +220,7 @@ export default class Workspace extends React.PureComponent<{}, State> { | |||
/> | |||
)} | |||
</WorkspacePortal> | |||
</> | |||
</WorkspaceContext.Provider> | |||
); | |||
} | |||
} |
@@ -17,6 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { createContext } from 'react'; | |||
export interface ComponentDescriptor { | |||
branchLike: T.BranchLike | undefined; | |||
@@ -32,7 +33,12 @@ export interface RuleDescriptor { | |||
organization: string; | |||
} | |||
export interface WorkspaceContext { | |||
export interface WorkspaceContextShape { | |||
openComponent: (component: ComponentDescriptor) => void; | |||
openRule: (rule: RuleDescriptor) => void; | |||
} | |||
export const WorkspaceContext = createContext<WorkspaceContextShape>({ | |||
openComponent: () => {}, | |||
openRule: () => {} | |||
}); |
@@ -17,9 +17,8 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow, ShallowRendererProps, ShallowWrapper, ReactWrapper } from 'enzyme'; | |||
import { ShallowWrapper, ReactWrapper } from 'enzyme'; | |||
import { InjectedRouter } from 'react-router'; | |||
import { IntlProvider } from 'react-intl'; | |||
export const mockEvent = { | |||
target: { blur() {} }, | |||
@@ -112,16 +111,6 @@ export function doAsync(fn?: Function): Promise<void> { | |||
}); | |||
} | |||
// Create the IntlProvider to retrieve context for wrapping around. | |||
const intlProvider = new IntlProvider({ locale: 'en' }, {}); | |||
const { intl } = intlProvider.getChildContext(); | |||
export function shallowWithIntl<C extends React.Component>( | |||
node: React.ReactElement<any>, | |||
options: ShallowRendererProps = {} | |||
) { | |||
return shallow<C>(node, { ...options, context: { intl, ...options.context } }); | |||
} | |||
export async function waitAndUpdate(wrapper: ShallowWrapper<any, any> | ReactWrapper<any, any>) { | |||
await new Promise(setImmediate); | |||
wrapper.update(); |