Browse Source

remove some usages of legacy react context

tags/7.5
Stas Vilchik 5 years ago
parent
commit
ce80f21152
83 changed files with 787 additions and 873 deletions
  1. 2
    8
      server/sonar-web/src/main/js/app/components/AdminContainer.tsx
  2. 9
    12
      server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
  3. 12
    14
      server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
  4. 6
    10
      server/sonar-web/src/main/js/app/components/Landing.tsx
  5. 35
    12
      server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
  6. 6
    7
      server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
  7. 2
    8
      server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx
  8. 23
    10
      server/sonar-web/src/main/js/app/components/embed-docs-modal/Suggestions.tsx
  9. 10
    1
      server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsContext.ts
  10. 18
    36
      server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx
  11. 2
    21
      server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx
  12. 30
    20
      server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx
  13. 4
    156
      server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap
  14. 44
    13
      server/sonar-web/src/main/js/app/components/extensions/Extension.tsx
  15. 2
    6
      server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx
  16. 2
    6
      server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx
  17. 2
    2
      server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.tsx
  18. 2
    2
      server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx
  19. 2
    2
      server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx
  20. 8
    13
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
  21. 8
    10
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
  22. 6
    8
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx
  23. 6
    7
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
  24. 15
    15
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
  25. 4
    1
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx
  26. 11
    11
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx
  27. 31
    15
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx
  28. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
  29. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap
  30. 3
    3
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
  31. 2
    4
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
  32. 6
    7
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
  33. 1
    6
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx
  34. 20
    4
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx
  35. 4
    22
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap
  36. 6
    0
      server/sonar-web/src/main/js/app/types.d.ts
  37. 6
    8
      server/sonar-web/src/main/js/apps/account/notifications/Notifications.tsx
  38. 10
    4
      server/sonar-web/src/main/js/apps/account/notifications/__tests__/Notifications-test.tsx
  39. 2
    2
      server/sonar-web/src/main/js/apps/code/components/App.tsx
  40. 8
    10
      server/sonar-web/src/main/js/apps/code/components/Search.tsx
  41. 6
    8
      server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
  42. 6
    8
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx
  43. 6
    2
      server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsIssues-test.tsx
  44. 13
    14
      server/sonar-web/src/main/js/apps/issues/components/App.tsx
  45. 2
    1
      server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
  46. 7
    10
      server/sonar-web/src/main/js/apps/marketplace/App.tsx
  47. 9
    10
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.tsx
  48. 5
    6
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.tsx
  49. 8
    9
      server/sonar-web/src/main/js/apps/overview/components/App.tsx
  50. 13
    9
      server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx
  51. 6
    8
      server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx
  52. 7
    9
      server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx
  53. 6
    8
      server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx
  54. 3
    2
      server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx
  55. 1
    1
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap
  56. 11
    12
      server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
  57. 10
    14
      server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx
  58. 6
    6
      server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
  59. 6
    3
      server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx
  60. 6
    7
      server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx
  61. 7
    8
      server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx
  62. 6
    7
      server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx
  63. 8
    11
      server/sonar-web/src/main/js/apps/quality-gates/components/DetailsApp.tsx
  64. 6
    9
      server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.tsx
  65. 8
    19
      server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx
  66. 7
    14
      server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.tsx
  67. 12
    16
      server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
  68. 13
    4
      server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx
  69. 7
    8
      server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx
  70. 7
    10
      server/sonar-web/src/main/js/apps/securityReports/components/App.tsx
  71. 51
    14
      server/sonar-web/src/main/js/apps/securityReports/components/__tests__/App-test.tsx
  72. 7
    16
      server/sonar-web/src/main/js/apps/system/components/App.tsx
  73. 4
    6
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboarding.tsx
  74. 10
    10
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboardingPage.tsx
  75. 13
    2
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/ProjectOnboarding-test.tsx
  76. 7
    10
      server/sonar-web/src/main/js/apps/users/UsersApp.tsx
  77. 4
    3
      server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-test.tsx
  78. 8
    20
      server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx
  79. 7
    7
      server/sonar-web/src/main/js/components/docs/DocLink.tsx
  80. 57
    15
      server/sonar-web/src/main/js/components/docs/__tests__/DocLink-test.tsx
  81. 10
    0
      server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocLink-test.tsx.snap
  82. 13
    12
      server/sonar-web/src/main/js/components/hoc/withRouter.tsx
  83. 6
    7
      server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.tsx

+ 2
- 8
server/sonar-web/src/main/js/app/components/AdminContainer.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 Helmet from 'react-helmet';
import { connect } from 'react-redux';
import MarketplaceContext, { defaultPendingPlugins } from './MarketplaceContext';
@@ -31,7 +30,7 @@ import { PluginPendingResult, getPendingPlugins } from '../../api/plugins';
import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';

interface StateProps {
appState: Pick<T.AppState, 'adminPages' | 'organizationsEnabled'>;
appState: Pick<T.AppState, 'adminPages' | 'canAdmin' | 'organizationsEnabled'>;
}

interface DispatchToProps {
@@ -50,18 +49,13 @@ interface State {

class AdminContainer extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
canAdmin: PropTypes.bool.isRequired
};

state: State = {
pendingPlugins: defaultPendingPlugins
};

componentDidMount() {
this.mounted = true;
if (!this.context.canAdmin) {
if (!this.props.appState.canAdmin) {
handleRequiredAuthorization();
} else {
this.fetchNavigationSettings();

+ 9
- 12
server/sonar-web/src/main/js/app/components/ComponentContainer.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 { differenceBy } from 'lodash';
import { ComponentContext } from './ComponentContext';
@@ -40,8 +39,10 @@ import {
isShortLivingBranch,
getBranchLikeQuery
} from '../../helpers/branches';
import { Store, getAppState } from '../../store/rootReducer';

interface Props {
appState: Pick<T.AppState, 'organizationsEnabled'>;
children: any;
fetchOrganizations: (organizations: string[]) => void;
location: {
@@ -66,15 +67,7 @@ const FETCH_STATUS_WAIT_TIME = 3000;
export class ComponentContainer extends React.PureComponent<Props, State> {
watchStatusTimer?: number;
mounted = false;

static contextTypes = {
organizationsEnabled: PropTypes.bool
};

constructor(props: Props) {
super(props);
this.state = { branchLikes: [], isPending: false, loading: true, warnings: [] };
}
state: State = { branchLikes: [], isPending: false, loading: true, warnings: [] };

componentDidMount() {
this.mounted = true;
@@ -122,7 +115,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
.then(([nav, data]) => {
const component = this.addQualifier({ ...nav, ...data });

if (this.context.organizationsEnabled) {
if (this.props.appState.organizationsEnabled) {
this.props.fetchOrganizations([component.organization]);
}
return component;
@@ -382,9 +375,13 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
}
}

const mapStateToProps = (state: Store) => ({
appState: getAppState(state)
});

const mapDispatchToProps = { fetchOrganizations };

export default connect(
null,
mapStateToProps,
mapDispatchToProps
)(ComponentContainer);

+ 12
- 14
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx View File

@@ -36,22 +36,20 @@ export default function GlobalContainer(props: Props) {
const { footer = <GlobalFooterContainer /> } = props;
return (
<SuggestionsProvider>
{({ suggestions }) => (
<StartupModal>
<div className="global-container">
<div className="page-wrapper" id="container">
<div className="page-container">
<Workspace>
<GlobalNav location={props.location} suggestions={suggestions} />
<GlobalMessagesContainer />
{props.children}
</Workspace>
</div>
<StartupModal>
<div className="global-container">
<div className="page-wrapper" id="container">
<div className="page-container">
<Workspace>
<GlobalNav location={props.location} />
<GlobalMessagesContainer />
{props.children}
</Workspace>
</div>
{footer}
</div>
</StartupModal>
)}
{footer}
</div>
</StartupModal>
</SuggestionsProvider>
);
}

+ 6
- 10
server/sonar-web/src/main/js/app/components/Landing.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 { withRouter, WithRouterProps } from 'react-router';
import { connect } from 'react-redux';
import { Location } from 'history';
import { getCurrentUser, Store } from '../../store/rootReducer';
@@ -33,22 +33,18 @@ interface OwnProps {
location: Location;
}

class Landing extends React.PureComponent<StateProps & OwnProps> {
static contextTypes = {
router: PropTypes.object.isRequired
};

class Landing extends React.PureComponent<StateProps & OwnProps & WithRouterProps> {
componentDidMount() {
const { currentUser } = this.props;
if (currentUser && isLoggedIn(currentUser)) {
if (currentUser.homepage) {
const homepage = getHomePageUrl(currentUser.homepage);
this.context.router.replace(homepage);
this.props.router.replace(homepage);
} else {
this.context.router.replace('/projects');
this.props.router.replace('/projects');
}
} else {
this.context.router.replace('/about');
this.props.router.replace('/about');
}
}

@@ -61,4 +57,4 @@ const mapStateToProps = (state: Store) => ({
currentUser: getCurrentUser(state)
});

export default connect(mapStateToProps)(Landing);
export default withRouter(connect(mapStateToProps)(Landing));

+ 35
- 12
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx View File

@@ -76,7 +76,10 @@ beforeEach(() => {

it('changes component', () => {
const wrapper = shallow<ComponentContainer>(
<ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'foo' } }}>
<ComponentContainer
appState={{ organizationsEnabled: false }}
fetchOrganizations={jest.fn()}
location={{ query: { id: 'foo' } }}>
<Inner />
</ComponentContainer>
);
@@ -100,7 +103,10 @@ it("loads branches for module's project", async () => {
});

mount(
<ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'moduleKey' } }}>
<ComponentContainer
appState={{ organizationsEnabled: false }}
fetchOrganizations={jest.fn()}
location={{ query: { id: 'moduleKey' } }}>
<Inner />
</ComponentContainer>
);
@@ -114,7 +120,10 @@ it("loads branches for module's project", async () => {

it("doesn't load branches portfolio", async () => {
const wrapper = mount(
<ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'portfolioKey' } }}>
<ComponentContainer
appState={{ organizationsEnabled: false }}
fetchOrganizations={jest.fn()}
location={{ query: { id: 'portfolioKey' } }}>
<Inner />
</ComponentContainer>
);
@@ -130,7 +139,10 @@ it("doesn't load branches portfolio", async () => {

it('updates branches on change', () => {
const wrapper = shallow(
<ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'portfolioKey' } }}>
<ComponentContainer
appState={{ organizationsEnabled: false }}
fetchOrganizations={jest.fn()}
location={{ query: { id: 'portfolioKey' } }}>
<Inner />
</ComponentContainer>
);
@@ -156,6 +168,7 @@ it('updates the branch measures', async () => {
(getPullRequests as jest.Mock<any>).mockResolvedValueOnce([]);
const wrapper = shallow(
<ComponentContainer
appState={{ organizationsEnabled: false }}
fetchOrganizations={jest.fn()}
location={{ query: { id: 'foo', branch: 'feature' } }}>
<Inner />
@@ -184,10 +197,12 @@ it('loads organization', async () => {

const fetchOrganizations = jest.fn();
mount(
<ComponentContainer fetchOrganizations={fetchOrganizations} location={{ query: { id: 'foo' } }}>
<ComponentContainer
appState={{ organizationsEnabled: true }}
fetchOrganizations={fetchOrganizations}
location={{ query: { id: 'foo' } }}>
<Inner />
</ComponentContainer>,
{ context: { organizationsEnabled: true } }
</ComponentContainer>
);

await new Promise(setImmediate);
@@ -198,10 +213,12 @@ it('fetches status', async () => {
(getComponentData as jest.Mock<any>).mockResolvedValueOnce({ organization: 'org' });

mount(
<ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'foo' } }}>
<ComponentContainer
appState={{ organizationsEnabled: true }}
fetchOrganizations={jest.fn()}
location={{ query: { id: 'foo' } }}>
<Inner />
</ComponentContainer>,
{ context: { organizationsEnabled: true } }
</ComponentContainer>
);

await new Promise(setImmediate);
@@ -210,7 +227,10 @@ it('fetches status', async () => {

it('filters correctly the pending tasks for a main branch', () => {
const wrapper = shallow(
<ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'foo' } }}>
<ComponentContainer
appState={{ organizationsEnabled: false }}
fetchOrganizations={jest.fn()}
location={{ query: { id: 'foo' } }}>
<Inner />
</ComponentContainer>
);
@@ -275,7 +295,10 @@ it('reload component after task progress finished', async () => {
const inProgressTask = { id: 'foo', status: STATUSES.IN_PROGRESS } as T.Task;
(getTasksForComponent as jest.Mock<any>).mockResolvedValueOnce({ queue: [inProgressTask] });
const wrapper = shallow(
<ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'foo' } }}>
<ComponentContainer
appState={{ organizationsEnabled: false }}
fetchOrganizations={jest.fn()}
location={{ query: { id: 'foo' } }}>
<Inner />
</ComponentContainer>
);

+ 6
- 7
server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx View File

@@ -20,7 +20,7 @@
import * as React from 'react';
import { Link } from 'react-router';
import ProductNewsMenuItem from './ProductNewsMenuItem';
import { SuggestionLink } from './SuggestionsProvider';
import { SuggestionsContext } from './SuggestionsContext';
import { translate } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/urls';
import { isSonarCloud } from '../../../helpers/system';
@@ -28,7 +28,6 @@ import { DropdownOverlay } from '../../../components/controls/Dropdown';

interface Props {
onClose: () => void;
suggestions: Array<SuggestionLink>;
}

export default class EmbedDocsPopup extends React.PureComponent<Props> {
@@ -36,14 +35,14 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> {
return <li className="menu-header">{text}</li>;
}

renderSuggestions() {
if (this.props.suggestions.length === 0) {
renderSuggestions = ({ suggestions }: { suggestions: T.SuggestionLink[] }) => {
if (suggestions.length === 0) {
return null;
}
return (
<>
{this.renderTitle(translate('embed_docs.suggestion'))}
{this.props.suggestions.map((suggestion, index) => (
{suggestions.map((suggestion, index) => (
<li key={index}>
<Link onClick={this.props.onClose} target="_blank" to={suggestion.link}>
{suggestion.text}
@@ -53,7 +52,7 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> {
<li className="divider" />
</>
);
}
};

renderIconLink(link: string, icon: string, text: string) {
return (
@@ -138,7 +137,7 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> {
return (
<DropdownOverlay>
<ul className="menu abs-width-240">
{this.renderSuggestions()}
<SuggestionsContext.Consumer>{this.renderSuggestions}</SuggestionsContext.Consumer>
<li>
<Link onClick={this.props.onClose} target="_blank" to="/documentation">
{translate('embed_docs.documentation')}

+ 2
- 8
server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx View File

@@ -18,7 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { SuggestionLink } from './SuggestionsProvider';
import Toggler from '../../../components/controls/Toggler';
import HelpIcon from '../../../components/icons-components/HelpIcon';
import { lazyLoad } from '../../../components/lazyLoad';
@@ -26,14 +25,11 @@ import { translate } from '../../../helpers/l10n';

const EmbedDocsPopup = lazyLoad(() => import('./EmbedDocsPopup'));

interface Props {
suggestions: Array<SuggestionLink>;
}
interface State {
helpOpen: boolean;
}

export default class EmbedDocsPopupHelper extends React.PureComponent<Props, State> {
export default class EmbedDocsPopupHelper extends React.PureComponent<{}, State> {
mounted = false;
state: State = { helpOpen: false };

@@ -81,9 +77,7 @@ export default class EmbedDocsPopupHelper extends React.PureComponent<Props, Sta
<Toggler
onRequestClose={this.closeHelp}
open={this.state.helpOpen}
overlay={
<EmbedDocsPopup onClose={this.closeHelp} suggestions={this.props.suggestions} />
}>
overlay={<EmbedDocsPopup onClose={this.closeHelp} />}>
<a className="navbar-help" href="#" onClick={this.handleClick} title={translate('help')}>
<HelpIcon />
</a>

+ 23
- 10
server/sonar-web/src/main/js/app/components/embed-docs-modal/Suggestions.tsx View File

@@ -18,33 +18,46 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { SuggestionsContext } from './SuggestionsContext';

interface Props {
suggestions: string;
}

export default class Suggestions extends React.PureComponent<Props> {
context!: { suggestions: SuggestionsContext };
export default function Suggestions({ suggestions }: Props) {
return (
<SuggestionsContext.Consumer>
{({ addSuggestions, removeSuggestions }) => (
<SuggestionsInner
addSuggestions={addSuggestions}
removeSuggestions={removeSuggestions}
suggestions={suggestions}
/>
)}
</SuggestionsContext.Consumer>
);
}

static contextTypes = {
suggestions: PropTypes.object.isRequired
};
interface SuggestionsInnerProps {
addSuggestions: (key: string) => void;
removeSuggestions: (key: string) => void;
suggestions: string;
}

class SuggestionsInner extends React.PureComponent<SuggestionsInnerProps> {
componentDidMount() {
this.context.suggestions.addSuggestions(this.props.suggestions);
this.props.addSuggestions(this.props.suggestions);
}

componentDidUpdate(prevProps: Props) {
if (prevProps.suggestions !== this.props.suggestions) {
this.context.suggestions.removeSuggestions(this.props.suggestions);
this.context.suggestions.addSuggestions(prevProps.suggestions);
this.props.removeSuggestions(this.props.suggestions);
this.props.addSuggestions(prevProps.suggestions);
}
}

componentWillUnmount() {
this.context.suggestions.removeSuggestions(this.props.suggestions);
this.props.removeSuggestions(this.props.suggestions);
}

render() {

+ 10
- 1
server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsContext.ts View File

@@ -17,7 +17,16 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
export interface SuggestionsContext {
import { createContext } from 'react';

interface SuggestionsContextShape {
addSuggestions: (key: string) => void;
removeSuggestions: (key: string) => void;
suggestions: T.SuggestionLink[];
}

export const SuggestionsContext = createContext<SuggestionsContextShape>({
addSuggestions: () => {},
removeSuggestions: () => {},
suggestions: []
});

+ 18
- 36
server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx View File

@@ -18,56 +18,33 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as suggestionsJson from 'Docs/EmbedDocsSuggestions.json';
import suggestionsJson from 'Docs/EmbedDocsSuggestions.json';
import { SuggestionsContext } from './SuggestionsContext';
import { isSonarCloud } from '../../../helpers/system';

export interface SuggestionLink {
link: string;
scope?: 'sonarcloud';
text: string;
}

interface SuggestionsJson {
[key: string]: SuggestionLink[];
}

interface Props {
children: ({ suggestions }: { suggestions: SuggestionLink[] }) => React.ReactNode;
[key: string]: T.SuggestionLink[];
}

interface State {
suggestions: SuggestionLink[];
suggestions: T.SuggestionLink[];
}

export default class SuggestionsProvider extends React.Component<Props, State> {
export default class SuggestionsProvider extends React.Component<{}, State> {
keys: string[] = [];

static childContextTypes = {
suggestions: PropTypes.object
};

state: State = { suggestions: [] };

getChildContext = (): { suggestions: SuggestionsContext } => {
return {
suggestions: {
addSuggestions: this.addSuggestions,
removeSuggestions: this.removeSuggestions
}
};
};

fetchSuggestions = () => {
const jsonList = suggestionsJson as SuggestionsJson;
let suggestions: SuggestionLink[] = [];
let suggestions: T.SuggestionLink[] = [];
this.keys.forEach(key => {
if (jsonList[key]) {
suggestions = [...jsonList[key], ...suggestions];
}
});
if (!isSonarCloud()) {
suggestions = suggestions.filter(suggestion => suggestion.scope !== 'sonarcloud');
}
this.setState({ suggestions });
};

@@ -82,10 +59,15 @@ export default class SuggestionsProvider extends React.Component<Props, State> {
};

render() {
const suggestions = isSonarCloud()
? this.state.suggestions
: this.state.suggestions.filter(suggestion => suggestion.scope !== 'sonarcloud');

return this.props.children({ suggestions });
return (
<SuggestionsContext.Provider
value={{
addSuggestions: this.addSuggestions,
removeSuggestions: this.removeSuggestions,
suggestions: this.state.suggestions
}}>
{this.props.children}
</SuggestionsContext.Provider>
);
}
}

+ 2
- 21
server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx View File

@@ -20,27 +20,8 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import EmbedDocsPopup from '../EmbedDocsPopup';
import { isSonarCloud } from '../../../../helpers/system';

jest.mock('../../../../helpers/system', () => ({ isSonarCloud: jest.fn().mockReturnValue(false) }));

const suggestions = [{ link: '#', text: 'foo' }, { link: '#', text: 'bar' }];

it('should display suggestion links', () => {
const context = {};
const wrapper = shallow(<EmbedDocsPopup onClose={jest.fn()} suggestions={suggestions} />, {
context
});
wrapper.update();
expect(wrapper).toMatchSnapshot();
});

it('should display correct links for SonarCloud', () => {
(isSonarCloud as jest.Mock<any>).mockReturnValueOnce(true);
const context = {};
const wrapper = shallow(<EmbedDocsPopup onClose={jest.fn()} suggestions={suggestions} />, {
context
});
wrapper.update();
it('should render', () => {
const wrapper = shallow(<EmbedDocsPopup onClose={jest.fn()} />);
expect(wrapper).toMatchSnapshot();
});

+ 30
- 20
server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx View File

@@ -25,8 +25,10 @@ import { isSonarCloud } from '../../../../helpers/system';
jest.mock(
'Docs/EmbedDocsSuggestions.json',
() => ({
pageA: [{ link: '/foo', text: 'Foo' }, { link: '/bar', text: 'Bar', scope: 'sonarcloud' }],
pageB: [{ link: '/qux', text: 'Qux' }]
default: {
pageA: [{ link: '/foo', text: 'Foo' }, { link: '/bar', text: 'Bar', scope: 'sonarcloud' }],
pageB: [{ link: '/qux', text: 'Qux' }]
}
}),
{ virtual: true }
);
@@ -34,33 +36,41 @@ jest.mock(
jest.mock('../../../../helpers/system', () => ({ isSonarCloud: jest.fn() }));

it('should add & remove suggestions', () => {
(isSonarCloud as jest.Mock).mockImplementation(() => false);
const children = jest.fn();
const wrapper = shallow(<SuggestionsProvider>{children}</SuggestionsProvider>);
const instance = wrapper.instance() as SuggestionsProvider;
expect(children).lastCalledWith({ suggestions: [] });
(isSonarCloud as jest.Mock).mockReturnValue(false);
const wrapper = shallow<SuggestionsProvider>(
<SuggestionsProvider>
<div />
</SuggestionsProvider>
);
const instance = wrapper.instance();
expect(wrapper.state('suggestions')).toEqual([]);

instance.addSuggestions('pageA');
expect(children).lastCalledWith({ suggestions: [{ link: '/foo', text: 'Foo' }] });
expect(wrapper.state('suggestions')).toEqual([{ link: '/foo', text: 'Foo' }]);

instance.addSuggestions('pageB');
expect(children).lastCalledWith({
suggestions: [{ link: '/qux', text: 'Qux' }, { link: '/foo', text: 'Foo' }]
});
expect(wrapper.state('suggestions')).toEqual([
{ link: '/qux', text: 'Qux' },
{ link: '/foo', text: 'Foo' }
]);

instance.removeSuggestions('pageA');
expect(children).lastCalledWith({ suggestions: [{ link: '/qux', text: 'Qux' }] });
expect(wrapper.state('suggestions')).toEqual([{ link: '/qux', text: 'Qux' }]);
});

it('should show sonarcloud pages', () => {
(isSonarCloud as jest.Mock).mockImplementation(() => true);
const children = jest.fn();
const wrapper = shallow(<SuggestionsProvider>{children}</SuggestionsProvider>);
const instance = wrapper.instance() as SuggestionsProvider;
expect(children).lastCalledWith({ suggestions: [] });
(isSonarCloud as jest.Mock).mockReturnValue(true);
const wrapper = shallow<SuggestionsProvider>(
<SuggestionsProvider>
<div />
</SuggestionsProvider>
);
const instance = wrapper.instance();
expect(wrapper.state('suggestions')).toEqual([]);

instance.addSuggestions('pageA');
expect(children).lastCalledWith({
suggestions: [{ link: '/foo', text: 'Foo' }, { link: '/bar', text: 'Bar', scope: 'sonarcloud' }]
});
expect(wrapper.state('suggestions')).toEqual([
{ link: '/foo', text: 'Foo' },
{ link: '/bar', text: 'Bar', scope: 'sonarcloud' }
]);
});

+ 4
- 156
server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap View File

@@ -1,165 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display correct links for SonarCloud 1`] = `
exports[`should render 1`] = `
<DropdownOverlay>
<ul
className="menu abs-width-240"
>
<li
className="menu-header"
>
embed_docs.suggestion
</li>
<li
key="0"
>
<Link
onClick={[MockFunction]}
onlyActiveOnIndex={false}
style={Object {}}
target="_blank"
to="#"
>
foo
</Link>
</li>
<li
key="1"
>
<Link
onClick={[MockFunction]}
onlyActiveOnIndex={false}
style={Object {}}
target="_blank"
to="#"
>
bar
</Link>
</li>
<li
className="divider"
/>
<li>
<Link
onClick={[MockFunction]}
onlyActiveOnIndex={false}
style={Object {}}
target="_blank"
to="/documentation"
>
embed_docs.documentation
</Link>
</li>
<li>
<Link
onClick={[MockFunction]}
onlyActiveOnIndex={false}
style={Object {}}
to="/web_api"
>
api_documentation.page
</Link>
</li>
<li
className="divider"
/>
<li>
<a
href="https://community.sonarsource.com/c/help/sc"
rel="noopener noreferrer"
target="_blank"
>
embed_docs.get_help
</a>
</li>
<li
className="divider"
/>
<li
className="menu-header"
>
embed_docs.stay_connected
</li>
<li>
<a
href="https://twitter.com/sonarcloud"
rel="noopener noreferrer"
target="_blank"
>
<img
alt="Twitter"
className="spacer-right"
height="18"
src="/images/embed-doc/twitter-icon.svg"
width="18"
/>
Twitter
</a>
</li>
<li>
<a
href="https://blog.sonarsource.com/product/SonarCloud"
rel="noopener noreferrer"
target="_blank"
>
<img
alt="embed_docs.news"
className="spacer-right"
height="18"
src="/images/sonarcloud-square-logo.svg"
width="18"
/>
embed_docs.news
</a>
</li>
<li>
<Connect(ProductNewsMenuItem)
tag="SonarCloud"
/>
</li>
</ul>
</DropdownOverlay>
`;

exports[`should display suggestion links 1`] = `
<DropdownOverlay>
<ul
className="menu abs-width-240"
>
<li
className="menu-header"
>
embed_docs.suggestion
</li>
<li
key="0"
>
<Link
onClick={[MockFunction]}
onlyActiveOnIndex={false}
style={Object {}}
target="_blank"
to="#"
>
foo
</Link>
</li>
<li
key="1"
>
<Link
onClick={[MockFunction]}
onlyActiveOnIndex={false}
style={Object {}}
target="_blank"
to="#"
>
bar
</Link>
</li>
<li
className="divider"
/>
<ContextConsumer>
<Component />
</ContextConsumer>
<li>
<Link
onClick={[MockFunction]}

+ 44
- 13
server/sonar-web/src/main/js/app/components/extensions/Extension.tsx View File

@@ -19,29 +19,38 @@
*/
import * as React from 'react';
import Helmet from 'react-helmet';
import * as PropTypes from 'prop-types';
import { withRouter, WithRouterProps } from 'react-router';
import { injectIntl, InjectedIntlProps } from 'react-intl';
import { connect } from 'react-redux';
import { getExtensionStart } from './utils';
import { translate } from '../../../helpers/l10n';
import getStore from '../../utils/getStore';
import { addGlobalErrorMessage } from '../../../store/globalMessages';
import { Store, getCurrentUser } from '../../../store/rootReducer';

interface OwnProps {
currentUser: T.CurrentUser;
extension: { key: string; name: string };
onFail: (message: string) => void;
options?: {};
}

type Props = OwnProps & WithRouterProps & InjectedIntlProps;
interface StateProps {
currentUser: T.CurrentUser;
}

class Extension extends React.PureComponent<Props> {
interface DispatchProps {
onFail: (message: string) => void;
}

type Props = OwnProps & WithRouterProps & InjectedIntlProps & StateProps & DispatchProps;

interface State {
extensionElement?: React.ReactElement<any>;
}

class Extension extends React.PureComponent<Props, State> {
container?: HTMLElement | null;
stop?: Function;

static contextTypes = {
suggestions: PropTypes.object.isRequired
};
state: State = {};

componentDidMount() {
this.startExtension();
@@ -62,16 +71,21 @@ class Extension extends React.PureComponent<Props> {

handleStart = (start: Function) => {
const store = getStore();
this.stop = start({
const result = start({
store,
el: this.container,
currentUser: this.props.currentUser,
intl: this.props.intl,
location: this.props.location,
router: this.props.router,
suggestions: this.context.suggestions,
...this.props.options
});

if (React.isValidElement(result)) {
this.setState({ extensionElement: result });
} else {
this.stop = result;
}
};

handleFailure = () => {
@@ -94,10 +108,27 @@ class Extension extends React.PureComponent<Props> {
return (
<div>
<Helmet title={this.props.extension.name} />
<div ref={container => (this.container = container)} />
{this.state.extensionElement ? (
this.state.extensionElement
) : (
<div ref={container => (this.container = container)} />
)}
</div>
);
}
}

export default injectIntl(withRouter(Extension));
function mapStateToProps(state: Store): StateProps {
return { currentUser: getCurrentUser(state) };
}

const mapDispatchToProps: DispatchProps = { onFail: addGlobalErrorMessage };

export default injectIntl<OwnProps & InjectedIntlProps>(
withRouter(
connect(
mapStateToProps,
mapDispatchToProps
)(Extension)
)
);

+ 2
- 6
server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { connect } from 'react-redux';
import ExtensionContainer from './ExtensionContainer';
import Extension from './Extension';
import NotFound from '../NotFound';
import { getAppState, Store } from '../../../store/rootReducer';

@@ -31,11 +31,7 @@ interface Props {
function GlobalAdminPageExtension(props: Props) {
const { extensionKey, pluginKey } = props.params;
const extension = (props.adminPages || []).find(p => p.key === `${pluginKey}/${extensionKey}`);
return extension ? (
<ExtensionContainer extension={extension} />
) : (
<NotFound withContainer={false} />
);
return extension ? <Extension extension={extension} /> : <NotFound withContainer={false} />;
}

const mapStateToProps = (state: Store) => ({

+ 2
- 6
server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { connect } from 'react-redux';
import ExtensionContainer from './ExtensionContainer';
import Extension from './Extension';
import NotFound from '../NotFound';
import { getAppState, Store } from '../../../store/rootReducer';

@@ -31,11 +31,7 @@ interface Props {
function GlobalPageExtension(props: Props) {
const { extensionKey, pluginKey } = props.params;
const extension = (props.globalPages || []).find(p => p.key === `${pluginKey}/${extensionKey}`);
return extension ? (
<ExtensionContainer extension={extension} />
) : (
<NotFound withContainer={false} />
);
return extension ? <Extension extension={extension} /> : <NotFound withContainer={false} />;
}

const mapStateToProps = (state: Store) => ({

+ 2
- 2
server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { connect } from 'react-redux';
import ExtensionContainer from './ExtensionContainer';
import Extension from './Extension';
import NotFound from '../NotFound';
import { getOrganizationByKey, Store } from '../../../store/rootReducer';
import { fetchOrganization } from '../../../apps/organizations/actions';
@@ -63,7 +63,7 @@ class OrganizationPageExtension extends React.PureComponent<Props> {

const extension = pages.find(p => p.key === `${pluginKey}/${extensionKey}`);
return extension ? (
<ExtensionContainer
<Extension
extension={extension}
options={{ organization, refreshOrganization: this.refreshOrganization }}
/>

+ 2
- 2
server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx View File

@@ -20,7 +20,7 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { Location } from 'history';
import ExtensionContainer from './ExtensionContainer';
import Extension from './Extension';
import NotFound from '../NotFound';
import { addGlobalErrorMessage } from '../../../store/globalMessages';

@@ -37,7 +37,7 @@ function ProjectAdminPageExtension(props: Props) {
component.configuration &&
(component.configuration.extensions || []).find(p => p.key === `${pluginKey}/${extensionKey}`);
return extension ? (
<ExtensionContainer extension={extension} options={{ component }} />
<Extension extension={extension} options={{ component }} />
) : (
<NotFound withContainer={false} />
);

+ 2
- 2
server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx View File

@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import ExtensionContainer from './ExtensionContainer';
import Extension from './Extension';
import NotFound from '../NotFound';

interface Props {
@@ -37,7 +37,7 @@ export default function ProjectPageExtension(props: Props) {
component.extensions &&
component.extensions.find(p => p.key === `${pluginKey}/${extensionKey}`);
return extension ? (
<ExtensionContainer extension={extension} options={{ component }} />
<Extension extension={extension} options={{ component }} />
) : (
<NotFound withContainer={false} />
);

+ 8
- 13
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.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 { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import ComponentNavBranchesMenu from './ComponentNavBranchesMenu';
@@ -38,8 +37,10 @@ import Toggler from '../../../../components/controls/Toggler';
import DropdownIcon from '../../../../components/icons-components/DropdownIcon';
import { isSonarCloud } from '../../../../helpers/system';
import { getPortfolioAdminUrl } from '../../../../helpers/urls';
import { withAppState } from '../../../../components/withAppState';

interface Props {
appState: Pick<T.AppState, 'branchesEnabled'>;
branchLikes: T.BranchLike[];
component: T.Component;
currentBranchLike: T.BranchLike;
@@ -50,17 +51,9 @@ interface State {
dropdownOpen: boolean;
}

export default class ComponentNavBranch extends React.PureComponent<Props, State> {
export class ComponentNavBranch extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
branchesEnabled: PropTypes.bool.isRequired,
canAdmin: PropTypes.bool.isRequired
};

state: State = {
dropdownOpen: false
};
state: State = { dropdownOpen: false };

componentDidMount() {
this.mounted = true;
@@ -145,7 +138,7 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
const { branchLikes, currentBranchLike } = this.props;
const { configuration, breadcrumbs } = this.props.component;

if (isSonarCloud() && !this.context.branchesEnabled) {
if (isSonarCloud() && !this.props.appState.branchesEnabled) {
return null;
}

@@ -170,7 +163,7 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
</div>
);
} else {
if (!this.context.branchesEnabled) {
if (!this.props.appState.branchesEnabled) {
return (
<div className="navbar-context-branches">
<BranchIcon
@@ -235,3 +228,5 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
);
}
}

export default withAppState(ComponentNavBranch);

+ 8
- 10
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.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 { Link } from 'react-router';
import ComponentNavBranchesMenuItem from './ComponentNavBranchesMenuItem';
import {
@@ -36,6 +35,7 @@ import { getBranchLikeUrl } from '../../../../helpers/urls';
import SearchBox from '../../../../components/controls/SearchBox';
import HelpTooltip from '../../../../components/controls/HelpTooltip';
import { DropdownOverlay } from '../../../../components/controls/Dropdown';
import { withRouter, Router } from '../../../../components/hoc/withRouter';

interface Props {
branchLikes: T.BranchLike[];
@@ -43,6 +43,7 @@ interface Props {
component: T.Component;
currentBranchLike: T.BranchLike;
onClose: () => void;
router: Pick<Router, 'push'>;
}

interface State {
@@ -50,14 +51,9 @@ interface State {
selected: T.BranchLike | undefined;
}

export default class ComponentNavBranchesMenu extends React.PureComponent<Props, State> {
private listNode?: HTMLUListElement | null;
private selectedBranchNode?: HTMLLIElement | null;

static contextTypes = {
router: PropTypes.object
};

export class ComponentNavBranchesMenu extends React.PureComponent<Props, State> {
listNode?: HTMLUListElement | null;
selectedBranchNode?: HTMLLIElement | null;
state: State = { query: '', selected: undefined };

componentDidMount() {
@@ -113,7 +109,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
openSelected = () => {
const selected = this.getSelected();
if (selected) {
this.context.router.push(this.getProjectBranchUrl(selected));
this.props.router.push(this.getProjectBranchUrl(selected));
}
};

@@ -263,3 +259,5 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
);
}
}

export default withRouter(ComponentNavBranchesMenu);

+ 6
- 8
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx View File

@@ -19,12 +19,13 @@
*/
import * as React from 'react';
import { Link } from 'react-router';
import * as PropTypes from 'prop-types';
import NavBarNotif from '../../../../components/nav/NavBarNotif';
import { translate } from '../../../../helpers/l10n';
import { isValidLicense } from '../../../../api/marketplace';
import { withAppState } from '../../../../components/withAppState';

interface Props {
appState: Pick<T.AppState, 'canAdmin'>;
currentTask?: T.Task;
}

@@ -33,13 +34,8 @@ interface State {
loading: boolean;
}

export default class ComponentNavLicenseNotif extends React.PureComponent<Props, State> {
export class ComponentNavLicenseNotif extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
canAdmin: PropTypes.bool.isRequired
};

state: State = { loading: false };

componentDidMount() {
@@ -88,7 +84,7 @@ export default class ComponentNavLicenseNotif extends React.PureComponent<Props,
return (
<NavBarNotif variant="error">
<span className="little-spacer-right">{currentTask.errorMessage}</span>
{this.context.canAdmin ? (
{this.props.appState.canAdmin ? (
<Link to="/admin/extension/license/app">
{translate('license.component_navigation.button', currentTask.errorType)}.
</Link>
@@ -99,3 +95,5 @@ export default class ComponentNavLicenseNotif extends React.PureComponent<Props,
);
}
}

export default withAppState(ComponentNavLicenseNotif);

+ 6
- 7
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx View File

@@ -20,7 +20,6 @@
import * as React from 'react';
import { Link } from 'react-router';
import * as classNames from 'classnames';
import * as PropTypes from 'prop-types';
import Dropdown from '../../../../components/controls/Dropdown';
import NavBarTabs from '../../../../components/nav/NavBarTabs';
import {
@@ -31,6 +30,7 @@ import {
} from '../../../../helpers/branches';
import { translate } from '../../../../helpers/l10n';
import DropdownIcon from '../../../../components/icons-components/DropdownIcon';
import { withAppState } from '../../../../components/withAppState';

const SETTINGS_URLS = [
'/project/admin',
@@ -49,16 +49,13 @@ const SETTINGS_URLS = [
];

interface Props {
appState: Pick<T.AppState, 'branchesEnabled'>;
branchLike: T.BranchLike | undefined;
component: T.Component;
location?: any;
}

export default class ComponentNavMenu extends React.PureComponent<Props> {
static contextTypes = {
branchesEnabled: PropTypes.bool.isRequired
};

export class ComponentNavMenu extends React.PureComponent<Props> {
isProject() {
return this.props.component.qualifier === 'TRK';
}
@@ -282,7 +279,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {

renderBranchesLink() {
if (
!this.context.branchesEnabled ||
!this.props.appState.branchesEnabled ||
!this.isProject() ||
!this.getConfiguration().showSettings
) {
@@ -504,3 +501,5 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
);
}
}

export default withAppState(ComponentNavMenu);

+ 15
- 15
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import ComponentNavBranch from '../ComponentNavBranch';
import { ComponentNavBranch } from '../ComponentNavBranch';
import { click } from '../../../../../helpers/testUtils';
import { isSonarCloud } from '../../../../../helpers/system';

@@ -37,11 +37,11 @@ it('renders main branch', () => {
expect(
shallow(
<ComponentNavBranch
appState={{ branchesEnabled: true }}
branchLikes={[mainBranch, fooBranch]}
component={component}
currentBranchLike={mainBranch}
/>,
{ context: { branchesEnabled: true, canAdmin: true } }
/>
)
).toMatchSnapshot();
});
@@ -58,11 +58,11 @@ it('renders short-living branch', () => {
expect(
shallow(
<ComponentNavBranch
appState={{ branchesEnabled: true }}
branchLikes={[branch, fooBranch]}
component={component}
currentBranchLike={branch}
/>,
{ context: { branchesEnabled: true, canAdmin: true } }
/>
)
).toMatchSnapshot();
});
@@ -79,11 +79,11 @@ it('renders pull request', () => {
expect(
shallow(
<ComponentNavBranch
appState={{ branchesEnabled: true }}
branchLikes={[pullRequest, fooBranch]}
component={component}
currentBranchLike={pullRequest}
/>,
{ context: { branchesEnabled: true, canAdmin: true } }
/>
)
).toMatchSnapshot();
});
@@ -92,11 +92,11 @@ it('opens menu', () => {
const component = {} as T.Component;
const wrapper = shallow(
<ComponentNavBranch
appState={{ branchesEnabled: true }}
branchLikes={[mainBranch, fooBranch]}
component={component}
currentBranchLike={mainBranch}
/>,
{ context: { branchesEnabled: true, canAdmin: true } }
/>
);
expect(wrapper.find('Toggler').prop('open')).toBe(false);
click(wrapper.find('a'));
@@ -107,11 +107,11 @@ it('renders single branch popup', () => {
const component = {} as T.Component;
const wrapper = shallow(
<ComponentNavBranch
appState={{ branchesEnabled: true }}
branchLikes={[mainBranch]}
component={component}
currentBranchLike={mainBranch}
/>,
{ context: { branchesEnabled: true, canAdmin: true } }
/>
);
expect(wrapper.find('DocTooltip')).toMatchSnapshot();
});
@@ -120,11 +120,11 @@ it('renders no branch support popup', () => {
const component = {} as T.Component;
const wrapper = shallow(
<ComponentNavBranch
appState={{ branchesEnabled: false }}
branchLikes={[mainBranch, fooBranch]}
component={component}
currentBranchLike={mainBranch}
/>,
{ context: { branchesEnabled: false, canAdmin: true } }
/>
);
expect(wrapper.find('DocTooltip')).toMatchSnapshot();
});
@@ -134,11 +134,11 @@ it('renders nothing on SonarCloud without branch support', () => {
const component = {} as T.Component;
const wrapper = shallow(
<ComponentNavBranch
appState={{ branchesEnabled: false }}
branchLikes={[mainBranch]}
component={component}
currentBranchLike={mainBranch}
/>,
{ context: { branchesEnabled: false, onSonarCloud: true, canAdmin: true } }
/>
);
expect(wrapper.type()).toBeNull();
});

+ 4
- 1
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import ComponentNavBranchesMenu from '../ComponentNavBranchesMenu';
import { ComponentNavBranchesMenu } from '../ComponentNavBranchesMenu';
import { elementKeydown } from '../../../../../helpers/testUtils';

const component = { key: 'component' } as T.Component;
@@ -38,6 +38,7 @@ it('renders list', () => {
component={component}
currentBranchLike={mainBranch()}
onClose={jest.fn()}
router={{ push: jest.fn() }}
/>
)
).toMatchSnapshot();
@@ -56,6 +57,7 @@ it('searches', () => {
component={component}
currentBranchLike={mainBranch()}
onClose={jest.fn()}
router={{ push: jest.fn() }}
/>
);
wrapper.setState({ query: 'bar' });
@@ -69,6 +71,7 @@ it('selects next & previous', () => {
component={component}
currentBranchLike={mainBranch()}
onClose={jest.fn()}
router={{ push: jest.fn() }}
/>
);
elementKeydown(wrapper.find('SearchBox'), 40);

+ 11
- 11
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import ComponentNavLicenseNotif from '../ComponentNavLicenseNotif';
import { ComponentNavLicenseNotif } from '../ComponentNavLicenseNotif';
import { isValidLicense } from '../../../../../api/marketplace';
import { waitAndUpdate } from '../../../../../helpers/testUtils';

@@ -39,15 +39,15 @@ beforeEach(() => {

it('renders background task license info correctly', async () => {
let wrapper = getWrapper({
currentTask: { status: 'FAILED', errorType: 'LICENSING', errorMessage: 'Foo' }
currentTask: { status: 'FAILED', errorType: 'LICENSING', errorMessage: 'Foo' } as T.Task
});
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();

wrapper = getWrapper(
{ currentTask: { status: 'FAILED', errorType: 'LICENSING', errorMessage: 'Foo' } },
{ canAdmin: false }
);
wrapper = getWrapper({
appState: { canAdmin: false },
currentTask: { status: 'FAILED', errorType: 'LICENSING', errorMessage: 'Foo' } as T.Task
});
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
});
@@ -55,7 +55,7 @@ it('renders background task license info correctly', async () => {
it('renders a different message if the license is valid', async () => {
(isValidLicense as jest.Mock<any>).mockResolvedValueOnce({ isValidLicense: true });
const wrapper = getWrapper({
currentTask: { status: 'FAILED', errorType: 'LICENSING', errorMessage: 'Foo' }
currentTask: { status: 'FAILED', errorType: 'LICENSING', errorMessage: 'Foo' } as T.Task
});
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
@@ -64,18 +64,18 @@ it('renders a different message if the license is valid', async () => {
it('renders correctly for LICENSING_LOC error', async () => {
(isValidLicense as jest.Mock<any>).mockResolvedValueOnce({ isValidLicense: true });
const wrapper = getWrapper({
currentTask: { status: 'FAILED', errorType: 'LICENSING_LOC', errorMessage: 'Foo' }
currentTask: { status: 'FAILED', errorType: 'LICENSING_LOC', errorMessage: 'Foo' } as T.Task
});
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
});

function getWrapper(props = {}, context = {}) {
function getWrapper(props: Partial<ComponentNavLicenseNotif['props']> = {}) {
return shallow(
<ComponentNavLicenseNotif
appState={{ canAdmin: true }}
currentTask={{ errorMessage: 'Foo', errorType: 'LICENSING' } as T.Task}
{...props}
/>,
{ context: { canAdmin: true, ...context } }
/>
);
}

+ 31
- 15
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import ComponentNavMenu from '../ComponentNavMenu';
import { ComponentNavMenu } from '../ComponentNavMenu';

const mainBranch: T.MainBranch = { isMain: true, name: 'master' };

@@ -37,9 +37,13 @@ it('should work with extensions', () => {
configuration: { showSettings: true, extensions: [{ key: 'foo', name: 'Foo' }] },
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }]
};
const wrapper = shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, {
context: { branchesEnabled: true }
});
const wrapper = shallow(
<ComponentNavMenu
appState={{ branchesEnabled: true }}
branchLike={mainBranch}
component={component}
/>
);
expect(wrapper.find('Dropdown[data-test="extensions"]')).toMatchSnapshot();
expect(wrapper.find('Dropdown[data-test="administration"]')).toMatchSnapshot();
});
@@ -56,9 +60,13 @@ it('should work with multiple extensions', () => {
{ key: 'component-bar', name: 'ComponentBar' }
]
};
const wrapper = shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, {
context: { branchesEnabled: true }
});
const wrapper = shallow(
<ComponentNavMenu
appState={{ branchesEnabled: true }}
branchLike={mainBranch}
component={component}
/>
);
expect(wrapper.find('Dropdown[data-test="extensions"]')).toMatchSnapshot();
expect(wrapper.find('Dropdown[data-test="administration"]')).toMatchSnapshot();
});
@@ -76,9 +84,13 @@ it('should work for short-living branches', () => {
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }]
};
expect(
shallow(<ComponentNavMenu branchLike={branch} component={component} />, {
context: { branchesEnabled: true }
})
shallow(
<ComponentNavMenu
appState={{ branchesEnabled: true }}
branchLike={branch}
component={component}
/>
)
).toMatchSnapshot();
});

@@ -88,14 +100,14 @@ it('should work for long-living branches', () => {
expect(
shallow(
<ComponentNavMenu
appState={{ branchesEnabled: true }}
branchLike={branch}
component={{
...baseComponent,
configuration: { showSettings },
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }]
}}
/>,
{ context: { branchesEnabled: true } }
/>
)
).toMatchSnapshot()
);
@@ -108,9 +120,13 @@ it('should work for all qualifiers', () => {
function checkWithQualifier(qualifier: string) {
const component = { ...baseComponent, configuration: { showSettings: true }, qualifier };
expect(
shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, {
context: { branchesEnabled: true }
})
shallow(
<ComponentNavMenu
appState={{ branchesEnabled: true }}
branchLike={mainBranch}
component={component}
/>
)
).toMatchSnapshot();
}
});

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap View File

@@ -46,7 +46,7 @@ exports[`renders 1`] = `
warnings={Array []}
/>
</div>
<ComponentNavMenu
<Connect(withAppState(ComponentNavMenu))
component={
Object {
"breadcrumbs": Array [

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap View File

@@ -72,7 +72,7 @@ exports[`renders background task in progress info correctly 1`] = `
`;

exports[`renders background task license info correctly 1`] = `
<ComponentNavLicenseNotif
<Connect(withAppState(ComponentNavLicenseNotif))
currentTask={
Object {
"errorMessage": "Foo",

+ 3
- 3
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap View File

@@ -11,7 +11,7 @@ exports[`renders main branch 1`] = `
onRequestClose={[Function]}
open={false}
overlay={
<ComponentNavBranchesMenu
<withRouter(ComponentNavBranchesMenu)
branchLikes={
Array [
Object {
@@ -88,7 +88,7 @@ exports[`renders pull request 1`] = `
onRequestClose={[Function]}
open={false}
overlay={
<ComponentNavBranchesMenu
<withRouter(ComponentNavBranchesMenu)
branchLikes={
Array [
Object {
@@ -180,7 +180,7 @@ exports[`renders short-living branch 1`] = `
onRequestClose={[Function]}
open={false}
overlay={
<ComponentNavBranchesMenu
<withRouter(ComponentNavBranchesMenu)
branchLikes={
Array [
Object {

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

@@ -30,7 +30,6 @@ import * as theme from '../../../theme';
import NavBar from '../../../../components/nav/NavBar';
import { lazyLoad } from '../../../../components/lazyLoad';
import { getCurrentUser, getAppState, Store } from '../../../../store/rootReducer';
import { SuggestionLink } from '../../embed-docs-modal/SuggestionsProvider';
import { isSonarCloud } from '../../../../helpers/system';
import { isLoggedIn } from '../../../../helpers/users';
import './GlobalNav.css';
@@ -44,7 +43,6 @@ interface StateProps {

interface OwnProps {
location: { pathname: string };
suggestions: Array<SuggestionLink>;
}

type Props = StateProps & OwnProps;
@@ -62,7 +60,7 @@ export class GlobalNav extends React.PureComponent<Props> {

<ul className="global-navbar-menu global-navbar-menu-right">
{isSonarCloud() && <GlobalNavExplore location={this.props.location} />}
<EmbedDocsPopupHelper suggestions={this.props.suggestions} />
<EmbedDocsPopupHelper />
<Search appState={appState} currentUser={currentUser} />
{isLoggedIn(currentUser) && (
<GlobalNavPlus
@@ -71,7 +69,7 @@ export class GlobalNav extends React.PureComponent<Props> {
openProjectOnboarding={this.context.openProjectOnboarding}
/>
)}
<GlobalNavUserContainer {...this.props} />
<GlobalNavUserContainer appState={appState} currentUser={currentUser} />
</ul>
</NavBar>
);

+ 6
- 7
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx View File

@@ -19,7 +19,6 @@
*/
import * as React from 'react';
import { sortBy } from 'lodash';
import * as PropTypes from 'prop-types';
import { Link } from 'react-router';
import * as theme from '../../../theme';
import Avatar from '../../../../components/ui/Avatar';
@@ -28,18 +27,16 @@ import { translate } from '../../../../helpers/l10n';
import { getBaseUrl } from '../../../../helpers/urls';
import Dropdown from '../../../../components/controls/Dropdown';
import { isLoggedIn } from '../../../../helpers/users';
import { withRouter, Router } from '../../../../components/hoc/withRouter';

interface Props {
appState: { organizationsEnabled?: boolean };
currentUser: T.CurrentUser;
organizations: T.Organization[];
router: Pick<Router, 'push'>;
}

export default class GlobalNavUser extends React.PureComponent<Props> {
static contextTypes = {
router: PropTypes.object
};

export class GlobalNavUser extends React.PureComponent<Props> {
handleLogin = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
const shouldReturnToCurrentPage = window.location.pathname !== `${getBaseUrl()}/about`;
@@ -54,7 +51,7 @@ export default class GlobalNavUser extends React.PureComponent<Props> {

handleLogout = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.context.router.push('/sessions/logout');
this.props.router.push('/sessions/logout');
};

renderAuthenticated() {
@@ -126,3 +123,5 @@ export default class GlobalNavUser extends React.PureComponent<Props> {
return isLoggedIn(this.props.currentUser) ? this.renderAuthenticated() : this.renderAnonymous();
}
}

export default withRouter(GlobalNavUser);

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

@@ -43,12 +43,7 @@ it('should render for SonarCloud', () => {
function runTest(mockedIsSonarCloud: boolean) {
(isSonarCloud as jest.Mock).mockImplementation(() => mockedIsSonarCloud);
const wrapper = shallow(
<GlobalNav
appState={appState}
currentUser={{ isLoggedIn: false }}
location={location}
suggestions={[]}
/>
<GlobalNav appState={appState} currentUser={{ isLoggedIn: false }} location={location} />
);
expect(wrapper).toMatchSnapshot();
wrapper.setProps({ currentUser: { isLoggedIn: true } });

+ 20
- 4
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import GlobalNavUser from '../GlobalNavUser';
import { GlobalNavUser } from '../GlobalNavUser';

const currentUser = { avatar: 'abcd1234', isLoggedIn: true, name: 'foo', email: 'foo@bar.baz' };
const organizations: T.Organization[] = [
@@ -32,14 +32,24 @@ const appState = { organizationsEnabled: true };
it('should render the right interface for anonymous user', () => {
const currentUser = { isLoggedIn: false };
const wrapper = shallow(
<GlobalNavUser appState={appState} currentUser={currentUser} organizations={[]} />
<GlobalNavUser
appState={appState}
currentUser={currentUser}
organizations={[]}
router={{ push: jest.fn() }}
/>
);
expect(wrapper).toMatchSnapshot();
});

it('should render the right interface for logged in user', () => {
const wrapper = shallow(
<GlobalNavUser appState={appState} currentUser={currentUser} organizations={[]} />
<GlobalNavUser
appState={appState}
currentUser={currentUser}
organizations={[]}
router={{ push: jest.fn() }}
/>
);
wrapper.setState({ open: true });
expect(wrapper.find('Dropdown')).toMatchSnapshot();
@@ -47,7 +57,12 @@ it('should render the right interface for logged in user', () => {

it('should render user organizations', () => {
const wrapper = shallow(
<GlobalNavUser appState={appState} currentUser={currentUser} organizations={organizations} />
<GlobalNavUser
appState={appState}
currentUser={currentUser}
organizations={organizations}
router={{ push: jest.fn() }}
/>
);
wrapper.setState({ open: true });
expect(wrapper.find('Dropdown')).toMatchSnapshot();
@@ -59,6 +74,7 @@ it('should not render user organizations when they are not activated', () => {
appState={{ organizationsEnabled: false }}
currentUser={currentUser}
organizations={organizations}
router={{ push: jest.fn() }}
/>
);
wrapper.setState({ open: true });

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

@@ -26,7 +26,6 @@ exports[`should render for SonarCloud 1`] = `
"pathname": "",
}
}
suggestions={Array []}
/>
<ul
className="global-navbar-menu global-navbar-menu-right"
@@ -38,9 +37,7 @@ exports[`should render for SonarCloud 1`] = `
}
}
/>
<EmbedDocsPopupHelper
suggestions={Array []}
/>
<EmbedDocsPopupHelper />
<withRouter(Search)
appState={
Object {
@@ -56,7 +53,7 @@ exports[`should render for SonarCloud 1`] = `
}
}
/>
<Connect(GlobalNavUser)
<Connect(withRouter(GlobalNavUser))
appState={
Object {
"canAdmin": false,
@@ -70,12 +67,6 @@ exports[`should render for SonarCloud 1`] = `
"isLoggedIn": false,
}
}
location={
Object {
"pathname": "",
}
}
suggestions={Array []}
/>
</ul>
</NavBar>
@@ -107,14 +98,11 @@ exports[`should render for SonarQube 1`] = `
"pathname": "",
}
}
suggestions={Array []}
/>
<ul
className="global-navbar-menu global-navbar-menu-right"
>
<EmbedDocsPopupHelper
suggestions={Array []}
/>
<EmbedDocsPopupHelper />
<withRouter(Search)
appState={
Object {
@@ -130,7 +118,7 @@ exports[`should render for SonarQube 1`] = `
}
}
/>
<Connect(GlobalNavUser)
<Connect(withRouter(GlobalNavUser))
appState={
Object {
"canAdmin": false,
@@ -144,12 +132,6 @@ exports[`should render for SonarQube 1`] = `
"isLoggedIn": false,
}
}
location={
Object {
"pathname": "",
}
}
suggestions={Array []}
/>
</ul>
</NavBar>

+ 6
- 0
server/sonar-web/src/main/js/app/types.d.ts View File

@@ -793,6 +793,12 @@ declare namespace T {
price: number;
}

export interface SuggestionLink {
link: string;
scope?: 'sonarcloud';
text: string;
}

export interface Task {
analysisId?: string;
branch?: string;

+ 6
- 8
server/sonar-web/src/main/js/apps/account/notifications/Notifications.tsx View File

@@ -20,7 +20,6 @@
import * as React from 'react';
import Helmet from 'react-helmet';
import { groupBy, partition, uniq, uniqBy, uniqWith } from 'lodash';
import * as PropTypes from 'prop-types';
import GlobalNotifications from './GlobalNotifications';
import Projects from './Projects';
import { NotificationProject } from './types';
@@ -28,8 +27,10 @@ import * as api from '../../../api/notifications';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import { translate } from '../../../helpers/l10n';
import { Alert } from '../../../components/ui/Alert';
import { withAppState } from '../../../components/withAppState';

export interface Props {
appState: Pick<T.AppState, 'organizationsEnabled'>;
fetchOrganizations: (organizations: string[]) => void;
}

@@ -41,13 +42,8 @@ interface State {
perProjectTypes: string[];
}

export default class Notifications extends React.PureComponent<Props, State> {
export class Notifications extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
organizationsEnabled: PropTypes.bool
};

state: State = {
channels: [],
globalTypes: [],
@@ -69,7 +65,7 @@ export default class Notifications extends React.PureComponent<Props, State> {
api.getNotifications().then(
response => {
if (this.mounted) {
if (this.context.organizationsEnabled) {
if (this.props.appState.organizationsEnabled) {
const organizations = uniq(response.notifications
.filter(n => n.organization)
.map(n => n.organization) as string[]);
@@ -174,6 +170,8 @@ export default class Notifications extends React.PureComponent<Props, State> {
}
}

export default withAppState(Notifications);

function areNotificationsEqual(a: T.Notification, b: T.Notification) {
return a.channel === b.channel && a.type === b.type && a.project === b.project;
}

+ 10
- 4
server/sonar-web/src/main/js/apps/account/notifications/__tests__/Notifications-test.tsx View File

@@ -20,7 +20,7 @@
/* eslint-disable import/order */
import * as React from 'react';
import { shallow } from 'enzyme';
import Notifications, { Props } from '../Notifications';
import { Notifications } from '../Notifications';
import { waitAndUpdate } from '../../../../helpers/testUtils';

jest.mock('../../../../api/notifications', () => ({
@@ -96,13 +96,19 @@ it('should NOT fetch organizations', async () => {

it('should fetch organizations', async () => {
const fetchOrganizations = jest.fn();
await shallowRender({ fetchOrganizations }, { organizationsEnabled: true });
await shallowRender({ appState: { organizationsEnabled: true }, fetchOrganizations });
expect(getNotifications).toBeCalled();
expect(fetchOrganizations).toBeCalledWith(['org']);
});

async function shallowRender(props?: Partial<Props>, context?: any) {
const wrapper = shallow(<Notifications fetchOrganizations={jest.fn()} {...props} />, { context });
async function shallowRender(props?: Partial<Notifications['props']>) {
const wrapper = shallow(
<Notifications
appState={{ organizationsEnabled: false }}
fetchOrganizations={jest.fn()}
{...props}
/>
);
await waitAndUpdate(wrapper);
return wrapper;
}

+ 2
- 2
server/sonar-web/src/main/js/apps/code/components/App.tsx View File

@@ -175,7 +175,7 @@ export class App extends React.PureComponent<Props, State> {
};

render() {
const { branchLike, component, location } = this.props;
const { branchLike, component } = this.props;
const { loading, baseComponent, components, breadcrumbs, total, sourceViewer } = this.state;
const shouldShowBreadcrumbs = breadcrumbs.length > 1;

@@ -193,7 +193,7 @@ export class App extends React.PureComponent<Props, State> {
<Suggestions suggestions="code" />
<Helmet title={sourceViewer !== undefined ? sourceViewer.name : defaultTitle} />

<Search branchLike={branchLike} component={component} location={location} />
<Search branchLike={branchLike} component={component} />

<div className="code-components">
{shouldShowBreadcrumbs && (

+ 8
- 10
server/sonar-web/src/main/js/apps/code/components/Search.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 * as classNames from 'classnames';
import Components from './Components';
import { getTree } from '../../../api/components';
@@ -26,11 +25,13 @@ import SearchBox from '../../../components/controls/SearchBox';
import { getBranchLikeQuery } from '../../../helpers/branches';
import { translate } from '../../../helpers/l10n';
import { getProjectUrl } from '../../../helpers/urls';
import { withRouter, Router, Location } from '../../../components/hoc/withRouter';

interface Props {
branchLike?: T.BranchLike;
component: T.ComponentMeasure;
location: {};
location: Location;
router: Pick<Router, 'push'>;
}

interface State {
@@ -40,13 +41,8 @@ interface State {
selectedIndex?: number;
}

export default class Search extends React.PureComponent<Props, State> {
class Search extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
router: PropTypes.object.isRequired
};

state: State = {
query: '',
loading: false
@@ -93,9 +89,9 @@ export default class Search extends React.PureComponent<Props, State> {
const selected = results[selectedIndex];

if (selected.refKey) {
this.context.router.push(getProjectUrl(selected.refKey));
this.props.router.push(getProjectUrl(selected.refKey));
} else {
this.context.router.push({
this.props.router.push({
pathname: '/code',
query: { id: component.key, selected: selected.key, ...getBranchLikeQuery(branchLike) }
});
@@ -200,3 +196,5 @@ export default class Search extends React.PureComponent<Props, State> {
);
}
}

export default withRouter(Search);

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

@@ -21,7 +21,6 @@ import * as React from 'react';
import { Helmet } from 'react-helmet';
import { connect } from 'react-redux';
import { withRouter, WithRouterProps } from 'react-router';
import * as PropTypes from 'prop-types';
import * as key from 'keymaster';
import { keyBy } from 'lodash';
import BulkChange from './BulkChange';
@@ -55,7 +54,8 @@ import {
getCurrentUser,
getLanguages,
getMyOrganizations,
Store
Store,
getAppState
} from '../../../store/rootReducer';
import { translate } from '../../../helpers/l10n';
import { RawQuery } from '../../../helpers/query';
@@ -68,6 +68,7 @@ const PAGE_SIZE = 100;
const LIMIT_BEFORE_LOAD_MORE = 5;

interface StateToProps {
appState: T.AppState;
currentUser: T.CurrentUser;
languages: T.Languages;
userOrganizations: T.Organization[];
@@ -99,10 +100,6 @@ interface State {
export class App extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
organizationsEnabled: PropTypes.bool
};

constructor(props: Props) {
super(props);
this.state = {
@@ -528,7 +525,7 @@ export class App extends React.PureComponent<Props, State> {
onFilterChange={this.handleFilterChange}
openFacets={this.state.openFacets}
organization={organization}
organizationsEnabled={this.context.organizationsEnabled}
organizationsEnabled={this.props.appState.organizationsEnabled}
query={this.state.query}
referencedProfiles={this.state.referencedProfiles}
referencedRepositories={this.state.referencedRepositories}
@@ -572,7 +569,7 @@ export class App extends React.PureComponent<Props, State> {
<div className="layout-page-main-inner">
{this.state.openRule ? (
<RuleDetails
allowCustomRules={!this.context.organizationsEnabled}
allowCustomRules={!this.props.appState.organizationsEnabled}
canWrite={this.state.canWrite}
hideQualityProfiles={hideQualityProfiles}
onActivate={this.handleRuleActivate}
@@ -643,6 +640,7 @@ function parseFacets(rawFacets: { property: string; values: { count: number; val
}

const mapStateToProps = (state: Store) => ({
appState: getAppState(state),
currentUser: getCurrentUser(state),
languages: getLanguages(state),
userOrganizations: getMyOrganizations(state)

+ 6
- 8
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.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 { Link } from 'react-router';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import Tooltip from '../../../components/controls/Tooltip';
@@ -26,8 +25,10 @@ import { getFacet } from '../../../api/issues';
import { getIssuesUrl } from '../../../helpers/urls';
import { formatMeasure } from '../../../helpers/measures';
import { translate } from '../../../helpers/l10n';
import { withAppState } from '../../../components/withAppState';

interface Props {
appState: Pick<T.AppState, 'branchesEnabled'>;
organization: string | undefined;
ruleDetails: Pick<T.RuleDetails, 'key' | 'type'>;
}
@@ -44,13 +45,8 @@ interface State {
total?: number;
}

export default class RuleDetailsIssues extends React.PureComponent<Props, State> {
export class RuleDetailsIssues extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
branchesEnabled: PropTypes.bool
};

state: State = { loading: true };

componentDidMount() {
@@ -119,7 +115,7 @@ export default class RuleDetailsIssues extends React.PureComponent<Props, State>
</span>
);

if (!this.context.branchesEnabled) {
if (!this.props.appState.branchesEnabled) {
return totalItem;
}

@@ -173,3 +169,5 @@ export default class RuleDetailsIssues extends React.PureComponent<Props, State>
);
}
}

export default withAppState(RuleDetailsIssues);

+ 6
- 2
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsIssues-test.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import RuleDetailsIssues from '../RuleDetailsIssues';
import { RuleDetailsIssues } from '../RuleDetailsIssues';
import { waitAndUpdate } from '../../../../helpers/testUtils';
import { getFacet } from '../../../../api/issues';

@@ -47,7 +47,11 @@ it('should handle hotspot rules', async () => {

async function check(ruleType: T.RuleType, requestedTypes: T.RuleType[] | undefined) {
const wrapper = shallow(
<RuleDetailsIssues organization="org" ruleDetails={{ key: 'foo', type: ruleType }} />
<RuleDetailsIssues
appState={{ branchesEnabled: false }}
organization="org"
ruleDetails={{ key: 'foo', type: ruleType }}
/>
);
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();

+ 13
- 14
server/sonar-web/src/main/js/apps/issues/components/App.tsx View File

@@ -21,7 +21,6 @@ import * as React from 'react';
import Helmet from 'react-helmet';
import * as key from 'keymaster';
import { keyBy, omit, union, without } from 'lodash';
import * as PropTypes from 'prop-types';
import BulkChangeModal from './BulkChangeModal';
import ComponentBreadcrumbs from './ComponentBreadcrumbs';
import IssuesList from './IssuesList';
@@ -73,9 +72,10 @@ import EmptySearch from '../../../components/common/EmptySearch';
import Checkbox from '../../../components/controls/Checkbox';
import DropdownIcon from '../../../components/icons-components/DropdownIcon';
import { isSonarCloud } from '../../../helpers/system';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import { withRouter, Location, Router } from '../../../components/hoc/withRouter';
import '../../../components/search-navigator.css';
import '../styles.css';
import DeferredSpinner from '../../../components/common/DeferredSpinner';

interface FetchIssuesPromise {
components: ReferencedComponent[];
@@ -94,10 +94,11 @@ interface Props {
currentUser: T.CurrentUser;
fetchIssues: (query: RawQuery, requestOrganizations?: boolean) => Promise<FetchIssuesPromise>;
hideAuthorFacet?: boolean;
location: { pathname: string; query: RawQuery };
location: Pick<Location, 'pathname' | 'query'>;
myIssues?: boolean;
onBranchesChange: () => void;
organization?: { key: string };
router: Pick<Router, 'push' | 'replace'>;
userOrganizations: T.Organization[];
}

@@ -130,13 +131,9 @@ export interface State {

const DEFAULT_QUERY = { resolved: 'false' };

export default class App extends React.PureComponent<Props, State> {
export class App extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
router: PropTypes.object.isRequired
};

constructor(props: Props) {
super(props);
this.state = {
@@ -372,16 +369,16 @@ export default class App extends React.PureComponent<Props, State> {
this.scrollToSelectedIssue
);
} else {
this.context.router.replace(path);
this.props.router.replace(path);
}
} else {
this.context.router.push(path);
this.props.router.push(path);
}
};

closeIssue = () => {
if (this.state.query) {
this.context.router.push({
this.props.router.push({
pathname: this.props.location.pathname,
query: {
...serializeQuery(this.state.query),
@@ -635,7 +632,7 @@ export default class App extends React.PureComponent<Props, State> {

handleFilterChange = (changes: Partial<Query>) => {
this.setState({ loading: true });
this.context.router.push({
this.props.router.push({
pathname: this.props.location.pathname,
query: {
...serializeQuery({ ...this.state.query, ...changes }),
@@ -651,7 +648,7 @@ export default class App extends React.PureComponent<Props, State> {
if (!this.props.component) {
saveMyIssues(myIssues);
}
this.context.router.push({
this.props.router.push({
pathname: this.props.location.pathname,
query: {
...serializeQuery({ ...this.state.query, assigned: true, assignees: [] }),
@@ -705,7 +702,7 @@ export default class App extends React.PureComponent<Props, State> {
};

handleReset = () => {
this.context.router.push({
this.props.router.push({
pathname: this.props.location.pathname,
query: {
...DEFAULT_QUERY,
@@ -1156,3 +1153,5 @@ export default class App extends React.PureComponent<Props, State> {
);
}
}

export default withRouter(App);

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

@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import App from '../App';
import { App } from '../App';
import { shallowWithIntl, waitAndUpdate } from '../../../../helpers/testUtils';

const replace = jest.fn();
@@ -60,6 +60,7 @@ const PROPS = {
onBranchesChange: () => {},
onSonarCloud: false,
organization: { key: 'foo' },
router: { push: jest.fn(), replace: jest.fn() },
userOrganizations: []
};


+ 7
- 10
server/sonar-web/src/main/js/apps/marketplace/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 { sortBy, uniqBy } from 'lodash';
import Helmet from 'react-helmet';
import Header from './Header';
@@ -36,15 +35,16 @@ import {
PluginPendingResult,
getInstalledPlugins
} from '../../api/plugins';
import { RawQuery } from '../../helpers/query';
import { translate } from '../../helpers/l10n';
import { withRouter, Location, Router } from '../../components/hoc/withRouter';
import './style.css';

export interface Props {
currentEdition?: T.EditionKey;
fetchPendingPlugins: () => void;
location: { pathname: string; query: RawQuery };
pendingPlugins: PluginPendingResult;
location: Location;
router: Pick<Router, 'push'>;
standaloneMode?: boolean;
updateCenterActive: boolean;
}
@@ -54,13 +54,8 @@ interface State {
plugins: Plugin[];
}

export default class App extends React.PureComponent<Props, State> {
class App extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
router: PropTypes.object.isRequired
};

state: State = { loadingPlugins: true, plugins: [] };

componentDidMount() {
@@ -108,7 +103,7 @@ export default class App extends React.PureComponent<Props, State> {

updateQuery = (newQuery: Partial<Query>) => {
const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery });
this.context.router.push({ pathname: this.props.location.pathname, query });
this.props.router.push({ pathname: this.props.location.pathname, query });
};

stopLoadingPlugins = () => {
@@ -151,3 +146,5 @@ export default class App extends React.PureComponent<Props, State> {
);
}
}

export default withRouter(App);

+ 9
- 10
server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.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 Helmet from 'react-helmet';
import { connect } from 'react-redux';
import ConfirmButton from '../../../components/controls/ConfirmButton';
@@ -29,6 +28,7 @@ import { Button } from '../../../components/ui/buttons';
import { getOrganizationBilling } from '../../../api/organizations';
import { isSonarCloud } from '../../../helpers/system';
import { Alert } from '../../../components/ui/Alert';
import { withRouter, Router } from '../../../components/hoc/withRouter';

interface DispatchToProps {
deleteOrganization: (key: string) => Promise<void>;
@@ -36,6 +36,7 @@ interface DispatchToProps {

interface OwnProps {
organization: Pick<T.Organization, 'key' | 'name'>;
router: Pick<Router, 'replace'>;
}

type Props = OwnProps & DispatchToProps;
@@ -46,10 +47,6 @@ interface State {

export class OrganizationDelete extends React.PureComponent<Props, State> {
mounted = false;
static contextTypes = {
router: PropTypes.object
};

state: State = {};

componentDidMount() {
@@ -82,7 +79,7 @@ export class OrganizationDelete extends React.PureComponent<Props, State> {

onDelete = () => {
return this.props.deleteOrganization(this.props.organization.key).then(() => {
this.context.router.replace('/');
this.props.router.replace('/');
});
};

@@ -128,7 +125,9 @@ export class OrganizationDelete extends React.PureComponent<Props, State> {

const mapDispatchToProps: DispatchToProps = { deleteOrganization: deleteOrganization as any };

export default connect(
null,
mapDispatchToProps
)(OrganizationDelete);
export default withRouter(
connect(
null,
mapDispatchToProps
)(OrganizationDelete)
);

+ 5
- 6
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.tsx View File

@@ -44,7 +44,7 @@ it('should redirect the page', async () => {
(isSonarCloud as jest.Mock).mockImplementation(() => false);
const deleteOrganization = jest.fn(() => Promise.resolve());
const replace = jest.fn();
const wrapper = getWrapper({ deleteOrganization }, { router: { replace } });
const wrapper = getWrapper({ deleteOrganization, router: { replace } });
(wrapper.instance() as OrganizationDelete).onDelete();
await waitAndUpdate(wrapper);
expect(deleteOrganization).toHaveBeenCalledWith('foo');
@@ -53,20 +53,19 @@ it('should redirect the page', async () => {

it('should show a info message for paying organization', async () => {
(isSonarCloud as jest.Mock).mockImplementation(() => true);
const wrapper = getWrapper({}, { onSonarCloud: true });
const wrapper = getWrapper({});
await waitAndUpdate(wrapper);
expect(getOrganizationBilling).toHaveBeenCalledWith('foo');
expect(wrapper).toMatchSnapshot();
});

function getWrapper(props = {}, context = {}) {
function getWrapper(props: Partial<OrganizationDelete['props']> = {}) {
return shallow(
<OrganizationDelete
deleteOrganization={jest.fn(() => Promise.resolve())}
organization={{ key: 'foo', name: 'Foo' }}
router={{ replace: jest.fn() }}
{...props}
/>,

{ context: { router: { replace: jest.fn() }, ...context } }
/>
);
}

+ 8
- 9
server/sonar-web/src/main/js/apps/overview/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 { Helmet } from 'react-helmet';
import EmptyOverview from './EmptyOverview';
import OverviewApp from './OverviewApp';
@@ -33,6 +32,7 @@ import {
getPathUrlAsString
} from '../../../helpers/urls';
import { isSonarCloud } from '../../../helpers/system';
import { withRouter, Router } from '../../../components/hoc/withRouter';

interface Props {
branchLike?: T.BranchLike;
@@ -41,27 +41,24 @@ interface Props {
isInProgress?: boolean;
isPending?: boolean;
onComponentChange: (changes: Partial<T.Component>) => void;
router: Pick<Router, 'replace'>;
}

export default class App extends React.PureComponent<Props> {
static contextTypes = {
router: PropTypes.object
};

export class App extends React.PureComponent<Props> {
componentDidMount() {
const { branchLike, component } = this.props;

if (this.isPortfolio()) {
this.context.router.replace({
this.props.router.replace({
pathname: '/portfolio',
query: { id: component.key }
});
} else if (this.isFile()) {
this.context.router.replace(
this.props.router.replace(
getCodeUrl(component.breadcrumbs[0].key, branchLike, component.key)
);
} else if (isShortLivingBranch(branchLike)) {
this.context.router.replace(getShortLivingBranchUrl(component.key, branchLike.name));
this.props.router.replace(getShortLivingBranchUrl(component.key, branchLike.name));
}
}

@@ -116,3 +113,5 @@ export default class App extends React.PureComponent<Props> {
);
}
}

export default withRouter(App);

+ 13
- 9
server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { mount, shallow } from 'enzyme';
import App from '../App';
import { App } from '../App';
import { isSonarCloud } from '../../../../helpers/system';

jest.mock('../../../../helpers/system', () => ({ isSonarCloud: jest.fn() }));
@@ -49,7 +49,7 @@ it('should render OverviewApp', () => {

it('should render EmptyOverview', () => {
expect(
getWrapper({ component: { key: 'foo' } })
getWrapper({ component: { key: 'foo' } as T.Component })
.find('EmptyOverview')
.exists()
).toBeTruthy();
@@ -58,7 +58,7 @@ it('should render EmptyOverview', () => {
it('should render SonarCloudEmptyOverview', () => {
(isSonarCloud as jest.Mock<any>).mockReturnValue(true);
expect(
getWrapper({ component: { key: 'foo' } })
getWrapper({ component: { key: 'foo' } as T.Component })
.find('Connect(SonarCloudEmptyOverview)')
.exists()
).toBeTruthy();
@@ -81,10 +81,8 @@ it('redirects on Code page for files', () => {
branchLikes={[branch]}
component={newComponent}
onComponentChange={jest.fn()}
/>,
{
context: { router: { replace } }
}
router={{ replace }}
/>
);
expect(replace).toBeCalledWith({
pathname: '/code',
@@ -92,8 +90,14 @@ it('redirects on Code page for files', () => {
});
});

function getWrapper(props = {}) {
function getWrapper(props: Partial<App['props']> = {}) {
return shallow(
<App branchLikes={[]} component={component} onComponentChange={jest.fn()} {...props} />
<App
branchLikes={[]}
component={component}
onComponentChange={jest.fn()}
router={{ replace: jest.fn() }}
{...props}
/>
);
}

+ 6
- 8
server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.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 MetaKey from './MetaKey';
import MetaOrganizationKey from './MetaOrganizationKey';
@@ -35,11 +34,13 @@ import {
getCurrentUser,
getMyOrganizations,
getOrganizationByKey,
Store
Store,
getAppState
} from '../../../store/rootReducer';
import PrivacyBadgeContainer from '../../../components/common/PrivacyBadgeContainer';

interface StateToProps {
appState: T.AppState;
currentUser: T.CurrentUser;
organization?: T.Organization;
userOrganizations: T.Organization[];
@@ -59,12 +60,8 @@ interface OwnProps {
type Props = OwnProps & StateToProps;

export class Meta extends React.PureComponent<Props> {
static contextTypes = {
organizationsEnabled: PropTypes.bool
};

renderQualityInfos() {
const { organizationsEnabled } = this.context;
const { organizationsEnabled } = this.props.appState;
const { component, currentUser, organization, userOrganizations } = this.props;
const { qualifier, qualityProfiles, qualityGate } = component;
const isProject = qualifier === 'TRK';
@@ -98,7 +95,7 @@ export class Meta extends React.PureComponent<Props> {
}

render() {
const { organizationsEnabled } = this.context;
const { organizationsEnabled } = this.props.appState;
const { branchLike, component, measures, metrics, organization } = this.props;
const { qualifier, description, visibility } = component;

@@ -164,6 +161,7 @@ export class Meta extends React.PureComponent<Props> {
}

const mapStateToProps = (state: Store, { component }: OwnProps) => ({
appState: getAppState(state),
currentUser: getCurrentUser(state),
organization: getOrganizationByKey(state, component.organization),
userOrganizations: getMyOrganizations(state)

+ 7
- 9
server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.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 { difference } from 'lodash';
import DeleteForm from './DeleteForm';
import Form from './Form';
@@ -30,12 +29,14 @@ import {
import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
import QualifierIcon from '../../../components/icons-components/QualifierIcon';
import { translate } from '../../../helpers/l10n';
import { withRouter, Router } from '../../../components/hoc/withRouter';

export interface Props {
interface Props {
fromDetails?: boolean;
organization?: { isDefault?: boolean; key: string };
permissionTemplate: T.PermissionTemplate;
refresh: () => void;
router: Pick<Router, 'replace'>;
topQualifiers: string[];
}

@@ -44,13 +45,8 @@ interface State {
updateModal: boolean;
}

export default class ActionsCell extends React.PureComponent<Props, State> {
export class ActionsCell extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
router: PropTypes.object
};

state: State = { deleteForm: false, updateModal: false };

componentDidMount() {
@@ -96,7 +92,7 @@ export default class ActionsCell extends React.PureComponent<Props, State> {
const pathname = this.props.organization
? `/organizations/${this.props.organization.key}/permission_templates`
: '/permission_templates';
this.context.router.replace(pathname);
this.props.router.replace(pathname);
this.props.refresh();
});
};
@@ -214,3 +210,5 @@ export default class ActionsCell extends React.PureComponent<Props, State> {
);
}
}

export default withRouter(ActionsCell);

+ 6
- 8
server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx View File

@@ -18,29 +18,25 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
import Form from './Form';
import { createPermissionTemplate } from '../../../api/permissions';
import { Button } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';
import { withRouter, Router } from '../../../components/hoc/withRouter';

interface Props {
organization?: { key: string };
ready?: boolean;
refresh: () => Promise<void>;
router: Pick<Router, 'push'>;
}

interface State {
createModal: boolean;
}

export default class Header extends React.PureComponent<Props, State> {
class Header extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
router: PropTypes.object
};

state: State = { createModal: false };

componentDidMount() {
@@ -72,7 +68,7 @@ export default class Header extends React.PureComponent<Props, State> {
const pathname = organization
? `/organizations/${organization}/permission_templates`
: '/permission_templates';
this.context.router.push({ pathname, query: { id: response.permissionTemplate.id } });
this.props.router.push({ pathname, query: { id: response.permissionTemplate.id } });
});
});
};
@@ -102,3 +98,5 @@ export default class Header extends React.PureComponent<Props, State> {
);
}
}

export default withRouter(Header);

+ 3
- 2
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import ActionsCell, { Props } from '../ActionsCell';
import { ActionsCell } from '../ActionsCell';

const SAMPLE = {
createdAt: '2018-01-01',
@@ -29,11 +29,12 @@ const SAMPLE = {
defaultFor: []
};

function renderActionsCell(props?: Partial<Props>) {
function renderActionsCell(props?: Partial<ActionsCell['props']>) {
return shallow(
<ActionsCell
permissionTemplate={SAMPLE}
refresh={() => true}
router={{ replace: jest.fn() }}
topQualifiers={['TRK', 'VW']}
{...props}
/>

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

@@ -7,7 +7,7 @@ exports[`renders 1`] = `
<h4>
project_activity.page
</h4>
<PreviewGraph
<withRouter(PreviewGraph)
history={
Object {
"coverage": Array [

+ 11
- 12
server/sonar-web/src/main/js/apps/projects/components/AllProjects.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 Helmet from 'react-helmet';
import { omitBy } from 'lodash';
import PageHeader from './PageHeader';
@@ -38,15 +37,17 @@ import { fetchProjects, parseSorting, SORTING_SWITCH } from '../utils';
import { parseUrlQuery, Query, hasFilterParams, hasVisualizationParams } from '../query';
import { isSonarCloud } from '../../../helpers/system';
import { isLoggedIn } from '../../../helpers/users';
import { withRouter, Location, Router } from '../../../components/hoc/withRouter';
import '../../../components/search-navigator.css';
import '../styles.css';

export interface Props {
interface Props {
currentUser: T.CurrentUser;
isFavorite: boolean;
location: { pathname: string; query: RawQuery };
location: Pick<Location, 'pathname' | 'query'>;
organization: T.Organization | undefined;
organizationsEnabled?: boolean;
router: Pick<Router, 'push' | 'replace'>;
storageOptionsSuffix?: string;
}

@@ -63,13 +64,9 @@ const PROJECTS_SORT = 'sonarqube.projects.sort';
const PROJECTS_VIEW = 'sonarqube.projects.view';
const PROJECTS_VISUALIZATION = 'sonarqube.projects.visualization';

export default class AllProjects extends React.PureComponent<Props, State> {
export class AllProjects extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
router: PropTypes.object.isRequired
};

constructor(props: Props) {
super(props);
this.state = { loading: true, query: {} };
@@ -187,7 +184,7 @@ export default class AllProjects extends React.PureComponent<Props, State> {
query.sort = (sort.sortDesc ? '-' : '') + SORTING_SWITCH[sort.sortValue];
}
}
this.context.router.push({ pathname: this.props.location.pathname, query });
this.props.router.push({ pathname: this.props.location.pathname, query });
} else {
this.updateLocationQuery(query);
}
@@ -210,7 +207,7 @@ export default class AllProjects extends React.PureComponent<Props, State> {

// if there is no visualization parameters (sort, view, visualization), but there are saved preferences in the localStorage
if (initialMount && !hasVisualizationParams(query) && savedOptionsSet) {
this.context.router.replace({ pathname: this.props.location.pathname, query: savedOptions });
this.props.router.replace({ pathname: this.props.location.pathname, query: savedOptions });
} else {
this.fetchProjects(query);
}
@@ -218,11 +215,11 @@ export default class AllProjects extends React.PureComponent<Props, State> {

updateLocationQuery = (newQuery: RawQuery) => {
const query = omitBy({ ...this.props.location.query, ...newQuery }, x => !x);
this.context.router.push({ pathname: this.props.location.pathname, query });
this.props.router.push({ pathname: this.props.location.pathname, query });
};

handleClearAll = () => {
this.context.router.push({ pathname: this.props.location.pathname });
this.props.router.push({ pathname: this.props.location.pathname });
};

renderSide = () => (
@@ -328,3 +325,5 @@ export default class AllProjects extends React.PureComponent<Props, State> {
);
}
}

export default withRouter(AllProjects);

+ 10
- 14
server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx View File

@@ -18,17 +18,18 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
import AllProjectsContainer from './AllProjectsContainer';
import { PROJECTS_DEFAULT_FILTER, PROJECTS_FAVORITE, PROJECTS_ALL } from '../utils';
import { get } from '../../../helpers/storage';
import { searchProjects } from '../../../api/components';
import { isSonarCloud } from '../../../helpers/system';
import { isLoggedIn } from '../../../helpers/users';
import { withRouter, Location, Router } from '../../../components/hoc/withRouter';

interface Props {
currentUser: T.CurrentUser;
location: { pathname: string; query: { [x: string]: string } };
location: Pick<Location, 'pathname' | 'query'>;
router: Pick<Router, 'replace'>;
}

interface State {
@@ -36,19 +37,12 @@ interface State {
shouldForceSorting?: string;
}

export default class DefaultPageSelector extends React.PureComponent<Props, State> {
static contextTypes = {
router: PropTypes.object.isRequired
};

constructor(props: Props) {
super(props);
this.state = {};
}
export class DefaultPageSelector extends React.PureComponent<Props, State> {
state: State = {};

componentDidMount() {
if (isSonarCloud() && !isLoggedIn(this.props.currentUser)) {
this.context.router.replace('/explore/projects');
this.props.router.replace('/explore/projects');
}

if (!isSonarCloud()) {
@@ -61,9 +55,9 @@ export default class DefaultPageSelector extends React.PureComponent<Props, Stat
if (prevProps.location !== this.props.location) {
this.defineIfShouldBeRedirected();
} else if (this.state.shouldBeRedirected === true) {
this.context.router.replace({ ...this.props.location, pathname: '/projects/favorite' });
this.props.router.replace({ ...this.props.location, pathname: '/projects/favorite' });
} else if (this.state.shouldForceSorting != null) {
this.context.router.replace({
this.props.router.replace({
...this.props.location,
query: {
...this.props.location.query,
@@ -142,3 +136,5 @@ export default class DefaultPageSelector extends React.PureComponent<Props, Stat
return null;
}
}

export default withRouter(DefaultPageSelector);

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

@@ -20,7 +20,7 @@
/* eslint-disable import/order */
import * as React from 'react';
import { shallow } from 'enzyme';
import AllProjects, { Props } from '../AllProjects';
import { AllProjects } from '../AllProjects';
import { get, save } from '../../../../helpers/storage';

jest.mock('../ProjectsList', () => ({
@@ -162,9 +162,9 @@ it('changes perspective to risk visualization', () => {
});

function shallowRender(
props: Partial<Props> = {},
push: Function = jest.fn(),
replace: Function = jest.fn()
props: Partial<AllProjects['props']> = {},
push = jest.fn(),
replace = jest.fn()
) {
const wrapper = shallow(
<AllProjects
@@ -173,9 +173,9 @@ function shallowRender(
location={{ pathname: '/projects', query: {} }}
organization={undefined}
organizationsEnabled={false}
router={{ push, replace }}
{...props}
/>,
{ context: { router: { push, replace } } }
/>
);
wrapper.setState({
loading: false,

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

@@ -35,7 +35,7 @@ jest.mock('../../../../api/components', () => ({

import * as React from 'react';
import { mount } from 'enzyme';
import DefaultPageSelector from '../DefaultPageSelector';
import { DefaultPageSelector } from '../DefaultPageSelector';
import { doAsync } from '../../../../helpers/testUtils';

const get = require('../../../../helpers/storage').get as jest.Mock<any>;
@@ -87,7 +87,10 @@ function mountRender(
replace: any = jest.fn()
) {
return mount(
<DefaultPageSelector currentUser={currentUser} location={{ pathname: '/projects', query }} />,
{ context: { router: { replace } } }
<DefaultPageSelector
currentUser={currentUser}
location={{ pathname: '/projects', query }}
router={{ replace }}
/>
);
}

+ 6
- 7
server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx View File

@@ -18,28 +18,25 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { copyQualityGate } from '../../../api/quality-gates';
import ConfirmModal from '../../../components/controls/ConfirmModal';
import { translate } from '../../../helpers/l10n';
import { getQualityGateUrl } from '../../../helpers/urls';
import { withRouter, Router } from '../../../components/hoc/withRouter';

interface Props {
onClose: () => void;
onCopy: () => Promise<void>;
organization?: string;
qualityGate: T.QualityGate;
router: Pick<Router, 'push'>;
}

interface State {
name: string;
}

export default class CopyQualityGateForm extends React.PureComponent<Props, State> {
static contextTypes = {
router: PropTypes.object
};

class CopyQualityGateForm extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = { name: props.qualityGate.name };
@@ -59,7 +56,7 @@ export default class CopyQualityGateForm extends React.PureComponent<Props, Stat

return copyQualityGate({ id: qualityGate.id, name, organization }).then(qualityGate => {
this.props.onCopy();
this.context.router.push(getQualityGateUrl(String(qualityGate.id), this.props.organization));
this.props.router.push(getQualityGateUrl(String(qualityGate.id), this.props.organization));
});
};

@@ -95,3 +92,5 @@ export default class CopyQualityGateForm extends React.PureComponent<Props, Stat
);
}
}

export default withRouter(CopyQualityGateForm);

+ 7
- 8
server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx View File

@@ -18,28 +18,25 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { createQualityGate } from '../../../api/quality-gates';
import ConfirmModal from '../../../components/controls/ConfirmModal';
import { translate } from '../../../helpers/l10n';
import { getQualityGateUrl } from '../../../helpers/urls';
import { withRouter, Router } from '../../../components/hoc/withRouter';

interface Props {
onClose: () => void;
onCreate: () => Promise<void>;
organization?: string;
router: Pick<Router, 'push'>;
}

interface State {
name: string;
}

export default class CreateQualityGateForm extends React.PureComponent<Props, State> {
static contextTypes = {
router: PropTypes.object
};

state = { name: '' };
class CreateQualityGateForm extends React.PureComponent<Props, State> {
state: State = { name: '' };

handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ name: event.currentTarget.value });
@@ -58,7 +55,7 @@ export default class CreateQualityGateForm extends React.PureComponent<Props, St
return this.props.onCreate().then(() => qualityGate);
})
.then(qualityGate => {
this.context.router.push(getQualityGateUrl(String(qualityGate.id), organization));
this.props.router.push(getQualityGateUrl(String(qualityGate.id), organization));
});
};

@@ -91,3 +88,5 @@ export default class CreateQualityGateForm extends React.PureComponent<Props, St
);
}
}

export default withRouter(CreateQualityGateForm);

+ 6
- 7
server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx View File

@@ -18,30 +18,27 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { deleteQualityGate } from '../../../api/quality-gates';
import ConfirmButton from '../../../components/controls/ConfirmButton';
import { Button } from '../../../components/ui/buttons';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getQualityGatesUrl } from '../../../helpers/urls';
import { withRouter, Router } from '../../../components/hoc/withRouter';

interface Props {
onDelete: () => Promise<void>;
organization?: string;
qualityGate: T.QualityGate;
router: Pick<Router, 'push'>;
}

export default class DeleteQualityGateForm extends React.PureComponent<Props> {
static contextTypes = {
router: PropTypes.object
};

class DeleteQualityGateForm extends React.PureComponent<Props> {
onDelete = () => {
const { organization, qualityGate } = this.props;
return deleteQualityGate({ id: qualityGate.id, organization })
.then(this.props.onDelete)
.then(() => {
this.context.router.push(getQualityGatesUrl(organization));
this.props.router.push(getQualityGatesUrl(organization));
});
};

@@ -70,3 +67,5 @@ export default class DeleteQualityGateForm extends React.PureComponent<Props> {
);
}
}

export default withRouter(DeleteQualityGateForm);

+ 8
- 11
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsApp.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 { withRouter, WithRouterProps } from 'react-router';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import DetailsHeader from './DetailsHeader';
@@ -44,7 +44,7 @@ interface DispatchToProps {
fetchMetrics: () => void;
}

type Props = StateToProps & DispatchToProps & OwnProps;
type Props = StateToProps & DispatchToProps & OwnProps & WithRouterProps;

interface State {
loading: boolean;
@@ -53,11 +53,6 @@ interface State {

export class DetailsApp extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
router: PropTypes.object.isRequired
};

state: State = { loading: true };

componentDidMount() {
@@ -173,7 +168,9 @@ const mapStateToProps = (state: Store): StateToProps => ({
metrics: getMetrics(state)
});

export default connect(
mapStateToProps,
mapDispatchToProps
)(DetailsApp);
export default withRouter(
connect(
mapStateToProps,
mapDispatchToProps
)(DetailsApp)
);

+ 6
- 9
server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.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 { withRouter, WithRouterProps } from 'react-router';
import Helmet from 'react-helmet';
import ListHeader from './ListHeader';
import List from './List';
@@ -30,7 +30,7 @@ import { getQualityGateUrl } from '../../../helpers/urls';
import '../../../components/search-navigator.css';
import '../styles.css';

interface Props {
interface Props extends WithRouterProps {
children: React.ReactElement<{
organization?: string;
refreshQualityGates: () => Promise<void>;
@@ -44,13 +44,8 @@ interface State {
qualityGates: T.QualityGate[];
}

export default class QualityGatesApp extends React.PureComponent<Props, State> {
class QualityGatesApp extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
router: PropTypes.object.isRequired
};

state: State = { canCreate: false, loading: true, qualityGates: [] };

componentDidMount() {
@@ -87,7 +82,7 @@ export default class QualityGatesApp extends React.PureComponent<Props, State> {
this.setState({ canCreate: actions.create, loading: false, qualityGates });

if (qualityGates && qualityGates.length === 1 && !actions.create) {
this.context.router.replace(
this.props.router.replace(
getQualityGateUrl(String(qualityGates[0].id), organization && organization.key)
);
}
@@ -156,3 +151,5 @@ export default class QualityGatesApp extends React.PureComponent<Props, State> {
);
}
}

export default withRouter(QualityGatesApp);

+ 8
- 19
server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.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 { withRouter, WithRouterProps } from 'react-router';
import Changelog from './Changelog';
import ChangelogSearch from './ChangelogSearch';
import ChangelogEmpty from './ChangelogEmpty';
@@ -28,13 +28,7 @@ import { getProfileChangelogPath } from '../utils';
import { Profile, ProfileChangelogEvent } from '../types';
import { parseDate, toShortNotSoISOString } from '../../../helpers/dates';

interface Props {
location: {
query: {
since?: string;
to?: string;
};
};
interface Props extends WithRouterProps {
organization: string | null;
profile: Profile;
}
@@ -46,16 +40,9 @@ interface State {
total?: number;
}

export default class ChangelogContainer extends React.PureComponent<Props, State> {
class ChangelogContainer extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
router: PropTypes.object
};

state: State = {
loading: true
};
state: State = { loading: true };

componentDidMount() {
this.mounted = true;
@@ -136,7 +123,7 @@ export default class ChangelogContainer extends React.PureComponent<Props, State
to: to && toShortNotSoISOString(to)
}
);
this.context.router.push(path);
this.props.router.push(path);
};

handleReset = () => {
@@ -145,7 +132,7 @@ export default class ChangelogContainer extends React.PureComponent<Props, State
this.props.profile.language,
this.props.organization
);
this.context.router.push(path);
this.props.router.push(path);
};

render() {
@@ -189,3 +176,5 @@ export default class ChangelogContainer extends React.PureComponent<Props, State
);
}
}

export default withRouter(ChangelogContainer);

+ 7
- 14
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.tsx View File

@@ -18,15 +18,14 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { withRouter, WithRouterProps } from 'react-router';
import ComparisonForm from './ComparisonForm';
import ComparisonResults from './ComparisonResults';
import { compareProfiles } from '../../../api/quality-profiles';
import { getProfileComparePath } from '../utils';
import { Profile } from '../types';

interface Props {
location: { query: { withKey?: string } };
interface Props extends WithRouterProps {
organization: string | null;
profile: Profile;
profiles: Profile[];
@@ -48,17 +47,9 @@ interface State {
}>;
}

export default class ComparisonContainer extends React.PureComponent<Props, State> {
class ComparisonContainer extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
router: PropTypes.object
};

constructor(props: Props) {
super(props);
this.state = { loading: false };
}
state: State = { loading: false };

componentDidMount() {
this.mounted = true;
@@ -104,7 +95,7 @@ export default class ComparisonContainer extends React.PureComponent<Props, Stat
this.props.organization,
withKey
);
this.context.router.push(path);
this.props.router.push(path);
};

render() {
@@ -145,3 +136,5 @@ export default class ComparisonContainer extends React.PureComponent<Props, Stat
);
}
}

export default withRouter(ComparisonContainer);

+ 12
- 16
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.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 RenameProfileForm from './RenameProfileForm';
import CopyProfileForm from './CopyProfileForm';
import DeleteProfileForm from './DeleteProfileForm';
@@ -31,12 +30,14 @@ import ActionsDropdown, {
ActionsDropdownItem,
ActionsDropdownDivider
} from '../../../components/controls/ActionsDropdown';
import { withRouter, Router } from '../../../components/hoc/withRouter';

interface Props {
className?: string;
fromList?: boolean;
organization: string | null;
profile: Profile;
router: Pick<Router, 'push' | 'replace'>;
updateProfiles: () => Promise<void>;
}

@@ -46,20 +47,13 @@ interface State {
renameFormOpen: boolean;
}

export default class ProfileActions extends React.PureComponent<Props, State> {
static contextTypes = {
router: PropTypes.object
export class ProfileActions extends React.PureComponent<Props, State> {
state: State = {
copyFormOpen: false,
deleteFormOpen: false,
renameFormOpen: false
};

constructor(props: Props) {
super(props);
this.state = {
copyFormOpen: false,
deleteFormOpen: false,
renameFormOpen: false
};
}

handleRenameClick = () => {
this.setState({ renameFormOpen: true });
};
@@ -69,7 +63,7 @@ export default class ProfileActions extends React.PureComponent<Props, State> {
this.props.updateProfiles().then(
() => {
if (!this.props.fromList) {
this.context.router.replace(
this.props.router.replace(
getProfilePath(name, this.props.profile.language, this.props.organization)
);
}
@@ -90,7 +84,7 @@ export default class ProfileActions extends React.PureComponent<Props, State> {
this.closeCopyForm();
this.props.updateProfiles().then(
() => {
this.context.router.push(
this.props.router.push(
getProfilePath(name, this.props.profile.language, this.props.organization)
);
},
@@ -111,7 +105,7 @@ export default class ProfileActions extends React.PureComponent<Props, State> {
};

handleProfileDelete = () => {
this.context.router.replace(getProfilesPath(this.props.organization));
this.props.router.replace(getProfilesPath(this.props.organization));
this.props.updateProfiles();
};

@@ -220,3 +214,5 @@ export default class ProfileActions extends React.PureComponent<Props, State> {
);
}
}

export default withRouter(ProfileActions);

+ 13
- 4
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import ProfileActions from '../ProfileActions';
import { ProfileActions } from '../ProfileActions';
import { click, waitAndUpdate } from '../../../../helpers/testUtils';

const PROFILE = {
@@ -40,7 +40,14 @@ const PROFILE = {

it('renders with no permissions', () => {
expect(
shallow(<ProfileActions organization="org" profile={PROFILE} updateProfiles={jest.fn()} />)
shallow(
<ProfileActions
organization="org"
profile={PROFILE}
router={{ push: jest.fn(), replace: jest.fn() }}
updateProfiles={jest.fn()}
/>
)
).toMatchSnapshot();
});

@@ -50,6 +57,7 @@ it('renders with permission to edit only', () => {
<ProfileActions
organization="org"
profile={{ ...PROFILE, actions: { edit: true } }}
router={{ push: jest.fn(), replace: jest.fn() }}
updateProfiles={jest.fn()}
/>
)
@@ -71,6 +79,7 @@ it('renders with all permissions', () => {
associateProjects: true
}
}}
router={{ push: jest.fn(), replace: jest.fn() }}
updateProfiles={jest.fn()}
/>
)
@@ -84,9 +93,9 @@ it('should copy profile', async () => {
<ProfileActions
organization="org"
profile={{ ...PROFILE, actions: { copy: true } }}
router={{ push, replace: jest.fn() }}
updateProfiles={updateProfiles}
/>,
{ context: { router: { push } } }
/>
);

click(wrapper.find('[id="quality-profile-copy"]'));

+ 7
- 8
server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.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 { Link } from 'react-router';
import CreateProfileForm from './CreateProfileForm';
import RestoreProfileForm from './RestoreProfileForm';
@@ -27,11 +26,13 @@ import { getProfilePath } from '../utils';
import { Actions } from '../../../api/quality-profiles';
import { Button } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';
import { withRouter, Router } from '../../../components/hoc/withRouter';

interface Props {
actions: Actions;
languages: Array<{ key: string; name: string }>;
organization: string | null;
router: Pick<Router, 'push'>;
updateProfiles: () => Promise<void>;
}

@@ -40,12 +41,8 @@ interface State {
restoreFormOpen: boolean;
}

export default class PageHeader extends React.PureComponent<Props, State> {
static contextTypes = {
router: PropTypes.object
};

state = {
class PageHeader extends React.PureComponent<Props, State> {
state: State = {
createFormOpen: false,
restoreFormOpen: false
};
@@ -57,7 +54,7 @@ export default class PageHeader extends React.PureComponent<Props, State> {
handleCreate = (profile: Profile) => {
this.props.updateProfiles().then(
() => {
this.context.router.push(
this.props.router.push(
getProfilePath(profile.name, profile.language, this.props.organization)
);
},
@@ -130,3 +127,5 @@ export default class PageHeader extends React.PureComponent<Props, State> {
);
}
}

export default withRouter(PageHeader);

+ 7
- 10
server/sonar-web/src/main/js/apps/securityReports/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 Helmet from 'react-helmet';
import { Link } from 'react-router';
import VulnerabilityList from './VulnerabilityList';
@@ -26,20 +25,21 @@ import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { translate } from '../../../helpers/l10n';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import Checkbox from '../../../components/controls/Checkbox';
import { RawQuery } from '../../../helpers/query';
import NotFound from '../../../app/components/NotFound';
import { getSecurityHotspots } from '../../../api/security-reports';
import { isLongLivingBranch } from '../../../helpers/branches';
import DocTooltip from '../../../components/docs/DocTooltip';
import { StandardType } from '../utils';
import { Alert } from '../../../components/ui/Alert';
import { withRouter, Location, Router } from '../../../components/hoc/withRouter';
import '../style.css';

interface Props {
branchLike?: T.BranchLike;
component: T.Component;
location: { pathname: string; query: RawQuery };
location: Pick<Location, 'pathname' | 'query'>;
params: { type: string };
router: Pick<Router, 'push'>;
}

interface State {
@@ -50,13 +50,9 @@ interface State {
showCWE: boolean;
}

export default class App extends React.PureComponent<Props, State> {
export class App extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
router: PropTypes.object.isRequired
};

constructor(props: Props) {
super(props);
this.state = {
@@ -115,8 +111,7 @@ export default class App extends React.PureComponent<Props, State> {
};

handleCheck = (checked: boolean) => {
const { router } = this.context;
router.push({
this.props.router.push({
pathname: this.props.location.pathname,
query: { id: this.props.component.key, showCWE: checked }
});
@@ -194,3 +189,5 @@ export default class App extends React.PureComponent<Props, State> {
);
}
}

export default withRouter(App);

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

@@ -78,7 +78,7 @@ jest.mock('../../../../api/security-reports', () => ({

import * as React from 'react';
import { shallow } from 'enzyme';
import App from '../App';
import { App } from '../App';
import { waitAndUpdate } from '../../../../helpers/testUtils';

const getSecurityHotspots = require('../../../../api/security-reports')
@@ -97,16 +97,32 @@ beforeEach(() => {
});

it('renders error on wrong type parameters', () => {
const wrapper = shallow(<App component={component} location={location} params={wrongParams} />, {
context
});
const wrapper = shallow(
<App
component={component}
location={location}
params={wrongParams}
router={{ push: jest.fn() }}
/>,
{
context
}
);
expect(wrapper).toMatchSnapshot();
});

it('renders owaspTop10', async () => {
const wrapper = shallow(<App component={component} location={location} params={owaspParams} />, {
context
});
const wrapper = shallow(
<App
component={component}
location={location}
params={owaspParams}
router={{ push: jest.fn() }}
/>,
{
context
}
);
await waitAndUpdate(wrapper);
expect(getSecurityHotspots).toBeCalledWith({
project: 'foo',
@@ -119,7 +135,12 @@ it('renders owaspTop10', async () => {

it('renders with cwe', () => {
const wrapper = shallow(
<App component={component} location={locationWithCWE} params={owaspParams} />,
<App
component={component}
location={locationWithCWE}
params={owaspParams}
router={{ push: jest.fn() }}
/>,
{ context }
);
expect(getSecurityHotspots).toBeCalledWith({
@@ -132,9 +153,17 @@ it('renders with cwe', () => {
});

it('handle checkbox for cwe display', async () => {
const wrapper = shallow(<App component={component} location={location} params={owaspParams} />, {
context
});
const wrapper = shallow(
<App
component={component}
location={location}
params={owaspParams}
router={{ push: jest.fn() }}
/>,
{
context
}
);
expect(getSecurityHotspots).toBeCalledWith({
project: 'foo',
standard: 'owaspTop10',
@@ -156,9 +185,17 @@ it('handle checkbox for cwe display', async () => {
});

it('renders sansTop25', () => {
const wrapper = shallow(<App component={component} location={location} params={sansParams} />, {
context
});
const wrapper = shallow(
<App
component={component}
location={location}
params={sansParams}
router={{ push: jest.fn() }}
/>,
{
context
}
);
expect(getSecurityHotspots).toBeCalledWith({
project: 'foo',
standard: 'sansTop25',

+ 7
- 16
server/sonar-web/src/main/js/apps/system/components/App.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 { withRouter, WithRouterProps } from 'react-router';
import Helmet from 'react-helmet';
import ClusterSysInfos from './ClusterSysInfos';
import PageHeader from './PageHeader';
@@ -35,29 +35,18 @@ import {
Query,
serializeQuery
} from '../utils';
import { RawQuery } from '../../../helpers/query';
import '../styles.css';

interface Props {
location: { pathname: string; query: RawQuery };
}
type Props = WithRouterProps;

interface State {
loading: boolean;
sysInfoData?: SysInfo;
}

export default class App extends React.PureComponent<Props, State> {
class App extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
router: PropTypes.object
};

constructor(props: Props) {
super(props);
this.state = { loading: true };
}
state: State = { loading: true };

componentDidMount() {
this.mounted = true;
@@ -97,7 +86,7 @@ export default class App extends React.PureComponent<Props, State> {

updateQuery = (newQuery: Query) => {
const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery });
this.context.router.replace({ pathname: this.props.location.pathname, query });
this.props.router.replace({ pathname: this.props.location.pathname, query });
};

renderSysInfo() {
@@ -145,3 +134,5 @@ export default class App extends React.PureComponent<Props, State> {
);
}
}

export default withRouter(App);

+ 4
- 6
server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboarding.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 Helmet from 'react-helmet';
import { connect } from 'react-redux';
import ProjectWatcher from './ProjectWatcher';
@@ -32,11 +31,13 @@ import { getProjectUrl } from '../../../helpers/urls';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { isSonarCloud } from '../../../helpers/system';
import { isLoggedIn } from '../../../helpers/users';
import { withRouter, Router } from '../../../components/hoc/withRouter';
import '../styles.css';

interface OwnProps {
automatic?: boolean;
onFinish: () => void;
router: Pick<Router, 'push'>;
}

interface StateProps {
@@ -56,9 +57,6 @@ interface State {

export class ProjectOnboarding extends React.PureComponent<Props, State> {
mounted = false;
static contextTypes = {
router: PropTypes.object
};

constructor(props: Props) {
super(props);
@@ -93,7 +91,7 @@ export class ProjectOnboarding extends React.PureComponent<Props, State> {
finishOnboarding = () => {
this.props.onFinish();
if (this.state.projectKey) {
this.context.router.push(getProjectUrl(this.state.projectKey));
this.props.router.push(getProjectUrl(this.state.projectKey));
}
};

@@ -203,4 +201,4 @@ const mapStateToProps = (state: Store): StateProps => {
};
};

export default connect(mapStateToProps)(ProjectOnboarding);
export default withRouter(connect(mapStateToProps)(ProjectOnboarding));

+ 10
- 10
server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboardingPage.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 { withRouter, WithRouterProps } from 'react-router';
import { connect } from 'react-redux';
import ProjectOnboardingModal from './ProjectOnboardingModal';
import { skipOnboarding } from '../../../store/users';
@@ -27,14 +27,12 @@ interface DispatchProps {
skipOnboarding: () => void;
}

export class ProjectOnboardingPage extends React.PureComponent<DispatchProps> {
static contextTypes = {
router: PropTypes.object.isRequired
};
type Props = DispatchProps & WithRouterProps;

export class ProjectOnboardingPage extends React.PureComponent<Props> {
onSkipOnboardingTutorial = () => {
this.props.skipOnboarding();
this.context.router.replace('/');
this.props.router.replace('/');
};

render() {
@@ -44,7 +42,9 @@ export class ProjectOnboardingPage extends React.PureComponent<DispatchProps> {

const mapDispatchToProps: DispatchProps = { skipOnboarding };

export default connect(
null,
mapDispatchToProps
)(ProjectOnboardingPage);
export default withRouter(
connect(
null,
mapDispatchToProps
)(ProjectOnboardingPage)
);

+ 13
- 2
server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/ProjectOnboarding-test.tsx View File

@@ -42,6 +42,7 @@ it('guides for on-premise', () => {
currentUser={currentUser}
onFinish={jest.fn()}
organizationsEnabled={false}
router={{ push: jest.fn() }}
/>
);
expect(wrapper).toMatchSnapshot();
@@ -55,7 +56,12 @@ it('guides for sonarcloud', () => {
(getInstance as jest.Mock<any>).mockImplementation(() => 'SonarCloud');
(isSonarCloud as jest.Mock<any>).mockImplementation(() => true);
const wrapper = shallow(
<ProjectOnboarding currentUser={currentUser} onFinish={jest.fn()} organizationsEnabled={true} />
<ProjectOnboarding
currentUser={currentUser}
onFinish={jest.fn()}
organizationsEnabled={true}
router={{ push: jest.fn() }}
/>
);
expect(wrapper).toMatchSnapshot();

@@ -73,7 +79,12 @@ it('finishes', () => {
(isSonarCloud as jest.Mock<any>).mockImplementation(() => false);
const onFinish = jest.fn();
const wrapper = shallow(
<ProjectOnboarding currentUser={currentUser} onFinish={onFinish} organizationsEnabled={false} />
<ProjectOnboarding
currentUser={currentUser}
onFinish={onFinish}
organizationsEnabled={false}
router={{ push: jest.fn() }}
/>
);
click(wrapper.find('ResetButtonLink'));
return doAsync(() => {

+ 7
- 10
server/sonar-web/src/main/js/apps/users/UsersApp.tsx View File

@@ -18,9 +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 Helmet from 'react-helmet';
import { Location } from 'history';
import Header from './Header';
import Search from './Search';
import UsersList from './UsersList';
@@ -29,11 +27,13 @@ import ListFooter from '../../components/controls/ListFooter';
import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import { getIdentityProviders, searchUsers } from '../../api/users';
import { translate } from '../../helpers/l10n';
import { withRouter, Location, Router } from '../../components/hoc/withRouter';

interface Props {
currentUser: { isLoggedIn: boolean; login?: string };
location: Location;
location: Pick<Location, 'query'>;
organizationsEnabled?: boolean;
router: Pick<Router, 'push'>;
}

interface State {
@@ -43,13 +43,8 @@ interface State {
users: T.User[];
}

export default class UsersApp extends React.PureComponent<Props, State> {
export class UsersApp extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
router: PropTypes.object.isRequired
};

state: State = { identityProviders: [], loading: true, users: [] };

componentDidMount() {
@@ -110,7 +105,7 @@ export default class UsersApp extends React.PureComponent<Props, State> {

updateQuery = (newQuery: Partial<Query>) => {
const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery });
this.context.router.push({ ...this.props.location, query });
this.props.router.push({ ...this.props.location, query });
};

updateTokensCount = (login: string, tokensCount: number) => {
@@ -148,3 +143,5 @@ export default class UsersApp extends React.PureComponent<Props, State> {
);
}
}

export default withRouter(UsersApp);

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

@@ -19,10 +19,10 @@
*/
/* eslint-disable import/order */
import * as React from 'react';
import { Location } from 'history';
import { shallow } from 'enzyme';
import UsersApp from '../UsersApp';
import { UsersApp } from '../UsersApp';
import { waitAndUpdate } from '../../../helpers/testUtils';
import { Location } from '../../../components/hoc/withRouter';

jest.mock('../../../api/users', () => ({
getIdentityProviders: jest.fn(() =>
@@ -77,12 +77,13 @@ it('should render correctly', async () => {
expect(wrapper).toMatchSnapshot();
});

function getWrapper(props = {}) {
function getWrapper(props: Partial<UsersApp['props']> = {}) {
return shallow(
<UsersApp
currentUser={currentUser}
location={location}
organizationsEnabled={true}
router={{ push: jest.fn() }}
{...props}
/>,
{

+ 8
- 20
server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx View File

@@ -18,9 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
import Helmet from 'react-helmet';
import { Link } from 'react-router';
import { Link, withRouter, WithRouterProps } from 'react-router';
import Menu from './Menu';
import Search from './Search';
import Domain from './Domain';
@@ -30,29 +29,17 @@ import { getActionKey, isDomainPathActive, Query, serializeQuery, parseQuery } f
import { scrollToElement } from '../../../helpers/scrolling';
import { translate } from '../../../helpers/l10n';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { RawQuery } from '../../../helpers/query';
import '../styles/web-api.css';

interface Props {
location: { pathname: string; query: RawQuery };
params: { splat?: string };
}
type Props = WithRouterProps;

interface State {
domains: DomainType[];
}

export default class WebApiApp extends React.PureComponent<Props, State> {
class WebApiApp extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
router: PropTypes.object.isRequired
};

constructor(props: Props) {
super(props);
this.state = { domains: [] };
}
state: State = { domains: [] };

componentDidMount() {
this.mounted = true;
@@ -99,7 +86,7 @@ export default class WebApiApp extends React.PureComponent<Props, State> {

updateQuery = (newQuery: Partial<Query>) => {
const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery });
this.context.router.push({ pathname: this.props.location.pathname, query });
this.props.router.push({ pathname: this.props.location.pathname, query });
};

toggleInternalInitially() {
@@ -127,14 +114,13 @@ export default class WebApiApp extends React.PureComponent<Props, State> {

handleToggleInternal = () => {
const splat = this.props.params.splat || '';
const { router } = this.context;
const { domains } = this.state;
const domain = domains.find(domain => isDomainPathActive(domain.path, splat));
const query = parseQuery(this.props.location.query);
const internal = !query.internal;

if (domain && domain.internal && !internal) {
router.push({
this.props.router.push({
pathname: '/web_api',
query: { ...serializeQuery(query), internal: false }
});
@@ -194,3 +180,5 @@ export default class WebApiApp extends React.PureComponent<Props, State> {
);
}
}

export default withRouter(WebApiApp);

+ 7
- 7
server/sonar-web/src/main/js/components/docs/DocLink.tsx View File

@@ -21,8 +21,10 @@ import * as React from 'react';
import { Link } from 'react-router';
import DetachIcon from '../icons-components/DetachIcon';
import { isSonarCloud } from '../../helpers/system';
import { withAppState } from '../withAppState';

interface OwnProps {
appState: Pick<T.AppState, 'canAdmin'>;
customProps?: {
[k: string]: any;
};
@@ -34,11 +36,7 @@ const SONARCLOUD_LINK = '/#sonarcloud#/';
const SONARQUBE_LINK = '/#sonarqube#/';
const SONARQUBE_ADMIN_LINK = '/#sonarqube-admin#/';

export default class DocLink extends React.PureComponent<Props> {
static contextTypes = {
canAdmin: () => null
};

export class DocLink extends React.PureComponent<Props> {
handleClickOnAnchor = (event: React.MouseEvent<HTMLAnchorElement>) => {
const { customProps, href = '#' } = this.props;
if (customProps && customProps.onAnchorClick) {
@@ -63,7 +61,7 @@ export default class DocLink extends React.PureComponent<Props> {
return <SonarQubeLink url={href}>{children}</SonarQubeLink>;
} else if (href.startsWith(SONARQUBE_ADMIN_LINK)) {
return (
<SonarQubeAdminLink canAdmin={this.context.canAdmin} url={href}>
<SonarQubeAdminLink canAdmin={this.props.appState.canAdmin} url={href}>
{children}
</SonarQubeAdminLink>
);
@@ -91,6 +89,8 @@ export default class DocLink extends React.PureComponent<Props> {
}
}

export default withAppState(DocLink);

interface SonarCloudLinkProps {
children: React.ReactNode;
url: string;
@@ -124,7 +124,7 @@ function SonarQubeLink({ children, url }: SonarQubeLinkProps) {
}

interface SonarQubeAdminLinkProps {
canAdmin: boolean;
canAdmin?: boolean;
children: React.ReactNode;
url: string;
}

+ 57
- 15
server/sonar-web/src/main/js/components/docs/__tests__/DocLink-test.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import DocLink from '../DocLink';
import { DocLink } from '../DocLink';
import { isSonarCloud } from '../../../helpers/system';

jest.mock('../../../helpers/system', () => ({
@@ -27,59 +27,101 @@ jest.mock('../../../helpers/system', () => ({
}));

it('should render simple link', () => {
expect(shallow(<DocLink href="http://sample.com">link text</DocLink>)).toMatchSnapshot();
expect(
shallow(
<DocLink appState={{ canAdmin: false }} href="http://sample.com">
link text
</DocLink>
)
).toMatchSnapshot();
});

it('should render documentation link', () => {
expect(shallow(<DocLink href="/foo/bar">link text</DocLink>)).toMatchSnapshot();
expect(
shallow(
<DocLink appState={{ canAdmin: false }} href="/foo/bar">
link text
</DocLink>
)
).toMatchSnapshot();
});

it('should render sonarcloud link on sonarcloud', () => {
(isSonarCloud as jest.Mock).mockImplementationOnce(() => true);
const wrapper = shallow(<DocLink href="/#sonarcloud#/foo/bar">link text</DocLink>);
const wrapper = shallow(
<DocLink appState={{ canAdmin: false }} href="/#sonarcloud#/foo/bar">
link text
</DocLink>
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('SonarCloudLink').dive()).toMatchSnapshot();
});

it('should not render sonarcloud link on sonarcloud', () => {
(isSonarCloud as jest.Mock).mockImplementationOnce(() => false);
const wrapper = shallow(<DocLink href="/#sonarcloud#/foo/bar">link text</DocLink>);
const wrapper = shallow(
<DocLink appState={{ canAdmin: false }} href="/#sonarcloud#/foo/bar">
link text
</DocLink>
);
expect(wrapper.find('SonarCloudLink').dive()).toMatchSnapshot();
});

it('should render sonarqube link on sonarqube', () => {
const wrapper = shallow(<DocLink href="/#sonarqube#/foo/bar">link text</DocLink>);
const wrapper = shallow(
<DocLink appState={{ canAdmin: false }} href="/#sonarqube#/foo/bar">
link text
</DocLink>
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('SonarQubeLink').dive()).toMatchSnapshot();
});

it('should not render sonarqube link on sonarcloud', () => {
(isSonarCloud as jest.Mock).mockImplementationOnce(() => true);
const wrapper = shallow(<DocLink href="/#sonarqube#/foo/bar">link text</DocLink>);
const wrapper = shallow(
<DocLink appState={{ canAdmin: false }} href="/#sonarqube#/foo/bar">
link text
</DocLink>
);
expect(wrapper.find('SonarQubeLink').dive()).toMatchSnapshot();
});

it('should render sonarqube admin link on sonarqube for admin', () => {
const wrapper = shallow(<DocLink href="/#sonarqube-admin#/foo/bar">link text</DocLink>, {
context: { canAdmin: true }
});
const wrapper = shallow(
<DocLink appState={{ canAdmin: true }} href="/#sonarqube-admin#/foo/bar">
link text
</DocLink>
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('SonarQubeAdminLink').dive()).toMatchSnapshot();
});

it('should not render sonarqube admin link on sonarqube for non-admin', () => {
const wrapper = shallow(<DocLink href="/#sonarqube-admin#/foo/bar">link text</DocLink>);
const wrapper = shallow(
<DocLink appState={{ canAdmin: false }} href="/#sonarqube-admin#/foo/bar">
link text
</DocLink>
);
expect(wrapper.find('SonarQubeAdminLink').dive()).toMatchSnapshot();
});

it('should not render sonarqube admin link on sonarcloud', () => {
(isSonarCloud as jest.Mock).mockImplementationOnce(() => true);
const wrapper = shallow(<DocLink href="/#sonarqube-admin#/foo/bar">link text</DocLink>, {
context: { canAdmin: true }
});
const wrapper = shallow(
<DocLink appState={{ canAdmin: true }} href="/#sonarqube-admin#/foo/bar">
link text
</DocLink>
);
expect(wrapper.find('SonarQubeAdminLink').dive()).toMatchSnapshot();
});

it.skip('should render documentation anchor', () => {
expect(shallow(<DocLink href="#quality-profiles">link text</DocLink>)).toMatchSnapshot();
expect(
shallow(
<DocLink appState={{ canAdmin: false }} href="#quality-profiles">
link text
</DocLink>
)
).toMatchSnapshot();
});

+ 10
- 0
server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocLink-test.tsx.snap View File

@@ -26,6 +26,11 @@ exports[`should not render sonarqube link on sonarcloud 1`] = `

exports[`should render documentation link 1`] = `
<Link
appState={
Object {
"canAdmin": false,
}
}
onlyActiveOnIndex={false}
style={Object {}}
to="/documentation/foo/bar"
@@ -37,6 +42,11 @@ exports[`should render documentation link 1`] = `
exports[`should render simple link 1`] = `
<Fragment>
<a
appState={
Object {
"canAdmin": false,
}
}
href="http://sample.com"
rel="noopener noreferrer"
target="_blank"

server/sonar-web/src/main/js/app/components/extensions/ExtensionContainer.tsx → server/sonar-web/src/main/js/components/hoc/withRouter.tsx View File

@@ -17,18 +17,19 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { connect } from 'react-redux';
import Extension from './Extension';
import { getCurrentUser, Store } from '../../../store/rootReducer';
import { addGlobalErrorMessage } from '../../../store/globalMessages';
import * as React from 'react';
import { withRouter as originalWithRouter, WithRouterProps } from 'react-router';

const mapStateToProps = (state: Store) => ({
currentUser: getCurrentUser(state)
});
export type Location = WithRouterProps['location'];
export type Router = WithRouterProps['router'];

const mapDispatchToProps = { onFail: addGlobalErrorMessage };
interface InjectedProps {
location?: Partial<Location>;
router?: Partial<Router>;
}

export default connect(
mapStateToProps,
mapDispatchToProps
)(Extension);
export function withRouter<P extends InjectedProps, S>(
WrappedComponent: React.ComponentClass<P & InjectedProps>
): React.ComponentClass<T.Omit<P, keyof InjectedProps>, S> {
return originalWithRouter(WrappedComponent as any);
}

+ 6
- 7
server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.tsx View File

@@ -19,7 +19,6 @@
*/
import * as React from 'react';
import { minBy } from 'lodash';
import * as PropTypes from 'prop-types';
import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
import PreviewGraphTooltips from './PreviewGraphTooltips';
import AdvancedTimeline from '../charts/AdvancedTimeline';
@@ -37,6 +36,7 @@ import {
import { get } from '../../helpers/storage';
import { formatMeasure, getShortType } from '../../helpers/measures';
import { getBranchLikeQuery } from '../../helpers/branches';
import { withRouter, Router } from '../hoc/withRouter';

interface History {
[x: string]: Array<{ date: Date; value?: string }>;
@@ -48,6 +48,7 @@ interface Props {
metrics: { [key: string]: T.Metric };
project: string;
renderWhenEmpty?: () => React.ReactNode;
router: Pick<Router, 'push'>;
}

interface State {
@@ -63,11 +64,7 @@ const GRAPH_PADDING = [4, 0, 4, 0];
const MAX_GRAPH_NB = 1;
const MAX_SERIES_PER_GRAPH = 3;

export default class PreviewGraph extends React.PureComponent<Props, State> {
static contextTypes = {
router: PropTypes.object
};

class PreviewGraph extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
@@ -140,7 +137,7 @@ export default class PreviewGraph extends React.PureComponent<Props, State> {
};

handleClick = () => {
this.context.router.push({
this.props.router.push({
pathname: '/project/activity',
query: { id: this.props.project, ...getBranchLikeQuery(this.props.branchLike) }
});
@@ -202,3 +199,5 @@ export default class PreviewGraph extends React.PureComponent<Props, State> {
);
}
}

export default withRouter(PreviewGraph);

Loading…
Cancel
Save