@@ -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> | |||
); |
@@ -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; | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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', |
@@ -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()} |
@@ -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'))); |
@@ -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); |
@@ -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} />; | |||
} | |||
} | |||
} |
@@ -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); |
@@ -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; | |||
} | |||
@@ -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); |
@@ -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} />; | |||
} |
@@ -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"> |
@@ -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> | |||
); |
@@ -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 { |
@@ -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); |
@@ -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']} />} |
@@ -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> | |||
); | |||
} |
@@ -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); |
@@ -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']} />} |
@@ -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> |
@@ -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, |
@@ -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 } } } | |||
); | |||
} |
@@ -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(); | |||
}); |
@@ -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()} |
@@ -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(); |
@@ -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(); | |||
}); |
@@ -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" | |||
/> |
@@ -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 { |
@@ -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", |
@@ -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", |
@@ -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> |
@@ -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)} | |||
/> | |||
); | |||
@@ -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); |
@@ -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(); | |||
}); |
@@ -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; |
@@ -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} />; | |||
} | |||
}; | |||
} |
@@ -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]; |
@@ -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); | |||
} |
@@ -97,6 +97,7 @@ more_x={0} more | |||
more_actions=More Actions | |||
my_favorite=My Favorite | |||
my_favorites=My Favorites | |||
my_projects=My Projects | |||
name=Name | |||
navigation=Navigation | |||
never=Never |