diff options
author | stanislavh <stanislav.honcharov@sonarsource.com> | 2023-04-21 14:56:17 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-04-25 20:03:00 +0000 |
commit | 3aa961cd6a7d63edb7ac011c27ec695dfb94c253 (patch) | |
tree | 43a1cae5fd31224a76457ed81e0c5bacfaf1fa1d /server/sonar-web/src | |
parent | fc5bd48d213b39a83a3828eaec1a9e89b5b88fbd (diff) | |
download | sonarqube-3aa961cd6a7d63edb7ac011c27ec695dfb94c253.tar.gz sonarqube-3aa961cd6a7d63edb7ac011c27ec695dfb94c253.zip |
SONAR-19069 Add Show more filters button
Diffstat (limited to 'server/sonar-web/src')
23 files changed, 368 insertions, 120 deletions
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx index a9b52ce93e8..1bb599037e1 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx @@ -125,6 +125,7 @@ export default function FacetsList(props: FacetsListProps) { sonarsourceSecurity={props.query.sonarsourceSecurity} sonarsourceSecurityOpen={!!props.openFacets.sonarsourceSecurity} sonarsourceSecurityStats={props.facets && props.facets.sonarsourceSecurity} + forceShow={true} /> <AvailableSinceFacet onChange={props.onFilterChange} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap index ec82fc3872b..c27efaa2bf5 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap @@ -40,6 +40,7 @@ exports[`should render correctly 1`] = ` fetchingOwaspTop10={false} fetchingOwaspTop10-2021={false} fetchingSonarSourceSecurity={false} + forceShow={true} onChange={[MockFunction]} onToggle={[MockFunction]} open={false} diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx index 113bd3addf5..78b6be118e1 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx @@ -61,6 +61,7 @@ describe('issues app', () => { it('should support OWASP Top 10 version 2021', async () => { const user = userEvent.setup(); renderIssueApp(); + await user.click(ui.showFiltersButton().get()); await user.click(screen.getByRole('button', { name: 'issues.facet.standards' })); const owaspTop102021 = screen.getByRole('button', { name: 'issues.facet.owaspTop10_2021' }); expect(owaspTop102021).toBeInTheDocument(); @@ -272,6 +273,7 @@ describe('issues app', () => { const user = userEvent.setup(); renderIssueApp(); await waitOnDataLoaded(); + await user.click(ui.showFiltersButton().get()); // Ensure issue type filter is unchecked await user.click(ui.typeFacet.get()); @@ -327,6 +329,7 @@ describe('issues app', () => { const user = userEvent.setup(); renderIssueApp(); await waitOnDataLoaded(); + await user.click(ui.showFiltersButton().get()); // Select a characteristic await user.click(ui.clearCharacteristicFilter.get()); @@ -435,6 +438,7 @@ describe('issues app', () => { issuesHandler.setCurrentUser(currentUser); renderIssueApp(currentUser); await waitOnDataLoaded(); + await user.click(ui.showFiltersButton().get()); // Select a specific date range such that only one issue matches await user.click(ui.creationDateFacet.get()); @@ -480,6 +484,7 @@ describe('issues app', () => { renderIssueApp(); + await user.click(ui.showFiltersButton().get()); await user.click(await ui.ruleFacet.find()); await user.type(ui.ruleFacetSearch.get(), 'rule'); expect(within(ui.ruleFacetList.get()).getAllByRole('checkbox')).toHaveLength(2); @@ -494,6 +499,7 @@ describe('issues app', () => { }) ).toBeInTheDocument(); + await user.click(await ui.typeFacet.find()); await user.click(ui.vulnerabilityIssueTypeFilter.get()); // after changing the issue type filter, search field is reset, so we type again await user.type(ui.ruleFacetSearch.get(), 'rule'); @@ -515,6 +521,7 @@ describe('issues app', () => { renderIssueApp(); + await user.click(ui.showFiltersButton().get()); await user.click(await ui.languageFacet.find()); expect(await ui.languageFacetList.find()).toBeInTheDocument(); expect( @@ -526,6 +533,7 @@ describe('issues app', () => { await user.click(ui.languageFacet.get()); expect(ui.languageFacetList.query()).not.toBeInTheDocument(); + await user.click(await ui.typeFacet.find()); await user.click(ui.vulnerabilityIssueTypeFilter.get()); await user.click(ui.languageFacet.get()); expect(await ui.languageFacetList.find()).toBeInTheDocument(); diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx index dc3e038eb77..4b482da7fea 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx @@ -33,11 +33,11 @@ import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; import EmptySearch from '../../../components/common/EmptySearch'; import FiltersHeader from '../../../components/common/FiltersHeader'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; +import { Button } from '../../../components/controls/buttons'; import ButtonToggle from '../../../components/controls/ButtonToggle'; import Checkbox from '../../../components/controls/Checkbox'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import ListFooter from '../../../components/controls/ListFooter'; -import { Button } from '../../../components/controls/buttons'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import withIndexationGuard from '../../../components/hoc/withIndexationGuard'; import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; @@ -80,23 +80,24 @@ import { CurrentUser, UserBase } from '../../../types/users'; import * as actions from '../actions'; import ConciseIssuesList from '../conciseIssuesList/ConciseIssuesList'; import ConciseIssuesListHeader from '../conciseIssuesList/ConciseIssuesListHeader'; +import FiltersVisibilityButton from '../sidebar/FiltersVisibilityButton'; import Sidebar from '../sidebar/Sidebar'; import '../styles.css'; import { - OpenFacets, - Query, - STANDARDS, areMyIssuesSelected, areQueriesEqual, getOpen, getOpenIssue, + OpenFacets, parseFacets, parseQuery, + Query, saveMyIssues, serializeQuery, shouldOpenSonarSourceSecurityFacet, shouldOpenStandardsChildFacet, shouldOpenStandardsFacet, + STANDARDS, } from '../utils'; import BulkChangeModal, { MAX_PAGE_SIZE } from './BulkChangeModal'; import IssueHeader from './IssueHeader'; @@ -129,6 +130,7 @@ export interface State { loadingMore: boolean; locationsNavigator: boolean; myIssues: boolean; + showAllFilters: boolean; openFacets: OpenFacets; openIssue?: Issue; openPopup?: { issue: string; name: string }; @@ -168,6 +170,7 @@ export class App extends React.PureComponent<Props, State> { loadingMore: false, locationsNavigator: false, myIssues: areMyIssuesSelected(props.location.query), + showAllFilters: false, openFacets: { characteristics: { [IssueCharacteristicFitFor.Production]: true, @@ -644,6 +647,12 @@ export class App extends React.PureComponent<Props, State> { return translateWithParameters('issues.bulk_change_X_issues', count); }; + handleShowFiltersChange = (showAllFilters: boolean) => { + this.setState({ + showAllFilters, + }); + }; + handleFilterChange = (changes: Partial<Query>) => { this.props.router.push({ pathname: this.props.location.pathname, @@ -905,7 +914,7 @@ export class App extends React.PureComponent<Props, State> { renderFacets() { const { component, currentUser, branchLike } = this.props; - const { query } = this.state; + const { query, showAllFilters } = this.state; return ( <div className="layout-page-filters"> @@ -934,12 +943,17 @@ export class App extends React.PureComponent<Props, State> { onFilterChange={this.handleFilterChange} openFacets={this.state.openFacets} query={query} + showAllFilters={showAllFilters} referencedComponentsById={this.state.referencedComponentsById} referencedComponentsByKey={this.state.referencedComponentsByKey} referencedLanguages={this.state.referencedLanguages} referencedRules={this.state.referencedRules} referencedUsers={this.state.referencedUsers} /> + <FiltersVisibilityButton + showAllFilters={showAllFilters} + onClick={this.handleShowFiltersChange} + /> </div> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx index 89d3d4f6f31..ff8ff15a035 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx @@ -39,6 +39,7 @@ interface Props { query: Query; stats: Dict<number> | undefined; referencedUsers: Dict<UserBase>; + forceShow: boolean; } export default class AssigneeFacet extends React.PureComponent<Props> { @@ -137,15 +138,20 @@ export default class AssigneeFacet extends React.PureComponent<Props> { }; render() { - const values = [...this.props.assignees]; - if (!this.props.assigned) { + const { forceShow, assignees, assigned, stats, open, fetching, query } = this.props; + const values = [...assignees]; + if (!assigned) { values.push(''); } + if (values.length < 1 && !forceShow) { + return null; + } + return ( <ListStyleFacet<UserBase> facetHeader={translate('issues.facet.assignees')} - fetching={this.props.fetching} + fetching={fetching} getFacetItemText={this.getAssigneeName} getSearchResultKey={(user) => user.login} getSearchResultText={(user) => user.name || user.login} @@ -157,13 +163,13 @@ export default class AssigneeFacet extends React.PureComponent<Props> { onItemClick={this.handleItemClick} onSearch={this.handleSearch} onToggle={this.props.onToggle} - open={this.props.open} + open={open} property="assignees" - query={omit(this.props.query, 'assigned', 'assignees')} + query={omit(query, 'assigned', 'assignees')} renderFacetItem={this.renderFacetItem} renderSearchResult={this.renderSearchResult} searchPlaceholder={translate('search.search_for_users')} - stats={this.props.stats} + stats={stats} values={values} /> ); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx index 1647c689495..ada0eb6b2d7 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx @@ -37,6 +37,7 @@ interface Props { query: Query; stats: Dict<number> | undefined; author: string[]; + forceShow: boolean; } const SEARCH_SIZE = 100; @@ -66,10 +67,16 @@ export default class AuthorFacet extends React.PureComponent<Props> { }; render() { + const { forceShow, fetching, open, stats, author, query } = this.props; + + if (author.length < 1 && !forceShow) { + return null; + } + return ( <ListStyleFacet<string> facetHeader={translate('issues.facet.authors')} - fetching={this.props.fetching} + fetching={fetching} getFacetItemText={this.identity} getSearchResultKey={this.identity} getSearchResultText={this.identity} @@ -77,14 +84,14 @@ export default class AuthorFacet extends React.PureComponent<Props> { onChange={this.props.onChange} onSearch={this.handleSearch} onToggle={this.props.onToggle} - open={this.props.open} + open={open} property="author" - query={omit(this.props.query, 'author')} + query={omit(query, 'author')} renderFacetItem={this.identity} renderSearchResult={this.renderSearchResult} searchPlaceholder={translate('search.search_for_authors')} - stats={this.props.stats} - values={this.props.author} + stats={stats} + values={author} /> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/CharacteristicFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/CharacteristicFacet.tsx index 094f3e27c72..eb0b153027e 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/CharacteristicFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/CharacteristicFacet.tsx @@ -154,7 +154,9 @@ export default class CharacteristicFacet extends React.PureComponent<Props> { {this.props.open && ( <> - <FacetItemsList>{availableCharacteristics.map(this.renderItem)}</FacetItemsList> + <FacetItemsList label={this.property}> + {availableCharacteristics.map(this.renderItem)} + </FacetItemsList> <MultipleSelectionHint options={Object.keys(availableCharacteristics).length} values={values.length} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx index 5918ccc99dd..fd37e3fb43c 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx @@ -50,6 +50,7 @@ interface Props { open: boolean; inNewCodePeriod: boolean; stats: Dict<number> | undefined; + forceShow: boolean; } export class CreationDateFacet extends React.PureComponent<Props & WrappedComponentProps> { @@ -259,10 +260,10 @@ export class CreationDateFacet extends React.PureComponent<Props & WrappedCompon if (createdAt) { return ( <div className="search-navigator-facet-container"> - <DateTimeFormatter date={this.props.createdAt} /> + <DateTimeFormatter date={createdAt} /> <br /> <span className="note"> - <DateFromNow date={this.props.createdAt} /> + <DateFromNow date={createdAt} /> </span> </div> ); @@ -287,7 +288,12 @@ export class CreationDateFacet extends React.PureComponent<Props & WrappedCompon } render() { - const { fetching, open } = this.props; + const { forceShow, open, fetching } = this.props; + const values = this.getValues(); + + if (values.length < 1 && !forceShow) { + return null; + } return ( <FacetBox property={this.property}> diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx index b4888017813..00b1ae90115 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx @@ -29,6 +29,7 @@ import { highlightTerm } from '../../../helpers/search'; import { BranchLike } from '../../../types/branch-like'; import { TreeComponentWithPath } from '../../../types/component'; import { Facet } from '../../../types/issues'; +import { MetricKey } from '../../../types/metrics'; import { Query } from '../utils'; interface Props { @@ -42,6 +43,7 @@ interface Props { open: boolean; query: Query; stats: Facet | undefined; + forceShow: boolean; } export default class DirectoryFacet extends React.PureComponent<Props> { @@ -73,7 +75,7 @@ export default class DirectoryFacet extends React.PureComponent<Props> { }; loadSearchResultCount = (directories: TreeComponentWithPath[]) => { - return this.props.loadSearchResultCount('directories', { + return this.props.loadSearchResultCount(MetricKey.directories, { directories: directories.map((directory) => directory.path), }); }; @@ -94,10 +96,16 @@ export default class DirectoryFacet extends React.PureComponent<Props> { }; render() { + const { forceShow, directories, stats, fetching, open, query } = this.props; + + if (directories.length < 1 && !forceShow) { + return null; + } + return ( <ListStyleFacet<TreeComponentWithPath> facetHeader={translate('issues.facet.directories')} - fetching={this.props.fetching} + fetching={fetching} getFacetItemText={this.getFacetItemText} getSearchResultKey={this.getSearchResultKey} getSearchResultText={this.getSearchResultText} @@ -106,14 +114,14 @@ export default class DirectoryFacet extends React.PureComponent<Props> { onChange={this.props.onChange} onSearch={this.handleSearch} onToggle={this.props.onToggle} - open={this.props.open} - property="directories" - query={omit(this.props.query, 'directories')} + open={open} + property={MetricKey.directories} + query={omit(query, MetricKey.directories)} renderFacetItem={this.renderFacetItem} renderSearchResult={this.renderSearchResult} searchPlaceholder={translate('search.search_for_directories')} - stats={this.props.stats} - values={this.props.directories} + stats={stats} + values={directories} /> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx index e315f8e0454..e27b23a5104 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx @@ -30,6 +30,7 @@ import { isDefined } from '../../../helpers/types'; import { BranchLike } from '../../../types/branch-like'; import { TreeComponentWithPath } from '../../../types/component'; import { Facet } from '../../../types/issues'; +import { MetricKey } from '../../../types/metrics'; import { Query } from '../utils'; interface Props { @@ -43,6 +44,7 @@ interface Props { open: boolean; query: Query; stats: Facet | undefined; + forceShow: boolean; } const MAX_PATH_LENGTH = 15; @@ -75,7 +77,7 @@ export default class FileFacet extends React.PureComponent<Props> { }; loadSearchResultCount = (files: TreeComponentWithPath[]) => { - return this.props.loadSearchResultCount('files', { + return this.props.loadSearchResultCount(MetricKey.files, { files: files .map((file) => { return file.path; @@ -106,10 +108,16 @@ export default class FileFacet extends React.PureComponent<Props> { }; render() { + const { forceShow, files, fetching, open, query, stats } = this.props; + + if (files.length < 1 && !forceShow) { + return null; + } + return ( <ListStyleFacet<TreeComponentWithPath> facetHeader={translate('issues.facet.files')} - fetching={this.props.fetching} + fetching={fetching} getFacetItemText={this.getFacetItemText} getSearchResultKey={this.getSearchResultKey} getSearchResultText={this.getSearchResultText} @@ -118,14 +126,14 @@ export default class FileFacet extends React.PureComponent<Props> { onChange={this.props.onChange} onSearch={this.handleSearch} onToggle={this.props.onToggle} - open={this.props.open} - property="files" - query={omit(this.props.query, 'files')} + open={open} + property={MetricKey.files} + query={omit(query, MetricKey.files)} renderFacetItem={this.renderFacetItem} renderSearchResult={this.renderSearchResult} searchPlaceholder={translate('search.search_for_files')} - stats={this.props.stats} - values={this.props.files} + stats={stats} + values={files} /> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/FiltersVisibilityButton.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/FiltersVisibilityButton.tsx new file mode 100644 index 00000000000..45da86c9112 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/FiltersVisibilityButton.tsx @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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 classNames from 'classnames'; +import React from 'react'; +import { Button } from '../../../components/controls/buttons'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + showAllFilters: boolean; + onClick: (val: boolean) => void; +} + +export default function FiltersVisibilityButton(props: Props) { + const { showAllFilters } = props; + + return ( + <div className="display-flex-justify-center spacer-top"> + <Button + onClick={() => props.onClick(!showAllFilters)} + className={classNames({ it__show_more_facets: !showAllFilters })} + > + {translate(showAllFilters ? 'issues.show_less_filters' : 'issues.show_more_filters')} + </Button> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx index 992f4a63b5c..a2b34e96573 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx @@ -39,6 +39,7 @@ interface Props { query: Query; referencedLanguages: Dict<ReferencedLanguage>; stats: Dict<number> | undefined; + forceShow: boolean; } class LanguageFacet extends React.PureComponent<Props> { @@ -79,10 +80,16 @@ class LanguageFacet extends React.PureComponent<Props> { }; render() { + const { forceShow, stats, selectedLanguages, open, fetching, query } = this.props; + + if (selectedLanguages.length < 1 && !forceShow) { + return null; + } + return ( <ListStyleFacet<Language> facetHeader={translate('issues.facet.languages')} - fetching={this.props.fetching} + fetching={fetching} getFacetItemText={this.getLanguageName} getSearchResultKey={(language) => language.key} getSearchResultText={(language) => language.name} @@ -91,14 +98,14 @@ class LanguageFacet extends React.PureComponent<Props> { onChange={this.props.onChange} onSearch={this.handleSearch} onToggle={this.props.onToggle} - open={this.props.open} + open={open} property="languages" - query={omit(this.props.query, 'languages')} + query={omit(query, 'languages')} renderFacetItem={this.getLanguageName} renderSearchResult={this.renderSearchResult} searchPlaceholder={translate('search.search_for_languages')} - stats={this.props.stats} - values={this.props.selectedLanguages} + stats={stats} + values={selectedLanguages} /> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx index 4c738b2f4fa..93ebce99746 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx @@ -26,6 +26,7 @@ import { translate } from '../../../helpers/l10n'; import { highlightTerm } from '../../../helpers/search'; import { ComponentQualifier } from '../../../types/component'; import { Facet, ReferencedComponent } from '../../../types/issues'; +import { MetricKey } from '../../../types/metrics'; import { Component, Dict, Paging } from '../../../types/types'; import { Query } from '../utils'; @@ -40,6 +41,7 @@ interface Props { query: Query; referencedComponents: Dict<ReferencedComponent>; stats: Dict<number> | undefined; + forceShow: boolean; } interface SearchedProject { @@ -95,7 +97,7 @@ export default class ProjectFacet extends React.PureComponent<Props> { }; loadSearchResultCount = (projects: SearchedProject[]) => { - return this.props.loadSearchResultCount('projects', { + return this.props.loadSearchResultCount(MetricKey.projects, { projects: projects.map((project) => project.key), }); }; @@ -117,10 +119,16 @@ export default class ProjectFacet extends React.PureComponent<Props> { ); render() { + const { forceShow, projects, stats, open, fetching, query } = this.props; + + if (projects.length < 1 && !forceShow) { + return null; + } + return ( <ListStyleFacet<SearchedProject> facetHeader={translate('issues.facet.projects')} - fetching={this.props.fetching} + fetching={fetching} getFacetItemText={this.getProjectName} getSearchResultKey={(project) => project.key} getSearchResultText={(project) => project.name} @@ -128,14 +136,14 @@ export default class ProjectFacet extends React.PureComponent<Props> { onChange={this.props.onChange} onSearch={this.handleSearch} onToggle={this.props.onToggle} - open={this.props.open} - property="projects" - query={omit(this.props.query, 'projects')} + open={open} + property={MetricKey.projects} + query={omit(query, MetricKey.projects)} renderFacetItem={this.renderFacetItem} renderSearchResult={this.renderSearchResult} searchPlaceholder={translate('search.search_for_projects')} - stats={this.props.stats} - values={this.props.projects} + stats={stats} + values={projects} /> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx index 32578f5dc75..c437f10efc4 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx @@ -37,6 +37,7 @@ interface Props { resolved: boolean; resolutions: string[]; stats: Dict<number> | undefined; + forceShow: boolean; } const RESOLUTIONS = [ @@ -55,10 +56,10 @@ export default class ResolutionFacet extends React.PureComponent<Props> { }; handleItemClick = (itemValue: string, multiple: boolean) => { - const { resolutions } = this.props; + const { resolutions, resolved } = this.props; if (itemValue === '') { // unresolved - this.props.onChange({ resolved: !this.props.resolved, resolutions: [] }); + this.props.onChange({ resolved: !resolved, resolutions: [] }); } else if (multiple) { const newValue = orderBy( resolutions.includes(itemValue) @@ -115,9 +116,13 @@ export default class ResolutionFacet extends React.PureComponent<Props> { }; render() { - const { fetching, open, resolutions, stats = {} } = this.props; + const { resolutions, stats = {}, forceShow, fetching, open } = this.props; const values = resolutions.map((resolution) => this.getFacetItemName(resolution)); + if (values.length < 1 && !forceShow) { + return null; + } + return ( <FacetBox property={this.property}> <FacetHeader diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx index 1da206faff1..494d420b810 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx @@ -36,6 +36,7 @@ interface Props { query: Query; referencedRules: Dict<ReferencedRule>; stats: Dict<number> | undefined; + forceShow: boolean; } export default class RuleFacet extends React.PureComponent<Props> { @@ -78,7 +79,11 @@ export default class RuleFacet extends React.PureComponent<Props> { }; render() { - const { fetching, open, query, stats } = this.props; + const { forceShow, stats, query, open, fetching } = this.props; + + if (query.rules.length < 1 && !forceShow) { + return null; + } return ( <ListStyleFacet<Rule> diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ScopeFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ScopeFacet.tsx index 329c046b92d..73320c77762 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ScopeFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ScopeFacet.tsx @@ -37,13 +37,17 @@ export interface ScopeFacetProps { open: boolean; scopes: string[]; stats: Dict<number> | undefined; + forceShow: boolean; } export default function ScopeFacet(props: ScopeFacetProps) { - const { fetching, open, scopes = [], stats = {} } = props; + const { fetching, open, scopes = [], stats = {}, forceShow } = props; const values = scopes.map((scope) => translate('issue.scope', scope)); const property = 'scopes'; + if (values.length < 1 && !forceShow) { + return null; + } return ( <FacetBox property={property}> diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx index 8f25db0785b..c677090a724 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx @@ -76,11 +76,21 @@ export interface Props { referencedLanguages: Dict<ReferencedLanguage>; referencedRules: Dict<ReferencedRule>; referencedUsers: Dict<UserBase>; + showAllFilters: boolean; } export class Sidebar extends React.PureComponent<Props> { renderComponentFacets() { - const { component, facets, loadingFacets, openFacets, query, branchLike } = this.props; + const { + component, + facets, + loadingFacets, + openFacets, + query, + branchLike, + showAllFilters, + loadSearchResultCount, + } = this.props; const hasFileOrDirectory = !isApplication(component?.qualifier) && !isPortfolioLike(component?.qualifier); if (!component || !hasFileOrDirectory) { @@ -88,7 +98,7 @@ export class Sidebar extends React.PureComponent<Props> { } const commonProps = { componentKey: component.key, - loadSearchResultCount: this.props.loadSearchResultCount, + loadSearchResultCount, onChange: this.props.onFilterChange, onToggle: this.props.onFacetToggle, query, @@ -102,6 +112,7 @@ export class Sidebar extends React.PureComponent<Props> { fetching={loadingFacets.directories === true} open={!!openFacets.directories} stats={facets.directories} + forceShow={showAllFilters} {...commonProps} /> )} @@ -111,6 +122,7 @@ export class Sidebar extends React.PureComponent<Props> { files={query.files} open={!!openFacets.files} stats={facets.files} + forceShow={showAllFilters} {...commonProps} /> </> @@ -126,6 +138,13 @@ export class Sidebar extends React.PureComponent<Props> { openFacets, query, branchLike, + showAllFilters, + loadingFacets, + loadSearchResultCount, + referencedRules, + referencedLanguages, + referencedComponentsByKey, + referencedUsers, } = this.props; const disableDeveloperAggregatedInfo = @@ -144,14 +163,14 @@ export class Sidebar extends React.PureComponent<Props> { <> {displayPeriodFilter && ( <PeriodFilter - fetching={this.props.loadingFacets.period === true} + fetching={loadingFacets.period === true} onChange={this.props.onFilterChange} stats={facets.period} newCodeSelected={query.inNewCodePeriod} /> )} <CharacteristicFacet - fetching={this.props.loadingFacets.characteristics === true} + fetching={loadingFacets.characteristics === true} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={openFacets.characteristics?.[IssueCharacteristicFitFor.Production]} @@ -169,7 +188,7 @@ export class Sidebar extends React.PureComponent<Props> { characteristics={query.characteristics as IssueCharacteristic[]} /> <SeverityFacet - fetching={this.props.loadingFacets.severities === true} + fetching={loadingFacets.severities === true} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.severities} @@ -177,47 +196,53 @@ export class Sidebar extends React.PureComponent<Props> { stats={facets.severities} /> <TypeFacet - fetching={this.props.loadingFacets.types === true} + fetching={loadingFacets.types === true} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.types} stats={facets.types} types={query.types} + forceShow={showAllFilters} /> + <ScopeFacet - fetching={this.props.loadingFacets.scopes === true} + fetching={loadingFacets.scopes === true} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.scopes} stats={facets.scopes} scopes={query.scopes} + forceShow={showAllFilters} /> + <ResolutionFacet - fetching={this.props.loadingFacets.resolutions === true} + fetching={loadingFacets.resolutions === true} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.resolutions} resolutions={query.resolutions} resolved={query.resolved} stats={facets.resolutions} + forceShow={showAllFilters} /> <StatusFacet - fetching={this.props.loadingFacets.statuses === true} + fetching={loadingFacets.statuses === true} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.statuses} stats={facets.statuses} statuses={query.statuses} + forceShow={showAllFilters} /> <StandardFacet cwe={query.cwe} cweOpen={!!openFacets.cwe} cweStats={facets.cwe} - fetchingCwe={this.props.loadingFacets.cwe === true} - fetchingOwaspTop10={this.props.loadingFacets.owaspTop10 === true} - fetchingOwaspTop10-2021={this.props.loadingFacets['owaspTop10-2021'] === true} - fetchingSonarSourceSecurity={this.props.loadingFacets.sonarsourceSecurity === true} - loadSearchResultCount={this.props.loadSearchResultCount} + fetchingCwe={loadingFacets.cwe === true} + fetchingOwaspTop10={loadingFacets.owaspTop10 === true} + fetchingOwaspTop10-2021={loadingFacets['owaspTop10-2021'] === true} + fetchingSonarSourceSecurity={loadingFacets.sonarsourceSecurity === true} + loadSearchResultCount={loadSearchResultCount} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.standards} @@ -231,6 +256,7 @@ export class Sidebar extends React.PureComponent<Props> { sonarsourceSecurity={query.sonarsourceSecurity} sonarsourceSecurityOpen={!!openFacets.sonarsourceSecurity} sonarsourceSecurityStats={facets.sonarsourceSecurity} + forceShow={showAllFilters} /> <CreationDateFacet component={component} @@ -239,58 +265,63 @@ export class Sidebar extends React.PureComponent<Props> { createdAt={query.createdAt} createdBefore={query.createdBefore} createdInLast={query.createdInLast} - fetching={this.props.loadingFacets.createdAt === true} + fetching={loadingFacets.createdAt === true} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.createdAt} inNewCodePeriod={query.inNewCodePeriod} stats={facets.createdAt} + forceShow={showAllFilters} /> <LanguageFacet - fetching={this.props.loadingFacets.languages === true} - loadSearchResultCount={this.props.loadSearchResultCount} + fetching={loadingFacets.languages === true} + loadSearchResultCount={loadSearchResultCount} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.languages} query={query} - referencedLanguages={this.props.referencedLanguages} + referencedLanguages={referencedLanguages} selectedLanguages={query.languages} stats={facets.languages} + forceShow={showAllFilters} /> <RuleFacet - fetching={this.props.loadingFacets.rules === true} - loadSearchResultCount={this.props.loadSearchResultCount} + fetching={loadingFacets.rules === true} + loadSearchResultCount={loadSearchResultCount} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.rules} query={query} - referencedRules={this.props.referencedRules} + referencedRules={referencedRules} stats={facets.rules} + forceShow={showAllFilters} /> <TagFacet component={component} branch={branch} - fetching={this.props.loadingFacets.tags === true} - loadSearchResultCount={this.props.loadSearchResultCount} + fetching={loadingFacets.tags === true} + loadSearchResultCount={loadSearchResultCount} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.tags} query={query} stats={facets.tags} tags={query.tags} + forceShow={showAllFilters} /> {displayProjectsFacet && ( <ProjectFacet component={component} - fetching={this.props.loadingFacets.projects === true} - loadSearchResultCount={this.props.loadSearchResultCount} + fetching={loadingFacets.projects === true} + loadSearchResultCount={loadSearchResultCount} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.projects} projects={query.projects} query={query} - referencedComponents={this.props.referencedComponentsByKey} + referencedComponents={referencedComponentsByKey} stats={facets.projects} + forceShow={showAllFilters} /> )} {this.renderComponentFacets()} @@ -298,27 +329,29 @@ export class Sidebar extends React.PureComponent<Props> { <AssigneeFacet assigned={query.assigned} assignees={query.assignees} - fetching={this.props.loadingFacets.assignees === true} - loadSearchResultCount={this.props.loadSearchResultCount} + fetching={loadingFacets.assignees === true} + loadSearchResultCount={loadSearchResultCount} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.assignees} query={query} - referencedUsers={this.props.referencedUsers} + referencedUsers={referencedUsers} stats={facets.assignees} + forceShow={showAllFilters} /> )} {displayAuthorFacet && !disableDeveloperAggregatedInfo && ( <AuthorFacet author={query.author} component={component} - fetching={this.props.loadingFacets.author === true} - loadSearchResultCount={this.props.loadSearchResultCount} + fetching={loadingFacets.author === true} + loadSearchResultCount={loadSearchResultCount} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} open={!!openFacets.author} query={query} stats={facets.author} + forceShow={showAllFilters} /> )} </> diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx index acc5443f5a0..c5b9c80cdf0 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx @@ -64,6 +64,7 @@ interface Props { sonarsourceSecurity: string[]; sonarsourceSecurityOpen: boolean; sonarsourceSecurityStats: Dict<number> | undefined; + forceShow: boolean; } interface State { @@ -99,19 +100,13 @@ export default class StandardFacet extends React.PureComponent<Props, State> { this.mounted = true; // load standards.json only if the facet is open, or there is a selected value - if ( - this.props.open || - this.props.owaspTop10.length > 0 || - this.props['owaspTop10-2021'].length > 0 || - this.props.cwe.length > 0 || - this.props.sonarsourceSecurity.length > 0 - ) { + if (this.isFacetVisible() || this.props.open) { this.loadStandards(); } } componentDidUpdate(prevProps: Props) { - if (!prevProps.open && this.props.open) { + if (!prevProps.open && this.props.open && this.isFacetVisible()) { this.loadStandards(); } } @@ -150,17 +145,15 @@ export default class StandardFacet extends React.PureComponent<Props, State> { }; getValues = () => { + const { sonarsourceSecurity, owaspTop10, 'owaspTop10-2021': owaspTop2021, cwe } = this.props; + const { standards } = this.state; return [ - ...this.props.sonarsourceSecurity.map((item) => - renderSonarSourceSecurityCategory(this.state.standards, item, true) + ...sonarsourceSecurity.map((item) => + renderSonarSourceSecurityCategory(standards, item, true) ), - ...this.props.owaspTop10.map((item) => - renderOwaspTop10Category(this.state.standards, item, true) - ), - ...this.props['owaspTop10-2021'].map((item) => - renderOwaspTop102021Category(this.state.standards, item, true) - ), - ...this.props.cwe.map((item) => renderCWECategory(this.state.standards, item)), + ...owaspTop10.map((item) => renderOwaspTop10Category(standards, item, true)), + ...owaspTop2021.map((item) => renderOwaspTop102021Category(standards, item, true)), + ...cwe.map((item) => renderCWECategory(standards, item)), ]; }; @@ -231,6 +224,19 @@ export default class StandardFacet extends React.PureComponent<Props, State> { : Promise.resolve({}); }; + isFacetVisible = () => { + const { + forceShow, + cwe, + sonarsourceSecurity, + owaspTop10, + 'owaspTop10-2021': owaspTop2021, + } = this.props; + const values = [...cwe, ...sonarsourceSecurity, ...owaspTop10, ...owaspTop2021]; + + return !(values.length < 1 && !forceShow); + }; + renderList = ( statsProp: StatsProp, valuesProp: ValuesProp, @@ -318,8 +324,8 @@ export default class StandardFacet extends React.PureComponent<Props, State> { } renderSonarSourceSecurityList() { - const stats = this.props.sonarsourceSecurityStats; - const values = this.props.sonarsourceSecurity; + const { sonarsourceSecurityStats: stats, sonarsourceSecurity: values } = this.props; + const { standards, showFullSonarSourceList } = this.state; if (!stats) { return null; @@ -328,15 +334,15 @@ export default class StandardFacet extends React.PureComponent<Props, State> { const sortedItems = sortBy( Object.keys(stats), (key) => -stats[key], - (key) => renderSonarSourceSecurityCategory(this.state.standards, key) + (key) => renderSonarSourceSecurityCategory(standards, key) ); - const limitedList = this.state.showFullSonarSourceList + const limitedList = showFullSonarSourceList ? sortedItems : sortedItems.slice(0, INITIAL_FACET_COUNT); // make sure all selected items are displayed - const selectedBelowLimit = this.state.showFullSonarSourceList + const selectedBelowLimit = showFullSonarSourceList ? [] : sortedItems.slice(INITIAL_FACET_COUNT).filter((item) => values.includes(item)); @@ -415,6 +421,8 @@ export default class StandardFacet extends React.PureComponent<Props, State> { sonarsourceSecurity, sonarsourceSecurityOpen, } = this.props; + const { standards } = this.state; + return ( <> <FacetBox className="is-inner" property={SecurityStandard.SONARSOURCE}> @@ -424,7 +432,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> { onClick={this.handleSonarSourceSecurityHeaderClick} open={sonarsourceSecurityOpen} values={sonarsourceSecurity.map((item) => - renderSonarSourceSecurityCategory(this.state.standards, item) + renderSonarSourceSecurityCategory(standards, item) )} /> {sonarsourceSecurityOpen && ( @@ -440,9 +448,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> { name={translate('issues.facet.owaspTop10_2021')} onClick={this.handleOwaspTop102021HeaderClick} open={owaspTop102021Open} - values={owaspTop102021.map((item) => - renderOwaspTop102021Category(this.state.standards, item) - )} + values={owaspTop102021.map((item) => renderOwaspTop102021Category(standards, item))} /> {owaspTop102021Open && ( <> @@ -470,9 +476,9 @@ export default class StandardFacet extends React.PureComponent<Props, State> { className="is-inner" facetHeader={translate('issues.facet.cwe')} fetching={fetchingCwe} - getFacetItemText={(item) => renderCWECategory(this.state.standards, item)} + getFacetItemText={(item) => renderCWECategory(standards, item)} getSearchResultKey={(item) => item} - getSearchResultText={(item) => renderCWECategory(this.state.standards, item)} + getSearchResultText={(item) => renderCWECategory(standards, item)} loadSearchResultCount={this.loadCWESearchResultCount} onChange={this.props.onChange} onSearch={this.handleCWESearch} @@ -480,9 +486,9 @@ export default class StandardFacet extends React.PureComponent<Props, State> { open={cweOpen} property={SecurityStandard.CWE} query={omit(query, 'cwe')} - renderFacetItem={(item) => renderCWECategory(this.state.standards, item)} + renderFacetItem={(item) => renderCWECategory(standards, item)} renderSearchResult={(item, query) => - highlightTerm(renderCWECategory(this.state.standards, item), query) + highlightTerm(renderCWECategory(standards, item), query) } searchPlaceholder={translate('search.search_for_cwe')} stats={cweStats} @@ -495,6 +501,10 @@ export default class StandardFacet extends React.PureComponent<Props, State> { render() { const { open } = this.props; + if (!this.isFacetVisible()) { + return null; + } + return ( <FacetBox property={this.property}> <FacetHeader diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx index d9143084c86..c2130d2a157 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx @@ -36,6 +36,7 @@ interface Props { open: boolean; stats: Dict<number> | undefined; statuses: string[]; + forceShow: boolean; } const STATUSES = ['OPEN', 'CONFIRMED', 'REOPENED', 'RESOLVED', 'CLOSED']; @@ -73,7 +74,8 @@ export default class StatusFacet extends React.PureComponent<Props> { } renderItem = (status: string) => { - const active = this.props.statuses.includes(status); + const { statuses } = this.props; + const active = statuses.includes(status); const stat = this.getStat(status); return ( @@ -91,9 +93,13 @@ export default class StatusFacet extends React.PureComponent<Props> { }; render() { - const { fetching, open, statuses, stats = {} } = this.props; + const { statuses, stats = {}, forceShow, fetching, open } = this.props; const values = statuses.map((status) => translate('issue.status', status)); + if (values.length < 1 && !forceShow) { + return null; + } + return ( <FacetBox property={this.property}> <FacetHeader diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx index 3744f609b40..46a8d52955a 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx @@ -40,6 +40,7 @@ interface Props { query: Query; stats: Dict<number> | undefined; tags: string[]; + forceShow: boolean; } const SEARCH_SIZE = 100; @@ -82,10 +83,16 @@ export default class TagFacet extends React.PureComponent<Props> { ); render() { + const { forceShow, tags, fetching, stats, open, query } = this.props; + + if (tags.length < 1 && !forceShow) { + return null; + } + return ( <ListStyleFacet<string> facetHeader={translate('issues.facet.tags')} - fetching={this.props.fetching} + fetching={fetching} getFacetItemText={this.getTagName} getSearchResultKey={(tag) => tag} getSearchResultText={(tag) => tag} @@ -93,14 +100,14 @@ export default class TagFacet extends React.PureComponent<Props> { onChange={this.props.onChange} onSearch={this.handleSearch} onToggle={this.props.onToggle} - open={this.props.open} + open={open} property="tags" - query={omit(this.props.query, 'tags')} + query={omit(query, 'tags')} renderFacetItem={this.renderTag} renderSearchResult={this.renderSearchResult} searchPlaceholder={translate('search.search_for_tags')} - stats={this.props.stats} - values={this.props.tags} + stats={stats} + values={tags} /> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx index 28b1a8f8668..51ea8f58f4b 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx @@ -37,6 +37,7 @@ interface Props { open: boolean; stats: Dict<number> | undefined; types: string[]; + forceShow: boolean; } export default class TypeFacet extends React.PureComponent<Props> { @@ -99,9 +100,13 @@ export default class TypeFacet extends React.PureComponent<Props> { }; render() { - const { fetching, open, types, stats = {} } = this.props; + const { types, stats = {}, forceShow, open, fetching } = this.props; const values = types.map((type) => translate('issue.type', type)); + if (values.length < 1 && !forceShow) { + return null; + } + return ( <FacetBox property={this.property}> <FacetHeader diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx index a02ea0f3be6..b1d4d1ca4dd 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx @@ -24,6 +24,7 @@ import { mockQuery } from '../../../../helpers/mocks/issues'; import { mockAppState } from '../../../../helpers/testMocks'; import { renderComponent } from '../../../../helpers/testReactTestingUtils'; import { ComponentQualifier } from '../../../../types/component'; +import { IssueSeverity } from '../../../../types/issues'; import { GlobalSettingKeys } from '../../../../types/settings'; import { Sidebar } from '../Sidebar'; @@ -93,6 +94,55 @@ it('should render correct facets for SubPortfolio', () => { ]); }); +it('should render only main visible facets: Characteristics & Severity', () => { + renderSidebar({ + component: mockComponent(), + showAllFilters: false, + query: mockQuery({ assigned: true }), + }); + + expect(screen.getAllByRole('button').map((button) => button.textContent)).toStrictEqual([ + 'issues.facet.characteristics.PRODUCTION', + 'issues.facet.characteristics.DEVELOPMENT', + 'issues.facet.severities', + ]); +}); + +it('should render secondary facets with filters applied eventhough "Show more filters" button isn`t toggled', () => { + renderSidebar({ + component: mockComponent(), + showAllFilters: false, + query: mockQuery({ + assigned: false, + tags: ['tag'], + rules: ['rule'], + directories: ['directory'], + cwe: ['security'], + languages: ['java'], + severities: [IssueSeverity.Blocker], + }), + }); + + expect(screen.getAllByRole('button').map((button) => button.textContent)).toStrictEqual([ + 'issues.facet.characteristics.PRODUCTION', + 'issues.facet.characteristics.DEVELOPMENT', + 'issues.facet.severities', + 'clear', + 'issues.facet.standards', + 'clear', + 'issues.facet.languages', + 'clear', + 'issues.facet.rules', + 'clear', + 'issues.facet.tags', + 'clear', + 'issues.facet.directories', + 'clear', + 'issues.facet.assignees', + 'clear', + ]); +}); + it.each([ ['week', '1w'], ['month', '1m'], @@ -129,6 +179,7 @@ function renderSidebar(props: Partial<Sidebar['props']> = {}) { referencedLanguages={{}} referencedRules={{}} referencedUsers={{}} + showAllFilters={true} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/issues/test-utils.tsx b/server/sonar-web/src/main/js/apps/issues/test-utils.tsx index 24654152bb9..d52b7ac367c 100644 --- a/server/sonar-web/src/main/js/apps/issues/test-utils.tsx +++ b/server/sonar-web/src/main/js/apps/issues/test-utils.tsx @@ -89,6 +89,8 @@ export const ui = { dateInputYearSelect: byRole('combobox', { name: 'Year:' }), clearAllFilters: byRole('button', { name: 'clear_all_filters' }), + showFiltersButton: (showMore = true) => + byRole('button', { name: `issues.show_${showMore ? 'more' : 'less'}_filters` }), ruleFacetList: byRole('list', { name: 'rules' }), languageFacetList: byRole('list', { name: 'languages' }), |