瀏覽代碼

SONAR-6400 allow to show more items in a facet (#597)

tags/7.5
Stas Vilchik 5 年之前
父節點
當前提交
15f3d9c258

+ 3
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx 查看文件

@@ -76,6 +76,9 @@ class LanguageFacet extends React.PureComponent<Props> {
getFacetItemText={this.getLanguageName}
getSearchResultKey={(language: InstalledLanguage) => language.key}
getSearchResultText={(language: InstalledLanguage) => language.name}
// TODO use defaults when rules search WS is updated
maxInitialItems={10}
maxItems={10}
onChange={this.props.onChange}
onSearch={this.handleSearch}
onToggle={this.props.onToggle}

+ 3
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/TagFacet.tsx 查看文件

@@ -69,6 +69,9 @@ export default class TagFacet extends React.PureComponent<Props> {
getFacetItemText={this.getTagName}
getSearchResultKey={tag => tag}
getSearchResultText={tag => tag}
// TODO use defaults when rules search WS is updated
maxInitialItems={10}
maxItems={10}
onChange={this.props.onChange}
onSearch={this.handleSearch}
onToggle={this.props.onToggle}

+ 1
- 1
server/sonar-web/src/main/js/components/facet/FacetItem.tsx 查看文件

@@ -55,7 +55,7 @@ export default class FacetItem extends React.PureComponent<Props> {
});

return this.props.disabled ? (
<span className={className} data-facet={this.props.value}>
<span className={className} data-facet={this.props.value} title={this.props.tooltip}>
<span className="facet-name">{name}</span>
{this.props.stat != null && <span className="facet-stat">{this.props.stat}</span>}
</span>

+ 89
- 23
server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx 查看文件

@@ -23,6 +23,7 @@ import FacetBox from './FacetBox';
import FacetHeader from './FacetHeader';
import FacetItem from './FacetItem';
import FacetItemsList from './FacetItemsList';
import ListStyleFacetFooter from './ListStyleFacetFooter';
import MultipleSelectionHint from './MultipleSelectionHint';
import { translate } from '../../helpers/l10n';
import DeferredSpinner from '../common/DeferredSpinner';
@@ -38,6 +39,8 @@ export interface Props<S> {
getSearchResultKey: (result: S) => string;
getSearchResultText: (result: S) => string;
loading?: boolean;
maxInitialItems?: number;
maxItems?: number;
onChange: (changes: { [x: string]: string | string[] }) => void;
onSearch: (query: string, page?: number) => Promise<{ results: S[]; paging: Paging }>;
onToggle: (property: string) => void;
@@ -46,25 +49,32 @@ export interface Props<S> {
renderFacetItem: (item: string) => React.ReactNode;
renderSearchResult: (result: S, query: string) => React.ReactNode;
searchPlaceholder: string;
values: string[];
stats: { [x: string]: number } | undefined;
values: string[];
}

interface State<S> {
autoFocus: boolean;
query: string;
searching: boolean;
searchResults?: S[];
searchPaging?: Paging;
searchResults?: S[];
showFullList: boolean;
}

export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S>> {
mounted = false;

static defaultProps = {
maxInitialItems: 15,
maxItems: 100
};

state: State<S> = {
autoFocus: false,
query: '',
searching: false
searching: false,
showFullList: false
};

componentDidMount() {
@@ -72,9 +82,18 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
}

componentDidUpdate(prevProps: Props<S>) {
// focus search field *only* if it was manually open
if (!prevProps.open && this.props.open) {
// focus search field *only* if it was manually open
this.setState({ autoFocus: true });
} else if (prevProps.open && !this.props.open) {
// reset state when closing the facet
this.setState({ query: '', searchResults: undefined, searching: false, showFullList: false });
} else if (
prevProps.stats !== this.props.stats &&
Object.keys(this.props.stats || {}).length < this.props.maxInitialItems!
) {
// show limited list if `stats` changed and there are less than 15 items
this.setState({ showFullList: false });
}
}

@@ -139,11 +158,29 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
}
};

getStat(item: string) {
getStat(item: string, zeroIfAbsent = false) {
const { stats } = this.props;
return stats ? stats[item] : undefined;
const defaultValue = zeroIfAbsent ? 0 : undefined;
return stats && stats[item] !== undefined ? stats && stats[item] : defaultValue;
}

showFullList = () => {
this.setState({ showFullList: true });
};

hideFullList = () => {
this.setState({ showFullList: false });
};

getLastActiveIndex = (list: string[]) => {
for (let i = list.length - 1; i >= 0; i--) {
if (this.props.values.includes(list[i])) {
return i;
}
}
return 0;
};

renderList() {
const { stats } = this.props;

@@ -151,27 +188,51 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
return null;
}

const items = sortBy(
const sortedItems = sortBy(
Object.keys(stats),
key => -stats[key],
key => this.props.getFacetItemText(key)
);

// limit the number of items to this.props.maxInitialItems,
// but make sure all (in other words, the last) selected items are displayed
const lastSelectedIndex = this.getLastActiveIndex(sortedItems);
const countToDisplay = Math.max(this.props.maxInitialItems!, lastSelectedIndex + 1);
const limitedList = this.state.showFullList
? sortedItems
: sortedItems.slice(0, countToDisplay);

const mightHaveMoreResults = sortedItems.length >= this.props.maxItems!;

return (
<FacetItemsList>
{items.map(item => (
<FacetItem
active={this.props.values.includes(item)}
key={item}
loading={this.props.loading}
name={this.props.renderFacetItem(item)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(item))}
tooltip={this.props.getFacetItemText(item)}
value={item}
/>
))}
</FacetItemsList>
<>
<FacetItemsList>
{limitedList.map(item => (
<FacetItem
active={this.props.values.includes(item)}
key={item}
loading={this.props.loading}
name={this.props.renderFacetItem(item)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(item))}
tooltip={this.props.getFacetItemText(item)}
value={item}
/>
))}
</FacetItemsList>
<ListStyleFacetFooter
count={limitedList.length}
showLess={this.state.showFullList ? this.hideFullList : undefined}
showMore={this.showFullList}
total={sortedItems.length}
/>
{mightHaveMoreResults &&
this.state.showFullList && (
<div className="alert alert-warning spacer-top">
{translate('facet_might_have_more_results')}
</div>
)}
</>
);
}

@@ -211,6 +272,7 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
{searchResults.map(result => this.renderSearchResult(result))}
</FacetItemsList>
<ListFooter
className="spacer-bottom"
count={searchResults.length}
loadMore={this.searchMore}
ready={!searching}
@@ -223,7 +285,11 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
renderSearchResult(result: S) {
const key = this.props.getSearchResultKey(result);
const active = this.props.values.includes(key);
const stat = this.getStat(key);

// default to 0 if we're sure there are not more results
const isFacetExhaustive = Object.keys(this.props.stats || {}).length < this.props.maxItems!;
const stat = this.getStat(key, isFacetExhaustive);

return (
<FacetItem
active={active}
@@ -232,7 +298,7 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
loading={this.props.loading}
name={this.props.renderSearchResult(result, this.state.query)}
onClick={this.handleItemClick}
stat={stat && formatFacetStat(stat)}
stat={formatFacetStat(stat)}
tooltip={this.props.getSearchResultText(result)}
value={key}
/>

+ 71
- 0
server/sonar-web/src/main/js/components/facet/ListStyleFacetFooter.tsx 查看文件

@@ -0,0 +1,71 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { translate, translateWithParameters } from '../../helpers/l10n';
import { formatMeasure } from '../../helpers/measures';

interface Props {
className?: string;
count: number;
showMore: () => void;
showLess: (() => void) | undefined;
total: number;
}

export default class ListStyleFacetFooter extends React.PureComponent<Props> {
handleShowMoreClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.props.showMore();
};

handleShowLessClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
if (this.props.showLess) {
this.props.showLess();
}
};

render() {
const { count, total } = this.props;
const hasMore = total > count;
const allShown = Boolean(total && total === count);

return (
<footer className="note spacer-top spacer-bottom text-center">
{translateWithParameters('x_show', formatMeasure(count, 'INT', null))}

{hasMore && (
<a className="spacer-left text-muted" href="#" onClick={this.handleShowMoreClick}>
{translate('show_more')}
</a>
)}

{this.props.showLess &&
allShown && (
<a className="spacer-left text-muted" href="#" onClick={this.handleShowLessClick}>
{translate('show_less')}
</a>
)}
</footer>
);
}
}

+ 44
- 0
server/sonar-web/src/main/js/components/facet/__tests__/ListStyleFacet-test.tsx 查看文件

@@ -116,6 +116,50 @@ it('should search', async () => {
expect(onSearch).lastCalledWith('blabla');
});

it('should limit the number of items', () => {
const wrapper = shallowRender({ maxInitialItems: 2, maxItems: 5 });
expect(wrapper.find('FacetItem').length).toBe(2);

wrapper.find('ListStyleFacetFooter').prop<Function>('showMore')();
wrapper.update();
expect(wrapper.find('FacetItem').length).toBe(3);

wrapper.find('ListStyleFacetFooter').prop<Function>('showLess')();
wrapper.update();
expect(wrapper.find('FacetItem').length).toBe(2);
});

it('should show warning that there might be more results', () => {
const wrapper = shallowRender({ maxInitialItems: 2, maxItems: 3 });
wrapper.find('ListStyleFacetFooter').prop<Function>('showMore')();
wrapper.update();
expect(wrapper.find('.alert-warning').exists()).toBe(true);
});

it('should reset state when closes', () => {
const wrapper = shallowRender();
wrapper.setState({
query: 'foobar',
searchResults: ['foo', 'bar'],
searching: true,
showFullList: true
});

wrapper.setProps({ open: false });
expect(wrapper.state('query')).toBe('');
expect(wrapper.state('searchResults')).toBe(undefined);
expect(wrapper.state('searching')).toBe(false);
expect(wrapper.state('showFullList')).toBe(false);
});

it('should collapse list when new stats have few results', () => {
const wrapper = shallowRender({ maxInitialItems: 2, maxItems: 3 });
wrapper.setState({ showFullList: true });

wrapper.setProps({ stats: { d: 1 } });
expect(wrapper.state('showFullList')).toBe(false);
});

function shallowRender(props: Partial<Props<string>> = {}) {
return shallow(
<ListStyleFacet

+ 56
- 0
server/sonar-web/src/main/js/components/facet/__tests__/ListStyleFacetFooter-test.tsx 查看文件

@@ -0,0 +1,56 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
import ListStyleFacetFooter from '../ListStyleFacetFooter';
import { click } from '../../../helpers/testUtils';

it('should not render "show more"', () => {
expect(
shallow(<ListStyleFacetFooter count={3} showLess={undefined} showMore={jest.fn()} total={3} />)
).toMatchSnapshot();
});

it('should show more', () => {
const showMore = jest.fn();
const wrapper = shallow(
<ListStyleFacetFooter count={3} showLess={undefined} showMore={showMore} total={15} />
);
expect(wrapper).toMatchSnapshot();
click(wrapper.find('a'));
expect(showMore).toBeCalled();
});

it('should show less', () => {
const showLess = jest.fn();
const wrapper = shallow(
<ListStyleFacetFooter count={15} showLess={showLess} showMore={jest.fn()} total={15} />
);
expect(wrapper).toMatchSnapshot();
click(wrapper.find('a'));
expect(showLess).toBeCalled();
});

it('should not render "show less"', () => {
const wrapper = shallow(
<ListStyleFacetFooter count={15} showLess={undefined} showMore={jest.fn()} total={15} />
);
expect(wrapper).toMatchSnapshot();
});

+ 1
- 0
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap 查看文件

@@ -36,6 +36,7 @@ exports[`should render disabled 1`] = `
<span
className="search-navigator-facet"
data-facet="bar"
title="foo"
>
<span
className="facet-name"

+ 100
- 79
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap 查看文件

@@ -25,44 +25,51 @@ exports[`should render 1`] = `
placeholder="search for foo..."
value=""
/>
<FacetItemsList>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="a"
loading={false}
name="a"
onClick={[Function]}
stat="10"
tooltip="a"
value="a"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="b"
loading={false}
name="b"
onClick={[Function]}
stat="8"
tooltip="b"
value="b"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="c"
loading={false}
name="c"
onClick={[Function]}
stat="1"
tooltip="c"
value="c"
<React.Fragment>
<FacetItemsList>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="a"
loading={false}
name="a"
onClick={[Function]}
stat="10"
tooltip="a"
value="a"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="b"
loading={false}
name="b"
onClick={[Function]}
stat="8"
tooltip="b"
value="b"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="c"
loading={false}
name="c"
onClick={[Function]}
stat="1"
tooltip="c"
value="c"
/>
</FacetItemsList>
<ListStyleFacetFooter
count={3}
showMore={[Function]}
total={3}
/>
</FacetItemsList>
</React.Fragment>
<MultipleSelectionHint
options={3}
values={0}
@@ -100,28 +107,31 @@ exports[`should search 1`] = `
<FacetItemsList>
<FacetItem
active={false}
disabled={false}
disabled={true}
halfWidth={false}
key="d"
loading={false}
name="d"
onClick={[Function]}
stat={0}
tooltip="d"
value="d"
/>
<FacetItem
active={false}
disabled={false}
disabled={true}
halfWidth={false}
key="e"
loading={false}
name="e"
onClick={[Function]}
stat={0}
tooltip="e"
value="e"
/>
</FacetItemsList>
<ListFooter
className="spacer-bottom"
count={2}
loadMore={[Function]}
ready={true}
@@ -165,39 +175,43 @@ exports[`should search 2`] = `
<FacetItemsList>
<FacetItem
active={false}
disabled={false}
disabled={true}
halfWidth={false}
key="d"
loading={false}
name="d"
onClick={[Function]}
stat={0}
tooltip="d"
value="d"
/>
<FacetItem
active={false}
disabled={false}
disabled={true}
halfWidth={false}
key="e"
loading={false}
name="e"
onClick={[Function]}
stat={0}
tooltip="e"
value="e"
/>
<FacetItem
active={false}
disabled={false}
disabled={true}
halfWidth={false}
key="f"
loading={false}
name="f"
onClick={[Function]}
stat={0}
tooltip="f"
value="f"
/>
</FacetItemsList>
<ListFooter
className="spacer-bottom"
count={3}
loadMore={[Function]}
ready={true}
@@ -237,44 +251,51 @@ exports[`should search 3`] = `
placeholder="search for foo..."
value=""
/>
<FacetItemsList>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="a"
loading={false}
name="a"
onClick={[Function]}
stat="10"
tooltip="a"
value="a"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="b"
loading={false}
name="b"
onClick={[Function]}
stat="8"
tooltip="b"
value="b"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="c"
loading={false}
name="c"
onClick={[Function]}
stat="1"
tooltip="c"
value="c"
<React.Fragment>
<FacetItemsList>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="a"
loading={false}
name="a"
onClick={[Function]}
stat="10"
tooltip="a"
value="a"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="b"
loading={false}
name="b"
onClick={[Function]}
stat="8"
tooltip="b"
value="b"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="c"
loading={false}
name="c"
onClick={[Function]}
stat="1"
tooltip="c"
value="c"
/>
</FacetItemsList>
<ListStyleFacetFooter
count={3}
showMore={[Function]}
total={3}
/>
</FacetItemsList>
</React.Fragment>
<MultipleSelectionHint
options={3}
values={0}

+ 47
- 0
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/ListStyleFacetFooter-test.tsx.snap 查看文件

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

exports[`should not render "show less" 1`] = `
<footer
className="note spacer-top spacer-bottom text-center"
>
x_show.15
</footer>
`;

exports[`should not render "show more" 1`] = `
<footer
className="note spacer-top spacer-bottom text-center"
>
x_show.3
</footer>
`;

exports[`should show less 1`] = `
<footer
className="note spacer-top spacer-bottom text-center"
>
x_show.15
<a
className="spacer-left text-muted"
href="#"
onClick={[Function]}
>
show_less
</a>
</footer>
`;

exports[`should show more 1`] = `
<footer
className="note spacer-top spacer-bottom text-center"
>
x_show.3
<a
className="spacer-left text-muted"
href="#"
onClick={[Function]}
>
show_more
</a>
</footer>
`;

+ 2
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties 查看文件

@@ -218,6 +218,7 @@ created_by=Created by
default_error_message=The request cannot be processed. Try again later.
default_severity=Default severity
edit_permissions=Edit Permissions
facet_might_have_more_results=There might be more results, try another set of filters to see them.
false_positive=False positive
go_back_to_homepage=Go back to the homepage
last_analysis_before=Last analysis before
@@ -241,6 +242,7 @@ set_as_default=Set as Default
short_number_suffix.g=G
short_number_suffix.k=k
short_number_suffix.m=M
show_less=Show Less
show_more=Show More
show_all=Show All
should_be_unique=Should be unique

Loading…
取消
儲存