diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2019-08-19 15:50:40 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-08-27 20:21:04 +0200 |
commit | 6e6c533c1299ec31a1c0923e259e135912220c17 (patch) | |
tree | 24178979acdd881d91e17f9feddef1663ef23517 | |
parent | de346d035e4f8bbafa6cc7edc43f8f865947038e (diff) | |
download | sonarqube-6e6c533c1299ec31a1c0923e259e135912220c17.tar.gz sonarqube-6e6c533c1299ec31a1c0923e259e135912220c17.zip |
SONAR-12403 Fix Issue filtering using File facet
5 files changed, 190 insertions, 32 deletions
diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index 66d352d7221..ea2f2381150 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -127,7 +127,7 @@ export function getComponent( } export interface TreeComponent extends T.LightComponent { - id: string; + id?: string; name: string; path?: string; refId?: string; @@ -136,21 +136,30 @@ export interface TreeComponent extends T.LightComponent { visibility: T.Visibility; } -export function getTree(data: { +export interface TreeComponentWithPath extends TreeComponent { + path: string; +} + +type GetTreeParams = { asc?: boolean; - branch?: string; component: string; p?: number; ps?: number; - pullRequest?: string; q?: string; - qualifiers?: string; s?: string; strategy?: 'all' | 'leaves' | 'children'; -}): Promise<{ baseComponent: TreeComponent; components: TreeComponent[]; paging: T.Paging }> { +} & T.BranchParameters; + +export function getTree<T = TreeComponent>( + data: GetTreeParams & { qualifiers?: string } +): Promise<{ baseComponent: TreeComponent; components: T[]; paging: T.Paging }> { return getJSON('/api/components/tree', data).catch(throwGlobalError); } +export function getFiles(data: GetTreeParams) { + return getTree<TreeComponentWithPath>({ ...data, qualifiers: 'FIL' }); +} + export function getComponentData(data: { component: string } & T.BranchParameters): Promise<any> { return getJSON('/api/components/show', data); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx index 6b3a11ba383..c2abf30cbce 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx @@ -23,56 +23,75 @@ import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { collapsePath } from 'sonar-ui-common/helpers/path'; import { highlightTerm } from 'sonar-ui-common/helpers/search'; -import { getTree, TreeComponent } from '../../../api/components'; +import { isDefined } from 'sonar-ui-common/helpers/types'; +import { getFiles, TreeComponentWithPath } from '../../../api/components'; import ListStyleFacet from '../../../components/facet/ListStyleFacet'; import { Facet, Query, ReferencedComponent } from '../utils'; interface Props { componentKey: string; fetching: boolean; - files: string[]; + fileUuids: string[]; loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>; onChange: (changes: Partial<Query>) => void; onToggle: (property: string) => void; open: boolean; query: Query; referencedComponents: T.Dict<ReferencedComponent>; - stats: T.Dict<number> | undefined; + stats: Facet | undefined; } export default class FileFacet extends React.PureComponent<Props> { - getFile = (file: string) => { + getFilePath = (fileUuid: string) => { const { referencedComponents } = this.props; - return referencedComponents[file] - ? collapsePath(referencedComponents[file].path || '', 15) - : file; + return referencedComponents[fileUuid] + ? collapsePath(referencedComponents[fileUuid].path || '', 15) + : fileUuid; }; - getFacetItemText = (file: string) => { + getReferencedComponent = (key: string) => { const { referencedComponents } = this.props; - return referencedComponents[file] ? referencedComponents[file].path || '' : file; + const fileUuid = Object.keys(referencedComponents).find(uuid => { + return referencedComponents[uuid].key === key; + }); + return fileUuid ? referencedComponents[fileUuid] : undefined; }; - getSearchResultKey = (file: TreeComponent) => { - return file.id; + getFacetItemText = (fileUuid: string) => { + const { referencedComponents } = this.props; + return referencedComponents[fileUuid] ? referencedComponents[fileUuid].path || '' : fileUuid; + }; + + getSearchResultKey = (file: TreeComponentWithPath) => { + const component = this.getReferencedComponent(file.key); + return component ? component.uuid : file.key; }; - getSearchResultText = (file: TreeComponent) => { - return file.path || file.name; + getSearchResultText = (file: TreeComponentWithPath) => { + return file.path; }; handleSearch = (query: string, page: number) => { - return getTree({ + return getFiles({ component: this.props.componentKey, q: query, - qualifiers: 'FIL', p: page, ps: 30 - }).then(({ components, paging }) => ({ paging, results: components })); + }).then(({ components, paging }) => ({ + paging, + results: components.filter(file => file.path !== undefined) + })); }; - loadSearchResultCount = (files: TreeComponent[]) => { - return this.props.loadSearchResultCount('files', { files: files.map(file => file.id) }); + loadSearchResultCount = (files: TreeComponentWithPath[]) => { + return this.props.loadSearchResultCount('files', { + files: files + .map(file => { + const component = this.getReferencedComponent(file.key); + return component && component.uuid; + }) + .filter(isDefined) + }); }; renderFile = (file: React.ReactNode) => ( @@ -82,18 +101,18 @@ export default class FileFacet extends React.PureComponent<Props> { </> ); - renderFacetItem = (file: string) => { - const name = this.getFile(file); + renderFacetItem = (fileUuid: string) => { + const name = this.getFilePath(fileUuid); return this.renderFile(name); }; - renderSearchResult = (file: TreeComponent, term: string) => { - return this.renderFile(highlightTerm(collapsePath(file.path || file.name, 15), term)); + renderSearchResult = (file: TreeComponentWithPath, term: string) => { + return this.renderFile(highlightTerm(collapsePath(file.path, 15), term)); }; render() { return ( - <ListStyleFacet<TreeComponent> + <ListStyleFacet<TreeComponentWithPath> facetHeader={translate('issues.facet.files')} fetching={this.props.fetching} getFacetItemText={this.getFacetItemText} @@ -111,7 +130,7 @@ export default class FileFacet extends React.PureComponent<Props> { renderSearchResult={this.renderSearchResult} searchPlaceholder={translate('search.search_for_files')} stats={this.props.stats} - values={this.props.files} + values={this.props.fileUuids} /> ); } 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 ba3e4c6c88c..cb882874c7d 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 @@ -36,7 +36,7 @@ import TypeFacet from './TypeFacet'; export interface Props { component: T.Component | undefined; - facets: T.Dict<Facet>; + facets: T.Dict<Facet | undefined>; hideAuthorFacet?: boolean; loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>; loadingFacets: T.Dict<boolean>; @@ -79,7 +79,7 @@ export default class Sidebar extends React.PureComponent<Props> { )} <FileFacet fetching={loadingFacets.files === true} - files={query.files} + fileUuids={query.files} open={!!openFacets.files} referencedComponents={this.props.referencedComponentsById} stats={facets.files} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/FileFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/FileFacet-test.tsx new file mode 100644 index 00000000000..51533428433 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/FileFacet-test.tsx @@ -0,0 +1,75 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { TreeComponentWithPath } from '../../../../api/components'; +import { Query, ReferencedComponent } from '../../utils'; +import FileFacet from '../FileFacet'; + +it('should render correctly', () => { + const wrapper = shallowRender(); + const instance = wrapper.instance(); + expect(wrapper).toMatchSnapshot(); + expect( + instance.renderSearchResult({ path: 'foo/bar.js' } as TreeComponentWithPath, 'foo') + ).toMatchSnapshot(); + expect(instance.renderFacetItem('fooUuid')).toMatchSnapshot(); +}); + +describe("ListStyleFacet's callback props", () => { + const wrapper = shallowRender(); + const instance = wrapper.instance(); + + test('#getSearchResultText()', () => { + expect(instance.getSearchResultText({ path: 'foo/bar.js' } as TreeComponentWithPath)).toBe( + 'foo/bar.js' + ); + }); + + test('#getSearchResultKey()', () => { + expect(instance.getSearchResultKey({ key: 'foo' } as TreeComponentWithPath)).toBe('fooUuid'); + expect(instance.getSearchResultKey({ key: 'bar' } as TreeComponentWithPath)).toBe('bar'); + }); + + test('#getFacetItemText()', () => { + expect(instance.getFacetItemText('fooUuid')).toBe('foo/bar.js'); + expect(instance.getFacetItemText('bar')).toBe('bar'); + }); +}); + +function shallowRender(props: Partial<FileFacet['props']> = {}) { + return shallow<FileFacet>( + <FileFacet + componentKey="foo" + fetching={false} + fileUuids={['foo', 'bar']} + loadSearchResultCount={jest.fn()} + onChange={jest.fn()} + onToggle={jest.fn()} + open={false} + query={{} as Query} + referencedComponents={{ + fooUuid: { key: 'foo', uuid: 'fooUuid', path: 'foo/bar.js' } as ReferencedComponent + }} + stats={undefined} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/FileFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/FileFacet-test.tsx.snap new file mode 100644 index 00000000000..e0c34a44074 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/FileFacet-test.tsx.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<ListStyleFacet + facetHeader="issues.facet.files" + fetching={false} + getFacetItemText={[Function]} + getSearchResultKey={[Function]} + getSearchResultText={[Function]} + loadSearchResultCount={[Function]} + maxInitialItems={15} + maxItems={100} + minSearchLength={3} + onChange={[MockFunction]} + onSearch={[Function]} + onToggle={[MockFunction]} + open={false} + property="files" + query={Object {}} + renderFacetItem={[Function]} + renderSearchResult={[Function]} + searchPlaceholder="search.search_for_files" + values={ + Array [ + "foo", + "bar", + ] + } +/> +`; + +exports[`should render correctly 2`] = ` +<React.Fragment> + <QualifierIcon + className="little-spacer-right" + qualifier="FIL" + /> + <React.Fragment> + <mark> + foo + </mark> + /bar.js + </React.Fragment> +</React.Fragment> +`; + +exports[`should render correctly 3`] = ` +<React.Fragment> + <QualifierIcon + className="little-spacer-right" + qualifier="FIL" + /> + foo/bar.js +</React.Fragment> +`; |