Quellcode durchsuchen

update projects page layout (#2182)

tags/6.5-M2
Stas Vilchik vor 7 Jahren
Ursprung
Commit
2772ae13b3
26 geänderte Dateien mit 387 neuen und 661 gelöschten Zeilen
  1. 2
    7
      it/it-tests/src/test/java/pageobjects/projects/ProjectsPage.java
  2. 2
    2
      server/sonar-web/src/main/js/apps/issues/components/App.js
  3. 2
    2
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesListHeader.js
  4. 5
    47
      server/sonar-web/src/main/js/apps/issues/styles.css
  5. 0
    4
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js
  6. 0
    4
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js
  7. 0
    10
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsContainer.js
  8. 76
    77
      server/sonar-web/src/main/js/apps/projects/components/AllProjects.js
  9. 7
    14
      server/sonar-web/src/main/js/apps/projects/components/App.js
  10. 0
    4
      server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.js
  11. 65
    18
      server/sonar-web/src/main/js/apps/projects/components/PageHeader.js
  12. 7
    2
      server/sonar-web/src/main/js/apps/projects/components/PageHeaderContainer.js
  13. 2
    2
      server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.js
  14. 0
    106
      server/sonar-web/src/main/js/apps/projects/components/ProjectsOptionBar.js
  15. 0
    29
      server/sonar-web/src/main/js/apps/projects/components/ProjectsOptionBarContainer.js
  16. 2
    5
      server/sonar-web/src/main/js/apps/projects/components/ProjectsSortingSelect.js
  17. 51
    3
      server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.js
  18. 0
    78
      server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectOptionBar-test.js
  19. 68
    16
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.js.snap
  20. 0
    136
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectOptionBar-test.js.snap
  21. 6
    6
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsSortingSelect-test.js.snap
  22. 2
    2
      server/sonar-web/src/main/js/apps/projects/filters/SearchFilter.js
  23. 10
    3
      server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.js
  24. 4
    20
      server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilter-test.js.snap
  25. 31
    61
      server/sonar-web/src/main/js/apps/projects/styles.css
  26. 45
    3
      server/sonar-web/src/main/less/components/page.less

+ 2
- 7
it/it-tests/src/test/java/pageobjects/projects/ProjectsPage.java Datei anzeigen

@@ -90,7 +90,7 @@ public class ProjectsPage {
}

public ProjectsPage searchProject(String search) {
SelenideElement searchInput = $(".projects-facet-search input");
SelenideElement searchInput = $(".projects-topbar-item-search input");
searchInput.setValue("").sendKeys(search);
return this;
}
@@ -115,11 +115,6 @@ public class ProjectsPage {
}

private SelenideElement getOpenTopBar() {
SelenideElement topBar = $(".projects-topbar-actions").should(Condition.exist);
if (!topBar.has(Condition.hasClass("open"))){
$(".js-projects-topbar-open").click();
}
topBar.should(Condition.hasClass("open"));
return topBar;
return $(".projects-topbar-items").should(Condition.exist);
}
}

+ 2
- 2
server/sonar-web/src/main/js/apps/issues/components/App.js Datei anzeigen

@@ -804,8 +804,8 @@ export default class App extends React.PureComponent {
{this.renderSide(openIssue)}

<div className="layout-page-main">
<div className="issues-header-panel issues-main-header">
<div className="issues-header-panel-inner issues-main-header-inner">
<div className="layout-page-header-panel layout-page-main-header issues-main-header">
<div className="layout-page-header-panel-inner layout-page-main-header-inner">
<div className="layout-page-main-inner">
{this.renderBulkChange(openIssue)}
{openIssue != null

+ 2
- 2
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesListHeader.js Datei anzeigen

@@ -36,8 +36,8 @@ export default function ConciseIssuesListHeader(props: Props) {
const { paging, selectedIndex } = props;

return (
<header className="issues-header-panel concise-issues-list-header">
<div className="issues-header-panel-inner concise-issues-list-header-inner">
<header className="layout-page-header-panel concise-issues-list-header">
<div className="layout-page-header-panel-inner concise-issues-list-header-inner">
<BackButton className="pull-left" onClick={props.onBackClick} />
{props.loading
? <i className="spinner pull-right" />

+ 5
- 47
server/sonar-web/src/main/js/apps/issues/styles.css Datei anzeigen

@@ -1,55 +1,13 @@
.issues-header-panel,
.issues-header-panel-inner {
height: 56px;
box-sizing: border-box;
}

.issues-header-panel {
margin-top: -20px;
}

.issues-header-panel-inner {
position: fixed;
z-index: 30;
line-height: 24px;
padding-top: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #e6e6e6;
background-color: #f3f3f3;
}

.issues-main-header {
margin-bottom: 20px;
}

.issues-main-header .component-name {
line-height: 24px;
}

.issues-main-header-inner {
left: calc(50vw - 360px + 1px);
right: 0;
padding-left: 20px;
padding-right: 20px;
}

@media (max-width: 1320px) {
.issues-main-header-inner {
left: 301px;
}
.concise-issues-list-header, .concise-issues-list-header-inner {
}

.issues-main-header-spinner {
margin-left: 1px;
margin-right: 9px;
margin-top: -1px;
.concise-issues-list-header {
}

.concise-issues-list-header,
.concise-issues-list-header-inner {}

.concise-issues-list-header {}

.concise-issues-list-header-inner {
width: 260px;
text-align: center;
@@ -156,8 +114,7 @@
text-align: right;
}

.issues .search-navigator-facet-header,
.issues .search-navigator-facet-list {
.issues .search-navigator-facet-header, .issues .search-navigator-facet-list {
padding-left: 0;
padding-right: 0;
}
@@ -167,7 +124,8 @@
padding-bottom: 8px;
}

.issues .search-navigator-facet-box:not(.hidden):not(.leak-facet-box) + .search-navigator-facet-box:not(.leak-facet-box) {
.issues .search-navigator-facet-box:not(.hidden):not(.leak-facet-box)
+ .search-navigator-facet-box:not(.leak-facet-box) {
border-top: none;
}


+ 0
- 4
server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js Datei anzeigen

@@ -26,8 +26,6 @@ export default class OrganizationFavoriteProjects extends React.PureComponent {
children?: React.Element<*>,
currentUser: { isLoggedIn: boolean },
location: Object,
optionBarOpen: boolean,
optionBarToggle: (open: boolean) => void,
organization: {
key: string
}
@@ -53,8 +51,6 @@ export default class OrganizationFavoriteProjects extends React.PureComponent {
<FavoriteProjectsContainer
currentUser={this.props.currentUser}
location={this.props.location}
optionBarOpen={this.props.optionBarOpen}
optionBarToggle={this.props.optionBarToggle}
organization={this.props.organization}
/>
</div>

+ 0
- 4
server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js Datei anzeigen

@@ -26,8 +26,6 @@ export default class OrganizationProjects extends React.PureComponent {
children?: React.Element<*>,
currentUser: { isLoggedIn: boolean },
location: Object,
optionBarOpen: boolean,
optionBarToggle: (open: boolean) => void,
organization: {
key: string
}
@@ -54,8 +52,6 @@ export default class OrganizationProjects extends React.PureComponent {
currentUser={this.props.currentUser}
isFavorite={false}
location={this.props.location}
optionBarOpen={this.props.optionBarOpen}
optionBarToggle={this.props.optionBarToggle}
organization={this.props.organization}
/>
</div>

+ 0
- 10
server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjectsContainer.js Datei anzeigen

@@ -23,20 +23,10 @@ import { connect } from 'react-redux';
import { getCurrentUser, getOrganizationByKey } from '../../../store/rootReducer';
import { updateOrganization } from '../actions';

type State = {
optionBarOpen: boolean
};

class OrganizationProjectsContainer extends React.PureComponent {
state: State = { optionBarOpen: false };

handleOptionBarToggle = (open: boolean) => this.setState({ optionBarOpen: open });

render() {
return React.cloneElement(this.props.children, {
currentUser: this.props.currentUser,
optionBarOpen: this.state.optionBarOpen,
optionBarToggle: this.handleOptionBarToggle,
organization: this.props.organization
});
}

+ 76
- 77
server/sonar-web/src/main/js/apps/projects/components/AllProjects.js Datei anzeigen

@@ -21,7 +21,6 @@
import React from 'react';
import Helmet from 'react-helmet';
import PageHeaderContainer from './PageHeaderContainer';
import ProjectsOptionBarContainer from './ProjectsOptionBarContainer';
import ProjectsListContainer from './ProjectsListContainer';
import ProjectsListFooterContainer from './ProjectsListFooterContainer';
import PageSidebar from './PageSidebar';
@@ -31,16 +30,14 @@ import { translate } from '../../../helpers/l10n';
import { SORTING_SWITCH, parseSorting } from '../utils';
import '../styles.css';

type Props = {
type Props = {|
isFavorite: boolean,
location: { pathname: string, query: { [string]: string } },
fetchProjects: (query: string, isFavorite: boolean, organization?: {}) => Promise<*>,
optionBarOpen: boolean,
optionBarToggle: (open: boolean) => void,
organization?: { key: string },
router: { push: ({ pathname: string, query?: {} }) => void },
currentUser?: { isLoggedIn: boolean }
};
|};

type State = {
query: { [string]: string }
@@ -67,11 +64,13 @@ export default class AllProjects extends React.PureComponent {
footer && footer.classList.remove('search-navigator-footer');
}

openOptionBar = (evt: Event & { currentTarget: HTMLElement }) => {
evt.currentTarget.blur();
evt.preventDefault();
this.props.optionBarToggle(true);
};
getView = () => this.state.query.view || 'overall';

getVisualization = () => this.state.query.visualization || 'risk';

getSort = () => this.state.query.sort || 'name';

isFiltered = () => Object.keys(this.state.query).some(key => this.state.query[key] != null);

handlePerspectiveChange = ({ view, visualization }: { view: string, visualization?: string }) => {
const query: { view: ?string, visualization: ?string, sort?: ?string } = {
@@ -111,78 +110,78 @@ export default class AllProjects extends React.PureComponent {
});
};

render() {
const { isFavorite, organization, optionBarOpen } = this.props;
const { query } = this.state;
const isFiltered = Object.keys(query).some(key => query[key] != null);

const view = query.view || 'overall';
const visualization = query.visualization || 'risk';
const selectedSort = query.sort || 'name';

const sideBarTop = (organization ? 95 : 30) + (optionBarOpen ? 45 : 0);
const contentTop = optionBarOpen ? 65 : 20;
renderSide = () => (
<div className="layout-page-side-outer">
<div
className="layout-page-side projects-page-side"
style={{ top: this.props.organization ? 95 : 30 }}>
<div className="layout-page-side-inner">
<div className="layout-page-filters">
<PageSidebar
isFavorite={this.props.isFavorite}
organization={this.props.organization}
query={this.state.query}
view={this.getView()}
visualization={this.getVisualization()}
/>
</div>
</div>
</div>
</div>
);

renderHeader = () => (
<div className="layout-page-header-panel layout-page-main-header">
<div className="layout-page-header-panel-inner layout-page-main-header-inner">
<div className="layout-page-main-inner">
<PageHeaderContainer
query={this.state.query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
onPerspectiveChange={this.handlePerspectiveChange}
onSortChange={this.handleSortChange}
selectedSort={this.getSort()}
currentUser={this.props.currentUser}
view={this.getView()}
visualization={this.getVisualization()}
/>
</div>
</div>
</div>
);

renderMain = () =>
(this.getView() === 'visualizations'
? <div className="layout-page-main-inner">
<VisualizationsContainer
sort={this.state.query.sort}
visualization={this.getVisualization()}
/>
</div>
: <div className="layout-page-main-inner">
<ProjectsListContainer
isFavorite={this.props.isFavorite}
isFiltered={this.isFiltered()}
organization={this.props.organization}
cardType={this.getView()}
/>
<ProjectsListFooterContainer
query={this.state.query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
</div>);

render() {
return (
<div>
<div className="layout-page projects-page">
<Helmet title={translate('projects.page')} />

<ProjectsOptionBarContainer
onPerspectiveChange={this.handlePerspectiveChange}
onSortChange={this.handleSortChange}
onToggleOptionBar={this.props.optionBarToggle}
open={optionBarOpen}
selectedSort={selectedSort}
currentUser={this.props.currentUser}
view={view}
visualization={visualization}
/>

<div className="layout-page projects-page">
<div className="layout-page-side-outer">
<div className="layout-page-side projects-page-side" style={{ top: sideBarTop }}>
<div className="layout-page-side-inner">
<div className="layout-page-filters">
<PageSidebar
isFavorite={isFavorite}
organization={organization}
query={query}
view={view}
visualization={visualization}
/>
</div>
</div>
</div>
</div>
{this.renderSide()}

<div
className="layout-page-main projects-page-content"
style={{ paddingTop: contentTop }}>
<div className="layout-page-main-inner">
<PageHeaderContainer
query={query}
isFavorite={isFavorite}
organization={organization}
onOpenOptionBar={this.openOptionBar}
optionBarOpen={optionBarOpen}
/>
{view !== 'visualizations' &&
<ProjectsListContainer
isFavorite={isFavorite}
isFiltered={isFiltered}
organization={organization}
cardType={view}
/>}
{view !== 'visualizations' &&
<ProjectsListFooterContainer
query={query}
isFavorite={isFavorite}
organization={organization}
/>}
{view === 'visualizations' &&
<VisualizationsContainer sort={query.sort} visualization={visualization} />}
</div>
</div>
<div className="layout-page-main projects-page-content">
{this.renderHeader()}
{this.renderMain()}
</div>
</div>
);

+ 7
- 14
server/sonar-web/src/main/js/apps/projects/components/App.js Datei anzeigen

@@ -20,32 +20,25 @@
//@flow
import React from 'react';

type State = {
optionBarOpen: boolean
};

export default class App extends React.PureComponent {
state: State = { optionBarOpen: false };

componentDidMount() {
const elem = document.querySelector('html');
elem && elem.classList.add('dashboard-page');
if (elem) {
elem.classList.add('dashboard-page');
}
}

componentWillUnmount() {
const elem = document.querySelector('html');
elem && elem.classList.remove('dashboard-page');
if (elem) {
elem.classList.remove('dashboard-page');
}
}

handleOptionBarToggle = (open: boolean) => this.setState({ optionBarOpen: open });

render() {
return (
<div id="projects-page">
{React.cloneElement(this.props.children, {
optionBarOpen: this.state.optionBarOpen,
optionBarToggle: this.handleOptionBarToggle
})}
{this.props.children}
</div>
);
}

+ 0
- 4
server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.js Datei anzeigen

@@ -29,8 +29,6 @@ import { searchProjects } from '../../../api/components';
type Props = {
currentUser: { isLoggedIn: boolean },
location: { query: {} },
optionBarOpen: boolean,
optionBarToggle: (open: boolean) => void,
router: {
replace: (location: { pathname?: string, query?: { [string]: string } }) => void
}
@@ -107,8 +105,6 @@ class DefaultPageSelector extends React.PureComponent {
<AllProjectsContainer
isFavorite={false}
location={this.props.location}
optionBarOpen={this.props.optionBarOpen}
optionBarToggle={this.props.optionBarToggle}
currentUser={this.props.currentUser}
/>
);

+ 65
- 18
server/sonar-web/src/main/js/apps/projects/components/PageHeader.js Datei anzeigen

@@ -19,41 +19,88 @@
*/
// @flow
import React from 'react';
import classNames from 'classnames';
import SearchFilterContainer from '../filters/SearchFilterContainer';
import Tooltip from '../../../components/controls/Tooltip';
import PerspectiveSelect from './PerspectiveSelect';
import ProjectsSortingSelect from './ProjectsSortingSelect';
import { translate } from '../../../helpers/l10n';

type Props = {
type Props = {|
currentUser?: { isLoggedIn: boolean },
isFavorite?: boolean,
loading: boolean,
onOpenOptionBar: () => void,
optionBarOpen?: boolean,
onPerspectiveChange: ({ view: string, visualization?: string }) => void,
organization?: { key: string },
projects: Array<*>,
projectsAppState: { loading: boolean, total?: number },
query: { [string]: string },
total?: number
};
onSortChange: (sort: string, desc: boolean) => void,
selectedSort: string,
view: string,
visualization?: string
|};

export default function PageHeader(props: Props) {
const renderSortingSelect = () => {
const { projectsAppState, projects, currentUser, view } = props;
const limitReached =
projects != null &&
projectsAppState.total != null &&
projects.length < projectsAppState.total;
const defaultOption = currentUser && currentUser.isLoggedIn ? 'name' : 'analysis_date';
if (view === 'visualizations' && !limitReached) {
return (
<Tooltip overlay={translate('projects.sort.disabled')}>
<div className="projects-topbar-item disabled">
<ProjectsSortingSelect
className="js-projects-sorting-select"
defaultOption={defaultOption}
onChange={props.onSortChange}
selectedSort={props.selectedSort}
view={props.view}
/>
</div>
</Tooltip>
);
}
return (
<ProjectsSortingSelect
className="projects-topbar-item js-projects-sorting-select"
defaultOption={defaultOption}
onChange={props.onSortChange}
selectedSort={props.selectedSort}
view={props.view}
/>
);
};

return (
<header className="page-header">
<header className="page-header projects-topbar-items">
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
onChange={props.onPerspectiveChange}
view={props.view}
visualization={props.visualization}
/>

{renderSortingSelect()}

<SearchFilterContainer
className="projects-topbar-item projects-topbar-item-search"
isFavorite={props.isFavorite}
organization={props.organization}
query={props.query}
/>
<div className="page-actions projects-page-actions text-right">
{!props.optionBarOpen &&
<a
className="button js-projects-topbar-open spacer-right"
href="#"
onClick={props.onOpenOptionBar}>
{translate('projects.view_settings')}
</a>}

{!!props.loading && <i className="spinner spacer-right" />}
<div
className={classNames('projects-topbar-item', 'is-last', {
'is-loading': props.projectsAppState.loading
})}>
{!!props.projectsAppState.loading && <i className="spinner spacer-right" />}

{props.total != null &&
{props.projectsAppState.total != null &&
<span>
<strong id="projects-total">{props.total}</strong>
<strong id="projects-total">{props.projectsAppState.total}</strong>
{' '}
{translate('projects._projects')}
</span>}

+ 7
- 2
server/sonar-web/src/main/js/apps/projects/components/PageHeaderContainer.js Datei anzeigen

@@ -19,6 +19,11 @@
*/
import { connect } from 'react-redux';
import PageHeader from './PageHeader';
import { getProjectsAppState } from '../../../store/rootReducer';
import { getProjects, getProjectsAppState } from '../../../store/rootReducer';

export default connect(state => getProjectsAppState(state))(PageHeader);
const mapStateToProps = state => ({
projects: getProjects(state),
projectsAppState: getProjectsAppState(state)
});

export default connect(mapStateToProps)(PageHeader);

+ 2
- 2
server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.js Datei anzeigen

@@ -26,12 +26,12 @@ import { VIEWS, VISUALIZATIONS } from '../utils';

export type Option = { label: string, type: string, value: string };

type Props = {
type Props = {|
className?: string,
onChange: ({ view: string, visualization?: string }) => void,
view: string,
visualization?: string
};
|};

export default class PerspectiveSelect extends React.PureComponent {
options: Array<Option>;

+ 0
- 106
server/sonar-web/src/main/js/apps/projects/components/ProjectsOptionBar.js Datei anzeigen

@@ -1,106 +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.
*/
//@flow
import React from 'react';
import classNames from 'classnames';
import Tooltip from '../../../components/controls/Tooltip';
import PerspectiveSelect from './PerspectiveSelect';
import ProjectsSortingSelect from './ProjectsSortingSelect';
import { translate } from '../../../helpers/l10n';

type Props = {
onPerspectiveChange: ({ view: string, visualization?: string }) => void,
onSortChange: (sort: string, desc: boolean) => void,
onToggleOptionBar: boolean => void,
open: boolean,
projects: Array<*>,
projectsAppState: { loading: boolean, total?: number },
selectedSort: string,
currentUser?: { isLoggedIn: boolean },
view: string,
visualization?: string
};

export default class ProjectsOptionBar extends React.PureComponent {
props: Props;

closeBar = (evt: Event & { currentTarget: HTMLElement }) => {
evt.currentTarget.blur();
evt.preventDefault();
this.props.onToggleOptionBar(false);
};

renderSortingSelect() {
const { projectsAppState, projects, currentUser, view } = this.props;
const limitReached =
projects != null &&
projectsAppState.total != null &&
projects.length < projectsAppState.total;
const defaultOption = currentUser && currentUser.isLoggedIn ? 'name' : 'analysis_date';
if (view === 'visualizations' && !limitReached) {
return (
<Tooltip overlay={translate('projects.sort.disabled')}>
<div>
<ProjectsSortingSelect
className="projects-topbar-item js-projects-sorting-select disabled"
defaultOption={defaultOption}
onChange={this.props.onSortChange}
selectedSort={this.props.selectedSort}
view={this.props.view}
/>
</div>
</Tooltip>
);
}
return (
<ProjectsSortingSelect
className="projects-topbar-item js-projects-sorting-select"
defaultOption={defaultOption}
onChange={this.props.onSortChange}
selectedSort={this.props.selectedSort}
view={this.props.view}
/>
);
}

render() {
const { open } = this.props;
return (
<div className="projects-topbar">
<div className={classNames('projects-topbar-actions', { open })}>
<div className="projects-topbar-actions-inner">
<button className="projects-topbar-button" onClick={this.closeBar}>
{translate('close')}
</button>
<div className="projects-topbar-items">
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
onChange={this.props.onPerspectiveChange}
view={this.props.view}
visualization={this.props.visualization}
/>
{this.renderSortingSelect()}
</div>
</div>
</div>
</div>
);
}
}

+ 0
- 29
server/sonar-web/src/main/js/apps/projects/components/ProjectsOptionBarContainer.js Datei anzeigen

@@ -1,29 +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 { connect } from 'react-redux';
import ProjectsOptionBar from './ProjectsOptionBar';
import { getProjects, getProjectsAppState } from '../../../store/rootReducer';

const mapStateToProps = state => ({
projects: getProjects(state),
projectsAppState: getProjectsAppState(state)
});

export default connect(mapStateToProps)(ProjectsOptionBar);

+ 2
- 5
server/sonar-web/src/main/js/apps/projects/components/ProjectsSortingSelect.js Datei anzeigen

@@ -65,10 +65,7 @@ export default class ProjectsSortingSelect extends React.PureComponent {
opt => (opt.value === this.props.defaultOption ? 0 : 1)
).map((opt: { value: string, class?: string }) => ({
value: opt.value,
label: translate('projects.sorting', opt.value) +
(opt.value === this.props.defaultOption
? ` (${translate('projects.sorting.default')})`
: ''),
label: translate('projects.sorting', opt.value),
class: opt.class
}));
};
@@ -88,7 +85,7 @@ export default class ProjectsSortingSelect extends React.PureComponent {
<div className={this.props.className}>
<label>{translate('projects.sort_by')}:</label>
<Select
className="little-spacer-left input-large"
className="little-spacer-left input-medium"
clearable={false}
onChange={this.handleSortChange}
optionComponent={ProjectsSortingSelectOption}

+ 51
- 3
server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.js Datei anzeigen

@@ -22,17 +22,65 @@ import { shallow } from 'enzyme';
import PageHeader from '../PageHeader';

it('should render correctly', () => {
expect(shallow(<PageHeader query={{ search: 'test' }} total="12" />)).toMatchSnapshot();
expect(
shallow(<PageHeader query={{ search: 'test' }} projectsAppState={{ total: 12 }} />)
).toMatchSnapshot();
});

it('should render correctly while loading', () => {
expect(
shallow(<PageHeader query={{ search: '' }} loading={true} isFavorite={true} total="2" />)
shallow(
<PageHeader
query={{ search: '' }}
isFavorite={true}
projectsAppState={{ loading: true, total: 2 }}
/>
)
).toMatchSnapshot();
});

it('should not render projects total', () => {
expect(
shallow(<PageHeader query={{ search: '' }} />).find('#projects-total').exists()
shallow(<PageHeader projectsAppState={{}} query={{ search: '' }} />)
.find('#projects-total')
.exists()
).toBeFalsy();
});

it('should render disabled sorting options for visualizations', () => {
expect(
shallow(
<PageHeader
open={true}
projectsAppState={{}}
view="visualizations"
visualization="coverage"
/>
)
).toMatchSnapshot();
});

it('should render switch the default sorting option for anonymous users', () => {
expect(
shallow(
<PageHeader
currentUser={{ isLoggedIn: true }}
open={true}
projectsAppState={{}}
view="overall"
visualization="risk"
/>
).find('ProjectsSortingSelect')
).toMatchSnapshot();
expect(
shallow(
<PageHeader
currentUser={{ isLoggedIn: false }}
open={true}
projectsAppState={{}}
view="leak"
visualization="risk"
/>
).find('ProjectsSortingSelect')
).toMatchSnapshot();
});

+ 0
- 78
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectOptionBar-test.js Datei anzeigen

@@ -1,78 +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 React from 'react';
import { shallow } from 'enzyme';
import ProjectsOptionBar from '../ProjectsOptionBar';
import { click } from '../../../../helpers/testUtils';

it('should render option bar closed', () => {
expect(shallow(<ProjectsOptionBar open={false} view="overall" />)).toMatchSnapshot();
});

it('should render option bar open', () => {
expect(
shallow(
<ProjectsOptionBar
open={true}
view="leak"
visualization="risk"
projects={[1, 2, 3]}
projectsAppState={{ total: 3 }}
currentUser={{ isLoggedIn: true }}
/>
)
).toMatchSnapshot();
});

it('should render disabled sorting options for visualizations', () => {
expect(
shallow(<ProjectsOptionBar open={true} view="visualizations" visualization="coverage" />)
).toMatchSnapshot();
});

it('should call close method correctly', () => {
const toggle = jest.fn();
const wrapper = shallow(<ProjectsOptionBar open={true} view="leak" onToggleOptionBar={toggle} />);
click(wrapper.find('.projects-topbar-button'));
expect(toggle.mock.calls).toMatchSnapshot();
});

it('should render switch the default sorting option for anonymous users', () => {
expect(
shallow(
<ProjectsOptionBar
open={true}
view="overall"
visualization="risk"
currentUser={{ isLoggedIn: true }}
/>
).find('ProjectsSortingSelect')
).toMatchSnapshot();
expect(
shallow(
<ProjectsOptionBar
open={true}
view="leak"
visualization="risk"
currentUser={{ isLoggedIn: false }}
/>
).find('ProjectsSortingSelect')
).toMatchSnapshot();
});

+ 68
- 16
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.js.snap Datei anzeigen

@@ -2,9 +2,17 @@

exports[`should render correctly 1`] = `
<header
className="page-header"
className="page-header projects-topbar-items"
>
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
/>
<ProjectsSortingSelect
className="projects-topbar-item js-projects-sorting-select"
defaultOption="analysis_date"
/>
<withRouter(SearchFilterContainer)
className="projects-topbar-item projects-topbar-item-search"
query={
Object {
"search": "test",
@@ -12,14 +20,8 @@ exports[`should render correctly 1`] = `
}
/>
<div
className="page-actions projects-page-actions text-right"
className="projects-topbar-item is-last"
>
<a
className="button js-projects-topbar-open spacer-right"
href="#"
>
projects.view_settings
</a>
<span>
<strong
id="projects-total"
@@ -35,9 +37,17 @@ exports[`should render correctly 1`] = `

exports[`should render correctly while loading 1`] = `
<header
className="page-header"
className="page-header projects-topbar-items"
>
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
/>
<ProjectsSortingSelect
className="projects-topbar-item js-projects-sorting-select"
defaultOption="analysis_date"
/>
<withRouter(SearchFilterContainer)
className="projects-topbar-item projects-topbar-item-search"
isFavorite={true}
query={
Object {
@@ -46,14 +56,8 @@ exports[`should render correctly while loading 1`] = `
}
/>
<div
className="page-actions projects-page-actions text-right"
className="projects-topbar-item is-last is-loading"
>
<a
className="button js-projects-topbar-open spacer-right"
href="#"
>
projects.view_settings
</a>
<i
className="spinner spacer-right"
/>
@@ -69,3 +73,51 @@ exports[`should render correctly while loading 1`] = `
</div>
</header>
`;

exports[`should render disabled sorting options for visualizations 1`] = `
<header
className="page-header projects-topbar-items"
>
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
view="visualizations"
visualization="coverage"
/>
<Tooltip
overlay="projects.sort.disabled"
placement="bottom"
>
<div
className="projects-topbar-item disabled"
>
<ProjectsSortingSelect
className="js-projects-sorting-select"
defaultOption="analysis_date"
view="visualizations"
/>
</div>
</Tooltip>
<withRouter(SearchFilterContainer)
className="projects-topbar-item projects-topbar-item-search"
/>
<div
className="projects-topbar-item is-last"
/>
</header>
`;

exports[`should render switch the default sorting option for anonymous users 1`] = `
<ProjectsSortingSelect
className="projects-topbar-item js-projects-sorting-select"
defaultOption="name"
view="overall"
/>
`;

exports[`should render switch the default sorting option for anonymous users 2`] = `
<ProjectsSortingSelect
className="projects-topbar-item js-projects-sorting-select"
defaultOption="analysis_date"
view="leak"
/>
`;

+ 0
- 136
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectOptionBar-test.js.snap Datei anzeigen

@@ -1,136 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should call close method correctly 1`] = `
Array [
Array [
false,
],
]
`;

exports[`should render disabled sorting options for visualizations 1`] = `
<div
className="projects-topbar"
>
<div
className="projects-topbar-actions open"
>
<div
className="projects-topbar-actions-inner"
>
<button
className="projects-topbar-button"
onClick={[Function]}
>
close
</button>
<div
className="projects-topbar-items"
>
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
view="visualizations"
visualization="coverage"
/>
<Tooltip
overlay="projects.sort.disabled"
placement="bottom"
>
<div>
<ProjectsSortingSelect
className="projects-topbar-item js-projects-sorting-select disabled"
defaultOption="analysis_date"
view="visualizations"
/>
</div>
</Tooltip>
</div>
</div>
</div>
</div>
`;

exports[`should render option bar closed 1`] = `
<div
className="projects-topbar"
>
<div
className="projects-topbar-actions"
>
<div
className="projects-topbar-actions-inner"
>
<button
className="projects-topbar-button"
onClick={[Function]}
>
close
</button>
<div
className="projects-topbar-items"
>
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
view="overall"
/>
<ProjectsSortingSelect
className="projects-topbar-item js-projects-sorting-select"
defaultOption="analysis_date"
view="overall"
/>
</div>
</div>
</div>
</div>
`;

exports[`should render option bar open 1`] = `
<div
className="projects-topbar"
>
<div
className="projects-topbar-actions open"
>
<div
className="projects-topbar-actions-inner"
>
<button
className="projects-topbar-button"
onClick={[Function]}
>
close
</button>
<div
className="projects-topbar-items"
>
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
view="leak"
visualization="risk"
/>
<ProjectsSortingSelect
className="projects-topbar-item js-projects-sorting-select"
defaultOption="name"
view="leak"
/>
</div>
</div>
</div>
</div>
`;

exports[`should render switch the default sorting option for anonymous users 1`] = `
<ProjectsSortingSelect
className="projects-topbar-item js-projects-sorting-select"
defaultOption="name"
view="overall"
/>
`;

exports[`should render switch the default sorting option for anonymous users 2`] = `
<ProjectsSortingSelect
className="projects-topbar-item js-projects-sorting-select"
defaultOption="analysis_date"
view="leak"
/>
`;

+ 6
- 6
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsSortingSelect-test.js.snap Datei anzeigen

@@ -12,7 +12,7 @@ exports[`should handle the descending sort direction 1`] = `
autosize={true}
backspaceRemoves={true}
backspaceToRemoveMessage="Press backspace to remove {label}"
className="little-spacer-left input-large"
className="little-spacer-left input-medium"
clearAllText="Clear all"
clearValueText="Clear value"
clearable={false}
@@ -41,7 +41,7 @@ exports[`should handle the descending sort direction 1`] = `
Array [
Object {
"class": undefined,
"label": "projects.sorting.name (projects.sorting.default)",
"label": "projects.sorting.name",
"value": "name",
},
Object {
@@ -121,7 +121,7 @@ exports[`should render correctly for leak view 1`] = `
autosize={true}
backspaceRemoves={true}
backspaceToRemoveMessage="Press backspace to remove {label}"
className="little-spacer-left input-large"
className="little-spacer-left input-medium"
clearAllText="Clear all"
clearValueText="Clear value"
clearable={false}
@@ -150,7 +150,7 @@ exports[`should render correctly for leak view 1`] = `
Array [
Object {
"class": undefined,
"label": "projects.sorting.analysis_date (projects.sorting.default)",
"label": "projects.sorting.analysis_date",
"value": "analysis_date",
},
Object {
@@ -230,7 +230,7 @@ exports[`should render correctly for overall view 1`] = `
autosize={true}
backspaceRemoves={true}
backspaceToRemoveMessage="Press backspace to remove {label}"
className="little-spacer-left input-large"
className="little-spacer-left input-medium"
clearAllText="Clear all"
clearValueText="Clear value"
clearable={false}
@@ -259,7 +259,7 @@ exports[`should render correctly for overall view 1`] = `
Array [
Object {
"class": undefined,
"label": "projects.sorting.name (projects.sorting.default)",
"label": "projects.sorting.name",
"value": "name",
},
Object {

+ 2
- 2
server/sonar-web/src/main/js/apps/projects/filters/SearchFilter.js Datei anzeigen

@@ -22,6 +22,7 @@ import React from 'react';
import { translate, translateWithParameters } from '../../../helpers/l10n';

type Props = {
className?: string,
handleSearch: (userString?: string) => void,
query: { search?: string }
};
@@ -63,11 +64,10 @@ export default class SearchFilter extends React.PureComponent {
const { userQuery } = this.state;
const shortQuery = userQuery != null && userQuery.length === 1;
return (
<div className="projects-facet-search" data-key="search">
<div className={this.props.className}>
<input
type="search"
value={userQuery || ''}
className="input-super-large"
placeholder={translate('projects.search')}
onChange={this.handleQueryChange}
autoComplete="off"

+ 10
- 3
server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.js Datei anzeigen

@@ -24,12 +24,13 @@ import { debounce } from 'lodash';
import { getFilterUrl } from './utils';
import SearchFilter from './SearchFilter';

type Props = {
type Props = {|
className?: string,
query: { search?: string },
router: { push: ({ pathname: string }) => void },
isFavorite?: boolean,
organization?: {}
};
|};

class SearchFilterContainer extends React.PureComponent {
handleSearch: (userQuery?: string) => void;
@@ -46,7 +47,13 @@ class SearchFilterContainer extends React.PureComponent {
}

render() {
return <SearchFilter query={this.props.query} handleSearch={this.handleSearch} />;
return (
<SearchFilter
className={this.props.className}
query={this.props.query}
handleSearch={this.handleSearch}
/>
);
}
}


+ 4
- 20
server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilter-test.js.snap Datei anzeigen

@@ -1,13 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display a help message when there is less than 2 characters 1`] = `
<div
className="projects-facet-search"
data-key="search"
>
<div>
<input
autoComplete="off"
className="input-super-large"
onChange={[Function]}
placeholder="projects.search"
type="search"
@@ -22,13 +18,9 @@ exports[`should display a help message when there is less than 2 characters 1`]
`;

exports[`should display a help message when there is less than 2 characters 2`] = `
<div
className="projects-facet-search"
data-key="search"
>
<div>
<input
autoComplete="off"
className="input-super-large"
onChange={[Function]}
placeholder="projects.search"
type="search"
@@ -38,13 +30,9 @@ exports[`should display a help message when there is less than 2 characters 2`]
`;

exports[`should render correctly without any search query 1`] = `
<div
className="projects-facet-search"
data-key="search"
>
<div>
<input
autoComplete="off"
className="input-super-large"
onChange={[Function]}
placeholder="projects.search"
type="search"
@@ -54,13 +42,9 @@ exports[`should render correctly without any search query 1`] = `
`;

exports[`should render with a search query 1`] = `
<div
className="projects-facet-search"
data-key="search"
>
<div>
<input
autoComplete="off"
className="input-super-large"
onChange={[Function]}
placeholder="projects.search"
type="search"

+ 31
- 61
server/sonar-web/src/main/js/apps/projects/styles.css Datei anzeigen

@@ -1,7 +1,3 @@
.projects-page-actions {
margin-bottom: 0;
}

.projects-page-side {
transition: top 150ms ease-out;
}
@@ -10,48 +6,27 @@
transition: padding-top 150ms ease-out;
}

.projects-topbar {
position: fixed;
width: 100%;
z-index: 100;
}

.projects-topbar-actions {
box-sizing: border-box;
position: absolute;
left: 0;
right: 0;
top: -50px;
z-index: 50;
flex-grow: 0.000001;
border-bottom: 1px solid #e6e6e6;
background-color: #fff;
transition: top 150ms ease-out;
.projects-topbar-items {
display: flex;
align-items: center;
flex-grow: 1;
}

.projects-topbar-actions.open {
top: 0;
.projects-topbar-item + .projects-topbar-item {
padding-left: 24px;
}

.projects-topbar-actions-inner {
position: relative;
margin: auto;
padding: 0 20px;
min-width: 1040px;
max-width: 1280px;
.projects-topbar-item .spinner {
top: -1px;
}

.projects-topbar-items {
display: flex;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
flex-grow: 1;
height: 46px;
.projects-topbar-item.is-last {
margin-left: auto;
padding-left: 32px;
}

.projects-topbar-item {
padding: 0 24px;
.projects-topbar-item.is-loading {
padding-left: 0;
}

.projects-topbar-item.disabled {
@@ -63,14 +38,22 @@
pointer-events: none !important;
}

.projects-topbar-button {
position: absolute;
right: 20px;
top: 10px;
.projects-topbar-item-search {
position: relative;
flex: 1;
}

.projects-sidebar {
width: 260px;
.projects-topbar-item-search input {
width: 100%;
max-width: 300px;
}

.projects-topbar-item-search .note {
position: absolute;
top: 1px;
left: 80px;
line-height: 24px;
pointer-events: none;
}

.projects-list .page-actions {
@@ -237,26 +220,12 @@
border-bottom: 1px solid #e6e6e6;
}

.projects-facet-search {
position: absolute;
bottom: 0;
left: 0;
width: 300px;
}

.projects-facet-search .note {
position: absolute;
top: 1px;
right: 30px;
line-height: 24px;
pointer-events: none;
}

.projects-facets-reset {
float: right;
}

.projects-facets-reset .button {}
.projects-facets-reset .button {
}

.projects-facet-bar {
display: inline-block;
@@ -272,7 +241,8 @@
}

.search-navigator-facet.active .projects-facet-bar-inner,
.search-navigator-facet-highlight-under-container .search-navigator-facet.active ~ .search-navigator-facet .projects-facet-bar-inner {
.search-navigator-facet-highlight-under-container .search-navigator-facet.active
~ .search-navigator-facet .projects-facet-bar-inner {
background-color: #4b9fd5;
}


+ 45
- 3
server/sonar-web/src/main/less/components/page.less Datei anzeigen

@@ -178,15 +178,19 @@
background: #f3f3f3;

@media (max-width: 1335px) {
& { width: 310px; }
& {
width: 310px;
}
}

.search-navigator-facets-list {
width: 260px;
margin-left: ~"calc(50vw - 640px + 290px - 260px - 37px)";

@media (max-width: 1335px) {
& { margin-left: 20px; }
& {
margin-left: 20px;
}
}
}
}
@@ -241,6 +245,40 @@
background-color: #f3f3f3;
}

.layout-page-header-panel, .layout-page-header-panel-inner {
height: 56px;
box-sizing: border-box;
}

.layout-page-header-panel {
margin-top: -20px;
}

.layout-page-header-panel-inner {
position: fixed;
z-index: 30;
line-height: 24px;
padding-top: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #e6e6e6;
background-color: #f3f3f3;
}

.layout-page-main-header {
margin-bottom: 20px;
}

.layout-page-main-header .component-name {
line-height: 24px;
}

.layout-page-main-header-inner {
left: ~"calc(50vw - 360px + 1px)";
right: 0;
padding-left: 20px;
padding-right: 20px;
}

@media (max-width: 1320px) {
.layout-page-side-outer {
width: 300px;
@@ -253,4 +291,8 @@
.layout-page-side-inner {
margin-left: 0;
}
}

.layout-page-main-header-inner {
left: 301px;
}
}

Laden…
Abbrechen
Speichern