1099493
] [1101757
]
* [tags/10.0.0.684321099493
] Status message not automatically announced * [1101757
] Visual list is not marked up as list
@@ -120,7 +120,7 @@ it('should behave correctly when using search', async () => { | |||
// Search with results that are deeper than the current level. | |||
await ui.searchForComponent('out'); | |||
expect(ui.childComponent(/out\.tsx/).get()).toBeInTheDocument(); | |||
expect(ui.searchResult(/out\.tsx/).get()).toBeInTheDocument(); | |||
// Search with no results. | |||
await ui.searchForComponent('nonexistent'); | |||
@@ -141,7 +141,7 @@ it('should behave correctly when using search', async () => { | |||
await act(async () => { | |||
await ui.arrowLeft(); | |||
}); | |||
expect(await ui.childComponent(/folderA/).find()).toBeInTheDocument(); | |||
expect(await ui.searchResult(/folderA/).find()).toBeInTheDocument(); | |||
}); | |||
it('should correcly handle long lists of components', async () => { | |||
@@ -370,6 +370,7 @@ function getPageObject(user: UserEvent) { | |||
const ui = { | |||
componentName: (name: string) => byText(name), | |||
childComponent: (name: string | RegExp) => byRole('cell', { name, exact: false }), | |||
searchResult: (name: string | RegExp) => byRole('link', { name, exact: false }), | |||
componentIsEmptyTxt: (qualifier: ComponentQualifier) => | |||
byText(`code_viewer.no_source_code_displayed_due_to_empty_analysis.${qualifier}`), | |||
searchInput: byRole('searchbox'), |
@@ -29,6 +29,18 @@ | |||
margin-top: -50px; | |||
} | |||
.code-components .boxed-group.search-results li { | |||
padding: var(--gridSize) calc(2 * var(--gridSize)); | |||
border-width: 0 0 0 2px; | |||
border-style: solid; | |||
border-color: transparent; | |||
} | |||
.code-components .boxed-group.search-results li.selected { | |||
background-color: var(--info100); | |||
border-left-color: var(--info500); | |||
} | |||
.code-components .table-wrapper { | |||
margin: 0 20px; | |||
} |
@@ -48,6 +48,7 @@ import { | |||
import Breadcrumbs from './Breadcrumbs'; | |||
import Components from './Components'; | |||
import Search from './Search'; | |||
import SearchResults from './SearchResults'; | |||
import SourceViewerWrapper from './SourceViewerWrapper'; | |||
interface Props { | |||
@@ -72,7 +73,7 @@ interface State { | |||
newCodeSelected: boolean; | |||
} | |||
export class CodeApp extends React.Component<Props, State> { | |||
class CodeApp extends React.Component<Props, State> { | |||
mounted = false; | |||
state: State; | |||
@@ -266,10 +267,9 @@ export class CodeApp extends React.Component<Props, State> { | |||
const hasComponents = components.length > 0 || searchResults !== undefined; | |||
const shouldShowBreadcrumbs = breadcrumbs.length > 1 && !showSearch; | |||
const showBreadcrumbs = breadcrumbs.length > 1 && !showSearch; | |||
const shouldShowComponentList = | |||
sourceViewer === undefined && components.length > 0 && !showSearch; | |||
const showComponentList = sourceViewer === undefined && components.length > 0 && !showSearch; | |||
const componentsClassName = classNames('boxed-group', 'spacer-top', { | |||
'new-loading': loading, | |||
@@ -333,7 +333,7 @@ export class CodeApp extends React.Component<Props, State> { | |||
</span> | |||
</div> | |||
)} | |||
{shouldShowBreadcrumbs && ( | |||
{showBreadcrumbs && ( | |||
<Breadcrumbs | |||
branchLike={branchLike} | |||
breadcrumbs={breadcrumbs} | |||
@@ -341,41 +341,43 @@ export class CodeApp extends React.Component<Props, State> { | |||
/> | |||
)} | |||
{shouldShowComponentList && ( | |||
<> | |||
<div className={componentsClassName}> | |||
<Components | |||
baseComponent={baseComponent} | |||
branchLike={branchLike} | |||
components={components} | |||
cycle={true} | |||
metrics={metrics} | |||
onEndOfList={this.handleLoadMore} | |||
onGoToParent={this.handleGoToParent} | |||
onHighlight={this.handleHighlight} | |||
onSelect={this.handleSelect} | |||
rootComponent={component} | |||
selected={highlighted} | |||
newCodeSelected={newCodeSelected} | |||
showAnalysisDate={isPortfolio} | |||
/> | |||
</div> | |||
<ListFooter count={components.length} loadMore={this.handleLoadMore} total={total} /> | |||
</> | |||
)} | |||
{showSearch && searchResults && ( | |||
<div className={componentsClassName}> | |||
<div className={componentsClassName}> | |||
{showComponentList && ( | |||
<Components | |||
baseComponent={baseComponent} | |||
branchLike={branchLike} | |||
components={components} | |||
cycle={true} | |||
metrics={metrics} | |||
onEndOfList={this.handleLoadMore} | |||
onGoToParent={this.handleGoToParent} | |||
onHighlight={this.handleHighlight} | |||
onSelect={this.handleSelect} | |||
rootComponent={component} | |||
selected={highlighted} | |||
newCodeSelected={newCodeSelected} | |||
showAnalysisDate={isPortfolio} | |||
/> | |||
)} | |||
{showSearch && ( | |||
<SearchResults | |||
branchLike={this.props.branchLike} | |||
components={searchResults} | |||
metrics={[]} | |||
onHighlight={this.handleHighlight} | |||
onSelect={this.handleSelect} | |||
rootComponent={component} | |||
selected={highlighted} | |||
/> | |||
)} | |||
<div role="status" className={showSearch ? 'text-center big-padded-bottom' : undefined}> | |||
{searchResults?.length === 0 && translate('no_results')} | |||
</div> | |||
</div> | |||
{showComponentList && ( | |||
<ListFooter count={components.length} loadMore={this.handleLoadMore} total={total} /> | |||
)} | |||
{sourceViewer !== undefined && !showSearch && ( | |||
@@ -396,6 +398,7 @@ export class CodeApp extends React.Component<Props, State> { | |||
); | |||
} | |||
} | |||
const StyledAlert = styled(Alert)` | |||
display: inline-flex; | |||
margin-bottom: 15px; |
@@ -44,7 +44,7 @@ interface Props { | |||
showAnalysisDate?: boolean; | |||
} | |||
export class Component extends React.PureComponent<Props> { | |||
class Component extends React.PureComponent<Props> { | |||
render() { | |||
const { | |||
branchLike, |
@@ -100,7 +100,7 @@ export default function ComponentName({ | |||
} | |||
return ( | |||
<span | |||
className="max-width-100 display-inline-block text-ellipsis" | |||
className="max-width-100 text-ellipsis" | |||
title={getTooltip(component)} | |||
aria-label={ariaLabel} | |||
> |
@@ -39,7 +39,7 @@ interface ComponentsProps { | |||
showAnalysisDate?: boolean; | |||
} | |||
export function Components(props: ComponentsProps) { | |||
function Components(props: ComponentsProps) { | |||
const { | |||
baseComponent, | |||
branchLike, |
@@ -28,7 +28,7 @@ import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { KeyboardKeys } from '../../../helpers/keycodes'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { ComponentQualifier, isView } from '../../../types/component'; | |||
import { ComponentMeasure } from '../../../types/types'; | |||
interface Props { | |||
@@ -47,7 +47,7 @@ interface State { | |||
loading: boolean; | |||
} | |||
export class Search extends React.PureComponent<Props, State> { | |||
class Search extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { | |||
query: '', | |||
@@ -99,11 +99,7 @@ export class Search extends React.PureComponent<Props, State> { | |||
}); | |||
} | |||
const qualifiers = [ | |||
ComponentQualifier.Portfolio, | |||
ComponentQualifier.SubPortfolio, | |||
ComponentQualifier.Application, | |||
].includes(component.qualifier as ComponentQualifier) | |||
const qualifiers = isView(component.qualifier) | |||
? [ComponentQualifier.SubPortfolio, ComponentQualifier.Project].join(',') | |||
: [ComponentQualifier.TestFile, ComponentQualifier.File].join(','); | |||
@@ -144,11 +140,11 @@ export class Search extends React.PureComponent<Props, State> { | |||
render() { | |||
const { component, newCodeSelected } = this.props; | |||
const { loading, query } = this.state; | |||
const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier); | |||
const isViewLike = isView(component.qualifier); | |||
return ( | |||
<div className="code-search" id="code-search"> | |||
{isPortfolio && ( | |||
{isViewLike && ( | |||
<span className="big-spacer-right"> | |||
<ButtonToggle | |||
disabled={!isEmpty(query)} | |||
@@ -172,7 +168,7 @@ export class Search extends React.PureComponent<Props, State> { | |||
onChange={this.handleQueryChange} | |||
onKeyDown={this.handleKeyDown} | |||
placeholder={translate( | |||
isPortfolio ? 'code.search_placeholder.portfolio' : 'code.search_placeholder' | |||
isViewLike ? 'code.search_placeholder.portfolio' : 'code.search_placeholder' | |||
)} | |||
value={this.state.query} | |||
/> |
@@ -0,0 +1,67 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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 classNames from 'classnames'; | |||
import { sortBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import withKeyboardNavigation, { | |||
WithKeyboardNavigationProps, | |||
} from '../../../components/hoc/withKeyboardNavigation'; | |||
import { getComponentMeasureUniqueKey } from '../../../helpers/component'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { ComponentMeasure } from '../../../types/types'; | |||
import ComponentName from './ComponentName'; | |||
export interface SearchResultsProps extends WithKeyboardNavigationProps { | |||
branchLike?: BranchLike; | |||
rootComponent: ComponentMeasure; | |||
newCodeSelected?: boolean; | |||
} | |||
function SearchResults(props: SearchResultsProps) { | |||
const { branchLike, components, newCodeSelected, rootComponent, selected } = props; | |||
return ( | |||
<ul> | |||
{components && | |||
components.length > 0 && | |||
sortBy( | |||
components, | |||
(c) => c.qualifier, | |||
(c) => c.name.toLowerCase(), | |||
(c) => (c.branch ? c.branch.toLowerCase() : '') | |||
).map((component) => ( | |||
<li | |||
className={classNames({ selected: selected?.key === component.key })} | |||
key={getComponentMeasureUniqueKey(component)} | |||
> | |||
<ComponentName | |||
branchLike={branchLike} | |||
canBrowse={true} | |||
component={component} | |||
rootComponent={rootComponent} | |||
newCodeSelected={newCodeSelected} | |||
/> | |||
</li> | |||
))} | |||
</ul> | |||
); | |||
} | |||
export default withKeyboardNavigation(SearchResults); |
@@ -32,7 +32,7 @@ export interface SourceViewerWrapperProps { | |||
onIssueChange?: (issue: Issue) => void; | |||
} | |||
export function SourceViewerWrapper(props: SourceViewerWrapperProps) { | |||
function SourceViewerWrapper(props: SourceViewerWrapperProps) { | |||
const { branchLike, component, componentMeasures, location } = props; | |||
const { line } = location.query; | |||
const finalLine = line ? Number(line) : undefined; |
@@ -62,12 +62,9 @@ export interface TreeComponentWithPath extends TreeComponent { | |||
export function isPortfolioLike( | |||
componentQualifier?: string | ComponentQualifier | |||
): componentQualifier is ComponentQualifier.Portfolio | ComponentQualifier.SubPortfolio { | |||
return Boolean( | |||
componentQualifier && | |||
[ | |||
ComponentQualifier.Portfolio.toString(), | |||
ComponentQualifier.SubPortfolio.toString(), | |||
].includes(componentQualifier) | |||
return ( | |||
componentQualifier === ComponentQualifier.Portfolio || | |||
componentQualifier === ComponentQualifier.SubPortfolio | |||
); | |||
} | |||
@@ -83,18 +80,21 @@ export function isProject( | |||
return componentQualifier === ComponentQualifier.Project; | |||
} | |||
export function isFile(componentQualifier?: string | ComponentQualifier): boolean { | |||
export function isFile( | |||
componentQualifier?: string | ComponentQualifier | |||
): componentQualifier is ComponentQualifier.File { | |||
return [ComponentQualifier.File, ComponentQualifier.TestFile].includes( | |||
componentQualifier as ComponentQualifier | |||
); | |||
} | |||
export function isView(componentQualifier?: string | ComponentQualifier): boolean { | |||
return [ | |||
ComponentQualifier.Portfolio, | |||
ComponentQualifier.SubPortfolio, | |||
ComponentQualifier.Application, | |||
].includes(componentQualifier as ComponentQualifier); | |||
export function isView( | |||
componentQualifier?: string | ComponentQualifier | |||
): componentQualifier is | |||
| ComponentQualifier.Application | |||
| ComponentQualifier.Portfolio | |||
| ComponentQualifier.SubPortfolio { | |||
return isPortfolioLike(componentQualifier) || isApplication(componentQualifier); | |||
} | |||
export interface ComponentContextShape { |