+++ /dev/null
-/*
- * 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>
- );
-}
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';
import { translate } from '../../../helpers/l10n';
import { BranchLike } from '../../../types/branch-like';
import { ComponentMeasure } from '../../../types/types';
-import PortfolioNewCodeToggle from './PortfolioNewCodeToggle';
interface Props {
branchLike?: BranchLike;
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
+++ /dev/null
-/*
- * 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}
- />
- );
-}
+++ /dev/null
-// 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>
-`;
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}
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}
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';
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';
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
+++ /dev/null
-/*
- * 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>
- );
- }
-}
<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]}
padding: var(--gridSize);
}
-.issues-my-issues-filter {
- margin-bottom: 24px;
- text-align: center;
-}
-
.issues-page-actions {
display: inline-block;
min-width: 80px;
* 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 = () => {
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));
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} />}
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()
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() });
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={{}}
+ />
+ );
}
exports[`should render \`leak\` view correctly 1`] = `
<div>
- <withCurrentUserContext(FavoriteFilter)
- query={
- Object {
- "view": "leak",
- }
- }
- />
+ <withRouter(withCurrentUserContext(FavoriteFilter)) />
<div
className="projects-facets-header clearfix"
>
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"
>
exports[`should render correctly 1`] = `
<div>
- <withCurrentUserContext(FavoriteFilter) />
+ <withRouter(withCurrentUserContext(FavoriteFilter)) />
<div
className="projects-facets-header clearfix"
>
exports[`should render correctly with no applications 1`] = `
<div>
- <withCurrentUserContext(FavoriteFilter) />
+ <withRouter(withCurrentUserContext(FavoriteFilter)) />
<div
className="projects-facets-header clearfix"
>
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);
}
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;