Browse Source

finish removing legacy react context (#1055)

tags/7.5
Stas Vilchik 5 years ago
parent
commit
7bba1341b9
50 changed files with 364 additions and 383 deletions
  1. 0
    16
      server/sonar-web/src/main/js/app/components/App.tsx
  2. 24
    0
      server/sonar-web/src/main/js/app/components/OnboardingContext.tsx
  3. 3
    11
      server/sonar-web/src/main/js/app/components/StartupModal.tsx
  4. 10
    8
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
  5. 2
    1
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
  6. 1
    1
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx
  7. 12
    1
      server/sonar-web/src/main/js/apps/code/components/Component.tsx
  8. 3
    9
      server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx
  9. 10
    8
      server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx
  10. 5
    7
      server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.tsx
  11. 9
    6
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.tsx
  12. 3
    3
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap
  13. 7
    22
      server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
  14. 6
    8
      server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx
  15. 6
    6
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap
  16. 6
    7
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.tsx
  17. 9
    1
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx
  18. 23
    8
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationJustCreated-test.tsx
  19. 5
    7
      server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.tsx
  20. 8
    6
      server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.tsx
  21. 2
    2
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.tsx
  22. 4
    7
      server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx
  23. 46
    55
      server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
  24. 15
    2
      server/sonar-web/src/main/js/apps/projects/components/ProjectsList.tsx
  25. 10
    1
      server/sonar-web/src/main/js/apps/projects/components/__tests__/EmptyInstance-test.tsx
  26. 6
    2
      server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx
  27. 1
    1
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
  28. 6
    8
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap
  29. 1
    3
      server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilterContainer-test.tsx
  30. 2
    4
      server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchableFilterFooter-test.tsx
  31. 5
    19
      server/sonar-web/src/main/js/apps/securityReports/components/__tests__/App-test.tsx
  32. 6
    11
      server/sonar-web/src/main/js/apps/tutorials/components/__tests__/TokenStep-test.tsx
  33. 1
    5
      server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx
  34. 10
    10
      server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx
  35. 1
    3
      server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx
  36. 1
    1
      server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap
  37. 1
    4
      server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-test.tsx
  38. 23
    14
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx
  39. 3
    9
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
  40. 3
    10
      server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx
  41. 17
    13
      server/sonar-web/src/main/js/components/controls/DateInput.tsx
  42. 4
    4
      server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx
  43. 10
    8
      server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap
  44. 3
    8
      server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx
  45. 13
    7
      server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx
  46. 1
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.tsx
  47. 6
    12
      server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.tsx.snap
  48. 3
    11
      server/sonar-web/src/main/js/components/workspace/Workspace.tsx
  49. 7
    1
      server/sonar-web/src/main/js/components/workspace/context.ts
  50. 1
    12
      server/sonar-web/src/main/js/helpers/testUtils.ts

+ 0
- 16
server/sonar-web/src/main/js/app/components/App.tsx View File

@@ -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();

+ 24
- 0
server/sonar-web/src/main/js/app/components/OnboardingContext.tsx View File

@@ -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>(() => {});

+ 3
- 11
server/sonar-web/src/main/js/app/components/StartupModal.tsx View File

@@ -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>
);
}
}

+ 10
- 8
server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx View File

@@ -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>

+ 2
- 1
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx View File

@@ -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 {

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx View File

@@ -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);
}

+ 12
- 1
server/sonar-web/src/main/js/apps/code/components/Component.tsx View File

@@ -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} />;

+ 3
- 9
server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx View File

@@ -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,

+ 10
- 8
server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx View File

@@ -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);

+ 5
- 7
server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.tsx View File

@@ -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);

+ 9
- 6
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.tsx View File

@@ -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}
/>
);
}

+ 3
- 3
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap View File

@@ -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 {

+ 7
- 22
server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx View File

@@ -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();

+ 6
- 8
server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx View File

@@ -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
- 6
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap View File

@@ -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",

+ 6
- 7
server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.tsx View File

@@ -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 = () => {

+ 9
- 1
server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx View File

@@ -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
);

+ 23
- 8
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationJustCreated-test.tsx View File

@@ -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');
});

+ 5
- 7
server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.tsx View File

@@ -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);

+ 8
- 6
server/sonar-web/src/main/js/apps/overview/components/__tests__/LeakPeriodLegend-test.tsx View File

@@ -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}
/>
);
}

+ 2
- 2
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.tsx View File

@@ -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={() => {}}

+ 4
- 7
server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx View File

@@ -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() {

+ 46
- 55
server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx View File

@@ -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 => ({

+ 15
- 2
server/sonar-web/src/main/js/apps/projects/components/ProjectsList.tsx View File

@@ -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>
);
}


+ 10
- 1
server/sonar-web/src/main/js/apps/projects/components/__tests__/EmptyInstance-test.tsx View File

@@ -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' }}
/>
)

+ 6
- 2
server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx View File

@@ -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();
});

+ 1
- 1
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap View File

@@ -46,7 +46,7 @@ exports[`renders for SonarCloud 1`] = `
className="huge-spacer-top"
>
<Button
onClick={[Function]}
onClick={[MockFunction]}
>
provisioning.analyze_new_project
</Button>

+ 6
- 8
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap View File

@@ -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>
`;

+ 1
- 3
server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilterContainer-test.tsx View File

@@ -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' });

+ 2
- 4
server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchableFilterFooter-test.tsx View File

@@ -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' });

+ 5
- 19
server/sonar-web/src/main/js/apps/securityReports/components/__tests__/App-test.tsx View File

@@ -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',

+ 6
- 11
server/sonar-web/src/main/js/apps/tutorials/components/__tests__/TokenStep-test.tsx View File

@@ -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}

+ 1
- 5
server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx View File

@@ -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>

+ 10
- 10
server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx View File

@@ -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} />

+ 1
- 3
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx View File

@@ -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'));

+ 1
- 1
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap View File

@@ -25,7 +25,7 @@ exports[`renders correctly 1`] = `
>
<Button
className="onboarding-choice"
onClick={[Function]}
onClick={[MockFunction]}
>
<OnboardingProjectIcon
className="big-spacer-bottom"

+ 1
- 4
server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-test.tsx View File

@@ -85,9 +85,6 @@ function getWrapper(props: Partial<UsersApp['props']> = {}) {
organizationsEnabled={true}
router={{ push: jest.fn() }}
{...props}
/>,
{
context: { router: {} }
}
/>
);
}

+ 23
- 14
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx View File

@@ -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')}

+ 3
- 9
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx View File

@@ -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() {

+ 3
- 10
server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx View File

@@ -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

+ 17
- 13
server/sonar-web/src/main/js/components/controls/DateInput.tsx View File

@@ -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 || ''} />;
});

+ 4
- 4
server/sonar-web/src/main/js/components/controls/__tests__/DateInput-test.tsx View File

@@ -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}

+ 10
- 8
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/DateInput-test.tsx.snap View File

@@ -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"

+ 3
- 8
server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx View File

@@ -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
});

+ 13
- 7
server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx View File

@@ -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">

+ 1
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.tsx View File

@@ -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"
/>

+ 6
- 12
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.tsx.snap View File

@@ -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"
>

+ 3
- 11
server/sonar-web/src/main/js/components/workspace/Workspace.tsx View File

@@ -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>
);
}
}

+ 7
- 1
server/sonar-web/src/main/js/components/workspace/context.ts View File

@@ -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: () => {}
});

+ 1
- 12
server/sonar-web/src/main/js/helpers/testUtils.ts View File

@@ -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();

Loading…
Cancel
Save