Browse Source

SONAR-9369 Add search for module, directory, file and author facets (#606)

tags/7.5
Stas Vilchik 5 years ago
parent
commit
20a8ceffbe

+ 14
- 5
server/sonar-web/src/main/js/api/components.ts View File

@@ -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> {

+ 9
- 0
server/sonar-web/src/main/js/api/issues.ts View File

@@ -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);
}

+ 2
- 1
server/sonar-web/src/main/js/apps/code/components/Search.tsx View File

@@ -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,

+ 36
- 80
server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx View File

@@ -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}
/>
);
}
}

+ 49
- 93
server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx View File

@@ -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}
/>
);
}
}

+ 55
- 90
server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx View File

@@ -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}
/>
);
}
}

+ 51
- 90
server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx View File

@@ -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}
/>
);
}
}

+ 15
- 11
server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx View File

@@ -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({

+ 5
- 1
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx View File

@@ -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}
/>
)}

+ 7
- 6
server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx View File

@@ -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) => {

+ 1
- 2
server/sonar-web/src/main/js/apps/project-admin/store/actions.js View File

@@ -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));
},

+ 39
- 14
server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx View File

@@ -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}
/>
)}
</>
);
}

+ 4
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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...


#------------------------------------------------------------------------------

Loading…
Cancel
Save