@@ -140,17 +140,26 @@ export function getComponent( | |||
export interface TreeComponent extends LightComponent { | |||
id: string; | |||
name: string; | |||
path?: string; | |||
refId?: string; | |||
refKey?: string; | |||
tags?: string[]; | |||
visibility: Visibility; | |||
} | |||
export function getTree( | |||
component: string, | |||
options: RequestData = {} | |||
): Promise<{ baseComponent: TreeComponent; components: TreeComponent[]; paging: Paging }> { | |||
return getJSON('/api/components/tree', { ...options, component }); | |||
export function getTree(data: { | |||
asc?: boolean; | |||
branch?: string; | |||
component: string; | |||
p?: number; | |||
ps?: number; | |||
pullRequest?: string; | |||
q?: string; | |||
qualifiers?: string; | |||
s?: string; | |||
strategy?: 'all' | 'leaves' | 'children'; | |||
}): Promise<{ baseComponent: TreeComponent; components: TreeComponent[]; paging: Paging }> { | |||
return getJSON('/api/components/tree', data).catch(throwGlobalError); | |||
} | |||
export function getComponentShow(data: { component: string } & BranchParameters): Promise<any> { |
@@ -165,3 +165,12 @@ export function setIssueType(data: { issue: string; type: string }): Promise<Iss | |||
export function bulkChangeIssues(issueKeys: string[], query: RequestData): Promise<void> { | |||
return post('/api/issues/bulk_change', { issues: issueKeys.join(), ...query }); | |||
} | |||
export function searchIssueAuthors(data: { | |||
organization?: string; | |||
project?: string; | |||
ps?: number; | |||
q?: string; | |||
}): Promise<string[]> { | |||
return getJSON('/api/issues/authors', data).then(r => r.authors, throwGlobalError); | |||
} |
@@ -133,7 +133,8 @@ export default class Search extends React.PureComponent<Props, State> { | |||
const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier); | |||
const qualifiers = isPortfolio ? 'SVW,TRK' : 'BRC,UTS,FIL'; | |||
getTree(component.key, { | |||
getTree({ | |||
component: component.key, | |||
q: query, | |||
s: 'qualifier,name', | |||
qualifiers, |
@@ -18,107 +18,63 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { sortBy, without } from 'lodash'; | |||
import { formatFacetStat, Query } from '../utils'; | |||
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 { Query } from '../utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint'; | |||
import ListStyleFacet from '../../../components/facet/ListStyleFacet'; | |||
import { searchIssueAuthors } from '../../../api/issues'; | |||
import { highlightTerm } from '../../../helpers/search'; | |||
interface Props { | |||
componentKey: string | undefined; | |||
fetching: boolean; | |||
loading?: boolean; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
organization: string | undefined; | |||
stats: { [x: string]: number } | undefined; | |||
authors: string[]; | |||
} | |||
export default class AuthorFacet extends React.PureComponent<Props> { | |||
property = 'authors'; | |||
static defaultProps = { | |||
open: true | |||
}; | |||
const SEARCH_SIZE = 100; | |||
handleItemClick = (itemValue: string, multiple: boolean) => { | |||
const { authors } = this.props; | |||
if (multiple) { | |||
const newValue = sortBy( | |||
authors.includes(itemValue) ? without(authors, itemValue) : [...authors, itemValue] | |||
); | |||
this.props.onChange({ [this.property]: newValue }); | |||
} else { | |||
this.props.onChange({ | |||
[this.property]: authors.includes(itemValue) && authors.length < 2 ? [] : [itemValue] | |||
}); | |||
} | |||
export default class AuthorFacet extends React.PureComponent<Props> { | |||
identity = (author: string) => { | |||
return author; | |||
}; | |||
handleHeaderClick = () => { | |||
this.props.onToggle(this.property); | |||
handleSearch = (query: string, _page: number) => { | |||
return searchIssueAuthors({ | |||
organization: this.props.organization, | |||
project: this.props.componentKey, | |||
ps: SEARCH_SIZE, // maximum | |||
q: query | |||
}).then(authors => ({ maxResults: authors.length === SEARCH_SIZE, results: authors })); | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ [this.property]: [] }); | |||
renderSearchResult = (author: string, term: string) => { | |||
return highlightTerm(author, term); | |||
}; | |||
getStat(author: string) { | |||
const { stats } = this.props; | |||
return stats ? stats[author] : undefined; | |||
} | |||
renderList() { | |||
const { stats } = this.props; | |||
if (!stats) { | |||
return null; | |||
} | |||
const authors = sortBy(Object.keys(stats), key => -stats[key]); | |||
return ( | |||
<FacetItemsList> | |||
{authors.map(author => ( | |||
<FacetItem | |||
active={this.props.authors.includes(author)} | |||
key={author} | |||
loading={this.props.loading} | |||
name={author} | |||
onClick={this.handleItemClick} | |||
stat={formatFacetStat(this.getStat(author))} | |||
tooltip={author} | |||
value={author} | |||
/> | |||
))} | |||
</FacetItemsList> | |||
); | |||
} | |||
render() { | |||
const { authors, 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.props.authors} | |||
/> | |||
<DeferredSpinner loading={this.props.fetching} /> | |||
{this.props.open && ( | |||
<> | |||
{this.renderList()} | |||
<MultipleSelectionHint options={Object.keys(stats).length} values={authors.length} /> | |||
</> | |||
)} | |||
</FacetBox> | |||
<ListStyleFacet | |||
facetHeader={translate('issues.facet.authors')} | |||
fetching={this.props.fetching} | |||
getFacetItemText={this.identity} | |||
getSearchResultKey={this.identity} | |||
getSearchResultText={this.identity} | |||
onChange={this.props.onChange} | |||
onSearch={this.handleSearch} | |||
onToggle={this.props.onToggle} | |||
open={this.props.open} | |||
property="authors" | |||
renderFacetItem={this.identity} | |||
renderSearchResult={this.renderSearchResult} | |||
searchPlaceholder={translate('search.search_for_authors')} | |||
stats={this.props.stats} | |||
values={this.props.authors} | |||
/> | |||
); | |||
} | |||
} |
@@ -18,127 +18,83 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { sortBy, without } from 'lodash'; | |||
import { formatFacetStat, Query, ReferencedComponent } from '../utils'; | |||
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 { Query } from '../utils'; | |||
import QualifierIcon from '../../../components/icons-components/QualifierIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import ListStyleFacet from '../../../components/facet/ListStyleFacet'; | |||
import { highlightTerm } from '../../../helpers/search'; | |||
import { getTree, TreeComponent } from '../../../api/components'; | |||
import { collapsePath } from '../../../helpers/path'; | |||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint'; | |||
interface Props { | |||
componentKey: string; | |||
fetching: boolean; | |||
directories: string[]; | |||
loading?: boolean; | |||
onChange: (changes: Partial<Query>) => void; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
referencedComponents: { [componentKey: string]: ReferencedComponent }; | |||
stats: { [x: string]: number } | undefined; | |||
} | |||
export default class DirectoryFacet extends React.PureComponent<Props> { | |||
property = 'directories'; | |||
static defaultProps = { | |||
open: true | |||
getFacetItemText = (directory: string) => { | |||
return collapsePath(directory, 15); | |||
}; | |||
handleItemClick = (itemValue: string, multiple: boolean) => { | |||
const { directories } = this.props; | |||
if (multiple) { | |||
const newValue = sortBy( | |||
directories.includes(itemValue) | |||
? without(directories, itemValue) | |||
: [...directories, itemValue] | |||
); | |||
this.props.onChange({ [this.property]: newValue }); | |||
} else { | |||
this.props.onChange({ | |||
[this.property]: | |||
directories.includes(itemValue) && directories.length < 2 ? [] : [itemValue] | |||
}); | |||
} | |||
getSearchResultKey = (directory: TreeComponent) => { | |||
return directory.name; | |||
}; | |||
handleHeaderClick = () => { | |||
this.props.onToggle(this.property); | |||
getSearchResultText = (directory: TreeComponent) => { | |||
return directory.name; | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ [this.property]: [] }); | |||
handleSearch = (query: string, page: number) => { | |||
return getTree({ | |||
component: this.props.componentKey, | |||
q: query, | |||
qualifiers: 'DIR', | |||
p: page, | |||
ps: 30 | |||
}).then(({ components, paging }) => ({ paging, results: components })); | |||
}; | |||
getStat(directory: string) { | |||
const { stats } = this.props; | |||
return stats ? stats[directory] : undefined; | |||
} | |||
renderName(directory: string) { | |||
return ( | |||
<span> | |||
<QualifierIcon className="little-spacer-right" qualifier="DIR" /> | |||
{directory} | |||
</span> | |||
); | |||
} | |||
renderList() { | |||
const { stats } = this.props; | |||
renderDirectory = (directory: React.ReactNode) => ( | |||
<> | |||
<QualifierIcon className="little-spacer-right" qualifier="DIR" /> | |||
{directory} | |||
</> | |||
); | |||
if (!stats) { | |||
return null; | |||
} | |||
// sort directories first by counts, then by path | |||
const directories = sortBy(Object.keys(stats), key => -stats[key], d => d); | |||
renderFacetItem = (directory: string) => { | |||
return this.renderDirectory(collapsePath(directory, 15)); | |||
}; | |||
return ( | |||
<FacetItemsList> | |||
{directories.map(directory => ( | |||
<FacetItem | |||
active={this.props.directories.includes(directory)} | |||
key={directory} | |||
loading={this.props.loading} | |||
name={this.renderName(directory)} | |||
onClick={this.handleItemClick} | |||
stat={formatFacetStat(this.getStat(directory))} | |||
tooltip={directory} | |||
value={directory} | |||
/> | |||
))} | |||
</FacetItemsList> | |||
); | |||
} | |||
renderSearchResult = (directory: TreeComponent, term: string) => { | |||
return this.renderDirectory(highlightTerm(collapsePath(directory.name), term)); | |||
}; | |||
render() { | |||
const { directories, stats = {} } = this.props; | |||
const values = directories.map(dir => collapsePath(dir)); | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={values} | |||
/> | |||
<DeferredSpinner loading={this.props.fetching} /> | |||
{this.props.open && ( | |||
<> | |||
{this.renderList()} | |||
<MultipleSelectionHint | |||
options={Object.keys(stats).length} | |||
values={directories.length} | |||
/> | |||
</> | |||
)} | |||
</FacetBox> | |||
<ListStyleFacet | |||
facetHeader={translate('issues.facet.directories')} | |||
fetching={this.props.fetching} | |||
getFacetItemText={this.getFacetItemText} | |||
getSearchResultKey={this.getSearchResultKey} | |||
getSearchResultText={this.getSearchResultText} | |||
minSearchLength={3} | |||
onChange={this.props.onChange} | |||
onSearch={this.handleSearch} | |||
onToggle={this.props.onToggle} | |||
open={this.props.open} | |||
property="directories" | |||
renderFacetItem={this.renderFacetItem} | |||
renderSearchResult={this.renderSearchResult} | |||
searchPlaceholder={translate('search.search_for_directories')} | |||
stats={this.props.stats} | |||
values={this.props.directories} | |||
/> | |||
); | |||
} | |||
} |
@@ -18,19 +18,16 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { sortBy, without } from 'lodash'; | |||
import { formatFacetStat, Query, ReferencedComponent } from '../utils'; | |||
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 { Query, ReferencedComponent } from '../utils'; | |||
import QualifierIcon from '../../../components/icons-components/QualifierIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { collapsePath } from '../../../helpers/path'; | |||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint'; | |||
import { TreeComponent, getTree } from '../../../api/components'; | |||
import ListStyleFacet from '../../../components/facet/ListStyleFacet'; | |||
import { highlightTerm } from '../../../helpers/search'; | |||
interface Props { | |||
componentKey: string; | |||
fetching: boolean; | |||
files: string[]; | |||
loading?: boolean; | |||
@@ -42,102 +39,70 @@ interface Props { | |||
} | |||
export default class FileFacet extends React.PureComponent<Props> { | |||
property = 'files'; | |||
static defaultProps = { | |||
open: true | |||
getFile = (file: string) => { | |||
const { referencedComponents } = this.props; | |||
return referencedComponents[file] ? collapsePath(referencedComponents[file].path, 15) : file; | |||
}; | |||
handleItemClick = (itemValue: string, multiple: boolean) => { | |||
const { files } = this.props; | |||
if (multiple) { | |||
const newValue = sortBy( | |||
files.includes(itemValue) ? without(files, itemValue) : [...files, itemValue] | |||
); | |||
this.props.onChange({ [this.property]: newValue }); | |||
} else { | |||
this.props.onChange({ | |||
[this.property]: files.includes(itemValue) && files.length < 2 ? [] : [itemValue] | |||
}); | |||
} | |||
getFacetItemText = (file: string) => { | |||
const { referencedComponents } = this.props; | |||
return referencedComponents[file] ? referencedComponents[file].path : file; | |||
}; | |||
handleHeaderClick = () => { | |||
this.props.onToggle(this.property); | |||
getSearchResultKey = (file: TreeComponent) => { | |||
return file.id; | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ [this.property]: [] }); | |||
getSearchResultText = (file: TreeComponent) => { | |||
return file.path || file.name; | |||
}; | |||
getStat(file: string) { | |||
const { stats } = this.props; | |||
return stats ? stats[file] : undefined; | |||
} | |||
getFileName(file: string) { | |||
const { referencedComponents } = this.props; | |||
return referencedComponents[file] ? collapsePath(referencedComponents[file].path, 15) : file; | |||
} | |||
renderName(file: string) { | |||
const name = this.getFileName(file); | |||
return ( | |||
<span> | |||
<QualifierIcon className="little-spacer-right" qualifier="FIL" /> | |||
{name} | |||
</span> | |||
); | |||
} | |||
renderList() { | |||
const { stats } = this.props; | |||
handleSearch = (query: string, page: number) => { | |||
return getTree({ | |||
component: this.props.componentKey, | |||
q: query, | |||
qualifiers: 'FIL', | |||
p: page, | |||
ps: 30 | |||
}).then(({ components, paging }) => ({ paging, results: components })); | |||
}; | |||
if (!stats) { | |||
return null; | |||
} | |||
renderFile = (file: React.ReactNode) => ( | |||
<> | |||
<QualifierIcon className="little-spacer-right" qualifier="FIL" /> | |||
{file} | |||
</> | |||
); | |||
const files = sortBy(Object.keys(stats), key => -stats[key]); | |||
renderFacetItem = (file: string) => { | |||
const name = this.getFile(file); | |||
return this.renderFile(name); | |||
}; | |||
return ( | |||
<FacetItemsList> | |||
{files.map(file => ( | |||
<FacetItem | |||
active={this.props.files.includes(file)} | |||
key={file} | |||
loading={this.props.loading} | |||
name={this.renderName(file)} | |||
onClick={this.handleItemClick} | |||
stat={formatFacetStat(this.getStat(file))} | |||
tooltip={this.getFileName(file)} | |||
value={file} | |||
/> | |||
))} | |||
</FacetItemsList> | |||
); | |||
} | |||
renderSearchResult = (file: TreeComponent, term: string) => { | |||
return this.renderFile(highlightTerm(collapsePath(file.path || file.name, 15), term)); | |||
}; | |||
render() { | |||
const { files, stats = {} } = this.props; | |||
const values = files.map(file => this.getFileName(file)); | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={values} | |||
/> | |||
<DeferredSpinner loading={this.props.fetching} /> | |||
{this.props.open && ( | |||
<> | |||
{this.renderList()} | |||
<MultipleSelectionHint options={Object.keys(stats).length} values={files.length} /> | |||
</> | |||
)} | |||
</FacetBox> | |||
<ListStyleFacet | |||
facetHeader={translate('issues.facet.files')} | |||
fetching={this.props.fetching} | |||
getFacetItemText={this.getFacetItemText} | |||
getSearchResultKey={this.getSearchResultKey} | |||
getSearchResultText={this.getSearchResultText} | |||
minSearchLength={3} | |||
onChange={this.props.onChange} | |||
onSearch={this.handleSearch} | |||
onToggle={this.props.onToggle} | |||
open={this.props.open} | |||
property="files" | |||
renderFacetItem={this.renderFacetItem} | |||
renderSearchResult={this.renderSearchResult} | |||
searchPlaceholder={translate('search.search_for_files')} | |||
stats={this.props.stats} | |||
values={this.props.files} | |||
/> | |||
); | |||
} | |||
} |
@@ -18,18 +18,15 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { sortBy, without } from 'lodash'; | |||
import { formatFacetStat, Query, ReferencedComponent } from '../utils'; | |||
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 { Query, ReferencedComponent } from '../utils'; | |||
import QualifierIcon from '../../../components/icons-components/QualifierIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint'; | |||
import ListStyleFacet from '../../../components/facet/ListStyleFacet'; | |||
import { TreeComponent, getTree } from '../../../api/components'; | |||
import { highlightTerm } from '../../../helpers/search'; | |||
interface Props { | |||
componentKey: string; | |||
fetching: boolean; | |||
loading?: boolean; | |||
modules: string[]; | |||
@@ -41,101 +38,65 @@ interface Props { | |||
} | |||
export default class ModuleFacet extends React.PureComponent<Props> { | |||
property = 'modules'; | |||
static defaultProps = { | |||
open: true | |||
getModuleName = (module: string) => { | |||
const { referencedComponents } = this.props; | |||
return referencedComponents[module] ? referencedComponents[module].name : module; | |||
}; | |||
handleItemClick = (itemValue: string, multiple: boolean) => { | |||
const { modules } = this.props; | |||
if (multiple) { | |||
const newValue = sortBy( | |||
modules.includes(itemValue) ? without(modules, itemValue) : [...modules, itemValue] | |||
); | |||
this.props.onChange({ [this.property]: newValue }); | |||
} else { | |||
this.props.onChange({ | |||
[this.property]: modules.includes(itemValue) && modules.length < 2 ? [] : [itemValue] | |||
}); | |||
} | |||
getSearchResultKey = (module: TreeComponent) => { | |||
return module.id; | |||
}; | |||
handleHeaderClick = () => { | |||
this.props.onToggle(this.property); | |||
getSearchResultText = (module: TreeComponent) => { | |||
return module.name; | |||
}; | |||
handleClear = () => { | |||
this.props.onChange({ [this.property]: [] }); | |||
handleSearch = (query: string, page: number) => { | |||
return getTree({ | |||
component: this.props.componentKey, | |||
q: query, | |||
qualifiers: 'BRC', | |||
p: page, | |||
ps: 30 | |||
}).then(({ components, paging }) => ({ paging, results: components })); | |||
}; | |||
getStat(module: string) { | |||
const { stats } = this.props; | |||
return stats ? stats[module] : undefined; | |||
} | |||
getModuleName(module: string) { | |||
const { referencedComponents } = this.props; | |||
return referencedComponents[module] ? referencedComponents[module].name : module; | |||
} | |||
renderName(module: string) { | |||
return ( | |||
<span> | |||
<QualifierIcon className="little-spacer-right" qualifier="BRC" /> | |||
{this.getModuleName(module)} | |||
</span> | |||
); | |||
} | |||
renderList() { | |||
const { stats } = this.props; | |||
renderModule = (module: React.ReactNode) => ( | |||
<> | |||
<QualifierIcon className="little-spacer-right" qualifier="BRC" /> | |||
{module} | |||
</> | |||
); | |||
if (!stats) { | |||
return null; | |||
} | |||
const modules = sortBy(Object.keys(stats), key => -stats[key]); | |||
renderFacetItem = (module: string) => { | |||
const name = this.getModuleName(module); | |||
return this.renderModule(name); | |||
}; | |||
return ( | |||
<FacetItemsList> | |||
{modules.map(module => ( | |||
<FacetItem | |||
active={this.props.modules.includes(module)} | |||
key={module} | |||
loading={this.props.loading} | |||
name={this.renderName(module)} | |||
onClick={this.handleItemClick} | |||
stat={formatFacetStat(this.getStat(module))} | |||
tooltip={this.getModuleName(module)} | |||
value={module} | |||
/> | |||
))} | |||
</FacetItemsList> | |||
); | |||
} | |||
renderSearchResult = (module: TreeComponent, term: string) => { | |||
return this.renderModule(highlightTerm(module.name, term)); | |||
}; | |||
render() { | |||
const { modules, stats = {} } = this.props; | |||
const values = modules.map(module => this.getModuleName(module)); | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
name={translate('issues.facet', this.property)} | |||
onClear={this.handleClear} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
values={values} | |||
/> | |||
<DeferredSpinner loading={this.props.fetching} /> | |||
{this.props.open && ( | |||
<> | |||
{this.renderList()} | |||
<MultipleSelectionHint options={Object.keys(stats).length} values={modules.length} /> | |||
</> | |||
)} | |||
</FacetBox> | |||
<ListStyleFacet | |||
facetHeader={translate('issues.facet.modules')} | |||
fetching={this.props.fetching} | |||
getFacetItemText={this.getModuleName} | |||
getSearchResultKey={this.getSearchResultKey} | |||
getSearchResultText={this.getSearchResultText} | |||
minSearchLength={3} | |||
onChange={this.props.onChange} | |||
onSearch={this.handleSearch} | |||
onToggle={this.props.onToggle} | |||
open={this.props.open} | |||
property="modules" | |||
renderFacetItem={this.renderFacetItem} | |||
renderSearchResult={this.renderSearchResult} | |||
searchPlaceholder={translate('search.search_for_modules')} | |||
stats={this.props.stats} | |||
values={this.props.modules} | |||
/> | |||
); | |||
} | |||
} |
@@ -53,17 +53,21 @@ export default class ProjectFacet extends React.PureComponent<Props> { | |||
): Promise<{ results: SearchedProject[]; paging: Paging }> => { | |||
const { component, organization } = this.props; | |||
if (component && ['VW', 'SVW', 'APP'].includes(component.qualifier)) { | |||
return getTree(component.key, { p: page, ps: 30, q: query, qualifiers: 'TRK' }).then( | |||
({ components, paging }) => ({ | |||
paging, | |||
results: components.map(component => ({ | |||
id: component.refId || component.id, | |||
key: component.key, | |||
name: component.name, | |||
organization: component.organization | |||
})) | |||
}) | |||
); | |||
return getTree({ | |||
component: component.key, | |||
p: page, | |||
ps: 30, | |||
q: query, | |||
qualifiers: 'TRK' | |||
}).then(({ components, paging }) => ({ | |||
paging, | |||
results: components.map(component => ({ | |||
id: component.refId || component.id, | |||
key: component.key, | |||
name: component.name, | |||
organization: component.organization | |||
})) | |||
})); | |||
} | |||
return searchProjects({ |
@@ -197,6 +197,7 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
)} | |||
{displayModulesFacet && ( | |||
<ModuleFacet | |||
componentKey={this.props.component!.key} | |||
fetching={this.props.loadingFacets.modules === true} | |||
loading={this.props.loading} | |||
modules={query.modules} | |||
@@ -209,18 +210,19 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
)} | |||
{displayDirectoriesFacet && ( | |||
<DirectoryFacet | |||
componentKey={this.props.component!.key} | |||
directories={query.directories} | |||
fetching={this.props.loadingFacets.directories === true} | |||
loading={this.props.loading} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.directories} | |||
referencedComponents={this.props.referencedComponents} | |||
stats={facets.directories} | |||
/> | |||
)} | |||
{displayFilesFacet && ( | |||
<FileFacet | |||
componentKey={this.props.component!.key} | |||
fetching={this.props.loadingFacets.files === true} | |||
files={query.files} | |||
loading={this.props.loading} | |||
@@ -249,11 +251,13 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
{displayAuthorFacet && ( | |||
<AuthorFacet | |||
authors={query.authors} | |||
componentKey={this.props.component && this.props.component.key} | |||
fetching={this.props.loadingFacets.authors === true} | |||
loading={this.props.loading} | |||
onChange={this.props.onFilterChange} | |||
onToggle={this.props.onFacetToggle} | |||
open={!!openFacets.authors} | |||
organization={organizationKey} | |||
stats={facets.authors} | |||
/> | |||
)} |
@@ -39,14 +39,15 @@ interface Props { | |||
tags: string[]; | |||
} | |||
const SEARCH_SIZE = 100; | |||
export default class TagFacet extends React.PureComponent<Props> { | |||
handleSearch = (query: string) => { | |||
return searchIssueTags({ organization: this.props.organization, ps: 50, q: query }).then( | |||
tags => ({ | |||
paging: { pageIndex: 1, pageSize: tags.length, total: tags.length }, | |||
results: tags | |||
}) | |||
); | |||
return searchIssueTags({ | |||
organization: this.props.organization, | |||
ps: SEARCH_SIZE, | |||
q: query | |||
}).then(tags => ({ maxResults: tags.length === SEARCH_SIZE, results: tags })); | |||
}; | |||
getTagName = (tag: string) => { |
@@ -72,8 +72,7 @@ const receiveProjectModules = (projectKey, modules) => ({ | |||
}); | |||
export const fetchProjectModules = projectKey => dispatch => { | |||
const options = { qualifiers: 'BRC', s: 'name', ps: 500 }; | |||
getTree(projectKey, options).then( | |||
getTree({ component: projectKey, qualifiers: 'BRC', s: 'name', ps: 500 }).then( | |||
r => { | |||
dispatch(receiveProjectModules(projectKey, r.components)); | |||
}, |
@@ -41,8 +41,12 @@ export interface Props<S> { | |||
loading?: boolean; | |||
maxInitialItems?: number; | |||
maxItems?: number; | |||
minSearchLength?: number; | |||
onChange: (changes: { [x: string]: string | string[] }) => void; | |||
onSearch: (query: string, page?: number) => Promise<{ results: S[]; paging: Paging }>; | |||
onSearch: ( | |||
query: string, | |||
page?: number | |||
) => Promise<{ maxResults?: boolean; results: S[]; paging?: Paging }>; | |||
onToggle: (property: string) => void; | |||
open: boolean; | |||
property: string; | |||
@@ -57,6 +61,7 @@ interface State<S> { | |||
autoFocus: boolean; | |||
query: string; | |||
searching: boolean; | |||
searchMaxResults?: boolean; | |||
searchPaging?: Paging; | |||
searchResults?: S[]; | |||
showFullList: boolean; | |||
@@ -87,7 +92,13 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S | |||
this.setState({ autoFocus: true }); | |||
} else if (prevProps.open && !this.props.open) { | |||
// reset state when closing the facet | |||
this.setState({ query: '', searchResults: undefined, searching: false, showFullList: false }); | |||
this.setState({ | |||
query: '', | |||
searchMaxResults: undefined, | |||
searchResults: undefined, | |||
searching: false, | |||
showFullList: false | |||
}); | |||
} else if ( | |||
prevProps.stats !== this.props.stats && | |||
Object.keys(this.props.stats || {}).length < this.props.maxInitialItems! | |||
@@ -132,9 +143,14 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S | |||
search = (query: string) => { | |||
if (query.length >= 2) { | |||
this.setState({ query, searching: true }); | |||
this.props.onSearch(query).then(({ paging, results }) => { | |||
this.props.onSearch(query).then(({ maxResults, paging, results }) => { | |||
if (this.mounted) { | |||
this.setState({ searching: false, searchResults: results, searchPaging: paging }); | |||
this.setState({ | |||
searching: false, | |||
searchMaxResults: maxResults, | |||
searchResults: results, | |||
searchPaging: paging | |||
}); | |||
} | |||
}, this.stopSearching); | |||
} else { | |||
@@ -241,12 +257,14 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S | |||
return null; | |||
} | |||
const { minSearchLength = 2 } = this.props; | |||
return ( | |||
<SearchBox | |||
autoFocus={this.state.autoFocus} | |||
className="little-spacer-top spacer-bottom" | |||
loading={this.state.searching} | |||
minLength={2} | |||
minLength={minSearchLength} | |||
onChange={this.search} | |||
placeholder={this.props.searchPlaceholder} | |||
value={this.state.query} | |||
@@ -255,13 +273,13 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S | |||
} | |||
renderSearchResults() { | |||
const { searching, searchResults, searchPaging } = this.state; | |||
const { searching, searchMaxResults, searchResults, searchPaging } = this.state; | |||
if (!searching && (!searchResults || !searchResults.length)) { | |||
return <div className="note spacer-bottom">{translate('no_results')}</div>; | |||
} | |||
if (!searchResults || !searchPaging) { | |||
if (!searchResults) { | |||
// initial search | |||
return null; | |||
} | |||
@@ -271,13 +289,20 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S | |||
<FacetItemsList> | |||
{searchResults.map(result => this.renderSearchResult(result))} | |||
</FacetItemsList> | |||
<ListFooter | |||
className="spacer-bottom" | |||
count={searchResults.length} | |||
loadMore={this.searchMore} | |||
ready={!searching} | |||
total={searchPaging.total} | |||
/> | |||
{searchMaxResults && ( | |||
<div className="alert alert-warning spacer-top"> | |||
{translate('facet_might_have_more_results')} | |||
</div> | |||
)} | |||
{searchPaging && ( | |||
<ListFooter | |||
className="spacer-bottom" | |||
count={searchResults.length} | |||
loadMore={this.searchMore} | |||
ready={!searching} | |||
total={searchPaging.total} | |||
/> | |||
)} | |||
</> | |||
); | |||
} |
@@ -922,6 +922,10 @@ search.search_for_tags=Search for tags... | |||
search.search_for_rules=Search for rules... | |||
search.search_for_languages=Search for languages... | |||
search.search_for_cwe=Search for CWEs... | |||
search.search_for_authors=Search for authors... | |||
search.search_for_directories=Search for directories... | |||
search.search_for_files=Search for files... | |||
search.search_for_modules=Search for modules... | |||
#------------------------------------------------------------------------------ |