]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14057 Highlight search query in results
authorJeremy Davis <jeremy.davis@sonarsource.com>
Tue, 24 Nov 2020 17:03:05 +0000 (18:03 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 25 Nov 2020 20:06:27 +0000 (20:06 +0000)
server/sonar-web/src/main/js/app/styles/init/type.css
server/sonar-web/src/main/js/apps/create/project/AzureProjectAccordion.tsx
server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx
server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectAccordion-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectAccordion-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/style.css
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index b04036497ff95f8e1c1c53946f94369fa1cd14c8..f0da00a85dd017a81b42cbdd7a0ae9a000d93555 100644 (file)
@@ -140,6 +140,10 @@ strong {
   font-weight: 600;
 }
 
+.underline {
+  text-decoration: underline;
+}
+
 mark {
   background: none;
   color: var(--baseFontColor);
index b74344df1377bbe1136e84577872f286fb113238..ddac1e389478bf011fe7cd426a58ffeb9e321f4e 100644 (file)
@@ -40,14 +40,40 @@ export interface AzureProjectAccordionProps {
   onSelectRepository: (repository: AzureRepository) => void;
   project: AzureProject;
   repositories?: AzureRepository[];
+  searchQuery?: string;
   selectedRepository?: AzureRepository;
   startsOpen: boolean;
 }
 
 const PAGE_SIZE = 30;
 
+function highlight(text: string, term?: string, underline = false) {
+  if (!term || !text.toLowerCase().includes(term.toLowerCase())) {
+    return text;
+  }
+
+  // Capture only the first occurence by using a capturing group to get
+  // everything after the first occurence
+  const [pre, found, post] = text.split(new RegExp(`(${term})(.*)`, 'i'));
+  return (
+    <>
+      {pre}
+      <strong className={classNames({ underline })}>{found}</strong>
+      {post}
+    </>
+  );
+}
+
 export default function AzureProjectAccordion(props: AzureProjectAccordionProps) {
-  const { importing, loading, startsOpen, project, repositories = [], selectedRepository } = props;
+  const {
+    importing,
+    loading,
+    startsOpen,
+    project,
+    repositories = [],
+    searchQuery,
+    selectedRepository
+  } = props;
 
   const [open, setOpen] = React.useState(startsOpen);
   const handleClick = () => {
@@ -70,7 +96,7 @@ export default function AzureProjectAccordion(props: AzureProjectAccordionProps)
       })}
       onClick={handleClick}
       open={open}
-      title={<h3 title={project.description}>{project.name}</h3>}>
+      title={<h3 title={project.description}>{highlight(project.name, searchQuery, true)}</h3>}>
       {open && (
         <DeferredSpinner loading={loading}>
           {/* The extra loading guard is to prevent the flash of the Alert */}
@@ -97,18 +123,16 @@ export default function AzureProjectAccordion(props: AzureProjectAccordionProps)
               <div className="display-flex-wrap">
                 {limitedRepositories.map(repo => (
                   <div
-                    className="display-flex-start spacer-right spacer-bottom create-project-azdo-repo"
+                    className="create-project-azdo-repo display-flex-start spacer-bottom padded-right"
                     key={repo.name}>
                     {repo.sqProjectKey ? (
                       <>
                         <CheckIcon className="spacer-right" fill={colors.green} size={14} />
                         <div className="overflow-hidden">
                           <div className="little-spacer-bottom text-ellipsis">
-                            <strong title={repo.sqProjectName}>
-                              <Link to={getProjectUrl(repo.sqProjectKey)}>
-                                {repo.sqProjectName}
-                              </Link>
-                            </strong>
+                            <Link to={getProjectUrl(repo.sqProjectKey)} title={repo.sqProjectName}>
+                              {highlight(repo.sqProjectName || repo.name, searchQuery)}
+                            </Link>
                           </div>
                           <em>{translate('onboarding.create_project.repository_imported')}</em>
                         </div>
@@ -120,9 +144,9 @@ export default function AzureProjectAccordion(props: AzureProjectAccordionProps)
                         disabled={importing}
                         onCheck={() => props.onSelectRepository(repo)}
                         value={repo.name}>
-                        <strong className="text-ellipsis" title={repo.name}>
-                          {repo.name}
-                        </strong>
+                        <span className="text-ellipsis" title={repo.name}>
+                          {highlight(repo.name, searchQuery)}
+                        </span>
                       </Radio>
                     )}
                   </div>
index 88bc3f494e0aaa27c51a05d1f5f7f1a27e76764d..84e232efb07a851d99432f8ae88614b39a3e16d7 100644 (file)
@@ -48,6 +48,7 @@ interface State {
   repositories: T.Dict<AzureRepository[]>;
   searching?: boolean;
   searchResults?: T.Dict<AzureRepository[]>;
+  searchQuery?: string;
   selectedRepository?: AzureRepository;
   settings?: AlmSettingsInstance;
   submittingToken?: boolean;
@@ -184,7 +185,7 @@ export default class AzureProjectCreate extends React.PureComponent<Props, State
     }
 
     if (searchQuery.length === 0) {
-      this.setState({ searchResults: undefined });
+      this.setState({ searchResults: undefined, searchQuery: undefined });
       return;
     }
 
@@ -195,7 +196,11 @@ export default class AzureProjectCreate extends React.PureComponent<Props, State
       .catch(() => []);
 
     if (this.mounted) {
-      this.setState({ searching: false, searchResults: groupBy(results, 'projectName') });
+      this.setState({
+        searching: false,
+        searchResults: groupBy(results, 'projectName'),
+        searchQuery
+      });
     }
   };
 
@@ -277,6 +282,7 @@ export default class AzureProjectCreate extends React.PureComponent<Props, State
       repositories,
       searching,
       searchResults,
+      searchQuery,
       selectedRepository,
       settings,
       submittingToken,
@@ -298,6 +304,7 @@ export default class AzureProjectCreate extends React.PureComponent<Props, State
         repositories={repositories}
         searching={searching}
         searchResults={searchResults}
+        searchQuery={searchQuery}
         selectedRepository={selectedRepository}
         settings={settings}
         showPersonalAccessTokenForm={!patIsValid || Boolean(location.query.resetPat)}
index 58b810243ed18a0baf769a6c00cf58cc641e3f87..f7ed43d943d0179a3f2fbfc2951c724b4c92e0d8 100644 (file)
@@ -44,6 +44,7 @@ export interface AzureProjectCreateRendererProps {
   repositories: T.Dict<AzureRepository[]>;
   searching?: boolean;
   searchResults?: T.Dict<AzureRepository[]>;
+  searchQuery?: string;
   selectedRepository?: AzureRepository;
   settings?: AlmSettingsInstance;
   showPersonalAccessTokenForm?: boolean;
@@ -61,6 +62,7 @@ export default function AzureProjectCreateRenderer(props: AzureProjectCreateRend
     repositories,
     searching,
     searchResults,
+    searchQuery,
     selectedRepository,
     settings,
     showPersonalAccessTokenForm,
@@ -99,7 +101,7 @@ export default function AzureProjectCreateRenderer(props: AzureProjectCreateRend
 
       {loading && <i className="spinner" />}
 
-      {!loading && !settings && (
+      {!loading && !(settings && settings.url) && (
         <WrongBindingCountAlert alm={AlmKeys.Azure} canAdmin={!!canAdmin} />
       )}
 
@@ -119,7 +121,7 @@ export default function AzureProjectCreateRenderer(props: AzureProjectCreateRend
             <div className="huge-spacer-bottom">
               <SearchBox
                 onChange={props.onSearch}
-                placeholder={translate('onboarding.create_project.search_repositories_by_name')}
+                placeholder={translate('onboarding.create_project.search_projects_repositories')}
               />
             </div>
             <DeferredSpinner loading={Boolean(searching)}>
@@ -131,6 +133,7 @@ export default function AzureProjectCreateRenderer(props: AzureProjectCreateRend
                 projects={projects}
                 repositories={repositories}
                 searchResults={searchResults}
+                searchQuery={searchQuery}
                 selectedRepository={selectedRepository}
               />
             </DeferredSpinner>
index b0cc921f4a6233bc8a949879a2ef3db116e5a043..89905bfc5dfc9308d596541827afc1814bff4438 100644 (file)
@@ -35,6 +35,7 @@ export interface AzureProjectsListProps {
   projects?: AzureProject[];
   repositories: T.Dict<AzureRepository[]>;
   searchResults?: T.Dict<AzureRepository[]>;
+  searchQuery?: string;
   selectedRepository?: AzureRepository;
 }
 
@@ -47,6 +48,7 @@ export default function AzureProjectsList(props: AzureProjectsListProps) {
     projects = [],
     repositories,
     searchResults,
+    searchQuery,
     selectedRepository
   } = props;
 
@@ -100,6 +102,7 @@ export default function AzureProjectsList(props: AzureProjectsListProps) {
           project={p}
           repositories={searchResults ? searchResults[p.name] : repositories[p.name]}
           selectedRepository={selectedRepository}
+          searchQuery={searchQuery}
           startsOpen={searchResults !== undefined || i === 0}
         />
       ))}
index 61588b9fbf61ffd4fb01386626dc0a2bf0860487..8ac44c5280c5a6e84a5d3232921133c0bf3ea19c 100644 (file)
@@ -40,6 +40,19 @@ it('should render correctly', () => {
   expect(shallowRender({ importing: true, repositories: [mockAzureRepository()] })).toMatchSnapshot(
     'importing'
   );
+  expect(
+    shallowRender({
+      repositories: [
+        mockAzureRepository({ name: 'this repo is the best' }),
+        mockAzureRepository({
+          name: 'This is a repo with class',
+          sqProjectKey: 'sq-key',
+          sqProjectName: 'SQ Name'
+        })
+      ],
+      searchQuery: 'repo'
+    })
+  ).toMatchSnapshot('search results');
 });
 
 it('should open when clicked', () => {
index 73b6e225ad1c942c5d430c02d74b8fb325eb904a..6c945df40d3afa52369bc24e26cd63f555c20d01 100644 (file)
@@ -157,6 +157,7 @@ it('should handle searching for repositories', async () => {
   await waitAndUpdate(wrapper);
   expect(wrapper.state().searching).toBe(false);
   expect(wrapper.state().searchResults).toEqual({ [repositories[0].projectName]: repositories });
+  expect(wrapper.state().searchQuery).toBe(query);
 
   // Ignore opening a project when search results are displayed
   (getAzureRepositories as jest.Mock).mockClear();
@@ -169,6 +170,7 @@ it('should handle searching for repositories', async () => {
   wrapper.instance().handleSearchRepositories('');
   expect(searchAzureRepositories).not.toBeCalled();
   expect(wrapper.state().searchResults).toBeUndefined();
+  expect(wrapper.state().searchQuery).toBeUndefined();
 });
 
 it('should select and import a repository', async () => {
index 99d677cb7c7f4f3838f535864715654493ed69c7..75ce5ede02ccde70f98543d6816eeec9006ce66e 100644 (file)
@@ -35,7 +35,7 @@ exports[`should render correctly: importing 1`] = `
       className="display-flex-wrap"
     >
       <div
-        className="display-flex-start spacer-right spacer-bottom create-project-azdo-repo"
+        className="create-project-azdo-repo display-flex-start spacer-bottom padded-right"
         key="Azure repo 1"
       >
         <Radio
@@ -45,12 +45,12 @@ exports[`should render correctly: importing 1`] = `
           onCheck={[Function]}
           value="Azure repo 1"
         >
-          <strong
+          <span
             className="text-ellipsis"
             title="Azure repo 1"
           >
             Azure repo 1
-          </strong>
+          </span>
         </Radio>
       </div>
     </div>
@@ -91,6 +91,97 @@ exports[`should render correctly: loading 1`] = `
 </BoxedGroupAccordion>
 `;
 
+exports[`should render correctly: search results 1`] = `
+<BoxedGroupAccordion
+  className="big-spacer-bottom open"
+  onClick={[Function]}
+  open={true}
+  title={
+    <h3
+      title="Azure Project"
+    >
+      azure-project-1
+    </h3>
+  }
+>
+  <DeferredSpinner
+    loading={false}
+  >
+    <div
+      className="display-flex-wrap"
+    >
+      <div
+        className="create-project-azdo-repo display-flex-start spacer-bottom padded-right"
+        key="this repo is the best"
+      >
+        <Radio
+          checked={false}
+          className="overflow-hidden"
+          disabled={false}
+          onCheck={[Function]}
+          value="this repo is the best"
+        >
+          <span
+            className="text-ellipsis"
+            title="this repo is the best"
+          >
+            this 
+            <strong
+              className=""
+            >
+              repo
+            </strong>
+             is the best
+          </span>
+        </Radio>
+      </div>
+      <div
+        className="create-project-azdo-repo display-flex-start spacer-bottom padded-right"
+        key="This is a repo with class"
+      >
+        <CheckIcon
+          className="spacer-right"
+          fill="#00aa00"
+          size={14}
+        />
+        <div
+          className="overflow-hidden"
+        >
+          <div
+            className="little-spacer-bottom text-ellipsis"
+          >
+            <Link
+              onlyActiveOnIndex={false}
+              style={Object {}}
+              title="SQ Name"
+              to={
+                Object {
+                  "pathname": "/dashboard",
+                  "query": Object {
+                    "branch": undefined,
+                    "id": "sq-key",
+                  },
+                }
+              }
+            >
+              SQ Name
+            </Link>
+          </div>
+          <em>
+            onboarding.create_project.repository_imported
+          </em>
+        </div>
+      </div>
+    </div>
+    <ListFooter
+      count={2}
+      loadMore={[Function]}
+      total={2}
+    />
+  </DeferredSpinner>
+</BoxedGroupAccordion>
+`;
+
 exports[`should render correctly: with repositories 1`] = `
 <BoxedGroupAccordion
   className="big-spacer-bottom open"
@@ -111,7 +202,7 @@ exports[`should render correctly: with repositories 1`] = `
       className="display-flex-wrap"
     >
       <div
-        className="display-flex-start spacer-right spacer-bottom create-project-azdo-repo"
+        className="create-project-azdo-repo display-flex-start spacer-bottom padded-right"
         key="Azure repo 1"
       >
         <Radio
@@ -121,16 +212,16 @@ exports[`should render correctly: with repositories 1`] = `
           onCheck={[Function]}
           value="Azure repo 1"
         >
-          <strong
+          <span
             className="text-ellipsis"
             title="Azure repo 1"
           >
             Azure repo 1
-          </strong>
+          </span>
         </Radio>
       </div>
       <div
-        className="display-flex-start spacer-right spacer-bottom create-project-azdo-repo"
+        className="create-project-azdo-repo display-flex-start spacer-bottom padded-right"
         key="Azure repo 1"
       >
         <CheckIcon
@@ -144,25 +235,22 @@ exports[`should render correctly: with repositories 1`] = `
           <div
             className="little-spacer-bottom text-ellipsis"
           >
-            <strong
+            <Link
+              onlyActiveOnIndex={false}
+              style={Object {}}
               title="SQ Name"
-            >
-              <Link
-                onlyActiveOnIndex={false}
-                style={Object {}}
-                to={
-                  Object {
-                    "pathname": "/dashboard",
-                    "query": Object {
-                      "branch": undefined,
-                      "id": "sq-key",
-                    },
-                  }
+              to={
+                Object {
+                  "pathname": "/dashboard",
+                  "query": Object {
+                    "branch": undefined,
+                    "id": "sq-key",
+                  },
                 }
-              >
-                SQ Name
-              </Link>
-            </strong>
+              }
+            >
+              SQ Name
+            </Link>
           </div>
           <em>
             onboarding.create_project.repository_imported
index 68879175e51d2e78bd62ed7b213ec045b9ba41ad..ea0a2ef9a3f3abf38b27edde744876114b822f65 100644 (file)
@@ -115,12 +115,16 @@ exports[`should render correctly: project list 1`] = `
       </span>
     }
   />
+  <WrongBindingCountAlert
+    alm="azure"
+    canAdmin={true}
+  />
   <div
     className="huge-spacer-bottom"
   >
     <SearchBox
       onChange={[MockFunction]}
-      placeholder="onboarding.create_project.search_repositories_by_name"
+      placeholder="onboarding.create_project.search_projects_repositories"
     />
   </div>
   <DeferredSpinner
@@ -172,6 +176,10 @@ exports[`should render correctly: token form 1`] = `
       </span>
     }
   />
+  <WrongBindingCountAlert
+    alm="azure"
+    canAdmin={true}
+  />
   <div
     className="display-flex-justify-center"
   >
index d2e9420fcbd76db7f715dd1e14a013013f461113..832494dfa18ff18fe7ec3b7a554efe4e0a576efb 100644 (file)
 }
 
 .create-project-azdo-repo {
-  width: 250px;
+  width: 410px;
   min-height: 40px;
+  box-sizing: border-box;
+  margin-right: auto;
 }
 
 .create-project-import-bbs .open .boxed-group-header {
index a0717a0f97e5f3a89f54ccd420f16db6798ae9ab..65f0a4d9e6e017517afc91826600a7dd8e8ee5e1 100644 (file)
@@ -3239,6 +3239,7 @@ onboarding.create_project.display_name.help=Some scanners might override the val
 onboarding.create_project.repository_imported=Already set up
 onboarding.create_project.see_project=See the project
 onboarding.create_project.search_repositories_by_name=Search for repository name starting with...
+onboarding.create_project.search_projects_repositories=Search for projects and repositories
 onboarding.create_project.search_repositories=Search for a repository
 onboarding.create_project.select_repositories=Select repositories
 onboarding.create_project.select_all_repositories=Select all available repositories