@@ -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) => { |
@@ -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 = () => { |
@@ -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) => { |
@@ -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) => ( |
@@ -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) => ( |
@@ -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) => { |
@@ -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); |
@@ -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) => ( |
@@ -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) => { |
@@ -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) => { |
@@ -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} |
@@ -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 = ( |
@@ -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) => { |
@@ -56,7 +56,6 @@ function renderAssigneeFacet(props?: Partial<Props>) { | |||
<AssigneeFacet | |||
assigned={true} | |||
assignees={[]} | |||
component={undefined} | |||
fetching={false} | |||
loadSearchResultCount={jest.fn()} | |||
onChange={jest.fn()} |
@@ -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: [], |
@@ -11,6 +11,7 @@ exports[`should render 1`] = ` | |||
loadSearchResultCount={[Function]} | |||
maxInitialItems={15} | |||
maxItems={100} | |||
minSearchLength={2} | |||
onChange={[MockFunction]} | |||
onClear={[Function]} | |||
onItemClick={[Function]} |
@@ -173,6 +173,7 @@ exports[`should render sub-facets 1`] = ` | |||
loadSearchResultCount={[Function]} | |||
maxInitialItems={15} | |||
maxItems={100} | |||
minSearchLength={2} | |||
onChange={[MockFunction]} | |||
onSearch={[Function]} | |||
onToggle={[MockFunction]} |
@@ -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 {}; | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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; | |||
} |
@@ -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; | |||
} |
@@ -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() { |
@@ -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); | |||
} |
@@ -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" |