@@ -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(); |
@@ -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); |
@@ -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> | |||
); | |||
} |
@@ -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)); |
@@ -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> | |||
); |
@@ -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')} |
@@ -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> |
@@ -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() { |
@@ -17,7 +17,16 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
export interface SuggestionsContext { | |||
import { createContext } from 'react'; | |||
interface SuggestionsContextShape { | |||
addSuggestions: (key: string) => void; | |||
removeSuggestions: (key: string) => void; | |||
suggestions: T.SuggestionLink[]; | |||
} | |||
export const SuggestionsContext = createContext<SuggestionsContextShape>({ | |||
addSuggestions: () => {}, | |||
removeSuggestions: () => {}, | |||
suggestions: [] | |||
}); |
@@ -18,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> | |||
); | |||
} | |||
} |
@@ -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(); | |||
}); |
@@ -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' } | |||
]); | |||
}); |
@@ -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" | |||
/> | |||
</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]} |
@@ -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) | |||
) | |||
); |
@@ -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) => ({ |
@@ -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) => ({ |
@@ -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 }} | |||
/> |
@@ -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} /> | |||
); |
@@ -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} /> | |||
); |
@@ -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); |
@@ -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); |
@@ -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); |
@@ -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); |
@@ -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(); | |||
}); |
@@ -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); |
@@ -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 } } | |||
/> | |||
); | |||
} |
@@ -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(); | |||
} | |||
}); |
@@ -46,7 +46,7 @@ exports[`renders 1`] = ` | |||
warnings={Array []} | |||
/> | |||
</div> | |||
<ComponentNavMenu | |||
<Connect(withAppState(ComponentNavMenu)) | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [ |
@@ -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", |
@@ -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 { |
@@ -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> | |||
); |
@@ -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); |
@@ -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 } }); |
@@ -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 }); |
@@ -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> |
@@ -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; |
@@ -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; | |||
} |
@@ -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; | |||
} |
@@ -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 && ( |
@@ -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); |
@@ -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) |
@@ -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); |
@@ -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(); |
@@ -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); |
@@ -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: [] | |||
}; | |||
@@ -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); |
@@ -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) | |||
); |
@@ -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 } } | |||
/> | |||
); | |||
} |
@@ -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); |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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) |
@@ -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); |
@@ -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); |
@@ -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} | |||
/> |
@@ -7,7 +7,7 @@ exports[`renders 1`] = ` | |||
<h4> | |||
project_activity.page | |||
</h4> | |||
<PreviewGraph | |||
<withRouter(PreviewGraph) | |||
history={ | |||
Object { | |||
"coverage": Array [ |
@@ -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); |
@@ -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); |
@@ -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, |
@@ -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 }} | |||
/> | |||
); | |||
} |
@@ -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); |
@@ -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); |
@@ -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); |
@@ -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) | |||
); |
@@ -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); |
@@ -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); |
@@ -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); |
@@ -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); |
@@ -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"]')); |
@@ -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); |
@@ -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); |
@@ -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', |
@@ -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); |
@@ -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)); |
@@ -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) | |||
); |
@@ -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(() => { |
@@ -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); |
@@ -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} | |||
/>, | |||
{ |
@@ -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); |
@@ -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; | |||
} |
@@ -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(); | |||
}); |
@@ -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" |
@@ -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); | |||
} |
@@ -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); |