@@ -1,53 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { Button } from '../../../components/controls/buttons'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export interface PortfolioNewCodeToggleProps { | |||
enabled: boolean; | |||
showNewCode: boolean; | |||
onNewCodeToggle: (newSelected: boolean) => void; | |||
} | |||
export default function PortfolioNewCodeToggle(props: PortfolioNewCodeToggleProps) { | |||
const { showNewCode, enabled } = props; | |||
return ( | |||
<Tooltip | |||
overlay={translate('code_viewer.portfolio_code_toggle_disabled.help')} | |||
visible={enabled ? false : undefined}> | |||
<div className="big-spacer-right button-group"> | |||
<Button | |||
disabled={!enabled} | |||
className={showNewCode ? 'button-active' : undefined} | |||
onClick={() => props.onNewCodeToggle(true)}> | |||
{translate('projects.view.new_code')} | |||
</Button> | |||
<Button | |||
disabled={!enabled} | |||
className={showNewCode ? undefined : 'button-active'} | |||
onClick={() => props.onNewCodeToggle(false)}> | |||
{translate('projects.view.overall_code')} | |||
</Button> | |||
</div> | |||
</Tooltip> | |||
); | |||
} |
@@ -20,6 +20,7 @@ | |||
import { isEmpty, omit } from 'lodash'; | |||
import * as React from 'react'; | |||
import { getTree } from '../../../api/components'; | |||
import ButtonToggle from '../../../components/controls/ButtonToggle'; | |||
import SearchBox from '../../../components/controls/SearchBox'; | |||
import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; | |||
import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | |||
@@ -28,7 +29,6 @@ import { KeyboardKeys } from '../../../helpers/keycodes'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { ComponentMeasure } from '../../../types/types'; | |||
import PortfolioNewCodeToggle from './PortfolioNewCodeToggle'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
@@ -140,10 +140,23 @@ export class Search extends React.PureComponent<Props, State> { | |||
return ( | |||
<div className="code-search" id="code-search"> | |||
{isPortfolio && ( | |||
<PortfolioNewCodeToggle | |||
enabled={isEmpty(query)} | |||
onNewCodeToggle={this.props.onNewCodeToggle} | |||
showNewCode={newCodeSelected} | |||
<ButtonToggle | |||
name="portfolio-scope" | |||
className="big-spacer-right" | |||
options={[ | |||
{ | |||
value: true, | |||
label: translate('projects.view.new_code'), | |||
disabled: !isEmpty(query) | |||
}, | |||
{ | |||
value: false, | |||
label: translate('projects.view.overall_code'), | |||
disabled: !isEmpty(query) | |||
} | |||
]} | |||
value={newCodeSelected} | |||
onCheck={this.props.onNewCodeToggle} | |||
/> | |||
)} | |||
<SearchBox |
@@ -1,64 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { Button } from '../../../../components/controls/buttons'; | |||
import Tooltip from '../../../../components/controls/Tooltip'; | |||
import PortfolioNewCodeToggle, { PortfolioNewCodeToggleProps } from '../PortfolioNewCodeToggle'; | |||
it('renders correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should show tooltips when disabled', () => { | |||
const wrapper = shallowRender({ enabled: false }); | |||
expect(wrapper.find(Tooltip).props().visible).toBeUndefined(); | |||
wrapper.setProps({ enabled: true }); | |||
expect(wrapper.find(Tooltip).props().visible).toBe(false); | |||
}); | |||
it('should toggle correctly', () => { | |||
const onNewCodeToggle = jest.fn(); | |||
const wrapper = shallowRender({ onNewCodeToggle }); | |||
wrapper | |||
.find(Button) | |||
.at(1) | |||
.simulate('click'); | |||
expect(onNewCodeToggle).toBeCalledWith(false); | |||
wrapper | |||
.find(Button) | |||
.at(0) | |||
.simulate('click'); | |||
expect(onNewCodeToggle).toBeCalledWith(true); | |||
}); | |||
function shallowRender(props?: Partial<PortfolioNewCodeToggleProps>) { | |||
return shallow( | |||
<PortfolioNewCodeToggle | |||
showNewCode={true} | |||
enabled={true} | |||
onNewCodeToggle={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -1,26 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders correctly 1`] = ` | |||
<Tooltip | |||
overlay="code_viewer.portfolio_code_toggle_disabled.help" | |||
visible={false} | |||
> | |||
<div | |||
className="big-spacer-right button-group" | |||
> | |||
<Button | |||
className="button-active" | |||
disabled={false} | |||
onClick={[Function]} | |||
> | |||
projects.view.new_code | |||
</Button> | |||
<Button | |||
disabled={false} | |||
onClick={[Function]} | |||
> | |||
projects.view.overall_code | |||
</Button> | |||
</div> | |||
</Tooltip> | |||
`; |
@@ -24,10 +24,26 @@ exports[`should render correcly: new code toggle for portfolio 1`] = ` | |||
className="code-search" | |||
id="code-search" | |||
> | |||
<PortfolioNewCodeToggle | |||
enabled={true} | |||
onNewCodeToggle={[MockFunction]} | |||
showNewCode={false} | |||
<ButtonToggle | |||
className="big-spacer-right" | |||
disabled={false} | |||
name="portfolio-scope" | |||
onCheck={[MockFunction]} | |||
options={ | |||
Array [ | |||
Object { | |||
"disabled": false, | |||
"label": "projects.view.new_code", | |||
"value": true, | |||
}, | |||
Object { | |||
"disabled": false, | |||
"label": "projects.view.overall_code", | |||
"value": false, | |||
}, | |||
] | |||
} | |||
value={false} | |||
/> | |||
<SearchBox | |||
minLength={3} | |||
@@ -48,10 +64,26 @@ exports[`should render correcly: new code toggle for portfolio disabled 1`] = ` | |||
className="code-search" | |||
id="code-search" | |||
> | |||
<PortfolioNewCodeToggle | |||
enabled={false} | |||
onNewCodeToggle={[MockFunction]} | |||
showNewCode={false} | |||
<ButtonToggle | |||
className="big-spacer-right" | |||
disabled={false} | |||
name="portfolio-scope" | |||
onCheck={[MockFunction]} | |||
options={ | |||
Array [ | |||
Object { | |||
"disabled": true, | |||
"label": "projects.view.new_code", | |||
"value": true, | |||
}, | |||
Object { | |||
"disabled": true, | |||
"label": "projects.view.overall_code", | |||
"value": false, | |||
}, | |||
] | |||
} | |||
value={false} | |||
/> | |||
<SearchBox | |||
minLength={3} |
@@ -31,6 +31,7 @@ import EmptySearch from '../../../components/common/EmptySearch'; | |||
import FiltersHeader from '../../../components/common/FiltersHeader'; | |||
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; | |||
import { Button } from '../../../components/controls/buttons'; | |||
import ButtonToggle from '../../../components/controls/ButtonToggle'; | |||
import Checkbox from '../../../components/controls/Checkbox'; | |||
import HelpTooltip from '../../../components/controls/HelpTooltip'; | |||
import ListFooter from '../../../components/controls/ListFooter'; | |||
@@ -94,7 +95,6 @@ import BulkChangeModal, { MAX_PAGE_SIZE } from './BulkChangeModal'; | |||
import IssuesList from './IssuesList'; | |||
import IssuesSourceViewer from './IssuesSourceViewer'; | |||
import IssueTabViewer from './IssueTabViewer'; | |||
import MyIssuesFilter from './MyIssuesFilter'; | |||
import NoIssues from './NoIssues'; | |||
import NoMyIssues from './NoMyIssues'; | |||
import PageActions from './PageActions'; | |||
@@ -884,10 +884,17 @@ export class App extends React.PureComponent<Props, State> { | |||
return ( | |||
<div className="layout-page-filters"> | |||
{currentUser.isLoggedIn && ( | |||
<MyIssuesFilter | |||
myIssues={this.state.myIssues} | |||
onMyIssuesChange={this.handleMyIssuesChange} | |||
/> | |||
<div className="display-flex-justify-center big-spacer-bottom"> | |||
<ButtonToggle | |||
name="my-issue-filter" | |||
options={[ | |||
{ value: true, label: translate('issues.my_issues') }, | |||
{ value: false, label: translate('all') } | |||
]} | |||
value={this.state.myIssues} | |||
onCheck={this.handleMyIssuesChange} | |||
/> | |||
</div> | |||
)} | |||
<FiltersHeader displayReset={this.isFiltered()} onReset={this.handleReset} /> | |||
<Sidebar |
@@ -1,54 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { Button } from '../../../components/controls/buttons'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
myIssues: boolean; | |||
onMyIssuesChange: (myIssues: boolean) => void; | |||
} | |||
export default class MyIssuesFilter extends React.PureComponent<Props> { | |||
handleClick = (myIssues: boolean) => () => { | |||
this.props.onMyIssuesChange(myIssues); | |||
}; | |||
render() { | |||
const { myIssues } = this.props; | |||
return ( | |||
<div className="issues-my-issues-filter"> | |||
<div className="button-group"> | |||
<Button | |||
className={myIssues ? 'button-active' : undefined} | |||
onClick={this.handleClick(true)}> | |||
{translate('issues.my_issues')} | |||
</Button> | |||
<Button | |||
className={myIssues ? undefined : 'button-active'} | |||
onClick={this.handleClick(false)}> | |||
{translate('all')} | |||
</Button> | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -57,10 +57,28 @@ exports[`should show warnning when not all projects are accessible 1`] = ` | |||
<div | |||
className="layout-page-filters" | |||
> | |||
<MyIssuesFilter | |||
myIssues={false} | |||
onMyIssuesChange={[Function]} | |||
/> | |||
<div | |||
className="display-flex-justify-center big-spacer-bottom" | |||
> | |||
<ButtonToggle | |||
disabled={false} | |||
name="my-issue-filter" | |||
onCheck={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "issues.my_issues", | |||
"value": true, | |||
}, | |||
Object { | |||
"label": "all", | |||
"value": false, | |||
}, | |||
] | |||
} | |||
value={false} | |||
/> | |||
</div> | |||
<FiltersHeader | |||
displayReset={true} | |||
onReset={[Function]} |
@@ -150,11 +150,6 @@ | |||
padding: var(--gridSize); | |||
} | |||
.issues-my-issues-filter { | |||
margin-bottom: 24px; | |||
text-align: center; | |||
} | |||
.issues-page-actions { | |||
display: inline-block; | |||
min-width: 80px; |
@@ -18,22 +18,20 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { NavLink } from 'react-router-dom'; | |||
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; | |||
import ButtonToggle from '../../../components/controls/ButtonToggle'; | |||
import { withRouter, WithRouterProps } from '../../../components/hoc/withRouter'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { save } from '../../../helpers/storage'; | |||
import { queryToSearch } from '../../../helpers/urls'; | |||
import { RawQuery } from '../../../types/types'; | |||
import { CurrentUser, isLoggedIn } from '../../../types/users'; | |||
import { PROJECTS_ALL, PROJECTS_DEFAULT_FILTER, PROJECTS_FAVORITE } from '../utils'; | |||
interface Props { | |||
interface Props extends WithRouterProps { | |||
currentUser: CurrentUser; | |||
query?: RawQuery; | |||
} | |||
const linkClass = ({ isActive }: { isActive: boolean }) => | |||
isActive ? 'button button-active' : 'button'; | |||
export const FAVORITE_PATHNAME = '/projects/favorite'; | |||
export const ALL_PATHNAME = '/projects'; | |||
export class FavoriteFilter extends React.PureComponent<Props> { | |||
handleSaveFavorite = () => { | |||
@@ -44,38 +42,39 @@ export class FavoriteFilter extends React.PureComponent<Props> { | |||
save(PROJECTS_DEFAULT_FILTER, PROJECTS_ALL); | |||
}; | |||
onFavoriteChange = (favorite: boolean) => { | |||
if (favorite) { | |||
this.handleSaveFavorite(); | |||
this.props.router.push(FAVORITE_PATHNAME); | |||
} else { | |||
this.handleSaveAll(); | |||
this.props.router.push(ALL_PATHNAME); | |||
} | |||
}; | |||
render() { | |||
const { | |||
location: { pathname } | |||
} = this.props; | |||
if (!isLoggedIn(this.props.currentUser)) { | |||
return null; | |||
} | |||
const pathnameForFavorite = '/projects/favorite'; | |||
const pathnameForAll = '/projects'; | |||
const search = queryToSearch(this.props.query); | |||
return ( | |||
<div className="page-header text-center"> | |||
<div className="button-group little-spacer-top"> | |||
<NavLink | |||
className={linkClass} | |||
id="favorite-projects" | |||
onClick={this.handleSaveFavorite} | |||
to={{ pathname: pathnameForFavorite, search }}> | |||
{translate('my_favorites')} | |||
</NavLink> | |||
<NavLink | |||
end={true} | |||
className={linkClass} | |||
id="all-projects" | |||
onClick={this.handleSaveAll} | |||
to={{ pathname: pathnameForAll, search }}> | |||
{translate('all')} | |||
</NavLink> | |||
</div> | |||
<ButtonToggle | |||
name="favorite-filter" | |||
options={[ | |||
{ value: true, label: translate('my_favorites') }, | |||
{ value: false, label: translate('all') } | |||
]} | |||
onCheck={this.onFavoriteChange} | |||
value={pathname === FAVORITE_PATHNAME} | |||
/> | |||
</div> | |||
); | |||
} | |||
} | |||
export default withCurrentUserContext(FavoriteFilter); | |||
export default withRouter(withCurrentUserContext(FavoriteFilter)); |
@@ -59,14 +59,9 @@ export default function PageSidebar(props: PageSidebarProps) { | |||
const maxFacetValue = getMaxFacetValue(facets); | |||
const facetProps = { onQueryChange, maxFacetValue }; | |||
let linkQuery: RawQuery | undefined = undefined; | |||
if (view !== 'overall') { | |||
linkQuery = { view }; | |||
} | |||
return ( | |||
<div> | |||
<FavoriteFilter query={linkQuery} /> | |||
<FavoriteFilter /> | |||
<div className="projects-facets-header clearfix"> | |||
{isFiltered && <ClearAll onClearAll={props.onClearAll} />} |
@@ -21,9 +21,14 @@ import { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import { save } from '../../../../helpers/storage'; | |||
import { mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks'; | |||
import { | |||
mockCurrentUser, | |||
mockLocation, | |||
mockLoggedInUser, | |||
mockRouter | |||
} from '../../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { FavoriteFilter } from '../FavoriteFilter'; | |||
import { ALL_PATHNAME, FavoriteFilter, FAVORITE_PATHNAME } from '../FavoriteFilter'; | |||
jest.mock('../../../../helpers/storage', () => ({ | |||
save: jest.fn() | |||
@@ -39,16 +44,20 @@ it('renders for logged in user', () => { | |||
expect(screen.queryByText('all')).toBeInTheDocument(); | |||
}); | |||
it('saves last selection', async () => { | |||
const user = userEvent.setup(); | |||
it.each([ | |||
['my_favorites', 'favorite', ALL_PATHNAME], | |||
['all', 'all', FAVORITE_PATHNAME] | |||
])( | |||
'saves last selection', | |||
async (optionTranslationId: string, localStorageValue: string, initialPathName: string) => { | |||
const user = userEvent.setup(); | |||
renderFavoriteFilter(); | |||
renderFavoriteFilter({ location: mockLocation({ pathname: initialPathName }) }); | |||
await user.click(screen.getByText('my_favorites')); | |||
expect(save).toBeCalledWith('sonarqube.projects.default', 'favorite'); | |||
await user.click(screen.getByText('all')); | |||
expect(save).toBeCalledWith('sonarqube.projects.default', 'all'); | |||
}); | |||
await user.click(screen.getByText(optionTranslationId)); | |||
expect(save).toHaveBeenLastCalledWith('sonarqube.projects.default', localStorageValue); | |||
} | |||
); | |||
it('does not render for anonymous', () => { | |||
renderFavoriteFilter({ currentUser: mockCurrentUser() }); | |||
@@ -57,7 +66,14 @@ it('does not render for anonymous', () => { | |||
function renderFavoriteFilter({ | |||
currentUser = mockLoggedInUser(), | |||
query = { size: 1 } | |||
location = mockLocation() | |||
}: Partial<FavoriteFilter['props']> = {}) { | |||
renderComponent(<FavoriteFilter currentUser={currentUser} query={query} />); | |||
renderComponent( | |||
<FavoriteFilter | |||
currentUser={currentUser} | |||
location={location} | |||
router={mockRouter()} | |||
params={{}} | |||
/> | |||
); | |||
} |
@@ -2,13 +2,7 @@ | |||
exports[`should render \`leak\` view correctly 1`] = ` | |||
<div> | |||
<withCurrentUserContext(FavoriteFilter) | |||
query={ | |||
Object { | |||
"view": "leak", | |||
} | |||
} | |||
/> | |||
<withRouter(withCurrentUserContext(FavoriteFilter)) /> | |||
<div | |||
className="projects-facets-header clearfix" | |||
> | |||
@@ -68,13 +62,7 @@ exports[`should render \`leak\` view correctly 1`] = ` | |||
exports[`should render \`leak\` view correctly with no applications 1`] = ` | |||
<div> | |||
<withCurrentUserContext(FavoriteFilter) | |||
query={ | |||
Object { | |||
"view": "leak", | |||
} | |||
} | |||
/> | |||
<withRouter(withCurrentUserContext(FavoriteFilter)) /> | |||
<div | |||
className="projects-facets-header clearfix" | |||
> | |||
@@ -131,7 +119,7 @@ exports[`should render \`leak\` view correctly with no applications 1`] = ` | |||
exports[`should render correctly 1`] = ` | |||
<div> | |||
<withCurrentUserContext(FavoriteFilter) /> | |||
<withRouter(withCurrentUserContext(FavoriteFilter)) /> | |||
<div | |||
className="projects-facets-header clearfix" | |||
> | |||
@@ -193,7 +181,7 @@ exports[`should render correctly 1`] = ` | |||
exports[`should render correctly with no applications 1`] = ` | |||
<div> | |||
<withCurrentUserContext(FavoriteFilter) /> | |||
<withRouter(withCurrentUserContext(FavoriteFilter)) /> | |||
<div | |||
className="projects-facets-header clearfix" | |||
> |
@@ -38,8 +38,7 @@ | |||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; | |||
} | |||
.button:hover, | |||
.button.button-active { | |||
.button:hover { | |||
background: var(--darkBlue); | |||
color: var(--white); | |||
} | |||
@@ -199,58 +198,6 @@ | |||
transform: translateY(-2px); | |||
} | |||
/* #region .button-group */ | |||
/* TODO drop usage of this class in SQ (already dropped from SC) */ | |||
.button-group { | |||
display: inline-block; | |||
vertical-align: middle; | |||
font-size: 0; | |||
white-space: nowrap; | |||
} | |||
.button-group > button, | |||
.button-group > .button { | |||
position: relative; | |||
z-index: var(--normalZIndex); | |||
display: inline-block; | |||
vertical-align: middle; | |||
margin: 0; | |||
cursor: pointer; | |||
} | |||
.button-group > .button:hover:not(.disabled), | |||
.button-group > .button:focus:not(.disabled), | |||
.button-group > .button:active:not(.disabled), | |||
.button-group > .button.active:not(.disabled) { | |||
z-index: var(--aboveNormalZIndex); | |||
} | |||
.button-group > .button.disabled { | |||
z-index: var(--belowNormalZIndex); | |||
} | |||
.button-group > .button:not(:first-child) { | |||
border-top-left-radius: 0; | |||
border-bottom-left-radius: 0; | |||
} | |||
.button-group > .button:not(:last-child):not(.dropdown-toggle) { | |||
border-top-right-radius: 0; | |||
border-bottom-right-radius: 0; | |||
} | |||
.button-group > .button + .button { | |||
margin-left: -1px; | |||
} | |||
.button-group > a:not(.button) { | |||
vertical-align: middle; | |||
margin: 0 8px; | |||
font-size: var(--smallFontSize); | |||
} | |||
/* #endregion */ | |||
/* #region .button-icon */ | |||
.button-icon { | |||
display: inline-flex; |