@@ -658,6 +658,30 @@ export default class App extends React.PureComponent<Props, State> { | |||
}); | |||
}; | |||
loadSearchResultCount = (changes: Partial<Query>) => { | |||
const { component } = this.props; | |||
const { myIssues, query } = this.state; | |||
const organizationKey = | |||
(component && component.organization) || | |||
(this.props.organization && this.props.organization.key); | |||
const parameters = { | |||
...getBranchLikeQuery(this.props.branchLike), | |||
componentKeys: component && component.key, | |||
s: 'FILE_LINE', | |||
...serializeQuery({ ...query, ...changes }), | |||
ps: 1, | |||
organization: organizationKey | |||
}; | |||
if (myIssues) { | |||
Object.assign(parameters, { assignees: '__me__' }); | |||
} | |||
return this.props.fetchIssues(parameters, false).then(reponse => reponse.paging.total); | |||
}; | |||
closeFacet = (property: string) => { | |||
this.setState(state => ({ | |||
openFacets: { ...state.openFacets, [property]: false } | |||
@@ -907,7 +931,7 @@ export default class App extends React.PureComponent<Props, State> { | |||
component={component} | |||
facets={this.state.facets} | |||
hideAuthorFacet={hideAuthorFacet} | |||
loading={this.state.loading} | |||
loadSearchResultCount={this.loadSearchResultCount} | |||
loadingFacets={this.state.loadingFacets} | |||
myIssues={this.state.myIssues} | |||
onFacetToggle={this.handleFacetToggle} |
@@ -18,101 +18,32 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { sortBy, uniq, without } from 'lodash'; | |||
import { | |||
searchAssignees, | |||
formatFacetStat, | |||
Query, | |||
ReferencedUser, | |||
SearchedAssignee | |||
} from '../utils'; | |||
import { Component, Paging } from '../../../app/types'; | |||
import FacetBox from '../../../components/facet/FacetBox'; | |||
import FacetHeader from '../../../components/facet/FacetHeader'; | |||
import FacetItem from '../../../components/facet/FacetItem'; | |||
import FacetItemsList from '../../../components/facet/FacetItemsList'; | |||
import { omit, sortBy, without } from 'lodash'; | |||
import { searchAssignees, Query, ReferencedUser, SearchedAssignee } from '../utils'; | |||
import { Component } from '../../../app/types'; | |||
import Avatar from '../../../components/ui/Avatar'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint'; | |||
import SearchBox from '../../../components/controls/SearchBox'; | |||
import ListFooter from '../../../components/controls/ListFooter'; | |||
import { highlightTerm } from '../../../helpers/search'; | |||
import ListStyleFacet from '../../../components/facet/ListStyleFacet'; | |||
export interface Props { | |||
assigned: boolean; | |||
assignees: string[]; | |||
component: Component | undefined; | |||
fetching: boolean; | |||
loading?: boolean; | |||
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
organization: string | undefined; | |||
query: Query; | |||
stats: { [x: string]: number } | undefined; | |||
referencedUsers: { [login: string]: ReferencedUser }; | |||
} | |||
interface State { | |||
query: string; | |||
searching: boolean; | |||
searchResults?: SearchedAssignee[]; | |||
searchPaging?: Paging; | |||
} | |||
export default class AssigneeFacet extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
property = 'assignees'; | |||
state: State = { | |||
query: '', | |||
searching: false | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
stopSearching = () => { | |||
if (this.mounted) { | |||
this.setState({ searching: false }); | |||
} | |||
}; | |||
search = (query: string) => { | |||
if (query.length >= 2) { | |||
this.setState({ query, searching: true }); | |||
searchAssignees(query, this.props.organization).then(({ paging, results }) => { | |||
if (this.mounted) { | |||
this.setState({ searching: false, searchResults: results, searchPaging: paging }); | |||
} | |||
}, this.stopSearching); | |||
} else { | |||
this.setState({ query, searching: false, searchResults: [] }); | |||
} | |||
}; | |||
searchMore = () => { | |||
const { query, searchPaging, searchResults } = this.state; | |||
if (query && searchResults && searchPaging) { | |||
this.setState({ searching: true }); | |||
searchAssignees(query, this.props.organization, searchPaging.pageIndex + 1).then( | |||
({ paging, results }) => { | |||
if (this.mounted) { | |||
this.setState({ | |||
searching: false, | |||
searchResults: [...searchResults, ...results], | |||
searchPaging: paging | |||
}); | |||
} | |||
}, | |||
this.stopSearching | |||
); | |||
} | |||
export default class AssigneeFacet extends React.PureComponent<Props> { | |||
handleSearch = (query: string, page?: number) => { | |||
return searchAssignees(query, this.props.organization, page); | |||
}; | |||
handleItemClick = (itemValue: string, multiple: boolean) => { | |||
@@ -124,221 +55,105 @@ export default class AssigneeFacet extends React.PureComponent<Props, State> { | |||
const newValue = sortBy( | |||
assignees.includes(itemValue) ? without(assignees, itemValue) : [...assignees, itemValue] | |||
); | |||
this.props.onChange({ assigned: true, [this.property]: newValue }); | |||
this.props.onChange({ assigned: true, assignees: newValue }); | |||
} else { | |||
this.props.onChange({ | |||
assigned: true, | |||
[this.property]: assignees.includes(itemValue) && assignees.length < 2 ? [] : [itemValue] | |||
assignees: assignees.includes(itemValue) && assignees.length < 2 ? [] : [itemValue] | |||
}); | |||
} | |||
}; | |||
handleHeaderClick = () => { | |||
this.props.onToggle(this.property); | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ assigned: true, assignees: [] }); | |||
}; | |||
handleSelect = (option: { value: string }) => { | |||
const { assignees } = this.props; | |||
this.props.onChange({ assigned: true, [this.property]: uniq([...assignees, option.value]) }); | |||
}; | |||
isAssigneeActive(assignee: string) { | |||
return assignee === '' ? !this.props.assigned : this.props.assignees.includes(assignee); | |||
} | |||
getAssigneeNameAndTooltip(assignee: string) { | |||
getAssigneeName = (assignee: string) => { | |||
if (assignee === '') { | |||
return { name: translate('unassigned'), tooltip: translate('unassigned') }; | |||
return translate('unassigned'); | |||
} else { | |||
const { referencedUsers } = this.props; | |||
if (referencedUsers[assignee]) { | |||
return { | |||
name: ( | |||
<span> | |||
<Avatar | |||
className="little-spacer-right" | |||
hash={referencedUsers[assignee].avatar} | |||
name={referencedUsers[assignee].name} | |||
size={16} | |||
/> | |||
{referencedUsers[assignee].name} | |||
</span> | |||
), | |||
tooltip: referencedUsers[assignee].name | |||
}; | |||
} else { | |||
return { name: assignee, tooltip: assignee }; | |||
} | |||
} | |||
} | |||
getStat(assignee: string) { | |||
const { stats } = this.props; | |||
return stats ? stats[assignee] : undefined; | |||
} | |||
getValues() { | |||
const values = this.props.assignees.map(assignee => { | |||
const user = this.props.referencedUsers[assignee]; | |||
return user ? user.name : assignee; | |||
}); | |||
if (!this.props.assigned) { | |||
values.push(translate('unassigned')); | |||
} | |||
return values; | |||
} | |||
renderOption = (option: { avatar: string; label: string }) => { | |||
return this.renderAssignee(option.avatar, option.label); | |||
}; | |||
renderAssignee = (avatar: string | undefined, name: string) => ( | |||
<span> | |||
{avatar !== undefined && ( | |||
<Avatar className="little-spacer-right" hash={avatar} name={name} size={16} /> | |||
)} | |||
{name} | |||
</span> | |||
); | |||
renderListItem(assignee: string) { | |||
const { name, tooltip } = this.getAssigneeNameAndTooltip(assignee); | |||
return ( | |||
<FacetItem | |||
active={this.isAssigneeActive(assignee)} | |||
key={assignee} | |||
loading={this.props.loading} | |||
name={name} | |||
onClick={this.handleItemClick} | |||
stat={formatFacetStat(this.getStat(assignee))} | |||
tooltip={tooltip} | |||
value={assignee} | |||
/> | |||
); | |||
} | |||
renderList() { | |||
const { stats } = this.props; | |||
if (!stats) { | |||
return null; | |||
} | |||
loadSearchResultCount = (assignee: SearchedAssignee) => { | |||
return this.props.loadSearchResultCount({ assigned: undefined, assignees: [assignee.login] }); | |||
}; | |||
const assignees = sortBy( | |||
getSortedItems = () => { | |||
const { stats = {} } = this.props; | |||
return sortBy( | |||
Object.keys(stats), | |||
// put unassigned first | |||
// put "not assigned" first | |||
key => (key === '' ? 0 : 1), | |||
// the sort by number | |||
key => -stats[key] | |||
); | |||
}; | |||
return ( | |||
<FacetItemsList>{assignees.map(assignee => this.renderListItem(assignee))}</FacetItemsList> | |||
); | |||
} | |||
renderSearch() { | |||
if (!this.props.stats || !Object.keys(this.props.stats).length) { | |||
return null; | |||
} | |||
return ( | |||
<SearchBox | |||
autoFocus={true} | |||
className="little-spacer-top spacer-bottom" | |||
loading={this.state.searching} | |||
minLength={2} | |||
onChange={this.search} | |||
placeholder={translate('search.search_for_users')} | |||
value={this.state.query} | |||
/> | |||
); | |||
} | |||
renderSearchResults() { | |||
const { searching, searchResults, searchPaging } = this.state; | |||
if (!searching && (!searchResults || !searchResults.length)) { | |||
return <div className="note spacer-bottom">{translate('no_results')}</div>; | |||
} | |||
if (!searchResults || !searchPaging) { | |||
// initial search | |||
return null; | |||
renderFacetItem = (assignee: string) => { | |||
if (assignee === '') { | |||
return translate('unassigned'); | |||
} | |||
return ( | |||
const user = this.props.referencedUsers[assignee]; | |||
return user ? ( | |||
<> | |||
<FacetItemsList> | |||
{searchResults.map(result => this.renderSearchResult(result))} | |||
</FacetItemsList> | |||
<ListFooter | |||
count={searchResults.length} | |||
loadMore={this.searchMore} | |||
ready={!searching} | |||
total={searchPaging.total} | |||
/> | |||
<Avatar className="little-spacer-right" hash={user.avatar} name={user.name} size={16} /> | |||
{user.name} | |||
</> | |||
) : ( | |||
assignee | |||
); | |||
} | |||
}; | |||
renderSearchResult(result: SearchedAssignee) { | |||
const active = this.props.assignees.includes(result.login); | |||
const stat = this.getStat(result.login); | |||
renderSearchResult = (result: SearchedAssignee, query: string) => { | |||
return ( | |||
<FacetItem | |||
active={active} | |||
disabled={!active && stat === 0} | |||
key={result.login} | |||
loading={this.props.loading} | |||
name={ | |||
<> | |||
{result.avatar !== undefined && ( | |||
<Avatar | |||
className="little-spacer-right" | |||
hash={result.avatar} | |||
name={result.name} | |||
size={16} | |||
/> | |||
)} | |||
{highlightTerm(result.name, this.state.query)} | |||
</> | |||
} | |||
onClick={this.handleItemClick} | |||
stat={stat && formatFacetStat(stat)} | |||
tooltip={result.name} | |||
value={result.login} | |||
/> | |||
<> | |||
{result.avatar !== undefined && ( | |||
<Avatar | |||
className="little-spacer-right" | |||
hash={result.avatar} | |||
name={result.name} | |||
size={16} | |||
/> | |||
)} | |||
{highlightTerm(result.name, query)} | |||
</> | |||
); | |||
} | |||
}; | |||
render() { | |||
const { assignees, stats = {} } = this.props; | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={this.getValues()} | |||
/> | |||
const values = [...this.props.assignees]; | |||
if (!this.props.assigned) { | |||
values.push(''); | |||
} | |||
<DeferredSpinner loading={this.props.fetching} /> | |||
{this.props.open && ( | |||
<> | |||
{this.renderSearch()} | |||
{this.state.query && this.state.searchResults !== undefined | |||
? this.renderSearchResults() | |||
: this.renderList()} | |||
<MultipleSelectionHint options={Object.keys(stats).length} values={assignees.length} /> | |||
</> | |||
)} | |||
</FacetBox> | |||
return ( | |||
<ListStyleFacet<SearchedAssignee> | |||
facetHeader={translate('issues.facet.assignees')} | |||
fetching={this.props.fetching} | |||
getFacetItemText={this.getAssigneeName} | |||
getSearchResultKey={user => user.login} | |||
getSearchResultText={user => user.name} | |||
// put "not assigned" item first | |||
getSortedItems={this.getSortedItems} | |||
loadSearchResultCount={this.loadSearchResultCount} | |||
onChange={this.props.onChange} | |||
onClear={this.handleClear} | |||
onItemClick={this.handleItemClick} | |||
onSearch={this.handleSearch} | |||
onToggle={this.props.onToggle} | |||
open={this.props.open} | |||
property="assignees" | |||
query={omit(this.props.query, 'assigned', 'assignees')} | |||
renderFacetItem={this.renderFacetItem} | |||
renderSearchResult={this.renderSearchResult} | |||
searchPlaceholder={translate('search.search_for_users')} | |||
stats={this.props.stats} | |||
values={values} | |||
/> | |||
); | |||
} | |||
} |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { omit } from 'lodash'; | |||
import { Query } from '../utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import ListStyleFacet from '../../../components/facet/ListStyleFacet'; | |||
@@ -27,11 +28,12 @@ import { highlightTerm } from '../../../helpers/search'; | |||
interface Props { | |||
componentKey: string | undefined; | |||
fetching: boolean; | |||
loading?: boolean; | |||
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
organization: string | undefined; | |||
query: Query; | |||
stats: { [x: string]: number } | undefined; | |||
authors: string[]; | |||
} | |||
@@ -52,23 +54,29 @@ 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] }); | |||
}; | |||
renderSearchResult = (author: string, term: string) => { | |||
return highlightTerm(author, term); | |||
}; | |||
render() { | |||
return ( | |||
<ListStyleFacet | |||
<ListStyleFacet<string> | |||
facetHeader={translate('issues.facet.authors')} | |||
fetching={this.props.fetching} | |||
getFacetItemText={this.identity} | |||
getSearchResultKey={this.identity} | |||
getSearchResultText={this.identity} | |||
loadSearchResultCount={this.loadSearchResultCount} | |||
onChange={this.props.onChange} | |||
onSearch={this.handleSearch} | |||
onToggle={this.props.onToggle} | |||
open={this.props.open} | |||
property="authors" | |||
query={omit(this.props.query, 'authors')} | |||
renderFacetItem={this.identity} | |||
renderSearchResult={this.renderSearchResult} | |||
searchPlaceholder={translate('search.search_for_authors')} |
@@ -42,7 +42,6 @@ interface Props { | |||
createdBefore: Date | undefined; | |||
createdInLast: string; | |||
fetching: boolean; | |||
loading?: boolean; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
@@ -173,7 +172,7 @@ export default class CreationDateFacet extends React.PureComponent<Props> { | |||
createdBefore: endDate, | |||
tooltip, | |||
x: index, | |||
y: this.props.loading ? 0 : stats[start] | |||
y: stats[start] | |||
}; | |||
}); | |||
@@ -226,7 +225,6 @@ export default class CreationDateFacet extends React.PureComponent<Props> { | |||
<div className="spacer-top issues-predefined-periods"> | |||
<FacetItem | |||
active={!this.hasValue()} | |||
loading={this.props.loading} | |||
name={translate('issues.facet.createdAt.all')} | |||
onClick={this.handlePeriodClick} | |||
tooltip={translate('issues.facet.createdAt.all')} | |||
@@ -235,7 +233,6 @@ export default class CreationDateFacet extends React.PureComponent<Props> { | |||
{component ? ( | |||
<FacetItem | |||
active={sinceLeakPeriod} | |||
loading={this.props.loading} | |||
name={translate('issues.new_code')} | |||
onClick={this.handleLeakPeriodClick} | |||
tooltip={translate('issues.leak_period')} | |||
@@ -245,7 +242,6 @@ export default class CreationDateFacet extends React.PureComponent<Props> { | |||
<> | |||
<FacetItem | |||
active={createdInLast === '1w'} | |||
loading={this.props.loading} | |||
name={translate('issues.facet.createdAt.last_week')} | |||
onClick={this.handlePeriodClick} | |||
tooltip={translate('issues.facet.createdAt.last_week')} | |||
@@ -253,7 +249,6 @@ export default class CreationDateFacet extends React.PureComponent<Props> { | |||
/> | |||
<FacetItem | |||
active={createdInLast === '1m'} | |||
loading={this.props.loading} | |||
name={translate('issues.facet.createdAt.last_month')} | |||
onClick={this.handlePeriodClick} | |||
tooltip={translate('issues.facet.createdAt.last_month')} | |||
@@ -261,7 +256,6 @@ export default class CreationDateFacet extends React.PureComponent<Props> { | |||
/> | |||
<FacetItem | |||
active={createdInLast === '1y'} | |||
loading={this.props.loading} | |||
name={translate('issues.facet.createdAt.last_year')} | |||
onClick={this.handlePeriodClick} | |||
tooltip={translate('issues.facet.createdAt.last_year')} |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { omit } from 'lodash'; | |||
import { Query } from '../utils'; | |||
import QualifierIcon from '../../../components/icons-components/QualifierIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
@@ -30,10 +31,11 @@ interface Props { | |||
componentKey: string; | |||
fetching: boolean; | |||
directories: string[]; | |||
loading?: boolean; | |||
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
query: Query; | |||
stats: { [x: string]: number } | undefined; | |||
} | |||
@@ -60,6 +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] }); | |||
}; | |||
renderDirectory = (directory: React.ReactNode) => ( | |||
<> | |||
<QualifierIcon className="little-spacer-right" qualifier="DIR" /> | |||
@@ -77,18 +83,20 @@ export default class DirectoryFacet extends React.PureComponent<Props> { | |||
render() { | |||
return ( | |||
<ListStyleFacet | |||
<ListStyleFacet<TreeComponent> | |||
facetHeader={translate('issues.facet.directories')} | |||
fetching={this.props.fetching} | |||
getFacetItemText={this.getFacetItemText} | |||
getSearchResultKey={this.getSearchResultKey} | |||
getSearchResultText={this.getSearchResultText} | |||
loadSearchResultCount={this.loadSearchResultCount} | |||
minSearchLength={3} | |||
onChange={this.props.onChange} | |||
onSearch={this.handleSearch} | |||
onToggle={this.props.onToggle} | |||
open={this.props.open} | |||
property="directories" | |||
query={omit(this.props.query, 'directories')} | |||
renderFacetItem={this.renderFacetItem} | |||
renderSearchResult={this.renderSearchResult} | |||
searchPlaceholder={translate('search.search_for_directories')} |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { omit } from 'lodash'; | |||
import { Query, ReferencedComponent } from '../utils'; | |||
import QualifierIcon from '../../../components/icons-components/QualifierIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
@@ -30,10 +31,11 @@ interface Props { | |||
componentKey: string; | |||
fetching: boolean; | |||
files: string[]; | |||
loading?: boolean; | |||
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
query: Query; | |||
referencedComponents: { [componentKey: string]: ReferencedComponent }; | |||
stats: { [x: string]: number } | undefined; | |||
} | |||
@@ -67,6 +69,10 @@ export default class FileFacet extends React.PureComponent<Props> { | |||
}).then(({ components, paging }) => ({ paging, results: components })); | |||
}; | |||
loadSearchResultCount = (file: TreeComponent) => { | |||
return this.props.loadSearchResultCount({ files: [file.id] }); | |||
}; | |||
renderFile = (file: React.ReactNode) => ( | |||
<> | |||
<QualifierIcon className="little-spacer-right" qualifier="FIL" /> | |||
@@ -85,18 +91,20 @@ export default class FileFacet extends React.PureComponent<Props> { | |||
render() { | |||
return ( | |||
<ListStyleFacet | |||
<ListStyleFacet<TreeComponent> | |||
facetHeader={translate('issues.facet.files')} | |||
fetching={this.props.fetching} | |||
getFacetItemText={this.getFacetItemText} | |||
getSearchResultKey={this.getSearchResultKey} | |||
getSearchResultText={this.getSearchResultText} | |||
loadSearchResultCount={this.loadSearchResultCount} | |||
minSearchLength={3} | |||
onChange={this.props.onChange} | |||
onSearch={this.handleSearch} | |||
onToggle={this.props.onToggle} | |||
open={this.props.open} | |||
property="files" | |||
query={omit(this.props.query, 'files')} | |||
renderFacetItem={this.renderFacetItem} | |||
renderSearchResult={this.renderSearchResult} | |||
searchPlaceholder={translate('search.search_for_files')} |
@@ -18,7 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { uniqBy } from 'lodash'; | |||
import { uniqBy, omit } from 'lodash'; | |||
import { connect } from 'react-redux'; | |||
import ListStyleFacet from '../../../components/facet/ListStyleFacet'; | |||
import { Query, ReferencedLanguage } from '../utils'; | |||
@@ -35,10 +35,11 @@ interface Props { | |||
fetching: boolean; | |||
installedLanguages: InstalledLanguage[]; | |||
languages: string[]; | |||
loading?: boolean; | |||
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
query: Query; | |||
referencedLanguages: { [languageKey: string]: ReferencedLanguage }; | |||
stats: { [x: string]: number } | undefined; | |||
} | |||
@@ -70,23 +71,29 @@ class LanguageFacet extends React.PureComponent<Props> { | |||
); | |||
}; | |||
loadSearchResultCount = (language: InstalledLanguage) => { | |||
return this.props.loadSearchResultCount({ languages: [language.key] }); | |||
}; | |||
renderSearchResult = ({ name }: InstalledLanguage, term: string) => { | |||
return highlightTerm(name, term); | |||
}; | |||
render() { | |||
return ( | |||
<ListStyleFacet | |||
<ListStyleFacet<InstalledLanguage> | |||
facetHeader={translate('issues.facet.languages')} | |||
fetching={this.props.fetching} | |||
getFacetItemText={this.getLanguageName} | |||
getSearchResultKey={(language: InstalledLanguage) => language.key} | |||
getSearchResultText={(language: InstalledLanguage) => language.name} | |||
getSearchResultKey={language => language.key} | |||
getSearchResultText={language => language.name} | |||
loadSearchResultCount={this.loadSearchResultCount} | |||
onChange={this.props.onChange} | |||
onSearch={this.handleSearch} | |||
onToggle={this.props.onToggle} | |||
open={this.props.open} | |||
property="languages" | |||
query={omit(this.props.query, 'languages')} | |||
renderFacetItem={this.getLanguageName} | |||
renderSearchResult={this.renderSearchResult} | |||
searchPlaceholder={translate('search.search_for_languages')} |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { omit } from 'lodash'; | |||
import { Query, ReferencedComponent } from '../utils'; | |||
import QualifierIcon from '../../../components/icons-components/QualifierIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
@@ -28,11 +29,12 @@ import { highlightTerm } from '../../../helpers/search'; | |||
interface Props { | |||
componentKey: string; | |||
fetching: boolean; | |||
loading?: boolean; | |||
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; | |||
modules: string[]; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
query: Query; | |||
referencedComponents: { [componentKey: string]: ReferencedComponent }; | |||
stats: { [x: string]: number } | undefined; | |||
} | |||
@@ -61,6 +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] }); | |||
}; | |||
renderModule = (module: React.ReactNode) => ( | |||
<> | |||
<QualifierIcon className="little-spacer-right" qualifier="BRC" /> | |||
@@ -79,18 +85,20 @@ export default class ModuleFacet extends React.PureComponent<Props> { | |||
render() { | |||
return ( | |||
<ListStyleFacet | |||
<ListStyleFacet<TreeComponent> | |||
facetHeader={translate('issues.facet.modules')} | |||
fetching={this.props.fetching} | |||
getFacetItemText={this.getModuleName} | |||
getSearchResultKey={this.getSearchResultKey} | |||
getSearchResultText={this.getSearchResultText} | |||
loadSearchResultCount={this.loadSearchResultCount} | |||
minSearchLength={3} | |||
onChange={this.props.onChange} | |||
onSearch={this.handleSearch} | |||
onToggle={this.props.onToggle} | |||
open={this.props.open} | |||
property="modules" | |||
query={omit(this.props.query, 'modules')} | |||
renderFacetItem={this.renderFacetItem} | |||
renderSearchResult={this.renderSearchResult} | |||
searchPlaceholder={translate('search.search_for_modules')} |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { omit } from 'lodash'; | |||
import ListStyleFacet from '../../../components/facet/ListStyleFacet'; | |||
import { Query, ReferencedComponent } from '../utils'; | |||
import { searchProjects, getTree } from '../../../api/components'; | |||
@@ -29,13 +30,14 @@ import { highlightTerm } from '../../../helpers/search'; | |||
interface Props { | |||
component: Component | undefined; | |||
loading?: boolean; | |||
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; | |||
fetching: boolean; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
organization: { key: string } | undefined; | |||
projects: string[]; | |||
query: Query; | |||
referencedComponents: { [componentKey: string]: ReferencedComponent }; | |||
stats: { [x: string]: number } | undefined; | |||
} | |||
@@ -91,6 +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] }); | |||
}; | |||
renderFacetItem = (project: string) => { | |||
const { referencedComponents } = this.props; | |||
return referencedComponents[project] ? ( | |||
@@ -125,17 +131,19 @@ export default class ProjectFacet extends React.PureComponent<Props> { | |||
render() { | |||
return ( | |||
<ListStyleFacet | |||
<ListStyleFacet<SearchedProject> | |||
facetHeader={translate('issues.facet.projects')} | |||
fetching={this.props.fetching} | |||
getFacetItemText={this.getProjectName} | |||
getSearchResultKey={(project: SearchedProject) => project.id} | |||
getSearchResultText={(project: SearchedProject) => project.name} | |||
getSearchResultKey={project => project.id} | |||
getSearchResultText={project => project.name} | |||
loadSearchResultCount={this.loadSearchResultCount} | |||
onChange={this.props.onChange} | |||
onSearch={this.handleSearch} | |||
onToggle={this.props.onToggle} | |||
open={this.props.open} | |||
property="projects" | |||
query={omit(this.props.query, 'projects')} | |||
renderFacetItem={this.renderFacetItem} | |||
renderSearchResult={this.renderSearchResult} | |||
searchPlaceholder={translate('search.search_for_projects')} |
@@ -30,7 +30,6 @@ import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHi | |||
interface Props { | |||
fetching: boolean; | |||
loading?: boolean; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
@@ -100,7 +99,6 @@ export default class ResolutionFacet extends React.PureComponent<Props> { | |||
disabled={stat === 0 && !active} | |||
halfWidth={true} | |||
key={resolution} | |||
loading={this.props.loading} | |||
name={this.getFacetItemName(resolution)} | |||
onClick={this.handleItemClick} | |||
stat={formatFacetStat(stat)} |
@@ -18,33 +18,28 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { omit } from 'lodash'; | |||
import ListStyleFacet from '../../../components/facet/ListStyleFacet'; | |||
import { Query, ReferencedRule } from '../utils'; | |||
import { searchRules } from '../../../api/rules'; | |||
import { Rule, Paging } from '../../../app/types'; | |||
import { Rule } from '../../../app/types'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
fetching: boolean; | |||
languages: string[]; | |||
loading?: boolean; | |||
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
organization: string | undefined; | |||
query: Query; | |||
referencedRules: { [ruleKey: string]: ReferencedRule }; | |||
rules: string[]; | |||
stats: { [x: string]: number } | undefined; | |||
} | |||
interface State { | |||
query: string; | |||
searching: boolean; | |||
searchResults?: Rule[]; | |||
searchPaging?: Paging; | |||
} | |||
export default class RuleFacet extends React.PureComponent<Props, State> { | |||
export default class RuleFacet extends React.PureComponent<Props> { | |||
handleSearch = (query: string, page = 1) => { | |||
const { languages, organization } = this.props; | |||
return searchRules({ | |||
@@ -63,6 +58,10 @@ export default class RuleFacet extends React.PureComponent<Props, State> { | |||
})); | |||
}; | |||
loadSearchResultCount = (rule: Rule) => { | |||
return this.props.loadSearchResultCount({ rules: [rule.key] }); | |||
}; | |||
getRuleName = (rule: string) => { | |||
const { referencedRules } = this.props; | |||
return referencedRules[rule] | |||
@@ -76,17 +75,19 @@ export default class RuleFacet extends React.PureComponent<Props, State> { | |||
render() { | |||
return ( | |||
<ListStyleFacet | |||
<ListStyleFacet<Rule> | |||
facetHeader={translate('issues.facet.rules')} | |||
fetching={this.props.fetching} | |||
getFacetItemText={this.getRuleName} | |||
getSearchResultKey={result => result.key} | |||
getSearchResultText={result => result.name} | |||
getSearchResultKey={rule => rule.key} | |||
getSearchResultText={rule => rule.name} | |||
loadSearchResultCount={this.loadSearchResultCount} | |||
onChange={this.props.onChange} | |||
onSearch={this.handleSearch} | |||
onToggle={this.props.onToggle} | |||
open={this.props.open} | |||
property="rules" | |||
query={omit(this.props.query, 'rules')} | |||
renderFacetItem={this.getRuleName} | |||
renderSearchResult={this.renderSearchResult} | |||
searchPlaceholder={translate('search.search_for_rules')} |
@@ -31,7 +31,6 @@ import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHi | |||
interface Props { | |||
fetching: boolean; | |||
loading?: boolean; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
@@ -85,7 +84,6 @@ export default class SeverityFacet extends React.PureComponent<Props> { | |||
disabled={stat === 0 && !active} | |||
halfWidth={true} | |||
key={severity} | |||
loading={this.props.loading} | |||
name={<SeverityHelper severity={severity} />} | |||
onClick={this.handleItemClick} | |||
stat={formatFacetStat(stat)} |
@@ -48,7 +48,7 @@ export interface Props { | |||
component: Component | undefined; | |||
facets: { [facet: string]: Facet }; | |||
hideAuthorFacet?: boolean; | |||
loading?: boolean; | |||
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; | |||
loadingFacets: { [key: string]: boolean }; | |||
myIssues: boolean; | |||
onFacetToggle: (property: string) => void; | |||
@@ -81,7 +81,6 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
<> | |||
<TypeFacet | |||
fetching={this.props.loadingFacets.types === true} | |||
loading={this.props.loading} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.types} | |||
@@ -90,7 +89,6 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
/> | |||
<SeverityFacet | |||
fetching={this.props.loadingFacets.severities === true} | |||
loading={this.props.loading} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.severities} | |||
@@ -99,7 +97,6 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
/> | |||
<ResolutionFacet | |||
fetching={this.props.loadingFacets.resolutions === true} | |||
loading={this.props.loading} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.resolutions} | |||
@@ -109,7 +106,6 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
/> | |||
<StatusFacet | |||
fetching={this.props.loadingFacets.statuses === true} | |||
loading={this.props.loading} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.statuses} | |||
@@ -123,7 +119,6 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
createdBefore={query.createdBefore} | |||
createdInLast={query.createdInLast} | |||
fetching={this.props.loadingFacets.createdAt === true} | |||
loading={this.props.loading} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.createdAt} | |||
@@ -133,21 +128,23 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
<LanguageFacet | |||
fetching={this.props.loadingFacets.languages === true} | |||
languages={query.languages} | |||
loading={this.props.loading} | |||
loadSearchResultCount={this.props.loadSearchResultCount} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.languages} | |||
query={query} | |||
referencedLanguages={this.props.referencedLanguages} | |||
stats={facets.languages} | |||
/> | |||
<RuleFacet | |||
fetching={this.props.loadingFacets.rules === true} | |||
languages={query.languages} | |||
loading={this.props.loading} | |||
loadSearchResultCount={this.props.loadSearchResultCount} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.rules} | |||
organization={organizationKey} | |||
query={query} | |||
referencedRules={this.props.referencedRules} | |||
rules={query.rules} | |||
stats={facets.rules} | |||
@@ -159,13 +156,14 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
fetchingCwe={this.props.loadingFacets.cwe === true} | |||
fetchingOwaspTop10={this.props.loadingFacets.owaspTop10 === true} | |||
fetchingSansTop25={this.props.loadingFacets.sansTop25 === true} | |||
loading={this.props.loading} | |||
loadSearchResultCount={this.props.loadSearchResultCount} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets[STANDARDS]} | |||
owaspTop10={query.owaspTop10} | |||
owaspTop10Open={!!openFacets.owaspTop10} | |||
owaspTop10Stats={facets.owaspTop10} | |||
query={query} | |||
sansTop25={query.sansTop25} | |||
sansTop25Open={!!openFacets.sansTop25} | |||
sansTop25Stats={facets.sansTop25} | |||
@@ -173,11 +171,12 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
<TagFacet | |||
component={component} | |||
fetching={this.props.loadingFacets.tags === true} | |||
loading={this.props.loading} | |||
loadSearchResultCount={this.props.loadSearchResultCount} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.tags} | |||
organization={organizationKey} | |||
query={query} | |||
stats={facets.tags} | |||
tags={query.tags} | |||
/> | |||
@@ -185,12 +184,13 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
<ProjectFacet | |||
component={component} | |||
fetching={this.props.loadingFacets.projects === true} | |||
loading={this.props.loading} | |||
loadSearchResultCount={this.props.loadSearchResultCount} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.projects} | |||
organization={this.props.organization} | |||
projects={query.projects} | |||
query={query} | |||
referencedComponents={this.props.referencedComponents} | |||
stats={facets.projects} | |||
/> | |||
@@ -199,11 +199,12 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
<ModuleFacet | |||
componentKey={this.props.component!.key} | |||
fetching={this.props.loadingFacets.modules === true} | |||
loading={this.props.loading} | |||
modules={query.modules} | |||
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} | |||
/> | |||
@@ -213,10 +214,11 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
componentKey={this.props.component!.key} | |||
directories={query.directories} | |||
fetching={this.props.loadingFacets.directories === true} | |||
loading={this.props.loading} | |||
loadSearchResultCount={this.props.loadSearchResultCount} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.directories} | |||
query={query} | |||
stats={facets.directories} | |||
/> | |||
)} | |||
@@ -225,10 +227,11 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
componentKey={this.props.component!.key} | |||
fetching={this.props.loadingFacets.files === true} | |||
files={query.files} | |||
loading={this.props.loading} | |||
loadSearchResultCount={this.props.loadSearchResultCount} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.files} | |||
query={query} | |||
referencedComponents={this.props.referencedComponents} | |||
stats={facets.files} | |||
/> | |||
@@ -239,11 +242,12 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
assignees={query.assignees} | |||
component={component} | |||
fetching={this.props.loadingFacets.assignees === true} | |||
loading={this.props.loading} | |||
loadSearchResultCount={this.props.loadSearchResultCount} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.assignees} | |||
organization={organizationKey} | |||
query={query} | |||
referencedUsers={this.props.referencedUsers} | |||
stats={facets.assignees} | |||
/> | |||
@@ -253,11 +257,12 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
authors={query.authors} | |||
componentKey={this.props.component && this.props.component.key} | |||
fetching={this.props.loadingFacets.authors === true} | |||
loading={this.props.loading} | |||
loadSearchResultCount={this.props.loadSearchResultCount} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.authors} | |||
organization={organizationKey} | |||
query={query} | |||
stats={facets.authors} | |||
/> | |||
)} |
@@ -18,7 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { sortBy, without } from 'lodash'; | |||
import { sortBy, without, omit } from 'lodash'; | |||
import { Query, STANDARDS, formatFacetStat } from '../utils'; | |||
import FacetBox from '../../../components/facet/FacetBox'; | |||
import FacetHeader from '../../../components/facet/FacetHeader'; | |||
@@ -33,8 +33,8 @@ import { | |||
} from '../../securityReports/utils'; | |||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint'; | |||
import SearchBox from '../../../components/controls/SearchBox'; | |||
import { highlightTerm } from '../../../helpers/search'; | |||
import ListStyleFacet from '../../../components/facet/ListStyleFacet'; | |||
export interface Props { | |||
cwe: string[]; | |||
@@ -43,13 +43,14 @@ export interface Props { | |||
fetchingOwaspTop10: boolean; | |||
fetchingSansTop25: boolean; | |||
fetchingCwe: boolean; | |||
loading?: boolean; | |||
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
owaspTop10: string[]; | |||
owaspTop10Open: boolean; | |||
owaspTop10Stats: { [x: string]: number } | undefined; | |||
query: Query; | |||
sansTop25: string[]; | |||
sansTop25Open: boolean; | |||
sansTop25Stats: { [x: string]: number } | undefined; | |||
@@ -132,10 +133,6 @@ export default class StandardFacet extends React.PureComponent<Props, State> { | |||
this.props.onToggle('sansTop25'); | |||
}; | |||
handleCWEHeaderClick = () => { | |||
this.props.onToggle('cwe'); | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ [this.property]: [], owaspTop10: [], sansTop25: [], cwe: [] }); | |||
}; | |||
@@ -158,20 +155,22 @@ export default class StandardFacet extends React.PureComponent<Props, State> { | |||
this.handleItemClick('owaspTop10', itemValue, multiple); | |||
}; | |||
handleCWEItemClick = (itemValue: string, multiple: boolean) => { | |||
this.handleItemClick('cwe', itemValue, multiple); | |||
}; | |||
handleSansTop25ItemClick = (itemValue: string, multiple: boolean) => { | |||
this.handleItemClick('sansTop25', itemValue, multiple); | |||
}; | |||
handleCWESelect = ({ value }: { value: string }) => { | |||
this.handleItemClick('cwe', value, true); | |||
handleCWESearch = (query: string) => { | |||
return Promise.resolve({ | |||
results: Object.keys(this.state.standards.cwe).filter(cwe => | |||
renderCWECategory(this.state.standards, cwe) | |||
.toLowerCase() | |||
.includes(query.toLowerCase()) | |||
) | |||
}); | |||
}; | |||
handleCWESearch = (query: string) => { | |||
this.setState({ cweQuery: query }); | |||
loadCWESearchResultCount = (category: string) => { | |||
return this.props.loadSearchResultCount({ cwe: [category] }); | |||
}; | |||
renderList = ( | |||
@@ -216,7 +215,6 @@ export default class StandardFacet extends React.PureComponent<Props, State> { | |||
<FacetItem | |||
active={values.includes(category)} | |||
key={category} | |||
loading={this.props.loading} | |||
name={renderName(this.state.standards, category)} | |||
onClick={onClick} | |||
stat={formatFacetStat(getStat(category))} | |||
@@ -247,45 +245,6 @@ export default class StandardFacet extends React.PureComponent<Props, State> { | |||
return this.renderHint('owaspTop10Stats', 'owaspTop10'); | |||
} | |||
renderCWEList() { | |||
const { cweQuery } = this.state; | |||
if (cweQuery) { | |||
const results = Object.keys(this.state.standards.cwe).filter(cwe => | |||
renderCWECategory(this.state.standards, cwe) | |||
.toLowerCase() | |||
.includes(cweQuery.toLowerCase()) | |||
); | |||
return this.renderFacetItemsList( | |||
this.props.cweStats, | |||
this.props.cwe, | |||
results, | |||
(standards: Standards, category: string) => | |||
highlightTerm(renderCWECategory(standards, category), cweQuery), | |||
renderCWECategory, | |||
this.handleCWEItemClick | |||
); | |||
} else { | |||
return this.renderList('cweStats', 'cwe', renderCWECategory, this.handleCWEItemClick); | |||
} | |||
} | |||
renderCWESearch() { | |||
return ( | |||
<SearchBox | |||
autoFocus={true} | |||
className="little-spacer-top spacer-bottom" | |||
onChange={this.handleCWESearch} | |||
placeholder={translate('search.search_for_cwe')} | |||
value={this.state.cweQuery} | |||
/> | |||
); | |||
} | |||
renderCWEHint() { | |||
return this.renderHint('cweStats', 'cwe'); | |||
} | |||
renderSansTop25List() { | |||
return this.renderList( | |||
'sansTop25Stats', | |||
@@ -336,22 +295,28 @@ export default class StandardFacet extends React.PureComponent<Props, State> { | |||
</> | |||
)} | |||
</FacetBox> | |||
<FacetBox className="is-inner" property="cwe"> | |||
<FacetHeader | |||
name={translate('issues.facet.cwe')} | |||
onClick={this.handleCWEHeaderClick} | |||
open={this.props.cweOpen} | |||
values={this.props.cwe.map(item => renderCWECategory(this.state.standards, item))} | |||
/> | |||
<DeferredSpinner loading={this.props.fetchingCwe} /> | |||
{this.props.cweOpen && ( | |||
<> | |||
{this.renderCWESearch()} | |||
{this.renderCWEList()} | |||
{this.renderCWEHint()} | |||
</> | |||
)} | |||
</FacetBox> | |||
<ListStyleFacet<string> | |||
className="is-inner" | |||
facetHeader={translate('issues.facet.cwe')} | |||
fetching={this.props.fetchingCwe} | |||
getFacetItemText={item => renderCWECategory(this.state.standards, item)} | |||
getSearchResultKey={item => item} | |||
getSearchResultText={item => renderCWECategory(this.state.standards, item)} | |||
loadSearchResultCount={this.loadCWESearchResultCount} | |||
onChange={this.props.onChange} | |||
onSearch={this.handleCWESearch} | |||
onToggle={this.props.onToggle} | |||
open={this.props.cweOpen} | |||
property="cwe" | |||
query={omit(this.props.query, 'cwe')} | |||
renderFacetItem={item => renderCWECategory(this.state.standards, item)} | |||
renderSearchResult={(item, query) => | |||
highlightTerm(renderCWECategory(this.state.standards, item), query) | |||
} | |||
searchPlaceholder={translate('search.search_for_cwe')} | |||
stats={this.props.cweStats} | |||
values={this.props.cwe} | |||
/> | |||
</> | |||
); | |||
} |
@@ -31,7 +31,6 @@ import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHi | |||
interface Props { | |||
fetching: boolean; | |||
loading?: boolean; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
@@ -85,7 +84,6 @@ export default class StatusFacet extends React.PureComponent<Props> { | |||
disabled={stat === 0 && !active} | |||
halfWidth={true} | |||
key={status} | |||
loading={this.props.loading} | |||
name={<StatusHelper resolution={undefined} status={status} />} | |||
onClick={this.handleItemClick} | |||
stat={formatFacetStat(stat)} |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { omit } from 'lodash'; | |||
import { Query } from '../utils'; | |||
import { searchIssueTags } from '../../../api/issues'; | |||
import * as theme from '../../../app/theme'; | |||
@@ -30,11 +31,12 @@ import { highlightTerm } from '../../../helpers/search'; | |||
interface Props { | |||
component: Component | undefined; | |||
fetching: boolean; | |||
loading?: boolean; | |||
loadSearchResultCount: (changes: Partial<Query>) => Promise<number>; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
organization: string | undefined; | |||
query: Query; | |||
stats: { [x: string]: number } | undefined; | |||
tags: string[]; | |||
} | |||
@@ -54,6 +56,10 @@ export default class TagFacet extends React.PureComponent<Props> { | |||
return tag; | |||
}; | |||
loadSearchResultCount = (tag: string) => { | |||
return this.props.loadSearchResultCount({ tags: [tag] }); | |||
}; | |||
renderTag = (tag: string) => { | |||
return ( | |||
<> | |||
@@ -72,17 +78,19 @@ export default class TagFacet extends React.PureComponent<Props> { | |||
render() { | |||
return ( | |||
<ListStyleFacet | |||
<ListStyleFacet<string> | |||
facetHeader={translate('issues.facet.tags')} | |||
fetching={this.props.fetching} | |||
getFacetItemText={this.getTagName} | |||
getSearchResultKey={tag => tag} | |||
getSearchResultText={tag => tag} | |||
loadSearchResultCount={this.loadSearchResultCount} | |||
onChange={this.props.onChange} | |||
onSearch={this.handleSearch} | |||
onToggle={this.props.onToggle} | |||
open={this.props.open} | |||
property="tags" | |||
query={omit(this.props.query, 'tags')} | |||
renderFacetItem={this.renderTag} | |||
renderSearchResult={this.renderSearchResult} | |||
searchPlaceholder={translate('search.search_for_tags')} |
@@ -31,7 +31,6 @@ import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHi | |||
interface Props { | |||
fetching: boolean; | |||
loading?: boolean; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
@@ -94,7 +93,6 @@ export default class TypeFacet extends React.PureComponent<Props> { | |||
active={active} | |||
disabled={stat === 0 && !active} | |||
key={type} | |||
loading={this.props.loading} | |||
name={ | |||
<span> | |||
<IssueTypeIcon query={type} /> {translate('issue.type', type)} |
@@ -20,56 +20,26 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import AssigneeFacet, { Props } from '../AssigneeFacet'; | |||
import { Query } from '../../utils'; | |||
jest.mock('../../../../store/rootReducer', () => ({})); | |||
const renderAssigneeFacet = (props?: Partial<Props>) => | |||
shallow( | |||
<AssigneeFacet | |||
assigned={true} | |||
assignees={[]} | |||
component={undefined} | |||
fetching={false} | |||
onChange={jest.fn()} | |||
onToggle={jest.fn()} | |||
open={true} | |||
organization={undefined} | |||
referencedUsers={{ foo: { avatar: 'avatart-foo', name: 'name-foo' } }} | |||
stats={{ '': 5, foo: 13, bar: 7, baz: 6 }} | |||
{...props} | |||
/> | |||
); | |||
it('should render', () => { | |||
expect(renderAssigneeFacet()).toMatchSnapshot(); | |||
}); | |||
it('should render without stats', () => { | |||
expect(renderAssigneeFacet({ stats: undefined })).toMatchSnapshot(); | |||
}); | |||
it('should select unassigned', () => { | |||
expect(renderAssigneeFacet({ assigned: false })).toMatchSnapshot(); | |||
}); | |||
it('should select user', () => { | |||
expect(renderAssigneeFacet({ assignees: ['foo'] })).toMatchSnapshot(); | |||
}); | |||
it('should render footer select option', () => { | |||
const wrapper = renderAssigneeFacet(); | |||
it('should select unassigned', () => { | |||
expect( | |||
(wrapper.instance() as AssigneeFacet).renderOption({ avatar: 'avatar-foo', label: 'name-foo' }) | |||
).toMatchSnapshot(); | |||
renderAssigneeFacet({ assigned: false }) | |||
.find('ListStyleFacet') | |||
.prop('values') | |||
).toEqual(['']); | |||
}); | |||
it('should call onChange', () => { | |||
const onChange = jest.fn(); | |||
const wrapper = renderAssigneeFacet({ assignees: ['foo'], onChange }); | |||
const itemOnClick = wrapper | |||
.find('FacetItem') | |||
.first() | |||
.prop<Function>('onClick'); | |||
const itemOnClick = wrapper.find('ListStyleFacet').prop<Function>('onItemClick'); | |||
itemOnClick(''); | |||
expect(onChange).lastCalledWith({ assigned: false, assignees: [] }); | |||
@@ -81,11 +51,22 @@ it('should call onChange', () => { | |||
expect(onChange).lastCalledWith({ assigned: true, assignees: ['baz', 'foo'] }); | |||
}); | |||
it('should call onToggle', () => { | |||
const onToggle = jest.fn(); | |||
const wrapper = renderAssigneeFacet({ onToggle }); | |||
const headerOnClick = wrapper.find('FacetHeader').prop<Function>('onClick'); | |||
headerOnClick(); | |||
expect(onToggle).lastCalledWith('assignees'); | |||
}); | |||
function renderAssigneeFacet(props?: Partial<Props>) { | |||
return shallow( | |||
<AssigneeFacet | |||
assigned={true} | |||
assignees={[]} | |||
component={undefined} | |||
fetching={false} | |||
loadSearchResultCount={jest.fn()} | |||
onChange={jest.fn()} | |||
onToggle={jest.fn()} | |||
open={true} | |||
organization={undefined} | |||
query={{} as Query} | |||
referencedUsers={{ foo: { avatar: 'avatart-foo', name: 'name-foo' } }} | |||
stats={{ '': 5, foo: 13, bar: 7, baz: 6 }} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -29,6 +29,7 @@ const renderSidebar = (props?: Partial<Props>) => | |||
<Sidebar | |||
component={undefined} | |||
facets={{}} | |||
loadSearchResultCount={jest.fn()} | |||
loadingFacets={{}} | |||
myIssues={false} | |||
onFacetToggle={jest.fn()} |
@@ -21,6 +21,7 @@ import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import StandardFacet, { Props } from '../StandardFacet'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
import { Query } from '../../utils'; | |||
it('should render closed', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
@@ -84,7 +85,6 @@ it('should select items', () => { | |||
selectAndCheck('owaspTop10', 'a1'); | |||
selectAndCheck('owaspTop10', 'a1', true, ['a1', 'a3']); | |||
selectAndCheck('sansTop25', 'foo'); | |||
selectAndCheck('cwe', '173'); | |||
function selectAndCheck(facet: string, value: string, multiple = false, expectedValue = [value]) { | |||
wrapper | |||
@@ -100,8 +100,6 @@ it('should toggle sub-facets', () => { | |||
const wrapper = shallowRender({ onToggle, open: true }); | |||
click(wrapper.find('FacetBox[property="owaspTop10"]').children('FacetHeader')); | |||
expect(onToggle).lastCalledWith('owaspTop10'); | |||
click(wrapper.find('FacetBox[property="cwe"]').children('FacetHeader')); | |||
expect(onToggle).lastCalledWith('cwe'); | |||
click(wrapper.find('FacetBox[property="sansTop25"]').children('FacetHeader')); | |||
expect(onToggle).lastCalledWith('sansTop25'); | |||
}); | |||
@@ -124,7 +122,6 @@ it('should display correct selection', () => { | |||
'Unknown CWE' | |||
]); | |||
checkValues('owaspTop10', ['A1 - a1 title', 'A3', 'Not OWAPS']); | |||
checkValues('cwe', ['CWE-42 - cwe-42 title', 'CWE-1111', 'Unknown CWE']); | |||
checkValues('sansTop25', ['Risky Resource Management', 'foo']); | |||
function checkValues(property: string, values: string[]) { | |||
@@ -137,16 +134,6 @@ it('should display correct selection', () => { | |||
} | |||
}); | |||
it('should search CWE', () => { | |||
const wrapper = shallowRender({ open: true, cwe: ['42'], cweOpen: true }); | |||
wrapper | |||
.find('FacetBox[property="cwe"]') | |||
.find('SearchBox') | |||
.prop<Function>('onChange')('unkn'); | |||
wrapper.update(); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<Props> = {}) { | |||
const wrapper = shallow( | |||
<StandardFacet | |||
@@ -156,12 +143,14 @@ function shallowRender(props: Partial<Props> = {}) { | |||
fetchingCwe={false} | |||
fetchingOwaspTop10={false} | |||
fetchingSansTop25={false} | |||
loadSearchResultCount={jest.fn()} | |||
onChange={jest.fn()} | |||
onToggle={jest.fn()} | |||
open={false} | |||
owaspTop10={[]} | |||
owaspTop10Open={false} | |||
owaspTop10Stats={{}} | |||
query={{} as Query} | |||
sansTop25={[]} | |||
sansTop25Open={false} | |||
sansTop25Stats={{}} |
@@ -1,321 +1,39 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render 1`] = ` | |||
<FacetBox | |||
<ListStyleFacet | |||
facetHeader="issues.facet.assignees" | |||
fetching={false} | |||
getFacetItemText={[Function]} | |||
getSearchResultKey={[Function]} | |||
getSearchResultText={[Function]} | |||
getSortedItems={[Function]} | |||
loadSearchResultCount={[Function]} | |||
maxInitialItems={15} | |||
maxItems={100} | |||
onChange={[MockFunction]} | |||
onClear={[Function]} | |||
onItemClick={[Function]} | |||
onSearch={[Function]} | |||
onToggle={[MockFunction]} | |||
open={true} | |||
property="assignees" | |||
> | |||
<FacetHeader | |||
name="issues.facet.assignees" | |||
onClear={[Function]} | |||
onClick={[Function]} | |||
open={true} | |||
values={Array []} | |||
/> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
/> | |||
<React.Fragment> | |||
<SearchBox | |||
autoFocus={true} | |||
className="little-spacer-top spacer-bottom" | |||
loading={false} | |||
minLength={2} | |||
onChange={[Function]} | |||
placeholder="search.search_for_users" | |||
value="" | |||
/> | |||
<FacetItemsList> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={false} | |||
loading={false} | |||
name="unassigned" | |||
onClick={[Function]} | |||
stat="5" | |||
tooltip="unassigned" | |||
value="" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={false} | |||
key="foo" | |||
loading={false} | |||
name={ | |||
<span> | |||
<Connect(Avatar) | |||
className="little-spacer-right" | |||
hash="avatart-foo" | |||
name="name-foo" | |||
size={16} | |||
/> | |||
name-foo | |||
</span> | |||
} | |||
onClick={[Function]} | |||
stat="13" | |||
tooltip="name-foo" | |||
value="foo" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={false} | |||
key="bar" | |||
loading={false} | |||
name="bar" | |||
onClick={[Function]} | |||
stat="7" | |||
tooltip="bar" | |||
value="bar" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={false} | |||
key="baz" | |||
loading={false} | |||
name="baz" | |||
onClick={[Function]} | |||
stat="6" | |||
tooltip="baz" | |||
value="baz" | |||
/> | |||
</FacetItemsList> | |||
<MultipleSelectionHint | |||
options={4} | |||
values={0} | |||
/> | |||
</React.Fragment> | |||
</FacetBox> | |||
`; | |||
exports[`should render footer select option 1`] = ` | |||
<span> | |||
<Connect(Avatar) | |||
className="little-spacer-right" | |||
hash="avatar-foo" | |||
name="name-foo" | |||
size={16} | |||
/> | |||
name-foo | |||
</span> | |||
`; | |||
exports[`should render without stats 1`] = ` | |||
<FacetBox | |||
property="assignees" | |||
> | |||
<FacetHeader | |||
name="issues.facet.assignees" | |||
onClear={[Function]} | |||
onClick={[Function]} | |||
open={true} | |||
values={Array []} | |||
/> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
/> | |||
<React.Fragment> | |||
<MultipleSelectionHint | |||
options={0} | |||
values={0} | |||
/> | |||
</React.Fragment> | |||
</FacetBox> | |||
`; | |||
exports[`should select unassigned 1`] = ` | |||
<FacetBox | |||
property="assignees" | |||
> | |||
<FacetHeader | |||
name="issues.facet.assignees" | |||
onClear={[Function]} | |||
onClick={[Function]} | |||
open={true} | |||
values={ | |||
Array [ | |||
"unassigned", | |||
] | |||
} | |||
/> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
/> | |||
<React.Fragment> | |||
<SearchBox | |||
autoFocus={true} | |||
className="little-spacer-top spacer-bottom" | |||
loading={false} | |||
minLength={2} | |||
onChange={[Function]} | |||
placeholder="search.search_for_users" | |||
value="" | |||
/> | |||
<FacetItemsList> | |||
<FacetItem | |||
active={true} | |||
disabled={false} | |||
halfWidth={false} | |||
loading={false} | |||
name="unassigned" | |||
onClick={[Function]} | |||
stat="5" | |||
tooltip="unassigned" | |||
value="" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={false} | |||
key="foo" | |||
loading={false} | |||
name={ | |||
<span> | |||
<Connect(Avatar) | |||
className="little-spacer-right" | |||
hash="avatart-foo" | |||
name="name-foo" | |||
size={16} | |||
/> | |||
name-foo | |||
</span> | |||
} | |||
onClick={[Function]} | |||
stat="13" | |||
tooltip="name-foo" | |||
value="foo" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={false} | |||
key="bar" | |||
loading={false} | |||
name="bar" | |||
onClick={[Function]} | |||
stat="7" | |||
tooltip="bar" | |||
value="bar" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={false} | |||
key="baz" | |||
loading={false} | |||
name="baz" | |||
onClick={[Function]} | |||
stat="6" | |||
tooltip="baz" | |||
value="baz" | |||
/> | |||
</FacetItemsList> | |||
<MultipleSelectionHint | |||
options={4} | |||
values={0} | |||
/> | |||
</React.Fragment> | |||
</FacetBox> | |||
`; | |||
exports[`should select user 1`] = ` | |||
<FacetBox | |||
property="assignees" | |||
> | |||
<FacetHeader | |||
name="issues.facet.assignees" | |||
onClear={[Function]} | |||
onClick={[Function]} | |||
open={true} | |||
values={ | |||
Array [ | |||
"name-foo", | |||
] | |||
query={Object {}} | |||
renderFacetItem={[Function]} | |||
renderSearchResult={[Function]} | |||
searchPlaceholder="search.search_for_users" | |||
stats={ | |||
Object { | |||
"": 5, | |||
"bar": 7, | |||
"baz": 6, | |||
"foo": 13, | |||
} | |||
/> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
/> | |||
<React.Fragment> | |||
<SearchBox | |||
autoFocus={true} | |||
className="little-spacer-top spacer-bottom" | |||
loading={false} | |||
minLength={2} | |||
onChange={[Function]} | |||
placeholder="search.search_for_users" | |||
value="" | |||
/> | |||
<FacetItemsList> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={false} | |||
loading={false} | |||
name="unassigned" | |||
onClick={[Function]} | |||
stat="5" | |||
tooltip="unassigned" | |||
value="" | |||
/> | |||
<FacetItem | |||
active={true} | |||
disabled={false} | |||
halfWidth={false} | |||
key="foo" | |||
loading={false} | |||
name={ | |||
<span> | |||
<Connect(Avatar) | |||
className="little-spacer-right" | |||
hash="avatart-foo" | |||
name="name-foo" | |||
size={16} | |||
/> | |||
name-foo | |||
</span> | |||
} | |||
onClick={[Function]} | |||
stat="13" | |||
tooltip="name-foo" | |||
value="foo" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={false} | |||
key="bar" | |||
loading={false} | |||
name="bar" | |||
onClick={[Function]} | |||
stat="7" | |||
tooltip="bar" | |||
value="bar" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={false} | |||
key="baz" | |||
loading={false} | |||
name="baz" | |||
onClick={[Function]} | |||
stat="6" | |||
tooltip="baz" | |||
value="baz" | |||
/> | |||
</FacetItemsList> | |||
<MultipleSelectionHint | |||
options={4} | |||
values={1} | |||
/> | |||
</React.Fragment> | |||
</FacetBox> | |||
} | |||
values={ | |||
Array [ | |||
"foo", | |||
] | |||
} | |||
/> | |||
`; |
@@ -163,166 +163,37 @@ exports[`should render sub-facets 1`] = ` | |||
/> | |||
</React.Fragment> | |||
</FacetBox> | |||
<FacetBox | |||
className="is-inner" | |||
property="cwe" | |||
> | |||
<FacetHeader | |||
name="issues.facet.cwe" | |||
onClick={[Function]} | |||
open={true} | |||
values={ | |||
Array [ | |||
"CWE-42 - cwe-42 title", | |||
] | |||
} | |||
/> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
/> | |||
<React.Fragment> | |||
<SearchBox | |||
autoFocus={true} | |||
className="little-spacer-top spacer-bottom" | |||
onChange={[Function]} | |||
placeholder="search.search_for_cwe" | |||
value="" | |||
/> | |||
<FacetItemsList> | |||
<FacetItem | |||
active={true} | |||
disabled={false} | |||
halfWidth={false} | |||
key="42" | |||
loading={false} | |||
name="CWE-42 - cwe-42 title" | |||
onClick={[Function]} | |||
stat="5" | |||
tooltip="CWE-42 - cwe-42 title" | |||
value="42" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={false} | |||
key="173" | |||
loading={false} | |||
name="CWE-173" | |||
onClick={[Function]} | |||
stat="3" | |||
tooltip="CWE-173" | |||
value="173" | |||
/> | |||
</FacetItemsList> | |||
<MultipleSelectionHint | |||
options={2} | |||
values={1} | |||
/> | |||
</React.Fragment> | |||
</FacetBox> | |||
</React.Fragment> | |||
</FacetBox> | |||
`; | |||
exports[`should search CWE 1`] = ` | |||
<FacetBox | |||
property="standards" | |||
> | |||
<FacetHeader | |||
name="issues.facet.standards" | |||
onClear={[Function]} | |||
onClick={[Function]} | |||
open={true} | |||
values={ | |||
Array [ | |||
"CWE-42 - cwe-42 title", | |||
] | |||
} | |||
/> | |||
<React.Fragment> | |||
<FacetBox | |||
className="is-inner" | |||
property="owaspTop10" | |||
> | |||
<FacetHeader | |||
name="issues.facet.owaspTop10" | |||
onClick={[Function]} | |||
open={false} | |||
values={Array []} | |||
/> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
/> | |||
</FacetBox> | |||
<FacetBox | |||
className="is-inner" | |||
property="sansTop25" | |||
> | |||
<FacetHeader | |||
name="issues.facet.sansTop25" | |||
onClick={[Function]} | |||
open={false} | |||
values={Array []} | |||
/> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
/> | |||
</FacetBox> | |||
<FacetBox | |||
<ListStyleFacet | |||
className="is-inner" | |||
facetHeader="issues.facet.cwe" | |||
fetching={false} | |||
getFacetItemText={[Function]} | |||
getSearchResultKey={[Function]} | |||
getSearchResultText={[Function]} | |||
loadSearchResultCount={[Function]} | |||
maxInitialItems={15} | |||
maxItems={100} | |||
onChange={[MockFunction]} | |||
onSearch={[Function]} | |||
onToggle={[MockFunction]} | |||
open={true} | |||
property="cwe" | |||
> | |||
<FacetHeader | |||
name="issues.facet.cwe" | |||
onClick={[Function]} | |||
open={true} | |||
values={ | |||
Array [ | |||
"CWE-42 - cwe-42 title", | |||
] | |||
query={Object {}} | |||
renderFacetItem={[Function]} | |||
renderSearchResult={[Function]} | |||
searchPlaceholder="search.search_for_cwe" | |||
stats={ | |||
Object { | |||
"173": 3, | |||
"42": 5, | |||
} | |||
/> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
/> | |||
<React.Fragment> | |||
<SearchBox | |||
autoFocus={true} | |||
className="little-spacer-top spacer-bottom" | |||
onChange={[Function]} | |||
placeholder="search.search_for_cwe" | |||
value="unkn" | |||
/> | |||
<FacetItemsList> | |||
<FacetItem | |||
active={false} | |||
disabled={false} | |||
halfWidth={false} | |||
key="unknown" | |||
loading={false} | |||
name={ | |||
<React.Fragment> | |||
<mark> | |||
Unkn | |||
</mark> | |||
own CWE | |||
</React.Fragment> | |||
} | |||
onClick={[Function]} | |||
tooltip="Unknown CWE" | |||
value="unknown" | |||
/> | |||
</FacetItemsList> | |||
<MultipleSelectionHint | |||
options={0} | |||
values={1} | |||
/> | |||
</React.Fragment> | |||
</FacetBox> | |||
} | |||
values={ | |||
Array [ | |||
"42", | |||
] | |||
} | |||
/> | |||
</React.Fragment> | |||
</FacetBox> | |||
`; |
@@ -0,0 +1,84 @@ | |||
/* | |||
* 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,0 +1,88 @@ | |||
/* | |||
* 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,6 +19,7 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import DeferredSpinner from '../common/DeferredSpinner'; | |||
export interface Props { | |||
active?: boolean; | |||
@@ -47,6 +48,22 @@ export default class FacetItem extends React.PureComponent<Props> { | |||
this.props.onClick(this.props.value, event.ctrlKey || event.metaKey); | |||
}; | |||
renderValue() { | |||
if (this.props.loading) { | |||
return ( | |||
<span className="facet-stat"> | |||
<DeferredSpinner /> | |||
</span> | |||
); | |||
} | |||
if (this.props.stat == null) { | |||
return null; | |||
} | |||
return <span className="facet-stat">{this.props.stat}</span>; | |||
} | |||
render() { | |||
const { name } = this.props; | |||
const className = classNames('search-navigator-facet', this.props.className, { | |||
@@ -57,7 +74,7 @@ export default class FacetItem extends React.PureComponent<Props> { | |||
return this.props.disabled ? ( | |||
<span className={className} data-facet={this.props.value} title={this.props.tooltip}> | |||
<span className="facet-name">{name}</span> | |||
{this.props.stat != null && <span className="facet-stat">{this.props.stat}</span>} | |||
{this.renderValue()} | |||
</span> | |||
) : ( | |||
<a | |||
@@ -67,7 +84,7 @@ export default class FacetItem extends React.PureComponent<Props> { | |||
onClick={this.handleClick} | |||
title={this.props.tooltip}> | |||
<span className="facet-name">{name}</span> | |||
{this.props.stat != null && <span className="facet-stat">{this.props.stat}</span>} | |||
{this.renderValue()} | |||
</a> | |||
); | |||
} |
@@ -0,0 +1,39 @@ | |||
/* | |||
* 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,6 +19,7 @@ | |||
*/ | |||
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'; | |||
@@ -31,18 +32,24 @@ 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'; | |||
export interface Props<S> { | |||
className?: string; | |||
facetHeader: string; | |||
fetching: boolean; | |||
getFacetItemText: (item: string) => string; | |||
getSearchResultKey: (result: S) => string; | |||
getSearchResultText: (result: S) => string; | |||
loading?: boolean; | |||
loadSearchResultCount?: (result: S) => Promise<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 | |||
@@ -50,9 +57,11 @@ export interface Props<S> { | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
property: string; | |||
query?: RawQuery; | |||
renderFacetItem: (item: string) => React.ReactNode; | |||
renderSearchResult: (result: S, query: string) => React.ReactNode; | |||
searchPlaceholder: string; | |||
getSortedItems?: () => string[]; | |||
stats: { [x: string]: number } | undefined; | |||
values: string[]; | |||
} | |||
@@ -64,6 +73,8 @@ interface State<S> { | |||
searchMaxResults?: boolean; | |||
searchPaging?: Paging; | |||
searchResults?: S[]; | |||
searchResultsCounts: { [key: string]: number }; | |||
searchResultsCountLoading: { [key: string]: boolean }; | |||
showFullList: boolean; | |||
} | |||
@@ -79,6 +90,8 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S | |||
autoFocus: false, | |||
query: '', | |||
searching: false, | |||
searchResultsCounts: {}, | |||
searchResultsCountLoading: {}, | |||
showFullList: false | |||
}; | |||
@@ -87,16 +100,31 @@ 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 }); | |||
} else if (prevProps.open && !this.props.open) { | |||
// reset state when closing the facet | |||
} else if ( | |||
(prevProps.open && !this.props.open) || | |||
!queriesEqual(prevProps.query || {}, this.props.query || {}) | |||
) { | |||
// reset state when closing the facet, or when query changes | |||
this.setState({ | |||
query: '', | |||
searchMaxResults: undefined, | |||
searchResults: undefined, | |||
searching: false, | |||
searchResultsCounts: {}, | |||
searchResultsCountLoading: {}, | |||
showFullList: false | |||
}); | |||
} else if ( | |||
@@ -113,16 +141,20 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S | |||
} | |||
handleItemClick = (itemValue: string, multiple: boolean) => { | |||
const { values } = this.props; | |||
if (multiple) { | |||
const newValue = sortBy( | |||
values.includes(itemValue) ? without(values, itemValue) : [...values, itemValue] | |||
); | |||
this.props.onChange({ [this.props.property]: newValue }); | |||
if (this.props.onItemClick) { | |||
this.props.onItemClick(itemValue, multiple); | |||
} else { | |||
this.props.onChange({ | |||
[this.props.property]: values.includes(itemValue) && values.length < 2 ? [] : [itemValue] | |||
}); | |||
const { values } = this.props; | |||
if (multiple) { | |||
const newValue = sortBy( | |||
values.includes(itemValue) ? without(values, itemValue) : [...values, itemValue] | |||
); | |||
this.props.onChange({ [this.props.property]: newValue }); | |||
} else { | |||
this.props.onChange({ | |||
[this.props.property]: values.includes(itemValue) && values.length < 2 ? [] : [itemValue] | |||
}); | |||
} | |||
} | |||
}; | |||
@@ -131,7 +163,9 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ [this.props.property]: [] }); | |||
if (this.props.onClear) { | |||
this.props.onClear(); | |||
} else this.props.onChange({ [this.props.property]: [] }); | |||
}; | |||
stopSearching = () => { | |||
@@ -174,10 +208,50 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S | |||
} | |||
}; | |||
getStat(item: string, zeroIfAbsent = false) { | |||
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 | |||
} | |||
})); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState(state => ({ | |||
searchResultsCountLoading: { | |||
...state.searchResultsCountLoading, | |||
[this.props.getSearchResultKey(result)]: false | |||
} | |||
})); | |||
} | |||
} | |||
); | |||
} | |||
}; | |||
getStat(item: string) { | |||
const { stats } = this.props; | |||
const defaultValue = zeroIfAbsent ? 0 : undefined; | |||
return stats && stats[item] !== undefined ? stats && stats[item] : defaultValue; | |||
return stats && stats[item] !== undefined ? stats && stats[item] : undefined; | |||
} | |||
showFullList = () => { | |||
@@ -204,11 +278,9 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S | |||
return null; | |||
} | |||
const sortedItems = sortBy( | |||
Object.keys(stats), | |||
key => -stats[key], | |||
key => this.props.getFacetItemText(key) | |||
); | |||
const sortedItems = this.props.getSortedItems | |||
? this.props.getSortedItems() | |||
: sortBy(Object.keys(stats), key => -stats[key], key => this.props.getFacetItemText(key)); | |||
// limit the number of items to this.props.maxInitialItems, | |||
// but make sure all (in other words, the last) selected items are displayed | |||
@@ -227,7 +299,6 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S | |||
<FacetItem | |||
active={this.props.values.includes(item)} | |||
key={item} | |||
loading={this.props.loading} | |||
name={this.props.renderFacetItem(item)} | |||
onClick={this.handleItemClick} | |||
stat={formatFacetStat(this.getStat(item))} | |||
@@ -313,14 +384,28 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S | |||
// default to 0 if we're sure there are not more results | |||
const isFacetExhaustive = Object.keys(this.props.stats || {}).length < this.props.maxItems!; | |||
const stat = this.getStat(key, isFacetExhaustive); | |||
return ( | |||
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 = ( | |||
<FacetItem | |||
active={active} | |||
disabled={!active && stat === 0} | |||
key={key} | |||
loading={this.props.loading} | |||
className={classNames({ 'list-style-facet-mouse-over-animation': canBeLoaded })} | |||
disabled={disabled} | |||
loading={loading} | |||
name={this.props.renderSearchResult(result, this.state.query)} | |||
onClick={this.handleItemClick} | |||
stat={formatFacetStat(stat)} | |||
@@ -328,13 +413,29 @@ 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() { | |||
const { stats = {} } = this.props; | |||
const { query, searching, searchResults } = this.state; | |||
const values = this.props.values.map(item => this.props.getFacetItemText(item)); | |||
const loadingResults = | |||
query !== '' && searching && (searchResults === undefined || searchResults.length === 0); | |||
const showList = !query || loadingResults; | |||
return ( | |||
<FacetBox property={this.props.property}> | |||
<FacetBox className={this.props.className} property={this.props.property}> | |||
<FacetHeader | |||
name={this.props.facetHeader} | |||
onClear={this.handleClear} | |||
@@ -347,9 +448,7 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S | |||
{this.props.open && ( | |||
<> | |||
{this.renderSearch()} | |||
{this.state.query && this.state.searchResults !== undefined | |||
? this.renderSearchResults() | |||
: this.renderList()} | |||
{showList ? this.renderList() : this.renderSearchResults()} | |||
<MultipleSelectionHint options={Object.keys(stats).length} values={values.length} /> | |||
</> | |||
)} |
@@ -34,10 +34,6 @@ it('should render stat', () => { | |||
expect(renderFacetItem({ stat: '13' })).toMatchSnapshot(); | |||
}); | |||
it('should loading stat', () => { | |||
expect(renderFacetItem({ loading: true })).toMatchSnapshot(); | |||
}); | |||
it('should render disabled', () => { | |||
expect(renderFacetItem({ disabled: true })).toMatchSnapshot(); | |||
}); |
@@ -18,7 +18,7 @@ | |||
* 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 ListStyleFacet, { Props } from '../ListStyleFacet'; | |||
import { waitAndUpdate } from '../../../helpers/testUtils'; | |||
@@ -146,10 +146,14 @@ it('should reset state when closes', () => { | |||
}); | |||
wrapper.setProps({ open: false }); | |||
expect(wrapper.state('query')).toBe(''); | |||
expect(wrapper.state('searchResults')).toBe(undefined); | |||
expect(wrapper.state('searching')).toBe(false); | |||
expect(wrapper.state('showFullList')).toBe(false); | |||
checkInitialState(wrapper); | |||
}); | |||
it('should reset search when query changes', () => { | |||
const wrapper = shallowRender({ query: { a: ['foo'] } }); | |||
wrapper.setState({ query: 'foo', searchResults: ['foo'], searchResultsCounts: { foo: 3 } }); | |||
wrapper.setProps({ query: { a: ['foo'], b: ['bar'] } }); | |||
checkInitialState(wrapper); | |||
}); | |||
it('should collapse list when new stats have few results', () => { | |||
@@ -160,6 +164,33 @@ 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 | |||
@@ -186,3 +217,12 @@ function shallowRender(props: Partial<Props<string>> = {}) { | |||
function identity(str: string) { | |||
return str; | |||
} | |||
function checkInitialState(wrapper: ShallowWrapper) { | |||
expect(wrapper.state('query')).toBe(''); | |||
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); | |||
} |
@@ -1,21 +1,5 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should loading stat 1`] = ` | |||
<a | |||
className="search-navigator-facet" | |||
data-facet="bar" | |||
href="#" | |||
onClick={[Function]} | |||
title="foo" | |||
> | |||
<span | |||
className="facet-name" | |||
> | |||
foo | |||
</span> | |||
</a> | |||
`; | |||
exports[`should render active 1`] = ` | |||
<a | |||
className="search-navigator-facet active" |
@@ -105,30 +105,38 @@ exports[`should search 1`] = ` | |||
/> | |||
<React.Fragment> | |||
<FacetItemsList> | |||
<FacetItem | |||
active={false} | |||
disabled={true} | |||
halfWidth={false} | |||
<React.Fragment | |||
key="d" | |||
loading={false} | |||
name="d" | |||
onClick={[Function]} | |||
stat={0} | |||
tooltip="d" | |||
value="d" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={true} | |||
halfWidth={false} | |||
> | |||
<FacetItem | |||
active={false} | |||
className="" | |||
disabled={true} | |||
halfWidth={false} | |||
loading={false} | |||
name="d" | |||
onClick={[Function]} | |||
stat={0} | |||
tooltip="d" | |||
value="d" | |||
/> | |||
</React.Fragment> | |||
<React.Fragment | |||
key="e" | |||
loading={false} | |||
name="e" | |||
onClick={[Function]} | |||
stat={0} | |||
tooltip="e" | |||
value="e" | |||
/> | |||
> | |||
<FacetItem | |||
active={false} | |||
className="" | |||
disabled={true} | |||
halfWidth={false} | |||
loading={false} | |||
name="e" | |||
onClick={[Function]} | |||
stat={0} | |||
tooltip="e" | |||
value="e" | |||
/> | |||
</React.Fragment> | |||
</FacetItemsList> | |||
<ListFooter | |||
className="spacer-bottom" | |||
@@ -173,42 +181,54 @@ exports[`should search 2`] = ` | |||
/> | |||
<React.Fragment> | |||
<FacetItemsList> | |||
<FacetItem | |||
active={false} | |||
disabled={true} | |||
halfWidth={false} | |||
<React.Fragment | |||
key="d" | |||
loading={false} | |||
name="d" | |||
onClick={[Function]} | |||
stat={0} | |||
tooltip="d" | |||
value="d" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={true} | |||
halfWidth={false} | |||
> | |||
<FacetItem | |||
active={false} | |||
className="" | |||
disabled={true} | |||
halfWidth={false} | |||
loading={false} | |||
name="d" | |||
onClick={[Function]} | |||
stat={0} | |||
tooltip="d" | |||
value="d" | |||
/> | |||
</React.Fragment> | |||
<React.Fragment | |||
key="e" | |||
loading={false} | |||
name="e" | |||
onClick={[Function]} | |||
stat={0} | |||
tooltip="e" | |||
value="e" | |||
/> | |||
<FacetItem | |||
active={false} | |||
disabled={true} | |||
halfWidth={false} | |||
> | |||
<FacetItem | |||
active={false} | |||
className="" | |||
disabled={true} | |||
halfWidth={false} | |||
loading={false} | |||
name="e" | |||
onClick={[Function]} | |||
stat={0} | |||
tooltip="e" | |||
value="e" | |||
/> | |||
</React.Fragment> | |||
<React.Fragment | |||
key="f" | |||
loading={false} | |||
name="f" | |||
onClick={[Function]} | |||
stat={0} | |||
tooltip="f" | |||
value="f" | |||
/> | |||
> | |||
<FacetItem | |||
active={false} | |||
className="" | |||
disabled={true} | |||
halfWidth={false} | |||
loading={false} | |||
name="f" | |||
onClick={[Function]} | |||
stat={0} | |||
tooltip="f" | |||
value="f" | |||
/> | |||
</React.Fragment> | |||
</FacetItemsList> | |||
<ListFooter | |||
className="spacer-bottom" |