From: vikvorona Date: Thu, 20 Apr 2023 08:14:09 +0000 (+0200) Subject: SONAR-18891 Filter out security hotspots from rules facet on issues page (#8061) X-Git-Tag: 10.1.0.73491~429 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=bd4ff1f7029ff40248bef92ddce6e02a099d8436;p=sonarqube.git SONAR-18891 Filter out security hotspots from rules facet on issues page (#8061) --- diff --git a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts index 22af6d71357..ec11f62ad5b 100644 --- a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts @@ -27,6 +27,7 @@ import { mockLoggedInUser, mockPaging, mockRawIssue, + mockRule, mockRuleDetails, } from '../../helpers/testMocks'; import { @@ -43,10 +44,12 @@ import { RawIssuesResponse, ReferencedComponent, } from '../../types/issues'; +import { SearchRulesQuery } from '../../types/rules'; import { Standards } from '../../types/security'; import { Dict, FlowType, + Rule, RuleActivation, RuleDetails, SnippetsByComponent, @@ -67,7 +70,7 @@ import { setIssueTransition, setIssueType, } from '../issues'; -import { getRuleDetails } from '../rules'; +import { getRuleDetails, searchRules } from '../rules'; import { dismissNotice, getCurrentUser, searchUsers } from '../users'; function mockReferenceComponent(override?: Partial) { @@ -103,6 +106,7 @@ export default class IssuesServiceMock { currentUser: LoggedInUser; standards?: Standards; defaultList: IssueData[]; + rulesList: Rule[]; list: IssueData[]; constructor() { @@ -439,11 +443,41 @@ export default class IssuesServiceMock { snippets: {}, }, ]; + this.rulesList = [ + mockRule({ + key: 'simpleRuleId', + name: 'Simple rule', + lang: 'java', + langName: 'Java', + type: 'CODE_SMELL', + }), + mockRule({ + key: 'advancedRuleId', + name: 'Advanced rule', + lang: 'web', + langName: 'HTML', + type: 'VULNERABILITY', + }), + mockRule({ + key: 'cpp:S6069', + lang: 'cpp', + langName: 'C++', + name: 'Security hotspot rule', + type: 'SECURITY_HOTSPOT', + }), + mockRule({ + key: 'tsql:S131', + name: '"CASE" expressions should end with "ELSE" clauses', + lang: 'tsql', + langName: 'T-SQL', + }), + ]; this.list = cloneDeep(this.defaultList); (searchIssues as jest.Mock).mockImplementation(this.handleSearchIssues); (getRuleDetails as jest.Mock).mockImplementation(this.handleGetRuleDetails); + jest.mocked(searchRules).mockImplementation(this.handleSearchRules); (getIssueFlowSnippets as jest.Mock).mockImplementation(this.handleGetIssueFlowSnippets); (bulkChangeIssues as jest.Mock).mockImplementation(this.handleBulkChangeIssues); (getCurrentUser as jest.Mock).mockImplementation(this.handleGetCurrentUser); @@ -511,6 +545,22 @@ export default class IssuesServiceMock { return this.reply(issue.snippets); }; + handleSearchRules = (req: SearchRulesQuery) => { + const rules = this.rulesList.filter((rule) => { + const query = req.q?.toLowerCase() || ''; + const nameMatches = rule.name.toLowerCase().includes(query); + const keyMatches = rule.key.toLowerCase().includes(query); + const isTypeRight = req.types?.includes(rule.type); + return isTypeRight && (nameMatches || keyMatches); + }); + return this.reply({ + p: 1, + ps: 30, + rules, + total: rules.length, + }); + }; + handleGetRuleDetails = (parameters: { actives?: boolean; key: string; @@ -709,6 +759,7 @@ export default class IssuesServiceMock { pageSize, total: filteredList.length, }), + rules: this.rulesList, users: [ { login: 'login0' }, { login: 'login1', name: 'Login 1' }, diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx index e92c377b173..410764f83cd 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx @@ -52,22 +52,24 @@ class AvailableSinceFacet extends React.PureComponent - {this.props.open && ( + {open && ( )} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx index fb924dfbe2a..6b8fd8e1956 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx @@ -93,7 +93,15 @@ export default class Facet extends React.PureComponent { }; render() { - const { disabled, renderTextName = defaultRenderName, stats } = this.props; + const { + children, + disabled, + disabledHelper, + open, + property, + renderTextName = defaultRenderName, + stats, + } = this.props; const values = this.props.values.map(renderTextName); const items = this.props.options || @@ -107,25 +115,25 @@ export default class Facet extends React.PureComponent { return ( - {this.props.children} + {children} - {this.props.open && items !== undefined && ( - {items.map(this.renderItem)} + {open && items !== undefined && ( + {items.map(this.renderItem)} )} - {this.props.open && this.props.renderFooter !== undefined && this.props.renderFooter()} + {open && this.props.renderFooter !== undefined && this.props.renderFooter()} ); } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx index 1214b3469a1..9fbac37e959 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx @@ -153,7 +153,7 @@ export default class ProfileFacet extends React.PureComponent { }; render() { - const { languages, referencedProfiles } = this.props; + const { languages, open, referencedProfiles } = this.props; let profiles = Object.values(referencedProfiles); if (languages.length > 0) { profiles = profiles.filter((profile) => languages.includes(profile.language)); @@ -164,13 +164,15 @@ export default class ProfileFacet extends React.PureComponent { (profile) => profile.languageName ); + const property = 'profile'; + return ( - + { /> - {this.props.open && {profiles.map(this.renderItem)}} + {open && {profiles.map(this.renderItem)}} ); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx index f42d1db33ad..d38472c3173 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx @@ -146,7 +146,7 @@ export default class DomainFacet extends React.PureComponent { }; render() { - const { domain } = this.props; + const { domain, open } = this.props; const helperMessageKey = `component_measures.domain_facets.${domain.name}.help`; const helper = hasMessage(helperMessageKey) ? translate(helperMessageKey) : undefined; return ( @@ -155,12 +155,12 @@ export default class DomainFacet extends React.PureComponent { helper={helper} name={getLocalizedMetricDomain(domain.name)} onClick={this.handleHeaderClick} - open={this.props.open} + open={open} values={this.getValues()} /> - {this.props.open && ( - + {open && ( + {this.renderOverviewFacet()} {this.renderItemsFacet()} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.tsx index 64f51904fc9..741f09af76d 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.tsx @@ -33,7 +33,7 @@ export default function ProjectOverviewFacet({ value, selected, onChange }: Prop const facetName = translate('component_measures.overview', value, 'facet'); return ( - + - + - + - + - + { // Rule await user.click(ui.ruleFacet.get()); await user.click(screen.getByRole('checkbox', { name: 'other' })); + expect(screen.getByRole('checkbox', { name: '(HTML) Advanced rule' })).toBeInTheDocument(); // Name should apply to the rule // Tag await user.click(ui.tagFacet.get()); @@ -463,6 +464,41 @@ describe('issues app', () => { expect(ui.issueItem2.get()).toBeInTheDocument(); expect(ui.issueItem3.get()).toBeInTheDocument(); }); + + it('should search for rules with proper types', async () => { + const user = userEvent.setup(); + + renderIssueApp(); + + await user.click(await ui.ruleFacet.find()); + await user.type(ui.ruleFacetSearch.get(), 'rule'); + expect(within(ui.ruleFacetList.get()).getAllByRole('checkbox')).toHaveLength(2); + expect( + within(ui.ruleFacetList.get()).getByRole('checkbox', { + name: /Advanced rule/, + }) + ).toBeInTheDocument(); + expect( + within(ui.ruleFacetList.get()).getByRole('checkbox', { + name: /Simple rule/, + }) + ).toBeInTheDocument(); + + 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'); + + expect( + within(ui.ruleFacetList.get()).getByRole('checkbox', { + name: /Advanced rule/, + }) + ).toBeInTheDocument(); + expect( + within(ui.ruleFacetList.get()).queryByRole('checkbox', { + name: /Simple rule/, + }) + ).not.toBeInTheDocument(); + }); }); }); 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 73ea507fc33..5918ccc99dd 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 @@ -287,18 +287,20 @@ export class CreationDateFacet extends React.PureComponent - {this.props.open && this.renderInner()} + {open && this.renderInner()} ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/PeriodFilter.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/PeriodFilter.tsx index aa581aff0b1..7bc7718e116 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/PeriodFilter.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/PeriodFilter.tsx @@ -55,7 +55,7 @@ export default function PeriodFilter(props: PeriodFilterProps) { return ( - + { }; render() { - const { resolutions, stats = {} } = this.props; + const { fetching, open, resolutions, stats = {} } = this.props; const values = resolutions.map((resolution) => this.getFacetItemName(resolution)); return ( - {this.props.open && ( + {open && ( <> - {RESOLUTIONS.map(this.renderItem)} + + {RESOLUTIONS.map(this.renderItem)} + ) => Promise; onChange: (changes: Partial) => void; onToggle: (property: string) => void; open: boolean; query: Query; referencedRules: Dict; - rules: string[]; stats: Dict | undefined; } export default class RuleFacet extends React.PureComponent { handleSearch = (query: string, page = 1) => { - const { languages } = this.props; + const { languages, types } = this.props.query; return searchRules({ f: 'name,langName', languages: languages.length ? languages.join() : undefined, q: query, p: page, ps: 30, + types: types.length + ? types.join() + : ISSUE_TYPES.filter((type) => type !== IssueType.SecurityHotspot).join(), s: 'name', include_external: true, }).then((response) => ({ @@ -76,10 +78,12 @@ export default class RuleFacet extends React.PureComponent { }; render() { + const { fetching, open, query, stats } = this.props; + return ( facetHeader={translate('issues.facet.rules')} - fetching={this.props.fetching} + fetching={fetching} getFacetItemText={this.getRuleName} getSearchResultKey={(rule) => rule.key} getSearchResultText={(rule) => rule.name} @@ -87,14 +91,14 @@ export default class RuleFacet extends React.PureComponent { onChange={this.props.onChange} onSearch={this.handleSearch} onToggle={this.props.onToggle} - open={this.props.open} + open={open} property="rules" - query={omit(this.props.query, 'rules')} + query={omit(query, 'rules')} renderFacetItem={this.getRuleName} renderSearchResult={this.renderSearchResult} searchPlaceholder={translate('search.search_for_rules')} - stats={this.props.stats} - values={this.props.rules} + stats={stats} + values={query.rules} /> ); } 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 1d5af535e13..329c046b92d 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 @@ -43,8 +43,10 @@ export default function ScopeFacet(props: ScopeFacetProps) { const { fetching, open, scopes = [], stats = {} } = props; const values = scopes.map((scope) => translate('issue.scope', scope)); + const property = 'scopes'; + return ( - + - + {SOURCE_SCOPES.map(({ scope, qualifier }) => { const active = scopes.includes(scope); const stat = stats[scope]; diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx index 9ac899bcec8..f587a50b359 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx @@ -27,7 +27,7 @@ import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHi import SeverityHelper from '../../../components/shared/SeverityHelper'; import { translate } from '../../../helpers/l10n'; import { Dict } from '../../../types/types'; -import { formatFacetStat, Query } from '../utils'; +import { Query, formatFacetStat } from '../utils'; interface Props { fetching: boolean; @@ -93,23 +93,23 @@ export default class SeverityFacet extends React.PureComponent { }; render() { - const { severities, stats = {} } = this.props; + const { fetching, open, severities, stats = {} } = this.props; const values = severities.map((severity) => translate('severity', severity)); return ( - {this.props.open && ( + {open && ( <> - {SEVERITIES.map(this.renderItem)} + {SEVERITIES.map(this.renderItem)} )} 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 a0be8e5c952..90460b7b8aa 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 @@ -238,14 +238,12 @@ export class Sidebar extends React.PureComponent { /> { return null; } const categories = sortBy(Object.keys(stats), (key) => -stats[key]); - return this.renderFacetItemsList(stats, values, categories, renderName, renderName, onClick); + return this.renderFacetItemsList( + stats, + values, + categories, + valuesProp, + renderName, + renderName, + onClick + ); }; // eslint-disable-next-line max-params @@ -251,6 +259,7 @@ export default class StandardFacet extends React.PureComponent { stats: any, values: string[], categories: string[], + listLabel: ValuesProp, renderName: (standards: Standards, category: string) => React.ReactNode, renderTooltip: (standards: Standards, category: string) => string, onClick: (x: string, multiple?: boolean) => void @@ -268,7 +277,7 @@ export default class StandardFacet extends React.PureComponent { }; return ( - + {categories.map((category) => ( { const allItemShown = limitedList.length + selectedBelowLimit.length === sortedItems.length; return ( <> - + {limitedList.map((item) => ( { {selectedBelowLimit.length > 0 && ( <> {!allItemShown &&
⋯
} - + {selectedBelowLimit.map((item) => ( { } renderSubFacets() { + const { + cwe, + cweOpen, + cweStats, + fetchingCwe, + fetchingOwaspTop10, + 'fetchingOwaspTop10-2021': fetchingOwaspTop102021, + fetchingSonarSourceSecurity, + owaspTop10, + owaspTop10Open, + 'owaspTop10-2021Open': owaspTop102021Open, + 'owaspTop10-2021': owaspTop102021, + query, + sonarsourceSecurity, + sonarsourceSecurityOpen, + } = this.props; return ( <> + open={sonarsourceSecurityOpen} + values={sonarsourceSecurity.map((item) => renderSonarSourceSecurityCategory(this.state.standards, item) )} /> - {this.props.sonarsourceSecurityOpen && ( + {sonarsourceSecurityOpen && ( <> {this.renderSonarSourceSecurityList()} {this.renderSonarSourceSecurityHint()} @@ -411,15 +436,15 @@ export default class StandardFacet extends React.PureComponent { + open={owaspTop102021Open} + values={owaspTop102021.map((item) => renderOwaspTop102021Category(this.state.standards, item) )} /> - {this.props['owaspTop10-2021Open'] && ( + {owaspTop102021Open && ( <> {this.renderOwaspTop102021List()} {this.renderOwaspTop102021Hint()} @@ -428,15 +453,13 @@ export default class StandardFacet extends React.PureComponent { - renderOwaspTop10Category(this.state.standards, item) - )} + open={owaspTop10Open} + values={owaspTop10.map((item) => renderOwaspTop10Category(this.state.standards, item))} /> - {this.props.owaspTop10Open && ( + {owaspTop10Open && ( <> {this.renderOwaspTop10List()} {this.renderOwaspTop10Hint()} @@ -446,7 +469,7 @@ export default class StandardFacet extends React.PureComponent { className="is-inner" facetHeader={translate('issues.facet.cwe')} - fetching={this.props.fetchingCwe} + fetching={fetchingCwe} getFacetItemText={(item) => renderCWECategory(this.state.standards, item)} getSearchResultKey={(item) => item} getSearchResultText={(item) => renderCWECategory(this.state.standards, item)} @@ -454,33 +477,35 @@ export default class StandardFacet extends React.PureComponent { onChange={this.props.onChange} onSearch={this.handleCWESearch} onToggle={this.props.onToggle} - open={this.props.cweOpen} + open={cweOpen} property={SecurityStandard.CWE} - query={omit(this.props.query, 'cwe')} + query={omit(query, 'cwe')} renderFacetItem={(item) => renderCWECategory(this.state.standards, item)} renderSearchResult={(item, query) => highlightTerm(renderCWECategory(this.state.standards, item), query) } searchPlaceholder={translate('search.search_for_cwe')} - stats={this.props.cweStats} - values={this.props.cwe} + stats={cweStats} + values={cwe} /> ); } render() { + const { open } = this.props; + return ( - {this.props.open && this.renderSubFacets()} + {open && this.renderSubFacets()} ); } 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 acd60cc09c4..d9143084c86 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 @@ -27,7 +27,7 @@ import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHi import StatusHelper from '../../../components/shared/StatusHelper'; import { translate } from '../../../helpers/l10n'; import { Dict } from '../../../types/types'; -import { formatFacetStat, Query } from '../utils'; +import { Query, formatFacetStat } from '../utils'; interface Props { fetching: boolean; @@ -91,23 +91,23 @@ export default class StatusFacet extends React.PureComponent { }; render() { - const { statuses, stats = {} } = this.props; + const { fetching, open, statuses, stats = {} } = this.props; const values = statuses.map((status) => translate('issue.status', status)); return ( - {this.props.open && ( + {open && ( <> - {STATUSES.map(this.renderItem)} + {STATUSES.map(this.renderItem)} )} 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 f9f130e3c5c..28b1a8f8668 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 @@ -28,7 +28,7 @@ import IssueTypeIcon from '../../../components/icons/IssueTypeIcon'; import { ISSUE_TYPES } from '../../../helpers/constants'; import { translate } from '../../../helpers/l10n'; import { Dict } from '../../../types/types'; -import { formatFacetStat, Query } from '../utils'; +import { Query, formatFacetStat } from '../utils'; interface Props { fetching: boolean; @@ -99,23 +99,23 @@ export default class TypeFacet extends React.PureComponent { }; render() { - const { types, stats = {} } = this.props; + const { fetching, open, types, stats = {} } = this.props; const values = types.map((type) => translate('issue.type', type)); return ( - {this.props.open && ( + {open && ( <> - + {ISSUE_TYPES.filter((t) => t !== 'SECURITY_HOTSPOT').map(this.renderItem)} 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 cae0215f918..5d92bcddc32 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 @@ -83,6 +83,9 @@ export const ui = { dateInputYearSelect: byRole('combobox', { name: 'Year:' }), clearAllFilters: byRole('button', { name: 'clear_all_filters' }), + + ruleFacetList: byRole('list', { name: 'rules' }), + ruleFacetSearch: byRole('searchbox', { name: 'search.search_for_rules' }), }; export async function waitOnDataLoaded() { diff --git a/server/sonar-web/src/main/js/components/facet/FacetBox.tsx b/server/sonar-web/src/main/js/components/facet/FacetBox.tsx index a4b465f4ace..a750f1ff3d7 100644 --- a/server/sonar-web/src/main/js/components/facet/FacetBox.tsx +++ b/server/sonar-web/src/main/js/components/facet/FacetBox.tsx @@ -27,12 +27,11 @@ export interface FacetBoxProps { } export default function FacetBox(props: FacetBoxProps) { + const { children, className, property } = props; + return ( -
- {props.children} +
+ {children}
); } diff --git a/server/sonar-web/src/main/js/components/facet/FacetItemsList.tsx b/server/sonar-web/src/main/js/components/facet/FacetItemsList.tsx index 96f28953a00..e86348de142 100644 --- a/server/sonar-web/src/main/js/components/facet/FacetItemsList.tsx +++ b/server/sonar-web/src/main/js/components/facet/FacetItemsList.tsx @@ -21,17 +21,12 @@ import * as React from 'react'; export interface FacetItemsListProps { children?: React.ReactNode; - title?: string; + label: string; } -export default function FacetItemsList({ children, title }: FacetItemsListProps) { +export default function FacetItemsList({ children, label }: FacetItemsListProps) { return ( -
- {title && ( -
- {title} -
- )} +
{children}
); diff --git a/server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx b/server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx index a87da01e050..b49859ee10f 100644 --- a/server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx +++ b/server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx @@ -240,7 +240,15 @@ export default class ListStyleFacet extends React.Component, State extends React.Component, State this.props.values.includes(item)); + : sortedItems.slice(maxInitialItems).filter((item) => values.includes(item)); - const mightHaveMoreResults = sortedItems.length >= this.props.maxItems; + const mightHaveMoreResults = sortedItems.length >= maxItems; return ( <> - + {limitedList.map((item) => ( extends React.Component, State 0 && ( <>
⋯
- + {selectedBelowLimit.map((item) => ( extends React.Component, State extends React.Component, State - + {searchResults.map((result) => this.renderSearchResult(result))} {searchMaxResults && ( @@ -386,31 +392,41 @@ export default class ListStyleFacet extends React.Component, State this.props.getFacetItemText(item)); + const values = propsValues.map((item) => this.props.getFacetItemText(item)); const loadingResults = query !== '' && searching && (searchResults === undefined || searchResults.length === 0); const showList = !query || loadingResults; return ( - {this.props.open && !disabled && ( + {open && !disabled && ( <> {this.renderSearch()} {showList ? this.renderList() : this.renderSearchResults()} diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/Facet-it.tsx b/server/sonar-web/src/main/js/components/facet/__tests__/Facet-it.tsx index fe4030f2433..dd13d2969b5 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/Facet-it.tsx +++ b/server/sonar-web/src/main/js/components/facet/__tests__/Facet-it.tsx @@ -75,11 +75,6 @@ it('should correctly render a disabled header', () => { expect(screen.queryByRole('checkbox', { name: 'foo' })).not.toBeInTheDocument(); }); -it('should correctly render a facet item list with title', () => { - renderFacet(undefined, { open: true }, { title: 'My list title' }); - expect(screen.getByText('My list title')).toBeInTheDocument(); -}); - function renderFacet( facetBoxProps: Partial = {}, facetHeaderProps: Partial = {}, @@ -90,8 +85,10 @@ function renderFacet( const [open, setOpen] = React.useState(facetHeaderProps.open ?? false); const [values, setValues] = React.useState(facetHeaderProps.values ?? undefined); + const property = 'foo'; + return ( - + setOpen(!open)} @@ -100,7 +97,7 @@ function renderFacet( /> {open && ( - + - + ⋯
- + - + - + - + - +