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