소스 검색

SONAR-6961 load counts for search results (#619)

tags/7.5
Stas Vilchik 5 년 전
부모
커밋
6a2c038752
25개의 변경된 파일265개의 추가작업 그리고 631개의 파일을 삭제
  1. 5
    2
      server/sonar-web/src/main/js/apps/issues/components/App.tsx
  2. 7
    6
      server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx
  3. 4
    4
      server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx
  4. 6
    4
      server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx
  5. 4
    4
      server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx
  6. 6
    4
      server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx
  7. 0
    69
      server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.tsx
  8. 6
    4
      server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx
  9. 6
    4
      server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx
  10. 4
    4
      server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx
  11. 47
    46
      server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
  12. 4
    4
      server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx
  13. 4
    4
      server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx
  14. 0
    1
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx
  15. 36
    23
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.tsx
  16. 1
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap
  17. 1
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap
  18. 1
    1
      server/sonar-web/src/main/js/apps/issues/utils.ts
  19. 0
    84
      server/sonar-web/src/main/js/components/controls/MouseOverHandler.tsx
  20. 0
    88
      server/sonar-web/src/main/js/components/controls/__tests__/MouseOverHandler-test.tsx
  21. 0
    10
      server/sonar-web/src/main/js/components/facet/FacetItem.tsx
  22. 0
    39
      server/sonar-web/src/main/js/components/facet/ListStyleFacet.css
  23. 60
    122
      server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx
  24. 8
    29
      server/sonar-web/src/main/js/components/facet/__tests__/ListStyleFacet-test.tsx
  25. 55
    75
      server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap

+ 5
- 2
server/sonar-web/src/main/js/apps/issues/components/App.tsx 파일 보기

@@ -661,7 +661,7 @@ export default class App extends React.PureComponent<Props, State> {
});
};

loadSearchResultCount = (changes: Partial<Query>) => {
loadSearchResultCount = (property: string, changes: Partial<Query>) => {
const { component } = this.props;
const { myIssues, query } = this.state;

@@ -672,6 +672,7 @@ export default class App extends React.PureComponent<Props, State> {
const parameters = {
...getBranchLikeQuery(this.props.branchLike),
componentKeys: component && component.key,
facets: mapFacet(property),
s: 'FILE_LINE',
...serializeQuery({ ...query, ...changes }),
ps: 1,
@@ -682,7 +683,9 @@ export default class App extends React.PureComponent<Props, State> {
Object.assign(parameters, { assignees: '__me__' });
}

return this.props.fetchIssues(parameters, false).then(reponse => reponse.paging.total);
return this.props
.fetchIssues(parameters, false)
.then(({ facets }) => parseFacets(facets)[property]);
};

closeFacet = (property: string) => {

+ 7
- 6
server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx 파일 보기

@@ -19,8 +19,7 @@
*/
import * as React from 'react';
import { omit, sortBy, without } from 'lodash';
import { searchAssignees, Query, ReferencedUser, SearchedAssignee } from '../utils';
import { Component } from '../../../app/types';
import { searchAssignees, Query, ReferencedUser, SearchedAssignee, Facet } from '../utils';
import Avatar from '../../../components/ui/Avatar';
import { translate } from '../../../helpers/l10n';
import { highlightTerm } from '../../../helpers/search';
@@ -29,9 +28,8 @@ import ListStyleFacet from '../../../components/facet/ListStyleFacet';
export interface Props {
assigned: boolean;
assignees: string[];
component: Component | undefined;
fetching: boolean;
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>;
loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
onChange: (changes: Partial<Query>) => void;
onToggle: (property: string) => void;
open: boolean;
@@ -77,8 +75,11 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
}
};

loadSearchResultCount = (assignee: SearchedAssignee) => {
return this.props.loadSearchResultCount({ assigned: undefined, assignees: [assignee.login] });
loadSearchResultCount = (assignees: SearchedAssignee[]) => {
return this.props.loadSearchResultCount('assignees', {
assigned: undefined,
assignees: assignees.map(assignee => assignee.login)
});
};

getSortedItems = () => {

+ 4
- 4
server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx 파일 보기

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { omit } from 'lodash';
import { Query } from '../utils';
import { Query, Facet } from '../utils';
import { translate } from '../../../helpers/l10n';
import ListStyleFacet from '../../../components/facet/ListStyleFacet';
import { searchIssueAuthors } from '../../../api/issues';
@@ -29,7 +29,7 @@ import { Component } from '../../../app/types';
interface Props {
component: Component | undefined;
fetching: boolean;
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>;
loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
onChange: (changes: Partial<Query>) => void;
onToggle: (property: string) => void;
open: boolean;
@@ -58,8 +58,8 @@ export default class AuthorFacet extends React.PureComponent<Props> {
}).then(authors => ({ maxResults: authors.length === SEARCH_SIZE, results: authors }));
};

loadSearchResultCount = (author: string) => {
return this.props.loadSearchResultCount({ authors: [author] });
loadSearchResultCount = (authors: string[]) => {
return this.props.loadSearchResultCount('authors', { authors });
};

renderSearchResult = (author: string, term: string) => {

+ 6
- 4
server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx 파일 보기

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { omit } from 'lodash';
import { Query } from '../utils';
import { Query, Facet } from '../utils';
import QualifierIcon from '../../../components/icons-components/QualifierIcon';
import { translate } from '../../../helpers/l10n';
import ListStyleFacet from '../../../components/facet/ListStyleFacet';
@@ -31,7 +31,7 @@ interface Props {
componentKey: string;
fetching: boolean;
directories: string[];
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>;
loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
onChange: (changes: Partial<Query>) => void;
onToggle: (property: string) => void;
open: boolean;
@@ -62,8 +62,10 @@ export default class DirectoryFacet extends React.PureComponent<Props> {
}).then(({ components, paging }) => ({ paging, results: components }));
};

loadSearchResultCount = (directory: TreeComponent) => {
return this.props.loadSearchResultCount({ directories: [directory.name] });
loadSearchResultCount = (directories: TreeComponent[]) => {
return this.props.loadSearchResultCount('directories', {
directories: directories.map(directory => directory.name)
});
};

renderDirectory = (directory: React.ReactNode) => (

+ 4
- 4
server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx 파일 보기

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { omit } from 'lodash';
import { Query, ReferencedComponent } from '../utils';
import { Query, ReferencedComponent, Facet } from '../utils';
import QualifierIcon from '../../../components/icons-components/QualifierIcon';
import { translate } from '../../../helpers/l10n';
import { collapsePath } from '../../../helpers/path';
@@ -31,7 +31,7 @@ interface Props {
componentKey: string;
fetching: boolean;
files: string[];
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>;
loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
onChange: (changes: Partial<Query>) => void;
onToggle: (property: string) => void;
open: boolean;
@@ -69,8 +69,8 @@ export default class FileFacet extends React.PureComponent<Props> {
}).then(({ components, paging }) => ({ paging, results: components }));
};

loadSearchResultCount = (file: TreeComponent) => {
return this.props.loadSearchResultCount({ files: [file.id] });
loadSearchResultCount = (files: TreeComponent[]) => {
return this.props.loadSearchResultCount('files', { files: files.map(file => file.id) });
};

renderFile = (file: React.ReactNode) => (

+ 6
- 4
server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx 파일 보기

@@ -21,7 +21,7 @@ import * as React from 'react';
import { uniqBy, omit } from 'lodash';
import { connect } from 'react-redux';
import ListStyleFacet from '../../../components/facet/ListStyleFacet';
import { Query, ReferencedLanguage } from '../utils';
import { Query, ReferencedLanguage, Facet } from '../utils';
import { getLanguages } from '../../../store/rootReducer';
import { translate } from '../../../helpers/l10n';
import { highlightTerm } from '../../../helpers/search';
@@ -35,7 +35,7 @@ interface Props {
fetching: boolean;
installedLanguages: InstalledLanguage[];
languages: string[];
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>;
loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
onChange: (changes: Partial<Query>) => void;
onToggle: (property: string) => void;
open: boolean;
@@ -71,8 +71,10 @@ class LanguageFacet extends React.PureComponent<Props> {
);
};

loadSearchResultCount = (language: InstalledLanguage) => {
return this.props.loadSearchResultCount({ languages: [language.key] });
loadSearchResultCount = (languages: InstalledLanguage[]) => {
return this.props.loadSearchResultCount('languages', {
languages: languages.map(language => language.key)
});
};

renderSearchResult = ({ name }: InstalledLanguage, term: string) => {

+ 0
- 69
server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.tsx 파일 보기

@@ -1,69 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { connect } from 'react-redux';
import { differenceWith } from 'lodash';
import Select from '../../../components/controls/Select';
import { translate } from '../../../helpers/l10n';
import { getLanguages } from '../../../store/rootReducer';

interface Props {
languages: Array<{ key: string; name: string }>;
onSelect: (value: string) => void;
selected: string[];
}

class LanguageFacetFooter extends React.PureComponent<Props> {
handleChange = (option: { value: string }) => {
this.props.onSelect(option.value);
};

render() {
const options = differenceWith(
this.props.languages,
this.props.selected,
(language, key) => language.key === key
).map(language => ({ label: language.name, value: language.key }));

if (options.length === 0) {
return null;
}

return (
<div className="search-navigator-facet-footer">
<Select
className="input-super-large"
clearable={false}
noResultsText={translate('select2.noMatches')}
onChange={this.handleChange}
options={options}
placeholder={translate('search.search_for_languages')}
searchable={true}
/>
</div>
);
}
}

const mapStateToProps = (state: any) => ({
languages: Object.values(getLanguages(state))
});

export default connect(mapStateToProps)(LanguageFacetFooter);

+ 6
- 4
server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx 파일 보기

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { omit } from 'lodash';
import { Query, ReferencedComponent } from '../utils';
import { Query, ReferencedComponent, Facet } from '../utils';
import QualifierIcon from '../../../components/icons-components/QualifierIcon';
import { translate } from '../../../helpers/l10n';
import ListStyleFacet from '../../../components/facet/ListStyleFacet';
@@ -29,7 +29,7 @@ import { highlightTerm } from '../../../helpers/search';
interface Props {
componentKey: string;
fetching: boolean;
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>;
loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
modules: string[];
onChange: (changes: Partial<Query>) => void;
onToggle: (property: string) => void;
@@ -63,8 +63,10 @@ export default class ModuleFacet extends React.PureComponent<Props> {
}).then(({ components, paging }) => ({ paging, results: components }));
};

loadSearchResultCount = (module: TreeComponent) => {
return this.props.loadSearchResultCount({ files: [module.id] });
loadSearchResultCount = (modules: TreeComponent[]) => {
return this.props.loadSearchResultCount('modules', {
modules: modules.map(module => module.id)
});
};

renderModule = (module: React.ReactNode) => (

+ 6
- 4
server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx 파일 보기

@@ -20,7 +20,7 @@
import * as React from 'react';
import { omit } from 'lodash';
import ListStyleFacet from '../../../components/facet/ListStyleFacet';
import { Query, ReferencedComponent } from '../utils';
import { Query, ReferencedComponent, Facet } from '../utils';
import { searchProjects, getTree } from '../../../api/components';
import { Component, Paging } from '../../../app/types';
import Organization from '../../../components/shared/Organization';
@@ -30,7 +30,7 @@ import { highlightTerm } from '../../../helpers/search';

interface Props {
component: Component | undefined;
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>;
loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
fetching: boolean;
onChange: (changes: Partial<Query>) => void;
onToggle: (property: string) => void;
@@ -93,8 +93,10 @@ export default class ProjectFacet extends React.PureComponent<Props> {
return referencedComponents[project] ? referencedComponents[project].name : project;
};

loadSearchResultCount = (project: SearchedProject) => {
return this.props.loadSearchResultCount({ projects: [project.id] });
loadSearchResultCount = (projects: SearchedProject[]) => {
return this.props.loadSearchResultCount('projects', {
projects: projects.map(project => project.id)
});
};

renderFacetItem = (project: string) => {

+ 4
- 4
server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx 파일 보기

@@ -20,7 +20,7 @@
import * as React from 'react';
import { omit } from 'lodash';
import ListStyleFacet from '../../../components/facet/ListStyleFacet';
import { Query, ReferencedRule } from '../utils';
import { Query, ReferencedRule, Facet } from '../utils';
import { searchRules } from '../../../api/rules';
import { Rule } from '../../../app/types';
import { translate } from '../../../helpers/l10n';
@@ -28,7 +28,7 @@ import { translate } from '../../../helpers/l10n';
interface Props {
fetching: boolean;
languages: string[];
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>;
loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
onChange: (changes: Partial<Query>) => void;
onToggle: (property: string) => void;
open: boolean;
@@ -58,8 +58,8 @@ export default class RuleFacet extends React.PureComponent<Props> {
}));
};

loadSearchResultCount = (rule: Rule) => {
return this.props.loadSearchResultCount({ rules: [rule.key] });
loadSearchResultCount = (rules: Rule[]) => {
return this.props.loadSearchResultCount('rules', { rules: rules.map(rule => rule.key) });
};

getRuleName = (ruleKey: string) => {

+ 47
- 46
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx 파일 보기

@@ -48,7 +48,7 @@ export interface Props {
component: Component | undefined;
facets: { [facet: string]: Facet };
hideAuthorFacet?: boolean;
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>;
loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
loadingFacets: { [key: string]: boolean };
myIssues: boolean;
onFacetToggle: (property: string) => void;
@@ -63,14 +63,56 @@ export interface Props {
}

export default class Sidebar extends React.PureComponent<Props> {
renderComponentFacets() {
const { component, facets, loadingFacets, openFacets, query } = this.props;
if (!component) {
return null;
}
const commonProps = {
componentKey: component.key,
loadSearchResultCount: this.props.loadSearchResultCount,
onChange: this.props.onFilterChange,
onToggle: this.props.onFacetToggle,
query
};
return (
<>
{component.qualifier !== 'DIR' && (
<ModuleFacet
fetching={loadingFacets.modules === true}
modules={query.modules}
open={!!openFacets.modules}
referencedComponents={this.props.referencedComponents}
stats={facets.modules}
{...commonProps}
/>
)}
{component.qualifier !== 'DIR' && (
<DirectoryFacet
directories={query.directories}
fetching={loadingFacets.directories === true}
open={!!openFacets.directories}
stats={facets.directories}
{...commonProps}
/>
)}
<FileFacet
fetching={loadingFacets.files === true}
files={query.files}
open={!!openFacets.files}
referencedComponents={this.props.referencedComponents}
stats={facets.files}
{...commonProps}
/>
</>
);
}

render() {
const { component, facets, hideAuthorFacet, openFacets, query } = this.props;

const displayProjectsFacet =
!component || !['TRK', 'BRC', 'DIR', 'DEV_PRJ'].includes(component.qualifier);
const displayModulesFacet = component !== undefined && component.qualifier !== 'DIR';
const displayDirectoriesFacet = component !== undefined && component.qualifier !== 'DIR';
const displayFilesFacet = component !== undefined;
const displayAuthorFacet = !hideAuthorFacet && (!component || component.qualifier !== 'DEV');

const organizationKey =
@@ -195,52 +237,11 @@ export default class Sidebar extends React.PureComponent<Props> {
stats={facets.projects}
/>
)}
{displayModulesFacet && (
<ModuleFacet
componentKey={this.props.component!.key}
fetching={this.props.loadingFacets.modules === true}
loadSearchResultCount={this.props.loadSearchResultCount}
modules={query.files}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.modules}
query={query}
referencedComponents={this.props.referencedComponents}
stats={facets.modules}
/>
)}
{displayDirectoriesFacet && (
<DirectoryFacet
componentKey={this.props.component!.key}
directories={query.directories}
fetching={this.props.loadingFacets.directories === true}
loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.directories}
query={query}
stats={facets.directories}
/>
)}
{displayFilesFacet && (
<FileFacet
componentKey={this.props.component!.key}
fetching={this.props.loadingFacets.files === true}
files={query.files}
loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.files}
query={query}
referencedComponents={this.props.referencedComponents}
stats={facets.files}
/>
)}
{this.renderComponentFacets()}
{!this.props.myIssues && (
<AssigneeFacet
assigned={query.assigned}
assignees={query.assignees}
component={component}
fetching={this.props.loadingFacets.assignees === true}
loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange}

+ 4
- 4
server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx 파일 보기

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { sortBy, without, omit } from 'lodash';
import { Query, STANDARDS, formatFacetStat } from '../utils';
import { Query, STANDARDS, formatFacetStat, Facet } from '../utils';
import FacetBox from '../../../components/facet/FacetBox';
import FacetHeader from '../../../components/facet/FacetHeader';
import { translate } from '../../../helpers/l10n';
@@ -43,7 +43,7 @@ export interface Props {
fetchingOwaspTop10: boolean;
fetchingSansTop25: boolean;
fetchingCwe: boolean;
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>;
loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
onChange: (changes: Partial<Query>) => void;
onToggle: (property: string) => void;
open: boolean;
@@ -169,8 +169,8 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
});
};

loadCWESearchResultCount = (category: string) => {
return this.props.loadSearchResultCount({ cwe: [category] });
loadCWESearchResultCount = (categories: string[]) => {
return this.props.loadSearchResultCount('cwe', { cwe: categories });
};

renderList = (

+ 4
- 4
server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx 파일 보기

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { omit } from 'lodash';
import { Query } from '../utils';
import { Query, Facet } from '../utils';
import { searchIssueTags } from '../../../api/issues';
import * as theme from '../../../app/theme';
import { Component } from '../../../app/types';
@@ -31,7 +31,7 @@ import { highlightTerm } from '../../../helpers/search';
interface Props {
component: Component | undefined;
fetching: boolean;
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>;
loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
onChange: (changes: Partial<Query>) => void;
onToggle: (property: string) => void;
open: boolean;
@@ -60,8 +60,8 @@ export default class TagFacet extends React.PureComponent<Props> {
return tag;
};

loadSearchResultCount = (tag: string) => {
return this.props.loadSearchResultCount({ tags: [tag] });
loadSearchResultCount = (tags: string[]) => {
return this.props.loadSearchResultCount('tags', { tags });
};

renderTag = (tag: string) => {

+ 0
- 1
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx 파일 보기

@@ -56,7 +56,6 @@ function renderAssigneeFacet(props?: Partial<Props>) {
<AssigneeFacet
assigned={true}
assignees={[]}
component={undefined}
fetching={false}
loadSearchResultCount={jest.fn()}
onChange={jest.fn()}

+ 36
- 23
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.tsx 파일 보기

@@ -18,34 +18,47 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import { shallow, ShallowWrapper } from 'enzyme';
import { flatten } from 'lodash';
import Sidebar, { Props } from '../Sidebar';
import { Query } from '../../utils';

jest.mock('../../../../store/rootReducer', () => ({}));

const renderSidebar = (props?: Partial<Props>) =>
shallow(
<Sidebar
component={undefined}
facets={{}}
loadSearchResultCount={jest.fn()}
loadingFacets={{}}
myIssues={false}
onFacetToggle={jest.fn()}
onFilterChange={jest.fn()}
openFacets={{}}
organization={undefined}
query={{} as Query}
referencedComponents={{}}
referencedLanguages={{}}
referencedRules={{}}
referencedUsers={{}}
{...props}
/>
)
.children()
.map(node => node.name());
const renderSidebar = (props?: Partial<Props>) => {
return flatten(
mapChildren(
shallow(
<Sidebar
component={undefined}
facets={{}}
loadSearchResultCount={jest.fn()}
loadingFacets={{}}
myIssues={false}
onFacetToggle={jest.fn()}
onFilterChange={jest.fn()}
openFacets={{}}
organization={undefined}
query={{} as Query}
referencedComponents={{}}
referencedLanguages={{}}
referencedRules={{}}
referencedUsers={{}}
{...props}
/>
)
)
);

function mapChildren(wrapper: ShallowWrapper) {
return wrapper.children().map(node => {
if (typeof node.type() === 'symbol') {
return node.children().map(node => node.name());
}
return node.name();
});
}
};

const component = {
breadcrumbs: [],

+ 1
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap 파일 보기

@@ -11,6 +11,7 @@ exports[`should render 1`] = `
loadSearchResultCount={[Function]}
maxInitialItems={15}
maxItems={100}
minSearchLength={2}
onChange={[MockFunction]}
onClear={[Function]}
onItemClick={[Function]}

+ 1
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap 파일 보기

@@ -173,6 +173,7 @@ exports[`should render sub-facets 1`] = `
loadSearchResultCount={[Function]}
maxInitialItems={15}
maxItems={100}
minSearchLength={2}
onChange={[MockFunction]}
onSearch={[Function]}
onToggle={[MockFunction]}

+ 1
- 1
server/sonar-web/src/main/js/apps/issues/utils.ts 파일 보기

@@ -157,7 +157,7 @@ export function mapFacet(facet: string) {
return propertyMapping[facet] || facet;
}

export function parseFacets(facets: RawFacet[]) {
export function parseFacets(facets: RawFacet[]): { [x: string]: Facet } {
if (!facets) {
return {};
}

+ 0
- 84
server/sonar-web/src/main/js/components/controls/MouseOverHandler.tsx 파일 보기

@@ -1,84 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { findDOMNode } from 'react-dom';

interface Props {
delay?: number;
onOver: () => void;
}

export default class MouseOverHandler extends React.Component<Props> {
mouseEnterInterval?: number;
mounted = false;

componentDidMount() {
this.mounted = true;

const node = this.getNode();
if (node) {
this.attachEvents(node);
}
}

componentWillUnmount() {
this.mounted = false;

const node = this.getNode();
if (node) {
this.detachEvents(node);
}
}

getNode = () => {
// eslint-disable-next-line react/no-find-dom-node
const node = findDOMNode(this);
return node && node instanceof Element ? node : undefined;
};

attachEvents = (node: Element) => {
node.addEventListener('mouseenter', this.handleMouseEnter);
node.addEventListener('mouseleave', this.handleMouseLeave);
};

detachEvents = (node: Element) => {
node.removeEventListener('mouseenter', this.handleMouseEnter);
node.removeEventListener('mouseleave', this.handleMouseLeave);
};

handleMouseEnter = () => {
this.mouseEnterInterval = window.setTimeout(() => {
if (this.mounted) {
this.props.onOver();
}
}, this.props.delay || 0);
};

handleMouseLeave = () => {
if (this.mouseEnterInterval !== undefined) {
window.clearInterval(this.mouseEnterInterval);
this.mouseEnterInterval = undefined;
}
};

render() {
return this.props.children;
}
}

+ 0
- 88
server/sonar-web/src/main/js/components/controls/__tests__/MouseOverHandler-test.tsx 파일 보기

@@ -1,88 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { mount } from 'enzyme';
import MouseOverHandler from '../MouseOverHandler';

jest.useFakeTimers();

it('should trigger after delay', () => {
const onOver = jest.fn();
const wrapper = mount(
<MouseOverHandler delay={1000} onOver={onOver}>
<div />
</MouseOverHandler>
);

const node = wrapper.getDOMNode();

event(node, 'mouseenter');
expect(onOver).not.toBeCalled();

jest.runTimersToTime(500);
expect(onOver).not.toBeCalled();

jest.runTimersToTime(1000);
expect(onOver).toBeCalled();
});

it('should not trigger when mouse is out', () => {
const onOver = jest.fn();
const wrapper = mount(
<MouseOverHandler delay={1000} onOver={onOver}>
<div />
</MouseOverHandler>
);

const node = wrapper.getDOMNode();

event(node, 'mouseenter');
expect(onOver).not.toBeCalled();

jest.runTimersToTime(500);
event(node, 'mouseleave');

jest.runTimersToTime(1000);
expect(onOver).not.toBeCalled();
});

it('should detach events', () => {
const onOver = jest.fn();
const wrapper = mount(
<MouseOverHandler delay={1000} onOver={onOver}>
<div />
</MouseOverHandler>
);

const node = wrapper.getDOMNode();

event(node, 'mouseenter');
expect(onOver).not.toBeCalled();

wrapper.unmount();

jest.runTimersToTime(1000);
expect(onOver).not.toBeCalled();
});

function event(node: Element, eventName: string) {
const event = new MouseEvent(eventName);
node.dispatchEvent(event);
}

+ 0
- 10
server/sonar-web/src/main/js/components/facet/FacetItem.tsx 파일 보기

@@ -19,14 +19,12 @@
*/
import * as React from 'react';
import * as classNames from 'classnames';
import DeferredSpinner from '../common/DeferredSpinner';

export interface Props {
active?: boolean;
className?: string;
disabled?: boolean;
halfWidth?: boolean;
loading?: boolean;
name: React.ReactNode;
onClick: (x: string, multiple?: boolean) => void;
stat?: React.ReactNode;
@@ -49,14 +47,6 @@ export default class FacetItem extends React.PureComponent<Props> {
};

renderValue() {
if (this.props.loading) {
return (
<span className="facet-stat">
<DeferredSpinner />
</span>
);
}

if (this.props.stat == null) {
return null;
}

+ 0
- 39
server/sonar-web/src/main/js/components/facet/ListStyleFacet.css 파일 보기

@@ -1,39 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
.list-style-facet-mouse-over-animation::after {
content: '';
position: absolute;
z-index: 1;
top: 0;
bottom: 0;
left: 0;
width: 0;
background-color: var(--lightBlue);
}

.list-style-facet-mouse-over-animation:hover::after {
width: 100%;
transition: width 0.5s linear;
}

.list-style-facet-mouse-over-animation .facet-name,
.list-style-facet-mouse-over-animation .facet-stat {
z-index: 2;
}

+ 60
- 122
server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx 파일 보기

@@ -19,7 +19,6 @@
*/
import * as React from 'react';
import { sortBy, without } from 'lodash';
import * as classNames from 'classnames';
import FacetBox from './FacetBox';
import FacetHeader from './FacetHeader';
import FacetItem from './FacetItem';
@@ -32,9 +31,13 @@ import { Paging } from '../../app/types';
import SearchBox from '../controls/SearchBox';
import ListFooter from '../controls/ListFooter';
import { formatMeasure } from '../../helpers/measures';
import MouseOverHandler from '../controls/MouseOverHandler';
import { queriesEqual, RawQuery } from '../../helpers/query';
import './ListStyleFacet.css';

interface SearchResponse<S> {
maxResults?: boolean;
results: S[];
paging?: Paging;
}

export interface Props<S> {
className?: string;
@@ -43,17 +46,14 @@ export interface Props<S> {
getFacetItemText: (item: string) => string;
getSearchResultKey: (result: S) => string;
getSearchResultText: (result: S) => string;
loadSearchResultCount?: (result: S) => Promise<number>;
maxInitialItems?: number;
maxItems?: number;
minSearchLength?: number;
loadSearchResultCount?: (result: S[]) => Promise<{ [x: string]: number }>;
maxInitialItems: number;
maxItems: number;
minSearchLength: number;
onChange: (changes: { [x: string]: string | string[] }) => void;
onClear?: () => void;
onItemClick?: (itemValue: string, multiple: boolean) => void;
onSearch: (
query: string,
page?: number
) => Promise<{ maxResults?: boolean; results: S[]; paging?: Paging }>;
onSearch: (query: string, page?: number) => Promise<SearchResponse<S>>;
onToggle: (property: string) => void;
open: boolean;
property: string;
@@ -74,7 +74,6 @@ interface State<S> {
searchPaging?: Paging;
searchResults?: S[];
searchResultsCounts: { [key: string]: number };
searchResultsCountLoading: { [key: string]: boolean };
showFullList: boolean;
}

@@ -83,7 +82,8 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S

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

state: State<S> = {
@@ -91,7 +91,6 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
query: '',
searching: false,
searchResultsCounts: {},
searchResultsCountLoading: {},
showFullList: false
};

@@ -100,16 +99,6 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
}

componentDidUpdate(prevProps: Props<S>) {
// always remember issue counts from `stats`
if (prevProps.stats !== this.props.stats) {
this.setState(state => ({
searchResultsCounts: {
...state.searchResultsCounts,
...this.props.stats
}
}));
}

if (!prevProps.open && this.props.open) {
// focus search field *only* if it was manually open
this.setState({ autoFocus: true });
@@ -124,12 +113,11 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
searchResults: undefined,
searching: false,
searchResultsCounts: {},
searchResultsCountLoading: {},
showFullList: false
});
} else if (
prevProps.stats !== this.props.stats &&
Object.keys(this.props.stats || {}).length < this.props.maxInitialItems!
Object.keys(this.props.stats || {}).length < this.props.maxInitialItems
) {
// show limited list if `stats` changed and there are less than 15 items
this.setState({ showFullList: false });
@@ -175,18 +163,23 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
};

search = (query: string) => {
if (query.length >= 2) {
if (query.length >= this.props.minSearchLength) {
this.setState({ query, searching: true });
this.props.onSearch(query).then(({ maxResults, paging, results }) => {
if (this.mounted) {
this.setState({
searching: false,
searchMaxResults: maxResults,
searchResults: results,
searchPaging: paging
});
}
}, this.stopSearching);
this.props
.onSearch(query)
.then(this.loadCountsForSearchResults)
.then(({ maxResults, paging, results, stats }) => {
if (this.mounted) {
this.setState(state => ({
searching: false,
searchMaxResults: maxResults,
searchResults: results,
searchPaging: paging,
searchResultsCounts: { ...state.searchResultsCounts, ...stats }
}));
}
})
.catch(this.stopSearching);
} else {
this.setState({ query, searching: false, searchResults: [] });
}
@@ -196,56 +189,33 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
const { query, searchPaging, searchResults } = this.state;
if (query && searchResults && searchPaging) {
this.setState({ searching: true });
this.props.onSearch(query, searchPaging.pageIndex + 1).then(({ paging, results }) => {
if (this.mounted) {
this.setState({
searching: false,
searchResults: [...searchResults, ...results],
searchPaging: paging
});
}
}, this.stopSearching);
}
};

handleSearchResultMouseOver = (result: S) => {
if (
this.props.loadSearchResultCount &&
this.state.searchResultsCounts[this.props.getSearchResultKey(result)] === undefined
) {
this.setState(state => ({
searchResultsCountLoading: {
...state.searchResultsCountLoading,
[this.props.getSearchResultKey(result)]: true
}
}));

this.props.loadSearchResultCount(result).then(
count => {
if (this.mounted) {
this.setState(state => ({
searchResultsCounts: {
...state.searchResultsCounts,
[this.props.getSearchResultKey(result)]: count
},
searchResultsCountLoading: {
...state.searchResultsCountLoading,
[this.props.getSearchResultKey(result)]: false
}
}));
}
},
() => {
this.props
.onSearch(query, searchPaging.pageIndex + 1)
.then(this.loadCountsForSearchResults)
.then(({ paging, results, stats }) => {
if (this.mounted) {
this.setState(state => ({
searchResultsCountLoading: {
...state.searchResultsCountLoading,
[this.props.getSearchResultKey(result)]: false
}
searching: false,
searchResults: [...searchResults, ...results],
searchPaging: paging,
searchResultsCounts: { ...state.searchResultsCounts, ...stats }
}));
}
}
);
})
.catch(this.stopSearching);
}
};

loadCountsForSearchResults = (response: SearchResponse<S>) => {
const { loadSearchResultCount = () => Promise.resolve({}) } = this.props;
const resultsToLoad = response.results.filter(result => {
const key = this.props.getSearchResultKey(result);
return this.getStat(key) === undefined && this.state.searchResultsCounts[key] === undefined;
});
if (resultsToLoad.length > 0) {
return loadSearchResultCount(resultsToLoad).then(stats => ({ ...response, stats }));
} else {
return { ...response, stats: {} };
}
};

@@ -285,12 +255,12 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
// limit the number of items to this.props.maxInitialItems,
// but make sure all (in other words, the last) selected items are displayed
const lastSelectedIndex = this.getLastActiveIndex(sortedItems);
const countToDisplay = Math.max(this.props.maxInitialItems!, lastSelectedIndex + 1);
const countToDisplay = Math.max(this.props.maxInitialItems, lastSelectedIndex + 1);
const limitedList = this.state.showFullList
? sortedItems
: sortedItems.slice(0, countToDisplay);

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

return (
<>
@@ -328,14 +298,12 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
return null;
}

const { minSearchLength = 2 } = this.props;

return (
<SearchBox
autoFocus={this.state.autoFocus}
className="little-spacer-top spacer-bottom"
loading={this.state.searching}
minLength={minSearchLength}
minLength={this.props.minSearchLength}
onChange={this.search}
placeholder={this.props.searchPlaceholder}
value={this.state.query}
@@ -381,31 +349,13 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
renderSearchResult(result: S) {
const key = this.props.getSearchResultKey(result);
const active = this.props.values.includes(key);

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

let stat: number | undefined = this.getStat(key);
let disabled = isFacetExhaustive && !active && stat === 0;
if (stat === undefined) {
stat = this.state.searchResultsCounts[key];
disabled = false; // do not disable facet if the count was requested after mouse over
}
if (stat === undefined && isFacetExhaustive) {
stat = 0;
disabled = !active;
}

const loading = this.state.searchResultsCountLoading[key];
const canBeLoaded =
!loading && this.props.loadSearchResultCount !== undefined && stat === undefined;

const facetItem = (
const stat = this.getStat(key) || this.state.searchResultsCounts[key];
const disabled = !active && stat === 0;
return (
<FacetItem
active={active}
className={classNames({ 'list-style-facet-mouse-over-animation': canBeLoaded })}
disabled={disabled}
loading={loading}
key={key}
name={this.props.renderSearchResult(result, this.state.query)}
onClick={this.handleItemClick}
stat={formatFacetStat(stat)}
@@ -413,18 +363,6 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
value={key}
/>
);

return (
<React.Fragment key={key}>
{canBeLoaded ? (
<MouseOverHandler delay={500} onOver={() => this.handleSearchResultMouseOver(result)}>
{facetItem}
</MouseOverHandler>
) : (
facetItem
)}
</React.Fragment>
);
}

render() {

+ 8
- 29
server/sonar-web/src/main/js/components/facet/__tests__/ListStyleFacet-test.tsx 파일 보기

@@ -76,19 +76,22 @@ it('should search', async () => {
results: ['d', 'e'],
paging: { pageIndex: 1, pageSize: 2, total: 3 }
});
const wrapper = shallowRender({ onSearch });
const loadSearchResultCount = jest.fn().mockResolvedValue({ d: 7, e: 3 });
const wrapper = shallowRender({ loadSearchResultCount, onSearch });

// search
wrapper.find('SearchBox').prop<Function>('onChange')('query');
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
expect(onSearch).lastCalledWith('query');
expect(loadSearchResultCount).lastCalledWith(['d', 'e']);

// load more results
onSearch.mockResolvedValue({
results: ['f'],
paging: { pageIndex: 2, pageSize: 2, total: 3 }
});
loadSearchResultCount.mockResolvedValue({ f: 5 });
wrapper.find('ListFooter').prop<Function>('loadMore')();
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
@@ -96,10 +99,12 @@ it('should search', async () => {

// clear search
onSearch.mockClear();
loadSearchResultCount.mockClear();
wrapper.find('SearchBox').prop<Function>('onChange')('');
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
expect(onSearch).not.toBeCalled();
expect(loadSearchResultCount).not.toBeCalled();

// search for no results
onSearch.mockResolvedValue({ results: [], paging: { pageIndex: 1, pageSize: 2, total: 0 } });
@@ -107,6 +112,7 @@ it('should search', async () => {
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
expect(onSearch).lastCalledWith('blabla');
expect(loadSearchResultCount).not.toBeCalled();

// search fails
onSearch.mockRejectedValue(undefined);
@@ -114,6 +120,7 @@ it('should search', async () => {
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot(); // should render previous results
expect(onSearch).lastCalledWith('blabla');
expect(loadSearchResultCount).not.toBeCalled();
});

it('should limit the number of items', () => {
@@ -164,33 +171,6 @@ it('should collapse list when new stats have few results', () => {
expect(wrapper.state('showFullList')).toBe(false);
});

it('should load count on mouse over', async () => {
const loadSearchResultCount = jest.fn().mockResolvedValue(5);
const onSearch = jest.fn().mockResolvedValue({
results: ['d', 'e'],
paging: { pageIndex: 1, pageSize: 2, total: 3 }
});
const wrapper = shallowRender({ loadSearchResultCount, maxItems: 1, onSearch });

// search
wrapper.find('SearchBox').prop<Function>('onChange')('query');
await waitAndUpdate(wrapper);

expect(firstFacetItem().prop('stat')).toBeUndefined();
wrapper
.find('MouseOverHandler')
.first()
.prop<Function>('onOver')();
expect(loadSearchResultCount).toBeCalledWith('d');
await waitAndUpdate(wrapper);

expect(firstFacetItem().prop('stat')).toBe('5');

function firstFacetItem() {
return wrapper.find('FacetItem').first();
}
});

function shallowRender(props: Partial<Props<string>> = {}) {
return shallow(
<ListStyleFacet
@@ -223,6 +203,5 @@ function checkInitialState(wrapper: ShallowWrapper) {
expect(wrapper.state('searchResults')).toBe(undefined);
expect(wrapper.state('searching')).toBe(false);
expect(wrapper.state('searchResultsCounts')).toEqual({});
expect(wrapper.state('searchResultsCountLoading')).toEqual({});
expect(wrapper.state('showFullList')).toBe(false);
}

+ 55
- 75
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap 파일 보기

@@ -105,38 +105,30 @@ exports[`should search 1`] = `
/>
<React.Fragment>
<FacetItemsList>
<React.Fragment
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="d"
>
<FacetItem
active={false}
className=""
disabled={true}
halfWidth={false}
loading={false}
name="d"
onClick={[Function]}
stat={0}
tooltip="d"
value="d"
/>
</React.Fragment>
<React.Fragment
loading={false}
name="d"
onClick={[Function]}
stat="7"
tooltip="d"
value="d"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="e"
>
<FacetItem
active={false}
className=""
disabled={true}
halfWidth={false}
loading={false}
name="e"
onClick={[Function]}
stat={0}
tooltip="e"
value="e"
/>
</React.Fragment>
loading={false}
name="e"
onClick={[Function]}
stat="3"
tooltip="e"
value="e"
/>
</FacetItemsList>
<ListFooter
className="spacer-bottom"
@@ -181,54 +173,42 @@ exports[`should search 2`] = `
/>
<React.Fragment>
<FacetItemsList>
<React.Fragment
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="d"
>
<FacetItem
active={false}
className=""
disabled={true}
halfWidth={false}
loading={false}
name="d"
onClick={[Function]}
stat={0}
tooltip="d"
value="d"
/>
</React.Fragment>
<React.Fragment
loading={false}
name="d"
onClick={[Function]}
stat="7"
tooltip="d"
value="d"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="e"
>
<FacetItem
active={false}
className=""
disabled={true}
halfWidth={false}
loading={false}
name="e"
onClick={[Function]}
stat={0}
tooltip="e"
value="e"
/>
</React.Fragment>
<React.Fragment
loading={false}
name="e"
onClick={[Function]}
stat="3"
tooltip="e"
value="e"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="f"
>
<FacetItem
active={false}
className=""
disabled={true}
halfWidth={false}
loading={false}
name="f"
onClick={[Function]}
stat={0}
tooltip="f"
value="f"
/>
</React.Fragment>
loading={false}
name="f"
onClick={[Function]}
stat="5"
tooltip="f"
value="f"
/>
</FacetItemsList>
<ListFooter
className="spacer-bottom"

Loading…
취소
저장