From ce80f2115245688992a727beeecd46ac43dca703 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Thu, 6 Dec 2018 15:53:55 +0100 Subject: [PATCH] remove some usages of legacy react context --- .../main/js/app/components/AdminContainer.tsx | 10 +- .../js/app/components/ComponentContainer.tsx | 21 +-- .../js/app/components/GlobalContainer.tsx | 26 ++- .../src/main/js/app/components/Landing.tsx | 16 +- .../__tests__/ComponentContainer-test.tsx | 47 +++-- .../embed-docs-modal/EmbedDocsPopup.tsx | 13 +- .../embed-docs-modal/EmbedDocsPopupHelper.tsx | 10 +- .../embed-docs-modal/Suggestions.tsx | 33 ++-- .../embed-docs-modal/SuggestionsContext.ts | 11 +- .../embed-docs-modal/SuggestionsProvider.tsx | 54 ++---- .../__tests__/EmbedDocsPopup-test.tsx | 23 +-- .../__tests__/SuggestionsProvider-test.tsx | 50 +++--- .../EmbedDocsPopup-test.tsx.snap | 160 +----------------- .../app/components/extensions/Extension.tsx | 57 +++++-- .../extensions/GlobalAdminPageExtension.tsx | 8 +- .../extensions/GlobalPageExtension.tsx | 8 +- .../extensions/OrganizationPageExtension.tsx | 4 +- .../extensions/ProjectAdminPageExtension.tsx | 4 +- .../extensions/ProjectPageExtension.tsx | 4 +- .../nav/component/ComponentNavBranch.tsx | 21 +-- .../component/ComponentNavBranchesMenu.tsx | 18 +- .../component/ComponentNavLicenseNotif.tsx | 14 +- .../nav/component/ComponentNavMenu.tsx | 13 +- .../__tests__/ComponentNavBranch-test.tsx | 30 ++-- .../ComponentNavBranchesMenu-test.tsx | 5 +- .../ComponentNavLicenseNotif-test.tsx | 22 +-- .../__tests__/ComponentNavMenu-test.tsx | 46 +++-- .../__snapshots__/ComponentNav-test.tsx.snap | 2 +- .../ComponentNavBgTaskNotif-test.tsx.snap | 2 +- .../ComponentNavBranch-test.tsx.snap | 6 +- .../app/components/nav/global/GlobalNav.tsx | 6 +- .../components/nav/global/GlobalNavUser.tsx | 13 +- .../nav/global/__tests__/GlobalNav-test.tsx | 7 +- .../global/__tests__/GlobalNavUser-test.tsx | 24 ++- .../__snapshots__/GlobalNav-test.tsx.snap | 26 +-- server/sonar-web/src/main/js/app/types.d.ts | 6 + .../account/notifications/Notifications.tsx | 14 +- .../__tests__/Notifications-test.tsx | 14 +- .../src/main/js/apps/code/components/App.tsx | 4 +- .../main/js/apps/code/components/Search.tsx | 18 +- .../js/apps/coding-rules/components/App.tsx | 14 +- .../components/RuleDetailsIssues.tsx | 14 +- .../__tests__/RuleDetailsIssues-test.tsx | 8 +- .../main/js/apps/issues/components/App.tsx | 27 ++- .../issues/components/__tests__/App-test.tsx | 3 +- .../src/main/js/apps/marketplace/App.tsx | 17 +- .../components/OrganizationDelete.tsx | 19 +-- .../__tests__/OrganizationDelete-test.tsx | 11 +- .../main/js/apps/overview/components/App.tsx | 17 +- .../components/__tests__/App-test.tsx | 22 ++- .../js/apps/overview/meta/MetaContainer.tsx | 14 +- .../components/ActionsCell.tsx | 16 +- .../components/Header.tsx | 14 +- .../components/__tests__/ActionsCell-test.tsx | 5 +- .../__snapshots__/Activity-test.tsx.snap | 2 +- .../apps/projects/components/AllProjects.tsx | 23 ++- .../components/DefaultPageSelector.tsx | 24 ++- .../components/__tests__/AllProjects-test.tsx | 12 +- .../__tests__/DefaultPageSelector-test.tsx | 9 +- .../components/CopyQualityGateForm.tsx | 13 +- .../components/CreateQualityGateForm.tsx | 15 +- .../components/DeleteQualityGateForm.tsx | 13 +- .../quality-gates/components/DetailsApp.tsx | 19 +-- .../components/QualityGatesApp.tsx | 15 +- .../changelog/ChangelogContainer.tsx | 27 +-- .../compare/ComparisonContainer.tsx | 21 +-- .../components/ProfileActions.tsx | 28 ++- .../__tests__/ProfileActions-test.tsx | 17 +- .../apps/quality-profiles/home/PageHeader.tsx | 15 +- .../apps/securityReports/components/App.tsx | 17 +- .../components/__tests__/App-test.tsx | 65 +++++-- .../main/js/apps/system/components/App.tsx | 23 +-- .../projectOnboarding/ProjectOnboarding.tsx | 10 +- .../ProjectOnboardingPage.tsx | 20 +-- .../__tests__/ProjectOnboarding-test.tsx | 15 +- .../src/main/js/apps/users/UsersApp.tsx | 17 +- .../js/apps/users/__tests__/UsersApp-test.tsx | 7 +- .../js/apps/web-api/components/WebApiApp.tsx | 28 +-- .../src/main/js/components/docs/DocLink.tsx | 14 +- .../docs/__tests__/DocLink-test.tsx | 72 ++++++-- .../__snapshots__/DocLink-test.tsx.snap | 10 ++ .../hoc/withRouter.tsx} | 25 +-- .../components/preview-graph/PreviewGraph.tsx | 13 +- 83 files changed, 787 insertions(+), 873 deletions(-) rename server/sonar-web/src/main/js/{app/components/extensions/ExtensionContainer.tsx => components/hoc/withRouter.tsx} (61%) diff --git a/server/sonar-web/src/main/js/app/components/AdminContainer.tsx b/server/sonar-web/src/main/js/app/components/AdminContainer.tsx index 6f65e1d4c26..8890e985cfc 100644 --- a/server/sonar-web/src/main/js/app/components/AdminContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/AdminContainer.tsx @@ -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; + appState: Pick; } interface DispatchToProps { @@ -50,18 +49,13 @@ interface State { class AdminContainer extends React.PureComponent { 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(); diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx index 0bc2bfa614e..a031c478169 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -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; children: any; fetchOrganizations: (organizations: string[]) => void; location: { @@ -66,15 +67,7 @@ const FETCH_STATUS_WAIT_TIME = 3000; export class ComponentContainer extends React.PureComponent { 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 { .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 { } } +const mapStateToProps = (state: Store) => ({ + appState: getAppState(state) +}); + const mapDispatchToProps = { fetchOrganizations }; export default connect( - null, + mapStateToProps, mapDispatchToProps )(ComponentContainer); diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx index 32ee2c13079..f5187c585be 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -36,22 +36,20 @@ export default function GlobalContainer(props: Props) { const { footer = } = props; return ( - {({ suggestions }) => ( - -
-
-
- - - - {props.children} - -
+ +
+
+
+ + + + {props.children} +
- {footer}
- - )} + {footer} +
+
); } diff --git a/server/sonar-web/src/main/js/app/components/Landing.tsx b/server/sonar-web/src/main/js/app/components/Landing.tsx index 310d2f27629..c5634b10a71 100644 --- a/server/sonar-web/src/main/js/app/components/Landing.tsx +++ b/server/sonar-web/src/main/js/app/components/Landing.tsx @@ -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 { - static contextTypes = { - router: PropTypes.object.isRequired - }; - +class Landing extends React.PureComponent { 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)); diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx index b661f086095..34d513e1b17 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx @@ -76,7 +76,10 @@ beforeEach(() => { it('changes component', () => { const wrapper = shallow( - + ); @@ -100,7 +103,10 @@ it("loads branches for module's project", async () => { }); mount( - + ); @@ -114,7 +120,10 @@ it("loads branches for module's project", async () => { it("doesn't load branches portfolio", async () => { const wrapper = mount( - + ); @@ -130,7 +139,10 @@ it("doesn't load branches portfolio", async () => { it('updates branches on change', () => { const wrapper = shallow( - + ); @@ -156,6 +168,7 @@ it('updates the branch measures', async () => { (getPullRequests as jest.Mock).mockResolvedValueOnce([]); const wrapper = shallow( @@ -184,10 +197,12 @@ it('loads organization', async () => { const fetchOrganizations = jest.fn(); mount( - + - , - { context: { organizationsEnabled: true } } + ); await new Promise(setImmediate); @@ -198,10 +213,12 @@ it('fetches status', async () => { (getComponentData as jest.Mock).mockResolvedValueOnce({ organization: 'org' }); mount( - + - , - { context: { organizationsEnabled: true } } + ); 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( - + ); @@ -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).mockResolvedValueOnce({ queue: [inProgressTask] }); const wrapper = shallow( - + ); diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx index 5cc79d2d179..b191d8879d9 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx @@ -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; } export default class EmbedDocsPopup extends React.PureComponent { @@ -36,14 +35,14 @@ export default class EmbedDocsPopup extends React.PureComponent { return
  • {text}
  • ; } - 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) => (
  • {suggestion.text} @@ -53,7 +52,7 @@ export default class EmbedDocsPopup extends React.PureComponent {
  • ); - } + }; renderIconLink(link: string, icon: string, text: string) { return ( @@ -138,7 +137,7 @@ export default class EmbedDocsPopup extends React.PureComponent { return (
      - {this.renderSuggestions()} + {this.renderSuggestions}
    • {translate('embed_docs.documentation')} diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx index 8e2486b5025..d02c95f18d7 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx @@ -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; -} interface State { helpOpen: boolean; } -export default class EmbedDocsPopupHelper extends React.PureComponent { +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 - }> + overlay={}> diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/Suggestions.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/Suggestions.tsx index 7ffd50b40cb..450bc63547c 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/Suggestions.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/Suggestions.tsx @@ -18,33 +18,46 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as PropTypes from 'prop-types'; import { SuggestionsContext } from './SuggestionsContext'; interface Props { suggestions: string; } -export default class Suggestions extends React.PureComponent { - context!: { suggestions: SuggestionsContext }; +export default function Suggestions({ suggestions }: Props) { + return ( + + {({ addSuggestions, removeSuggestions }) => ( + + )} + + ); +} - static contextTypes = { - suggestions: PropTypes.object.isRequired - }; +interface SuggestionsInnerProps { + addSuggestions: (key: string) => void; + removeSuggestions: (key: string) => void; + suggestions: string; +} +class SuggestionsInner extends React.PureComponent { 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() { diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsContext.ts b/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsContext.ts index 8292383c5db..c97e2e8a476 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsContext.ts +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsContext.ts @@ -17,7 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -export interface SuggestionsContext { +import { createContext } from 'react'; + +interface SuggestionsContextShape { addSuggestions: (key: string) => void; removeSuggestions: (key: string) => void; + suggestions: T.SuggestionLink[]; } + +export const SuggestionsContext = createContext({ + addSuggestions: () => {}, + removeSuggestions: () => {}, + suggestions: [] +}); diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx index 909c6a5d1d4..1095071b8c3 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx @@ -18,56 +18,33 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as PropTypes from 'prop-types'; -// eslint-disable-next-line import/no-extraneous-dependencies -import * as suggestionsJson from 'Docs/EmbedDocsSuggestions.json'; +import suggestionsJson from 'Docs/EmbedDocsSuggestions.json'; import { SuggestionsContext } from './SuggestionsContext'; import { isSonarCloud } from '../../../helpers/system'; -export interface SuggestionLink { - link: string; - scope?: 'sonarcloud'; - text: string; -} - interface SuggestionsJson { - [key: string]: SuggestionLink[]; -} - -interface Props { - children: ({ suggestions }: { suggestions: SuggestionLink[] }) => React.ReactNode; + [key: string]: T.SuggestionLink[]; } interface State { - suggestions: SuggestionLink[]; + suggestions: T.SuggestionLink[]; } -export default class SuggestionsProvider extends React.Component { +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 { }; render() { - const suggestions = isSonarCloud() - ? this.state.suggestions - : this.state.suggestions.filter(suggestion => suggestion.scope !== 'sonarcloud'); - - return this.props.children({ suggestions }); + return ( + + {this.props.children} + + ); } } diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx index e21c57fb020..0520bde7738 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx @@ -20,27 +20,8 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import EmbedDocsPopup from '../EmbedDocsPopup'; -import { isSonarCloud } from '../../../../helpers/system'; -jest.mock('../../../../helpers/system', () => ({ isSonarCloud: jest.fn().mockReturnValue(false) })); - -const suggestions = [{ link: '#', text: 'foo' }, { link: '#', text: 'bar' }]; - -it('should display suggestion links', () => { - const context = {}; - const wrapper = shallow(, { - context - }); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); -}); - -it('should display correct links for SonarCloud', () => { - (isSonarCloud as jest.Mock).mockReturnValueOnce(true); - const context = {}; - const wrapper = shallow(, { - context - }); - wrapper.update(); +it('should render', () => { + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx index 2092c80a14d..eee016635fd 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/SuggestionsProvider-test.tsx @@ -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({children}); - const instance = wrapper.instance() as SuggestionsProvider; - expect(children).lastCalledWith({ suggestions: [] }); + (isSonarCloud as jest.Mock).mockReturnValue(false); + const wrapper = shallow( + +
      + + ); + 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({children}); - const instance = wrapper.instance() as SuggestionsProvider; - expect(children).lastCalledWith({ suggestions: [] }); + (isSonarCloud as jest.Mock).mockReturnValue(true); + const wrapper = shallow( + +
      + + ); + 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' } + ]); }); diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap index 434b4be4f7b..d02e45c3e85 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap @@ -1,165 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should display correct links for SonarCloud 1`] = ` +exports[`should render 1`] = ` - -`; - -exports[`should display suggestion links 1`] = ` - -
        -
      • - embed_docs.suggestion -
      • -
      • - - foo - -
      • -
      • - - bar - -
      • -
      • + + +
      • void; options?: {}; } -type Props = OwnProps & WithRouterProps & InjectedIntlProps; +interface StateProps { + currentUser: T.CurrentUser; +} -class Extension extends React.PureComponent { +interface DispatchProps { + onFail: (message: string) => void; +} + +type Props = OwnProps & WithRouterProps & InjectedIntlProps & StateProps & DispatchProps; + +interface State { + extensionElement?: React.ReactElement; +} + +class Extension extends React.PureComponent { 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 { 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 { return (
        -
        (this.container = container)} /> + {this.state.extensionElement ? ( + this.state.extensionElement + ) : ( +
        (this.container = container)} /> + )}
        ); } } -export default injectIntl(withRouter(Extension)); +function mapStateToProps(state: Store): StateProps { + return { currentUser: getCurrentUser(state) }; +} + +const mapDispatchToProps: DispatchProps = { onFail: addGlobalErrorMessage }; + +export default injectIntl( + withRouter( + connect( + mapStateToProps, + mapDispatchToProps + )(Extension) + ) +); diff --git a/server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx b/server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx index 8db930dd62b..17119b71b6c 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx @@ -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 ? ( - - ) : ( - - ); + return extension ? : ; } const mapStateToProps = (state: Store) => ({ diff --git a/server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx b/server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx index b37ac541bea..e1b967b6c96 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx @@ -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 ? ( - - ) : ( - - ); + return extension ? : ; } const mapStateToProps = (state: Store) => ({ diff --git a/server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.tsx b/server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.tsx index 16ae71708ac..a372d0f1f48 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.tsx @@ -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 { const extension = pages.find(p => p.key === `${pluginKey}/${extensionKey}`); return extension ? ( - diff --git a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx index 9502e405b63..7f2e85d30fe 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx @@ -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 ? ( - + ) : ( ); diff --git a/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx b/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx index ada4b4f3e69..c98752e7a32 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx @@ -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 ? ( - + ) : ( ); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx index 8fee54db550..ba26c68f3b2 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx @@ -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; branchLikes: T.BranchLike[]; component: T.Component; currentBranchLike: T.BranchLike; @@ -50,17 +51,9 @@ interface State { dropdownOpen: boolean; } -export default class ComponentNavBranch extends React.PureComponent { +export class ComponentNavBranch extends React.PureComponent { 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 ); } else { - if (!this.context.branchesEnabled) { + if (!this.props.appState.branchesEnabled) { return (
        void; + router: Pick; } interface State { @@ -50,14 +51,9 @@ interface State { selected: T.BranchLike | undefined; } -export default class ComponentNavBranchesMenu extends React.PureComponent { - private listNode?: HTMLUListElement | null; - private selectedBranchNode?: HTMLLIElement | null; - - static contextTypes = { - router: PropTypes.object - }; - +export class ComponentNavBranchesMenu extends React.PureComponent { + listNode?: HTMLUListElement | null; + selectedBranchNode?: HTMLLIElement | null; state: State = { query: '', selected: undefined }; componentDidMount() { @@ -113,7 +109,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent { 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; currentTask?: T.Task; } @@ -33,13 +34,8 @@ interface State { loading: boolean; } -export default class ComponentNavLicenseNotif extends React.PureComponent { +export class ComponentNavLicenseNotif extends React.PureComponent { mounted = false; - - static contextTypes = { - canAdmin: PropTypes.bool.isRequired - }; - state: State = { loading: false }; componentDidMount() { @@ -88,7 +84,7 @@ export default class ComponentNavLicenseNotif extends React.PureComponent {currentTask.errorMessage} - {this.context.canAdmin ? ( + {this.props.appState.canAdmin ? ( {translate('license.component_navigation.button', currentTask.errorType)}. @@ -99,3 +95,5 @@ export default class ComponentNavLicenseNotif extends React.PureComponent; branchLike: T.BranchLike | undefined; component: T.Component; location?: any; } -export default class ComponentNavMenu extends React.PureComponent { - static contextTypes = { - branchesEnabled: PropTypes.bool.isRequired - }; - +export class ComponentNavMenu extends React.PureComponent { isProject() { return this.props.component.qualifier === 'TRK'; } @@ -282,7 +279,7 @@ export default class ComponentNavMenu extends React.PureComponent { 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 { ); } } + +export default withAppState(ComponentNavMenu); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx index 17c44b1beb9..083d976a86a 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx @@ -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( , - { context: { branchesEnabled: true, canAdmin: true } } + /> ) ).toMatchSnapshot(); }); @@ -58,11 +58,11 @@ it('renders short-living branch', () => { expect( shallow( , - { context: { branchesEnabled: true, canAdmin: true } } + /> ) ).toMatchSnapshot(); }); @@ -79,11 +79,11 @@ it('renders pull request', () => { expect( shallow( , - { context: { branchesEnabled: true, canAdmin: true } } + /> ) ).toMatchSnapshot(); }); @@ -92,11 +92,11 @@ it('opens menu', () => { const component = {} as T.Component; const wrapper = shallow( , - { 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( , - { 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( , - { 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( , - { context: { branchesEnabled: false, onSonarCloud: true, canAdmin: true } } + /> ); expect(wrapper.type()).toBeNull(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx index 8f35a1c252f..1410ea5f74f 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx @@ -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); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx index 8528a2701f7..6c90f7a9c8d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavLicenseNotif-test.tsx @@ -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).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).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 = {}) { return shallow( , - { context: { canAdmin: true, ...context } } + /> ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx index 992673fb9b3..75166e4b509 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx @@ -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(, { - context: { branchesEnabled: true } - }); + const wrapper = shallow( + + ); 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(, { - context: { branchesEnabled: true } - }); + const wrapper = shallow( + + ); 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(, { - context: { branchesEnabled: true } - }) + shallow( + + ) ).toMatchSnapshot(); }); @@ -88,14 +100,14 @@ it('should work for long-living branches', () => { expect( shallow( , - { 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(, { - context: { branchesEnabled: true } - }) + shallow( + + ) ).toMatchSnapshot(); } }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap index 3e0cf719787..65d8dd19a62 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap @@ -46,7 +46,7 @@ exports[`renders 1`] = ` warnings={Array []} />
        - ; } type Props = StateProps & OwnProps; @@ -62,7 +60,7 @@ export class GlobalNav extends React.PureComponent {
          {isSonarCloud() && } - + {isLoggedIn(currentUser) && ( { openProjectOnboarding={this.context.openProjectOnboarding} /> )} - +
        ); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx index 488a642a771..fe1b048163e 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx @@ -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; } -export default class GlobalNavUser extends React.PureComponent { - static contextTypes = { - router: PropTypes.object - }; - +export class GlobalNavUser extends React.PureComponent { handleLogin = (event: React.SyntheticEvent) => { event.preventDefault(); const shouldReturnToCurrentPage = window.location.pathname !== `${getBaseUrl()}/about`; @@ -54,7 +51,7 @@ export default class GlobalNavUser extends React.PureComponent { handleLogout = (event: React.SyntheticEvent) => { 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 { return isLoggedIn(this.props.currentUser) ? this.renderAuthenticated() : this.renderAnonymous(); } } + +export default withRouter(GlobalNavUser); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx index 99130935150..1656f723eda 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNav-test.tsx @@ -43,12 +43,7 @@ it('should render for SonarCloud', () => { function runTest(mockedIsSonarCloud: boolean) { (isSonarCloud as jest.Mock).mockImplementation(() => mockedIsSonarCloud); const wrapper = shallow( - + ); expect(wrapper).toMatchSnapshot(); wrapper.setProps({ currentUser: { isLoggedIn: true } }); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx index 49e6ef10666..4b2f85fa03c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx @@ -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( - + ); expect(wrapper).toMatchSnapshot(); }); it('should render the right interface for logged in user', () => { const wrapper = shallow( - + ); 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( - + ); 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 }); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap index 62c2757d57b..2f92e6871af 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap @@ -26,7 +26,6 @@ exports[`should render for SonarCloud 1`] = ` "pathname": "", } } - suggestions={Array []} />
          - + -
        @@ -107,14 +98,11 @@ exports[`should render for SonarQube 1`] = ` "pathname": "", } } - suggestions={Array []} />
          - + -
        diff --git a/server/sonar-web/src/main/js/app/types.d.ts b/server/sonar-web/src/main/js/app/types.d.ts index c7ab0e8a71c..16cae8e6cee 100644 --- a/server/sonar-web/src/main/js/app/types.d.ts +++ b/server/sonar-web/src/main/js/app/types.d.ts @@ -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; diff --git a/server/sonar-web/src/main/js/apps/account/notifications/Notifications.tsx b/server/sonar-web/src/main/js/apps/account/notifications/Notifications.tsx index a6eadc5ff65..e98edcad03f 100644 --- a/server/sonar-web/src/main/js/apps/account/notifications/Notifications.tsx +++ b/server/sonar-web/src/main/js/apps/account/notifications/Notifications.tsx @@ -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; fetchOrganizations: (organizations: string[]) => void; } @@ -41,13 +42,8 @@ interface State { perProjectTypes: string[]; } -export default class Notifications extends React.PureComponent { +export class Notifications extends React.PureComponent { mounted = false; - - static contextTypes = { - organizationsEnabled: PropTypes.bool - }; - state: State = { channels: [], globalTypes: [], @@ -69,7 +65,7 @@ export default class Notifications extends React.PureComponent { 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 { } } +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; } diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/Notifications-test.tsx b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/Notifications-test.tsx index e0124a0806c..98f46b51ac1 100644 --- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/Notifications-test.tsx +++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/Notifications-test.tsx @@ -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, context?: any) { - const wrapper = shallow(, { context }); +async function shallowRender(props?: Partial) { + const wrapper = shallow( + + ); await waitAndUpdate(wrapper); return wrapper; } diff --git a/server/sonar-web/src/main/js/apps/code/components/App.tsx b/server/sonar-web/src/main/js/apps/code/components/App.tsx index 9aa8d3a9e97..15a551aa714 100644 --- a/server/sonar-web/src/main/js/apps/code/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/App.tsx @@ -175,7 +175,7 @@ export class App extends React.PureComponent { }; 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 { - +
        {shouldShowBreadcrumbs && ( diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.tsx b/server/sonar-web/src/main/js/apps/code/components/Search.tsx index f8ae8822fd5..4f67a985671 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Search.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Search.tsx @@ -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; } interface State { @@ -40,13 +41,8 @@ interface State { selectedIndex?: number; } -export default class Search extends React.PureComponent { +class Search extends React.PureComponent { mounted = false; - - static contextTypes = { - router: PropTypes.object.isRequired - }; - state: State = { query: '', loading: false @@ -93,9 +89,9 @@ export default class Search extends React.PureComponent { 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 { ); } } + +export default withRouter(Search); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx index 89d1f04d5de..7fc402e7416 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx @@ -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 { mounted = false; - static contextTypes = { - organizationsEnabled: PropTypes.bool - }; - constructor(props: Props) { super(props); this.state = { @@ -528,7 +525,7 @@ export class App extends React.PureComponent { 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 {
        {this.state.openRule ? ( ({ + appState: getAppState(state), currentUser: getCurrentUser(state), languages: getLanguages(state), userOrganizations: getMyOrganizations(state) diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx index 8f0d290c3eb..820f62f1ec9 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx @@ -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; organization: string | undefined; ruleDetails: Pick; } @@ -44,13 +45,8 @@ interface State { total?: number; } -export default class RuleDetailsIssues extends React.PureComponent { +export class RuleDetailsIssues extends React.PureComponent { mounted = false; - - static contextTypes = { - branchesEnabled: PropTypes.bool - }; - state: State = { loading: true }; componentDidMount() { @@ -119,7 +115,7 @@ export default class RuleDetailsIssues extends React.PureComponent ); - if (!this.context.branchesEnabled) { + if (!this.props.appState.branchesEnabled) { return totalItem; } @@ -173,3 +169,5 @@ export default class RuleDetailsIssues extends React.PureComponent ); } } + +export default withAppState(RuleDetailsIssues); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsIssues-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsIssues-test.tsx index 731cddcfeed..675e7f90d75 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsIssues-test.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetailsIssues-test.tsx @@ -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( - + ); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.tsx b/server/sonar-web/src/main/js/apps/issues/components/App.tsx index 4faebc2f57f..275eeb83903 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/App.tsx @@ -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; hideAuthorFacet?: boolean; - location: { pathname: string; query: RawQuery }; + location: Pick; myIssues?: boolean; onBranchesChange: () => void; organization?: { key: string }; + router: Pick; userOrganizations: T.Organization[]; } @@ -130,13 +131,9 @@ export interface State { const DEFAULT_QUERY = { resolved: 'false' }; -export default class App extends React.PureComponent { +export class App extends React.PureComponent { 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 { 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 { handleFilterChange = (changes: Partial) => { 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 { 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 { }; 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 { ); } } + +export default withRouter(App); diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx index 7a14512a402..ff574566f0d 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx @@ -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: [] }; diff --git a/server/sonar-web/src/main/js/apps/marketplace/App.tsx b/server/sonar-web/src/main/js/apps/marketplace/App.tsx index ca09d9a5ddf..6bd1920e6be 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/App.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/App.tsx @@ -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; standaloneMode?: boolean; updateCenterActive: boolean; } @@ -54,13 +54,8 @@ interface State { plugins: Plugin[]; } -export default class App extends React.PureComponent { +class App extends React.PureComponent { 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 { updateQuery = (newQuery: Partial) => { 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 { ); } } + +export default withRouter(App); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.tsx index 613f2e8a631..5efb5710d7f 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.tsx @@ -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; @@ -36,6 +36,7 @@ interface DispatchToProps { interface OwnProps { organization: Pick; + router: Pick; } type Props = OwnProps & DispatchToProps; @@ -46,10 +47,6 @@ interface State { export class OrganizationDelete extends React.PureComponent { mounted = false; - static contextTypes = { - router: PropTypes.object - }; - state: State = {}; componentDidMount() { @@ -82,7 +79,7 @@ export class OrganizationDelete extends React.PureComponent { 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 { const mapDispatchToProps: DispatchToProps = { deleteOrganization: deleteOrganization as any }; -export default connect( - null, - mapDispatchToProps -)(OrganizationDelete); +export default withRouter( + connect( + null, + mapDispatchToProps + )(OrganizationDelete) +); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.tsx b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.tsx index 04cc3670168..e699a4e4397 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.tsx @@ -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 = {}) { return shallow( Promise.resolve())} organization={{ key: 'foo', name: 'Foo' }} + router={{ replace: jest.fn() }} {...props} - />, - - { context: { router: { replace: jest.fn() }, ...context } } + /> ); } diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.tsx b/server/sonar-web/src/main/js/apps/overview/components/App.tsx index 74ec5533243..330cc2d5b20 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/App.tsx @@ -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) => void; + router: Pick; } -export default class App extends React.PureComponent { - static contextTypes = { - router: PropTypes.object - }; - +export class App extends React.PureComponent { 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 { ); } } + +export default withRouter(App); diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx index afac1a5750e..376cf0f1fcf 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx @@ -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).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 = {}) { return shallow( - + ); } diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx index f4a9b938b37..b955ae314fc 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx @@ -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 { - 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 { } 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 { } const mapStateToProps = (state: Store, { component }: OwnProps) => ({ + appState: getAppState(state), currentUser: getCurrentUser(state), organization: getOrganizationByKey(state, component.organization), userOrganizations: getMyOrganizations(state) diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx index ce073542841..19b59711f72 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx @@ -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; topQualifiers: string[]; } @@ -44,13 +45,8 @@ interface State { updateModal: boolean; } -export default class ActionsCell extends React.PureComponent { +export class ActionsCell extends React.PureComponent { 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 { 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 { ); } } + +export default withRouter(ActionsCell); diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx index e6d9b84dc88..f6204e2e727 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx @@ -18,29 +18,25 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as PropTypes from 'prop-types'; import Form from './Form'; import { createPermissionTemplate } from '../../../api/permissions'; import { Button } from '../../../components/ui/buttons'; import { translate } from '../../../helpers/l10n'; +import { withRouter, Router } from '../../../components/hoc/withRouter'; interface Props { organization?: { key: string }; ready?: boolean; refresh: () => Promise; + router: Pick; } interface State { createModal: boolean; } -export default class Header extends React.PureComponent { +class Header extends React.PureComponent { mounted = false; - - static contextTypes = { - router: PropTypes.object - }; - state: State = { createModal: false }; componentDidMount() { @@ -72,7 +68,7 @@ export default class Header extends React.PureComponent { 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 { ); } } + +export default withRouter(Header); diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx index a3ea017810c..615d1e8ff5d 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.tsx @@ -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) { +function renderActionsCell(props?: Partial) { return shallow( true} + router={{ replace: jest.fn() }} topQualifiers={['TRK', 'VW']} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap index d48d3089a5d..66a2f952e56 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Activity-test.tsx.snap @@ -7,7 +7,7 @@ exports[`renders 1`] = `

        project_activity.page

        - ; organization: T.Organization | undefined; organizationsEnabled?: boolean; + router: Pick; 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 { +export class AllProjects extends React.PureComponent { 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 { 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 { // 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 { 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 { ); } } + +export default withRouter(AllProjects); diff --git a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx index dd86e540523..34cd6b76449 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx @@ -18,17 +18,18 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as PropTypes from 'prop-types'; import AllProjectsContainer from './AllProjectsContainer'; import { PROJECTS_DEFAULT_FILTER, PROJECTS_FAVORITE, PROJECTS_ALL } from '../utils'; import { get } from '../../../helpers/storage'; import { searchProjects } from '../../../api/components'; import { isSonarCloud } from '../../../helpers/system'; import { isLoggedIn } from '../../../helpers/users'; +import { withRouter, Location, Router } from '../../../components/hoc/withRouter'; interface Props { currentUser: T.CurrentUser; - location: { pathname: string; query: { [x: string]: string } }; + location: Pick; + router: Pick; } interface State { @@ -36,19 +37,12 @@ interface State { shouldForceSorting?: string; } -export default class DefaultPageSelector extends React.PureComponent { - static contextTypes = { - router: PropTypes.object.isRequired - }; - - constructor(props: Props) { - super(props); - this.state = {}; - } +export class DefaultPageSelector extends React.PureComponent { + 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 ({ @@ -162,9 +162,9 @@ it('changes perspective to risk visualization', () => { }); function shallowRender( - props: Partial = {}, - push: Function = jest.fn(), - replace: Function = jest.fn() + props: Partial = {}, + push = jest.fn(), + replace = jest.fn() ) { const wrapper = shallow( , - { context: { router: { push, replace } } } + /> ); wrapper.setState({ loading: false, diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx index e3d269914db..ce99e965fdc 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx @@ -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; @@ -87,7 +87,10 @@ function mountRender( replace: any = jest.fn() ) { return mount( - , - { context: { router: { replace } } } + ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx index b7e2c9831d2..d91c838bd27 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx @@ -18,28 +18,25 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as PropTypes from 'prop-types'; import { copyQualityGate } from '../../../api/quality-gates'; import ConfirmModal from '../../../components/controls/ConfirmModal'; import { translate } from '../../../helpers/l10n'; import { getQualityGateUrl } from '../../../helpers/urls'; +import { withRouter, Router } from '../../../components/hoc/withRouter'; interface Props { onClose: () => void; onCopy: () => Promise; organization?: string; qualityGate: T.QualityGate; + router: Pick; } interface State { name: string; } -export default class CopyQualityGateForm extends React.PureComponent { - static contextTypes = { - router: PropTypes.object - }; - +class CopyQualityGateForm extends React.PureComponent { constructor(props: Props) { super(props); this.state = { name: props.qualityGate.name }; @@ -59,7 +56,7 @@ export default class CopyQualityGateForm extends React.PureComponent { 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 void; onCreate: () => Promise; organization?: string; + router: Pick; } interface State { name: string; } -export default class CreateQualityGateForm extends React.PureComponent { - static contextTypes = { - router: PropTypes.object - }; - - state = { name: '' }; +class CreateQualityGateForm extends React.PureComponent { + state: State = { name: '' }; handleNameChange = (event: React.SyntheticEvent) => { this.setState({ name: event.currentTarget.value }); @@ -58,7 +55,7 @@ export default class CreateQualityGateForm extends React.PureComponent 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 Promise; organization?: string; qualityGate: T.QualityGate; + router: Pick; } -export default class DeleteQualityGateForm extends React.PureComponent { - static contextTypes = { - router: PropTypes.object - }; - +class DeleteQualityGateForm extends React.PureComponent { 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 { ); } } + +export default withRouter(DeleteQualityGateForm); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsApp.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsApp.tsx index 95bb2bcfb76..5756d0cda7d 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsApp.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsApp.tsx @@ -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 { 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) +); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.tsx index 56e3986f26c..8961c35c9c2 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.tsx @@ -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; @@ -44,13 +44,8 @@ interface State { qualityGates: T.QualityGate[]; } -export default class QualityGatesApp extends React.PureComponent { +class QualityGatesApp extends React.PureComponent { 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 { 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 { ); } } + +export default withRouter(QualityGatesApp); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx index 7c690dbc3df..f30e37ebfdc 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx @@ -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 { +class ChangelogContainer extends React.PureComponent { 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 { @@ -145,7 +132,7 @@ export default class ChangelogContainer extends React.PureComponent; } -export default class ComparisonContainer extends React.PureComponent { +class ComparisonContainer extends React.PureComponent { 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; updateProfiles: () => Promise; } @@ -46,20 +47,13 @@ interface State { renameFormOpen: boolean; } -export default class ProfileActions extends React.PureComponent { - static contextTypes = { - router: PropTypes.object +export class ProfileActions extends React.PureComponent { + 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 { 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 { 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 { }; 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 { ); } } + +export default withRouter(ProfileActions); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx index 6e12e93f278..a14dff85288 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx @@ -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() + shallow( + + ) ).toMatchSnapshot(); }); @@ -50,6 +57,7 @@ it('renders with permission to edit only', () => { ) @@ -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 () => { , - { context: { router: { push } } } + /> ); click(wrapper.find('[id="quality-profile-copy"]')); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx index b674d118d13..216cb7daa2f 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx @@ -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; updateProfiles: () => Promise; } @@ -40,12 +41,8 @@ interface State { restoreFormOpen: boolean; } -export default class PageHeader extends React.PureComponent { - static contextTypes = { - router: PropTypes.object - }; - - state = { +class PageHeader extends React.PureComponent { + state: State = { createFormOpen: false, restoreFormOpen: false }; @@ -57,7 +54,7 @@ export default class PageHeader extends React.PureComponent { 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 { ); } } + +export default withRouter(PageHeader); diff --git a/server/sonar-web/src/main/js/apps/securityReports/components/App.tsx b/server/sonar-web/src/main/js/apps/securityReports/components/App.tsx index 8ab102b5744..8a479d2e1c6 100755 --- a/server/sonar-web/src/main/js/apps/securityReports/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/securityReports/components/App.tsx @@ -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; params: { type: string }; + router: Pick; } interface State { @@ -50,13 +50,9 @@ interface State { showCWE: boolean; } -export default class App extends React.PureComponent { +export class App extends React.PureComponent { 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 { }; 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 { ); } } + +export default withRouter(App); diff --git a/server/sonar-web/src/main/js/apps/securityReports/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/securityReports/components/__tests__/App-test.tsx index b934385a71b..983786ae8df 100644 --- a/server/sonar-web/src/main/js/apps/securityReports/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/securityReports/components/__tests__/App-test.tsx @@ -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(, { - context - }); + const wrapper = shallow( + , + { + context + } + ); expect(wrapper).toMatchSnapshot(); }); it('renders owaspTop10', async () => { - const wrapper = shallow(, { - context - }); + const wrapper = shallow( + , + { + context + } + ); await waitAndUpdate(wrapper); expect(getSecurityHotspots).toBeCalledWith({ project: 'foo', @@ -119,7 +135,12 @@ it('renders owaspTop10', async () => { it('renders with cwe', () => { const wrapper = shallow( - , + , { context } ); expect(getSecurityHotspots).toBeCalledWith({ @@ -132,9 +153,17 @@ it('renders with cwe', () => { }); it('handle checkbox for cwe display', async () => { - const wrapper = shallow(, { - context - }); + const wrapper = shallow( + , + { + 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(, { - context - }); + const wrapper = shallow( + , + { + context + } + ); expect(getSecurityHotspots).toBeCalledWith({ project: 'foo', standard: 'sansTop25', diff --git a/server/sonar-web/src/main/js/apps/system/components/App.tsx b/server/sonar-web/src/main/js/apps/system/components/App.tsx index 2d4b850ae93..960a1d39469 100644 --- a/server/sonar-web/src/main/js/apps/system/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/App.tsx @@ -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 { +class App extends React.PureComponent { 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 { 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 { ); } } + +export default withRouter(App); diff --git a/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboarding.tsx b/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboarding.tsx index e21b74aa1bb..9081375b868 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboarding.tsx +++ b/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboarding.tsx @@ -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; } interface StateProps { @@ -56,9 +57,6 @@ interface State { export class ProjectOnboarding extends React.PureComponent { mounted = false; - static contextTypes = { - router: PropTypes.object - }; constructor(props: Props) { super(props); @@ -93,7 +91,7 @@ export class ProjectOnboarding extends React.PureComponent { 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)); diff --git a/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboardingPage.tsx b/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboardingPage.tsx index d22ab7bcf52..04715cb8f19 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboardingPage.tsx +++ b/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboardingPage.tsx @@ -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 { - static contextTypes = { - router: PropTypes.object.isRequired - }; +type Props = DispatchProps & WithRouterProps; +export class ProjectOnboardingPage extends React.PureComponent { onSkipOnboardingTutorial = () => { this.props.skipOnboarding(); - this.context.router.replace('/'); + this.props.router.replace('/'); }; render() { @@ -44,7 +42,9 @@ export class ProjectOnboardingPage extends React.PureComponent { const mapDispatchToProps: DispatchProps = { skipOnboarding }; -export default connect( - null, - mapDispatchToProps -)(ProjectOnboardingPage); +export default withRouter( + connect( + null, + mapDispatchToProps + )(ProjectOnboardingPage) +); diff --git a/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/ProjectOnboarding-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/ProjectOnboarding-test.tsx index 96bbc79475a..2259a3460ee 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/ProjectOnboarding-test.tsx +++ b/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/ProjectOnboarding-test.tsx @@ -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).mockImplementation(() => 'SonarCloud'); (isSonarCloud as jest.Mock).mockImplementation(() => true); const wrapper = shallow( - + ); expect(wrapper).toMatchSnapshot(); @@ -73,7 +79,12 @@ it('finishes', () => { (isSonarCloud as jest.Mock).mockImplementation(() => false); const onFinish = jest.fn(); const wrapper = shallow( - + ); click(wrapper.find('ResetButtonLink')); return doAsync(() => { diff --git a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx index e833d41cd55..5cec1105cc4 100644 --- a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx +++ b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx @@ -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; organizationsEnabled?: boolean; + router: Pick; } interface State { @@ -43,13 +43,8 @@ interface State { users: T.User[]; } -export default class UsersApp extends React.PureComponent { +export class UsersApp extends React.PureComponent { 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 { updateQuery = (newQuery: Partial) => { 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 { ); } } + +export default withRouter(UsersApp); diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-test.tsx b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-test.tsx index 5adb4d2dae6..b706ad6a175 100644 --- a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-test.tsx @@ -19,10 +19,10 @@ */ /* eslint-disable import/order */ import * as React from 'react'; -import { Location } from 'history'; import { shallow } from 'enzyme'; -import UsersApp from '../UsersApp'; +import { UsersApp } from '../UsersApp'; import { waitAndUpdate } from '../../../helpers/testUtils'; +import { Location } from '../../../components/hoc/withRouter'; jest.mock('../../../api/users', () => ({ getIdentityProviders: jest.fn(() => @@ -77,12 +77,13 @@ it('should render correctly', async () => { expect(wrapper).toMatchSnapshot(); }); -function getWrapper(props = {}) { +function getWrapper(props: Partial = {}) { return shallow( , { diff --git a/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx b/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx index 77630d3f8a4..437b2b78fbd 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx @@ -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 { +class WebApiApp extends React.PureComponent { 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 { updateQuery = (newQuery: Partial) => { 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 { 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 { ); } } + +export default withRouter(WebApiApp); diff --git a/server/sonar-web/src/main/js/components/docs/DocLink.tsx b/server/sonar-web/src/main/js/components/docs/DocLink.tsx index f091b342294..c632c7eac07 100644 --- a/server/sonar-web/src/main/js/components/docs/DocLink.tsx +++ b/server/sonar-web/src/main/js/components/docs/DocLink.tsx @@ -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; 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 { - static contextTypes = { - canAdmin: () => null - }; - +export class DocLink extends React.PureComponent { handleClickOnAnchor = (event: React.MouseEvent) => { const { customProps, href = '#' } = this.props; if (customProps && customProps.onAnchorClick) { @@ -63,7 +61,7 @@ export default class DocLink extends React.PureComponent { return {children}; } else if (href.startsWith(SONARQUBE_ADMIN_LINK)) { return ( - + {children} ); @@ -91,6 +89,8 @@ export default class DocLink extends React.PureComponent { } } +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; } diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/DocLink-test.tsx b/server/sonar-web/src/main/js/components/docs/__tests__/DocLink-test.tsx index 54871ff53f3..d2f06d237fb 100644 --- a/server/sonar-web/src/main/js/components/docs/__tests__/DocLink-test.tsx +++ b/server/sonar-web/src/main/js/components/docs/__tests__/DocLink-test.tsx @@ -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(link text)).toMatchSnapshot(); + expect( + shallow( + + link text + + ) + ).toMatchSnapshot(); }); it('should render documentation link', () => { - expect(shallow(link text)).toMatchSnapshot(); + expect( + shallow( + + link text + + ) + ).toMatchSnapshot(); }); it('should render sonarcloud link on sonarcloud', () => { (isSonarCloud as jest.Mock).mockImplementationOnce(() => true); - const wrapper = shallow(link text); + const wrapper = shallow( + + link text + + ); 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(link text); + const wrapper = shallow( + + link text + + ); expect(wrapper.find('SonarCloudLink').dive()).toMatchSnapshot(); }); it('should render sonarqube link on sonarqube', () => { - const wrapper = shallow(link text); + const wrapper = shallow( + + link text + + ); 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(link text); + const wrapper = shallow( + + link text + + ); expect(wrapper.find('SonarQubeLink').dive()).toMatchSnapshot(); }); it('should render sonarqube admin link on sonarqube for admin', () => { - const wrapper = shallow(link text, { - context: { canAdmin: true } - }); + const wrapper = shallow( + + link text + + ); expect(wrapper).toMatchSnapshot(); expect(wrapper.find('SonarQubeAdminLink').dive()).toMatchSnapshot(); }); it('should not render sonarqube admin link on sonarqube for non-admin', () => { - const wrapper = shallow(link text); + const wrapper = shallow( + + link text + + ); expect(wrapper.find('SonarQubeAdminLink').dive()).toMatchSnapshot(); }); it('should not render sonarqube admin link on sonarcloud', () => { (isSonarCloud as jest.Mock).mockImplementationOnce(() => true); - const wrapper = shallow(link text, { - context: { canAdmin: true } - }); + const wrapper = shallow( + + link text + + ); expect(wrapper.find('SonarQubeAdminLink').dive()).toMatchSnapshot(); }); it.skip('should render documentation anchor', () => { - expect(shallow(link text)).toMatchSnapshot(); + expect( + shallow( + + link text + + ) + ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocLink-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocLink-test.tsx.snap index 7b07cbda797..94a51e8b63b 100644 --- a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocLink-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocLink-test.tsx.snap @@ -26,6 +26,11 @@ exports[`should not render sonarqube link on sonarcloud 1`] = ` exports[`should render documentation link 1`] = ` ({ - currentUser: getCurrentUser(state) -}); +export type Location = WithRouterProps['location']; +export type Router = WithRouterProps['router']; -const mapDispatchToProps = { onFail: addGlobalErrorMessage }; +interface InjectedProps { + location?: Partial; + router?: Partial; +} -export default connect( - mapStateToProps, - mapDispatchToProps -)(Extension); +export function withRouter

        ( + WrappedComponent: React.ComponentClass

        +): React.ComponentClass, S> { + return originalWithRouter(WrappedComponent as any); +} diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.tsx b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.tsx index 177af3e2515..0fffacc9150 100644 --- a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.tsx +++ b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.tsx @@ -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; } 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 { - static contextTypes = { - router: PropTypes.object - }; - +class PreviewGraph extends React.PureComponent { constructor(props: Props) { super(props); const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM); @@ -140,7 +137,7 @@ export default class PreviewGraph extends React.PureComponent { }; 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 { ); } } + +export default withRouter(PreviewGraph); -- 2.39.5