]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18328 SONAR-18332 [1099493] [1101757]
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Thu, 2 Feb 2023 10:33:05 +0000 (11:33 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 3 Feb 2023 14:26:01 +0000 (14:26 +0000)
* [1099493] Status message not automatically announced
* [1101757] Visual list is not marked up as list

server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts
server/sonar-web/src/main/js/apps/code/code.css
server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx
server/sonar-web/src/main/js/apps/code/components/Component.tsx
server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx
server/sonar-web/src/main/js/apps/code/components/Components.tsx
server/sonar-web/src/main/js/apps/code/components/Search.tsx
server/sonar-web/src/main/js/apps/code/components/SearchResults.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx
server/sonar-web/src/main/js/types/component.ts

index 22a367dc1b458037d906b4a3a05b612d29934fd5..46cda04a7f47bc716bfed26879ff405fe118b3c1 100644 (file)
@@ -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'),
index 4a74cd5c19bde8d69a10a402583b9051709d2136..8c9864aaf9611d25149e102169cd009031dd9dd4 100644 (file)
   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;
 }
index b7dbcdd5bc9119b5df93fad8e53b3105d4e14c46..646599998aa83ceeb1799f87b07ce07b1f38d16c 100644 (file)
@@ -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;
index 7393e3f7556c287b6974fb816f2bad0c430345a7..d42aa9fb47b6367e0a921b6d48e1b1245851a110 100644 (file)
@@ -44,7 +44,7 @@ interface Props {
   showAnalysisDate?: boolean;
 }
 
-export class Component extends React.PureComponent<Props> {
+class Component extends React.PureComponent<Props> {
   render() {
     const {
       branchLike,
index 0494af1de42e0a0542b625e21874891196938a12..da5c4be773885114dfcb77721fdb94e0b8202757 100644 (file)
@@ -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}
     >
index 3e0c96d8a0ef87020ee27ca2db33c7a49344c15f..159551a7e1e606b5273268b1cebd4c9f23c3a5a6 100644 (file)
@@ -39,7 +39,7 @@ interface ComponentsProps {
   showAnalysisDate?: boolean;
 }
 
-export function Components(props: ComponentsProps) {
+function Components(props: ComponentsProps) {
   const {
     baseComponent,
     branchLike,
index 0a95d1baece8672dfa8945f00e9500e58bc0d196..089de348926fbc3a600c6199032cfbe1ca5e5177 100644 (file)
@@ -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}
         />
diff --git a/server/sonar-web/src/main/js/apps/code/components/SearchResults.tsx b/server/sonar-web/src/main/js/apps/code/components/SearchResults.tsx
new file mode 100644 (file)
index 0000000..f45407b
--- /dev/null
@@ -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);
index 634327f86e34f31be04df0c20a23abf6e012fd0e..b3a343345e0de2e8e37616b95430c947e969ff60 100644 (file)
@@ -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;
index 640b555d9f41722e715195e6c9e5637771f9f81d..e0b98f13d3ef6f2154831c20bd0aaa67b17b80e6 100644 (file)
@@ -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 {