diff options
Diffstat (limited to 'server/sonar-web/src')
39 files changed, 533 insertions, 308 deletions
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js index 2a1d784aa1c..02b3ab3ce11 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js @@ -30,7 +30,8 @@ export default class GlobalNavMenu extends React.PureComponent { currentUser: PropTypes.object.isRequired, location: PropTypes.shape({ pathname: PropTypes.string.isRequired - }).isRequired + }).isRequired, + sonarCloud: PropTypes.bool }; static defaultProps = { @@ -46,7 +47,7 @@ export default class GlobalNavMenu extends React.PureComponent { return ( <li> <Link to="/projects" activeClassName="active"> - {translate('projects.page')} + {this.props.sonarCloud ? translate('my_projects') : translate('projects.page')} </Link> </li> ); diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index 73239aeb430..f18764601e4 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -133,3 +133,19 @@ export enum Visibility { Public = 'public', Private = 'private' } + +export interface CurrentUser { + isLoggedIn: boolean; + showOnboardingTutorial?: boolean; +} + +export interface LoggedInUser extends CurrentUser { + avatar?: string; + email?: string; + isLoggedIn: true; + name: string; +} + +export function isLoggedIn(user: CurrentUser): user is LoggedInUser { + return user.isLoggedIn; +} diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.tsx index 07f220f0d96..65169afa93e 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.tsx @@ -18,8 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import App from '../../projects/components/App'; -import AllProjects from '../../projects/components/AllProjects'; +import AllProjectsContainer from '../../projects/components/AllProjectsContainer'; interface Props { location: { pathname: string; query: { [x: string]: string } }; @@ -28,14 +27,10 @@ interface Props { export default function OrganizationProjects(props: Props) { return ( - <div id="projects-page"> - <App> - <AllProjects - isFavorite={false} - location={props.location} - organization={props.organization} - /> - </App> - </div> + <AllProjectsContainer + isFavorite={false} + location={props.location} + organization={props.organization} + /> ); } diff --git a/server/sonar-web/src/main/js/apps/organizations/routes.js b/server/sonar-web/src/main/js/apps/organizations/routes.js index 072bb955202..b0010c5e416 100644 --- a/server/sonar-web/src/main/js/apps/organizations/routes.js +++ b/server/sonar-web/src/main/js/apps/organizations/routes.js @@ -21,7 +21,6 @@ import OrganizationPageContainer from './components/OrganizationPage'; import OrganizationPageExtension from '../../app/components/extensions/OrganizationPageExtension'; import OrganizationContainer from './components/OrganizationContainer'; import OrganizationProjects from './components/OrganizationProjects'; -import OrganizationFavoriteProjects from './components/OrganizationFavoriteProjects'; import OrganizationRules from './components/OrganizationRules'; import OrganizationAdminContainer from './components/OrganizationAdmin'; import OrganizationEdit from './components/OrganizationEdit'; @@ -51,17 +50,7 @@ const routes = [ { path: 'projects', component: OrganizationContainer, - childRoutes: [ - { - indexRoute: { - component: OrganizationProjects - } - }, - { - path: 'favorite', - component: OrganizationFavoriteProjects - } - ] + childRoutes: [{ indexRoute: { component: OrganizationProjects } }] }, { path: 'issues', diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx index cd5f60bd1eb..fb4e108e3db 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx @@ -24,6 +24,7 @@ import PageHeader from './PageHeader'; import ProjectsList from './ProjectsList'; import PageSidebar from './PageSidebar'; import Visualizations from '../visualizations/Visualizations'; +import { CurrentUser, isLoggedIn } from '../../../app/types'; import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication'; import ListFooter from '../../../components/controls/ListFooter'; import { translate } from '../../../helpers/l10n'; @@ -34,10 +35,13 @@ import { Project, Facets } from '../types'; import { fetchProjects, parseSorting, SORTING_SWITCH } from '../utils'; import { parseUrlQuery, Query } from '../query'; -interface Props { +export interface Props { + currentUser: CurrentUser; isFavorite: boolean; location: { pathname: string; query: { [x: string]: string } }; + onSonarCloud: boolean; organization?: { key: string }; + organizationsEnabled: boolean; } interface State { @@ -53,8 +57,6 @@ export default class AllProjects extends React.PureComponent<Props, State> { mounted: boolean; static contextTypes = { - currentUser: PropTypes.object.isRequired, - organizationsEnabled: PropTypes.bool, router: PropTypes.object.isRequired }; @@ -65,7 +67,13 @@ export default class AllProjects extends React.PureComponent<Props, State> { componentDidMount() { this.mounted = true; - if (this.props.isFavorite && !this.context.currentUser.isLoggedIn) { + + const html = document.querySelector('html'); + if (html) { + html.classList.add('dashboard-page'); + } + + if (this.props.isFavorite && !isLoggedIn(this.props.currentUser)) { handleRequiredAuthentication(); return; } @@ -84,6 +92,12 @@ export default class AllProjects extends React.PureComponent<Props, State> { componentWillUnmount() { this.mounted = false; + + const html = document.querySelector('html'); + if (html) { + html.classList.remove('dashboard-page'); + } + const footer = document.getElementById('footer'); if (footer) { footer.classList.remove('page-footer-with-sidebar'); @@ -231,6 +245,7 @@ export default class AllProjects extends React.PureComponent<Props, State> { isFavorite={this.props.isFavorite} organization={this.props.organization} query={this.state.query} + showFavoriteFilter={!this.props.onSonarCloud} view={this.getView()} visualization={this.getVisualization()} /> @@ -245,7 +260,7 @@ export default class AllProjects extends React.PureComponent<Props, State> { <div className="layout-page-header-panel-inner layout-page-main-header-inner"> <div className="layout-page-main-inner"> <PageHeader - currentUser={this.context.currentUser} + currentUser={this.props.currentUser} isFavorite={this.props.isFavorite} loading={this.state.loading} onPerspectiveChange={this.handlePerspectiveChange} @@ -268,7 +283,7 @@ export default class AllProjects extends React.PureComponent<Props, State> { <div className="layout-page-main-inner"> {this.state.projects && ( <Visualizations - displayOrganizations={!this.props.organization && !!this.context.organizationsEnabled} + displayOrganizations={!this.props.organization && this.props.organizationsEnabled} projects={this.state.projects} sort={this.state.query.sort} total={this.state.total} @@ -299,7 +314,7 @@ export default class AllProjects extends React.PureComponent<Props, State> { render() { return ( - <div className="layout-page projects-page"> + <div className="layout-page projects-page" id="projects-page"> <Helmet title={translate('projects.page')} /> {this.renderSide()} diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.tsx new file mode 100644 index 00000000000..5eb37454226 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.tsx @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { connect } from 'react-redux'; +import { CurrentUser } from '../../../app/types'; +import { lazyLoad } from '../../../components/lazyLoad'; +import { + getCurrentUser, + areThereCustomOrganizations, + getGlobalSettingValue +} from '../../../store/rootReducer'; + +interface StateProps { + currentUser: CurrentUser; + onSonarCloud: boolean; + organizationsEnabled: boolean; +} + +const stateToProps = (state: any) => { + const onSonarCloudSetting = getGlobalSettingValue(state, 'sonar.sonarcloud.enabled'); + return { + currentUser: getCurrentUser(state), + onSonarCloud: Boolean(onSonarCloudSetting && onSonarCloudSetting.value === 'true'), + organizationsEnabled: areThereCustomOrganizations(state) + }; +}; + +export default connect<StateProps, any, any>(stateToProps)(lazyLoad(() => import('./AllProjects'))); diff --git a/server/sonar-web/src/main/js/apps/projects/components/App.tsx b/server/sonar-web/src/main/js/apps/projects/components/App.tsx deleted file mode 100644 index 0e605b910e8..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/components/App.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import { connect } from 'react-redux'; -import * as PropTypes from 'prop-types'; -import { - getCurrentUser, - getLanguages, - areThereCustomOrganizations -} from '../../../store/rootReducer'; - -interface Props { - currentUser: { isLoggedIn: boolean }; - languages: { [key: string]: { key: string; name: string } }; - organizationsEnabled: boolean; -} - -class App extends React.PureComponent<Props> { - static childContextTypes = { - currentUser: PropTypes.object.isRequired, - languages: PropTypes.object.isRequired, - organizationsEnabled: PropTypes.bool - }; - - getChildContext() { - return { - currentUser: this.props.currentUser, - languages: this.props.languages, - organizationsEnabled: this.props.organizationsEnabled - }; - } - - componentDidMount() { - const elem = document.querySelector('html'); - if (elem) { - elem.classList.add('dashboard-page'); - } - } - - componentWillUnmount() { - const elem = document.querySelector('html'); - if (elem) { - elem.classList.remove('dashboard-page'); - } - } - - render() { - return <div id="projects-page">{this.props.children}</div>; - } -} - -const mapStateToProps = (state: any) => ({ - currentUser: getCurrentUser(state), - languages: getLanguages(state), - organizationsEnabled: areThereCustomOrganizations(state) -}); - -export default connect<any, any, any>(mapStateToProps)(App); diff --git a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx index c5d4197f1be..56d148f82b2 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx @@ -19,12 +19,15 @@ */ import * as React from 'react'; import * as PropTypes from 'prop-types'; -import AllProjects from './AllProjects'; +import AllProjectsContainer from './AllProjectsContainer'; import { isFavoriteSet, isAllSet } from '../../../helpers/storage'; import { searchProjects } from '../../../api/components'; +import { CurrentUser, isLoggedIn } from '../../../app/types'; interface Props { + currentUser: CurrentUser; location: { pathname: string; query: { [x: string]: string } }; + onSonarCloud: boolean; } interface State { @@ -34,7 +37,6 @@ interface State { export default class DefaultPageSelector extends React.PureComponent<Props, State> { static contextTypes = { - currentUser: PropTypes.object.isRequired, router: PropTypes.object.isRequired }; @@ -44,22 +46,26 @@ export default class DefaultPageSelector extends React.PureComponent<Props, Stat } componentDidMount() { - this.defineIfShouldBeRedirected(); + if (!this.props.onSonarCloud) { + this.defineIfShouldBeRedirected(); + } } componentDidUpdate(prevProps: Props) { - if (prevProps.location !== this.props.location) { - this.defineIfShouldBeRedirected(); - } else if (this.state.shouldBeRedirected === true) { - this.context.router.replace({ ...this.props.location, pathname: '/projects/favorite' }); - } else if (this.state.shouldForceSorting != null) { - this.context.router.replace({ - ...this.props.location, - query: { - ...this.props.location.query, - sort: this.state.shouldForceSorting - } - }); + if (!this.props.onSonarCloud) { + if (prevProps.location !== this.props.location) { + this.defineIfShouldBeRedirected(); + } else if (this.state.shouldBeRedirected === true) { + this.context.router.replace({ ...this.props.location, pathname: '/projects/favorite' }); + } else if (this.state.shouldForceSorting != null) { + this.context.router.replace({ + ...this.props.location, + query: { + ...this.props.location.query, + sort: this.state.shouldForceSorting + } + }); + } } } @@ -67,7 +73,7 @@ export default class DefaultPageSelector extends React.PureComponent<Props, Stat if (Object.keys(this.props.location.query).length > 0) { // show ALL projects when there are some filters this.setState({ shouldBeRedirected: false, shouldForceSorting: undefined }); - } else if (!this.context.currentUser.isLoggedIn) { + } else if (!isLoggedIn(this.props.currentUser)) { // show ALL projects if user is anonymous if (!this.props.location.query || !this.props.location.query.sort) { // force default sorting to last analysis date @@ -92,11 +98,15 @@ export default class DefaultPageSelector extends React.PureComponent<Props, Stat } render() { + if (this.props.onSonarCloud) { + return <AllProjectsContainer isFavorite={true} location={this.props.location} />; + } + const { shouldBeRedirected, shouldForceSorting } = this.state; if (shouldBeRedirected == null || shouldBeRedirected === true || shouldForceSorting != null) { return null; } else { - return <AllProjects isFavorite={false} location={this.props.location} />; + return <AllProjectsContainer isFavorite={false} location={this.props.location} />; } } } diff --git a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelectorContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelectorContainer.tsx new file mode 100644 index 00000000000..d2c993f8bdc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelectorContainer.tsx @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { connect } from 'react-redux'; +import DefaultPageSelector from './DefaultPageSelector'; +import { CurrentUser } from '../../../app/types'; +import { getCurrentUser, getGlobalSettingValue } from '../../../store/rootReducer'; + +interface StateProps { + currentUser: CurrentUser; + onSonarCloud: boolean; +} + +const stateToProps = (state: any) => { + const onSonarCloudSetting = getGlobalSettingValue(state, 'sonar.sonarcloud.enabled'); + return { + currentUser: getCurrentUser(state), + onSonarCloud: Boolean(onSonarCloudSetting && onSonarCloudSetting.value === 'true') + }; +}; + +export default connect<StateProps>(stateToProps)(DefaultPageSelector); diff --git a/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx b/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx index 25c9d92209b..83ccd1a9176 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx @@ -18,22 +18,19 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as PropTypes from 'prop-types'; import { IndexLink, Link } from 'react-router'; import { translate } from '../../../helpers/l10n'; +import { CurrentUser, isLoggedIn } from '../../../app/types'; import { saveAll, saveFavorite } from '../../../helpers/storage'; import { RawQuery } from '../../../helpers/query'; interface Props { + currentUser: CurrentUser; organization?: { key: string }; query?: RawQuery; } export default class FavoriteFilter extends React.PureComponent<Props> { - static contextTypes = { - currentUser: PropTypes.object.isRequired - }; - handleSaveFavorite = () => { if (!this.props.organization) { saveFavorite(); @@ -47,7 +44,7 @@ export default class FavoriteFilter extends React.PureComponent<Props> { }; render() { - if (!this.context.currentUser.isLoggedIn) { + if (!isLoggedIn(this.props.currentUser)) { return null; } diff --git a/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilterContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilterContainer.tsx new file mode 100644 index 00000000000..260072166bb --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilterContainer.tsx @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import FavoriteFilter from './FavoriteFilter'; +import { withCurrentUser } from '../../../store/withCurrentUser'; + +export default withCurrentUser(FavoriteFilter); diff --git a/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.tsx index 1bfa07b70f2..bc104756b11 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.tsx @@ -18,8 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import AllProjects from './AllProjects'; +import AllProjectsContainer from './AllProjectsContainer'; export default function FavoriteProjectsContainer(props: any) { - return <AllProjects isFavorite={true} {...props} />; + return <AllProjectsContainer isFavorite={true} {...props} />; } diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx index c8de3ad3906..8b401d2a627 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx @@ -23,12 +23,13 @@ import SearchFilterContainer from '../filters/SearchFilterContainer'; import Tooltip from '../../../components/controls/Tooltip'; import PerspectiveSelect from './PerspectiveSelect'; import ProjectsSortingSelect from './ProjectsSortingSelect'; +import { CurrentUser, isLoggedIn } from '../../../app/types'; import { translate } from '../../../helpers/l10n'; import { RawQuery } from '../../../helpers/query'; import { Project } from '../types'; interface Props { - currentUser?: { isLoggedIn: boolean }; + currentUser: CurrentUser; isFavorite?: boolean; loading: boolean; onPerspectiveChange: (x: { view: string; visualization?: string }) => void; @@ -45,7 +46,7 @@ interface Props { export default function PageHeader(props: Props) { const { loading, total, projects, currentUser, view } = props; const limitReached = projects != null && total != null && projects.length < total; - const defaultOption = currentUser && currentUser.isLoggedIn ? 'name' : 'analysis_date'; + const defaultOption = isLoggedIn(currentUser) ? 'name' : 'analysis_date'; return ( <header className="page-header projects-topbar-items"> diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx index 6da09bf1157..52315fbefa5 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx @@ -20,8 +20,8 @@ import * as React from 'react'; import { Link } from 'react-router'; import { flatMap } from 'lodash'; -import FavoriteFilter from './FavoriteFilter'; -import LanguagesFilter from '../filters/LanguagesFilter'; +import FavoriteFilterContainer from './FavoriteFilterContainer'; +import LanguagesFilterContainer from '../filters/LanguagesFilterContainer'; import CoverageFilter from '../filters/CoverageFilter'; import DuplicationsFilter from '../filters/DuplicationsFilter'; import MaintainabilityFilter from '../filters/MaintainabilityFilter'; @@ -45,6 +45,7 @@ interface Props { isFavorite: boolean; organization?: { key: string }; query: RawQuery; + showFavoriteFilter: boolean; view: string; visualization: string; } @@ -71,7 +72,9 @@ export default function PageSidebar(props: Props) { return ( <div> - <FavoriteFilter query={linkQuery} organization={organization} /> + {props.showFavoriteFilter && ( + <FavoriteFilterContainer query={linkQuery} organization={organization} /> + )} <div className="projects-facets-header clearfix"> {isFiltered && ( @@ -156,7 +159,11 @@ export default function PageSidebar(props: Props) { value={query.new_lines} /> ]} - <LanguagesFilter {...facetProps} facet={facets && facets.languages} value={query.languages} /> + <LanguagesFilterContainer + {...facetProps} + facet={facets && facets.languages} + value={query.languages} + /> <TagsFilter {...facetProps} facet={facets && facets.tags} value={query.tags} /> </div> ); diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguages.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguages.tsx index 277a3b6eec8..76f581dd53a 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguages.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguages.tsx @@ -18,56 +18,47 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as PropTypes from 'prop-types'; import { sortBy } from 'lodash'; import Tooltip from '../../../components/controls/Tooltip'; import { translate } from '../../../helpers/l10n'; - -interface Languages { - [key: string]: { key: string; name: string }; -} +import { Languages } from '../../../store/languages/reducer'; interface Props { distribution?: string; + languages: Languages; } -export default class ProjectCardLanguages extends React.PureComponent<Props> { - static contextTypes = { - languages: PropTypes.object.isRequired - }; - - render() { - if (this.props.distribution === undefined) { - return null; - } +export default function ProjectCardLanguages({ distribution, languages }: Props) { + if (distribution === undefined) { + return null; + } - const parsedLanguages = this.props.distribution.split(';').map(item => item.split('=')); - const finalLanguages = sortBy(parsedLanguages, l => -1 * Number(l[1])).map(l => - getLanguageName(this.context.languages, l[0]) - ); + const parsedLanguages = distribution.split(';').map(item => item.split('=')); + const finalLanguages = sortBy(parsedLanguages, l => -1 * Number(l[1])).map(l => + getLanguageName(languages, l[0]) + ); - const tooltip = ( - <span> - {finalLanguages.map(language => ( - <span key={language}> - {language} - <br /> - </span> - ))} - </span> - ); + const tooltip = ( + <span> + {finalLanguages.map(language => ( + <span key={language}> + {language} + <br /> + </span> + ))} + </span> + ); - const languagesText = - finalLanguages.slice(0, 2).join(', ') + (finalLanguages.length > 2 ? ', ...' : ''); + const languagesText = + finalLanguages.slice(0, 2).join(', ') + (finalLanguages.length > 2 ? ', ...' : ''); - return ( - <div className="project-card-languages"> - <Tooltip placement="bottom" overlay={tooltip}> - <span>{languagesText}</span> - </Tooltip> - </div> - ); - } + return ( + <div className="project-card-languages"> + <Tooltip placement="bottom" overlay={tooltip}> + <span>{languagesText}</span> + </Tooltip> + </div> + ); } function getLanguageName(languages: Languages, key: string): string { diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguagesContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguagesContainer.tsx new file mode 100644 index 00000000000..8dbdca9f24a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguagesContainer.tsx @@ -0,0 +1,33 @@ +/* +* SonarQube +* Copyright (C) 2009-2017 SonarSource SA +* mailto:contact AT sonarsource DOT com +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 3 of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this program; if not, write to the Free Software Foundation, +* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ +import { connect } from 'react-redux'; +import ProjectCardLanguages from './ProjectCardLanguages'; +import { Languages } from '../../../store/languages/reducer'; +import { getLanguages } from '../../../store/rootReducer'; + +interface StateProps { + languages: Languages; +} + +const stateToProps = (state: any) => ({ + languages: getLanguages(state) +}); + +export default connect<StateProps>(stateToProps)(ProjectCardLanguages); diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx index d8ce816507c..8a5b4f6d216 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx @@ -23,7 +23,7 @@ import DateFromNow from '../../../components/intl/DateFromNow'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import ProjectCardQualityGate from './ProjectCardQualityGate'; import ProjectCardLeakMeasures from './ProjectCardLeakMeasures'; -import ProjectCardOrganization from './ProjectCardOrganization'; +import ProjectCardOrganizationContainer from './ProjectCardOrganizationContainer'; import Favorite from '../../../components/controls/Favorite'; import TagsList from '../../../components/tags/TagsList'; import PrivateBadge from '../../../components/common/PrivateBadge'; @@ -52,7 +52,9 @@ export default function ProjectCardLeak({ organization, project }: Props) { /> )} <h2 className="project-card-name"> - {!organization && <ProjectCardOrganization organization={project.organization} />} + {!organization && ( + <ProjectCardOrganizationContainer organization={project.organization} /> + )} <Link to={{ pathname: '/dashboard', query: { id: project.key } }}>{project.name}</Link> </h2> {project.analysisDate && <ProjectCardQualityGate status={measures!['alert_status']} />} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOrganization.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOrganization.tsx index ce8c98ffd81..db72f6cedc4 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOrganization.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOrganization.tsx @@ -18,31 +18,22 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as PropTypes from 'prop-types'; import OrganizationLink from '../../../components/ui/OrganizationLink'; interface Props { organization?: { key: string; name: string }; + organizationsEnabled: boolean; } -export default class ProjectCardOrganization extends React.PureComponent<Props> { - static contextTypes = { - organizationsEnabled: PropTypes.bool - }; - - render() { - const { organization } = this.props; - const { organizationsEnabled } = this.context; - - if (!organization || !organizationsEnabled) { - return null; - } - - return ( - <span className="text-normal"> - <OrganizationLink organization={organization}>{organization.name}</OrganizationLink> - <span className="slash-separator" /> - </span> - ); +export default function ProjectCardOrganization({ organization, organizationsEnabled }: Props) { + if (!organization || !organizationsEnabled) { + return null; } + + return ( + <span className="text-normal"> + <OrganizationLink organization={organization}>{organization.name}</OrganizationLink> + <span className="slash-separator" /> + </span> + ); } diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOrganizationContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOrganizationContainer.tsx new file mode 100644 index 00000000000..099ee541cba --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOrganizationContainer.tsx @@ -0,0 +1,32 @@ +/* +* SonarQube +* Copyright (C) 2009-2017 SonarSource SA +* mailto:contact AT sonarsource DOT com +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 3 of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this program; if not, write to the Free Software Foundation, +* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ +import { connect } from 'react-redux'; +import ProjectCardOrganization from './ProjectCardOrganization'; +import { areThereCustomOrganizations } from '../../../store/rootReducer'; + +interface StateProps { + organizationsEnabled: boolean; +} + +const stateToProps = (state: any) => ({ + organizationsEnabled: areThereCustomOrganizations(state) +}); + +export default connect<StateProps>(stateToProps)(ProjectCardOrganization); diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx index 7dcae1fe7a2..d8ecbe86f66 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx @@ -22,7 +22,7 @@ import { Link } from 'react-router'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import ProjectCardQualityGate from './ProjectCardQualityGate'; import ProjectCardOverallMeasures from './ProjectCardOverallMeasures'; -import ProjectCardOrganization from './ProjectCardOrganization'; +import ProjectCardOrganizationContainer from './ProjectCardOrganizationContainer'; import Favorite from '../../../components/controls/Favorite'; import TagsList from '../../../components/tags/TagsList'; import PrivateBadge from '../../../components/common/PrivateBadge'; @@ -51,7 +51,9 @@ export default function ProjectCardOverall({ organization, project }: Props) { /> )} <h2 className="project-card-name"> - {!organization && <ProjectCardOrganization organization={project.organization} />} + {!organization && ( + <ProjectCardOrganizationContainer organization={project.organization} /> + )} <Link to={{ pathname: '/dashboard', query: { id: project.key } }}>{project.name}</Link> </h2> {project.analysisDate && <ProjectCardQualityGate status={measures['alert_status']} />} diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx index 5bba45518c1..0fd1b4048ef 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import ProjectCardLanguages from './ProjectCardLanguages'; +import ProjectCardLanguagesContainer from './ProjectCardLanguagesContainer'; import Measure from '../../../components/measure/Measure'; import Rating from '../../../components/ui/Rating'; import CoverageRating from '../../../components/ui/CoverageRating'; @@ -152,7 +152,9 @@ export default function ProjectCardOverallMeasures({ measures }: Props) { </span> </div> <div className="project-card-measure-label"> - <ProjectCardLanguages distribution={measures['ncloc_language_distribution']} /> + <ProjectCardLanguagesContainer + distribution={measures['ncloc_language_distribution']} + /> </div> </div> </div> diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx index 8a12762240d..f3a02bcc1a7 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx @@ -20,7 +20,7 @@ /* eslint-disable import/order */ import * as React from 'react'; import { mount, shallow } from 'enzyme'; -import AllProjects from '../AllProjects'; +import AllProjects, { Props } from '../AllProjects'; import { getView, saveSort, saveView, saveVisualization } from '../../../../helpers/storage'; jest.mock('../ProjectsList', () => ({ @@ -168,19 +168,31 @@ it('changes perspective to risk visualization', () => { function mountRender(props: any = {}, push: Function = jest.fn(), replace: Function = jest.fn()) { return mount( <AllProjects + currentUser={{ isLoggedIn: true }} fetchProjects={jest.fn()} isFavorite={false} location={{ pathname: '/projects', query: {} }} {...props} />, - { context: { currentUser: { isLoggedIn: true }, router: { push, replace } } } + { context: { router: { push, replace } } } ); } -function shallowRender(props: any = {}, push: Function = jest.fn(), replace: Function = jest.fn()) { +function shallowRender( + props: Partial<Props> = {}, + push: Function = jest.fn(), + replace: Function = jest.fn() +) { const wrapper = shallow( - <AllProjects isFavorite={false} location={{ pathname: '/projects', query: {} }} {...props} />, - { context: { currentUser: { isLoggedIn: true }, router: { push, replace } } } + <AllProjects + currentUser={{ isLoggedIn: true }} + isFavorite={false} + location={{ pathname: '/projects', query: {} }} + onSonarCloud={false} + organizationsEnabled={false} + {...props} + />, + { context: { router: { push, replace } } } ); wrapper.setState({ loading: false, diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx index b5d80b02f7e..19e0932dd3a 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx @@ -18,9 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /* eslint-disable import/first, import/order */ -jest.mock('../AllProjects', () => ({ +jest.mock('../AllProjectsContainer', () => ({ // eslint-disable-next-line - default: function AllProjects() { + default: function AllProjectsContainer() { return null; } })); @@ -37,6 +37,7 @@ jest.mock('../../../../api/components', () => ({ import * as React from 'react'; import { mount } from 'enzyme'; import DefaultPageSelector from '../DefaultPageSelector'; +import { CurrentUser } from '../../../../app/types'; import { doAsync } from '../../../../helpers/testUtils'; const isFavoriteSet = require('../../../../helpers/storage').isFavoriteSet as jest.Mock<any>; @@ -84,8 +85,17 @@ it('fetches favorites', () => { }); }); -function mountRender(user: any = { isLoggedIn: true }, query: any = {}, replace: any = jest.fn()) { - return mount(<DefaultPageSelector location={{ pathname: '/projects', query }} />, { - context: { currentUser: user, router: { replace } } - }); +function mountRender( + currentUser: CurrentUser = { isLoggedIn: true }, + query: any = {}, + replace: any = jest.fn() +) { + return mount( + <DefaultPageSelector + currentUser={currentUser} + location={{ pathname: '/projects', query }} + onSonarCloud={false} + />, + { context: { router: { replace } } } + ); } diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx index 5b235e0a193..3f90ede583f 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx @@ -38,11 +38,11 @@ beforeEach(() => { }); it('renders for logged in user', () => { - expect(shallow(<FavoriteFilter query={query} />, { context: { currentUser } })).toMatchSnapshot(); + expect(shallow(<FavoriteFilter currentUser={currentUser} query={query} />)).toMatchSnapshot(); }); it('saves last selection', () => { - const wrapper = shallow(<FavoriteFilter query={query} />, { context: { currentUser } }); + const wrapper = shallow(<FavoriteFilter currentUser={currentUser} query={query} />); click(wrapper.find('#favorite-projects')); expect(saveFavorite).toBeCalled(); click(wrapper.find('#all-projects')); @@ -51,16 +51,16 @@ it('saves last selection', () => { it('handles organization', () => { expect( - shallow(<FavoriteFilter organization={{ key: 'org' }} query={query} />, { - context: { currentUser } - }) + shallow( + <FavoriteFilter currentUser={currentUser} organization={{ key: 'org' }} query={query} /> + ) ).toMatchSnapshot(); }); it('does not save last selection with organization', () => { - const wrapper = shallow(<FavoriteFilter organization={{ key: 'org' }} query={query} />, { - context: { currentUser } - }); + const wrapper = shallow( + <FavoriteFilter currentUser={currentUser} organization={{ key: 'org' }} query={query} /> + ); click(wrapper.find('#favorite-projects')); expect(saveFavorite).not.toBeCalled(); click(wrapper.find('#all-projects')); @@ -69,8 +69,6 @@ it('does not save last selection with organization', () => { it('does not render for anonymous', () => { expect( - shallow(<FavoriteFilter query={query} />, { - context: { currentUser: { isLoggedIn: false } } - }).type() + shallow(<FavoriteFilter currentUser={{ isLoggedIn: false }} query={query} />).type() ).toBeNull(); }); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx index 86ef27cc3a5..279297daf13 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx @@ -70,6 +70,7 @@ it('should render switch the default sorting option for anonymous users', () => function shallowRender(props?: {}) { return shallow( <PageHeader + currentUser={{ isLoggedIn: false }} loading={false} onPerspectiveChange={jest.fn()} onSortChange={jest.fn()} diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.tsx index b6ba776c0da..e19492bd40e 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.tsx @@ -23,14 +23,26 @@ import PageSidebar from '../PageSidebar'; it('should render correctly', () => { const sidebar = shallow( - <PageSidebar query={{ size: '3' }} view="overall" visualization="risk" isFavorite={true} /> + <PageSidebar + isFavorite={true} + query={{ size: '3' }} + showFavoriteFilter={true} + view="overall" + visualization="risk" + /> ); expect(sidebar).toMatchSnapshot(); }); it('should render `leak` view correctly', () => { const sidebar = shallow( - <PageSidebar query={{ view: 'leak' }} view="leak" visualization="risk" isFavorite={false} /> + <PageSidebar + isFavorite={false} + query={{ view: 'leak' }} + showFavoriteFilter={true} + view="leak" + visualization="risk" + /> ); expect(sidebar).toMatchSnapshot(); }); @@ -38,10 +50,11 @@ it('should render `leak` view correctly', () => { it('reset function should work correctly with view and visualizations', () => { const sidebar = shallow( <PageSidebar + isFavorite={false} query={{ view: 'visualizations', visualization: 'bugs' }} + showFavoriteFilter={true} view="visualizations" visualization="bugs" - isFavorite={false} /> ); expect(sidebar.find('.projects-facets-reset').exists()).toBeFalsy(); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLanguages-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLanguages-test.tsx index c81dffce5e4..ec986854e76 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLanguages-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLanguages-test.tsx @@ -28,26 +28,26 @@ const languages = { it('renders', () => { expect( - shallow(<ProjectCardLanguages distribution="java=137;js=15" />, { context: { languages } }) + shallow(<ProjectCardLanguages distribution="java=137;js=15" languages={languages} />) ).toMatchSnapshot(); }); it('sorts languages', () => { expect( - shallow(<ProjectCardLanguages distribution="java=13;js=152" />, { context: { languages } }) + shallow(<ProjectCardLanguages distribution="java=13;js=152" languages={languages} />) ).toMatchSnapshot(); }); it('handles unknown languages', () => { expect( - shallow(<ProjectCardLanguages distribution="java=13;cpp=18" />, { context: { languages } }) + shallow(<ProjectCardLanguages distribution="java=13;cpp=18" languages={languages} />) ).toMatchSnapshot(); expect( - shallow(<ProjectCardLanguages distribution="java=13;<null>=18" />, { context: { languages } }) + shallow(<ProjectCardLanguages distribution="java=13;<null>=18" languages={languages} />) ).toMatchSnapshot(); }); it('does not render', () => { - expect(shallow(<ProjectCardLanguages />, { context: { languages } }).type()).toBeNull(); + expect(shallow(<ProjectCardLanguages languages={languages} />).type()).toBeNull(); }); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap index ef7225c06eb..14e6af9b4e5 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap @@ -3,6 +3,7 @@ exports[`renders 1`] = ` <div className="layout-page projects-page" + id="projects-page" > <HelmetWrapper defer={true} @@ -51,6 +52,7 @@ exports[`renders 1`] = ` "visualization": undefined, } } + showFavoriteFilter={true} view="overall" visualization="risk" /> @@ -174,6 +176,7 @@ exports[`renders 1`] = ` exports[`renders 2`] = ` <div className="layout-page projects-page" + id="projects-page" > <HelmetWrapper defer={true} @@ -204,6 +207,7 @@ exports[`renders 2`] = ` "view": "visualizations", } } + showFavoriteFilter={true} view="visualizations" visualization="risk" /> diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.tsx.snap index 78961fde19f..cabe4555b86 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.tsx.snap @@ -25,7 +25,7 @@ exports[`reset function should work correctly with view and visualizations 1`] = exports[`should render \`leak\` view correctly 1`] = ` <div> - <FavoriteFilter + <Connect(FavoriteFilter) query={ Object { "view": "leak", @@ -101,7 +101,7 @@ exports[`should render \`leak\` view correctly 1`] = ` } } /> - <LanguagesFilter + <Connect(LanguagesFilter) isFavorite={false} query={ Object { @@ -122,7 +122,7 @@ exports[`should render \`leak\` view correctly 1`] = ` exports[`should render correctly 1`] = ` <div> - <FavoriteFilter /> + <Connect(FavoriteFilter) /> <div className="projects-facets-header clearfix" > @@ -210,7 +210,7 @@ exports[`should render correctly 1`] = ` } value="3" /> - <LanguagesFilter + <Connect(LanguagesFilter) isFavorite={true} query={ Object { diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap index e293ca6efc9..e53e1df3fe9 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap @@ -11,7 +11,7 @@ exports[`should display the leak measures and quality gate 1`] = ` <h2 className="project-card-name" > - <ProjectCardOrganization + <Connect(ProjectCardOrganization) organization={ Object { "key": "org", diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap index 7fdfbd65dbe..115f01c7533 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap @@ -11,7 +11,7 @@ exports[`should display the overall measures and quality gate 1`] = ` <h2 className="project-card-name" > - <ProjectCardOrganization + <Connect(ProjectCardOrganization) organization={ Object { "key": "org", diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap index ed38e592644..74ef1f08cfe 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap @@ -280,7 +280,7 @@ exports[`should render correctly with all data 1`] = ` <div className="project-card-measure-label" > - <ProjectCardLanguages /> + <Connect(ProjectCardLanguages) /> </div> </div> </div> @@ -320,7 +320,7 @@ exports[`should render ncloc correctly 1`] = ` <div className="project-card-measure-label" > - <ProjectCardLanguages /> + <Connect(ProjectCardLanguages) /> </div> </div> </div> diff --git a/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilter.tsx index 385425c48f6..fce64b5de4c 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilter.tsx +++ b/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilter.tsx @@ -18,19 +18,19 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as PropTypes from 'prop-types'; import { difference, sortBy } from 'lodash'; import Filter from './Filter'; import FilterHeader from './FilterHeader'; import SearchableFilterFooter from './SearchableFilterFooter'; import SearchableFilterOption from './SearchableFilterOption'; -import { getLanguageByKey } from '../../../store/languages/reducer'; +import { getLanguageByKey, Languages } from '../../../store/languages/reducer'; import { translate } from '../../../helpers/l10n'; import { Facet } from '../types'; interface Props { facet?: Facet; isFavorite?: boolean; + languages: Languages; maxFacetValue?: number; organization?: { key: string }; property?: string; @@ -41,18 +41,14 @@ interface Props { const LIST_SIZE = 10; export default class LanguagesFilter extends React.Component<Props> { - static contextTypes = { - languages: PropTypes.object.isRequired - }; - getSearchOptions = () => { - let languageKeys = Object.keys(this.context.languages); + let languageKeys = Object.keys(this.props.languages); if (this.props.facet) { languageKeys = difference(languageKeys, Object.keys(this.props.facet)); } return languageKeys .slice(0, LIST_SIZE) - .map(key => ({ label: this.context.languages[key].name, value: key })); + .map(key => ({ label: this.props.languages[key].name, value: key })); }; getSortedOptions = (facet: Facet = {}) => @@ -63,7 +59,7 @@ export default class LanguagesFilter extends React.Component<Props> { renderOption = (option: string) => ( <SearchableFilterOption optionKey={option} - option={getLanguageByKey(this.context.languages, option)} + option={getLanguageByKey(this.props.languages, option)} /> ); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.tsx b/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.tsx new file mode 100644 index 00000000000..f6de2415a99 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.tsx @@ -0,0 +1,33 @@ +/* +* SonarQube +* Copyright (C) 2009-2017 SonarSource SA +* mailto:contact AT sonarsource DOT com +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 3 of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this program; if not, write to the Free Software Foundation, +* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ +import { connect } from 'react-redux'; +import LanguagesFilter from './LanguagesFilter'; +import { Languages } from '../../../store/languages/reducer'; +import { getLanguages } from '../../../store/rootReducer'; + +interface StateProps { + languages: Languages; +} + +const stateToProps = (state: any) => ({ + languages: getLanguages(state) +}); + +export default connect<StateProps>(stateToProps)(LanguagesFilter); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx index 27ea25d17dd..ff0bf4f8b37 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx @@ -33,21 +33,21 @@ const languages = { const languagesFacet = { java: 39, cs: 4, js: 1 }; it('should render the languages without the ones in the facet', () => { - const wrapper = shallow(<LanguagesFilter query={{ languages: null }} facet={languagesFacet} />, { - context: { languages } - }); + const wrapper = shallow( + <LanguagesFilter facet={languagesFacet} languages={languages} query={{ languages: null }} /> + ); expect(wrapper).toMatchSnapshot(); }); it('should render the languages facet with the selected languages', () => { const wrapper = shallow( <LanguagesFilter - query={{ languages: ['java', 'cs'] }} - value={['java', 'cs']} facet={languagesFacet} isFavorite={true} - />, - { context: { languages } } + languages={languages} + query={{ languages: ['java', 'cs'] }} + value={['java', 'cs']} + /> ); expect(wrapper).toMatchSnapshot(); expect(wrapper.find('Filter').shallow()).toMatchSnapshot(); @@ -68,12 +68,12 @@ it('should render maximum 10 languages in the searchbox results', () => { }; const wrapper = shallow( <LanguagesFilter - query={{ languages: ['java', 'g'] }} - value={['java', 'g']} facet={{ ...languagesFacet, g: 1 }} isFavorite={true} - />, - { context: { languages: manyLanguages } } + languages={manyLanguages} + query={{ languages: ['java', 'g'] }} + value={['java', 'g']} + /> ); expect(wrapper).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/projects/routes.ts b/server/sonar-web/src/main/js/apps/projects/routes.ts index c6f3df309f5..8d260c75965 100644 --- a/server/sonar-web/src/main/js/apps/projects/routes.ts +++ b/server/sonar-web/src/main/js/apps/projects/routes.ts @@ -17,37 +17,21 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { RouterState, IndexRouteProps, RouteComponent, RedirectFunction } from 'react-router'; +import { RouterState, RedirectFunction } from 'react-router'; +import DefaultPageSelectorContainer from './components/DefaultPageSelectorContainer'; +import FavoriteProjectsContainer from './components/FavoriteProjectsContainer'; import { saveAll } from '../../helpers/storage'; const routes = [ + { indexRoute: { component: DefaultPageSelectorContainer } }, { - getComponent(_: RouterState, callback: (err: any, component: RouteComponent) => any) { - import('./components/App').then(i => callback(null, i.default)); - }, - childRoutes: [ - { - getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) { - import('./components/DefaultPageSelector').then(i => - callback(null, { component: i.default }) - ); - } - }, - { - path: 'all', - onEnter(_: RouterState, replace: RedirectFunction) { - saveAll(); - replace('/projects'); - } - }, - { - path: 'favorite', - getComponent(_: RouterState, callback: (err: any, component: RouteComponent) => any) { - import('./components/FavoriteProjectsContainer').then(i => callback(null, i.default)); - } - } - ] - } + path: 'all', + onEnter(_: RouterState, replace: RedirectFunction) { + saveAll(); + replace('/projects'); + } + }, + { path: 'favorite', component: FavoriteProjectsContainer } ]; export default routes; diff --git a/server/sonar-web/src/main/js/components/lazyLoad.tsx b/server/sonar-web/src/main/js/components/lazyLoad.tsx new file mode 100644 index 00000000000..dfc83a1e939 --- /dev/null +++ b/server/sonar-web/src/main/js/components/lazyLoad.tsx @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; + +interface Loader { + (): Promise<{ default: React.ComponentClass }>; +} + +export function lazyLoad(loader: Loader) { + interface State { + Component?: React.ComponentClass; + } + + // use `React.Component`, not `React.PureComponent` to always re-render + // and let the child component decide if it needs to change + return class LazyLoader extends React.Component<any, State> { + mounted: boolean; + state: State = {}; + + componentDidMount() { + this.mounted = true; + loader().then(i => this.receiveComponent(i.default), () => {}); + } + + componentWillUnmount() { + this.mounted = false; + } + + receiveComponent = (Component: React.ComponentClass) => { + if (this.mounted) { + this.setState({ Component }); + } + }; + + render() { + const { Component } = this.state; + + if (!Component) { + return null; + } + + return <Component {...this.props} />; + } + }; +} diff --git a/server/sonar-web/src/main/js/store/languages/reducer.js b/server/sonar-web/src/main/js/store/languages/reducer.ts index 52177241e61..a137ef7c0b5 100644 --- a/server/sonar-web/src/main/js/store/languages/reducer.js +++ b/server/sonar-web/src/main/js/store/languages/reducer.ts @@ -20,7 +20,11 @@ import { keyBy } from 'lodash'; import { RECEIVE_LANGUAGES } from './actions'; -const reducer = (state = {}, action = {}) => { +export interface Languages { + [key: string]: { key: string; name: string }; +} + +const reducer = (state: Languages = {}, action: any = {}) => { if (action.type === RECEIVE_LANGUAGES) { return keyBy(action.languages, 'key'); } @@ -30,6 +34,6 @@ const reducer = (state = {}, action = {}) => { export default reducer; -export const getLanguages = state => state; +export const getLanguages = (state: Languages) => state; -export const getLanguageByKey = (state, key) => state[key]; +export const getLanguageByKey = (state: Languages, key: string) => state[key]; diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.tsx b/server/sonar-web/src/main/js/store/withCurrentUser.tsx index d2413620662..1a2d815e5a8 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.tsx +++ b/server/sonar-web/src/main/js/store/withCurrentUser.tsx @@ -1,7 +1,7 @@ /* * SonarQube * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com + * mailto:contact AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -17,25 +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 * as React from 'react'; -import App from '../../projects/components/App'; -import AllProjects from '../../projects/components/AllProjects'; +import { getCurrentUser } from './rootReducer'; +import { CurrentUser } from '../app/types'; -interface Props { - location: { pathname: string; query: { [x: string]: string } }; - organization: { key: string }; +interface StateProps { + currentUser: CurrentUser; } -export default function OrganizationFavoriteProjects(props: Props) { - return ( - <div id="projects-page"> - <App> - <AllProjects - isFavorite={true} - location={props.location} - organization={props.organization} - /> - </App> - </div> - ); +export function withCurrentUser<P extends StateProps>(Component: React.ComponentClass<P>) { + function mapStateToProps(state: any): StateProps { + return { currentUser: getCurrentUser(state) }; + } + + return connect<StateProps>(mapStateToProps)(Component); } |