diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2018-12-06 15:53:55 +0100 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-12-11 20:20:58 +0100 |
commit | ce80f2115245688992a727beeecd46ac43dca703 (patch) | |
tree | 93d7716eb310d5baad9c5b58bd84073774b3f4a2 /server/sonar-web | |
parent | 0a3fef5c6baba580a558b996bd23b435dfc9e4aa (diff) | |
download | sonarqube-ce80f2115245688992a727beeecd46ac43dca703.tar.gz sonarqube-ce80f2115245688992a727beeecd46ac43dca703.zip |
remove some usages of legacy react context
Diffstat (limited to 'server/sonar-web')
83 files changed, 787 insertions, 873 deletions
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<T.AppState, 'adminPages' | 'organizationsEnabled'>; + appState: Pick<T.AppState, 'adminPages' | 'canAdmin' | 'organizationsEnabled'>; } interface DispatchToProps { @@ -50,18 +49,13 @@ interface State { class AdminContainer extends React.PureComponent<Props, State> { mounted = false; - - static contextTypes = { - canAdmin: PropTypes.bool.isRequired - }; - state: State = { pendingPlugins: defaultPendingPlugins }; componentDidMount() { this.mounted = true; - if (!this.context.canAdmin) { + if (!this.props.appState.canAdmin) { handleRequiredAuthorization(); } else { this.fetchNavigationSettings(); 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<T.AppState, 'organizationsEnabled'>; children: any; fetchOrganizations: (organizations: string[]) => void; location: { @@ -66,15 +67,7 @@ const FETCH_STATUS_WAIT_TIME = 3000; export class ComponentContainer extends React.PureComponent<Props, State> { watchStatusTimer?: number; mounted = false; - - static contextTypes = { - organizationsEnabled: PropTypes.bool - }; - - constructor(props: Props) { - super(props); - this.state = { branchLikes: [], isPending: false, loading: true, warnings: [] }; - } + state: State = { branchLikes: [], isPending: false, loading: true, warnings: [] }; componentDidMount() { this.mounted = true; @@ -122,7 +115,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> { .then(([nav, data]) => { const component = this.addQualifier({ ...nav, ...data }); - if (this.context.organizationsEnabled) { + if (this.props.appState.organizationsEnabled) { this.props.fetchOrganizations([component.organization]); } return component; @@ -382,9 +375,13 @@ export class ComponentContainer extends React.PureComponent<Props, State> { } } +const mapStateToProps = (state: Store) => ({ + appState: getAppState(state) +}); + const mapDispatchToProps = { fetchOrganizations }; export default connect( - null, + mapStateToProps, mapDispatchToProps )(ComponentContainer); 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 = <GlobalFooterContainer /> } = props; return ( <SuggestionsProvider> - {({ suggestions }) => ( - <StartupModal> - <div className="global-container"> - <div className="page-wrapper" id="container"> - <div className="page-container"> - <Workspace> - <GlobalNav location={props.location} suggestions={suggestions} /> - <GlobalMessagesContainer /> - {props.children} - </Workspace> - </div> + <StartupModal> + <div className="global-container"> + <div className="page-wrapper" id="container"> + <div className="page-container"> + <Workspace> + <GlobalNav location={props.location} /> + <GlobalMessagesContainer /> + {props.children} + </Workspace> </div> - {footer} </div> - </StartupModal> - )} + {footer} + </div> + </StartupModal> </SuggestionsProvider> ); } 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<StateProps & OwnProps> { - static contextTypes = { - router: PropTypes.object.isRequired - }; - +class Landing extends React.PureComponent<StateProps & OwnProps & WithRouterProps> { componentDidMount() { const { currentUser } = this.props; if (currentUser && isLoggedIn(currentUser)) { if (currentUser.homepage) { const homepage = getHomePageUrl(currentUser.homepage); - this.context.router.replace(homepage); + this.props.router.replace(homepage); } else { - this.context.router.replace('/projects'); + this.props.router.replace('/projects'); } } else { - this.context.router.replace('/about'); + this.props.router.replace('/about'); } } @@ -61,4 +57,4 @@ const mapStateToProps = (state: Store) => ({ currentUser: getCurrentUser(state) }); -export default connect(mapStateToProps)(Landing); +export default withRouter(connect(mapStateToProps)(Landing)); 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<ComponentContainer>( - <ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'foo' } }}> + <ComponentContainer + appState={{ organizationsEnabled: false }} + fetchOrganizations={jest.fn()} + location={{ query: { id: 'foo' } }}> <Inner /> </ComponentContainer> ); @@ -100,7 +103,10 @@ it("loads branches for module's project", async () => { }); mount( - <ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'moduleKey' } }}> + <ComponentContainer + appState={{ organizationsEnabled: false }} + fetchOrganizations={jest.fn()} + location={{ query: { id: 'moduleKey' } }}> <Inner /> </ComponentContainer> ); @@ -114,7 +120,10 @@ it("loads branches for module's project", async () => { it("doesn't load branches portfolio", async () => { const wrapper = mount( - <ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'portfolioKey' } }}> + <ComponentContainer + appState={{ organizationsEnabled: false }} + fetchOrganizations={jest.fn()} + location={{ query: { id: 'portfolioKey' } }}> <Inner /> </ComponentContainer> ); @@ -130,7 +139,10 @@ it("doesn't load branches portfolio", async () => { it('updates branches on change', () => { const wrapper = shallow( - <ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'portfolioKey' } }}> + <ComponentContainer + appState={{ organizationsEnabled: false }} + fetchOrganizations={jest.fn()} + location={{ query: { id: 'portfolioKey' } }}> <Inner /> </ComponentContainer> ); @@ -156,6 +168,7 @@ it('updates the branch measures', async () => { (getPullRequests as jest.Mock<any>).mockResolvedValueOnce([]); const wrapper = shallow( <ComponentContainer + appState={{ organizationsEnabled: false }} fetchOrganizations={jest.fn()} location={{ query: { id: 'foo', branch: 'feature' } }}> <Inner /> @@ -184,10 +197,12 @@ it('loads organization', async () => { const fetchOrganizations = jest.fn(); mount( - <ComponentContainer fetchOrganizations={fetchOrganizations} location={{ query: { id: 'foo' } }}> + <ComponentContainer + appState={{ organizationsEnabled: true }} + fetchOrganizations={fetchOrganizations} + location={{ query: { id: 'foo' } }}> <Inner /> - </ComponentContainer>, - { context: { organizationsEnabled: true } } + </ComponentContainer> ); await new Promise(setImmediate); @@ -198,10 +213,12 @@ it('fetches status', async () => { (getComponentData as jest.Mock<any>).mockResolvedValueOnce({ organization: 'org' }); mount( - <ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'foo' } }}> + <ComponentContainer + appState={{ organizationsEnabled: true }} + fetchOrganizations={jest.fn()} + location={{ query: { id: 'foo' } }}> <Inner /> - </ComponentContainer>, - { context: { organizationsEnabled: true } } + </ComponentContainer> ); await new Promise(setImmediate); @@ -210,7 +227,10 @@ it('fetches status', async () => { it('filters correctly the pending tasks for a main branch', () => { const wrapper = shallow( - <ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'foo' } }}> + <ComponentContainer + appState={{ organizationsEnabled: false }} + fetchOrganizations={jest.fn()} + location={{ query: { id: 'foo' } }}> <Inner /> </ComponentContainer> ); @@ -275,7 +295,10 @@ it('reload component after task progress finished', async () => { const inProgressTask = { id: 'foo', status: STATUSES.IN_PROGRESS } as T.Task; (getTasksForComponent as jest.Mock<any>).mockResolvedValueOnce({ queue: [inProgressTask] }); const wrapper = shallow( - <ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'foo' } }}> + <ComponentContainer + appState={{ organizationsEnabled: false }} + fetchOrganizations={jest.fn()} + location={{ query: { id: 'foo' } }}> <Inner /> </ComponentContainer> ); 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<SuggestionLink>; } export default class EmbedDocsPopup extends React.PureComponent<Props> { @@ -36,14 +35,14 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> { return <li className="menu-header">{text}</li>; } - renderSuggestions() { - if (this.props.suggestions.length === 0) { + renderSuggestions = ({ suggestions }: { suggestions: T.SuggestionLink[] }) => { + if (suggestions.length === 0) { return null; } return ( <> {this.renderTitle(translate('embed_docs.suggestion'))} - {this.props.suggestions.map((suggestion, index) => ( + {suggestions.map((suggestion, index) => ( <li key={index}> <Link onClick={this.props.onClose} target="_blank" to={suggestion.link}> {suggestion.text} @@ -53,7 +52,7 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> { <li className="divider" /> </> ); - } + }; renderIconLink(link: string, icon: string, text: string) { return ( @@ -138,7 +137,7 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> { return ( <DropdownOverlay> <ul className="menu abs-width-240"> - {this.renderSuggestions()} + <SuggestionsContext.Consumer>{this.renderSuggestions}</SuggestionsContext.Consumer> <li> <Link onClick={this.props.onClose} target="_blank" to="/documentation"> {translate('embed_docs.documentation')} 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<SuggestionLink>; -} interface State { helpOpen: boolean; } -export default class EmbedDocsPopupHelper extends React.PureComponent<Props, State> { +export default class EmbedDocsPopupHelper extends React.PureComponent<{}, State> { mounted = false; state: State = { helpOpen: false }; @@ -81,9 +77,7 @@ export default class EmbedDocsPopupHelper extends React.PureComponent<Props, Sta <Toggler onRequestClose={this.closeHelp} open={this.state.helpOpen} - overlay={ - <EmbedDocsPopup onClose={this.closeHelp} suggestions={this.props.suggestions} /> - }> + overlay={<EmbedDocsPopup onClose={this.closeHelp} />}> <a className="navbar-help" href="#" onClick={this.handleClick} title={translate('help')}> <HelpIcon /> </a> 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<Props> { - context!: { suggestions: SuggestionsContext }; +export default function Suggestions({ suggestions }: Props) { + return ( + <SuggestionsContext.Consumer> + {({ addSuggestions, removeSuggestions }) => ( + <SuggestionsInner + addSuggestions={addSuggestions} + removeSuggestions={removeSuggestions} + suggestions={suggestions} + /> + )} + </SuggestionsContext.Consumer> + ); +} - static contextTypes = { - suggestions: PropTypes.object.isRequired - }; +interface SuggestionsInnerProps { + addSuggestions: (key: string) => void; + removeSuggestions: (key: string) => void; + suggestions: string; +} +class SuggestionsInner extends React.PureComponent<SuggestionsInnerProps> { componentDidMount() { - this.context.suggestions.addSuggestions(this.props.suggestions); + this.props.addSuggestions(this.props.suggestions); } componentDidUpdate(prevProps: Props) { if (prevProps.suggestions !== this.props.suggestions) { - this.context.suggestions.removeSuggestions(this.props.suggestions); - this.context.suggestions.addSuggestions(prevProps.suggestions); + this.props.removeSuggestions(this.props.suggestions); + this.props.addSuggestions(prevProps.suggestions); } } componentWillUnmount() { - this.context.suggestions.removeSuggestions(this.props.suggestions); + this.props.removeSuggestions(this.props.suggestions); } render() { 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<SuggestionsContextShape>({ + 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<Props, State> { +export default class SuggestionsProvider extends React.Component<{}, State> { keys: string[] = []; - - static childContextTypes = { - suggestions: PropTypes.object - }; - state: State = { suggestions: [] }; - getChildContext = (): { suggestions: SuggestionsContext } => { - return { - suggestions: { - addSuggestions: this.addSuggestions, - removeSuggestions: this.removeSuggestions - } - }; - }; - fetchSuggestions = () => { const jsonList = suggestionsJson as SuggestionsJson; - let suggestions: SuggestionLink[] = []; + let suggestions: T.SuggestionLink[] = []; this.keys.forEach(key => { if (jsonList[key]) { suggestions = [...jsonList[key], ...suggestions]; } }); + if (!isSonarCloud()) { + suggestions = suggestions.filter(suggestion => suggestion.scope !== 'sonarcloud'); + } this.setState({ suggestions }); }; @@ -82,10 +59,15 @@ export default class SuggestionsProvider extends React.Component<Props, State> { }; render() { - const suggestions = isSonarCloud() - ? this.state.suggestions - : this.state.suggestions.filter(suggestion => suggestion.scope !== 'sonarcloud'); - - return this.props.children({ suggestions }); + return ( + <SuggestionsContext.Provider + value={{ + addSuggestions: this.addSuggestions, + removeSuggestions: this.removeSuggestions, + suggestions: this.state.suggestions + }}> + {this.props.children} + </SuggestionsContext.Provider> + ); } } 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(<EmbedDocsPopup onClose={jest.fn()} suggestions={suggestions} />, { - context - }); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); -}); - -it('should display correct links for SonarCloud', () => { - (isSonarCloud as jest.Mock<any>).mockReturnValueOnce(true); - const context = {}; - const wrapper = shallow(<EmbedDocsPopup onClose={jest.fn()} suggestions={suggestions} />, { - context - }); - wrapper.update(); +it('should render', () => { + const wrapper = shallow(<EmbedDocsPopup onClose={jest.fn()} />); expect(wrapper).toMatchSnapshot(); }); 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(<SuggestionsProvider>{children}</SuggestionsProvider>); - const instance = wrapper.instance() as SuggestionsProvider; - expect(children).lastCalledWith({ suggestions: [] }); + (isSonarCloud as jest.Mock).mockReturnValue(false); + const wrapper = shallow<SuggestionsProvider>( + <SuggestionsProvider> + <div /> + </SuggestionsProvider> + ); + const instance = wrapper.instance(); + expect(wrapper.state('suggestions')).toEqual([]); instance.addSuggestions('pageA'); - expect(children).lastCalledWith({ suggestions: [{ link: '/foo', text: 'Foo' }] }); + expect(wrapper.state('suggestions')).toEqual([{ link: '/foo', text: 'Foo' }]); instance.addSuggestions('pageB'); - expect(children).lastCalledWith({ - suggestions: [{ link: '/qux', text: 'Qux' }, { link: '/foo', text: 'Foo' }] - }); + expect(wrapper.state('suggestions')).toEqual([ + { link: '/qux', text: 'Qux' }, + { link: '/foo', text: 'Foo' } + ]); instance.removeSuggestions('pageA'); - expect(children).lastCalledWith({ suggestions: [{ link: '/qux', text: 'Qux' }] }); + expect(wrapper.state('suggestions')).toEqual([{ link: '/qux', text: 'Qux' }]); }); it('should show sonarcloud pages', () => { - (isSonarCloud as jest.Mock).mockImplementation(() => true); - const children = jest.fn(); - const wrapper = shallow(<SuggestionsProvider>{children}</SuggestionsProvider>); - const instance = wrapper.instance() as SuggestionsProvider; - expect(children).lastCalledWith({ suggestions: [] }); + (isSonarCloud as jest.Mock).mockReturnValue(true); + const wrapper = shallow<SuggestionsProvider>( + <SuggestionsProvider> + <div /> + </SuggestionsProvider> + ); + const instance = wrapper.instance(); + expect(wrapper.state('suggestions')).toEqual([]); instance.addSuggestions('pageA'); - expect(children).lastCalledWith({ - suggestions: [{ link: '/foo', text: 'Foo' }, { link: '/bar', text: 'Bar', scope: 'sonarcloud' }] - }); + expect(wrapper.state('suggestions')).toEqual([ + { link: '/foo', text: 'Foo' }, + { link: '/bar', text: 'Bar', scope: 'sonarcloud' } + ]); }); 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`] = ` <DropdownOverlay> <ul className="menu abs-width-240" > - <li - className="menu-header" - > - embed_docs.suggestion - </li> - <li - key="0" - > - <Link - onClick={[MockFunction]} - onlyActiveOnIndex={false} - style={Object {}} - target="_blank" - to="#" - > - foo - </Link> - </li> - <li - key="1" - > - <Link - onClick={[MockFunction]} - onlyActiveOnIndex={false} - style={Object {}} - target="_blank" - to="#" - > - bar - </Link> - </li> - <li - className="divider" - /> - <li> - <Link - onClick={[MockFunction]} - onlyActiveOnIndex={false} - style={Object {}} - target="_blank" - to="/documentation" - > - embed_docs.documentation - </Link> - </li> - <li> - <Link - onClick={[MockFunction]} - onlyActiveOnIndex={false} - style={Object {}} - to="/web_api" - > - api_documentation.page - </Link> - </li> - <li - className="divider" - /> - <li> - <a - href="https://community.sonarsource.com/c/help/sc" - rel="noopener noreferrer" - target="_blank" - > - embed_docs.get_help - </a> - </li> - <li - className="divider" - /> - <li - className="menu-header" - > - embed_docs.stay_connected - </li> - <li> - <a - href="https://twitter.com/sonarcloud" - rel="noopener noreferrer" - target="_blank" - > - <img - alt="Twitter" - className="spacer-right" - height="18" - src="/images/embed-doc/twitter-icon.svg" - width="18" - /> - Twitter - </a> - </li> - <li> - <a - href="https://blog.sonarsource.com/product/SonarCloud" - rel="noopener noreferrer" - target="_blank" - > - <img - alt="embed_docs.news" - className="spacer-right" - height="18" - src="/images/sonarcloud-square-logo.svg" - width="18" - /> - embed_docs.news - </a> - </li> - <li> - <Connect(ProductNewsMenuItem) - tag="SonarCloud" - /> - </li> - </ul> -</DropdownOverlay> -`; - -exports[`should display suggestion links 1`] = ` -<DropdownOverlay> - <ul - className="menu abs-width-240" - > - <li - className="menu-header" - > - embed_docs.suggestion - </li> - <li - key="0" - > - <Link - onClick={[MockFunction]} - onlyActiveOnIndex={false} - style={Object {}} - target="_blank" - to="#" - > - foo - </Link> - </li> - <li - key="1" - > - <Link - onClick={[MockFunction]} - onlyActiveOnIndex={false} - style={Object {}} - target="_blank" - to="#" - > - bar - </Link> - </li> - <li - className="divider" - /> + <ContextConsumer> + <Component /> + </ContextConsumer> <li> <Link onClick={[MockFunction]} diff --git a/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx b/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx index b3e6c9343ec..c02b5962601 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx @@ -19,29 +19,38 @@ */ import * as React from 'react'; import Helmet from 'react-helmet'; -import * as PropTypes from 'prop-types'; import { withRouter, WithRouterProps } from 'react-router'; import { injectIntl, InjectedIntlProps } from 'react-intl'; +import { connect } from 'react-redux'; import { getExtensionStart } from './utils'; import { translate } from '../../../helpers/l10n'; import getStore from '../../utils/getStore'; +import { addGlobalErrorMessage } from '../../../store/globalMessages'; +import { Store, getCurrentUser } from '../../../store/rootReducer'; interface OwnProps { - currentUser: T.CurrentUser; extension: { key: string; name: string }; - onFail: (message: string) => void; options?: {}; } -type Props = OwnProps & WithRouterProps & InjectedIntlProps; +interface StateProps { + currentUser: T.CurrentUser; +} -class Extension extends React.PureComponent<Props> { +interface DispatchProps { + onFail: (message: string) => void; +} + +type Props = OwnProps & WithRouterProps & InjectedIntlProps & StateProps & DispatchProps; + +interface State { + extensionElement?: React.ReactElement<any>; +} + +class Extension extends React.PureComponent<Props, State> { container?: HTMLElement | null; stop?: Function; - - static contextTypes = { - suggestions: PropTypes.object.isRequired - }; + state: State = {}; componentDidMount() { this.startExtension(); @@ -62,16 +71,21 @@ class Extension extends React.PureComponent<Props> { handleStart = (start: Function) => { const store = getStore(); - this.stop = start({ + const result = start({ store, el: this.container, currentUser: this.props.currentUser, intl: this.props.intl, location: this.props.location, router: this.props.router, - suggestions: this.context.suggestions, ...this.props.options }); + + if (React.isValidElement(result)) { + this.setState({ extensionElement: result }); + } else { + this.stop = result; + } }; handleFailure = () => { @@ -94,10 +108,27 @@ class Extension extends React.PureComponent<Props> { return ( <div> <Helmet title={this.props.extension.name} /> - <div ref={container => (this.container = container)} /> + {this.state.extensionElement ? ( + this.state.extensionElement + ) : ( + <div ref={container => (this.container = container)} /> + )} </div> ); } } -export default injectIntl(withRouter(Extension)); +function mapStateToProps(state: Store): StateProps { + return { currentUser: getCurrentUser(state) }; +} + +const mapDispatchToProps: DispatchProps = { onFail: addGlobalErrorMessage }; + +export default injectIntl<OwnProps & InjectedIntlProps>( + withRouter( + connect( + mapStateToProps, + mapDispatchToProps + )(Extension) + ) +); 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 ? ( - <ExtensionContainer extension={extension} /> - ) : ( - <NotFound withContainer={false} /> - ); + return extension ? <Extension extension={extension} /> : <NotFound withContainer={false} />; } 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 ? ( - <ExtensionContainer extension={extension} /> - ) : ( - <NotFound withContainer={false} /> - ); + return extension ? <Extension extension={extension} /> : <NotFound withContainer={false} />; } 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<Props> { const extension = pages.find(p => p.key === `${pluginKey}/${extensionKey}`); return extension ? ( - <ExtensionContainer + <Extension extension={extension} options={{ organization, refreshOrganization: this.refreshOrganization }} /> 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 ? ( - <ExtensionContainer extension={extension} options={{ component }} /> + <Extension extension={extension} options={{ component }} /> ) : ( <NotFound withContainer={false} /> ); 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 ? ( - <ExtensionContainer extension={extension} options={{ component }} /> + <Extension extension={extension} options={{ component }} /> ) : ( <NotFound withContainer={false} /> ); 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<T.AppState, 'branchesEnabled'>; branchLikes: T.BranchLike[]; component: T.Component; currentBranchLike: T.BranchLike; @@ -50,17 +51,9 @@ interface State { dropdownOpen: boolean; } -export default class ComponentNavBranch extends React.PureComponent<Props, State> { +export class ComponentNavBranch extends React.PureComponent<Props, State> { mounted = false; - - static contextTypes = { - branchesEnabled: PropTypes.bool.isRequired, - canAdmin: PropTypes.bool.isRequired - }; - - state: State = { - dropdownOpen: false - }; + state: State = { dropdownOpen: false }; componentDidMount() { this.mounted = true; @@ -145,7 +138,7 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State const { branchLikes, currentBranchLike } = this.props; const { configuration, breadcrumbs } = this.props.component; - if (isSonarCloud() && !this.context.branchesEnabled) { + if (isSonarCloud() && !this.props.appState.branchesEnabled) { return null; } @@ -170,7 +163,7 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State </div> ); } else { - if (!this.context.branchesEnabled) { + if (!this.props.appState.branchesEnabled) { return ( <div className="navbar-context-branches"> <BranchIcon @@ -235,3 +228,5 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State ); } } + +export default withAppState(ComponentNavBranch); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx index 5f876d1a6f6..ca027fdfa48 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.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 ComponentNavBranchesMenuItem from './ComponentNavBranchesMenuItem'; import { @@ -36,6 +35,7 @@ import { getBranchLikeUrl } from '../../../../helpers/urls'; import SearchBox from '../../../../components/controls/SearchBox'; import HelpTooltip from '../../../../components/controls/HelpTooltip'; import { DropdownOverlay } from '../../../../components/controls/Dropdown'; +import { withRouter, Router } from '../../../../components/hoc/withRouter'; interface Props { branchLikes: T.BranchLike[]; @@ -43,6 +43,7 @@ interface Props { component: T.Component; currentBranchLike: T.BranchLike; onClose: () => void; + router: Pick<Router, 'push'>; } interface State { @@ -50,14 +51,9 @@ interface State { selected: T.BranchLike | undefined; } -export default class ComponentNavBranchesMenu extends React.PureComponent<Props, State> { - private listNode?: HTMLUListElement | null; - private selectedBranchNode?: HTMLLIElement | null; - - static contextTypes = { - router: PropTypes.object - }; - +export class ComponentNavBranchesMenu extends React.PureComponent<Props, State> { + listNode?: HTMLUListElement | null; + selectedBranchNode?: HTMLLIElement | null; state: State = { query: '', selected: undefined }; componentDidMount() { @@ -113,7 +109,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, openSelected = () => { const selected = this.getSelected(); if (selected) { - this.context.router.push(this.getProjectBranchUrl(selected)); + this.props.router.push(this.getProjectBranchUrl(selected)); } }; @@ -263,3 +259,5 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, ); } } + +export default withRouter(ComponentNavBranchesMenu); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx index 116cb1489fa..bcc3ac9f057 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavLicenseNotif.tsx @@ -19,12 +19,13 @@ */ import * as React from 'react'; import { Link } from 'react-router'; -import * as PropTypes from 'prop-types'; import NavBarNotif from '../../../../components/nav/NavBarNotif'; import { translate } from '../../../../helpers/l10n'; import { isValidLicense } from '../../../../api/marketplace'; +import { withAppState } from '../../../../components/withAppState'; interface Props { + appState: Pick<T.AppState, 'canAdmin'>; currentTask?: T.Task; } @@ -33,13 +34,8 @@ interface State { loading: boolean; } -export default class ComponentNavLicenseNotif extends React.PureComponent<Props, State> { +export class ComponentNavLicenseNotif extends React.PureComponent<Props, State> { mounted = false; - - static contextTypes = { - canAdmin: PropTypes.bool.isRequired - }; - state: State = { loading: false }; componentDidMount() { @@ -88,7 +84,7 @@ export default class ComponentNavLicenseNotif extends React.PureComponent<Props, return ( <NavBarNotif variant="error"> <span className="little-spacer-right">{currentTask.errorMessage}</span> - {this.context.canAdmin ? ( + {this.props.appState.canAdmin ? ( <Link to="/admin/extension/license/app"> {translate('license.component_navigation.button', currentTask.errorType)}. </Link> @@ -99,3 +95,5 @@ export default class ComponentNavLicenseNotif extends React.PureComponent<Props, ); } } + +export default withAppState(ComponentNavLicenseNotif); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx index 1499680b2a8..7aeec61ee84 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx @@ -20,7 +20,6 @@ import * as React from 'react'; import { Link } from 'react-router'; import * as classNames from 'classnames'; -import * as PropTypes from 'prop-types'; import Dropdown from '../../../../components/controls/Dropdown'; import NavBarTabs from '../../../../components/nav/NavBarTabs'; import { @@ -31,6 +30,7 @@ import { } from '../../../../helpers/branches'; import { translate } from '../../../../helpers/l10n'; import DropdownIcon from '../../../../components/icons-components/DropdownIcon'; +import { withAppState } from '../../../../components/withAppState'; const SETTINGS_URLS = [ '/project/admin', @@ -49,16 +49,13 @@ const SETTINGS_URLS = [ ]; interface Props { + appState: Pick<T.AppState, 'branchesEnabled'>; branchLike: T.BranchLike | undefined; component: T.Component; location?: any; } -export default class ComponentNavMenu extends React.PureComponent<Props> { - static contextTypes = { - branchesEnabled: PropTypes.bool.isRequired - }; - +export class ComponentNavMenu extends React.PureComponent<Props> { isProject() { return this.props.component.qualifier === 'TRK'; } @@ -282,7 +279,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { renderBranchesLink() { if ( - !this.context.branchesEnabled || + !this.props.appState.branchesEnabled || !this.isProject() || !this.getConfiguration().showSettings ) { @@ -504,3 +501,5 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { ); } } + +export default withAppState(ComponentNavMenu); 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( <ComponentNavBranch + appState={{ branchesEnabled: true }} branchLikes={[mainBranch, fooBranch]} component={component} currentBranchLike={mainBranch} - />, - { context: { branchesEnabled: true, canAdmin: true } } + /> ) ).toMatchSnapshot(); }); @@ -58,11 +58,11 @@ it('renders short-living branch', () => { expect( shallow( <ComponentNavBranch + appState={{ branchesEnabled: true }} branchLikes={[branch, fooBranch]} component={component} currentBranchLike={branch} - />, - { context: { branchesEnabled: true, canAdmin: true } } + /> ) ).toMatchSnapshot(); }); @@ -79,11 +79,11 @@ it('renders pull request', () => { expect( shallow( <ComponentNavBranch + appState={{ branchesEnabled: true }} branchLikes={[pullRequest, fooBranch]} component={component} currentBranchLike={pullRequest} - />, - { context: { branchesEnabled: true, canAdmin: true } } + /> ) ).toMatchSnapshot(); }); @@ -92,11 +92,11 @@ it('opens menu', () => { const component = {} as T.Component; const wrapper = shallow( <ComponentNavBranch + appState={{ branchesEnabled: true }} branchLikes={[mainBranch, fooBranch]} component={component} currentBranchLike={mainBranch} - />, - { context: { branchesEnabled: true, canAdmin: true } } + /> ); expect(wrapper.find('Toggler').prop('open')).toBe(false); click(wrapper.find('a')); @@ -107,11 +107,11 @@ it('renders single branch popup', () => { const component = {} as T.Component; const wrapper = shallow( <ComponentNavBranch + appState={{ branchesEnabled: true }} branchLikes={[mainBranch]} component={component} currentBranchLike={mainBranch} - />, - { context: { branchesEnabled: true, canAdmin: true } } + /> ); expect(wrapper.find('DocTooltip')).toMatchSnapshot(); }); @@ -120,11 +120,11 @@ it('renders no branch support popup', () => { const component = {} as T.Component; const wrapper = shallow( <ComponentNavBranch + appState={{ branchesEnabled: false }} branchLikes={[mainBranch, fooBranch]} component={component} currentBranchLike={mainBranch} - />, - { context: { branchesEnabled: false, canAdmin: true } } + /> ); expect(wrapper.find('DocTooltip')).toMatchSnapshot(); }); @@ -134,11 +134,11 @@ it('renders nothing on SonarCloud without branch support', () => { const component = {} as T.Component; const wrapper = shallow( <ComponentNavBranch + appState={{ branchesEnabled: false }} branchLikes={[mainBranch]} component={component} currentBranchLike={mainBranch} - />, - { context: { branchesEnabled: false, onSonarCloud: true, canAdmin: true } } + /> ); expect(wrapper.type()).toBeNull(); }); 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<any>).mockResolvedValueOnce({ isValidLicense: true }); const wrapper = getWrapper({ - currentTask: { status: 'FAILED', errorType: 'LICENSING', errorMessage: 'Foo' } + currentTask: { status: 'FAILED', errorType: 'LICENSING', errorMessage: 'Foo' } as T.Task }); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); @@ -64,18 +64,18 @@ it('renders a different message if the license is valid', async () => { it('renders correctly for LICENSING_LOC error', async () => { (isValidLicense as jest.Mock<any>).mockResolvedValueOnce({ isValidLicense: true }); const wrapper = getWrapper({ - currentTask: { status: 'FAILED', errorType: 'LICENSING_LOC', errorMessage: 'Foo' } + currentTask: { status: 'FAILED', errorType: 'LICENSING_LOC', errorMessage: 'Foo' } as T.Task }); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); }); -function getWrapper(props = {}, context = {}) { +function getWrapper(props: Partial<ComponentNavLicenseNotif['props']> = {}) { return shallow( <ComponentNavLicenseNotif + appState={{ canAdmin: true }} currentTask={{ errorMessage: 'Foo', errorType: 'LICENSING' } as T.Task} {...props} - />, - { context: { canAdmin: true, ...context } } + /> ); } 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(<ComponentNavMenu branchLike={mainBranch} component={component} />, { - context: { branchesEnabled: true } - }); + const wrapper = shallow( + <ComponentNavMenu + appState={{ branchesEnabled: true }} + branchLike={mainBranch} + component={component} + /> + ); expect(wrapper.find('Dropdown[data-test="extensions"]')).toMatchSnapshot(); expect(wrapper.find('Dropdown[data-test="administration"]')).toMatchSnapshot(); }); @@ -56,9 +60,13 @@ it('should work with multiple extensions', () => { { key: 'component-bar', name: 'ComponentBar' } ] }; - const wrapper = shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, { - context: { branchesEnabled: true } - }); + const wrapper = shallow( + <ComponentNavMenu + appState={{ branchesEnabled: true }} + branchLike={mainBranch} + component={component} + /> + ); expect(wrapper.find('Dropdown[data-test="extensions"]')).toMatchSnapshot(); expect(wrapper.find('Dropdown[data-test="administration"]')).toMatchSnapshot(); }); @@ -76,9 +84,13 @@ it('should work for short-living branches', () => { extensions: [{ key: 'component-foo', name: 'ComponentFoo' }] }; expect( - shallow(<ComponentNavMenu branchLike={branch} component={component} />, { - context: { branchesEnabled: true } - }) + shallow( + <ComponentNavMenu + appState={{ branchesEnabled: true }} + branchLike={branch} + component={component} + /> + ) ).toMatchSnapshot(); }); @@ -88,14 +100,14 @@ it('should work for long-living branches', () => { expect( shallow( <ComponentNavMenu + appState={{ branchesEnabled: true }} branchLike={branch} component={{ ...baseComponent, configuration: { showSettings }, extensions: [{ key: 'component-foo', name: 'ComponentFoo' }] }} - />, - { context: { branchesEnabled: true } } + /> ) ).toMatchSnapshot() ); @@ -108,9 +120,13 @@ it('should work for all qualifiers', () => { function checkWithQualifier(qualifier: string) { const component = { ...baseComponent, configuration: { showSettings: true }, qualifier }; expect( - shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, { - context: { branchesEnabled: true } - }) + shallow( + <ComponentNavMenu + appState={{ branchesEnabled: true }} + branchLike={mainBranch} + component={component} + /> + ) ).toMatchSnapshot(); } }); 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 []} /> </div> - <ComponentNavMenu + <Connect(withAppState(ComponentNavMenu)) component={ Object { "breadcrumbs": Array [ diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap index 1386465802b..1eb0a5d137b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBgTaskNotif-test.tsx.snap @@ -72,7 +72,7 @@ exports[`renders background task in progress info correctly 1`] = ` `; exports[`renders background task license info correctly 1`] = ` -<ComponentNavLicenseNotif +<Connect(withAppState(ComponentNavLicenseNotif)) currentTask={ Object { "errorMessage": "Foo", diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap index 86a8372f886..6bad17ad6b4 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap @@ -11,7 +11,7 @@ exports[`renders main branch 1`] = ` onRequestClose={[Function]} open={false} overlay={ - <ComponentNavBranchesMenu + <withRouter(ComponentNavBranchesMenu) branchLikes={ Array [ Object { @@ -88,7 +88,7 @@ exports[`renders pull request 1`] = ` onRequestClose={[Function]} open={false} overlay={ - <ComponentNavBranchesMenu + <withRouter(ComponentNavBranchesMenu) branchLikes={ Array [ Object { @@ -180,7 +180,7 @@ exports[`renders short-living branch 1`] = ` onRequestClose={[Function]} open={false} overlay={ - <ComponentNavBranchesMenu + <withRouter(ComponentNavBranchesMenu) branchLikes={ Array [ Object { diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx index e5b1ff86885..a136ce19518 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx @@ -30,7 +30,6 @@ import * as theme from '../../../theme'; import NavBar from '../../../../components/nav/NavBar'; import { lazyLoad } from '../../../../components/lazyLoad'; import { getCurrentUser, getAppState, Store } from '../../../../store/rootReducer'; -import { SuggestionLink } from '../../embed-docs-modal/SuggestionsProvider'; import { isSonarCloud } from '../../../../helpers/system'; import { isLoggedIn } from '../../../../helpers/users'; import './GlobalNav.css'; @@ -44,7 +43,6 @@ interface StateProps { interface OwnProps { location: { pathname: string }; - suggestions: Array<SuggestionLink>; } type Props = StateProps & OwnProps; @@ -62,7 +60,7 @@ export class GlobalNav extends React.PureComponent<Props> { <ul className="global-navbar-menu global-navbar-menu-right"> {isSonarCloud() && <GlobalNavExplore location={this.props.location} />} - <EmbedDocsPopupHelper suggestions={this.props.suggestions} /> + <EmbedDocsPopupHelper /> <Search appState={appState} currentUser={currentUser} /> {isLoggedIn(currentUser) && ( <GlobalNavPlus @@ -71,7 +69,7 @@ export class GlobalNav extends React.PureComponent<Props> { openProjectOnboarding={this.context.openProjectOnboarding} /> )} - <GlobalNavUserContainer {...this.props} /> + <GlobalNavUserContainer appState={appState} currentUser={currentUser} /> </ul> </NavBar> ); 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<Router, 'push'>; } -export default class GlobalNavUser extends React.PureComponent<Props> { - static contextTypes = { - router: PropTypes.object - }; - +export class GlobalNavUser extends React.PureComponent<Props> { handleLogin = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); const shouldReturnToCurrentPage = window.location.pathname !== `${getBaseUrl()}/about`; @@ -54,7 +51,7 @@ export default class GlobalNavUser extends React.PureComponent<Props> { handleLogout = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); - this.context.router.push('/sessions/logout'); + this.props.router.push('/sessions/logout'); }; renderAuthenticated() { @@ -126,3 +123,5 @@ export default class GlobalNavUser extends React.PureComponent<Props> { return isLoggedIn(this.props.currentUser) ? this.renderAuthenticated() : this.renderAnonymous(); } } + +export default withRouter(GlobalNavUser); 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( - <GlobalNav - appState={appState} - currentUser={{ isLoggedIn: false }} - location={location} - suggestions={[]} - /> + <GlobalNav appState={appState} currentUser={{ isLoggedIn: false }} location={location} /> ); expect(wrapper).toMatchSnapshot(); wrapper.setProps({ currentUser: { isLoggedIn: true } }); 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( - <GlobalNavUser appState={appState} currentUser={currentUser} organizations={[]} /> + <GlobalNavUser + appState={appState} + currentUser={currentUser} + organizations={[]} + router={{ push: jest.fn() }} + /> ); expect(wrapper).toMatchSnapshot(); }); it('should render the right interface for logged in user', () => { const wrapper = shallow( - <GlobalNavUser appState={appState} currentUser={currentUser} organizations={[]} /> + <GlobalNavUser + appState={appState} + currentUser={currentUser} + organizations={[]} + router={{ push: jest.fn() }} + /> ); wrapper.setState({ open: true }); expect(wrapper.find('Dropdown')).toMatchSnapshot(); @@ -47,7 +57,12 @@ it('should render the right interface for logged in user', () => { it('should render user organizations', () => { const wrapper = shallow( - <GlobalNavUser appState={appState} currentUser={currentUser} organizations={organizations} /> + <GlobalNavUser + appState={appState} + currentUser={currentUser} + organizations={organizations} + router={{ push: jest.fn() }} + /> ); wrapper.setState({ open: true }); expect(wrapper.find('Dropdown')).toMatchSnapshot(); @@ -59,6 +74,7 @@ it('should not render user organizations when they are not activated', () => { appState={{ organizationsEnabled: false }} currentUser={currentUser} organizations={organizations} + router={{ push: jest.fn() }} /> ); wrapper.setState({ open: true }); 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 []} /> <ul className="global-navbar-menu global-navbar-menu-right" @@ -38,9 +37,7 @@ exports[`should render for SonarCloud 1`] = ` } } /> - <EmbedDocsPopupHelper - suggestions={Array []} - /> + <EmbedDocsPopupHelper /> <withRouter(Search) appState={ Object { @@ -56,7 +53,7 @@ exports[`should render for SonarCloud 1`] = ` } } /> - <Connect(GlobalNavUser) + <Connect(withRouter(GlobalNavUser)) appState={ Object { "canAdmin": false, @@ -70,12 +67,6 @@ exports[`should render for SonarCloud 1`] = ` "isLoggedIn": false, } } - location={ - Object { - "pathname": "", - } - } - suggestions={Array []} /> </ul> </NavBar> @@ -107,14 +98,11 @@ exports[`should render for SonarQube 1`] = ` "pathname": "", } } - suggestions={Array []} /> <ul className="global-navbar-menu global-navbar-menu-right" > - <EmbedDocsPopupHelper - suggestions={Array []} - /> + <EmbedDocsPopupHelper /> <withRouter(Search) appState={ Object { @@ -130,7 +118,7 @@ exports[`should render for SonarQube 1`] = ` } } /> - <Connect(GlobalNavUser) + <Connect(withRouter(GlobalNavUser)) appState={ Object { "canAdmin": false, @@ -144,12 +132,6 @@ exports[`should render for SonarQube 1`] = ` "isLoggedIn": false, } } - location={ - Object { - "pathname": "", - } - } - suggestions={Array []} /> </ul> </NavBar> 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<T.AppState, 'organizationsEnabled'>; fetchOrganizations: (organizations: string[]) => void; } @@ -41,13 +42,8 @@ interface State { perProjectTypes: string[]; } -export default class Notifications extends React.PureComponent<Props, State> { +export class Notifications extends React.PureComponent<Props, State> { mounted = false; - - static contextTypes = { - organizationsEnabled: PropTypes.bool - }; - state: State = { channels: [], globalTypes: [], @@ -69,7 +65,7 @@ export default class Notifications extends React.PureComponent<Props, State> { api.getNotifications().then( response => { if (this.mounted) { - if (this.context.organizationsEnabled) { + if (this.props.appState.organizationsEnabled) { const organizations = uniq(response.notifications .filter(n => n.organization) .map(n => n.organization) as string[]); @@ -174,6 +170,8 @@ export default class Notifications extends React.PureComponent<Props, State> { } } +export default withAppState(Notifications); + function areNotificationsEqual(a: T.Notification, b: T.Notification) { return a.channel === b.channel && a.type === b.type && a.project === b.project; } 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<Props>, context?: any) { - const wrapper = shallow(<Notifications fetchOrganizations={jest.fn()} {...props} />, { context }); +async function shallowRender(props?: Partial<Notifications['props']>) { + const wrapper = shallow( + <Notifications + appState={{ organizationsEnabled: false }} + fetchOrganizations={jest.fn()} + {...props} + /> + ); await waitAndUpdate(wrapper); return wrapper; } 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<Props, State> { }; render() { - const { branchLike, component, location } = this.props; + const { branchLike, component } = this.props; const { loading, baseComponent, components, breadcrumbs, total, sourceViewer } = this.state; const shouldShowBreadcrumbs = breadcrumbs.length > 1; @@ -193,7 +193,7 @@ export class App extends React.PureComponent<Props, State> { <Suggestions suggestions="code" /> <Helmet title={sourceViewer !== undefined ? sourceViewer.name : defaultTitle} /> - <Search branchLike={branchLike} component={component} location={location} /> + <Search branchLike={branchLike} component={component} /> <div className="code-components"> {shouldShowBreadcrumbs && ( 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<Router, 'push'>; } interface State { @@ -40,13 +41,8 @@ interface State { selectedIndex?: number; } -export default class Search extends React.PureComponent<Props, State> { +class Search extends React.PureComponent<Props, State> { mounted = false; - - static contextTypes = { - router: PropTypes.object.isRequired - }; - state: State = { query: '', loading: false @@ -93,9 +89,9 @@ export default class Search extends React.PureComponent<Props, State> { const selected = results[selectedIndex]; if (selected.refKey) { - this.context.router.push(getProjectUrl(selected.refKey)); + this.props.router.push(getProjectUrl(selected.refKey)); } else { - this.context.router.push({ + this.props.router.push({ pathname: '/code', query: { id: component.key, selected: selected.key, ...getBranchLikeQuery(branchLike) } }); @@ -200,3 +196,5 @@ export default class Search extends React.PureComponent<Props, State> { ); } } + +export default withRouter(Search); 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<Props, State> { mounted = false; - static contextTypes = { - organizationsEnabled: PropTypes.bool - }; - constructor(props: Props) { super(props); this.state = { @@ -528,7 +525,7 @@ export class App extends React.PureComponent<Props, State> { onFilterChange={this.handleFilterChange} openFacets={this.state.openFacets} organization={organization} - organizationsEnabled={this.context.organizationsEnabled} + organizationsEnabled={this.props.appState.organizationsEnabled} query={this.state.query} referencedProfiles={this.state.referencedProfiles} referencedRepositories={this.state.referencedRepositories} @@ -572,7 +569,7 @@ export class App extends React.PureComponent<Props, State> { <div className="layout-page-main-inner"> {this.state.openRule ? ( <RuleDetails - allowCustomRules={!this.context.organizationsEnabled} + allowCustomRules={!this.props.appState.organizationsEnabled} canWrite={this.state.canWrite} hideQualityProfiles={hideQualityProfiles} onActivate={this.handleRuleActivate} @@ -643,6 +640,7 @@ function parseFacets(rawFacets: { property: string; values: { count: number; val } const mapStateToProps = (state: Store) => ({ + appState: getAppState(state), currentUser: getCurrentUser(state), languages: getLanguages(state), userOrganizations: getMyOrganizations(state) 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<T.AppState, 'branchesEnabled'>; organization: string | undefined; ruleDetails: Pick<T.RuleDetails, 'key' | 'type'>; } @@ -44,13 +45,8 @@ interface State { total?: number; } -export default class RuleDetailsIssues extends React.PureComponent<Props, State> { +export class RuleDetailsIssues extends React.PureComponent<Props, State> { mounted = false; - - static contextTypes = { - branchesEnabled: PropTypes.bool - }; - state: State = { loading: true }; componentDidMount() { @@ -119,7 +115,7 @@ export default class RuleDetailsIssues extends React.PureComponent<Props, State> </span> ); - if (!this.context.branchesEnabled) { + if (!this.props.appState.branchesEnabled) { return totalItem; } @@ -173,3 +169,5 @@ export default class RuleDetailsIssues extends React.PureComponent<Props, State> ); } } + +export default withAppState(RuleDetailsIssues); 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( - <RuleDetailsIssues organization="org" ruleDetails={{ key: 'foo', type: ruleType }} /> + <RuleDetailsIssues + appState={{ branchesEnabled: false }} + organization="org" + ruleDetails={{ key: 'foo', type: ruleType }} + /> ); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); 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<FetchIssuesPromise>; hideAuthorFacet?: boolean; - location: { pathname: string; query: RawQuery }; + location: Pick<Location, 'pathname' | 'query'>; myIssues?: boolean; onBranchesChange: () => void; organization?: { key: string }; + router: Pick<Router, 'push' | 'replace'>; userOrganizations: T.Organization[]; } @@ -130,13 +131,9 @@ export interface State { const DEFAULT_QUERY = { resolved: 'false' }; -export default class App extends React.PureComponent<Props, State> { +export class App extends React.PureComponent<Props, State> { mounted = false; - static contextTypes = { - router: PropTypes.object.isRequired - }; - constructor(props: Props) { super(props); this.state = { @@ -372,16 +369,16 @@ export default class App extends React.PureComponent<Props, State> { this.scrollToSelectedIssue ); } else { - this.context.router.replace(path); + this.props.router.replace(path); } } else { - this.context.router.push(path); + this.props.router.push(path); } }; closeIssue = () => { if (this.state.query) { - this.context.router.push({ + this.props.router.push({ pathname: this.props.location.pathname, query: { ...serializeQuery(this.state.query), @@ -635,7 +632,7 @@ export default class App extends React.PureComponent<Props, State> { handleFilterChange = (changes: Partial<Query>) => { this.setState({ loading: true }); - this.context.router.push({ + this.props.router.push({ pathname: this.props.location.pathname, query: { ...serializeQuery({ ...this.state.query, ...changes }), @@ -651,7 +648,7 @@ export default class App extends React.PureComponent<Props, State> { if (!this.props.component) { saveMyIssues(myIssues); } - this.context.router.push({ + this.props.router.push({ pathname: this.props.location.pathname, query: { ...serializeQuery({ ...this.state.query, assigned: true, assignees: [] }), @@ -705,7 +702,7 @@ export default class App extends React.PureComponent<Props, State> { }; handleReset = () => { - this.context.router.push({ + this.props.router.push({ pathname: this.props.location.pathname, query: { ...DEFAULT_QUERY, @@ -1156,3 +1153,5 @@ export default class App extends React.PureComponent<Props, State> { ); } } + +export default withRouter(App); 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<Router, 'push'>; standaloneMode?: boolean; updateCenterActive: boolean; } @@ -54,13 +54,8 @@ interface State { plugins: Plugin[]; } -export default class App extends React.PureComponent<Props, State> { +class App extends React.PureComponent<Props, State> { mounted = false; - - static contextTypes = { - router: PropTypes.object.isRequired - }; - state: State = { loadingPlugins: true, plugins: [] }; componentDidMount() { @@ -108,7 +103,7 @@ export default class App extends React.PureComponent<Props, State> { updateQuery = (newQuery: Partial<Query>) => { const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery }); - this.context.router.push({ pathname: this.props.location.pathname, query }); + this.props.router.push({ pathname: this.props.location.pathname, query }); }; stopLoadingPlugins = () => { @@ -151,3 +146,5 @@ export default class App extends React.PureComponent<Props, State> { ); } } + +export default withRouter(App); 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<void>; @@ -36,6 +36,7 @@ interface DispatchToProps { interface OwnProps { organization: Pick<T.Organization, 'key' | 'name'>; + router: Pick<Router, 'replace'>; } type Props = OwnProps & DispatchToProps; @@ -46,10 +47,6 @@ interface State { export class OrganizationDelete extends React.PureComponent<Props, State> { mounted = false; - static contextTypes = { - router: PropTypes.object - }; - state: State = {}; componentDidMount() { @@ -82,7 +79,7 @@ export class OrganizationDelete extends React.PureComponent<Props, State> { onDelete = () => { return this.props.deleteOrganization(this.props.organization.key).then(() => { - this.context.router.replace('/'); + this.props.router.replace('/'); }); }; @@ -128,7 +125,9 @@ export class OrganizationDelete extends React.PureComponent<Props, State> { const mapDispatchToProps: DispatchToProps = { deleteOrganization: deleteOrganization as any }; -export default connect( - null, - mapDispatchToProps -)(OrganizationDelete); +export default withRouter( + connect( + null, + mapDispatchToProps + )(OrganizationDelete) +); 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<OrganizationDelete['props']> = {}) { return shallow( <OrganizationDelete deleteOrganization={jest.fn(() => Promise.resolve())} organization={{ key: 'foo', name: 'Foo' }} + router={{ replace: jest.fn() }} {...props} - />, - - { context: { router: { replace: jest.fn() }, ...context } } + /> ); } 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<T.Component>) => void; + router: Pick<Router, 'replace'>; } -export default class App extends React.PureComponent<Props> { - static contextTypes = { - router: PropTypes.object - }; - +export class App extends React.PureComponent<Props> { componentDidMount() { const { branchLike, component } = this.props; if (this.isPortfolio()) { - this.context.router.replace({ + this.props.router.replace({ pathname: '/portfolio', query: { id: component.key } }); } else if (this.isFile()) { - this.context.router.replace( + this.props.router.replace( getCodeUrl(component.breadcrumbs[0].key, branchLike, component.key) ); } else if (isShortLivingBranch(branchLike)) { - this.context.router.replace(getShortLivingBranchUrl(component.key, branchLike.name)); + this.props.router.replace(getShortLivingBranchUrl(component.key, branchLike.name)); } } @@ -116,3 +113,5 @@ export default class App extends React.PureComponent<Props> { ); } } + +export default withRouter(App); 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<any>).mockReturnValue(true); expect( - getWrapper({ component: { key: 'foo' } }) + getWrapper({ component: { key: 'foo' } as T.Component }) .find('Connect(SonarCloudEmptyOverview)') .exists() ).toBeTruthy(); @@ -81,10 +81,8 @@ it('redirects on Code page for files', () => { branchLikes={[branch]} component={newComponent} onComponentChange={jest.fn()} - />, - { - context: { router: { replace } } - } + router={{ replace }} + /> ); expect(replace).toBeCalledWith({ pathname: '/code', @@ -92,8 +90,14 @@ it('redirects on Code page for files', () => { }); }); -function getWrapper(props = {}) { +function getWrapper(props: Partial<App['props']> = {}) { return shallow( - <App branchLikes={[]} component={component} onComponentChange={jest.fn()} {...props} /> + <App + branchLikes={[]} + component={component} + onComponentChange={jest.fn()} + router={{ replace: jest.fn() }} + {...props} + /> ); } 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<Props> { - static contextTypes = { - organizationsEnabled: PropTypes.bool - }; - renderQualityInfos() { - const { organizationsEnabled } = this.context; + const { organizationsEnabled } = this.props.appState; const { component, currentUser, organization, userOrganizations } = this.props; const { qualifier, qualityProfiles, qualityGate } = component; const isProject = qualifier === 'TRK'; @@ -98,7 +95,7 @@ export class Meta extends React.PureComponent<Props> { } render() { - const { organizationsEnabled } = this.context; + const { organizationsEnabled } = this.props.appState; const { branchLike, component, measures, metrics, organization } = this.props; const { qualifier, description, visibility } = component; @@ -164,6 +161,7 @@ export class Meta extends React.PureComponent<Props> { } const mapStateToProps = (state: Store, { component }: OwnProps) => ({ + appState: getAppState(state), currentUser: getCurrentUser(state), organization: getOrganizationByKey(state, component.organization), userOrganizations: getMyOrganizations(state) 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<Router, 'replace'>; topQualifiers: string[]; } @@ -44,13 +45,8 @@ interface State { updateModal: boolean; } -export default class ActionsCell extends React.PureComponent<Props, State> { +export class ActionsCell extends React.PureComponent<Props, State> { mounted = false; - - static contextTypes = { - router: PropTypes.object - }; - state: State = { deleteForm: false, updateModal: false }; componentDidMount() { @@ -96,7 +92,7 @@ export default class ActionsCell extends React.PureComponent<Props, State> { const pathname = this.props.organization ? `/organizations/${this.props.organization.key}/permission_templates` : '/permission_templates'; - this.context.router.replace(pathname); + this.props.router.replace(pathname); this.props.refresh(); }); }; @@ -214,3 +210,5 @@ export default class ActionsCell extends React.PureComponent<Props, State> { ); } } + +export default withRouter(ActionsCell); 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<void>; + router: Pick<Router, 'push'>; } interface State { createModal: boolean; } -export default class Header extends React.PureComponent<Props, State> { +class Header extends React.PureComponent<Props, State> { mounted = false; - - static contextTypes = { - router: PropTypes.object - }; - state: State = { createModal: false }; componentDidMount() { @@ -72,7 +68,7 @@ export default class Header extends React.PureComponent<Props, State> { const pathname = organization ? `/organizations/${organization}/permission_templates` : '/permission_templates'; - this.context.router.push({ pathname, query: { id: response.permissionTemplate.id } }); + this.props.router.push({ pathname, query: { id: response.permissionTemplate.id } }); }); }); }; @@ -102,3 +98,5 @@ export default class Header extends React.PureComponent<Props, State> { ); } } + +export default withRouter(Header); 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<Props>) { +function renderActionsCell(props?: Partial<ActionsCell['props']>) { return shallow( <ActionsCell permissionTemplate={SAMPLE} refresh={() => 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`] = ` <h4> project_activity.page </h4> - <PreviewGraph + <withRouter(PreviewGraph) history={ Object { "coverage": Array [ diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx index 9094431da5d..7c3fa2102a8 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.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 { omitBy } from 'lodash'; import PageHeader from './PageHeader'; @@ -38,15 +37,17 @@ import { fetchProjects, parseSorting, SORTING_SWITCH } from '../utils'; import { parseUrlQuery, Query, hasFilterParams, hasVisualizationParams } from '../query'; import { isSonarCloud } from '../../../helpers/system'; import { isLoggedIn } from '../../../helpers/users'; +import { withRouter, Location, Router } from '../../../components/hoc/withRouter'; import '../../../components/search-navigator.css'; import '../styles.css'; -export interface Props { +interface Props { currentUser: T.CurrentUser; isFavorite: boolean; - location: { pathname: string; query: RawQuery }; + location: Pick<Location, 'pathname' | 'query'>; organization: T.Organization | undefined; organizationsEnabled?: boolean; + router: Pick<Router, 'push' | 'replace'>; storageOptionsSuffix?: string; } @@ -63,13 +64,9 @@ const PROJECTS_SORT = 'sonarqube.projects.sort'; const PROJECTS_VIEW = 'sonarqube.projects.view'; const PROJECTS_VISUALIZATION = 'sonarqube.projects.visualization'; -export default class AllProjects extends React.PureComponent<Props, State> { +export class AllProjects extends React.PureComponent<Props, State> { mounted = false; - static contextTypes = { - router: PropTypes.object.isRequired - }; - constructor(props: Props) { super(props); this.state = { loading: true, query: {} }; @@ -187,7 +184,7 @@ export default class AllProjects extends React.PureComponent<Props, State> { query.sort = (sort.sortDesc ? '-' : '') + SORTING_SWITCH[sort.sortValue]; } } - this.context.router.push({ pathname: this.props.location.pathname, query }); + this.props.router.push({ pathname: this.props.location.pathname, query }); } else { this.updateLocationQuery(query); } @@ -210,7 +207,7 @@ export default class AllProjects extends React.PureComponent<Props, State> { // if there is no visualization parameters (sort, view, visualization), but there are saved preferences in the localStorage if (initialMount && !hasVisualizationParams(query) && savedOptionsSet) { - this.context.router.replace({ pathname: this.props.location.pathname, query: savedOptions }); + this.props.router.replace({ pathname: this.props.location.pathname, query: savedOptions }); } else { this.fetchProjects(query); } @@ -218,11 +215,11 @@ export default class AllProjects extends React.PureComponent<Props, State> { updateLocationQuery = (newQuery: RawQuery) => { const query = omitBy({ ...this.props.location.query, ...newQuery }, x => !x); - this.context.router.push({ pathname: this.props.location.pathname, query }); + this.props.router.push({ pathname: this.props.location.pathname, query }); }; handleClearAll = () => { - this.context.router.push({ pathname: this.props.location.pathname }); + this.props.router.push({ pathname: this.props.location.pathname }); }; renderSide = () => ( @@ -328,3 +325,5 @@ export default class AllProjects extends React.PureComponent<Props, State> { ); } } + +export default withRouter(AllProjects); 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<Location, 'pathname' | 'query'>; + router: Pick<Router, 'replace'>; } interface State { @@ -36,19 +37,12 @@ interface State { shouldForceSorting?: string; } -export default class DefaultPageSelector extends React.PureComponent<Props, State> { - static contextTypes = { - router: PropTypes.object.isRequired - }; - - constructor(props: Props) { - super(props); - this.state = {}; - } +export class DefaultPageSelector extends React.PureComponent<Props, State> { + state: State = {}; componentDidMount() { if (isSonarCloud() && !isLoggedIn(this.props.currentUser)) { - this.context.router.replace('/explore/projects'); + this.props.router.replace('/explore/projects'); } if (!isSonarCloud()) { @@ -61,9 +55,9 @@ export default class DefaultPageSelector extends React.PureComponent<Props, Stat if (prevProps.location !== this.props.location) { this.defineIfShouldBeRedirected(); } else if (this.state.shouldBeRedirected === true) { - this.context.router.replace({ ...this.props.location, pathname: '/projects/favorite' }); + this.props.router.replace({ ...this.props.location, pathname: '/projects/favorite' }); } else if (this.state.shouldForceSorting != null) { - this.context.router.replace({ + this.props.router.replace({ ...this.props.location, query: { ...this.props.location.query, @@ -142,3 +136,5 @@ export default class DefaultPageSelector extends React.PureComponent<Props, Stat return null; } } + +export default withRouter(DefaultPageSelector); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx index babf7c999e0..12e7e5d81e5 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx @@ -20,7 +20,7 @@ /* eslint-disable import/order */ import * as React from 'react'; import { shallow } from 'enzyme'; -import AllProjects, { Props } from '../AllProjects'; +import { AllProjects } from '../AllProjects'; import { get, save } from '../../../../helpers/storage'; jest.mock('../ProjectsList', () => ({ @@ -162,9 +162,9 @@ it('changes perspective to risk visualization', () => { }); function shallowRender( - props: Partial<Props> = {}, - push: Function = jest.fn(), - replace: Function = jest.fn() + props: Partial<AllProjects['props']> = {}, + push = jest.fn(), + replace = jest.fn() ) { const wrapper = shallow( <AllProjects @@ -173,9 +173,9 @@ function shallowRender( location={{ pathname: '/projects', query: {} }} organization={undefined} organizationsEnabled={false} + router={{ push, replace }} {...props} - />, - { context: { router: { push, replace } } } + /> ); wrapper.setState({ loading: false, 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<any>; @@ -87,7 +87,10 @@ function mountRender( replace: any = jest.fn() ) { return mount( - <DefaultPageSelector currentUser={currentUser} location={{ pathname: '/projects', query }} />, - { context: { router: { replace } } } + <DefaultPageSelector + currentUser={currentUser} + location={{ pathname: '/projects', query }} + router={{ replace }} + /> ); } 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<void>; organization?: string; qualityGate: T.QualityGate; + router: Pick<Router, 'push'>; } interface State { name: string; } -export default class CopyQualityGateForm extends React.PureComponent<Props, State> { - static contextTypes = { - router: PropTypes.object - }; - +class CopyQualityGateForm extends React.PureComponent<Props, State> { constructor(props: Props) { super(props); this.state = { name: props.qualityGate.name }; @@ -59,7 +56,7 @@ export default class CopyQualityGateForm extends React.PureComponent<Props, Stat return copyQualityGate({ id: qualityGate.id, name, organization }).then(qualityGate => { this.props.onCopy(); - this.context.router.push(getQualityGateUrl(String(qualityGate.id), this.props.organization)); + this.props.router.push(getQualityGateUrl(String(qualityGate.id), this.props.organization)); }); }; @@ -95,3 +92,5 @@ export default class CopyQualityGateForm extends React.PureComponent<Props, Stat ); } } + +export default withRouter(CopyQualityGateForm); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx index e9b20ed5a64..558cf854834 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.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 { createQualityGate } from '../../../api/quality-gates'; import ConfirmModal from '../../../components/controls/ConfirmModal'; import { translate } from '../../../helpers/l10n'; import { getQualityGateUrl } from '../../../helpers/urls'; +import { withRouter, Router } from '../../../components/hoc/withRouter'; interface Props { onClose: () => void; onCreate: () => Promise<void>; organization?: string; + router: Pick<Router, 'push'>; } interface State { name: string; } -export default class CreateQualityGateForm extends React.PureComponent<Props, State> { - static contextTypes = { - router: PropTypes.object - }; - - state = { name: '' }; +class CreateQualityGateForm extends React.PureComponent<Props, State> { + state: State = { name: '' }; handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => { this.setState({ name: event.currentTarget.value }); @@ -58,7 +55,7 @@ export default class CreateQualityGateForm extends React.PureComponent<Props, St return this.props.onCreate().then(() => qualityGate); }) .then(qualityGate => { - this.context.router.push(getQualityGateUrl(String(qualityGate.id), organization)); + this.props.router.push(getQualityGateUrl(String(qualityGate.id), organization)); }); }; @@ -91,3 +88,5 @@ export default class CreateQualityGateForm extends React.PureComponent<Props, St ); } } + +export default withRouter(CreateQualityGateForm); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx index f523a4f12f1..84d9f38bdc9 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx @@ -18,30 +18,27 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as PropTypes from 'prop-types'; import { deleteQualityGate } from '../../../api/quality-gates'; import ConfirmButton from '../../../components/controls/ConfirmButton'; import { Button } from '../../../components/ui/buttons'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getQualityGatesUrl } from '../../../helpers/urls'; +import { withRouter, Router } from '../../../components/hoc/withRouter'; interface Props { onDelete: () => Promise<void>; organization?: string; qualityGate: T.QualityGate; + router: Pick<Router, 'push'>; } -export default class DeleteQualityGateForm extends React.PureComponent<Props> { - static contextTypes = { - router: PropTypes.object - }; - +class DeleteQualityGateForm extends React.PureComponent<Props> { onDelete = () => { const { organization, qualityGate } = this.props; return deleteQualityGate({ id: qualityGate.id, organization }) .then(this.props.onDelete) .then(() => { - this.context.router.push(getQualityGatesUrl(organization)); + this.props.router.push(getQualityGatesUrl(organization)); }); }; @@ -70,3 +67,5 @@ export default class DeleteQualityGateForm extends React.PureComponent<Props> { ); } } + +export default withRouter(DeleteQualityGateForm); 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<Props, State> { mounted = false; - - static contextTypes = { - router: PropTypes.object.isRequired - }; - state: State = { loading: true }; componentDidMount() { @@ -173,7 +168,9 @@ const mapStateToProps = (state: Store): StateToProps => ({ metrics: getMetrics(state) }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(DetailsApp); +export default withRouter( + connect( + mapStateToProps, + mapDispatchToProps + )(DetailsApp) +); 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<void>; @@ -44,13 +44,8 @@ interface State { qualityGates: T.QualityGate[]; } -export default class QualityGatesApp extends React.PureComponent<Props, State> { +class QualityGatesApp extends React.PureComponent<Props, State> { mounted = false; - - static contextTypes = { - router: PropTypes.object.isRequired - }; - state: State = { canCreate: false, loading: true, qualityGates: [] }; componentDidMount() { @@ -87,7 +82,7 @@ export default class QualityGatesApp extends React.PureComponent<Props, State> { this.setState({ canCreate: actions.create, loading: false, qualityGates }); if (qualityGates && qualityGates.length === 1 && !actions.create) { - this.context.router.replace( + this.props.router.replace( getQualityGateUrl(String(qualityGates[0].id), organization && organization.key) ); } @@ -156,3 +151,5 @@ export default class QualityGatesApp extends React.PureComponent<Props, State> { ); } } + +export default withRouter(QualityGatesApp); 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<Props, State> { +class ChangelogContainer extends React.PureComponent<Props, State> { mounted = false; - - static contextTypes = { - router: PropTypes.object - }; - - state: State = { - loading: true - }; + state: State = { loading: true }; componentDidMount() { this.mounted = true; @@ -136,7 +123,7 @@ export default class ChangelogContainer extends React.PureComponent<Props, State to: to && toShortNotSoISOString(to) } ); - this.context.router.push(path); + this.props.router.push(path); }; handleReset = () => { @@ -145,7 +132,7 @@ export default class ChangelogContainer extends React.PureComponent<Props, State this.props.profile.language, this.props.organization ); - this.context.router.push(path); + this.props.router.push(path); }; render() { @@ -189,3 +176,5 @@ export default class ChangelogContainer extends React.PureComponent<Props, State ); } } + +export default withRouter(ChangelogContainer); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.tsx index bb3fa09aa81..9e28e800200 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.tsx @@ -18,15 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as PropTypes from 'prop-types'; +import { withRouter, WithRouterProps } from 'react-router'; import ComparisonForm from './ComparisonForm'; import ComparisonResults from './ComparisonResults'; import { compareProfiles } from '../../../api/quality-profiles'; import { getProfileComparePath } from '../utils'; import { Profile } from '../types'; -interface Props { - location: { query: { withKey?: string } }; +interface Props extends WithRouterProps { organization: string | null; profile: Profile; profiles: Profile[]; @@ -48,17 +47,9 @@ interface State { }>; } -export default class ComparisonContainer extends React.PureComponent<Props, State> { +class ComparisonContainer extends React.PureComponent<Props, State> { mounted = false; - - static contextTypes = { - router: PropTypes.object - }; - - constructor(props: Props) { - super(props); - this.state = { loading: false }; - } + state: State = { loading: false }; componentDidMount() { this.mounted = true; @@ -104,7 +95,7 @@ export default class ComparisonContainer extends React.PureComponent<Props, Stat this.props.organization, withKey ); - this.context.router.push(path); + this.props.router.push(path); }; render() { @@ -145,3 +136,5 @@ export default class ComparisonContainer extends React.PureComponent<Props, Stat ); } } + +export default withRouter(ComparisonContainer); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx index ac464d475c8..8947a6fd0ed 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.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 RenameProfileForm from './RenameProfileForm'; import CopyProfileForm from './CopyProfileForm'; import DeleteProfileForm from './DeleteProfileForm'; @@ -31,12 +30,14 @@ import ActionsDropdown, { ActionsDropdownItem, ActionsDropdownDivider } from '../../../components/controls/ActionsDropdown'; +import { withRouter, Router } from '../../../components/hoc/withRouter'; interface Props { className?: string; fromList?: boolean; organization: string | null; profile: Profile; + router: Pick<Router, 'push' | 'replace'>; updateProfiles: () => Promise<void>; } @@ -46,20 +47,13 @@ interface State { renameFormOpen: boolean; } -export default class ProfileActions extends React.PureComponent<Props, State> { - static contextTypes = { - router: PropTypes.object +export class ProfileActions extends React.PureComponent<Props, State> { + state: State = { + copyFormOpen: false, + deleteFormOpen: false, + renameFormOpen: false }; - constructor(props: Props) { - super(props); - this.state = { - copyFormOpen: false, - deleteFormOpen: false, - renameFormOpen: false - }; - } - handleRenameClick = () => { this.setState({ renameFormOpen: true }); }; @@ -69,7 +63,7 @@ export default class ProfileActions extends React.PureComponent<Props, State> { this.props.updateProfiles().then( () => { if (!this.props.fromList) { - this.context.router.replace( + this.props.router.replace( getProfilePath(name, this.props.profile.language, this.props.organization) ); } @@ -90,7 +84,7 @@ export default class ProfileActions extends React.PureComponent<Props, State> { this.closeCopyForm(); this.props.updateProfiles().then( () => { - this.context.router.push( + this.props.router.push( getProfilePath(name, this.props.profile.language, this.props.organization) ); }, @@ -111,7 +105,7 @@ export default class ProfileActions extends React.PureComponent<Props, State> { }; handleProfileDelete = () => { - this.context.router.replace(getProfilesPath(this.props.organization)); + this.props.router.replace(getProfilesPath(this.props.organization)); this.props.updateProfiles(); }; @@ -220,3 +214,5 @@ export default class ProfileActions extends React.PureComponent<Props, State> { ); } } + +export default withRouter(ProfileActions); 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(<ProfileActions organization="org" profile={PROFILE} updateProfiles={jest.fn()} />) + shallow( + <ProfileActions + organization="org" + profile={PROFILE} + router={{ push: jest.fn(), replace: jest.fn() }} + updateProfiles={jest.fn()} + /> + ) ).toMatchSnapshot(); }); @@ -50,6 +57,7 @@ it('renders with permission to edit only', () => { <ProfileActions organization="org" profile={{ ...PROFILE, actions: { edit: true } }} + router={{ push: jest.fn(), replace: jest.fn() }} updateProfiles={jest.fn()} /> ) @@ -71,6 +79,7 @@ it('renders with all permissions', () => { associateProjects: true } }} + router={{ push: jest.fn(), replace: jest.fn() }} updateProfiles={jest.fn()} /> ) @@ -84,9 +93,9 @@ it('should copy profile', async () => { <ProfileActions organization="org" profile={{ ...PROFILE, actions: { copy: true } }} + router={{ push, replace: jest.fn() }} updateProfiles={updateProfiles} - />, - { context: { router: { push } } } + /> ); click(wrapper.find('[id="quality-profile-copy"]')); 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<Router, 'push'>; updateProfiles: () => Promise<void>; } @@ -40,12 +41,8 @@ interface State { restoreFormOpen: boolean; } -export default class PageHeader extends React.PureComponent<Props, State> { - static contextTypes = { - router: PropTypes.object - }; - - state = { +class PageHeader extends React.PureComponent<Props, State> { + state: State = { createFormOpen: false, restoreFormOpen: false }; @@ -57,7 +54,7 @@ export default class PageHeader extends React.PureComponent<Props, State> { handleCreate = (profile: Profile) => { this.props.updateProfiles().then( () => { - this.context.router.push( + this.props.router.push( getProfilePath(profile.name, profile.language, this.props.organization) ); }, @@ -130,3 +127,5 @@ export default class PageHeader extends React.PureComponent<Props, State> { ); } } + +export default withRouter(PageHeader); 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<Location, 'pathname' | 'query'>; params: { type: string }; + router: Pick<Router, 'push'>; } interface State { @@ -50,13 +50,9 @@ interface State { showCWE: boolean; } -export default class App extends React.PureComponent<Props, State> { +export class App extends React.PureComponent<Props, State> { mounted = false; - static contextTypes = { - router: PropTypes.object.isRequired - }; - constructor(props: Props) { super(props); this.state = { @@ -115,8 +111,7 @@ export default class App extends React.PureComponent<Props, State> { }; handleCheck = (checked: boolean) => { - const { router } = this.context; - router.push({ + this.props.router.push({ pathname: this.props.location.pathname, query: { id: this.props.component.key, showCWE: checked } }); @@ -194,3 +189,5 @@ export default class App extends React.PureComponent<Props, State> { ); } } + +export default withRouter(App); 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(<App component={component} location={location} params={wrongParams} />, { - context - }); + const wrapper = shallow( + <App + component={component} + location={location} + params={wrongParams} + router={{ push: jest.fn() }} + />, + { + context + } + ); expect(wrapper).toMatchSnapshot(); }); it('renders owaspTop10', async () => { - const wrapper = shallow(<App component={component} location={location} params={owaspParams} />, { - context - }); + const wrapper = shallow( + <App + component={component} + location={location} + params={owaspParams} + router={{ push: jest.fn() }} + />, + { + context + } + ); await waitAndUpdate(wrapper); expect(getSecurityHotspots).toBeCalledWith({ project: 'foo', @@ -119,7 +135,12 @@ it('renders owaspTop10', async () => { it('renders with cwe', () => { const wrapper = shallow( - <App component={component} location={locationWithCWE} params={owaspParams} />, + <App + component={component} + location={locationWithCWE} + params={owaspParams} + router={{ push: jest.fn() }} + />, { context } ); expect(getSecurityHotspots).toBeCalledWith({ @@ -132,9 +153,17 @@ it('renders with cwe', () => { }); it('handle checkbox for cwe display', async () => { - const wrapper = shallow(<App component={component} location={location} params={owaspParams} />, { - context - }); + const wrapper = shallow( + <App + component={component} + location={location} + params={owaspParams} + router={{ push: jest.fn() }} + />, + { + context + } + ); expect(getSecurityHotspots).toBeCalledWith({ project: 'foo', standard: 'owaspTop10', @@ -156,9 +185,17 @@ it('handle checkbox for cwe display', async () => { }); it('renders sansTop25', () => { - const wrapper = shallow(<App component={component} location={location} params={sansParams} />, { - context - }); + const wrapper = shallow( + <App + component={component} + location={location} + params={sansParams} + router={{ push: jest.fn() }} + />, + { + context + } + ); expect(getSecurityHotspots).toBeCalledWith({ project: 'foo', standard: 'sansTop25', 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<Props, State> { +class App extends React.PureComponent<Props, State> { mounted = false; - - static contextTypes = { - router: PropTypes.object - }; - - constructor(props: Props) { - super(props); - this.state = { loading: true }; - } + state: State = { loading: true }; componentDidMount() { this.mounted = true; @@ -97,7 +86,7 @@ export default class App extends React.PureComponent<Props, State> { updateQuery = (newQuery: Query) => { const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery }); - this.context.router.replace({ pathname: this.props.location.pathname, query }); + this.props.router.replace({ pathname: this.props.location.pathname, query }); }; renderSysInfo() { @@ -145,3 +134,5 @@ export default class App extends React.PureComponent<Props, State> { ); } } + +export default withRouter(App); 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<Router, 'push'>; } interface StateProps { @@ -56,9 +57,6 @@ interface State { export class ProjectOnboarding extends React.PureComponent<Props, State> { mounted = false; - static contextTypes = { - router: PropTypes.object - }; constructor(props: Props) { super(props); @@ -93,7 +91,7 @@ export class ProjectOnboarding extends React.PureComponent<Props, State> { finishOnboarding = () => { this.props.onFinish(); if (this.state.projectKey) { - this.context.router.push(getProjectUrl(this.state.projectKey)); + this.props.router.push(getProjectUrl(this.state.projectKey)); } }; @@ -203,4 +201,4 @@ const mapStateToProps = (state: Store): StateProps => { }; }; -export default connect(mapStateToProps)(ProjectOnboarding); +export default withRouter(connect(mapStateToProps)(ProjectOnboarding)); 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<DispatchProps> { - static contextTypes = { - router: PropTypes.object.isRequired - }; +type Props = DispatchProps & WithRouterProps; +export class ProjectOnboardingPage extends React.PureComponent<Props> { onSkipOnboardingTutorial = () => { this.props.skipOnboarding(); - this.context.router.replace('/'); + this.props.router.replace('/'); }; render() { @@ -44,7 +42,9 @@ export class ProjectOnboardingPage extends React.PureComponent<DispatchProps> { const mapDispatchToProps: DispatchProps = { skipOnboarding }; -export default connect( - null, - mapDispatchToProps -)(ProjectOnboardingPage); +export default withRouter( + connect( + null, + mapDispatchToProps + )(ProjectOnboardingPage) +); 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<any>).mockImplementation(() => 'SonarCloud'); (isSonarCloud as jest.Mock<any>).mockImplementation(() => true); const wrapper = shallow( - <ProjectOnboarding currentUser={currentUser} onFinish={jest.fn()} organizationsEnabled={true} /> + <ProjectOnboarding + currentUser={currentUser} + onFinish={jest.fn()} + organizationsEnabled={true} + router={{ push: jest.fn() }} + /> ); expect(wrapper).toMatchSnapshot(); @@ -73,7 +79,12 @@ it('finishes', () => { (isSonarCloud as jest.Mock<any>).mockImplementation(() => false); const onFinish = jest.fn(); const wrapper = shallow( - <ProjectOnboarding currentUser={currentUser} onFinish={onFinish} organizationsEnabled={false} /> + <ProjectOnboarding + currentUser={currentUser} + onFinish={onFinish} + organizationsEnabled={false} + router={{ push: jest.fn() }} + /> ); click(wrapper.find('ResetButtonLink')); return doAsync(() => { 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<Location, 'query'>; organizationsEnabled?: boolean; + router: Pick<Router, 'push'>; } interface State { @@ -43,13 +43,8 @@ interface State { users: T.User[]; } -export default class UsersApp extends React.PureComponent<Props, State> { +export class UsersApp extends React.PureComponent<Props, State> { mounted = false; - - static contextTypes = { - router: PropTypes.object.isRequired - }; - state: State = { identityProviders: [], loading: true, users: [] }; componentDidMount() { @@ -110,7 +105,7 @@ export default class UsersApp extends React.PureComponent<Props, State> { updateQuery = (newQuery: Partial<Query>) => { const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery }); - this.context.router.push({ ...this.props.location, query }); + this.props.router.push({ ...this.props.location, query }); }; updateTokensCount = (login: string, tokensCount: number) => { @@ -148,3 +143,5 @@ export default class UsersApp extends React.PureComponent<Props, State> { ); } } + +export default withRouter(UsersApp); 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<UsersApp['props']> = {}) { return shallow( <UsersApp currentUser={currentUser} location={location} organizationsEnabled={true} + router={{ push: jest.fn() }} {...props} />, { 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<Props, State> { +class WebApiApp extends React.PureComponent<Props, State> { mounted = false; - - static contextTypes = { - router: PropTypes.object.isRequired - }; - - constructor(props: Props) { - super(props); - this.state = { domains: [] }; - } + state: State = { domains: [] }; componentDidMount() { this.mounted = true; @@ -99,7 +86,7 @@ export default class WebApiApp extends React.PureComponent<Props, State> { updateQuery = (newQuery: Partial<Query>) => { const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery }); - this.context.router.push({ pathname: this.props.location.pathname, query }); + this.props.router.push({ pathname: this.props.location.pathname, query }); }; toggleInternalInitially() { @@ -127,14 +114,13 @@ export default class WebApiApp extends React.PureComponent<Props, State> { handleToggleInternal = () => { const splat = this.props.params.splat || ''; - const { router } = this.context; const { domains } = this.state; const domain = domains.find(domain => isDomainPathActive(domain.path, splat)); const query = parseQuery(this.props.location.query); const internal = !query.internal; if (domain && domain.internal && !internal) { - router.push({ + this.props.router.push({ pathname: '/web_api', query: { ...serializeQuery(query), internal: false } }); @@ -194,3 +180,5 @@ export default class WebApiApp extends React.PureComponent<Props, State> { ); } } + +export default withRouter(WebApiApp); 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<T.AppState, 'canAdmin'>; customProps?: { [k: string]: any; }; @@ -34,11 +36,7 @@ const SONARCLOUD_LINK = '/#sonarcloud#/'; const SONARQUBE_LINK = '/#sonarqube#/'; const SONARQUBE_ADMIN_LINK = '/#sonarqube-admin#/'; -export default class DocLink extends React.PureComponent<Props> { - static contextTypes = { - canAdmin: () => null - }; - +export class DocLink extends React.PureComponent<Props> { handleClickOnAnchor = (event: React.MouseEvent<HTMLAnchorElement>) => { const { customProps, href = '#' } = this.props; if (customProps && customProps.onAnchorClick) { @@ -63,7 +61,7 @@ export default class DocLink extends React.PureComponent<Props> { return <SonarQubeLink url={href}>{children}</SonarQubeLink>; } else if (href.startsWith(SONARQUBE_ADMIN_LINK)) { return ( - <SonarQubeAdminLink canAdmin={this.context.canAdmin} url={href}> + <SonarQubeAdminLink canAdmin={this.props.appState.canAdmin} url={href}> {children} </SonarQubeAdminLink> ); @@ -91,6 +89,8 @@ export default class DocLink extends React.PureComponent<Props> { } } +export default withAppState(DocLink); + interface SonarCloudLinkProps { children: React.ReactNode; url: string; @@ -124,7 +124,7 @@ function SonarQubeLink({ children, url }: SonarQubeLinkProps) { } interface SonarQubeAdminLinkProps { - canAdmin: boolean; + canAdmin?: boolean; children: React.ReactNode; url: string; } 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(<DocLink href="http://sample.com">link text</DocLink>)).toMatchSnapshot(); + expect( + shallow( + <DocLink appState={{ canAdmin: false }} href="http://sample.com"> + link text + </DocLink> + ) + ).toMatchSnapshot(); }); it('should render documentation link', () => { - expect(shallow(<DocLink href="/foo/bar">link text</DocLink>)).toMatchSnapshot(); + expect( + shallow( + <DocLink appState={{ canAdmin: false }} href="/foo/bar"> + link text + </DocLink> + ) + ).toMatchSnapshot(); }); it('should render sonarcloud link on sonarcloud', () => { (isSonarCloud as jest.Mock).mockImplementationOnce(() => true); - const wrapper = shallow(<DocLink href="/#sonarcloud#/foo/bar">link text</DocLink>); + const wrapper = shallow( + <DocLink appState={{ canAdmin: false }} href="/#sonarcloud#/foo/bar"> + link text + </DocLink> + ); expect(wrapper).toMatchSnapshot(); expect(wrapper.find('SonarCloudLink').dive()).toMatchSnapshot(); }); it('should not render sonarcloud link on sonarcloud', () => { (isSonarCloud as jest.Mock).mockImplementationOnce(() => false); - const wrapper = shallow(<DocLink href="/#sonarcloud#/foo/bar">link text</DocLink>); + const wrapper = shallow( + <DocLink appState={{ canAdmin: false }} href="/#sonarcloud#/foo/bar"> + link text + </DocLink> + ); expect(wrapper.find('SonarCloudLink').dive()).toMatchSnapshot(); }); it('should render sonarqube link on sonarqube', () => { - const wrapper = shallow(<DocLink href="/#sonarqube#/foo/bar">link text</DocLink>); + const wrapper = shallow( + <DocLink appState={{ canAdmin: false }} href="/#sonarqube#/foo/bar"> + link text + </DocLink> + ); expect(wrapper).toMatchSnapshot(); expect(wrapper.find('SonarQubeLink').dive()).toMatchSnapshot(); }); it('should not render sonarqube link on sonarcloud', () => { (isSonarCloud as jest.Mock).mockImplementationOnce(() => true); - const wrapper = shallow(<DocLink href="/#sonarqube#/foo/bar">link text</DocLink>); + const wrapper = shallow( + <DocLink appState={{ canAdmin: false }} href="/#sonarqube#/foo/bar"> + link text + </DocLink> + ); expect(wrapper.find('SonarQubeLink').dive()).toMatchSnapshot(); }); it('should render sonarqube admin link on sonarqube for admin', () => { - const wrapper = shallow(<DocLink href="/#sonarqube-admin#/foo/bar">link text</DocLink>, { - context: { canAdmin: true } - }); + const wrapper = shallow( + <DocLink appState={{ canAdmin: true }} href="/#sonarqube-admin#/foo/bar"> + link text + </DocLink> + ); expect(wrapper).toMatchSnapshot(); expect(wrapper.find('SonarQubeAdminLink').dive()).toMatchSnapshot(); }); it('should not render sonarqube admin link on sonarqube for non-admin', () => { - const wrapper = shallow(<DocLink href="/#sonarqube-admin#/foo/bar">link text</DocLink>); + const wrapper = shallow( + <DocLink appState={{ canAdmin: false }} href="/#sonarqube-admin#/foo/bar"> + link text + </DocLink> + ); expect(wrapper.find('SonarQubeAdminLink').dive()).toMatchSnapshot(); }); it('should not render sonarqube admin link on sonarcloud', () => { (isSonarCloud as jest.Mock).mockImplementationOnce(() => true); - const wrapper = shallow(<DocLink href="/#sonarqube-admin#/foo/bar">link text</DocLink>, { - context: { canAdmin: true } - }); + const wrapper = shallow( + <DocLink appState={{ canAdmin: true }} href="/#sonarqube-admin#/foo/bar"> + link text + </DocLink> + ); expect(wrapper.find('SonarQubeAdminLink').dive()).toMatchSnapshot(); }); it.skip('should render documentation anchor', () => { - expect(shallow(<DocLink href="#quality-profiles">link text</DocLink>)).toMatchSnapshot(); + expect( + shallow( + <DocLink appState={{ canAdmin: false }} href="#quality-profiles"> + link text + </DocLink> + ) + ).toMatchSnapshot(); }); 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`] = ` <Link + appState={ + Object { + "canAdmin": false, + } + } onlyActiveOnIndex={false} style={Object {}} to="/documentation/foo/bar" @@ -37,6 +42,11 @@ exports[`should render documentation link 1`] = ` exports[`should render simple link 1`] = ` <Fragment> <a + appState={ + Object { + "canAdmin": false, + } + } href="http://sample.com" rel="noopener noreferrer" target="_blank" diff --git a/server/sonar-web/src/main/js/app/components/extensions/ExtensionContainer.tsx b/server/sonar-web/src/main/js/components/hoc/withRouter.tsx index a3e36e6c76a..1eb0e97588e 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/ExtensionContainer.tsx +++ b/server/sonar-web/src/main/js/components/hoc/withRouter.tsx @@ -17,18 +17,19 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { connect } from 'react-redux'; -import Extension from './Extension'; -import { getCurrentUser, Store } from '../../../store/rootReducer'; -import { addGlobalErrorMessage } from '../../../store/globalMessages'; +import * as React from 'react'; +import { withRouter as originalWithRouter, WithRouterProps } from 'react-router'; -const mapStateToProps = (state: Store) => ({ - currentUser: getCurrentUser(state) -}); +export type Location = WithRouterProps['location']; +export type Router = WithRouterProps['router']; -const mapDispatchToProps = { onFail: addGlobalErrorMessage }; +interface InjectedProps { + location?: Partial<Location>; + router?: Partial<Router>; +} -export default connect( - mapStateToProps, - mapDispatchToProps -)(Extension); +export function withRouter<P extends InjectedProps, S>( + WrappedComponent: React.ComponentClass<P & InjectedProps> +): React.ComponentClass<T.Omit<P, keyof InjectedProps>, S> { + return originalWithRouter(WrappedComponent as any); +} 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<Router, 'push'>; } interface State { @@ -63,11 +64,7 @@ const GRAPH_PADDING = [4, 0, 4, 0]; const MAX_GRAPH_NB = 1; const MAX_SERIES_PER_GRAPH = 3; -export default class PreviewGraph extends React.PureComponent<Props, State> { - static contextTypes = { - router: PropTypes.object - }; - +class PreviewGraph extends React.PureComponent<Props, State> { constructor(props: Props) { super(props); const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM); @@ -140,7 +137,7 @@ export default class PreviewGraph extends React.PureComponent<Props, State> { }; handleClick = () => { - this.context.router.push({ + this.props.router.push({ pathname: '/project/activity', query: { id: this.props.project, ...getBranchLikeQuery(this.props.branchLike) } }); @@ -202,3 +199,5 @@ export default class PreviewGraph extends React.PureComponent<Props, State> { ); } } + +export default withRouter(PreviewGraph); |