From: Stas Vilchik Date: Wed, 8 Aug 2018 16:46:50 +0000 (+0200) Subject: SONAR-6400 allow to show more items in a facet (#597) X-Git-Tag: 7.5~567 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=15f3d9c2584cca304590ad68dea4d025ac356813;p=sonarqube.git SONAR-6400 allow to show more items in a facet (#597) --- diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx index 2157620ad2f..fcb95135e1c 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx @@ -76,6 +76,9 @@ class LanguageFacet extends React.PureComponent { 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} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/TagFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/TagFacet.tsx index be3049145fa..491a8e5e4db 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/TagFacet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/TagFacet.tsx @@ -69,6 +69,9 @@ export default class TagFacet extends React.PureComponent { 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} diff --git a/server/sonar-web/src/main/js/components/facet/FacetItem.tsx b/server/sonar-web/src/main/js/components/facet/FacetItem.tsx index 0f86901fffb..70beec9c901 100644 --- a/server/sonar-web/src/main/js/components/facet/FacetItem.tsx +++ b/server/sonar-web/src/main/js/components/facet/FacetItem.tsx @@ -55,7 +55,7 @@ export default class FacetItem extends React.PureComponent { }); return this.props.disabled ? ( - + {name} {this.props.stat != null && {this.props.stat}} diff --git a/server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx b/server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx index ae2222ea4cb..4b0bc9c93ac 100644 --- a/server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx +++ b/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 { 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 { 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 { autoFocus: boolean; query: string; searching: boolean; - searchResults?: S[]; searchPaging?: Paging; + searchResults?: S[]; + showFullList: boolean; } export default class ListStyleFacet extends React.Component, State> { mounted = false; + static defaultProps = { + maxInitialItems: 15, + maxItems: 100 + }; + state: State = { autoFocus: false, query: '', - searching: false + searching: false, + showFullList: false }; componentDidMount() { @@ -72,9 +82,18 @@ export default class ListStyleFacet extends React.Component, State) { - // 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 extends React.Component, State { + 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 extends React.Component, State -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 ( - - {items.map(item => ( - - ))} - + <> + + {limitedList.map(item => ( + + ))} + + + {mightHaveMoreResults && + this.state.showFullList && ( +
+ {translate('facet_might_have_more_results')} +
+ )} + ); } @@ -211,6 +272,7 @@ export default class ListStyleFacet extends React.Component, State this.renderSearchResult(result))} extends React.Component, State extends React.Component, State diff --git a/server/sonar-web/src/main/js/components/facet/ListStyleFacetFooter.tsx b/server/sonar-web/src/main/js/components/facet/ListStyleFacetFooter.tsx new file mode 100644 index 00000000000..b4fe7eb4d83 --- /dev/null +++ b/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 { + handleShowMoreClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + this.props.showMore(); + }; + + handleShowLessClick = (event: React.MouseEvent) => { + 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 ( + + ); + } +} diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/ListStyleFacet-test.tsx b/server/sonar-web/src/main/js/components/facet/__tests__/ListStyleFacet-test.tsx index 7cb3f16a0a0..c9c4180d1ea 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/ListStyleFacet-test.tsx +++ b/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('showMore')(); + wrapper.update(); + expect(wrapper.find('FacetItem').length).toBe(3); + + wrapper.find('ListStyleFacetFooter').prop('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('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> = {}) { return shallow( { + expect( + shallow() + ).toMatchSnapshot(); +}); + +it('should show more', () => { + const showMore = jest.fn(); + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + click(wrapper.find('a')); + expect(showMore).toBeCalled(); +}); + +it('should show less', () => { + const showLess = jest.fn(); + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + click(wrapper.find('a')); + expect(showLess).toBeCalled(); +}); + +it('should not render "show less"', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap index 725eb2a401f..eaa0c2cf53c 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap @@ -36,6 +36,7 @@ exports[`should render disabled 1`] = ` - - - - + + + + + + - + - - - - + + + + + + - + + x_show.15 + +`; + +exports[`should not render "show more" 1`] = ` +
+ x_show.3 +
+`; + +exports[`should show less 1`] = ` + +`; + +exports[`should show more 1`] = ` + +`; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index cbe505478e9..56570e0cfd7 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/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