Browse Source

[NO-JIRA] Consistently use ButtonToggle component

tags/9.6.0.59041
Philippe Perrin 1 year ago
parent
commit
abcb9a091d

+ 0
- 53
server/sonar-web/src/main/js/apps/code/components/PortfolioNewCodeToggle.tsx View File

@@ -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>
);
}

+ 18
- 5
server/sonar-web/src/main/js/apps/code/components/Search.tsx View File

@@ -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

+ 0
- 64
server/sonar-web/src/main/js/apps/code/components/__tests__/PortfolioNewCodeToggle-test.tsx View File

@@ -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}
/>
);
}

+ 0
- 26
server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/PortfolioNewCodeToggle-test.tsx.snap View File

@@ -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>
`;

+ 40
- 8
server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/Search-test.tsx.snap View File

@@ -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}

+ 12
- 5
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx View File

@@ -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

+ 0
- 54
server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.tsx View File

@@ -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>
);
}
}

+ 22
- 4
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesApp-test.tsx.snap View File

@@ -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]}

+ 0
- 5
server/sonar-web/src/main/js/apps/issues/styles.css View File

@@ -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;

+ 29
- 30
server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx View File

@@ -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));

+ 1
- 6
server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx View File

@@ -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} />}

+ 28
- 12
server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx View File

@@ -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={{}}
/>
);
}

+ 4
- 16
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.tsx.snap View File

@@ -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"
>

+ 1
- 54
server/sonar-web/src/main/js/components/controls/buttons.css View File

@@ -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;

Loading…
Cancel
Save