Browse Source

SONAR-20510 Always show project name in project facet

tags/10.5.0.89998
Viktor Vorona 2 months ago
parent
commit
afa1e65cfb

+ 31
- 0
server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts View File

@@ -46,6 +46,7 @@ import {
getDuplications,
getSources,
getTree,
searchProjects,
setApplicationTags,
setProjectTags,
} from '../components';
@@ -57,6 +58,7 @@ import {
} from './data/components';
import { mockIssuesList } from './data/issues';
import { MeasureRecords, mockFullMeasureData } from './data/measures';
import { mockProjects } from './data/projects';
import { listAllComponent, listChildComponent, listLeavesComponent } from './data/utils';

jest.mock('../components');
@@ -64,11 +66,13 @@ jest.mock('../components');
export default class ComponentsServiceMock {
failLoadingComponentStatus: HttpStatus | undefined = undefined;
defaultComponents: ComponentTree[];
defaultProjects: ComponentRaw[];
components: ComponentTree[];
defaultSourceFiles: SourceFile[];
sourceFiles: SourceFile[];
defaultMeasures: MeasureRecords;
measures: MeasureRecords;
projects: ComponentRaw[];

constructor(components?: ComponentTree[], sourceFiles?: SourceFile[], measures?: MeasureRecords) {
this.defaultComponents = components || [mockFullComponentTree()];
@@ -80,10 +84,12 @@ export default class ComponentsServiceMock {
(acc, tree) => ({ ...acc, ...mockFullMeasureData(tree, issueList) }),
{},
);
this.defaultProjects = mockProjects();

this.components = cloneDeep(this.defaultComponents);
this.sourceFiles = cloneDeep(this.defaultSourceFiles);
this.measures = cloneDeep(this.defaultMeasures);
this.projects = cloneDeep(this.defaultProjects);

jest.mocked(getComponentTree).mockImplementation(this.handleGetComponentTree);
jest.mocked(getChildren).mockImplementation(this.handleGetChildren);
@@ -99,8 +105,33 @@ export default class ComponentsServiceMock {
jest.mocked(getBreadcrumbs).mockImplementation(this.handleGetBreadcrumbs);
jest.mocked(setProjectTags).mockImplementation(this.handleSetProjectTags);
jest.mocked(setApplicationTags).mockImplementation(this.handleSetApplicationTags);
jest.mocked(searchProjects).mockImplementation(this.handleSearchProjects);
}

handleSearchProjects: typeof searchProjects = (data) => {
const pageIndex = data.p ?? 1;
const pageSize = data.ps ?? 100;

const components = this.projects
.filter((c) => {
if (data.filter && data.filter.startsWith('query')) {
const query = data.filter.split('query=')[1];
return c.key.includes(query) || c.name.includes(query);
}
})
.map((c) => c);

return this.reply({
components: components.slice((pageIndex - 1) * pageSize, pageIndex * pageSize),
facets: [],
paging: {
pageSize,
pageIndex,
total: components.length,
},
});
};

findComponentTree = (key: string, from?: ComponentTree) => {
let tree: ComponentTree | undefined;
const recurse = (node: ComponentTree): boolean => {

+ 1
- 1
server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts View File

@@ -325,7 +325,7 @@ export default class IssuesServiceMock {
issueStatuses: ISSUE_STATUSES,
types: ISSUE_TYPES,
scopes: SOURCE_SCOPES.map(({ scope }) => scope),
projects: ['org.project1', 'org.project2'],
projects: ['org.project1', 'org.sonarsource.javascript:javascript'],
impactSoftwareQualities: Object.values(SoftwareQuality),
impactSeverities: Object.values(SoftwareImpactSeverity),
cleanCodeAttributeCategories: cleanCodeCategories,

+ 1
- 1
server/sonar-web/src/main/js/api/mocks/data/issues.ts View File

@@ -385,7 +385,7 @@ export function mockIssuesList(baseComponentKey = PARENT_COMPONENT_KEY): IssueDa
quickFixAvailable: true,
tags: ['unused'],
codeVariants: ['variant 1', 'variant 2'],
project: 'org.project2',
project: 'org.sonarsource.javascript:javascript',
assignee: 'email1@sonarsource.com',
author: 'email3@sonarsource.com',
issueStatus: IssueStatus.Confirmed,

+ 6
- 1
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx View File

@@ -115,7 +115,12 @@ describe('issues app filtering', () => {

// Project
await user.click(ui.projectFacet.get());
await user.click(screen.getByRole('checkbox', { name: 'org.project2' }));
expect(
screen.getByRole('checkbox', { name: 'org.sonarsource.javascript:javascript' }),
).toHaveTextContent('SonarJS');
await user.click(
screen.getByRole('checkbox', { name: 'org.sonarsource.javascript:javascript' }),
);

// Assignee
await user.click(ui.assigneeFacet.get());

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

@@ -17,12 +17,13 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { ProjectIcon } from 'design-system';
import { ProjectIcon, Spinner } from 'design-system';
import { omit } from 'lodash';
import * as React from 'react';
import { getTree, searchProjects } from '../../../api/components';
import { translate } from '../../../helpers/l10n';
import { highlightTerm } from '../../../helpers/search';
import { useProjectQuery } from '../../../queries/projects';
import { ComponentQualifier } from '../../../types/component';
import { Facet, ReferencedComponent } from '../../../types/issues';
import { MetricKey } from '../../../types/metrics';
@@ -48,13 +49,23 @@ interface SearchedProject {
name: string;
}

export class ProjectFacet extends React.PureComponent<Props> {
handleSearch = (
export function ProjectFacet(props: Readonly<Props>) {
const {
component,
fetching,
onChange,
onToggle,
open,
projects,
query,
referencedComponents,
stats,
} = props;

const handleSearch = (
query: string,
page = 1,
): Promise<{ results: SearchedProject[]; paging: Paging }> => {
const { component } = this.props;

if (
component &&
[
@@ -91,29 +102,27 @@ export class ProjectFacet extends React.PureComponent<Props> {
}));
};

getProjectName = (project: string) => {
const { referencedComponents } = this.props;

const getProjectName = (project: string) => {
return referencedComponents[project] ? referencedComponents[project].name : project;
};

loadSearchResultCount = (projects: SearchedProject[]) => {
return this.props.loadSearchResultCount(MetricKey.projects, {
const loadSearchResultCount = (projects: SearchedProject[]) => {
return props.loadSearchResultCount(MetricKey.projects, {
projects: projects.map((project) => project.key),
});
};

renderFacetItem = (projectKey: string) => {
const renderFacetItem = (projectKey: string) => {
const projectName = getProjectName(projectKey);
return (
<span>
<ProjectIcon className="sw-mr-1" />

{this.getProjectName(projectKey)}
</span>
<ProjectItem
projectKey={projectKey}
projectName={projectName === projectKey ? undefined : projectName}
/>
);
};

renderSearchResult = (project: Pick<SearchedProject, 'name'>, term: string) => (
const renderSearchResult = (project: Pick<SearchedProject, 'name'>, term: string) => (
<>
<ProjectIcon className="sw-mr-1" />

@@ -121,27 +130,52 @@ export class ProjectFacet extends React.PureComponent<Props> {
</>
);

render() {
return (
<ListStyleFacet<SearchedProject>
facetHeader={translate('issues.facet.projects')}
fetching={this.props.fetching}
getFacetItemText={this.getProjectName}
getSearchResultKey={(project) => project.key}
getSearchResultText={(project) => project.name}
loadSearchResultCount={this.loadSearchResultCount}
onChange={this.props.onChange}
onSearch={this.handleSearch}
onToggle={this.props.onToggle}
open={this.props.open}
property={MetricKey.projects}
query={omit(this.props.query, MetricKey.projects)}
renderFacetItem={this.renderFacetItem}
renderSearchResult={this.renderSearchResult}
searchPlaceholder={translate('search.search_for_projects')}
stats={this.props.stats}
values={this.props.projects}
/>
);
}
return (
<ListStyleFacet<SearchedProject>
facetHeader={translate('issues.facet.projects')}
fetching={fetching}
getFacetItemText={getProjectName}
getSearchResultKey={(project) => project.key}
getSearchResultText={(project) => project.name}
loadSearchResultCount={loadSearchResultCount}
onChange={onChange}
onSearch={handleSearch}
onToggle={onToggle}
open={open}
property={MetricKey.projects}
query={omit(query, MetricKey.projects)}
renderFacetItem={renderFacetItem}
renderSearchResult={renderSearchResult}
searchPlaceholder={translate('search.search_for_projects')}
stats={stats}
values={projects}
/>
);
}

function ProjectItem({
projectKey,
projectName,
}: Readonly<{
projectKey: string;
projectName?: string;
}>) {
const { data, isLoading } = useProjectQuery(projectKey, {
enabled: projectName === undefined,
select: (data) => data.components.find((el) => el.key === projectKey),
});

const label = projectName ?? (isLoading ? '' : data?.name ?? projectKey);

return (
<div className="sw-flex sw-items-center">
<ProjectIcon className="sw-mr-1" />

<Spinner loading={projectName === undefined && isLoading} />

<span className="sw-min-w-0 sw-truncate" title={label}>
{label}
</span>
</div>
);
}

+ 35
- 0
server/sonar-web/src/main/js/queries/projects.ts View File

@@ -0,0 +1,35 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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 { UseQueryOptions, useQuery } from '@tanstack/react-query';
import { searchProjects } from '../api/components';

export function useProjectQuery<T = Awaited<ReturnType<typeof searchProjects>>>(
key: string,
options?: Omit<
UseQueryOptions<Awaited<ReturnType<typeof searchProjects>>, Error, T>,
'queryKey' | 'queryFn'
>,
) {
return useQuery({
queryKey: ['project', key],
queryFn: ({ queryKey: [, key] }) => searchProjects({ filter: `query=${key}` }),
...options,
});
}

Loading…
Cancel
Save