]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19069 Add Show more filters button
authorstanislavh <stanislav.honcharov@sonarsource.com>
Fri, 21 Apr 2023 12:56:17 +0000 (14:56 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 25 Apr 2023 20:03:00 +0000 (20:03 +0000)
24 files changed:
server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/CharacteristicFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/FiltersVisibilityButton.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/ScopeFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx
server/sonar-web/src/main/js/apps/issues/test-utils.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index a9b52ce93e83006507c6c3385f257bd6843d5762..1bb599037e1ea938ab5d8ff776ccc8c0deb55398 100644 (file)
@@ -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}
index ec82fc3872b597182fa2b058319922338422c1ab..c27efaa2bf5b19546315896e5733d33586017108 100644 (file)
@@ -40,6 +40,7 @@ exports[`should render correctly 1`] = `
     fetchingOwaspTop10={false}
     fetchingOwaspTop10-2021={false}
     fetchingSonarSourceSecurity={false}
+    forceShow={true}
     onChange={[MockFunction]}
     onToggle={[MockFunction]}
     open={false}
index 113bd3addf5b87b4796601c5f916bba0ff5b879d..78b6be118e14a0814994ac79abe8ef3622e362df 100644 (file)
@@ -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();
index dc3e038eb770f33a82d961a4790705b86bfe436b..4b482da7feaab7ec2402f66ab3cc384d9e9baa37 100644 (file)
@@ -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>
     );
   }
index 89d3d4f6f314a24df3952e8a1ce9e913ca4a6fbd..ff8ff15a03520e51191a8030315bc4a0f30d9c45 100644 (file)
@@ -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}
       />
     );
index 1647c689495b7290016d160eec58a5bd441a47c6..ada0eb6b2d79cfbd0f19118f68679ed13d88034c 100644 (file)
@@ -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}
       />
     );
   }
index 094f3e27c7294748da4d5a63b6780e1bfc355ff9..eb0b153027ea6dd5744cfca673e323cabc69ab46 100644 (file)
@@ -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}
index 5918ccc99dd43cf7df6c994e23175ac102c3f49f..fd37e3fb43ceb369c3c593a9be1eba190862d967 100644 (file)
@@ -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}>
index b4888017813d190c1a403f0504e8d7331118b32e..00b1ae90115ec601966b9bb92d2c9276a55f1306 100644 (file)
@@ -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}
       />
     );
   }
index e315f8e04540d6b484422d09ad6ff140b15984c5..e27b23a51047e6ddcde7e05e05ca2b6668da5512 100644 (file)
@@ -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 (file)
index 0000000..45da86c
--- /dev/null
@@ -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>
+  );
+}
index 992f4a63b5c2b409a9a9b1eb60f557051726a525..a2b34e96573b833009f220676643a3888a7f4aba 100644 (file)
@@ -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}
       />
     );
   }
index 4c738b2f4fac86f6af144ae60c913d07e08f036b..93ebce99746e1e159c3fe2fdaff7a6196a623b2e 100644 (file)
@@ -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}
       />
     );
   }
index 32578f5dc75edee1fb590afc26f05a387946ba0c..c437f10efc4c017d13b7a1ffb60f21e398a3cd38 100644 (file)
@@ -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
index 1da206faff1930b5603b4cc829e9127105f2129c..494d420b810724a7a0e98b1fa1106bb8c574900c 100644 (file)
@@ -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>
index 329c046b92d66194908831a1bedee8059b9f5295..73320c777627a279d2f1cd217e8ae09ea48bd747 100644 (file)
@@ -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}>
index 8f25db0785b9742c47976f3170574beefc7e5e07..c677090a7241f344147f90e85c3ac009bb645cb0 100644 (file)
@@ -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}
           />
         )}
       </>
index acc5443f5a0eaf35b349d9317917cf3c7d2285ca..c5b9c80cdf08d05760eff442d130e84587b35d0b 100644 (file)
@@ -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
index d9143084c8623175c07280a2afe0782ff713d1a0..c2130d2a157d70f2b5b755e855f0872e15f99798 100644 (file)
@@ -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
index 3744f609b40961c67a5efbc1234b9b1103d7189f..46a8d52955a4b921ef88825c4d079e048dfdf5a1 100644 (file)
@@ -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}
       />
     );
   }
index 28b1a8f8668d278ced49628b9cb2105d8253a4ea..51ea8f58f4bc0d4b6d939e60c98830558dac4d10 100644 (file)
@@ -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
index a02ea0f3be6fef937a662a531cf1b333d7977ade..b1d4d1ca4ddc835011dd67ef99fafe409351993a 100644 (file)
@@ -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}
     />
   );
index 24654152bb979e2805869b0514c52e8441238e40..d52b7ac367cffd7053c860e56f986370ee29987f 100644 (file)
@@ -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' }),
index 4b7548022979d72bc739144a1c014679d94842da..6da9c0135364973313f657a9d7ef6c31188569ec 100644 (file)
@@ -989,6 +989,8 @@ issues.no_issues=No Issues. Hooray!
 issues.x_more_locations=+ {0} more locations
 issues.not_all_issue_show=Not all issues are included
 issues.not_all_issue_show_why=You do not have access to all projects in this portfolio
+issues.show_more_filters=Show more filters
+issues.show_less_filters=Show less filters
 
 #------------------------------------------------------------------------------
 #