From afa1e65cfbe2f17f39ba73e1fe2dd2a543c5bf9a Mon Sep 17 00:00:00 2001 From: Viktor Vorona Date: Thu, 15 Feb 2024 11:19:17 +0100 Subject: [PATCH] SONAR-20510 Always show project name in project facet --- .../js/api/mocks/ComponentsServiceMock.ts | 31 +++++ .../main/js/api/mocks/IssuesServiceMock.ts | 2 +- .../src/main/js/api/mocks/data/issues.ts | 2 +- .../__tests__/IssuesApp-Filtering-it.tsx | 7 +- .../js/apps/issues/sidebar/ProjectFacet.tsx | 114 ++++++++++++------ .../sonar-web/src/main/js/queries/projects.ts | 35 ++++++ 6 files changed, 148 insertions(+), 43 deletions(-) create mode 100644 server/sonar-web/src/main/js/queries/projects.ts diff --git a/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts index 6fb1144fcdd..8f0404dda49 100644 --- a/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts @@ -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 => { diff --git a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts index 833a508712d..11e3de98d6c 100644 --- a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts @@ -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, diff --git a/server/sonar-web/src/main/js/api/mocks/data/issues.ts b/server/sonar-web/src/main/js/api/mocks/data/issues.ts index 3efa63ae18d..9bdcd439460 100644 --- a/server/sonar-web/src/main/js/api/mocks/data/issues.ts +++ b/server/sonar-web/src/main/js/api/mocks/data/issues.ts @@ -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, diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx index 35501cdd50e..a00e7fe5450 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx @@ -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()); 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 0c47345603a..19371524f0d 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 @@ -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 { - handleSearch = ( +export function ProjectFacet(props: Readonly) { + 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 { })); }; - 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 ( - - - - {this.getProjectName(projectKey)} - + ); }; - renderSearchResult = (project: Pick, term: string) => ( + const renderSearchResult = (project: Pick, term: string) => ( <> @@ -121,27 +130,52 @@ export class ProjectFacet extends React.PureComponent { ); - render() { - return ( - - 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 ( + + 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 ( +
+ + + + + + {label} + +
+ ); } diff --git a/server/sonar-web/src/main/js/queries/projects.ts b/server/sonar-web/src/main/js/queries/projects.ts new file mode 100644 index 00000000000..9fe51a5ccd1 --- /dev/null +++ b/server/sonar-web/src/main/js/queries/projects.ts @@ -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>>( + key: string, + options?: Omit< + UseQueryOptions>, Error, T>, + 'queryKey' | 'queryFn' + >, +) { + return useQuery({ + queryKey: ['project', key], + queryFn: ({ queryKey: [, key] }) => searchProjects({ filter: `query=${key}` }), + ...options, + }); +} -- 2.39.5