aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/issues
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/apps/issues')
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/App.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx181
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx158
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx235
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx153
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx73
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx142
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap42
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap12
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap131
-rw-r--r--server/sonar-web/src/main/js/apps/issues/utils.ts40
15 files changed, 594 insertions, 606 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.tsx b/server/sonar-web/src/main/js/apps/issues/components/App.tsx
index 446956be3eb..83f5e3d1fe0 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/App.tsx
@@ -50,7 +50,8 @@ import {
ReferencedUser,
saveMyIssues,
serializeQuery,
- STANDARDS
+ STANDARDS,
+ ReferencedRule
} from '../utils';
import {
Component,
@@ -90,7 +91,7 @@ interface FetchIssuesPromise {
issues: Issue[];
languages: ReferencedLanguage[];
paging: Paging;
- rules: { name: string }[];
+ rules: ReferencedRule[];
users: ReferencedUser[];
}
@@ -125,7 +126,7 @@ export interface State {
query: Query;
referencedComponents: { [componentKey: string]: ReferencedComponent };
referencedLanguages: { [languageKey: string]: ReferencedLanguage };
- referencedRules: { [ruleKey: string]: { name: string } };
+ referencedRules: { [ruleKey: string]: ReferencedRule };
referencedUsers: { [login: string]: ReferencedUser };
selected?: string;
selectedFlowIndex?: number;
diff --git a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
index 1a2859fa7f6..43c03a88fdb 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
@@ -155,7 +155,9 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
};
handleAssigneeSearch = (query: string) => {
- return searchAssignees(query, this.state.organization);
+ return searchAssignees(query, this.state.organization).then(({ results }) =>
+ results.map(r => ({ avatar: r.avatar, label: r.name, value: r.login }))
+ );
};
handleAssigneeSelect = (assignee: AssigneeOption) => {
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx
index 6c1b8275cff..d7b83bf6228 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx
@@ -19,10 +19,15 @@
*/
import * as React from 'react';
import { sortBy, uniq, without } from 'lodash';
-import { searchAssignees, formatFacetStat, Query, ReferencedUser } from '../utils';
-import { Component } from '../../../app/types';
+import {
+ searchAssignees,
+ formatFacetStat,
+ Query,
+ ReferencedUser,
+ SearchedAssignee
+} from '../utils';
+import { Component, Paging } from '../../../app/types';
import FacetBox from '../../../components/facet/FacetBox';
-import FacetFooter from '../../../components/facet/FacetFooter';
import FacetHeader from '../../../components/facet/FacetHeader';
import FacetItem from '../../../components/facet/FacetItem';
import FacetItemsList from '../../../components/facet/FacetItemsList';
@@ -30,6 +35,9 @@ 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';
export interface Props {
assigned: boolean;
@@ -45,11 +53,66 @@ export interface Props {
referencedUsers: { [login: string]: ReferencedUser };
}
-export default class AssigneeFacet extends React.PureComponent<Props> {
+interface State {
+ query: string;
+ searching: boolean;
+ searchResults?: SearchedAssignee[];
+ searchPaging?: Paging;
+}
+
+export default class AssigneeFacet extends React.PureComponent<Props, State> {
+ mounted = false;
property = 'assignees';
- static defaultProps = {
- open: true
+ 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
+ );
+ }
};
handleItemClick = (itemValue: string, multiple: boolean) => {
@@ -78,10 +141,6 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
this.props.onChange({ assigned: true, assignees: [] });
};
- handleSearch = (query: string) => {
- return searchAssignees(query, this.props.organization);
- };
-
handleSelect = (option: { value: string }) => {
const { assignees } = this.props;
this.props.onChange({ assigned: true, [this.property]: uniq([...assignees, option.value]) });
@@ -134,21 +193,18 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
}
renderOption = (option: { avatar: string; label: string }) => {
- return (
- <span>
- {option.avatar !== undefined && (
- <Avatar
- className="little-spacer-right"
- hash={option.avatar}
- name={option.label}
- size={16}
- />
- )}
- {option.label}
- </span>
- );
+ 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 (
@@ -185,16 +241,77 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
);
}
- renderFooter() {
- if (!this.props.stats) {
+ 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;
}
return (
- <FacetFooter
- onSearch={this.handleSearch}
- onSelect={this.handleSelect}
- renderOption={this.renderOption}
+ <>
+ <FacetItemsList>
+ {searchResults.map(result => this.renderSearchResult(result))}
+ </FacetItemsList>
+ <ListFooter
+ count={searchResults.length}
+ loadMore={this.searchMore}
+ ready={!searching}
+ total={searchPaging.total}
+ />
+ </>
+ );
+ }
+
+ renderSearchResult(result: SearchedAssignee) {
+ const active = this.props.assignees.includes(result.login);
+ const stat = this.getStat(result.login);
+ 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}
/>
);
}
@@ -214,8 +331,10 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
<DeferredSpinner loading={this.props.fetching} />
{this.props.open && (
<>
- {this.renderList()}
- {this.renderFooter()}
+ {this.renderSearch()}
+ {this.state.query && this.state.searchResults !== undefined
+ ? this.renderSearchResults()
+ : this.renderList()}
<MultipleSelectionHint options={Object.keys(stats).length} values={assignees.length} />
</>
)}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx
index 0be9e8b7f00..65f2144844e 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx
@@ -18,19 +18,22 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { sortBy, uniq, without } from 'lodash';
-import LanguageFacetFooter from './LanguageFacetFooter';
-import { formatFacetStat, Query, ReferencedLanguage } 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 { uniqBy } from 'lodash';
+import { connect } from 'react-redux';
+import ListStyleFacet from '../../../components/facet/ListStyleFacet';
+import { Query, ReferencedLanguage } from '../utils';
+import { getLanguages } from '../../../store/rootReducer';
import { translate } from '../../../helpers/l10n';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
-import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';
+import { highlightTerm } from '../../../helpers/search';
+
+interface InstalledLanguage {
+ key: string;
+ name: string;
+}
interface Props {
fetching: boolean;
+ installedLanguages: InstalledLanguage[];
languages: string[];
loading?: boolean;
onChange: (changes: Partial<Query>) => void;
@@ -40,109 +43,62 @@ interface Props {
stats: { [x: string]: number } | undefined;
}
-export default class LanguageFacet extends React.PureComponent<Props> {
- property = 'languages';
-
- static defaultProps = {
- open: true
- };
-
- handleItemClick = (itemValue: string, multiple: boolean) => {
- const { languages } = this.props;
- if (multiple) {
- const newValue = sortBy(
- languages.includes(itemValue) ? without(languages, itemValue) : [...languages, itemValue]
- );
- this.props.onChange({ [this.property]: newValue });
- } else {
- this.props.onChange({
- [this.property]: languages.includes(itemValue) && languages.length < 2 ? [] : [itemValue]
- });
- }
- };
-
- handleHeaderClick = () => {
- this.props.onToggle(this.property);
- };
-
- handleClear = () => {
- this.props.onChange({ [this.property]: [] });
- };
-
- getLanguageName(language: string) {
+class LanguageFacet extends React.PureComponent<Props> {
+ getLanguageName = (language: string) => {
const { referencedLanguages } = this.props;
return referencedLanguages[language] ? referencedLanguages[language].name : language;
- }
-
- getStat(language: string) {
- const { stats } = this.props;
- return stats ? stats[language] : undefined;
- }
-
- handleSelect = (language: string) => {
- const { languages } = this.props;
- this.props.onChange({ [this.property]: uniq([...languages, language]) });
};
- renderList() {
- const { stats } = this.props;
-
- if (!stats) {
- return null;
- }
-
- const languages = sortBy(Object.keys(stats), key => -stats[key]);
-
- return (
- <FacetItemsList>
- {languages.map(language => (
- <FacetItem
- active={this.props.languages.includes(language)}
- key={language}
- loading={this.props.loading}
- name={this.getLanguageName(language)}
- onClick={this.handleItemClick}
- stat={formatFacetStat(this.getStat(language))}
- tooltip={this.getLanguageName(language)}
- value={language}
- />
- ))}
- </FacetItemsList>
+ handleSearch = (query: string) => {
+ const options = this.getAllPossibleOptions();
+ const results = options.filter(language =>
+ language.name.toLowerCase().includes(query.toLowerCase())
);
- }
+ const paging = { pageIndex: 1, pageSize: results.length, total: results.length };
+ return Promise.resolve({ paging, results });
+ };
- renderFooter() {
- if (!this.props.stats) {
- return null;
- }
+ getAllPossibleOptions = () => {
+ const { installedLanguages, stats = {} } = this.props;
- return (
- <LanguageFacetFooter onSelect={this.handleSelect} selected={Object.keys(this.props.stats)} />
+ // add any language that presents in the facet, but might not be installed
+ // for such language we don't know their display name, so let's just use their key
+ // and make sure we reference each language only once
+ return uniqBy(
+ [...installedLanguages, ...Object.keys(stats).map(key => ({ key, name: key }))],
+ language => language.key
);
- }
+ };
+
+ renderSearchResult = ({ name }: InstalledLanguage, term: string) => {
+ return highlightTerm(name, term);
+ };
render() {
- const { languages, stats = {} } = this.props;
- const values = this.props.languages.map(language => this.getLanguageName(language));
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()}
- {this.renderFooter()}
- <MultipleSelectionHint options={Object.keys(stats).length} values={languages.length} />
- </>
- )}
- </FacetBox>
+ <ListStyleFacet
+ facetHeader={translate('issues.facet.languages')}
+ fetching={this.props.fetching}
+ getFacetItemText={this.getLanguageName}
+ getSearchResultKey={(language: InstalledLanguage) => language.key}
+ getSearchResultText={(language: InstalledLanguage) => language.name}
+ onChange={this.props.onChange}
+ onSearch={this.handleSearch}
+ onToggle={this.props.onToggle}
+ open={this.props.open}
+ property="languages"
+ renderFacetItem={this.getLanguageName}
+ renderSearchResult={this.renderSearchResult}
+ searchPlaceholder={translate('search.search_for_languages')}
+ stats={this.props.stats}
+ values={this.props.languages}
+ />
);
}
}
+
+const mapStateToProps = (state: any) => ({
+ installedLanguages: Object.values(getLanguages(state))
+});
+
+export default connect(mapStateToProps)(LanguageFacet);
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx
index 823e453bf2a..24f4915fb09 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx
@@ -18,20 +18,14 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { sortBy, uniq, without } from 'lodash';
-import { formatFacetStat, Query, ReferencedComponent } from '../utils';
+import ListStyleFacet from '../../../components/facet/ListStyleFacet';
+import { Query, ReferencedComponent } from '../utils';
import { searchProjects, getTree } from '../../../api/components';
-import { Component } 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 FacetFooter from '../../../components/facet/FacetFooter';
+import { Component, Paging } from '../../../app/types';
import Organization from '../../../components/shared/Organization';
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 { highlightTerm } from '../../../helpers/search';
interface Props {
component: Component | undefined;
@@ -46,177 +40,104 @@ interface Props {
stats: { [x: string]: number } | undefined;
}
-export default class ProjectFacet extends React.PureComponent<Props> {
- property = 'projects';
-
- static defaultProps = {
- open: true
- };
-
- handleItemClick = (itemValue: string, multiple: boolean) => {
- const { projects } = this.props;
- if (multiple) {
- const newValue = sortBy(
- projects.includes(itemValue) ? without(projects, itemValue) : [...projects, itemValue]
- );
- this.props.onChange({ [this.property]: newValue });
- } else {
- this.props.onChange({
- [this.property]: projects.includes(itemValue) && projects.length < 2 ? [] : [itemValue]
- });
- }
- };
-
- handleHeaderClick = () => {
- this.props.onToggle(this.property);
- };
-
- handleClear = () => {
- this.props.onChange({ [this.property]: [] });
- };
+interface SearchedProject {
+ id: string;
+ name: string;
+ organization: string;
+}
- handleSearch = (query: string) => {
+export default class ProjectFacet extends React.PureComponent<Props> {
+ handleSearch = (
+ query: string,
+ page = 1
+ ): Promise<{ results: SearchedProject[]; paging: Paging }> => {
const { component, organization } = this.props;
if (component && ['VW', 'SVW', 'APP'].includes(component.qualifier)) {
- return getTree(component.key, { ps: 50, q: query, qualifiers: 'TRK' }).then(response =>
- response.components.map((component: any) => ({
- label: component.name,
- organization: component.organization,
- value: component.refId
- }))
+ 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 searchProjects({
- ps: 50,
+ p: page,
+ ps: 30,
filter: query ? `query = "${query}"` : '',
organization: organization && organization.key
- }).then(response =>
- response.components.map(component => ({
- label: component.name,
- organization: component.organization,
- value: component.id
+ }).then(({ components, paging }) => ({
+ paging,
+ results: components.map(component => ({
+ id: component.id,
+ key: component.key,
+ name: component.name,
+ organization: component.organization
}))
- );
- };
-
- handleSelect = (option: { value: string }) => {
- const { projects } = this.props;
- this.props.onChange({ [this.property]: uniq([...projects, option.value]) });
+ }));
};
- getStat(project: string) {
- const { stats } = this.props;
- return stats ? stats[project] : undefined;
- }
-
- getProjectName(project: string) {
+ getProjectName = (project: string) => {
const { referencedComponents } = this.props;
return referencedComponents[project] ? referencedComponents[project].name : project;
- }
-
- getProjectNameAndTooltip(project: string) {
- const { organization, referencedComponents } = this.props;
- return referencedComponents[project]
- ? {
- name: (
- <span>
- <QualifierIcon className="little-spacer-right" qualifier="TRK" />
- {!organization && (
- <Organization
- link={false}
- organizationKey={referencedComponents[project].organization}
- />
- )}
- {referencedComponents[project].name}
- </span>
- ),
- tooltip: referencedComponents[project].name
- }
- : {
- name: (
- <span>
- <QualifierIcon className="little-spacer-right" qualifier="TRK" />
- {project}
- </span>
- ),
- tooltip: project
- };
- }
+ };
- renderOption = (option: { label: string; organization: string }) => {
- return (
+ renderFacetItem = (project: string) => {
+ const { referencedComponents } = this.props;
+ return referencedComponents[project] ? (
+ this.renderProject(referencedComponents[project])
+ ) : (
<span>
- <Organization link={false} organizationKey={option.organization} />
- {option.label}
+ <QualifierIcon className="little-spacer-right" qualifier="TRK" />
+ {project}
</span>
);
};
- renderListItem(project: string) {
- const { name, tooltip } = this.getProjectNameAndTooltip(project);
- return (
- <FacetItem
- active={this.props.projects.includes(project)}
- key={project}
- loading={this.props.loading}
- name={name}
- onClick={this.handleItemClick}
- stat={formatFacetStat(this.getStat(project))}
- tooltip={tooltip}
- value={project}
- />
- );
- }
-
- renderList() {
- const { stats } = this.props;
-
- if (!stats) {
- return null;
- }
-
- const projects = sortBy(Object.keys(stats), key => -stats[key]);
-
- return <FacetItemsList>{projects.map(project => this.renderListItem(project))}</FacetItemsList>;
- }
-
- renderFooter() {
- if (!this.props.stats) {
- return null;
- }
+ renderProject = (project: Pick<SearchedProject, 'name' | 'organization'>) => (
+ <span>
+ <QualifierIcon className="little-spacer-right" qualifier="TRK" />
+ {!this.props.organization && (
+ <Organization link={false} organizationKey={project.organization} />
+ )}
+ {project.name}
+ </span>
+ );
+
+ renderSearchResult = (project: Pick<SearchedProject, 'name' | 'organization'>, term: string) => (
+ <>
+ <QualifierIcon className="little-spacer-right" qualifier="TRK" />
+ {!this.props.organization && (
+ <Organization link={false} organizationKey={project.organization} />
+ )}
+ {highlightTerm(project.name, term)}
+ </>
+ );
+ render() {
return (
- <FacetFooter
- minimumQueryLength={3}
+ <ListStyleFacet
+ facetHeader={translate('issues.facet.projects')}
+ fetching={this.props.fetching}
+ getFacetItemText={this.getProjectName}
+ getSearchResultKey={(project: SearchedProject) => project.id}
+ getSearchResultText={(project: SearchedProject) => project.name}
+ onChange={this.props.onChange}
onSearch={this.handleSearch}
- onSelect={this.handleSelect}
- renderOption={this.renderOption}
+ onToggle={this.props.onToggle}
+ open={this.props.open}
+ property="projects"
+ renderFacetItem={this.renderFacetItem}
+ renderSearchResult={this.renderSearchResult}
+ searchPlaceholder={translate('search.search_for_projects')}
+ stats={this.props.stats}
+ values={this.props.projects}
/>
);
}
-
- render() {
- const { projects, stats = {} } = this.props;
- const values = this.props.projects.map(project => this.getProjectName(project));
- 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()}
- {this.renderFooter()}
- <MultipleSelectionHint options={Object.keys(stats).length} values={projects.length} />
- </>
- )}
- </FacetBox>
- );
- }
}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx
index e2ec1cdc721..be0c32bc774 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx
@@ -18,17 +18,11 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { sortBy, uniq, without } from 'lodash';
-import { formatFacetStat, Query } from '../utils';
+import ListStyleFacet from '../../../components/facet/ListStyleFacet';
+import { Query, ReferencedRule } from '../utils';
import { searchRules } from '../../../api/rules';
-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 FacetFooter from '../../../components/facet/FacetFooter';
+import { Rule, Paging } from '../../../app/types';
import { translate } from '../../../helpers/l10n';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
-import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';
interface Props {
fetching: boolean;
@@ -38,126 +32,67 @@ interface Props {
onToggle: (property: string) => void;
open: boolean;
organization: string | undefined;
- referencedRules: { [ruleKey: string]: { name: string } };
+ referencedRules: { [ruleKey: string]: ReferencedRule };
rules: string[];
stats: { [x: string]: number } | undefined;
}
-export default class RuleFacet extends React.PureComponent<Props> {
- property = 'rules';
-
- static defaultProps = {
- open: true
- };
-
- handleItemClick = (itemValue: string, multiple: boolean) => {
- const { rules } = this.props;
- if (multiple) {
- const newValue = sortBy(
- rules.includes(itemValue) ? without(rules, itemValue) : [...rules, itemValue]
- );
- this.props.onChange({ [this.property]: newValue });
- } else {
- this.props.onChange({
- [this.property]: rules.includes(itemValue) && rules.length < 2 ? [] : [itemValue]
- });
- }
- };
-
- handleHeaderClick = () => {
- this.props.onToggle(this.property);
- };
-
- handleClear = () => {
- this.props.onChange({ [this.property]: [] });
- };
+interface State {
+ query: string;
+ searching: boolean;
+ searchResults?: Rule[];
+ searchPaging?: Paging;
+}
- handleSearch = (query: string) => {
+export default class RuleFacet extends React.PureComponent<Props, State> {
+ handleSearch = (query: string, page = 1) => {
const { languages, organization } = this.props;
return searchRules({
f: 'name,langName',
languages: languages.length ? languages.join() : undefined,
organization,
q: query,
+ p: page,
+ ps: 30,
+ s: 'name',
// eslint-disable-next-line camelcase
include_external: true
- }).then(response =>
- response.rules.map(rule => ({ label: `(${rule.langName}) ${rule.name}`, value: rule.key }))
- );
+ }).then(response => ({
+ paging: { pageIndex: response.p, pageSize: response.ps, total: response.total },
+ results: response.rules
+ }));
};
- handleSelect = (option: { value: string }) => {
- const { rules } = this.props;
- this.props.onChange({ [this.property]: uniq([...rules, option.value]) });
- };
-
- getRuleName(rule: string): string {
+ getRuleName = (rule: string) => {
const { referencedRules } = this.props;
- return referencedRules[rule] ? referencedRules[rule].name : rule;
- }
-
- getStat(rule: string) {
- const { stats } = this.props;
- return stats ? stats[rule] : undefined;
- }
-
- renderList() {
- const { stats } = this.props;
-
- if (!stats) {
- return null;
- }
-
- const rules = sortBy(Object.keys(stats), key => -stats[key], key => this.getRuleName(key));
-
- return (
- <FacetItemsList>
- {rules.map(rule => (
- <FacetItem
- active={this.props.rules.includes(rule)}
- key={rule}
- loading={this.props.loading}
- name={this.getRuleName(rule)}
- onClick={this.handleItemClick}
- stat={formatFacetStat(this.getStat(rule))}
- tooltip={this.getRuleName(rule)}
- value={rule}
- />
- ))}
- </FacetItemsList>
- );
- }
-
- renderFooter() {
- if (!this.props.stats) {
- return null;
- }
+ return referencedRules[rule]
+ ? `(${referencedRules[rule].langName}) ${referencedRules[rule].name}`
+ : rule;
+ };
- return <FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />;
- }
+ renderSearchResult = (rule: Rule) => {
+ return `(${rule.langName}) ${rule.name}`;
+ };
render() {
- const { rules, stats = {} } = this.props;
- const values = rules.map(rule => this.getRuleName(rule));
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()}
- {this.renderFooter()}
- <MultipleSelectionHint options={Object.keys(stats).length} values={rules.length} />
- </>
- )}
- </FacetBox>
+ <ListStyleFacet
+ facetHeader={translate('issues.facet.rules')}
+ fetching={this.props.fetching}
+ getFacetItemText={this.getRuleName}
+ getSearchResultKey={result => result.key}
+ getSearchResultText={result => result.name}
+ onChange={this.props.onChange}
+ onSearch={this.handleSearch}
+ onToggle={this.props.onToggle}
+ open={this.props.open}
+ property="rules"
+ renderFacetItem={this.getRuleName}
+ renderSearchResult={this.renderSearchResult}
+ searchPlaceholder={translate('search.search_for_rules')}
+ stats={this.props.stats}
+ values={this.props.rules}
+ />
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
index 2953934dd4a..9b6c3217a94 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
@@ -39,6 +39,7 @@ import {
ReferencedComponent,
ReferencedUser,
ReferencedLanguage,
+ ReferencedRule,
STANDARDS
} from '../utils';
import { Component } from '../../../app/types';
@@ -57,7 +58,7 @@ export interface Props {
query: Query;
referencedComponents: { [componentKey: string]: ReferencedComponent };
referencedLanguages: { [languageKey: string]: ReferencedLanguage };
- referencedRules: { [ruleKey: string]: { name: string } };
+ referencedRules: { [ruleKey: string]: ReferencedRule };
referencedUsers: { [login: string]: ReferencedUser };
}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx
index c5582c3fc1b..e3cb518d74a 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx
@@ -25,7 +25,6 @@ import FacetHeader from '../../../components/facet/FacetHeader';
import { translate } from '../../../helpers/l10n';
import FacetItemsList from '../../../components/facet/FacetItemsList';
import FacetItem from '../../../components/facet/FacetItem';
-import Select from '../../../components/controls/Select';
import {
renderOwaspTop10Category,
renderSansTop25Category,
@@ -34,6 +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';
export interface Props {
cwe: string[];
@@ -55,6 +56,7 @@ export interface Props {
}
interface State {
+ cweQuery: string;
standards: Standards;
}
@@ -64,7 +66,10 @@ type ValuesProp = 'owaspTop10' | 'sansTop25' | 'cwe';
export default class StandardFacet extends React.PureComponent<Props, State> {
mounted = false;
property = STANDARDS;
- state: State = { standards: { owaspTop10: {}, sansTop25: {}, cwe: {} } };
+ state: State = {
+ cweQuery: '',
+ standards: { owaspTop10: {}, sansTop25: {}, cwe: {} }
+ };
componentDidMount() {
this.mounted = true;
@@ -165,6 +170,10 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
this.handleItemClick('cwe', value, true);
};
+ handleCWESearch = (query: string) => {
+ this.setState({ cweQuery: query });
+ };
+
renderList = (
statsProp: StatsProp,
valuesProp: ValuesProp,
@@ -173,13 +182,22 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
) => {
const stats = this.props[statsProp];
const values = this.props[valuesProp];
-
if (!stats) {
return null;
}
-
const categories = sortBy(Object.keys(stats), key => -stats[key]);
+ return this.renderFacetItemsList(stats, values, categories, renderName, renderName, onClick);
+ };
+ // eslint-disable-next-line max-params
+ renderFacetItemsList = (
+ stats: any,
+ values: string[],
+ categories: string[],
+ renderName: (standards: Standards, category: string) => React.ReactNode,
+ renderTooltip: (standards: Standards, category: string) => string,
+ onClick: (x: string, multiple?: boolean) => void
+ ) => {
if (!categories.length) {
return (
<div className="search-navigator-facet-empty little-spacer-top">
@@ -202,7 +220,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
name={renderName(this.state.standards, category)}
onClick={onClick}
stat={formatFacetStat(getStat(category))}
- tooltip={renderName(this.state.standards, category)}
+ tooltip={renderTooltip(this.state.standards, category)}
value={category}
/>
))}
@@ -230,26 +248,37 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
}
renderCWEList() {
- return this.renderList('cweStats', 'cwe', renderCWECategory, this.handleCWEItemClick);
+ 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() {
- const options = Object.keys(this.state.standards.cwe).map(cwe => ({
- label: renderCWECategory(this.state.standards, cwe),
- value: cwe
- }));
return (
- <div className="search-navigator-facet-footer">
- <Select
- className="input-super-large"
- clearable={false}
- noResultsText={translate('select2.noMatches')}
- onChange={this.handleCWESelect}
- options={options}
- placeholder={translate('search.search_for_cwe')}
- searchable={true}
- />
- </div>
+ <SearchBox
+ autoFocus={true}
+ className="little-spacer-top spacer-bottom"
+ onChange={this.handleCWESearch}
+ placeholder={translate('search.search_for_cwe')}
+ value={this.state.cweQuery}
+ />
);
}
@@ -317,8 +346,8 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
<DeferredSpinner loading={this.props.fetchingCwe} />
{this.props.cweOpen && (
<>
- {this.renderCWEList()}
{this.renderCWESearch()}
+ {this.renderCWEList()}
{this.renderCWEHint()}
</>
)}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx
index 9ebf4422d95..75d251a4dfe 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx
@@ -18,20 +18,14 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { sortBy, uniq, without } from 'lodash';
-import { formatFacetStat, Query } from '../utils';
+import { Query } from '../utils';
import { searchIssueTags } from '../../../api/issues';
import * as theme from '../../../app/theme';
import { Component } from '../../../app/types';
-import FacetBox from '../../../components/facet/FacetBox';
-import FacetFooter from '../../../components/facet/FacetFooter';
-import FacetHeader from '../../../components/facet/FacetHeader';
-import FacetItem from '../../../components/facet/FacetItem';
-import FacetItemsList from '../../../components/facet/FacetItemsList';
import TagsIcon from '../../../components/icons-components/TagsIcon';
import { translate } from '../../../helpers/l10n';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
-import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';
+import ListStyleFacet from '../../../components/facet/ListStyleFacet';
+import { highlightTerm } from '../../../helpers/search';
interface Props {
component: Component | undefined;
@@ -46,116 +40,54 @@ interface Props {
}
export default class TagFacet extends React.PureComponent<Props> {
- property = 'tags';
-
- static defaultProps = {
- open: true
- };
-
- handleItemClick = (itemValue: string, multiple: boolean) => {
- const { tags } = this.props;
- if (multiple) {
- const { tags } = this.props;
- const newValue = sortBy(
- tags.includes(itemValue) ? without(tags, itemValue) : [...tags, itemValue]
- );
- this.props.onChange({ [this.property]: newValue });
- } else {
- this.props.onChange({
- [this.property]: tags.includes(itemValue) && tags.length < 2 ? [] : [itemValue]
- });
- }
- };
-
- handleHeaderClick = () => {
- this.props.onToggle(this.property);
- };
-
- handleClear = () => {
- this.props.onChange({ [this.property]: [] });
- };
-
handleSearch = (query: string) => {
- return searchIssueTags({ organization: this.props.organization, ps: 50, q: query }).then(tags =>
- tags.map(tag => ({ label: tag, value: tag }))
+ return searchIssueTags({ organization: this.props.organization, ps: 50, q: query }).then(
+ tags => ({
+ paging: { pageIndex: 1, pageSize: tags.length, total: tags.length },
+ results: tags
+ })
);
};
- handleSelect = (option: { value: string }) => {
- const { tags } = this.props;
- this.props.onChange({ [this.property]: uniq([...tags, option.value]) });
+ getTagName = (tag: string) => {
+ return tag;
};
- getStat(tag: string) {
- const { stats } = this.props;
- return stats ? stats[tag] : undefined;
- }
-
- renderTag(tag: string) {
+ renderTag = (tag: string) => {
return (
- <span>
+ <>
<TagsIcon className="little-spacer-right" fill={theme.gray60} />
{tag}
- </span>
+ </>
);
- }
-
- renderList() {
- const { stats } = this.props;
-
- if (!stats) {
- return null;
- }
-
- const tags = sortBy(Object.keys(stats), key => -stats[key]);
-
- return (
- <FacetItemsList>
- {tags.map(tag => (
- <FacetItem
- active={this.props.tags.includes(tag)}
- key={tag}
- loading={this.props.loading}
- name={this.renderTag(tag)}
- onClick={this.handleItemClick}
- stat={formatFacetStat(this.getStat(tag))}
- tooltip={tag}
- value={tag}
- />
- ))}
- </FacetItemsList>
- );
- }
+ };
- renderFooter() {
- if (!this.props.stats) {
- return null;
- }
-
- return <FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />;
- }
+ renderSearchResult = (tag: string, term: string) => (
+ <>
+ <TagsIcon className="little-spacer-right" fill={theme.gray60} />
+ {highlightTerm(tag, term)}
+ </>
+ );
render() {
- const { tags, 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.tags}
- />
-
- <DeferredSpinner loading={this.props.fetching} />
- {this.props.open && (
- <>
- {this.renderList()}
- {this.renderFooter()}
- <MultipleSelectionHint options={Object.keys(stats).length} values={tags.length} />
- </>
- )}
- </FacetBox>
+ <ListStyleFacet
+ facetHeader={translate('issues.facet.tags')}
+ fetching={this.props.fetching}
+ getFacetItemText={this.getTagName}
+ getSearchResultKey={tag => tag}
+ getSearchResultText={tag => tag}
+ onChange={this.props.onChange}
+ onSearch={this.handleSearch}
+ onToggle={this.props.onToggle}
+ open={this.props.open}
+ property="tags"
+ renderFacetItem={this.renderTag}
+ renderSearchResult={this.renderSearchResult}
+ searchPlaceholder={translate('search.search_for_tags')}
+ stats={this.props.stats}
+ values={this.props.tags}
+ />
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx
index 64217b4de0b..0d797101882 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx
@@ -89,12 +89,3 @@ it('should call onToggle', () => {
headerOnClick();
expect(onToggle).lastCalledWith('assignees');
});
-
-it('should handle footer callbacks', () => {
- const onChange = jest.fn();
- const wrapper = renderAssigneeFacet({ assignees: ['foo'], onChange });
- const onSelect = wrapper.find('FacetFooter').prop<Function>('onSelect');
-
- onSelect({ value: 'qux' });
- expect(onChange).lastCalledWith({ assigned: true, assignees: ['foo', 'qux'] });
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx
index 815d6b03f93..41d59a42a92 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx
@@ -138,13 +138,13 @@ it('should display correct selection', () => {
});
it('should search CWE', () => {
- const onChange = jest.fn();
- const wrapper = shallowRender({ onChange, open: true, cwe: ['42'], cweOpen: true });
+ const wrapper = shallowRender({ open: true, cwe: ['42'], cweOpen: true });
wrapper
.find('FacetBox[property="cwe"]')
- .find('Select')
- .prop<Function>('onChange')({ value: '111' });
- expect(onChange).toBeCalledWith({ cwe: ['111', '42'] });
+ .find('SearchBox')
+ .prop<Function>('onChange')('unkn');
+ wrapper.update();
+ expect(wrapper).toMatchSnapshot();
});
function shallowRender(props: Partial<Props> = {}) {
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap
index ed02501141f..1ab0e42e2a6 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap
@@ -16,6 +16,15 @@ exports[`should render 1`] = `
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}
@@ -75,11 +84,6 @@ exports[`should render 1`] = `
value="baz"
/>
</FacetItemsList>
- <FacetFooter
- onSearch={[Function]}
- onSelect={[Function]}
- renderOption={[Function]}
- />
<MultipleSelectionHint
options={4}
values={0}
@@ -144,6 +148,15 @@ exports[`should select unassigned 1`] = `
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}
@@ -203,11 +216,6 @@ exports[`should select unassigned 1`] = `
value="baz"
/>
</FacetItemsList>
- <FacetFooter
- onSearch={[Function]}
- onSelect={[Function]}
- renderOption={[Function]}
- />
<MultipleSelectionHint
options={4}
values={0}
@@ -236,6 +244,15 @@ exports[`should select user 1`] = `
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}
@@ -295,11 +312,6 @@ exports[`should select user 1`] = `
value="baz"
/>
</FacetItemsList>
- <FacetFooter
- onSearch={[Function]}
- onSelect={[Function]}
- renderOption={[Function]}
- />
<MultipleSelectionHint
options={4}
values={1}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap
index db112435946..bc2e66a808c 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap
@@ -7,7 +7,7 @@ Array [
"ResolutionFacet",
"StatusFacet",
"CreationDateFacet",
- "LanguageFacet",
+ "Connect(LanguageFacet)",
"RuleFacet",
"StandardFacet",
"TagFacet",
@@ -26,7 +26,7 @@ Array [
"ResolutionFacet",
"StatusFacet",
"CreationDateFacet",
- "LanguageFacet",
+ "Connect(LanguageFacet)",
"RuleFacet",
"StandardFacet",
"TagFacet",
@@ -43,7 +43,7 @@ Array [
"ResolutionFacet",
"StatusFacet",
"CreationDateFacet",
- "LanguageFacet",
+ "Connect(LanguageFacet)",
"RuleFacet",
"StandardFacet",
"TagFacet",
@@ -60,7 +60,7 @@ Array [
"ResolutionFacet",
"StatusFacet",
"CreationDateFacet",
- "LanguageFacet",
+ "Connect(LanguageFacet)",
"RuleFacet",
"StandardFacet",
"TagFacet",
@@ -79,7 +79,7 @@ Array [
"ResolutionFacet",
"StatusFacet",
"CreationDateFacet",
- "LanguageFacet",
+ "Connect(LanguageFacet)",
"RuleFacet",
"StandardFacet",
"TagFacet",
@@ -98,7 +98,7 @@ Array [
"ResolutionFacet",
"StatusFacet",
"CreationDateFacet",
- "LanguageFacet",
+ "Connect(LanguageFacet)",
"RuleFacet",
"StandardFacet",
"TagFacet",
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap
index 4d59f3a0b4d..939fb386fd6 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap
@@ -182,6 +182,13 @@ exports[`should render sub-facets 1`] = `
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}
@@ -208,32 +215,110 @@ exports[`should render sub-facets 1`] = `
value="173"
/>
</FacetItemsList>
- <div
- className="search-navigator-facet-footer"
- >
- <Select
- className="input-super-large"
- clearable={false}
- noResultsText="select2.noMatches"
- onChange={[Function]}
- options={
- Array [
- Object {
- "label": "CWE-42 - cwe-42 title",
- "value": "42",
- },
- Object {
- "label": "Unknown CWE",
- "value": "unknown",
- },
- ]
+ <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
+ 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="unkn"
+ />
+ <FacetItemsList>
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
+ key="unknown"
+ loading={false}
+ name={
+ <React.Fragment>
+ <mark>
+ Unkn
+ </mark>
+ own CWE
+ </React.Fragment>
}
- placeholder="search.search_for_cwe"
- searchable={true}
+ onClick={[Function]}
+ tooltip="Unknown CWE"
+ value="unknown"
/>
- </div>
+ </FacetItemsList>
<MultipleSelectionHint
- options={2}
+ options={0}
values={1}
/>
</React.Fragment>
diff --git a/server/sonar-web/src/main/js/apps/issues/utils.ts b/server/sonar-web/src/main/js/apps/issues/utils.ts
index fbbbf4dafae..e6449228325 100644
--- a/server/sonar-web/src/main/js/apps/issues/utils.ts
+++ b/server/sonar-web/src/main/js/apps/issues/utils.ts
@@ -19,7 +19,7 @@
*/
import { searchMembers } from '../../api/organizations';
import { searchUsers } from '../../api/users';
-import { Issue } from '../../app/types';
+import { Issue, Paging } from '../../app/types';
import { formatMeasure } from '../../helpers/measures';
import { get, save } from '../../helpers/storage';
import {
@@ -201,24 +201,28 @@ export interface ReferencedLanguage {
name: string;
}
-export const searchAssignees = (query: string, organization?: string) => {
+export interface ReferencedRule {
+ langName: string;
+ name: string;
+}
+
+export interface SearchedAssignee {
+ avatar?: string;
+ login: string;
+ name: string;
+}
+
+export const searchAssignees = (
+ query: string,
+ organization: string | undefined,
+ page = 1
+): Promise<{ paging: Paging; results: SearchedAssignee[] }> => {
return organization
- ? searchMembers({ organization, ps: 50, q: query }).then(response =>
- response.users.map(user => ({
- avatar: user.avatar,
- label: user.name,
- value: user.login
- }))
- )
- : searchUsers({ q: query }).then(response =>
- response.users.map(user => ({
- // TODO this WS returns no avatar
- avatar: user.avatar,
- email: user.email,
- label: user.name,
- value: user.login
- }))
- );
+ ? searchMembers({ organization, p: page, ps: 50, q: query }).then(({ paging, users }) => ({
+ paging,
+ results: users
+ }))
+ : searchUsers({ p: page, q: query }).then(({ paging, users }) => ({ paging, results: users }));
};
const LOCALSTORAGE_MY = 'my';