diff options
Diffstat (limited to 'server/sonar-web/src/main/js/apps/issues')
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'; |