]> source.dussan.org Git - sonarqube.git/commitdiff
remove some usages of legacy react context
authorStas Vilchik <stas.vilchik@sonarsource.com>
Thu, 6 Dec 2018 14:53:55 +0000 (15:53 +0100)
committerSonarTech <sonartech@sonarsource.com>
Tue, 11 Dec 2018 19:20:58 +0000 (20:20 +0100)
84 files changed:
server/sonar-web/src/main/js/app/components/AdminContainer.tsx
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/app/components/Landing.tsx
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx
server/sonar-web/src/main/js/app/components/embed-docs-modal/Suggestions.tsx
server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsContext.ts
server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx
server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx
server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx
server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap
server/sonar-web/src/main/js/app/components/extensions/Extension.tsx
server/sonar-web/src/main/js/app/components/extensions/ExtensionContainer.tsx [deleted file]
server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx
server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx
server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.tsx
server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx
server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap
server/sonar-web/src/main/js/app/types.d.ts
server/sonar-web/src/main/js/apps/account/notifications/Notifications.tsx
server/sonar-web/src/main/js/apps/account/notifications/__tests__/Notifications-test.tsx
server/sonar-web/src/main/js/apps/code/components/App.tsx
server/sonar-web/src/main/js/apps/code/components/Search.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsIssues-test.tsx
server/sonar-web/src/main/js/apps/issues/components/App.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/marketplace/App.tsx
server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.tsx
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.tsx
server/sonar-web/src/main/js/apps/overview/components/App.tsx
server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsApp.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.tsx
server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx
server/sonar-web/src/main/js/apps/securityReports/components/App.tsx
server/sonar-web/src/main/js/apps/securityReports/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/system/components/App.tsx
server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboarding.tsx
server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboardingPage.tsx
server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/ProjectOnboarding-test.tsx
server/sonar-web/src/main/js/apps/users/UsersApp.tsx
server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-test.tsx
server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx
server/sonar-web/src/main/js/components/docs/DocLink.tsx
server/sonar-web/src/main/js/components/docs/__tests__/DocLink-test.tsx
server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocLink-test.tsx.snap
server/sonar-web/src/main/js/components/hoc/withRouter.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.tsx

index 6f65e1d4c2613e2c8e205646a5e0a032f47cc8f2..8890e985cfc466240470cc4f7f692b4505b73d45 100644 (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();
index 0bc2bfa614e372095771d5e963208d2a69bfadfd..a031c4781698790a857052ea5f923a90d6db3f6e 100644 (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);
index 32ee2c1307971842a4314159f4f46cf2b7e048df..f5187c585be05e9e39de97d0201f4560f3d5681b 100644 (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>
   );
 }
index 310d2f27629834da1d4158c1330c8b2c83366a19..c5634b10a71af000ca596dfebe5d2b6910bbd036 100644 (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));
index b661f086095c34cf2fa85b557119eff32290b7de..34d513e1b17aa81ea7efa8109186892bcb1f47dc 100644 (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>
   );
index 5cc79d2d179fbae5b44eb7d64117a79f659ba1fe..b191d8879d93691373ae195203d3ccaf307152d8 100644 (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')}
index 8e2486b50259f67a86534ad229987a3bb2c0c922..d02c95f18d7936db1ffc172d3df89646849ae9d5 100644 (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>
index 7ffd50b40cb508c5ef09e2fce928d3ec2e31fd56..450bc63547c2b50befeae02cc1b35b2f0e2985aa 100644 (file)
  * 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() {
index 8292383c5db63c9acb63fe56e7294b3001fcf46e..c97e2e8a4760c48614279cd6ad63ea21e3e3be01 100644 (file)
  * 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: []
+});
index 909c6a5d1d43c20df77c082545c8050d0852e6d2..1095071b8c3a9650b5fdc94f5664af535d7b9d04 100644 (file)
  * 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>
+    );
   }
 }
index e21c57fb020a93fc0b7f1509032c0791d8a18686..0520bde7738bc52241f2d9e53a16375fce823a81 100644 (file)
 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();
 });
index 2092c80a14db426a2b529f63bddfd25a23c752c8..eee016635fd3a443609de2440cee02422e870e04 100644 (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' }
+  ]);
 });
index 434b4be4f7be85a00afec7627e7eb6a651733465..d02e45c3e85776f6f601495cb4f9b057c021bc1e 100644 (file)
 // 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]}
index b3e6c9343eceb2a68b2e5e481bd1e10a72227644..c02b5962601a1262ea5a3e06a774a298339346fb 100644 (file)
  */
 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)
+  )
+);
diff --git a/server/sonar-web/src/main/js/app/components/extensions/ExtensionContainer.tsx b/server/sonar-web/src/main/js/app/components/extensions/ExtensionContainer.tsx
deleted file mode 100644 (file)
index a3e36e6..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { connect } from 'react-redux';
-import Extension from './Extension';
-import { getCurrentUser, Store } from '../../../store/rootReducer';
-import { addGlobalErrorMessage } from '../../../store/globalMessages';
-
-const mapStateToProps = (state: Store) => ({
-  currentUser: getCurrentUser(state)
-});
-
-const mapDispatchToProps = { onFail: addGlobalErrorMessage };
-
-export default connect(
-  mapStateToProps,
-  mapDispatchToProps
-)(Extension);
index 8db930dd62b6b2af22c116b99887ea81e1ffb886..17119b71b6ca7a7476aba8a3af1ad95afcd3f861 100644 (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) => ({
index b37ac541bea77d42b89919db2bedfd01464581af..e1b967b6c964b6818b5c74d538ffe2c7729defc0 100644 (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) => ({
index 16ae71708ac231d66373cee1ecc1cf6446132d9c..a372d0f1f48c9d4a1dd0ce7deaf990f4f887a14b 100644 (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 }}
       />
index 9502e405b63a93ff7a29d30f91bdaa0c91bae267..7f2e85d30fe67b5ab109a714b5448551619d4b52 100644 (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} />
   );
index ada4b4f3e695c252ca5c31c12730f12c9c73259a..c98752e7a320298e6455b6b5774783fd65ac1beb 100644 (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} />
   );
index 8fee54db550e9e0a7da15e99832b096b616aab7c..ba26c68f3b276ef0c9c8ce273e079cace42a850e 100644 (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);
index 5f876d1a6f6cefcf9fcc148236320619020ef753..ca027fdfa48eb9553887de12236ec9215aa3d818 100644 (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);
index 116cb1489fa3d95f5f62a854af0c261b4db62da4..bcc3ac9f057e0332788947f452dde970a05b0845 100644 (file)
  */
 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);
index 1499680b2a8c7564c37a7e647b6d7044ea78410b..7aeec61ee843c05eec598643933003700eae166b 100644 (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);
index 17c44b1beb9bfac0b6ba3352926ee68bfa97c8a9..083d976a86a6250a512b84f3c7cc6b2cb91d87e3 100644 (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();
 });
index 8f35a1c252fb39125ad8598b1f7d746480399945..1410ea5f74ff5a3c551e906ea5e5065f5911c491 100644 (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);
index 8528a2701f71dc8b34ede02c9ace6317367ccdf9..6c90f7a9c8dcf64992a06cf2b20453deea9b8b43 100644 (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 } }
+    />
   );
 }
index 992673fb9b36c8deb17d7a9174be5f2f6fd69d04..75166e4b5094ddd82835cf95a135de4433927990 100644 (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();
   }
 });
index 3e0cf71978743dabc5f9514041bf5a7e2c1b15be..65d8dd19a62385236c8672e938a07fb35be394cc 100644 (file)
@@ -46,7 +46,7 @@ exports[`renders 1`] = `
       warnings={Array []}
     />
   </div>
-  <ComponentNavMenu
+  <Connect(withAppState(ComponentNavMenu))
     component={
       Object {
         "breadcrumbs": Array [
index 1386465802b42317c757c28c7c75945abb648bcd..1eb0a5d137bcb8487a2936c38337f54c85780ce3 100644 (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",
index 86a8372f88648d1361b5348b2ef07c7d4aef77a1..6bad17ad6b4ddf3ce76daf5d2430b10f6f94f4be 100644 (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 {
index e5b1ff868850558da89d1f4668c9e9b80347665b..a136ce1951834fd4ee443c6aeece5e47e1493dc7 100644 (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>
     );
index 488a642a771201e266d6d901c501e4969735033c..fe1b048163eed096eb71a9270031c94a841d9b50 100644 (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);
index 9913093515072947f849bcdfea10955ee1af0610..1656f723eda659fcba7901df0b433c2a6829c80d 100644 (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 } });
index 49e6ef1066649d502e8a0b8cdea99d96f5fbd534..4b2f85fa03cdf9d8ef84945f291c0fa06dd4944a 100644 (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 });
index 62c2757d57b4d3b0a779b37a110f90d751ac8404..2f92e6871af8401a1db0bf880f90318e20c6c8f5 100644 (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>
index c7ab0e8a71c15e97826e6354d9e3a65c29e36faf..16cae8e6cee55285309b8d0008e38042bffa6ff7 100644 (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;
index a6eadc5ff65abe637b82ab43e461e2ca60bf8d2a..e98edcad03fbd66e04ace96aa3a5dd2ab657d353 100644 (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;
 }
index e0124a0806c4e8528ee1cb9b8e1b27f324695e5d..98f46b51ac1ab2897647e9f3a1e351537ea6a27b 100644 (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;
 }
index 9aa8d3a9e97a4d9b3d37b3e3364a92af9fdcb05a..15a551aa71409579f931dfbf6da2246b39423457 100644 (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 && (
index f8ae8822fd5f48509512d8ea27b7c16112f8de55..4f67a985671c55bf11c38b60b9959f555fac825e 100644 (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);
index 89d1f04d5de7ecb1b94e4e36d9f33a854768b53c..7fc402e7416b65e3bad7699ecf5f2afe711eb204 100644 (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)
index 8f0d290c3ebe25f70c5d0936b829ee08b260b681..820f62f1ec91f4e4fbb49628fbf75c8c2e9da933 100644 (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);
index 731cddcfeed728d0dccdd04bcf6b81ef21bd21e5..675e7f90d75588205dd016717274cf421e8a7ea6 100644 (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();
index 4faebc2f57f851a4039dbe46a4e51edaca6887a1..275eeb839037256f3b553f4119c63b3dec9efe3c 100644 (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);
index 7a14512a40204969e6ce2630ed3efb6eddc4e7d1..ff574566f0dbfeeefba75d840ba4bc5b5448cf2b 100644 (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: []
 };
 
index ca09d9a5ddffb728dfdf90b3eeab7057e791b056..6bd1920e6be6020e7d36d10375fd6d967ad8649a 100644 (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);
index 613f2e8a6311af5664ab7c1e7f848eda7eaacb1c..5efb5710d7f1633432096fff1bc8f4ea4251e8c4 100644 (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)
+);
index 04cc36701683bd25e38c7d3f6ae59697fdfb1599..e699a4e43970053d2ccff457e4b923b2dcd6aeae 100644 (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 } }
+    />
   );
 }
index 74ec55332437bfecf6b466e4dc8f1462549045bd..330cc2d5b20a0bef1476fb3552e8e2b4d04a5322 100644 (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);
index afac1a5750e4651fd207e60119f43831111f62f4..376cf0f1fcf99fbe8fb697c1da1df6f2ddfb028f 100644 (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}
+    />
   );
 }
index f4a9b938b37122180806c8e928245bea2fea0655..b955ae314fcdb3434e0bcf06fb07c729d64cee22 100644 (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)
index ce073542841e53a39b1f53c95f6341dc69d68f54..19b59711f7203e984cc817e00630efe36899be9b 100644 (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);
index e6d9b84dc8855a07f6fcd0d0d8cb056c39cea27c..f6204e2e727bf12ceedf619a4ae4c73a515aa3c6 100644 (file)
  * 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);
index a3ea017810cc0e7d6f7e7e4ce2c67316d695cdba..615d1e8ff5d39e19b318f3874cbca7db21dff3bf 100644 (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}
     />
index d48d3089a5d63e4d00cfdf4feddb5a24dc4077a6..66a2f952e569bb2ca73faf2dc6197e46e6b61ae0 100644 (file)
@@ -7,7 +7,7 @@ exports[`renders 1`] = `
   <h4>
     project_activity.page
   </h4>
-  <PreviewGraph
+  <withRouter(PreviewGraph)
     history={
       Object {
         "coverage": Array [
index 9094431da5d31063ed0f16af80ae84cafb021960..7c3fa2102a86cded53b2bccfbd3bd0b35c43b82c 100644 (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);
index dd86e54052342135df07e2e30182cac88a561f8d..34cd6b76449ec8cb45dbc1f363e367f92e00d203 100644 (file)
  * 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);
index babf7c999e01c83bb6d14370c11b3c23bb03b5c6..12e7e5d81e5907c07f8b747c9dfa4295c3ff4ac1 100644 (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,
index e3d269914db41cbe420619aba9d3ecd79b2171a8..ce99e965fdca2f42ebdf06ddef4909cfd8497518 100644 (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 }}
+    />
   );
 }
index b7e2c9831d2692921ae8fc47a9fa1254b1752200..d91c838bd271f039a126f7890c266e9620783c65 100644 (file)
  * 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);
index e9b20ed5a64f72ba5c86144a2331ca14e2905bcd..558cf854834ab754cf4f30cb485dff797b6e73f6 100644 (file)
  * 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);
index f523a4f12f13f42b0fccf82cbabe73c84773cda6..84d9f38bdc986f4251cee6f603a6a970a93b7718 100644 (file)
  * 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);
index 95bb2bcfb76090c98e652fe2efcba105920d00e0..5756d0cda7d38a5b947e45f538541b0bafb6ea93 100644 (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)
+);
index 56e3986f26c1443963a3d45576383f72674a7877..8961c35c9c2e32924efcc651c032550d9ab68f9d 100644 (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);
index 7c690dbc3df0ddcb869a422fb3ad7665344212d5..f30e37ebfdc0a43487c9e59e207f550fbeb6b605 100644 (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);
index bb3fa09aa81f99a5b9c072d56322d72752980c5a..9e28e8002000d13f7edbeb8d8f66f002d162c90d 100644 (file)
  * 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);
index ac464d475c83d16b16c1027f50825ffdad12050a..8947a6fd0edc802d4129a785857d85a9a710ef26 100644 (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);
index 6e12e93f278dd552ea138c7578596b0d3e1b5f37..a14dff8528826700509d62a088f43dd797fc6b72 100644 (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"]'));
index b674d118d137a617b2ade4504394cad7b5c9c360..216cb7daa2fd649cea682f11b3f186702bbf65a5 100644 (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);
index 8ab102b574418cae926d6d7aeea07fd49c362d76..8a479d2e1c6244faee14e2a90c3716ce85094f03 100755 (executable)
@@ -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);
index b934385a71bd0c58ad23c5af8c9b77a73dea74ab..983786ae8dfe141103ed4fd7a501424035f5ea76 100644 (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',
index 2d4b850ae9343ed828e8d9cdb3e1deb37828baf3..960a1d39469c0b23db99809717482f23b4b94f47 100644 (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);
index e21b74aa1bb7a83748187091e17274613aebef39..9081375b868113902bdf6e15a371ab0bf2fa9680 100644 (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));
index d22ab7bcf52fd70eddce803d2354f50f9d83842f..04715cb8f19ccc2463951291804cc3b7cdc07090 100644 (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)
+);
index 96bbc79475a1b64d689b8772b68ac395419d4d79..2259a3460ee4d92997f8736fccbad93fe1906f63 100644 (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(() => {
index e833d41cd55983b145bc633301af5a289a1f28cd..5cec1105cc4e22eae30766ceb23600d071aabbef 100644 (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);
index 5adb4d2dae6a8d19ba401aba3704cf2857eb779c..b706ad6a1750d3e85669d199dc3a1d1dafa6149d 100644 (file)
  */
 /* 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}
     />,
     {
index 77630d3f8a44a30e705f6c596836766c73f0b975..437b2b78fbd68bb2dea0bb2587ebe33a576888e0 100644 (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);
index f091b3422942c10cf5753e2f01ff64f918914748..c632c7eac079bde6a15fd47a06716b4c2c517516 100644 (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;
 }
index 54871ff53f366c886f305e3f7036f52f294de4e0..d2f06d237fbe55d8bdeab313f302491e8012f39b 100644 (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();
 });
index 7b07cbda7971dfa23fb826f3e49371f5a3e36f71..94a51e8b63b3aa2b83c6c6ad2cdc447e01ecc100 100644 (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"
diff --git a/server/sonar-web/src/main/js/components/hoc/withRouter.tsx b/server/sonar-web/src/main/js/components/hoc/withRouter.tsx
new file mode 100644 (file)
index 0000000..1eb0e97
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { withRouter as originalWithRouter, WithRouterProps } from 'react-router';
+
+export type Location = WithRouterProps['location'];
+export type Router = WithRouterProps['router'];
+
+interface InjectedProps {
+  location?: Partial<Location>;
+  router?: Partial<Router>;
+}
+
+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);
+}
index 177af3e25156ebe6d2de22dda8d8f960bfc041fa..0fffacc9150669b9663121e5f3eb8eec9627db6a 100644 (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);